Private
Public Access
Fix desktop tray unread indicator: clear only on genuine room entry
The tray indicator must stay visible while a message is unread and clear only when the user actually opens the room containing it -- never merely because the window was shown, focused, restored, or opened from the tray. Adds window.dsDesktop.roomOpened() (called exactly once per genuine transition to a real room id, via useDesktopRoomBridge) and wires up window.dsDesktop.setUnreadCount() (declared but never actually called before this) to fire whenever the unread-room count changes. Both are no-ops in browser builds, matching this bridge's existing optional- chaining convention. Frontend unread state stays authoritative -- clearRoomIndicators is pulled out of ChatShellPage as a pure function (same behavior, now directly testable) so opening one room only clears that room's unread/mention flags, leaving every other room's state untouched. Also adds a frontend test runner (vitest + React Testing Library) -- none existed in this project before. 10 new tests cover: roomOpened firing once on entry, not refiring on an unrelated rerender with the same room id, refiring on a genuine room-to-room transition, never firing with no room id, setUnreadCount firing independently of roomOpened, clearing one room preserving every other room's unread state, and the desktop bridge remaining fully optional. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
047f47d524
commit
3349cadea2
9 files changed
+1155
-16
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { MyRoomItem } from '../types'
|
||||
import { clearRoomIndicators } from './roomIndicators'
|
||||
|
||||
function makeRoom(overrides: Partial<MyRoomItem> & { id: string }): MyRoomItem {
|
||||
return {
|
||||
name: overrides.id,
|
||||
description: null,
|
||||
is_private: false,
|
||||
is_dm: false,
|
||||
is_archived: false,
|
||||
owner_id: 'owner',
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
role: 'member',
|
||||
has_unread: false,
|
||||
has_mention: false,
|
||||
dm_partner: null,
|
||||
email_notifications: false,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('clearRoomIndicators', () => {
|
||||
it('clears has_unread and has_mention on the matching room', () => {
|
||||
const rooms = [makeRoom({ id: 'room-1', has_unread: true, has_mention: true })]
|
||||
const result = clearRoomIndicators(rooms, 'room-1')
|
||||
expect(result[0].has_unread).toBe(false)
|
||||
expect(result[0].has_mention).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves unread state on every other room', () => {
|
||||
const rooms = [
|
||||
makeRoom({ id: 'room-1', has_unread: true, has_mention: true }),
|
||||
makeRoom({ id: 'room-2', has_unread: true, has_mention: false }),
|
||||
makeRoom({ id: 'room-3', has_unread: false, has_mention: false }),
|
||||
]
|
||||
const result = clearRoomIndicators(rooms, 'room-1')
|
||||
const room2 = result.find((r) => r.id === 'room-2')
|
||||
const room3 = result.find((r) => r.id === 'room-3')
|
||||
expect(room2?.has_unread).toBe(true)
|
||||
expect(room3?.has_unread).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves untouched rooms as the same object reference', () => {
|
||||
const room2 = makeRoom({ id: 'room-2', has_unread: true })
|
||||
const rooms = [makeRoom({ id: 'room-1', has_unread: true }), room2]
|
||||
const result = clearRoomIndicators(rooms, 'room-1')
|
||||
expect(result[1]).toBe(room2)
|
||||
})
|
||||
|
||||
it('is a no-op if the room id is not found', () => {
|
||||
const rooms = [makeRoom({ id: 'room-1', has_unread: true })]
|
||||
const result = clearRoomIndicators(rooms, 'does-not-exist')
|
||||
expect(result[0].has_unread).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user