feat(chat): add transient presence flash and 5s reconnect grace period
CI / test (push) Successful in 1m33s
CI / docker (push) Successful in 2m10s

This commit is contained in:
zwitschi committed 2026-08-13 20:45:52 +02:00
1 parent 4eb88a4594
commit cef002862e
19 files changed
+299 -37

No files matched your search

+21
View File
@@ -105,6 +105,7 @@ beforeEach(() => {
<span id="room-name">Main Channel</span>
<button type="button" onclick="togglePrivateDM()">New DM</button>
</div>
<div id="chat-flash" class="chat-flash" aria-live="polite" hidden></div>
<section id="messages" class="chat-messages" aria-live="polite"></section>
<section id="presence-panel" class="presence-panel">
<div id="presence-list" class="presence-list"></div>
@@ -581,3 +582,23 @@ describe("responsive layout", () => {
expect(np?.getAttribute("aria-live")).toBe("polite");
});
});
describe("presence flash", () => {
it("flashes join/leave messages instead of appending chat rows", async () => {
await import("../src/main").then((mod) => mod.initPlayerAndChat(async () => ({ default: HlsMock }) as any));
const ws = websocketInstances[0];
ws.onmessage?.({ data: JSON.stringify({ type: "system", text: "alice joined the room", username: "system" }) });
const flash = document.getElementById("chat-flash")!;
expect(flash.textContent).toBe("alice joined");
expect(flash.hidden).toBe(false);
// Join/leave must not be rendered as chat rows.
const messages = document.getElementById("messages")!;
expect(messages.textContent).not.toContain("joined");
ws.onmessage?.({ data: JSON.stringify({ type: "system", text: "alice left the room", username: "system" }) });
expect(flash.textContent).toBe("alice left");
});
});
+26 -2
View File
@@ -22,6 +22,8 @@ let usersVisible = true;
let typingUser = "";
let lastTypingSent = 0;
let typingTimeout: number | undefined;
let flashTimeout: number | undefined;
let flashFadeTimeout: number | undefined;
const onlineUsers = new Set<string>();
// Player error recovery state (STREAMING.md contract)
@@ -96,20 +98,25 @@ function connectWebSocket(room: string) {
return;
}
hideTyping(msg.username);
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
// Track presence from system join/leave messages
// Presence join/leave announcements become transient flashes, not chat rows.
if (msg.type === "system" && msg.username === "system") {
const joined = msg.text?.match(/^(\S+) joined /);
const left = msg.text?.match(/^(\S+) left /);
if (joined) {
onlineUsers.add(joined[1]);
renderPresence();
showFlash(`${joined[1]} joined`);
return;
} else if (left) {
onlineUsers.delete(left[1]);
renderPresence();
showFlash(`${left[1]} left`);
return;
}
}
addMessage(msg.type || "public", msg.text, msg.username, msg.timestamp);
};
ws.onclose = () => {
@@ -142,6 +149,23 @@ function addMessage(type: string, text: string, sender = "", timestamp = new Dat
messages.scrollTop = messages.scrollHeight;
}
function showFlash(text: string) {
const el = document.getElementById("chat-flash");
if (!el) return;
el.textContent = text;
el.hidden = false;
el.classList.remove("fade-out");
window.clearTimeout(flashTimeout);
window.clearTimeout(flashFadeTimeout);
flashTimeout = window.setTimeout(() => {
el.classList.add("fade-out");
flashFadeTimeout = window.setTimeout(() => {
el.hidden = true;
el.classList.remove("fade-out");
}, 500);
}, 3000);
}
function renderPresence() {
const list = document.getElementById("presence-list");
if (!list) return;
+24
View File
@@ -327,6 +327,28 @@ select {
padding: 0.25rem 0.75rem 0;
}
.chat-flash {
position: absolute;
top: 0.5rem;
left: 0.5rem;
right: 0.5rem;
z-index: 5;
padding: 0.4rem 0.75rem;
font-size: 0.83rem;
text-align: center;
color: var(--ink-0);
background: color-mix(in srgb, var(--brand) 30%, var(--panel-2));
border: 1px solid var(--line);
border-radius: var(--radius-1);
opacity: 1;
transition: opacity 0.5s ease;
pointer-events: none;
}
.chat-flash.fade-out {
opacity: 0;
}
.btn {
border: 1px solid transparent;
border-radius: var(--radius-1);
@@ -650,6 +672,7 @@ select {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 0;
position: relative;
}
.chat-header,
@@ -761,6 +784,7 @@ select {
.message-text {
word-break: break-word;
font-size: 0.95rem;
}
.message-time {