start working on friends system

This commit is contained in:
2026-07-22 17:15:05 +10:00
parent e99d312937
commit abd51e347c
12 changed files with 26028 additions and 8 deletions

240
client/script_friends.js Normal file
View File

@@ -0,0 +1,240 @@
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();
// 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);
return;
}
const usernameHeader = res.headers.get("X-Username");
usernames.set(id, usernameHeader);
return usernameHeader;
} catch (err) {
console.error("Error fetching /me:", err);
}
}
document.addEventListener('DOMContentLoaded', function() {
fetchSelfId();
if (typeof Notification !== "undefined") {
Notification.requestPermission();
}
// create a websocket
socket = new WebSocket("ws://" + 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 = 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) {
recievedMessages.set(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();
}
}
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);
});
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) {
fetchUsername(user);
}
}
for (const user of connectedFriends) {
const newElement = document.createElement("p");
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 */
function openUser(user) {
currentUser = user;
const messagesList = document.getElementById("friends-messages");
messagesList.innerHTML = "";
const messages = recievedMessages.get(user);
if (messages === undefined) {
return;
}
for (const message of messages) {
renderMessage(message);
}
}
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 = "";
}