feat(desktop): add workspace rail unread observer#1428
Merged
Conversation
Collaborator
Author
Add a Discord/Slack-style workspace rail on the far left of the app: one
button per workspace with initials, active highlight, click-to-switch
(reusing the existing switchWorkspace plumbing), and an Add Workspace
button. The rail auto-hides with a single workspace and is mounted as its
own column beside the sidebar so it stays visible when the sidebar is
collapsed. On macOS it starts its buttons below the traffic lights and the
top-chrome controls sit just past them.
Unread is observed for inactive workspaces without touching the active
relay singleton: a separate read-only relay client polls each inactive
workspace on an interval, reuses the shared NIP-RS read-state snapshot
parser, and reports { hasUnread, count (mentions), state } per workspace.
The rail renders a dot for any unread and a mention-count badge, and never
reports "no unread" for a relay it could not observe (unknown/error states).
Covered by rail + observer unit tests and rail e2e (visibility, switching,
collapse, macOS traffic-light clearance).
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
3708248 to
0a75e77
Compare
Two small rail follow-ups:
- Remove the sidebar search header's top padding so the search bar
top-aligns with the channel header on the right (was ~9px lower).
- Derive workspace-button initials from a punctuation-stripped name so a
name like "B (relay)" shows "BR" instead of "B(". Add unit coverage.
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Narrow the rail column (w-14 -> w-12) so the workspace buttons sit closer to the sidebar and reduce the gap between the rail and the search bar. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Right-align the workspace squares within the rail (items-end) so they sit close to the search bar / sidebar content instead of centered with a gap, and so the active/unread pill lands at the rail's left edge instead of clipping off the window. Closes the excess space between the rail buttons and the search input. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Revert the buttons to centered in the rail so they keep an even margin on both sides — right-aligning (items-end) made them too tight against the main content panel when the sidebar is collapsed. Nudge the active/unread pill so it renders at the rail's left edge instead of clipping. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Shrink the workspace button box h-10 -> h-9 so the square's top aligns with the sidebar search input (the h-9 square was centered in an h-10 box, sitting 2px low). Recolor the active/unread pill from bg-sidebar-foreground to bg-primary so it uses the accent. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Move the active/unread pill from left-[-4px] to left-[-6px] so it sits flush at the rail's left edge with a small gap before the button, instead of abutting it (now visible since the pill uses the accent color). Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Add a workspaceRail preview feature so the far-left rail is hidden by default and opt-in via Settings > Experiments. AppShell gates both the rail render and the hasWorkspaceRail layout flag on it, so with the flag off the top chrome falls back to its normal traffic-light offset. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Apply biome formatting to the workspace rail/observer files and remove an unused workspacesKey dependency flagged by the exhaustive-deps lint in useWorkspaceUnread. Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Each workspace mounts a fresh React-Query client, so switching workspaces (or back to one just visited) started cold and blocked the sidebar on a multi-round-trip get_channels(). Snapshot the last successful channel list to localStorage keyed by relay URL and seed useChannelsQuery with it as already-stale initialData: the sidebar paints instantly from the snapshot while the live fetch revalidates. Snapshots are GC'd on workspace removal, mirroring the existing self-profile cache. Co-authored-by: Brain <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
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.


Summary
Testing
Full desktop suite green:
just desktop-check,just desktop-typecheck,just desktop-test(1428/1428).Rail e2e coverage added in
desktop/tests/e2e/workspace-rail.spec.tsand validated by Brain one test at a time locally:Instant channel-list paint
Each workspace mounts a fresh React-Query client, so switching workspaces (or switching back to one just visited) started cold and blocked the sidebar on a multi-round-trip
get_channels(). This adds a per-relay channel snapshot:getChannels(), the sorted channel list is persisted tolocalStoragekeyed by normalized relay URL (channelSnapshot.ts, mirroring the existingselfProfileStoragepattern — versioned, tolerant reads/writes).useChannelsQueryseeds from that snapshot asinitialDatawithinitialDataUpdatedAt: 0, so the sidebar paints instantly with real data (names, types, unread) while the live fetch revalidates immediately in the background.Scoped by relay URL, which matches today's workspace de-dupe (one relay = one workspace). If multiple identities on the same relay are ever supported, the key gains a
:pubkeysuffix — a one-line change.All in
desktop/src— no relay/DB/Rust changes. Covered bychannelSnapshot.test.mjs(round-trip, key normalization, malformed/wrong-version tolerance, storage-failure tolerance).Notes
Cross-relay unread/mention e2e is intentionally skipped for this PR because the current harness does not run two live relays. Observer behavior is covered by focused unit tests.
Screenshots to be uploaded in a follow-up PR comment.