fix: exclude voice channels from unread computation to eliminate ghost notifications

Voice channels rendered with VoiceChannel component have no text reading/acking
UI, so messages in them created phantom unread indicators on the space sidebar
that users could never clear. Root cause was a message in the counter-strike
voice channel with no read state.

Three-layer fix:
- spaceStore: track voiceChannelIds set, exclude voice channels from
  channelLastMessageIds so setReadStates never marks them unread
- useWebSocket: skip markChannelUnread for voice channels on message_created,
  prune orphaned unreads on every ready event
- chatStore: validate preserved unreads against channelToSpaceMap to drop
  orphans that don't map to any known channel
This commit is contained in:
Jannis Braun
2026-03-16 23:57:24 +01:00
parent bf64c4678b
commit bc4dd81632
3 changed files with 40 additions and 5 deletions
+10 -1
View File
@@ -38,6 +38,7 @@ interface SpaceState {
spacePermissions: Map<string, string>; // spaceId → myPermissions decimal string
channelPermissions: Map<string, string>; // channelId → myPermissions decimal string
channelOriginMap: Map<string, string>; // channelId → instance origin ('' = home)
voiceChannelIds: Set<string>; // channelIds that are voice channels (excluded from unread)
categoryOriginMap: Map<string, string>; // categoryId → instance origin ('' = home)
_layoutUpdatedAt: number;
setSpaces: (spaces: TaggedSpace[]) => void;
@@ -127,6 +128,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
spacePermissions: new Map(),
channelPermissions: new Map(),
channelOriginMap: new Map(),
voiceChannelIds: new Set(),
categoryOriginMap: new Map(),
_layoutUpdatedAt: 0,
@@ -147,6 +149,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
spacePermissions: new Map(),
channelPermissions: new Map(),
channelOriginMap: new Map(),
voiceChannelIds: new Set(),
categoryOriginMap: new Map(),
_layoutUpdatedAt: 0,
});
@@ -576,6 +579,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
const spacePermissions = new Map(get().spacePermissions);
const channelPermissions = new Map(get().channelPermissions);
const channelOriginMap = new Map(get().channelOriginMap);
const voiceChannelIds = new Set(get().voiceChannelIds);
const categoryOriginMap = new Map(get().categoryOriginMap);
// If home, clear home-origin entries first to avoid stale data
@@ -586,6 +590,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
channelLastMessageIds.delete(key);
channelPermissions.delete(key);
channelOriginMap.delete(key);
voiceChannelIds.delete(key);
}
}
// Also clear server permissions for this origin
@@ -602,6 +607,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
channelLastMessageIds.delete(key);
channelPermissions.delete(key);
channelOriginMap.delete(key);
voiceChannelIds.delete(key);
}
}
for (const s of get().spaces) {
@@ -619,7 +625,9 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
for (const ch of srv.channels) {
channelToSpaceMap.set(ch.id, srv.id);
channelOriginMap.set(ch.id, origin);
if (ch.lastMessageId) {
if (ch.type === 'voice') {
voiceChannelIds.add(ch.id);
} else if (ch.lastMessageId) {
channelLastMessageIds.set(ch.id, ch.lastMessageId);
}
if (ch.myPermissions) {
@@ -663,6 +671,7 @@ export const useSpaceStore = create<SpaceState>((set, get) => ({
spacePermissions,
channelPermissions,
channelOriginMap,
voiceChannelIds,
categoryOriginMap,
};