Some tyhigns.
This commit is contained in:
@@ -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" });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const isDev = process.env.NODE_ENV !== "production";
|
||||
let livereload, connectLivereload;
|
||||
if (isDev) {
|
||||
@@ -8,6 +9,19 @@ if (isDev) {
|
||||
}
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
const DOMAIN_FILE = path.join(__dirname, "domain.txt");
|
||||
|
||||
function readWsDomain() {
|
||||
try {
|
||||
const val = fs.readFileSync(DOMAIN_FILE, "utf8").trim();
|
||||
if (val) return val;
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
console.warn("Failed to read domain.txt:", err.message);
|
||||
}
|
||||
}
|
||||
return process.env.WS_DOMAIN || "localhost:8080";
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
const liveReloadServer = livereload.createServer();
|
||||
@@ -17,6 +31,10 @@ if (isDev) {
|
||||
|
||||
app.use(express.static("public"));
|
||||
|
||||
app.get("/ws-domain", (_req, res) => {
|
||||
res.json({ domain: readWsDomain() });
|
||||
});
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user