diff --git a/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.test.ts b/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.test.ts index aa0c84516..869bd66fe 100644 --- a/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.test.ts +++ b/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.test.ts @@ -97,6 +97,51 @@ describe('background read state reducer', () => { expect(recorder.total).toBe(1); }); + test('a leftover mention in a channel the account can no longer see is not counted, as in the foreground', () => { + reducer.applyReady( + readyPayload({ + private_channels: [{id: 'dm-open', type: ChannelTypes.DM}], + read_states: [ + {id: 'dm-closed', mention_count: 1}, + {id: 'left-guild-channel', mention_count: 1}, + {id: 'dm-open', mention_count: 2}, + ], + }), + ); + + expect([...recorder.counts]).toEqual([['dm-open', 2]]); + }); + + test('a leftover mention appears once its guild arrives and its channel is known', () => { + reducer.applyReady( + readyPayload({ + guilds: [{id: 'guild-9', unavailable: true}], + read_states: [{id: 'late-1', mention_count: 2}], + }), + ); + expect(recorder.total).toBe(0); + + reducer.applyDispatch('GUILD_CREATE', {id: 'guild-9', channels: [{id: 'late-1'}], members: []}); + + expect(recorder.counts.get('late-1')).toBe(2); + }); + + test('a reopened direct message brings its leftover mention back', () => { + reducer.applyReady(readyPayload({read_states: [{id: 'dm-closed', mention_count: 1}]})); + + reducer.applyDispatch('CHANNEL_CREATE', {id: 'dm-closed', type: ChannelTypes.DM}); + + expect(recorder.counts.get('dm-closed')).toBe(1); + }); + + test('an acknowledgement for a channel the account cannot see is not counted', () => { + reducer.applyReady(readyPayload()); + + reducer.applyDispatch('MESSAGE_ACK', {channel_id: 'gone', message_id: 'm', mention_count: 1}); + + expect(recorder.total).toBe(0); + }); + test('a muted guild still contributes an explicit mention, exactly as the foreground badge does', () => { reducer.applyReady(readyPayload({user_guild_settings: [{guild_id: 'guild-1', muted: true}]})); diff --git a/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.ts b/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.ts index 1ca281021..f739ac875 100644 --- a/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.ts +++ b/fluxer_app/src/features/gateway/transport/BackgroundReadStateReducer.ts @@ -258,7 +258,13 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink { this.storeMentionCount(channelId, mentionCount); } } - this.observeMentionCounts(new Map(this.mentionCounts), BackgroundMentionCountMode.REPLACE); + const visible = new Map(); + for (const [channelId, mentionCount] of this.mentionCounts) { + if (this.channelDetails.has(channelId)) { + visible.set(channelId, mentionCount); + } + } + this.observeMentionCounts(visible, BackgroundMentionCountMode.REPLACE); } applyDispatch(type: string, data: unknown): void { @@ -339,21 +345,20 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink { return; } this.storeGuildDetails(guild); - for (const entry of asArray(guild.channels)) { + const revealed = new Map(); + for (const entry of [...asArray(guild.channels), ...asArray(guild.threads)]) { const channel = asRecord(entry); const channelId = asString(channel?.id); if (channel !== null && channelId !== null) { this.linkChannel(guildId, channelId); - this.storeChannelDetails(channelId, channel); + const mentionCount = this.storeChannelDetails(channelId, channel); + if (mentionCount > 0) { + revealed.set(channelId, mentionCount); + } } } - for (const entry of asArray(guild.threads)) { - const thread = asRecord(entry); - const threadId = asString(thread?.id); - if (thread !== null && threadId !== null) { - this.linkChannel(guildId, threadId); - this.storeChannelDetails(threadId, thread); - } + if (revealed.size > 0) { + this.observeMentionCounts(revealed, BackgroundMentionCountMode.MERGE); } for (const entry of asArray(guild.members)) { const member = asRecord(entry); @@ -402,12 +407,21 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink { if (guildId !== null) { this.linkChannel(guildId, channelId); } - this.storeChannelDetails(channelId, channel); + this.revealChannel(channelId, channel); } - private storeChannelDetails(channelId: string, channel: Record): void { + private revealChannel(channelId: string, channel: Record): void { + const mentionCount = this.storeChannelDetails(channelId, channel); + if (mentionCount > 0) { + this.observeMentionCounts(new Map([[channelId, mentionCount]]), BackgroundMentionCountMode.MERGE); + } + } + + private storeChannelDetails(channelId: string, channel: Record): number { this.assertChannelCapacity(this.channelDetails, channelId); + const known = this.channelDetails.has(channelId); this.channelDetails.set(channelId, readChannelDetails(channel)); + return known ? 0 : (this.mentionCounts.get(channelId) ?? 0); } private storeRelationship(value: unknown): void { @@ -448,6 +462,9 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink { if (guildId !== null) { this.linkChannel(guildId, channelId); } + if (!this.channelDetails.has(channelId)) { + this.revealChannel(channelId, {}); + } const muted = this.isGuildOrChannelMuted(guildId, channelId); const authorBlocked = authorId !== null && this.blockedUserIds.has(authorId); const mentioned = this.messageMentionsUser(message, guildId, muted, authorBlocked); @@ -637,6 +654,9 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink { } else { this.mentionCounts.delete(channelId); } + if (mentionCount > 0 && !this.channelDetails.has(channelId)) { + return; + } this.observeMentionCounts(new Map([[channelId, mentionCount]]), BackgroundMentionCountMode.MERGE); }