From 95f1784dbc5f2920722e169cc95e714455569962 Mon Sep 17 00:00:00 2001 From: Salastil Date: Fri, 9 Oct 2026 15:37:39 -0400 Subject: [PATCH] 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 --- nobilis | 2 +- src/renderer/src/state/store.test.ts | 31 ++++++++++++++++++++++++++++ src/renderer/src/state/store.ts | 22 +++++++++++++++++++- 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/renderer/src/state/store.test.ts diff --git a/nobilis b/nobilis index 5444b7a..d1bbcaf 160000 --- a/nobilis +++ b/nobilis @@ -1 +1 @@ -Subproject commit 5444b7a4ef9650e53b9f1e58c1c296087fce7fe2 +Subproject commit d1bbcaf724972450ffd759a8ba514414259bb869 diff --git a/src/renderer/src/state/store.test.ts b/src/renderer/src/state/store.test.ts new file mode 100644 index 0000000..53401cb --- /dev/null +++ b/src/renderer/src/state/store.test.ts @@ -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, before: Record = {}): Record { + 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 +} + +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]) + }) +}) diff --git a/src/renderer/src/state/store.ts b/src/renderer/src/state/store.ts index 647819c..f20509c 100644 --- a/src/renderer/src/state/store.ts +++ b/src/renderer/src/state/store.ts @@ -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