From 5e2bf700fb971253f872fdfe6f6d35a3a3b310d0 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Wed, 8 Jul 2026 10:52:06 -0400 Subject: [PATCH] fix(desktop): add unread dot to workspace rail for channel unreads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace rail only badged other workspaces for mentions, never for regular channel unreads. `workspaceRailIndicators` computed `showBadge` exclusively from `mentionCount > 0` and never consulted `unread.hasUnread`, so a workspace with channel activity but no mentions showed nothing. Add a two-tier indicator matching the rest of the app: - Numeric mention badge (existing) — unchanged. - Plain `h-2 w-2` unread dot when `hasUnread && !showBadge`, positioned at `-bottom-0.5 -right-0.5` with `ring-2 ring-sidebar` to read against the rail, testid `workspace-rail-unread-dot-${id}`, sr-only "unread" label. Both indicators are gated on `state === "ready"` — non-ready observations never surface either affordance. `showBadge` and `showDot` are mutually exclusive by construction. Tooltip also updated: dot state reads " — unread" to match app tone. Tests updated: the case that previously enshrined the missing affordance (`hasUnread: true, state: "ready"` → `showBadge: false`) now also asserts `showDot: true`. New `showDot` assertions added for all branches. Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- .../sidebar/ui/WorkspaceRail.test.mjs | 16 +++++++++---- .../src/features/sidebar/ui/WorkspaceRail.tsx | 23 ++++++++++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/desktop/src/features/sidebar/ui/WorkspaceRail.test.mjs b/desktop/src/features/sidebar/ui/WorkspaceRail.test.mjs index 8f33ca1cbc..358af9c53d 100644 --- a/desktop/src/features/sidebar/ui/WorkspaceRail.test.mjs +++ b/desktop/src/features/sidebar/ui/WorkspaceRail.test.mjs @@ -25,22 +25,25 @@ describe("workspaceRailIndicators", () => { it("shows no badge for an observed workspace with unread but no mentions", () => { const r = workspaceRailIndicators({ hasUnread: true, state: "ready" }); assert.equal(r.showBadge, false); + assert.equal(r.showDot, true); assert.equal(r.pending, false); }); - it("shows no badge for an observed workspace with no unread", () => { + it("shows no badge and no dot for an observed workspace with no unread", () => { const r = workspaceRailIndicators({ hasUnread: false, state: "ready" }); assert.equal(r.showBadge, false); + assert.equal(r.showDot, false); assert.equal(r.pending, false); }); - it("shows a mention badge with the count when mentions are present", () => { + it("shows a mention badge with the count when mentions are present — no dot", () => { const r = workspaceRailIndicators({ hasUnread: true, count: 3, state: "ready", }); assert.equal(r.showBadge, true); + assert.equal(r.showDot, false); assert.equal(r.mentionCount, 3); assert.equal(r.badgeLabel, "3"); }); @@ -54,30 +57,33 @@ describe("workspaceRailIndicators", () => { assert.equal(r.badgeLabel, "99+"); }); - it("never reports mentions for an unobserved (unknown) workspace", () => { + it("never reports mentions or dot for an unobserved (unknown) workspace", () => { const r = workspaceRailIndicators({ hasUnread: true, count: 5, state: "unknown", }); assert.equal(r.showBadge, false); + assert.equal(r.showDot, false); assert.equal(r.mentionCount, 0); assert.equal(r.pending, true); }); - it("treats loading as pending, not as no-unread", () => { + it("treats loading as pending — no badge, no dot", () => { const r = workspaceRailIndicators({ hasUnread: false, state: "loading" }); assert.equal(r.pending, true); assert.equal(r.showBadge, false); + assert.equal(r.showDot, false); }); - it("never reports mentions on an errored observation", () => { + it("never reports mentions or dot on an errored observation", () => { const r = workspaceRailIndicators({ hasUnread: true, count: 2, state: "error", }); assert.equal(r.showBadge, false); + assert.equal(r.showDot, false); assert.equal(r.mentionCount, 0); assert.equal(r.pending, false); }); diff --git a/desktop/src/features/sidebar/ui/WorkspaceRail.tsx b/desktop/src/features/sidebar/ui/WorkspaceRail.tsx index 12984f95e0..2d6b98cbd4 100644 --- a/desktop/src/features/sidebar/ui/WorkspaceRail.tsx +++ b/desktop/src/features/sidebar/ui/WorkspaceRail.tsx @@ -45,21 +45,29 @@ export function workspaceInitials(name: string): string { /** * Presentation decisions for one workspace button, derived from its observed * mention state. Pure so it can be unit-tested without a DOM. The `state` guard - * ensures we NEVER render a mention badge for a relay we could not observe + * ensures we NEVER render any indicator for a relay we could not observe * (`unknown`/`loading`/`error`) — only a `ready` observation is trusted. + * + * Two-tier indicator system: + * - `showBadge`: numeric mention count (mentions/thread-replies present). + * - `showDot`: plain unread dot when there are regular channel unreads but no + * mentions. Mutually exclusive with `showBadge` by construction. */ export function workspaceRailIndicators(unread: WorkspaceUnreadState): { mentionCount: number; showBadge: boolean; + showDot: boolean; pending: boolean; badgeLabel: string; } { const observed = unread.state === "ready"; const mentionCount = observed ? (unread.count ?? 0) : 0; const showBadge = mentionCount > 0; + const showDot = observed && unread.hasUnread && !showBadge; return { mentionCount, showBadge, + showDot, pending: unread.state === "unknown" || unread.state === "loading", badgeLabel: mentionCount > MAX_BADGE ? `${MAX_BADGE}+` : String(mentionCount), @@ -81,12 +89,14 @@ function WorkspaceButton({ onSwitch: () => void; menu: React.ReactNode; }) { - const { mentionCount, showBadge, pending, badgeLabel } = + const { mentionCount, showBadge, showDot, pending, badgeLabel } = workspaceRailIndicators(unread); const tooltipLabel = showBadge ? `${workspace.name} — ${mentionCount} mention${mentionCount === 1 ? "" : "s"}` - : workspace.name; + : showDot + ? `${workspace.name} — unread` + : workspace.name; return ( @@ -129,6 +139,13 @@ function WorkspaceButton({ > {badgeLabel} + ) : showDot ? ( + + unread + ) : null}