fix(desktop): add unread dot to workspace rail for channel unreads (#1637)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-07-08 13:17:22 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 9683ef8079
commit f968601ef6
2 changed files with 31 additions and 8 deletions
@@ -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);
});
@@ -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 (
<ContextMenu>
@@ -129,6 +139,13 @@ function WorkspaceButton({
>
{badgeLabel}
</span>
) : showDot ? (
<span
className="absolute -bottom-0.5 -right-0.5 h-2 w-2 shrink-0 rounded-full bg-primary ring-2 ring-sidebar"
data-testid={`workspace-rail-unread-dot-${workspace.id}`}
>
<span className="sr-only">unread</span>
</span>
) : null}
</button>
</ContextMenuTrigger>