//Global Variables
const profile = {
username: "Alice"
};
var Service = {
origin: location.origin,
getAllRooms: function(){
return new Promise((resolve, reject) => {
fetch( `${this.origin}/chat`)
.then( response => {
if(response.ok){
return response.json();
} else {
// Reject promise with client side error
return response.text().then(clientErrorMessage => {
throw new Error(clientErrorMessage);
});
}
})
.then(data => resolve(data))
// Reject promise with server side error
.catch(error => reject(error));
});
},
addRoom: function(data) {
return new Promise((resolve, reject) => {
fetch( `${this.origin}/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then( response => {
if(!response.ok){
return response.text().then(clientErrorMessage => {
throw new Error(clientErrorMessage);
});
}
return response.json();
})
.then(data => resolve(data))
.catch(error => reject(error));
});
},
getLastConversation: function(room_id, before){
return new Promise((resolve, reject) => {
fetch(`${this.origin}/chat/${room_id}/messages?before=${encodeURIComponent(before)}`)
.then( response => {
if(response.ok){
return response.json();
} else {
// Reject promise with client side error
return response.text().then(clientErrorMessage => {
throw new Error(clientErrorMessage);
});
}
})
.then(data => resolve(data))
// Reject promise with server side error
.catch(error => reject(error));
});
},
getProfile: function() {
return fetch('/profile')
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch profile');
}
return response.json();
})
.catch(error => {
console.error('Error fetching profile:', error);
});
}
};
// Removes the contents of the given DOM element (equivalent to elem.innerHTML = '' but faster)
function emptyDOM (elem){
while (elem.firstChild) elem.removeChild(elem.firstChild);
}
// Creates a DOM element from the given HTML string
function createDOM (htmlString){
let template = document.createElement('template');
template.innerHTML = htmlString.trim();
return template.content.firstChild;
}
class LobbyView {
constructor(lobby) {
this.lobby = lobby;
const lobbyHtml = `
`;
this.elem = createDOM(lobbyHtml);
this.listElem = this.elem.querySelector("ul.room-list");
this.inputElem = this.elem.querySelector("input");
this.buttonElem = this.elem.querySelector("button");
this.buttonElem.addEventListener('click', () => this.handleInputButton());
this.lobby.onNewRoom = (room) => {
const listItemStr = `
${room.name}
`
const listItem = createDOM(listItemStr);
this.listElem.appendChild(listItem);
};
this.redrawList();
}
handleInputButton() {
console.log(this.inputElem)
const roomName = this.inputElem.value.trim();
if(roomName){
Service.addRoom({name: roomName}).then(newRoom => {
this.lobby.addRoom(newRoom.id, newRoom.name, newRoom.image, []);
});
this.inputElem.value = '';
this.redrawList();
}
}
redrawList() {
emptyDOM(this.listElem);
for (const roomId in this.lobby.rooms) {
const room = this.lobby.rooms[roomId];
const listItemStr = `
${room.name}
`
const listItem = createDOM(listItemStr);
this.listElem.appendChild(listItem);
}
}
}
class ChatView {
constructor(socket) {
this.socket = socket;
const chatHtml = `
Everyone in CPEN322
Maddy
Hello Everyone
Leo
Hi
`;
this.elem = createDOM(chatHtml);
this.titleElem = this.elem.querySelector("h4");
this.chatElem = this.elem.querySelector("div.message-list");
this.chatElem.addEventListener('wheel', (event) => this.handleScroll(event));
this.inputElem = this.elem.querySelector("textarea");
this.inputElem.addEventListener('keyup', (event) => this.handleSendMessage(event));
this.buttonElem = this.elem.querySelector("button");
this.buttonElem.addEventListener('click', () => this.sendMessage());
this.room = null;
}
sendMessage(){
const textMessage = this.inputElem.value.trim();
this.room.addMessage(profile.username, textMessage);
// Construct message for the server
const sendMessage = JSON.stringify({
roomId: this.room.id,
username: profile.username,
text: textMessage
});
// Send message to server
this.socket.send(sendMessage);
this.inputElem.value = '';
}
handleSendMessage(event){
if(event.key == 'Enter' && !event.shiftKey){
event.preventDefault(); // Don't use enter for new line
this.sendMessage();
}
}
handleScroll(event){
if(this.room && this.room.canLoadConversation && event.deltaY < 0 && this.chatElem.scrollTop === 0){
this.room.getLastConversation.next();
}
}
setRoom(room){
this.room = room;
this.titleElem.textContent = room.name;
emptyDOM(this.chatElem);
// Add each message to the chat dynamically
for(let i = 0; i < this.room.messages.length; i++){
this.addMessageToChat(this.room.messages[i]);
}
this.room.onNewMessage = (message) => {
this.addMessageToChat(message);
};
this.room.onInappropriateMessage = (originalMessage, moderatedMessage) => {
this.removeMyInappropriateMessageFromChat(originalMessage);
this.addMessageToChat(moderatedMessage);
};
this.room.onSendMessageBan = (originalMessage, shouldRemoveLastMessage) => {
if (shouldRemoveLastMessage) {
this.removeMyInappropriateMessageFromChat(originalMessage);
}
this.disableMessaging();
}
this.room.onFetchConversation = (conversation) => {
const scrollHeightBefore = this.chatElem.scrollHeight;
for(let i = conversation.messages.length - 1; i >= 0 ; i--){
this.addMessageToChatOnFetch(conversation.messages[i]);
}
const scrollHeightAfter = this.chatElem.scrollHeight;
this.chatElem.scrollTop = scrollHeightAfter - scrollHeightBefore;
};
// Fake scroll event to load conversation
this.handleScroll({deltaY: -1});
}
createMessageDiv(message){
const messageDiv = document.createElement('div');
// Determine whose message it is
messageDiv.className = 'message' + (message.username === profile.username ? ' my-message' : '');
// Construct user span for messages
const messageUserSpan = document.createElement('span');
messageUserSpan.className = 'message-user';
messageUserSpan.textContent = message.username;
messageDiv.appendChild(messageUserSpan);
// Construct user text span for messages
const messageTextSpan = document.createElement('span');
messageTextSpan.className = 'message-text';
messageTextSpan.textContent = this.sanitizeText(message.text);
messageDiv.appendChild(messageTextSpan);
return messageDiv;
}
sanitizeText(text){
return text.replace(//g, '');
}
// Add message at to top of the chat
addMessageToChatOnFetch(message){
const messageDiv = this.createMessageDiv(message);
this.chatElem.prepend(messageDiv);
}
// Add message at the bottom of the chat
addMessageToChat(message){
const messageDiv = this.createMessageDiv(message);
this.chatElem.appendChild(messageDiv);
}
removeMyInappropriateMessageFromChat(originalMessage){
const allMessages = document.querySelectorAll('div.my-message');
const filteredDivs = Array.from(allMessages).filter(div => div.textContent.includes(originalMessage.text));
if(filteredDivs.length > 0){
this.chatElem.removeChild(filteredDivs[filteredDivs.length - 1]);
}
}
disableMessaging(){
this.inputElem.disabled = true;
this.buttonElem.disabled = true;
}
}
class ProfileView {
constructor() {
const profileHtml = `
`;
this.elem = createDOM(profileHtml);
}
}
class Room {
constructor(id, name, image='assets/everyone-icon.png', messages=[]) {
this.id = id;
this.name = name;
this.image = image;
this.messages = messages;
this.createdAt = Date.now();
this.canLoadConversation = true;
this.getLastConversation = makeConversationLoader(this);
}
updateInappropriateMessage(originalMessage, moderatedMessage){
const index = this.messages.findIndex(message => message.text === originalMessage.text && message.username === originalMessage.username);
if(index !== -1){
this.messages[index] = moderatedMessage;
if(typeof this.onInappropriateMessage === "function"){
this.onInappropriateMessage(originalMessage, moderatedMessage);
}
}
}
removeLastMessageAndDisableChat(message, shouldRemoveLastMessage){
if(typeof this.onSendMessageBan === "function"){
this.onSendMessageBan(message, shouldRemoveLastMessage);
}
}
addMessage(username, text){
if(!text.trim()){
return;
}
const message = {
username: username,
text: text
}
this.messages.push(message);
if (typeof this.onNewMessage === 'function') {
this.onNewMessage(message);
}
}
addConversation(conversation){
// Make sure conversation is not empty & has the array messages
if(!conversation || !Array.isArray(conversation.messages) || conversation.messages.length === 0 ){
return;
}
const orderedConversation = conversation.messages.concat(this.messages);
orderedConversation.sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp));
this.messages = orderedConversation;
if(typeof this.onFetchConversation === "function"){
this.onFetchConversation(conversation);
}
}
}
class Lobby {
constructor() {
this.rooms = {}
}
getRoom(roomId) {
return this.rooms[roomId];
}
addRoom(id, name, image, messages) {
const newRoom = new Room(id, name, image, messages);
this.rooms[id] = newRoom;
if (typeof this.onNewRoom === 'function') {
this.onNewRoom(newRoom);
}
}
}
function* makeConversationLoader(room){
var lastConversationTimestamp = room.createdAt;
let loadMoreData = true;
while(loadMoreData){
room.canLoadConversation = false;
if(!room.canLoadConversation){
try {
const before = lastConversationTimestamp;
const data = yield new Promise((resolve,reject) => {
Service.getLastConversation(room.id, before)
.then(data => {
if(data && data.messages && data.messages.length > 0 ){
lastConversationTimestamp = data.timestamp;
room.addConversation(data);
room.canLoadConversation = true;
resolve(data);
} else {
loadMoreData = false;
resolve(null);
}
})
.catch(error => {
room.canLoadConversation = false;
reject(error);
});
});
} catch(error) {
console.error("Error:", error);
}
}
}
}
function main () {
const lobby = new Lobby();
const lobbyView = new LobbyView(lobby);
const socket = new WebSocket("ws://localhost:8000");
const chatView = new ChatView(socket);
const profileView = new ProfileView();
socket.addEventListener('message', function(event) {
const messageData = JSON.parse(event.data);
const room = lobby.getRoom(messageData.roomId);
switch(messageData.type){
case 'chat':
const { username, text} = messageData;
room.addMessage(username, text);
break;
case 'inappropriate-alert':
alert(messageData.alert);
const {originalMessage, moderatedMessage} = messageData;
room.updateInappropriateMessage(originalMessage, moderatedMessage);
break;
case 'restricted-alert':
alert(messageData.alert);
const message = messageData.originalMessage;
const shouldRemoveLastMessage = messageData.shouldRemoveLastMessage;
room.removeLastMessageAndDisableChat(message, shouldRemoveLastMessage);
break;
}
});
Service.getProfile().then(profileData => {
if (profileData && profileData.username) {
profile.username = profileData.username;
}
}).catch(error => {
console.error("Error fetching profile:", error);
});
const refreshLobby = () => {
Service.getAllRooms().then(returnedRooms => {
returnedRooms.forEach(roomDetails => {
if(lobby.rooms[roomDetails._id]) {
// Room already exists so update information
lobby.rooms[roomDetails._id].name = roomDetails.name;
lobby.rooms[roomDetails._id].image = roomDetails.image;
} else {
// Create new room
lobby.addRoom(roomDetails._id,roomDetails.name, roomDetails.image, roomDetails.messages);
}
});
lobbyView.redrawList();
})
.catch(error => {
console.error("Error while refreshing lobby: ", error);
});
}
refreshLobby();
// Refresh the lobby every 5 seconds
setInterval(refreshLobby, 5000);
const renderRoute = () => {
const pageView = document.getElementById("page-view")
emptyDOM(pageView);
const hash = window.location.hash.substring(2); // Removing '#' from the hash
var pageContent;
if (hash === "") {
pageContent = lobbyView.elem;
} else if (hash.includes("chat")) {
const roomID = hash.split('/')[1];
const room = lobby.getRoom(roomID);
// Check that room exists
if(room){
chatView.setRoom(room);
pageContent = chatView.elem;
} else {
// If room doesn't exist just go to lobby
pageContent = lobbyView.elem;
}
} else if (hash.includes("profile")) {
pageContent = profileView.elem;
}
pageView.appendChild(pageContent);
}
window.addEventListener('popstate', renderRoute);
renderRoute()
cpen322.export(arguments.callee, { renderRoute, lobbyView, chatView, profileView, lobby, refreshLobby, socket });
}
window.addEventListener('load', main);