diff --git a/web/src/lib/tinode/topics.ts b/web/src/lib/tinode/topics.ts index 47568324..15984eb0 100644 --- a/web/src/lib/tinode/topics.ts +++ b/web/src/lib/tinode/topics.ts @@ -750,6 +750,24 @@ function reconcileMeSubs() { c.unread = prev.unread; notifyTopic(c.topic); } + // Separate, real bug this same guard doesn't cover (live-confirmed 2026-09-24, reported + // directly): sending your OWN message into a topic that's already open bumps `confirmedReadSeq` + // synchronously (ConversationView's noteRead effect), long before the async `{note read}` this + // relies on has actually reached the server. A `{get}` already in flight since before that + // moment can come back with a real, ADVANCED `seq` (the new message the server just accepted) + // paired with a `read` that hasn't caught up server-side yet -- `seq <= prev.seq` above is + // false (seq genuinely changed), so that guard correctly doesn't fire, and this spurious + // `unread:1` gets applied verbatim even though the user is the one who just sent it. Clamp + // `read` to what THIS client already knows it has confirmed seeing, regardless of what the + // fetch says -- a floor, never pushes `read` past the server's own value when the server is + // genuinely ahead of this record (e.g. read from a different, already-synced device). + const confirmed = confirmedReadSeq.get(c.topic) || 0; + if (confirmed > (c.read || 0)) { + c.read = confirmed; + if ((c.recv || 0) < confirmed) c.recv = confirmed; + c.unread = Math.max(0, (c.seq || 0) - confirmed); + notifyTopic(c.topic); + } } }) .catch(() => {});