mattermost-mobile/app/actions/websocket/index.ts
Kyriakos Z 477c7cf1bf
MM-40093 [Gekidou]: adds remote action to sync threads (#6135)
* MM-40093: adds remote action to sync threads

Syncing threads fetches all unread threads if there are no threads in
the DB.
If there are threads in the DB it fetches all threads newest than the
newest one we have stored.

Fetching happens in batches of 60 threads at a time.

* Fixes querying for all threads list

* Syncs threads on WS reconnection

* Addresses review comments

* Addresses review comments
2022-04-12 15:03:05 +03:00

382 lines
14 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchMissingSidebarInfo, switchToChannelById} from '@actions/remote/channel';
import {AppEntryData, AppEntryError, fetchAppEntryData, teamsToRemove} from '@actions/remote/entry/common';
import {fetchPostsForUnreadChannels, fetchPostsSince} from '@actions/remote/post';
import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import {fetchAllTeams, fetchTeamsChannelsAndUnreadPosts} from '@actions/remote/team';
import {fetchNewThreads} from '@actions/remote/thread';
import {fetchStatusByIds, updateAllUsersSince} from '@actions/remote/user';
import {WebsocketEvents} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {queryChannelsById, getDefaultChannelForTeam} from '@queries/servers/channel';
import {prepareModels} from '@queries/servers/entry';
import {getCommonSystemValues, getConfig, getCurrentChannelId, getWebSocketLastDisconnected, resetWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {isDMorGM} from '@utils/channel';
import {isTablet} from '@utils/helpers';
import {isCRTEnabled} from '@utils/thread';
import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category';
import {handleChannelConvertedEvent, handleChannelCreatedEvent,
handleChannelDeletedEvent,
handleChannelMemberUpdatedEvent,
handleChannelUnarchiveEvent,
handleChannelUpdatedEvent,
handleChannelViewedEvent,
handleDirectAddedEvent,
handleUserAddedToChannelEvent,
handleUserRemovedFromChannelEvent} from './channel';
import {handleNewPostEvent, handlePostDeleted, handlePostEdited, handlePostUnread} from './posts';
import {handlePreferenceChangedEvent, handlePreferencesChangedEvent, handlePreferencesDeletedEvent} from './preferences';
import {handleAddCustomEmoji, handleReactionRemovedFromPostEvent, handleReactionAddedToPostEvent} from './reactions';
import {handleUserRoleUpdatedEvent, handleTeamMemberRoleUpdatedEvent, handleRoleUpdatedEvent} from './roles';
import {handleLicenseChangedEvent, handleConfigChangedEvent} from './system';
import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent} from './teams';
import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads';
import {handleUserUpdatedEvent, handleUserTypingEvent} from './users';
// ESR: 5.37
const alreadyConnected = new Set<string>();
export async function handleFirstConnect(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const config = await getConfig(database);
const lastDisconnect = await getWebSocketLastDisconnected(database);
// ESR: 5.37
if (lastDisconnect && config?.EnableReliableWebSockets !== 'true' && alreadyConnected.has(serverUrl)) {
handleReconnect(serverUrl);
return;
}
alreadyConnected.add(serverUrl);
resetWebSocketLastDisconnected(operator);
fetchStatusByIds(serverUrl, ['me']);
}
export function handleReconnect(serverUrl: string) {
doReconnect(serverUrl);
}
export async function handleClose(serverUrl: string, lastDisconnect: number) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
await operator.handleSystem({
systems: [
{
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
value: lastDisconnect.toString(10),
},
],
prepareRecordsOnly: false,
});
}
async function doReconnect(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const tabletDevice = await isTablet();
const system = await getCommonSystemValues(database);
const lastDisconnectedAt = await getWebSocketLastDisconnected(database);
resetWebSocketLastDisconnected(operator);
let {config, license} = await fetchConfigAndLicense(serverUrl);
if (!config) {
config = system.config;
}
if (!license) {
license = system.license;
}
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, system.currentTeamId);
const fetchedError = (fetchedData as AppEntryError).error;
if (fetchedError) {
return;
}
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData;
const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true);
const profiles: UserProfile[] = [];
if (chData?.channels?.length) {
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(prefData.preferences || [], config, license);
let direct: Channel[];
[chData.channels, direct] = chData.channels.reduce(([others, channels], c: Channel) => {
if (isDMorGM(c)) {
channels.push(c);
} else {
others.push(c);
}
return [others, channels];
}, [[], []] as Channel[][]);
if (direct.length) {
const {directChannels, users} = await fetchMissingSidebarInfo(serverUrl, direct, meData.user?.locale, teammateDisplayNameSetting, system.currentUserId, true);
if (directChannels?.length) {
chData.channels.push(...directChannels);
}
if (users?.length) {
profiles.push(...users);
}
}
}
// if no longer a member of the current team
if (initialTeamId !== system.currentTeamId) {
let cId = '';
if (tabletDevice) {
if (!cId) {
const channel = await getDefaultChannelForTeam(database, initialTeamId);
if (channel) {
cId = channel.id;
}
}
switchToChannelById(serverUrl, cId, initialTeamId);
} else {
setCurrentTeamAndChannelId(operator, initialTeamId, cId);
}
}
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
let removeChannels;
if (removeChannelIds?.length) {
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
}
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData});
if (rolesData.roles?.length) {
modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true}));
}
if (profiles.length) {
modelPromises.push(operator.handleUsers({users: profiles, prepareRecordsOnly: true}));
}
if (modelPromises.length) {
const models = await Promise.all(modelPromises);
const flattenedModels = models.flat();
if (flattenedModels?.length > 0) {
try {
await operator.batchRecords(flattenedModels);
} catch {
// eslint-disable-next-line no-console
console.log('FAILED TO BATCH WS reconnection');
}
}
}
const currentChannelId = await getCurrentChannelId(database);
if (currentChannelId) {
// https://mattermost.atlassian.net/browse/MM-40098
fetchPostsSince(serverUrl, currentChannelId, lastDisconnectedAt);
// defer fetching posts for unread channels on initial team
if (chData?.channels && chData.memberships) {
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, currentChannelId);
}
}
// defer fetch channels and unread posts for other teams
if (teamData.teams?.length && teamData.memberships?.length) {
await fetchTeamsChannelsAndUnreadPosts(serverUrl, lastDisconnectedAt, teamData.teams, teamData.memberships, initialTeamId);
}
if (prefData.preferences && isCRTEnabled(prefData.preferences, config)) {
if (initialTeamId) {
await fetchNewThreads(serverUrl, initialTeamId, false);
}
if (teamData.teams?.length) {
for await (const team of teamData.teams) {
if (team.id !== initialTeamId) {
// need to await here since GM/DM threads in different teams overlap
await fetchNewThreads(serverUrl, team.id, false);
}
}
}
}
fetchAllTeams(serverUrl);
updateAllUsersSince(serverUrl, lastDisconnectedAt);
// https://mattermost.atlassian.net/browse/MM-41520
}
export async function handleEvent(serverUrl: string, msg: WebSocketMessage) {
switch (msg.event) {
case WebsocketEvents.POSTED:
case WebsocketEvents.EPHEMERAL_MESSAGE:
handleNewPostEvent(serverUrl, msg);
break;
case WebsocketEvents.POST_EDITED:
handlePostEdited(serverUrl, msg);
break;
case WebsocketEvents.POST_DELETED:
handlePostDeleted(serverUrl, msg);
break;
case WebsocketEvents.POST_UNREAD:
handlePostUnread(serverUrl, msg);
break;
case WebsocketEvents.LEAVE_TEAM:
handleLeaveTeamEvent(serverUrl, msg);
break;
case WebsocketEvents.UPDATE_TEAM:
handleUpdateTeamEvent(serverUrl, msg);
break;
case WebsocketEvents.ADDED_TO_TEAM:
handleUserAddedToTeamEvent(serverUrl, msg);
break;
case WebsocketEvents.USER_ADDED:
handleUserAddedToChannelEvent(serverUrl, msg);
break;
case WebsocketEvents.USER_REMOVED:
handleUserRemovedFromChannelEvent(serverUrl, msg);
break;
case WebsocketEvents.USER_UPDATED:
handleUserUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.ROLE_UPDATED:
handleRoleUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.USER_ROLE_UPDATED:
handleUserRoleUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.MEMBERROLE_UPDATED:
handleTeamMemberRoleUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CATEGORY_CREATED:
handleCategoryCreatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CATEGORY_UPDATED:
handleCategoryUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CATEGORY_ORDER_UPDATED:
handleCategoryOrderUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CATEGORY_DELETED:
handleCategoryDeletedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_CREATED:
handleChannelCreatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_DELETED:
handleChannelDeletedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_UNARCHIVED:
handleChannelUnarchiveEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_UPDATED:
handleChannelUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_CONVERTED:
handleChannelConvertedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_VIEWED:
handleChannelViewedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_MEMBER_UPDATED:
handleChannelMemberUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.CHANNEL_SCHEME_UPDATED:
// Do nothing, handled by CHANNEL_UPDATED due to changes in the channel scheme.
break;
case WebsocketEvents.DIRECT_ADDED:
case WebsocketEvents.GROUP_ADDED:
handleDirectAddedEvent(serverUrl, msg);
break;
case WebsocketEvents.PREFERENCE_CHANGED:
handlePreferenceChangedEvent(serverUrl, msg);
break;
case WebsocketEvents.PREFERENCES_CHANGED:
handlePreferencesChangedEvent(serverUrl, msg);
break;
case WebsocketEvents.PREFERENCES_DELETED:
handlePreferencesDeletedEvent(serverUrl, msg);
break;
case WebsocketEvents.STATUS_CHANGED:
break;
// return dispatch(handleStatusChangedEvent(msg));
case WebsocketEvents.TYPING:
handleUserTypingEvent(serverUrl, msg);
break;
case WebsocketEvents.REACTION_ADDED:
handleReactionAddedToPostEvent(serverUrl, msg);
break;
case WebsocketEvents.REACTION_REMOVED:
handleReactionRemovedFromPostEvent(serverUrl, msg);
break;
case WebsocketEvents.EMOJI_ADDED:
handleAddCustomEmoji(serverUrl, msg);
break;
case WebsocketEvents.LICENSE_CHANGED:
handleLicenseChangedEvent(serverUrl, msg);
break;
case WebsocketEvents.CONFIG_CHANGED:
handleConfigChangedEvent(serverUrl, msg);
break;
case WebsocketEvents.OPEN_DIALOG:
break;
case WebsocketEvents.THREAD_UPDATED:
handleThreadUpdatedEvent(serverUrl, msg);
break;
case WebsocketEvents.THREAD_READ_CHANGED:
handleThreadReadChangedEvent(serverUrl, msg);
break;
case WebsocketEvents.THREAD_FOLLOW_CHANGED:
handleThreadFollowChangedEvent(serverUrl, msg);
break;
case WebsocketEvents.APPS_FRAMEWORK_REFRESH_BINDINGS:
break;
// return dispatch(handleRefreshAppsBindings());
}
}