Some tyhigns.

This commit is contained in:
2026-03-11 00:27:19 -04:00
parent 3b1b4f63e2
commit cbd2270628
14 changed files with 571 additions and 163 deletions

View File

@@ -29,118 +29,127 @@ document.addEventListener("DOMContentLoaded", () => {
const defaultName = "Anon";
const MAX_NAME_LEN = 15; // server limit is NAME_MAX_LENGTH-1 (15 chars)
const { socket, sendMessage, sendName } = initWebSocket(
defaultName,
chat,
inputContainer,
usersList,
);
// Renames.
nameButton.addEventListener("click", () => {
const newName = nameInput.value.trim() || defaultName;
if (newName.length > MAX_NAME_LEN) {
addNotice(
`<span class="err">Name may be at most ${MAX_NAME_LEN} characters.</span>`,
// Fetch WebSocket domain from the client server so deployments can point to a
// remote websocket host without rebuilding the bundle.
fetch("/ws-domain")
.then((res) => res.json())
.then(({ domain }) => domain || "")
.catch(() => "")
.then((wsDomain) => {
const { socket, sendMessage, sendName } = initWebSocket(
defaultName,
chat,
inputContainer,
usersList,
wsDomain,
);
renderChat(chat, inputContainer);
nameInput.focus();
return;
}
sendName(newName);
updateUser(getMyId(), newName);
const groups = getGroupedUsers();
usersList.innerHTML = "Online: " + groups.join(", ");
// Renames.
nameButton.addEventListener("click", () => {
const newName = nameInput.value.trim() || defaultName;
if (newName.length > MAX_NAME_LEN) {
addNotice(
`<span class="err">Name may be at most ${MAX_NAME_LEN} characters.</span>`,
);
renderChat(chat, inputContainer);
nameInput.focus();
return;
}
nameInput.value = "";
nameInput.placeholder = `Name: ${newName}`;
input.focus();
});
sendName(newName);
updateUser(getMyId(), newName);
const groups = getGroupedUsers();
usersList.innerHTML = "Online: " + groups.join(", ");
// Enter to set name.
nameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") nameButton.click();
});
nameInput.value = "";
nameInput.placeholder = `Name: ${newName}`;
input.focus();
});
// Button to send.
sendButton.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
sendMessage(text);
input.value = "";
input.focus();
});
// Enter to set name.
nameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") nameButton.click();
});
// Enter to send and escape to go to root.
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendButton.click();
} else if (e.key === "Escape") {
setReplyTo(null);
input.placeholder = "";
renderChat(chat, inputContainer);
input.focus();
}
});
// Button to send.
sendButton.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
sendMessage(text);
input.value = "";
input.focus();
});
// Click message to reply.
chat.addEventListener("click", (e) => {
const msgDiv = e.target.closest(".msg");
if (!msgDiv) return;
const id = msgDiv.dataset.id;
if (!id) return;
const threads = getThreads();
const author = threads.get(id)?.username;
setReplyTo(id);
setFocused(id);
input.placeholder = author ? `Replying to @${author}` : "";
renderChat(chat, inputContainer);
input.focus();
});
// Enter to send and escape to go to root.
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendButton.click();
} else if (e.key === "Escape") {
setReplyTo(null);
input.placeholder = "";
renderChat(chat, inputContainer);
input.focus();
}
});
// Go back to root.
rootButton.addEventListener("click", () => {
setReplyTo(null);
clearFocused();
input.placeholder = "";
renderChat(chat, inputContainer);
input.focus();
});
// Click message to reply.
chat.addEventListener("click", (e) => {
const msgDiv = e.target.closest(".msg");
if (!msgDiv) return;
const id = msgDiv.dataset.id;
if (!id) return;
const threads = getThreads();
const author = threads.get(id)?.username;
setReplyTo(id);
setFocused(id);
input.placeholder = author ? `Replying to @${author}` : "";
renderChat(chat, inputContainer);
input.focus();
});
// Global Escape handler: clear reply context, focus input, and clear focused message.
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
setReplyTo(null);
clearFocused();
input.placeholder = "";
renderChat(chat, inputContainer);
input.focus();
}
});
// Go back to root.
rootButton.addEventListener("click", () => {
setReplyTo(null);
clearFocused();
input.placeholder = "";
renderChat(chat, inputContainer);
input.focus();
});
// Arrow key navigation for focused messages (also sets reply target).
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
const msgs = Array.from(chat.querySelectorAll("div.msg"));
if (!msgs.length) return;
let idx = msgs.findIndex((div) => div.dataset.id === getFocused());
if (idx === -1) {
idx = e.key === "ArrowDown" ? -1 : msgs.length;
}
idx =
e.key === "ArrowDown"
? Math.min(msgs.length - 1, idx + 1)
: Math.max(0, idx - 1);
const newId = msgs[idx].dataset.id;
setFocused(newId);
setReplyTo(newId); // Move input box under focused message.
const threads = getThreads();
const author = threads.get(newId)?.username;
input.placeholder = author ? `Replying to @${author}` : "";
renderChat(chat, inputContainer);
const newDiv = chat.querySelector(`div[data-id="${newId}"]`);
if (newDiv) newDiv.scrollIntoView({ block: "nearest" });
}
});
// Global Escape handler: clear reply context, focus input, and clear focused message.
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
setReplyTo(null);
clearFocused();
input.placeholder = "";
renderChat(chat, inputContainer);
input.focus();
}
});
// Arrow key navigation for focused messages (also sets reply target).
document.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
const msgs = Array.from(chat.querySelectorAll("div.msg"));
if (!msgs.length) return;
let idx = msgs.findIndex((div) => div.dataset.id === getFocused());
if (idx === -1) {
idx = e.key === "ArrowDown" ? -1 : msgs.length;
}
idx =
e.key === "ArrowDown"
? Math.min(msgs.length - 1, idx + 1)
: Math.max(0, idx - 1);
const newId = msgs[idx].dataset.id;
setFocused(newId);
setReplyTo(newId); // Move input box under focused message.
const threads = getThreads();
const author = threads.get(newId)?.username;
input.placeholder = author ? `Replying to @${author}` : "";
renderChat(chat, inputContainer);
const newDiv = chat.querySelector(`div[data-id="${newId}"]`);
if (newDiv) newDiv.scrollIntoView({ block: "nearest" });
}
});
});
});

View File

@@ -27,14 +27,20 @@ import { renderChat } from "./render.js";
* @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) {
const socket = new WebSocket( //"ws://localhost:8080");
(location.protocol === "https:" ? "wss" : "ws") +
"://" +
location.host +
"/ws",
export function initWebSocket(
username,
chatEl,
inputContainer,
usersListEl,
wsDomain,
) {
const socket = new WebSocket(
`${location.protocol === "https:" ? "wss" : "ws"}://${
wsDomain || location.host
}/ws`,
"coms",
);