fix(app): ignore leftover mentions in unseen channels (#3290)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 17:35:59 +02:00
1 parent d6e3254a16
commit 4da8dec4ac
2 files changed
+76 -11

No files matched your search

@@ -97,6 +97,51 @@ describe('background read state reducer', () => {
expect(recorder.total).toBe(1); 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', () => { 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}]})); reducer.applyReady(readyPayload({user_guild_settings: [{guild_id: 'guild-1', muted: true}]}));
@@ -258,7 +258,13 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink {
this.storeMentionCount(channelId, mentionCount); this.storeMentionCount(channelId, mentionCount);
} }
} }
this.observeMentionCounts(new Map(this.mentionCounts), BackgroundMentionCountMode.REPLACE); const visible = new Map<string, number>();
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 { applyDispatch(type: string, data: unknown): void {
@@ -339,21 +345,20 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink {
return; return;
} }
this.storeGuildDetails(guild); this.storeGuildDetails(guild);
for (const entry of asArray(guild.channels)) { const revealed = new Map<string, number>();
for (const entry of [...asArray(guild.channels), ...asArray(guild.threads)]) {
const channel = asRecord(entry); const channel = asRecord(entry);
const channelId = asString(channel?.id); const channelId = asString(channel?.id);
if (channel !== null && channelId !== null) { if (channel !== null && channelId !== null) {
this.linkChannel(guildId, channelId); 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)) { for (const entry of asArray(guild.members)) {
const member = asRecord(entry); const member = asRecord(entry);
@@ -402,12 +407,21 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink {
if (guildId !== null) { if (guildId !== null) {
this.linkChannel(guildId, channelId); this.linkChannel(guildId, channelId);
} }
this.storeChannelDetails(channelId, channel); this.revealChannel(channelId, channel);
} }
private storeChannelDetails(channelId: string, channel: Record<string, unknown>): void { private revealChannel(channelId: string, channel: Record<string, unknown>): void {
const mentionCount = this.storeChannelDetails(channelId, channel);
if (mentionCount > 0) {
this.observeMentionCounts(new Map([[channelId, mentionCount]]), BackgroundMentionCountMode.MERGE);
}
}
private storeChannelDetails(channelId: string, channel: Record<string, unknown>): number {
this.assertChannelCapacity(this.channelDetails, channelId); this.assertChannelCapacity(this.channelDetails, channelId);
const known = this.channelDetails.has(channelId);
this.channelDetails.set(channelId, readChannelDetails(channel)); this.channelDetails.set(channelId, readChannelDetails(channel));
return known ? 0 : (this.mentionCounts.get(channelId) ?? 0);
} }
private storeRelationship(value: unknown): void { private storeRelationship(value: unknown): void {
@@ -448,6 +462,9 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink {
if (guildId !== null) { if (guildId !== null) {
this.linkChannel(guildId, channelId); this.linkChannel(guildId, channelId);
} }
if (!this.channelDetails.has(channelId)) {
this.revealChannel(channelId, {});
}
const muted = this.isGuildOrChannelMuted(guildId, channelId); const muted = this.isGuildOrChannelMuted(guildId, channelId);
const authorBlocked = authorId !== null && this.blockedUserIds.has(authorId); const authorBlocked = authorId !== null && this.blockedUserIds.has(authorId);
const mentioned = this.messageMentionsUser(message, guildId, muted, authorBlocked); const mentioned = this.messageMentionsUser(message, guildId, muted, authorBlocked);
@@ -637,6 +654,9 @@ export class BackgroundReadStateReducer implements BackgroundSnapshotSink {
} else { } else {
this.mentionCounts.delete(channelId); this.mentionCounts.delete(channelId);
} }
if (mentionCount > 0 && !this.channelDetails.has(channelId)) {
return;
}
this.observeMentionCounts(new Map([[channelId, mentionCount]]), BackgroundMentionCountMode.MERGE); this.observeMentionCounts(new Map([[channelId, mentionCount]]), BackgroundMentionCountMode.MERGE);
} }