233 lines
7.0 KiB
JavaScript
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 };
|
|
}
|