Oh man that's a lot of changes.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// coms/client/public/data.js
|
||||
// Data module: holds the in‐memory threads, notices and reply state
|
||||
|
||||
// Map of all messages and notices by ID
|
||||
// Map of all messages and notices by ID (string IDs)
|
||||
// Client’s 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 root‐level IDs (messages and notices)
|
||||
// Ordered list of root‐level IDs (messages and notices, stored as strings)
|
||||
const rootIds = [];
|
||||
|
||||
// Negative counter to generate unique IDs for notices
|
||||
let noticeCounter = -1;
|
||||
|
||||
// ID of the message we’re currently replying to (or null)
|
||||
// ID of the message we’re 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 user’s 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 root‐level IDs. */
|
||||
/** @returns {string[]} The ordered list of root‐level IDs. */
|
||||
function getRootIds() {
|
||||
return rootIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set this client’s own session ID.
|
||||
* @param {number} id
|
||||
* @param {string|number} id
|
||||
*/
|
||||
function setMyId(id) {
|
||||
myId = id;
|
||||
myId = String(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this client’s own session ID.
|
||||
* @returns {number|null}
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function getMyId() {
|
||||
return myId;
|
||||
}
|
||||
|
||||
/** @returns {number|null} The current reply‐to ID. */
|
||||
/** @returns {string|null} The current reply‐to ID. */
|
||||
function getReplyTo() {
|
||||
return replyTo;
|
||||
}
|
||||
|
||||
/** @param {number|null} id — Set the current reply‐to ID. */
|
||||
/** @param {string|null} id — Set the current reply‐to ID. */
|
||||
function setReplyTo(id) {
|
||||
replyTo = id;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user