[MM-67507] Fix threads not being refetch when switching translations (#9512)
* [MM-67507] Fix threads not being refetch when switching translations * Fix test
This commit is contained in:
parent
9793383832
commit
058d080e9a
7 changed files with 178 additions and 13 deletions
|
|
@ -821,7 +821,7 @@ describe('updateMyChannelFromWebsocket', () => {
|
|||
|
||||
await updateMyChannelFromWebsocket(serverUrl, {...channelMember, autotranslation_disabled: true}, false);
|
||||
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId});
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId, teamId: channel.team_id});
|
||||
});
|
||||
|
||||
it('updates member autotranslation from channelMember', async () => {
|
||||
|
|
@ -1226,7 +1226,7 @@ describe('deletePostsForChannel', () => {
|
|||
expect(error).toBeFalsy();
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenCalledWith({serverUrl, channelId});
|
||||
expect(listener).toHaveBeenCalledWith({serverUrl, channelId, teamId: channel.team_id});
|
||||
|
||||
const myChannel = await getMyChannel(operator.database, channelId);
|
||||
expect(myChannel?.lastFetchedAt).toBe(0);
|
||||
|
|
|
|||
|
|
@ -540,7 +540,7 @@ export async function deletePostsForChannel(serverUrl: string, channelId: string
|
|||
|
||||
if (preparedModels.length && !prepareRecordsOnly) {
|
||||
await operator.batchRecords(preparedModels, 'deletePostsForChannel');
|
||||
DeviceEventEmitter.emit(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId});
|
||||
DeviceEventEmitter.emit(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId, teamId: channel.teamId});
|
||||
}
|
||||
|
||||
return {error: false, models: preparedModels};
|
||||
|
|
|
|||
|
|
@ -786,7 +786,7 @@ describe('setChannelAutotranslation', () => {
|
|||
expect(result.channel!.autotranslation).toBe(false);
|
||||
expect(mockClient.setChannelAutotranslation).toHaveBeenCalledWith(channelId, false);
|
||||
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId});
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId, teamId});
|
||||
});
|
||||
|
||||
it('should handle client error', async () => {
|
||||
|
|
@ -814,7 +814,7 @@ describe('setMyChannelAutotranslation', () => {
|
|||
expect(result.error).toBeUndefined();
|
||||
expect(result.data).toBe(true);
|
||||
expect(mockClient.setMyChannelAutotranslation).toHaveBeenCalledWith(channelId, false);
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId});
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(Events.POST_DELETED_FOR_CHANNEL, {serverUrl, channelId, teamId});
|
||||
});
|
||||
|
||||
it('should handle client error', async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import {waitFor} from '@testing-library/react-native';
|
||||
|
||||
import {ActionType} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
|
|
@ -20,8 +20,10 @@ import {
|
|||
loadEarlierThreads,
|
||||
fetchAndSwitchToThread,
|
||||
syncThreadsIfNeeded,
|
||||
batchTeamThreadSync,
|
||||
} from './thread';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
||||
const serverUrl = 'baseHandler.test.com';
|
||||
|
|
@ -62,14 +64,15 @@ const throwFunc = () => {
|
|||
throw Error('error');
|
||||
};
|
||||
|
||||
const mockClient = {
|
||||
getThread: jest.fn((userId: string, _teamId: string, threadId: string) => ({...thread1, id: threadId})),
|
||||
const partialClient: Partial<Client> = {
|
||||
getThread: jest.fn((userId: string, _teamId: string, threadId: string) => Promise.resolve({...thread1, id: threadId})),
|
||||
updateTeamThreadsAsRead: jest.fn(),
|
||||
markThreadAsRead: jest.fn(),
|
||||
markThreadAsUnread: jest.fn(),
|
||||
updateThreadFollow: jest.fn(),
|
||||
getThreads: jest.fn(() => ({threads})),
|
||||
getThreads: jest.fn(() => Promise.resolve({threads, total: threads.length, total_unread_mentions: 0, total_unread_threads: 0})),
|
||||
};
|
||||
const mockClient = partialClient as Client;
|
||||
|
||||
let mockGetIsCRTEnabled: jest.Mock;
|
||||
jest.mock('@queries/servers/thread', () => {
|
||||
|
|
@ -82,7 +85,7 @@ jest.mock('@queries/servers/thread', () => {
|
|||
});
|
||||
|
||||
beforeAll(() => {
|
||||
// eslint-disable-next-line
|
||||
|
||||
// @ts-ignore
|
||||
NetworkManager.getClient = () => mockClient;
|
||||
});
|
||||
|
|
@ -98,7 +101,7 @@ afterEach(async () => {
|
|||
|
||||
describe('get threads', () => {
|
||||
it('fetchThread - handle error', async () => {
|
||||
mockClient.getThread.mockImplementationOnce(jest.fn(throwFunc));
|
||||
jest.mocked(mockClient.getThread).mockImplementationOnce(jest.fn(throwFunc));
|
||||
const result = await fetchThread(serverUrl, teamId, thread1.id);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
|
|
@ -229,7 +232,7 @@ describe('get threads', () => {
|
|||
|
||||
describe('update threads', () => {
|
||||
it('updateTeamThreadsAsRead - handle error', async () => {
|
||||
mockClient.updateTeamThreadsAsRead.mockImplementationOnce(jest.fn(throwFunc));
|
||||
jest.mocked(mockClient.updateTeamThreadsAsRead).mockImplementationOnce(jest.fn(throwFunc));
|
||||
const result = await updateTeamThreadsAsRead(serverUrl, teamId);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.error).toBeDefined();
|
||||
|
|
@ -302,3 +305,109 @@ describe('update threads', () => {
|
|||
expect(result.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('batchTeamThreadSync', () => {
|
||||
// We don't use fake timers since they break the
|
||||
// database operations.
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(mockClient.getThreads).mockClear();
|
||||
});
|
||||
|
||||
const teamFilter = (id: string) => {
|
||||
return (call: unknown[]) => call[1] === id && call[6] === undefined;
|
||||
};
|
||||
|
||||
// fetchThreads calls twice the function, one for the unreads and one for
|
||||
// the latests. We expect that part to work correctly and if one is called
|
||||
// for unreads, the one for the latest is also called.
|
||||
const allCallsFilter = (call: unknown[]) => call[6] === undefined;
|
||||
|
||||
it('returns early when CRT is disabled', async () => {
|
||||
mockGetIsCRTEnabled.mockResolvedValueOnce(false);
|
||||
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
|
||||
await waitFor(() => expect(mockClient.getThreads).not.toHaveBeenCalled(), {timeout: 600});
|
||||
});
|
||||
|
||||
it('schedules sync after timeout when CRT is enabled', async () => {
|
||||
mockGetIsCRTEnabled.mockResolvedValue(true);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
|
||||
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
expect(mockClient.getThreads).not.toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
|
||||
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
|
||||
expect(getThreadsCalls.filter(teamFilter(teamId))).toHaveLength(1);
|
||||
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('batches multiple teamIds and syncs each after timeout', async () => {
|
||||
mockGetIsCRTEnabled.mockResolvedValue(true);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
|
||||
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
await batchTeamThreadSync(serverUrl, 'teamid2');
|
||||
expect(mockClient.getThreads).not.toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
|
||||
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
|
||||
expect(getThreadsCalls.filter(teamFilter(teamId))).toHaveLength(1);
|
||||
expect(getThreadsCalls.filter(teamFilter('teamid2'))).toHaveLength(1);
|
||||
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('calling several times for the same team id does not create multiple calls', async () => {
|
||||
mockGetIsCRTEnabled.mockResolvedValue(true);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
|
||||
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
|
||||
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
|
||||
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
|
||||
expect(getThreadsCalls.filter(teamFilter(teamId))).toHaveLength(1);
|
||||
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('includes direct channels when empty teamId is in batch only once', async () => {
|
||||
mockGetIsCRTEnabled.mockResolvedValue(true);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
|
||||
|
||||
await batchTeamThreadSync(serverUrl, '');
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
await batchTeamThreadSync(serverUrl, 'teamid2');
|
||||
await batchTeamThreadSync(serverUrl, 'teamid3');
|
||||
|
||||
await waitFor(() => expect(mockClient.getThreads).toHaveBeenCalled());
|
||||
expect(mockClient.getThreads).toHaveBeenCalled();
|
||||
const getThreadsCalls = jest.mocked(mockClient.getThreads).mock.calls;
|
||||
expect(getThreadsCalls.filter(teamFilter(teamId)).length).toBe(1);
|
||||
expect(getThreadsCalls.filter(teamFilter('teamid2')).length).toBe(1);
|
||||
expect(getThreadsCalls.filter(teamFilter('teamid3')).length).toBe(1);
|
||||
expect(getThreadsCalls.filter(allCallsFilter)).toHaveLength(3);
|
||||
expect(getThreadsCalls.filter((call) => call[10] === false && call[6] === undefined).length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs error when syncTeamThreads returns error', async () => {
|
||||
mockGetIsCRTEnabled.mockResolvedValue(true);
|
||||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
|
||||
const logErrorSpy = jest.spyOn(require('@utils/log'), 'logError').mockImplementation(() => {});
|
||||
jest.mocked(mockClient.getThreads).mockImplementationOnce(() => {
|
||||
throw new Error('sync failed');
|
||||
});
|
||||
|
||||
await batchTeamThreadSync(serverUrl, teamId);
|
||||
|
||||
await waitFor(() => expect(logErrorSpy).toHaveBeenCalled());
|
||||
expect(logErrorSpy).toHaveBeenCalledWith('batchTeamThreadSync: Error', expect.any(Error));
|
||||
logErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -530,3 +530,53 @@ export const loadEarlierThreads = async (serverUrl: string, teamId: string, last
|
|||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
const handlePostDeletedForChannelIfNeededBatch: Record<string, {
|
||||
timeout: NodeJS.Timeout | undefined;
|
||||
teamIds: Set<string>;
|
||||
}> = {};
|
||||
|
||||
const handlePostDeletedForChannelIfNeededBatchTimeout = 500;
|
||||
|
||||
export const batchTeamThreadSync = async (serverUrl: string, teamId: string) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const isCRTEnabled = await getIsCRTEnabled(database);
|
||||
if (!isCRTEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let existingBatch = handlePostDeletedForChannelIfNeededBatch[serverUrl];
|
||||
if (!existingBatch) {
|
||||
existingBatch = {
|
||||
timeout: undefined,
|
||||
teamIds: new Set(),
|
||||
};
|
||||
handlePostDeletedForChannelIfNeededBatch[serverUrl] = existingBatch;
|
||||
}
|
||||
|
||||
clearTimeout(existingBatch.timeout);
|
||||
|
||||
existingBatch.teamIds.add(teamId);
|
||||
|
||||
existingBatch.timeout = setTimeout(async () => {
|
||||
const batch = handlePostDeletedForChannelIfNeededBatch[serverUrl];
|
||||
if (!batch) {
|
||||
return;
|
||||
}
|
||||
delete handlePostDeletedForChannelIfNeededBatch[serverUrl];
|
||||
|
||||
const includeDirect = batch.teamIds.has('');
|
||||
const teamIds = Array.from(batch.teamIds).filter((t) => t !== '');
|
||||
const promises = teamIds.map((t, idx) => syncTeamThreads(serverUrl, t, {excludeDirect: !includeDirect || idx !== 0, refresh: true, fetchOnly: false}));
|
||||
const results = await Promise.all(promises);
|
||||
for (const r of results) {
|
||||
if (r.error) {
|
||||
logError('batchTeamThreadSync: Error', r.error);
|
||||
}
|
||||
}
|
||||
}, handlePostDeletedForChannelIfNeededBatchTimeout);
|
||||
} catch (error) {
|
||||
logError('handlePostDeletedForChannelIfNeeded: Error', error);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ const ChannelHandler = <TBase extends Constructor<ServerDataOperatorBase>>(super
|
|||
const lastPostAt = isCRT ? (chan.last_root_post_at || chan.last_post_at) : chan.last_post_at;
|
||||
if ((chan && e.lastPostAt < lastPostAt) ||
|
||||
e.isUnread !== my.is_unread || e.lastViewedAt < my.last_viewed_at ||
|
||||
e.roles !== my.roles
|
||||
e.roles !== my.roles || e.autotranslationDisabled !== my.autotranslation_disabled
|
||||
) {
|
||||
res.push(my);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {Alert, DeviceEventEmitter, Linking, NativeEventEmitter} from 'react-nati
|
|||
import semver from 'semver';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {batchTeamThreadSync} from '@actions/remote/thread';
|
||||
import {Device, Events, Sso} from '@constants';
|
||||
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
|
@ -46,6 +47,7 @@ class GlobalEventHandlerSingleton {
|
|||
DeviceEventEmitter.addListener(Events.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
|
||||
splitViewEmitter.addListener('SplitViewChanged', this.onSplitViewChanged);
|
||||
Linking.addEventListener('url', this.onDeepLink);
|
||||
DeviceEventEmitter.addListener(Events.POST_DELETED_FOR_CHANNEL, this.onPostDeletedForChannel);
|
||||
}
|
||||
|
||||
init = () => {
|
||||
|
|
@ -53,6 +55,10 @@ class GlobalEventHandlerSingleton {
|
|||
this.JavascriptAndNativeErrorHandler?.initializeErrorHandling();
|
||||
};
|
||||
|
||||
onPostDeletedForChannel = async ({serverUrl, teamId}: {serverUrl: string; teamId: string}) => {
|
||||
batchTeamThreadSync(serverUrl, teamId);
|
||||
};
|
||||
|
||||
onDeepLink = async (event: LinkingCallbackArg) => {
|
||||
if (event.url?.startsWith(Sso.REDIRECT_URL_SCHEME) || event.url?.startsWith(Sso.REDIRECT_URL_SCHEME_DEV)) {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue