//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} - Room Icon ${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} - Room Icon ${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 = `
    Profile Picture - icon
    `; 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);