Merge pull request #108 from ArnasDon/fix/inbox-unread-badge-clears-on-select

fix(inbox): clear unread badge instantly when opening a conversation
This commit is contained in:
Arnas Donauskas 2026-05-22 09:30:12 +03:00 committed by GitHub
commit 2b3393b49a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -235,8 +235,22 @@ export default function InboxPage() {
if (event.eventType === "UPDATE") {
if (knownConvIdsRef.current.has(conv.id)) {
// If this UPDATE is for the conv the user is currently viewing,
// suppress the incoming unread_count — the user is reading it
// RIGHT NOW, so any positive value would just flicker the badge
// back on for the ~100ms it takes for the reset effect's server
// UPDATE to round-trip. Non-active convs take the value as-is.
const isActive = activeConversation?.id === conv.id;
setConversations((prev) =>
prev.map((c) => (c.id === conv.id ? { ...c, ...conv } : c)),
prev.map((c) =>
c.id === conv.id
? {
...c,
...conv,
unread_count: isActive ? 0 : conv.unread_count,
}
: c,
),
);
} else {
// UPDATE arrived before the INSERT (or after a missed INSERT)
@ -339,6 +353,17 @@ export default function InboxPage() {
setActiveConversation(match);
setActiveContact(match.contact ?? null);
setMessages([]);
// Mirror the optimistic unread reset that handleSelectConversation
// does — the user just deep-linked into this conv, treat that the
// same as a click. Leaves activeConversation.unread_count alone so
// the MessageThread reset effect still fires the server UPDATE.
if (match.unread_count > 0) {
setConversations((prev) =>
prev.map((c) =>
c.id === match.id ? { ...c, unread_count: 0 } : c,
),
);
}
}
}
},
@ -355,6 +380,22 @@ export default function InboxPage() {
setActiveConversation(conv);
setActiveContact(conv.contact ?? null);
setMessages([]);
// Optimistically clear the unread badge for this conv. The
// server-side reset is fired by the unread-reset effect inside
// MessageThread (which reads activeConversation.unread_count, not
// the list copy — so we deliberately leave that intact below to
// keep the effect firing), and the realtime UPDATE that comes
// back will sync to 0 again as a no-op. Zeroing the list copy
// here means the user sees the badge disappear the instant they
// click instead of waiting for the round-trip — and it persists
// even if the realtime UPDATE is dropped.
setConversations((prev) =>
prev.map((c) =>
c.id === conv.id && c.unread_count > 0
? { ...c, unread_count: 0 }
: c,
),
);
// Record the selection on the deep-link ref BEFORE we change the
// URL. The router.replace below flips `deepLinkConvId`, which can
// in turn cause ConversationList to refetch and eventually call