Fix GM/DM unread threads highlighting all teams as unreads (#8905)

* Fix GM/DM unread threads highlighting all teams as unreads

* Use options object instead of boolean flags

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Daniel Espino García 2025-06-10 15:30:58 +02:00 committed by GitHub
parent 1a695a1273
commit 9fcc6e580d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 202 additions and 22 deletions

View file

@ -249,7 +249,7 @@ export async function processReceivedThreads(serverUrl: string, threads: Thread[
export async function markTeamThreadsAsRead(serverUrl: string, teamId: string, prepareRecordsOnly = false) {
try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const threads = await queryThreadsInTeam(database, teamId, true, true, true).fetch();
const threads = await queryThreadsInTeam(database, teamId, {onlyUnreads: true, hasReplies: true, isFollowing: true}).fetch();
const models = threads.map((thread) => thread.prepareUpdate((record) => {
record.unreadMentions = 0;
record.unreadReplies = 0;

View file

@ -6,7 +6,7 @@ import {switchMap} from 'rxjs/operators';
import {observeCurrentChannelId, observeCurrentTeamId} from '@queries/servers/system';
import {observeTeamLastChannelId} from '@queries/servers/team';
import {observeUnreadsAndMentionsInTeam} from '@queries/servers/thread';
import {observeUnreadsAndMentions} from '@queries/servers/thread';
import ThreadsButton from './threads_button';
@ -24,7 +24,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
),
unreadsAndMentions: currentTeamId.pipe(
switchMap(
(teamId) => observeUnreadsAndMentionsInTeam(database, teamId),
(teamId) => observeUnreadsAndMentions(database, {teamId, includeDmGm: true}),
),
),
};

View file

@ -9,7 +9,7 @@ import {MM_TABLES} from '@constants/database';
import DatabaseManager from '@database/manager';
import {observeAllMyChannelNotifyProps} from '@queries/servers/channel';
import {queryMyTeams} from '@queries/servers/team';
import {getIsCRTEnabled, observeThreadMentionCount, queryThreads, observeUnreadsAndMentionsInTeam} from '@queries/servers/thread';
import {getIsCRTEnabled, observeThreadMentionCount, queryThreads, observeUnreadsAndMentions} from '@queries/servers/thread';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
@ -41,7 +41,7 @@ export const subscribeServerUnreadAndMentions = (serverUrl: string, observer: Un
observeWithColumns(['is_unread', 'mentions_count']).
pipe(
combineLatestWith(observeAllMyChannelNotifyProps(server.database)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, true)),
combineLatestWith(observeUnreadsAndMentions(server.database, {includeDmGm: true})),
map$(([[myChannels, settings], {unreads, mentions}]) => ({myChannels, settings, threadUnreads: unreads, threadMentionCount: mentions})),
).
subscribe(observer);
@ -60,7 +60,7 @@ export const subscribeMentionsByServer = (serverUrl: string, observer: ServerUnr
query(Q.on(CHANNEL, Q.where('delete_at', Q.eq(0)))).
observeWithColumns(['mentions_count']).
pipe(
combineLatestWith(observeThreadMentionCount(server.database, undefined, true)),
combineLatestWith(observeThreadMentionCount(server.database, {includeDmGm: true})),
map$(([myChannels, threadMentionCount]) => ({myChannels, threadMentionCount})),
).
subscribe(observer.bind(undefined, serverUrl));
@ -79,7 +79,7 @@ export const subscribeUnreadAndMentionsByServer = (serverUrl: string, observer:
observeWithColumns(['mentions_count', 'is_unread']).
pipe(
combineLatestWith(observeAllMyChannelNotifyProps(server.database)),
combineLatestWith(observeUnreadsAndMentionsInTeam(server.database, undefined, true)),
combineLatestWith(observeUnreadsAndMentions(server.database, {includeDmGm: true})),
map$(([[myChannels, settings], {unreads, mentions}]) => ({myChannels, settings, threadUnreads: unreads, threadMentionCount: mentions})),
).
subscribe(observer.bind(undefined, serverUrl));
@ -108,7 +108,7 @@ export const getTotalMentionsForServer = async (serverUrl: string) => {
let includeDmGm = true;
const myTeamIds = await queryMyTeams(database).fetchIds();
for await (const teamId of myTeamIds) {
const threads = await queryThreads(database, teamId, false, includeDmGm).extend(
const threads = await queryThreads(database, {teamId, includeDmGm}).extend(
Q.where('unread_mentions', Q.gt(0)),
).fetch();
includeDmGm = false;

View file

@ -4,7 +4,7 @@
/* eslint-disable max-lines */
import {processReceivedThreads} from '@actions/local/thread';
import {Config, Preferences, Screens} from '@constants';
import {ActionType, Config, Preferences, Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import TestHelper from '@test/test_helper';
@ -1219,6 +1219,146 @@ describe('Team Queries', () => {
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
it('should not consider the team unread when a gm or a dm are unread', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'all' as const};
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
// Setup DM channel with unreads
let channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'dm1', team_id: '', type: 'D', total_msg_count: 30})],
[TestHelper.fakeMyChannel({
channel_id: 'dm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
await operator.batchRecords([...channelModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
// Setup GM channel with unreads
channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'gm1', team_id: '', type: 'G', total_msg_count: 30})],
[TestHelper.fakeMyChannel({
channel_id: 'gm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
await operator.batchRecords([...channelModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
it('should not consider the team unread if a thread in a dm or a gm has unread messages', async () => {
const subscriptionNext = jest.fn();
const notify_props = {mark_unread: 'all' as const};
const result = observeIsTeamUnread(database, teamId);
result.subscribe({next: subscriptionNext});
await TestHelper.wait(waitTime);
// The subscription always return the first value
expect(subscriptionNext).toHaveBeenCalledWith(false);
subscriptionNext.mockClear();
// Setup DM channel with thread
let channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'dm1', team_id: '', type: 'D', total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: 'dm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
const dmPost = TestHelper.fakePost({id: 'dm_thread', channel_id: 'dm1'});
const dmThreads = [{
...TestHelper.fakeThread({
id: 'dm_thread',
participants: undefined,
reply_count: 2,
last_reply_at: 123,
is_following: true,
unread_replies: 2,
unread_mentions: 1,
}),
lastFetchedAt: 0,
}];
const dmModels = await operator.handleThreads({threads: dmThreads, prepareRecordsOnly: false});
let postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [],
posts: [dmPost],
prepareRecordsOnly: true,
});
await operator.batchRecords([...channelModels, ...dmModels, ...postModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
// Setup GM channel with thread
channelModels = (await Promise.all((await prepareAllMyChannels(
operator,
[TestHelper.fakeChannel({id: 'gm1', team_id: '', type: 'G', total_msg_count: 20})],
[TestHelper.fakeMyChannel({
channel_id: 'gm1',
user_id: userId,
notify_props,
msg_count: 20,
})],
false,
)))).flat();
const gmPost = TestHelper.fakePost({id: 'gm_thread', channel_id: 'gm1'});
const gmThreads = [{
...TestHelper.fakeThread({
id: 'gm_thread',
participants: undefined,
reply_count: 3,
last_reply_at: 123,
is_following: true,
unread_replies: 3,
unread_mentions: 2,
}),
lastFetchedAt: 123,
}];
const gmModels = await operator.handleThreads({threads: gmThreads, prepareRecordsOnly: false});
postModels = await operator.handlePosts({
actionType: ActionType.POSTS.RECEIVED_IN_CHANNEL,
order: [],
posts: [gmPost],
prepareRecordsOnly: true,
});
await operator.batchRecords([...channelModels, ...gmModels, ...postModels], 'test');
await TestHelper.wait(waitTime);
expect(subscriptionNext).not.toHaveBeenCalled();
});
});
describe('observeSortedJoinedTeams', () => {

View file

@ -14,7 +14,7 @@ import {prepareDeleteCategory} from './categories';
import {prepareDeleteChannel, getDefaultChannelForTeam, observeMyChannelMentionCount, observeMyChannelUnreads} from './channel';
import {queryPreferencesByCategoryAndName} from './preference';
import {patchTeamHistory, getConfig, getTeamHistory, observeCurrentTeamId, getCurrentTeamId} from './system';
import {observeThreadMentionCount, observeUnreadsAndMentionsInTeam} from './thread';
import {observeThreadMentionCount, observeUnreadsAndMentions} from './thread';
import {getCurrentUser} from './user';
import type {MyChannelModel} from '@database/models/server';
@ -416,7 +416,7 @@ export const observeCurrentTeam = (database: Database) => {
export function observeMentionCount(database: Database, teamId?: string, includeDmGm?: boolean): Observable<number> {
const channelMentionCountObservable = observeMyChannelMentionCount(database, teamId);
const threadMentionCountObservable = observeThreadMentionCount(database, teamId, includeDmGm);
const threadMentionCountObservable = observeThreadMentionCount(database, {teamId, includeDmGm});
return channelMentionCountObservable.pipe(
combineLatestWith(threadMentionCountObservable),
@ -427,7 +427,7 @@ export function observeMentionCount(database: Database, teamId?: string, include
export function observeIsTeamUnread(database: Database, teamId: string): Observable<boolean> {
const channelUnreads = observeMyChannelUnreads(database, teamId);
const threadsUnreadsAndMentions = observeUnreadsAndMentionsInTeam(database, teamId);
const threadsUnreadsAndMentions = observeUnreadsAndMentions(database, {teamId});
return channelUnreads.pipe(
combineLatestWith(threadsUnreadsAndMentions),

View file

@ -87,8 +87,16 @@ export const observeTeamIdByThread = (database: Database, thread: ThreadModel) =
return observeTeamIdByThreadId(database, thread.id);
};
export const observeUnreadsAndMentionsInTeam = (database: Database, teamId?: string, includeDmGm?: boolean): Observable<{unreads: boolean; mentions: number}> => {
const observeThreads = () => queryThreads(database, teamId, true, includeDmGm).
type ObserveUnreadsAndMentionsOptions = {
teamId?: string;
includeDmGm?: boolean;
};
export const observeUnreadsAndMentions = (database: Database, {
teamId,
includeDmGm,
}: ObserveUnreadsAndMentionsOptions): Observable<{unreads: boolean; mentions: number}> => {
const observeThreads = () => queryThreads(database, {teamId, onlyUnreads: true, includeDmGm}).
observeWithColumns(['unread_replies', 'unread_mentions']).
pipe(
switchMap((threads) => {
@ -154,7 +162,21 @@ export const prepareThreadsFromReceivedPosts = async (operator: ServerDataOperat
return models;
};
export const queryThreadsInTeam = (database: Database, teamId: string, onlyUnreads?: boolean, hasReplies?: boolean, isFollowing?: boolean, sort?: boolean, earliest?: number): Query<ThreadModel> => {
type QueryThreadsInTeamOptions = {
onlyUnreads?: boolean;
hasReplies?: boolean;
isFollowing?: boolean;
sort?: boolean;
earliest?: number;
};
export const queryThreadsInTeam = (database: Database, teamId: string, {
onlyUnreads,
hasReplies,
isFollowing,
sort,
earliest,
}: QueryThreadsInTeamOptions): Query<ThreadModel> => {
const query: Q.Clause[] = [
Q.experimentalNestedJoin(POST, CHANNEL),
Q.on(POST, Q.on(CHANNEL, Q.where('delete_at', 0))),
@ -193,14 +215,32 @@ export const queryTeamThreadsSync = (database: Database, teamId: string) => {
);
};
export function observeThreadMentionCount(database: Database, teamId?: string, includeDmGm?: boolean): Observable<number> {
return observeUnreadsAndMentionsInTeam(database, teamId, includeDmGm).pipe(
type ObserveThreadMentionCountOptions = {
teamId?: string;
includeDmGm?: boolean;
};
export function observeThreadMentionCount(database: Database, {
teamId,
includeDmGm,
}: ObserveThreadMentionCountOptions): Observable<number> {
return observeUnreadsAndMentions(database, {teamId, includeDmGm}).pipe(
switchMap(({mentions}) => of$(mentions)),
distinctUntilChanged(),
);
}
export const queryThreads = (database: Database, teamId?: string, onlyUnreads = false, includeDmGm = true): Query<ThreadModel> => {
type QueryThreadsOptions = {
teamId?: string;
onlyUnreads?: boolean;
includeDmGm?: boolean;
};
export const queryThreads = (database: Database, {
teamId,
onlyUnreads,
includeDmGm,
}: QueryThreadsOptions): Query<ThreadModel> => {
const query: Q.Clause[] = [
Q.where('is_following', true),
Q.where('reply_count', Q.gt(0)),

View file

@ -14,7 +14,7 @@ import type {WithDatabaseArgs} from '@typings/database/database';
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
const teamId = observeCurrentTeamId(database);
const unreadsCount = teamId.pipe(switchMap((id) => queryThreadsInTeam(database, id, true, true, true).observeCount(false)));
const unreadsCount = teamId.pipe(switchMap((id) => queryThreadsInTeam(database, id, {onlyUnreads: true, hasReplies: true, isFollowing: true}).observeCount(false)));
const hasUnreads = unreadsCount.pipe(
switchMap((count) => of$(count > 0)),
distinctUntilChanged(),

View file

@ -31,7 +31,7 @@ const enhanced = withObservables(['tab', 'teamId'], ({database, tab, teamId}: Pr
threads: teamThreadsSyncObserver.pipe(
switchMap((teamThreadsSync) => {
const earliest = tab === 'all' ? teamThreadsSync?.[0]?.earliest : 0;
return queryThreadsInTeam(database, teamId, getOnlyUnreads, true, true, true, earliest).observe();
return queryThreadsInTeam(database, teamId, {onlyUnreads: getOnlyUnreads, hasReplies: true, isFollowing: true, sort: true, earliest}).observe();
}),
),
};

View file

@ -11,7 +11,7 @@ import {filterAndSortMyChannels, makeChannelsMap} from '@helpers/database';
import {getChannelById, observeChannelsByLastPostAt, observeNotifyPropsByChannels, queryMyChannelUnreads} from '@queries/servers/channel';
import {querySidebarPreferences} from '@queries/servers/preference';
import {observeLastUnreadChannelId} from '@queries/servers/system';
import {observeUnreadsAndMentionsInTeam} from '@queries/servers/thread';
import {observeUnreadsAndMentions} from '@queries/servers/thread';
import UnreadCategories from './unreads';
@ -62,7 +62,7 @@ const enhanced = withObservables(['currentTeamId', 'isTablet', 'onlyUnreads'], (
}
return of$([]);
}));
const unreadThreads = observeUnreadsAndMentionsInTeam(database, currentTeamId, true);
const unreadThreads = observeUnreadsAndMentions(database, {teamId: currentTeamId, includeDmGm: true});
return {
unreadChannels,