feat(chat): add transient presence flash and 5s reconnect grace period
This commit is contained in:
1 parent
4eb88a4594
commit
cef002862e
19 files changed
+299
-37
No files matched your search
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user