fix(desktop): project home read-state from the shared NIP-RS marker#631
Merged
Conversation
Reading a channel in-thread now clears the Home panel indicators for items in that channel — both the row blue dot / bold styling and the sidebar Home badge. Before, Home maintained its own local "done" set (`sprout-home-feed-done.v1`) and "seen" set (`sprout-home-feed-seen.v1`) that were only advanced by interacting with Home itself, so a message read in-channel stayed unread in Home indefinitely. Changes: * `useUnreadChannels` now also returns `getEffectiveTimestamp` and `readStateVersion` from the underlying `ReadStateManager`, so other surfaces can project per-item read-state off the same NIP-RS marker without instantiating a second manager. * `AppShellContext` exposes those two fields as `getChannelReadAt` and `readStateVersion` for any descendant. * New `useHomeInboxReadState` hook in `features/home/` composes the projection: channel-backed items are read iff `latestActivityAt <= getChannelReadAt(channelId)`; items without a channel (reminders etc.) fall back to the local `useFeedItemState` done-set. Mark/undo for channel-backed items is routed through `markChannelRead`/`markChannelUnread`, keeping the storage hook policy-free. * `useHomeFeedNotificationState` gains the same NIP-RS-aware predicate for the sidebar Home badge (using `FeedItem.createdAt`, not `latestActivityAt`, since the badge works over raw feed items). * To preserve a single `ReadStateManager`, the badge state computation is lifted out of `useHomeFeedNotifications` and into `AppShell`, after `useUnreadChannels` has mounted. Behavioural note: marking an older channel-backed Home item "unread" rolls the NIP-RS read marker back to that item's timestamp for the whole channel, so newer events in that channel become unread too. That matches NIP-RS's channel-level granularity; it isn't per-message granular. No data migration: stale channel-id entries in the old localStorage done-set are ignored by the new projection and age out naturally. Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
tlongwell-block
commented
May 20, 2026
Collaborator
Author
tlongwell-block
left a comment
There was a problem hiding this comment.
Reviewed against the 9/10+ bar. This is the shape I wanted:
- Single read-state source: Home consumes the AppShell-mounted
useUnreadChannels/ReadStateManager; no second manager. - Hook boundaries stay clean:
useFeedItemStateremains local-storage-only, and the NIP-RS projection is isolated inuseHomeInboxReadState. - Timestamp semantics look right: inbox rows use
InboxItem.latestActivityAt; badge items use rawFeedItem.createdAt; both compare strictly/ inclusively in the expected direction. - Badge hook split is minimal and preserves the
isHomeActiveshort-circuit / existing Home-seen behavior for non-channel items. - Mark read/unread routing is consistent with NIP-RS channel-level markers, including the documented rollback caveat.
Local verification: pnpm --dir desktop exec biome check on the touched TS/TSX files passed. Full local pnpm --dir desktop typecheck is blocked in my checkout by a missing @radix-ui/react-context-menu install, so I’m relying on CI for full desktop typecheck/e2e; current PR checks are still in progress for Desktop/Desktop E2E/Desktop Build/cross-compile, with the already-finished checks green.
No blocking comments from me. Approving, conditional on CI finishing green.
The Home badge predicate I added in b1f13f6 treated `getChannelReadAt` returning `null` as "unread". That's correct for a freshly-arrived event in a channel the user has never opened — but it broke two cases: 1. `profile.spec.ts:69` (mock mode, no relay client): every channel-backed feed item appears unread because the read-state hooks no-op. The test pushed one mock mention and saw count=2 (the preseeded `mock-feed-mention` plus the new push), expecting 1. 2. `integration.spec.ts:246` (live mention into the channel the user is currently reading): the in-channel marker auto-advances past the new mention, so the predicate correctly says "read". The test expected the badge to tick to 1 — that was the *old* design's behavior (driven by a local seen-set that didn't know about in-channel reads). The new design's behavior is what Tyler asked for: if you're reading the channel, the home badge shouldn't light up for messages arriving in it. Fixes: - `useHomeFeedNotificationState`: only consult the NIP-RS marker when it's non-null. When the channel has no marker yet (cold start, mock mode), fall back to the legacy `!seenFeedIdSet.has(id)` predicate so a freshly-seen feed item doesn't keep tripping the badge forever. - `integration.spec.ts:246`: replace the `sidebar-home-count = "1"` assertion (which embodied the old design) with an assertion that the refetched home inbox contains the mention text after navigating Home. That preserves the test's actual purpose ("live mentions refetch the home feed without waiting for polling") under the new contract. The sibling test for forum mentions (mention into a channel the user is NOT viewing) is unaffected and still asserts the badge ticks to 1 — that case goes through the seen-set fallback path. Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com> Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
This was referenced May 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the bug Tyler filed in #sprout-bugs: the Home panel kept items marked unread even after they'd been seen in their channel.
Why
Home maintained its own local "done"/"seen" sets in localStorage that were only advanced by interacting with Home itself. So a message read in-channel stayed unread in Home indefinitely. The NIP-RS read marker that the channel sidebar already uses was the right source of truth; Home just wasn't consulting it.
What
useUnreadChannelsalso returnsgetEffectiveTimestampandreadStateVersionfrom the sharedReadStateManager.AppShellContextexposes them asgetChannelReadAt(channelId)andreadStateVersion.useHomeInboxReadStatehook projects per-item read-state off the marker: channel-backed items are read ifflatestActivityAt <= getChannelReadAt(channelId). Items without a channel (reminders) fall back to the existing local done-set. Mark/undo for channel-backed items routes throughmarkChannelRead/markChannelUnread.useHomeFeedNotificationState(sidebar Home badge) gets the same predicate. To preserve a singleReadStateManager, this hook is lifted out ofuseHomeFeedNotificationsand called inAppShellafteruseUnreadChannelshas mounted.Single read-state source, hook boundaries preserved.
Behavioural note
Marking an older channel-backed Home item "unread" rolls the NIP-RS marker back to that item's timestamp for the whole channel — newer events in that channel become unread too. That matches NIP-RS's channel-level granularity; it isn't per-message granular. Surfaced in the commit message for future spelunkers.
Test plan
pnpm typecheck,pnpm check,pnpm lintall green.integration.spec.ts:246still assertssidebar-home-count → 0after navigating to Home; theisHomeActiveshort-circuit at the top ofuseHomeFeedNotificationStatepreserves that semantic.#generaland#agents, opened#general, returned to Home — the alice/#general row dot cleared while the two#agentsrows kept theirs. Screenshots posted in the Sprout thread: https://sprout-oss.stage.blox.sqprod.co/channels/d14cd131-6084-4c9c-ba4d-24fb6bfc4263/thread/6e046dd99ab761dfbe737095a1282d6b1cd684fc0e9e1c78fb710ff6babaaa71File-size overrides
Bumped two existing biome
max-linesoverrides with one-line justifications matching the surrounding style:HomeView.tsx500→505 (new hook wiring),AppShell.tsx800→815 (lifted notification-state call).