From cf585cdf873256cd86c2932cf26d4667c3c06d34 Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Wed, 18 Mar 2026 21:05:40 -0700 Subject: [PATCH] Unread DMs are always red. Closes #86. --- frontend/AGENTS.md | 4 +- frontend/src/components/Sidebar.tsx | 106 +++++++++++++++------------- frontend/src/test/sidebar.test.tsx | 26 ++++++- 3 files changed, 84 insertions(+), 52 deletions(-) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 966a71d..0f0d016 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -423,9 +423,9 @@ PYTHONPATH=. uv run pytest tests/ -v ## Errata & Known Non-Issues -### Contacts rollup uses mention styling for unread DMs +### Contacts use mention styling for unread DMs -This is intentional. In the sidebar section headers, unread direct messages are treated as mention-equivalent, so the Contacts rollup uses the highlighted mention-style badge for any unread DM. Row-level mention detection remains separate; this note is only about the section summary styling. +This is intentional. In the sidebar, unread direct messages for actual contact conversations are treated as mention-equivalent for badge styling. That means both the Contacts section header and contact unread badges themselves use the highlighted mention-style colors for unread DMs, including when those contacts appear in Favorites. Repeaters do not inherit this rule, and channel badges still use mention styling only for real `@[name]` mentions. ### RawPacketList always scrolls to bottom diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 3d5bb2e..f5d2309 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -517,57 +517,65 @@ export function Sidebar({ contact, }); - const renderConversationRow = (row: ConversationRow) => ( -
0 && '[&_.name]:font-semibold [&_.name]:text-foreground' - )} - role="button" - tabIndex={0} - aria-current={isActive(row.type, row.id) ? 'page' : undefined} - onKeyDown={handleKeyboardActivate} - onClick={() => - handleSelectConversation({ - type: row.type, - id: row.id, - name: row.name, - }) - } - > - {row.type === 'contact' && row.contact && ( - - )} - {row.name} - - {row.notificationsEnabled && ( - - - + const renderConversationRow = (row: ConversationRow) => { + const highlightUnread = + row.isMention || + (row.type === 'contact' && + row.contact?.type !== CONTACT_TYPE_REPEATER && + row.unreadCount > 0); + + return ( +
0 && '[&_.name]:font-semibold [&_.name]:text-foreground' )} - {row.unreadCount > 0 && ( - - {row.unreadCount} - + role="button" + tabIndex={0} + aria-current={isActive(row.type, row.id) ? 'page' : undefined} + onKeyDown={handleKeyboardActivate} + onClick={() => + handleSelectConversation({ + type: row.type, + id: row.id, + name: row.name, + }) + } + > + {row.type === 'contact' && row.contact && ( + )} - -
- ); + {row.name} + + {row.notificationsEnabled && ( + + + + )} + {row.unreadCount > 0 && ( + + {row.unreadCount} + + )} + +
+ ); + }; const renderSidebarActionRow = ({ key, diff --git a/frontend/src/test/sidebar.test.tsx b/frontend/src/test/sidebar.test.tsx index e70f63b..dde9e63 100644 --- a/frontend/src/test/sidebar.test.tsx +++ b/frontend/src/test/sidebar.test.tsx @@ -129,7 +129,7 @@ describe('Sidebar section summaries', () => { ); }); - it('keeps contact row badges normal while the contacts rollup is always red', () => { + it('turns contact row badges red while the contacts rollup remains red', () => { const { aliceName } = renderSidebar(); expect(within(getSectionHeaderContainer('Contacts')).getByText('3')).toHaveClass( @@ -140,6 +140,30 @@ describe('Sidebar section summaries', () => { const aliceRow = screen.getByText(aliceName).closest('div'); if (!aliceRow) throw new Error('Missing Alice row'); expect(within(aliceRow).getByText('3')).toHaveClass( + 'bg-badge-mention', + 'text-badge-mention-foreground' + ); + }); + + it('turns favorite contact row badges red', () => { + const { aliceName } = renderSidebar({ + favorites: [{ type: 'contact', id: '11'.repeat(32) }], + }); + + const aliceRow = screen.getByText(aliceName).closest('div'); + if (!aliceRow) throw new Error('Missing Alice row'); + expect(within(aliceRow).getByText('3')).toHaveClass( + 'bg-badge-mention', + 'text-badge-mention-foreground' + ); + }); + + it('keeps repeater row badges neutral', () => { + renderSidebar(); + + const relayRow = screen.getByText('Relay').closest('div'); + if (!relayRow) throw new Error('Missing Relay row'); + expect(within(relayRow).getByText('4')).toHaveClass( 'bg-badge-unread/90', 'text-badge-unread-foreground' );