Files
coms/client/public/wsModule.js
2026-03-28 12:48:48 -04:00

233 lines
7.0 KiB
JavaScript

import {
addMessage,
addNotice,
getReplyTo,
addUser,
removeUser,
updateUser,
getGroupedUsers,
setMyId,
getMyId,
getThreads,
clearData,
users,
setFocused,
setReplyTo,
} from "./data.js";
import { renderChat } from "./render.js";
import { notifyDirectReply } from "./notifications.js";
/**
* Initialize WebSocket connection and wire up event handlers.
*
* @param {string} username - The user's chosen name.
* @param {HTMLElement} chatEl - The container element for chat entries.
* @param {HTMLElement} inputContainer - The message input area element.
* @param {HTMLElement} usersListEl - The element displaying the online users.
* @param {string} wsDomain - Host (and optional port) for the WebSocket server.
* @returns {{ socket: WebSocket, sendMessage: (content: string) => void }}
*/
export function initWebSocket(
username,
chatEl,
inputContainer,
usersListEl,
wsDomain,
) {
const socket = new WebSocket(
`${location.protocol === "https:" ? "wss" : "ws"}://${
wsDomain || location.host
}/ws`,
"coms",
);
socket.onopen = () => {
// Announce join.
socket.send(JSON.stringify({ type: "join", data: { name: username } }));
renderChat(chatEl, inputContainer);
};
socket.onmessage = (event) => {
const packet = JSON.parse(event.data);
switch (packet.type) {
case "welcome": {
// Initial handshake. Populate ids, online list, and history if provided.
clearData();
const { id, online = [], history = [] } = packet.data;
setMyId(String(id));
online.forEach(({ id: uid, name }) => addUser(String(uid), name));
history.forEach((m) => {
addMessage({
id: String(m.id),
username: m.author?.name || "Anon",
authorId: m.author?.id ? String(m.author.id) : null,
content: m.content,
ts: m.timestamp,
parent:
m.parent === null || m.parent === undefined || m.parent === "-1"
? null
: String(m.parent),
});
});
const groups = getGroupedUsers();
usersListEl.innerHTML = groups.length
? "Online: " + groups.join(", ")
: "Online: ?";
renderChat(chatEl, inputContainer);
break;
}
case "join_evt": {
// A new user joined.
const { id, name } = packet.data;
addUser(String(id), name);
addNotice(
`<span class="name">${new Option(name).innerHTML}</span> joined.`,
false,
);
const groups = getGroupedUsers();
usersListEl.innerHTML = "Online: " + groups.join(", ");
renderChat(chatEl, inputContainer);
break;
}
case "leave_evt": {
const { id, name } = packet.data;
const sid = String(id);
const lastName = users.get(sid) || name;
addNotice(
`<span class="name">${new Option(lastName).innerHTML}</span> left.`,
false,
);
removeUser(sid);
const groups = getGroupedUsers();
usersListEl.innerHTML = "Online: " + groups.join(", ");
renderChat(chatEl, inputContainer);
break;
}
case "name_evt": {
// A user changed name.
const {
user: { id, name: oldName },
new_name: newName,
} = packet.data;
updateUser(String(id), newName);
addNotice(
`<span class="name">${new Option(oldName).innerHTML}</span> changed name to <span class="name">${new Option(newName).innerHTML}</span>`,
);
const groups = getGroupedUsers();
usersListEl.innerHTML = "Online: " + groups.join(", ");
renderChat(chatEl, inputContainer);
break;
}
case "msg_evt": {
// A chat message arrived.
const { id, author, content, parent, timestamp } = packet.data;
const username = author?.name || "Anon";
const ts = timestamp || Math.floor(Date.now() / 1000);
if (author?.id) addUser(String(author.id), username);
addMessage({
id: String(id),
username,
authorId: author?.id ? String(author.id) : null,
content,
ts,
parent:
parent === null || parent === undefined || parent === "-1"
? null
: String(parent),
});
// Notify when someone else replies directly to one of my root messages.
const myId = getMyId();
const parentId =
parent === null || parent === undefined || parent === "-1"
? null
: String(parent);
if (myId && parentId && author?.id && String(author.id) !== myId) {
const parentMsg = getThreads().get(parentId);
if (
parentMsg &&
parentMsg.parent === null &&
parentMsg.authorId === myId
) {
notifyDirectReply(username, content);
}
}
renderChat(chatEl, inputContainer);
break;
}
case "msg_ack": {
const { status, id } = packet.data;
if (status !== "success") {
addNotice(
`<span class="err">Message failed: ${new Option(status).innerHTML}</span>`,
);
renderChat(chatEl, inputContainer);
} else if (id) {
const replyTarget = getReplyTo();
if (replyTarget === null) {
const sid = String(id);
setFocused(sid);
setReplyTo(sid);
}
}
break;
}
case "name_ack": {
const { status, name } = packet.data;
if (status !== "success") {
addNotice(
`<span class="err">Name change failed: ${new Option(status).innerHTML}</span>`,
);
renderChat(chatEl, inputContainer);
}
break;
}
case "ping": {
const ts = packet.data?.ts ?? Math.floor(Date.now() / 1000);
socket.send(JSON.stringify({ type: "pong", data: { ts } }));
break;
}
default:
console.warn("Unknown packet type:", packet.type);
renderChat(chatEl, inputContainer);
}
};
socket.onerror = (err) => {
console.error("WebSocket error:", err);
addNotice(
'<span class="err">WebSocket error. See console for details.</span>',
);
renderChat(chatEl, inputContainer);
};
socket.onclose = () => {
addNotice("Disconnected.", false);
renderChat(chatEl, inputContainer);
};
/**
* Send a chat message via WebSocket.
* @param {string} content - The message text to send.
*/
function sendMessage(content) {
const parent = getReplyTo();
socket.send(
JSON.stringify({
type: "msg",
data: { content, parent: parent == null ? -1 : parent },
}),
);
}
/**
* Send a name change packet via WebSocket.
* @param {string} username - The new username to set.
*/
function sendName(username) {
socket.send(JSON.stringify({ type: "name", data: { name: username } }));
}
return { socket, sendMessage, sendName };
}