290 lines
8.7 KiB
JavaScript
290 lines
8.7 KiB
JavaScript
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<number>} */
|
|
let connectedFriends = [];
|
|
|
|
// map of userId's to arrays of Message objects
|
|
/** @type {Map<number, Array<Message>>} */
|
|
let recievedMessages = new Map();
|
|
|
|
// map of userId's to usernames
|
|
/** @type {Map<number, string>} */
|
|
let usernames = new Map();
|
|
|
|
// map of userId's to how many unread messages there are
|
|
/** @type {Map<number, number>} */
|
|
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<Message>}
|
|
*/
|
|
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 = "";
|
|
}
|