Oh man that's a lot of changes.

This commit is contained in:
2026-02-28 15:59:03 -05:00
parent 226c4a8b52
commit de55288d37
11 changed files with 864 additions and 336 deletions

View File

@@ -27,7 +27,7 @@ document.addEventListener("DOMContentLoaded", () => {
inputContainer.style.display = "flex";
const defaultName = "Anon";
const MAX_NAME_LEN = 31;
const MAX_NAME_LEN = 15; // server limit is NAME_MAX_LENGTH-1 (15 chars)
const { socket, sendMessage, sendName } = initWebSocket(
defaultName,
@@ -88,8 +88,8 @@ document.addEventListener("DOMContentLoaded", () => {
chat.addEventListener("click", (e) => {
const msgDiv = e.target.closest(".msg");
if (!msgDiv) return;
const id = Number(msgDiv.dataset.id);
if (!isFinite(id)) return;
const id = msgDiv.dataset.id;
if (!id) return;
const threads = getThreads();
const author = threads.get(id)?.username;
setReplyTo(id);
@@ -124,9 +124,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
const msgs = Array.from(chat.querySelectorAll("div.msg"));
if (!msgs.length) return;
let idx = msgs.findIndex(
(div) => Number(div.dataset.id) === getFocused(),
);
let idx = msgs.findIndex((div) => div.dataset.id === getFocused());
if (idx === -1) {
idx = e.key === "ArrowDown" ? -1 : msgs.length;
}
@@ -134,7 +132,7 @@ document.addEventListener("DOMContentLoaded", () => {
e.key === "ArrowDown"
? Math.min(msgs.length - 1, idx + 1)
: Math.max(0, idx - 1);
const newId = Number(msgs[idx].dataset.id);
const newId = msgs[idx].dataset.id;
setFocused(newId);
setReplyTo(newId); // Move input box under focused message.
const threads = getThreads();

View File

@@ -1,7 +1,7 @@
// coms/client/public/data.js
// Data module: holds the inmemory threads, notices and reply state
// Map of all messages and notices by ID
// Map of all messages and notices by ID (string IDs)
// Clients own session ID
let myId = null;
const threads = new Map();
@@ -9,28 +9,28 @@ const threads = new Map();
// Map of connected users by unique ID
const users = new Map();
// Ordered list of rootlevel IDs (messages and notices)
// Ordered list of rootlevel IDs (messages and notices, stored as strings)
const rootIds = [];
// Negative counter to generate unique IDs for notices
let noticeCounter = -1;
// ID of the message were currently replying to (or null)
// ID of the message were currently replying to (string or null)
let replyTo = null;
// ID of the message currently focused for navigation (or null)
// ID of the message currently focused for navigation (string or null)
let focusedId = null;
/**
* Set the focused message ID (or null to clear).
* @param {number|null} id
* @param {string|null} id
*/
function setFocused(id) {
focusedId = id;
}
/**
* Get the currently focused message ID.
* @returns {number|null}
* @returns {string|null}
*/
function getFocused() {
return focusedId;
@@ -46,7 +46,7 @@ function clearFocused() {
/**
* Register a user with their unique ID.
* @param {number} id
* @param {string} id
* @param {string} name
*/
function addUser(id, name) {
@@ -55,7 +55,7 @@ function addUser(id, name) {
/**
* Remove a user by their ID.
* @param {number} id
* @param {string} id
*/
function removeUser(id) {
users.delete(id);
@@ -63,7 +63,7 @@ function removeUser(id) {
/**
* Update a users name.
* @param {number} id
* @param {string} id
* @param {string} name
*/
function updateUser(id, name) {
@@ -72,7 +72,7 @@ function updateUser(id, name) {
/**
* Get a list of {id, name} objects for all users.
* @returns {{id:number, name:string}[]}
* @returns {{id:string, name:string}[]}
*/
function getUsers() {
return Array.from(users.entries()).map(([id, name]) => ({ id, name }));
@@ -95,14 +95,31 @@ function getGroupedUsers() {
/**
* Add a new chat message to the thread structure.
* @param {{id: number, username: string, content: string, ts: number, parent?: number}} msg
* @param {{id: string|number, username: string, content: string, ts: number, parent?: string|number|null, authorId?: string|number|null}} msg
*/
function addMessage({ id, username, content, ts, parent = null }) {
threads.set(id, { id, username, content, ts, parent, children: [] });
if (parent !== null && threads.has(parent)) {
threads.get(parent).children.push(id);
function addMessage({
id,
username,
content,
ts,
parent = null,
authorId = null,
}) {
const sid = String(id);
const sparent = parent === null ? null : String(parent);
threads.set(sid, {
id: sid,
username,
authorId: authorId === null ? null : String(authorId),
content,
ts,
parent: sparent,
children: [],
});
if (sparent !== null && threads.has(sparent)) {
threads.get(sparent).children.push(sid);
} else {
rootIds.push(id);
rootIds.push(sid);
}
}
@@ -112,7 +129,7 @@ function addMessage({ id, username, content, ts, parent = null }) {
*/
function addNotice(content) {
const ts = Math.floor(Date.now() / 1000);
const id = noticeCounter--;
const id = String(noticeCounter--);
threads.set(id, {
id,
username: "",
@@ -132,40 +149,41 @@ function clearData() {
rootIds.length = 0;
noticeCounter = -1;
replyTo = null;
users.clear();
}
/** @returns {Map<number,object>} The threads map. */
/** @returns {Map<string,object>} The threads map. */
function getThreads() {
return threads;
}
/** @returns {number[]} The ordered list of rootlevel IDs. */
/** @returns {string[]} The ordered list of rootlevel IDs. */
function getRootIds() {
return rootIds;
}
/**
* Set this clients own session ID.
* @param {number} id
* @param {string|number} id
*/
function setMyId(id) {
myId = id;
myId = String(id);
}
/**
* Get this clients own session ID.
* @returns {number|null}
* @returns {string|null}
*/
function getMyId() {
return myId;
}
/** @returns {number|null} The current replyto ID. */
/** @returns {string|null} The current replyto ID. */
function getReplyTo() {
return replyTo;
}
/** @param {number|null} id — Set the current replyto ID. */
/** @param {string|null} id — Set the current replyto ID. */
function setReplyTo(id) {
replyTo = id;
}

View File

@@ -9,6 +9,7 @@ import {
addNotice,
getReplyTo,
addUser,
removeUser,
updateUser,
getGroupedUsers,
setMyId,
@@ -39,8 +40,7 @@ export function initWebSocket(username, chatEl, inputContainer, usersListEl) {
socket.onopen = () => {
// Announce join.
socket.send(JSON.stringify({ type: "join", data: { username } }));
//addNotice(`Connected as <span class="name">${username}</span>`);
socket.send(JSON.stringify({ type: "join", data: { name: username } }));
renderChat(chatEl, inputContainer);
};
@@ -48,32 +48,60 @@ export function initWebSocket(username, chatEl, inputContainer, usersListEl) {
const packet = JSON.parse(event.data);
switch (packet.type) {
case "welcome": {
// Initial user list.
const myId = packet.data.you;
// Initial handshake. Populate ids, online list, and history if provided.
clearData();
setMyId(myId);
const users = packet.data.users || [];
users.forEach(({ id, username }) => addUser(id, username));
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 = "Online: " + groups.join(", ");
//addNotice(`<i>Users online: ${groups.join(", ")}</i>`);
usersListEl.innerHTML = groups.length
? "Online: " + groups.join(", ")
: "Online: ?";
break;
}
case "join-event": {
case "join_evt": {
// A new user joined.
const { id, username } = packet.data;
addUser(id, username);
const { id, name } = packet.data;
addUser(String(id), name);
addNotice(
`<span class="name">${new Option(username).innerHTML}</span> joined.`,
`<span class="name">${new Option(name).innerHTML}</span> joined.`,
);
const groups = getGroupedUsers();
usersListEl.innerHTML = "Online: " + groups.join(", ");
break;
}
case "name-event": {
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.`,
);
removeUser(sid);
const groups = getGroupedUsers();
usersListEl.innerHTML = "Online: " + groups.join(", ");
break;
}
case "name_evt": {
// A user changed name.
const { id, new: newName, old: oldName } = packet.data;
updateUser(id, newName);
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>`,
);
@@ -81,19 +109,53 @@ export function initWebSocket(username, chatEl, inputContainer, usersListEl) {
usersListEl.innerHTML = "Online: " + groups.join(", ");
break;
}
case "msg-event": {
case "msg_evt": {
// A chat message arrived.
const { id, username: u, content, ts, parent } = packet.data;
addMessage({ id, username: u, content, ts, parent });
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),
});
break;
}
case "msg-ack": {
const { id } = packet.data;
if (getReplyTo() === null) {
setFocused(id);
setReplyTo(id);
case "msg_ack": {
const { status, id } = packet.data;
if (status !== "success") {
addNotice(
`<span class="err">Message failed: ${new Option(status).innerHTML}</span>`,
);
} else if (id) {
const replyTarget = getReplyTo();
if (replyTarget === null) {
const sid = String(id);
setFocused(sid);
setReplyTo(sid);
}
}
renderChat(chatEl, inputContainer);
break;
}
case "name_ack": {
const { status, name } = packet.data;
if (status !== "success") {
addNotice(
`<span class="err">Name change failed: ${new Option(status).innerHTML}</span>`,
);
}
break;
}
case "ping": {
const ts = packet.data?.ts ?? Math.floor(Date.now() / 1000);
socket.send(JSON.stringify({ type: "pong", data: { ts } }));
break;
}
default:
@@ -122,7 +184,12 @@ export function initWebSocket(username, chatEl, inputContainer, usersListEl) {
*/
function sendMessage(content) {
const parent = getReplyTo();
socket.send(JSON.stringify({ type: "msg", data: { content, parent } }));
socket.send(
JSON.stringify({
type: "msg",
data: { content, parent: parent == null ? -1 : parent },
}),
);
}
/**
@@ -130,7 +197,7 @@ export function initWebSocket(username, chatEl, inputContainer, usersListEl) {
* @param {string} username - The new username to set.
*/
function sendName(username) {
socket.send(JSON.stringify({ type: "name", data: { username } }));
socket.send(JSON.stringify({ type: "name", data: { name: username } }));
}
return { socket, sendMessage, sendName };