class Message { sender = 0; content = ""; constructor(sender, content) { this.sender = sender; this.content = content; } } // websocket connection /** @type {WebSocket} */ let socket; // array of userId's /** @type {Array} */ let connectedFriends = []; // map of userId's to arrays of Message objects /** @type {Map>} */ let recievedMessages = new Map(); // map of userId's to usernames /** @type {Map} */ let usernames = new Map(); // map of userId's to how many unread messages there are /** @type {Map} */ let unreads = new Map(); // userId of the current user who is being talked to let currentUser = 0; // userId of the logged-in user (this client). Fetched from /me on load. // /me responds with headers X-Logged-In, X-Username, X-Bio, X-User-ID // (no body), so we read the id back out of the response headers. let selfId = 0; // notification sound, loaded after ws initialization let sound; async function fetchSelfId() { try { const res = await fetch("/me"); if (!res.ok) { console.error("Failed to fetch /me:", res.status); return; } const loggedIn = res.headers.get("X-Logged-In"); if (loggedIn !== "true") { console.log("Not logged in"); return; } const idHeader = res.headers.get("X-User-ID"); selfId = parseInt(idHeader, 10); } catch (err) { console.error("Error fetching /me:", err); } } /** * @param {number} id * @returns {string} */ async function fetchUsername(id) { try { const res = await fetch("/userinfo/" + id); if (!res.ok) { console.error("Failed to fetch /userinfo/" + id + " :", res.status, await res.text()); return; } const usernameHeader = res.headers.get("X-Username"); usernames.set(id, usernameHeader); return usernameHeader; } catch (err) { console.error("Error fetching /me:", err); } } /** * * @param {number} id * @returns {Array} */ async function getMessageHistory(id) { try { const res = await fetch("/friends/chatHistory/" + id); if (!res.ok) { console.error("Failed to fetch /friends/chatHistory/" + id + " :", res.status, await res.text()); return []; } const json = await res.json(); // the server wraps the array in { "messages": [...] }, not a bare array return json.messages.map(m => new Message(m.sender, m.content)); } catch (err) { console.error("Error fetching /me:", err); return []; } } document.addEventListener('DOMContentLoaded', function() { fetchSelfId(); if (typeof Notification !== "undefined") { Notification.requestPermission(); } // create a websocket socket = new WebSocket("wss://" + document.location.hostname + ":" + document.location.port + "/friends/ws"); socket.onopen = function() { console.log("connected to server!"); setInterval(() => { // ping the server every 5 seconds if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 3, userId: 0 })); } }, 5000); }; socket.onmessage = async function(event) { console.log("recieved data", event.data); const data = JSON.parse(event.data); switch (data.type) { case "ok": { console.log("Server is ok"); break; } case "error": { console.error("Server sent an error:", data.content); break; } case "message": { console.log("message from", data.userId, ":", data.content); // update the messages variable if (recievedMessages.get(data.userId) === undefined) { // get previously recieved messages recievedMessages.set(data.userId, await getMessageHistory(data.userId)); } recievedMessages.get(data.userId).push(new Message(data.userId, data.content)); if (currentUser === data.userId) { updateMessages(); } // send a notification to the user if (document.hidden || currentUser != data.userId) { if (Notification !== undefined) { new Notification(usernames.get(data.userId), {body: data.content}); } if (audio !== undefined) { audio.play(); } const currentUnreads = unreads.get(data.userId); if (currentUnreads === undefined) { unreads.set(data.userId, 1); } else { unreads.set(data.userId, currentUnreads + 1); } updateOnlineUsers(); } break; } case "users": { console.log("updating user list"); connectedFriends = data.content; updateOnlineUsers(); break; } } } socket.onerror = function(error) { console.log(error); alert("There was an error with the socket! Check console logs for details. Refresh the page to reconnect"); socket.close(); } socket.onclose = function(event) { console.log("disconnected from server"); } audio = new Audio(); audio.preload = "auto"; audio.src = "/notify.opus"; document.body.appendChild(audio); }); async function updateOnlineUsers() { const friendsList = document.getElementById("friends-list-links"); friendsList.innerHTML = ""; // Get everyone's username for (const user of connectedFriends) { if (usernames.get(user) === undefined) { await fetchUsername(user); } } for (const user of connectedFriends) { const newElement = document.createElement("p"); const userUnreads = unreads.get(user); if (userUnreads !== undefined) { newElement.textContent = "[" + userUnreads + "] "; } else { newElement.textContent = ""; } newElement.textContent += usernames.get(user); newElement.onclick = function() { openUser(user); } friendsList.appendChild(newElement); } } /** @param {Message} message */ function renderMessage(message) { const messagesList = document.getElementById("friends-messages"); const newElement = document.createElement("p"); const who = message.sender === selfId ? "You" : usernames.get(message.sender); newElement.textContent = who + ": " + message.content; messagesList.appendChild(newElement); messagesList.scrollTop = messagesList.scrollHeight; } function updateMessages() { const messages = recievedMessages.get(currentUser); if (messages === undefined || messages.length === 0) { return; } const message = messages[messages.length - 1]; renderMessage(message); } /** @param {number} user */ async function openUser(user) { currentUser = user; const messagesList = document.getElementById("friends-messages"); messagesList.innerHTML = ""; if (recievedMessages.get(user) === undefined) { recievedMessages.set(user, await getMessageHistory(user)); } // another openUser() call may have run while we were awaiting, and // switched the user again - bail out if we're no longer viewing this one if (currentUser !== user) { return; } const messages = recievedMessages.get(user); for (const message of messages) { renderMessage(message); } unreads.delete(user); updateOnlineUsers(); } function sendMessage() { if (currentUser === 0) { alert("Select a friend to message first"); return; } const textarea = document.getElementById("friends-textarea"); const content = textarea.value.trim(); if (content === "") { return; } if (socket.readyState !== WebSocket.OPEN) { alert("Not connected to the server. Refresh the page and try again."); return; } socket.send(JSON.stringify({ type: 0, userId: currentUser, content: content })); // optimistically show the message locally, since the server only // relays messages between the two other parties, not back to the sender if (recievedMessages.get(currentUser) === undefined) { recievedMessages.set(currentUser, []); } const sentMessage = new Message(selfId, content); recievedMessages.get(currentUser).push(sentMessage); renderMessage(sentMessage); textarea.value = ""; }