Files
coms/client/public/app.js
2026-02-14 16:02:53 -05:00

149 lines
4.2 KiB
JavaScript

import {
setReplyTo,
getThreads,
addNotice,
updateUser,
getGroupedUsers,
getMyId,
getUsers,
getFocused,
setFocused,
clearFocused,
} from "./data.js";
import { renderChat } from "./render.js";
import { initWebSocket } from "./wsModule.js";
document.addEventListener("DOMContentLoaded", () => {
const chat = document.getElementById("chat");
const inputContainer = document.getElementById("inputContainer");
const rootButton = document.getElementById("rootButton");
const sendButton = document.getElementById("sendButton");
const input = document.getElementById("messageInput");
const usersList = document.getElementById("userList");
const nameInput = document.getElementById("nameInput");
const nameButton = document.getElementById("nameButton");
chat.style.display = "";
inputContainer.style.display = "flex";
const defaultName = "Anon";
const MAX_NAME_LEN = 31;
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>`,
);
renderChat(chat, inputContainer);
nameInput.focus();
return;
}
sendName(newName);
updateUser(getMyId(), newName);
const groups = getGroupedUsers();
usersList.innerHTML = "Online: " + groups.join(", ");
nameInput.value = "";
nameInput.placeholder = `Name: ${newName}`;
input.focus();
});
// Enter to set name.
nameInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") nameButton.click();
});
// Button to send.
sendButton.addEventListener("click", () => {
const text = input.value.trim();
if (!text) return;
sendMessage(text);
input.value = "";
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();
}
});
// Click message to reply.
chat.addEventListener("click", (e) => {
const msgDiv = e.target.closest(".msg");
if (!msgDiv) return;
const id = Number(msgDiv.dataset.id);
if (!isFinite(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();
});
// Go back to root.
rootButton.addEventListener("click", () => {
setReplyTo(null);
clearFocused();
input.placeholder = "";
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();
}
});
// 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) => Number(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 = Number(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" });
}
});
});