v3: chat mentions stop going missing, and live events get a light sound — the server skips a mention notification for anyone with the chatbox open, on the grounds that they're reading it, but "open" also meant a chatbox left open in a background tab, and the same modal cleared the unread badge on every arrival, so a mention there left no bell, no badge and no trace. Reading the chat now means open and on screen: the focus flag is re-announced on every visibilitychange, and the unread count only clears while the tab is visible. A reconnect with the chatbox open refetches the latest page, since whatever was said while the socket was down never reached the live buffer. The server's mention regex excluded a backslash and the letter w rather than word characters (\\w inside a class), so an email address mentioned its domain; it now matches the autocomplete's rule. On top of that, sounds synthesised with Web Audio — a soft blip for chat you aren't looking at, throttled, and a two-note chime for notifications and for a mention you're reading live, which gets no notification of its own — claimed through localStorage so several tabs chime once, with an on/off toggle under a new Notifications section in settings. The tab title carries the unread count, composed with the page title in useDocumentTitle so navigation doesn't drop it.
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 46s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-22 19:25:19 +00:00
parent c9fb4450ad
commit b313f70d31
11 changed files with 520 additions and 206 deletions

View File

@@ -284,6 +284,7 @@ export function ChatModal({ onClose }: { onClose: () => void }) {
clearUnreadChat,
setChatTyping,
setChatOpen,
connectionEpoch,
} = useWS();
const { authFetch, user } = useAuth();
const canModerate = can(user, "chat:moderate");
@@ -352,10 +353,43 @@ export function ChatModal({ onClose }: { onClose: () => void }) {
};
}, [authFetch]);
// Clear the unread badge on open and as new messages arrive while open.
// Messages sent while the socket was down never reached the live buffer:
// after a reconnect, fetch the latest page again and fold it into history
// (the merge above dedupes against what's already shown).
const seenEpochRef = useRef(connectionEpoch);
useEffect(() => {
clearUnreadChat();
}, [chatMessages, clearUnreadChat]);
if (connectionEpoch === seenEpochRef.current) return;
seenEpochRef.current = connectionEpoch;
let cancelled = false;
(async () => {
try {
const res = await authFetch(`${API_URL}/api/chat?limit=${PAGE_SIZE}`);
const json = await res.json();
if (cancelled || !json.success) return;
const items = (json.data as RawChatMessage[]).map(deserializeChatMessage);
setHistory((prev) => [...prev, ...items]);
} catch {
// The next reconnect or reopen will try again.
}
})();
return () => {
cancelled = true;
};
}, [authFetch, connectionEpoch]);
// Whether the tab is on screen: a chatbox left open in a background tab isn't
// being read, so its messages should stay unread until the user comes back.
const [tabVisible, setTabVisible] = useState(() => !document.hidden);
useEffect(() => {
const onVisibility = () => setTabVisible(!document.hidden);
document.addEventListener("visibilitychange", onVisibility);
return () => document.removeEventListener("visibilitychange", onVisibility);
}, []);
// Clear the unread badge on open and as new messages arrive while read.
useEffect(() => {
if (tabVisible) clearUnreadChat();
}, [chatMessages, clearUnreadChat, tabVisible]);
// Tell the server the chatbox is open so it skips redundant mention
// notifications while the user is reading; clear the flag on close.