A room unmuted on the account stops being drawn as muted: the buffer merge takes the flags the daemon leaves out as cleared; an unmute the server did not take is reported

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
2026-10-09 15:37:39 -04:00
co-authored by Claude Sonnet 5.5
parent 0dc048cdc0
commit 95f1784dbc
3 changed files with 53 additions and 2 deletions
+1 -1
Submodule nobilis updated: 5444b7a4ef...d1bbcaf724
+31
View File
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import { ChatStore } from './store'
/** The store, with a conversation already in its list, and the daemon's update for it applied. */
function after(update: Record<string, unknown>, before: Record<string, unknown> = {}): Record<string, unknown> {
const store = new ChatStore() as unknown as {
state: { buffers: unknown[] }
handleBufferListChange: (data: unknown) => void
}
const base = { id: 'm|!r', accountId: 'm', kind: 'channel', name: '!r', lastActivityTs: 0, unread: 0, highlight: false, mentions: 0 }
store.state = { ...store.state, buffers: [{ ...base, ...before }] }
// What the daemon sends carries no unread tally: that is this window's own.
store.handleBufferListChange({ id: base.id, accountId: base.accountId, kind: base.kind, name: base.name, lastActivityTs: 0, ...update })
return store.state.buffers[0] as Record<string, unknown>
}
describe('an update to a conversation the window already has', () => {
it('takes a mute off when the daemon leaves it out', () => {
// The daemon omits a flag that is off, so absent has to mean cleared: a
// room unmuted on the account stayed drawn as muted, with an Unmute that
// had nothing left to do.
expect(after({}, { serverMuted: true }).serverMuted).toBe(false)
expect(after({ serverMuted: true }, {}).serverMuted).toBe(true)
})
it('clears the other flags the same way, and keeps the unread count it holds', () => {
const b = after({}, { favourite: true, lowPriority: true, readOnly: 'No permission', forum: true, unread: 4, mentions: 2, highlight: true })
expect([b.favourite, b.lowPriority, b.readOnly, b.forum]).toEqual([false, false, undefined, false])
expect([b.unread, b.mentions, b.highlight]).toEqual([4, 2, true])
})
})
+21 -1
View File
@@ -3731,8 +3731,28 @@ export class ChatStore {
// leaves an optional field out when it is not set, so a plain spread
// kept the last value it ever had - a room that reconnected stayed
// marked as interrupted, and the banner over it stayed up.
//
// The same for every flag the daemon leaves out when it is off - muted on
// the account, favourite, low priority, read-only, a forum. A merge that
// kept the last value it ever had left a room unmuted on the server still
// drawn as muted here, with an Unmute that had nothing left to do.
this.set({
buffers: buffers.map((b) => (b.id === data.id ? { ...b, ...data, link: data.link, syncing: data.syncing, topic: data.topic } : b))
buffers: buffers.map((b) =>
b.id === data.id
? {
...b,
...data,
link: data.link,
syncing: data.syncing,
topic: data.topic,
serverMuted: data.serverMuted ?? false,
favourite: data.favourite ?? false,
lowPriority: data.lowPriority ?? false,
readOnly: data.readOnly,
forum: data.forum ?? false
}
: b
)
})
this.followPeekJoin(data)
return