// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import RNUtils from '@mattermost/rnutils'; import {markChannelAsViewed} from '@actions/local/channel'; import {autoCacheCleanup} from '@actions/local/ephemeral_mode/cleanup'; import {dataRetentionCleanup, expiredBoRPostCleanup, performVacuum} from '@actions/local/systems'; import {markChannelAsRead} from '@actions/remote/channel'; import {fetchAccessControlAttributeFields, fetchChannelAttributeValues} from '@actions/remote/classification'; import { entry, handleEntryAfterLoadNavigation, setExtraSessionProps, } from '@actions/remote/entry/common'; import {deferredAppEntryActions} from '@actions/remote/entry/deferred'; import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post'; import {openAllUnreadChannels} from '@actions/remote/preference'; import {autoUpdateTimezone} from '@actions/remote/user'; import {checkIsAgentsPluginEnabled} from '@agents/actions/remote/agents_status'; import {handleAgentsReconnect} from '@agents/actions/websocket/reconnect'; import {loadConfigAndCalls} from '@calls/actions/calls'; import {isSupportedServerCalls} from '@calls/utils'; import {Screens} from '@constants'; import DatabaseManager from '@database/manager'; import AppsManager from '@managers/apps_manager'; import SessionAttributesManager from '@managers/session_attributes_manager'; import {handlePlaybookReconnect} from '@playbooks/actions/websocket/reconnect'; import {getActiveServerUrl} from '@queries/app/servers'; import {getLastPostInThread} from '@queries/servers/post'; import { getConfig, getCurrentChannelId, getCurrentTeamId, getLicense, getLastFullSync, setLastFullSync, } from '@queries/servers/system'; import {getIsCRTEnabled} from '@queries/servers/thread'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; import {NavigationStore} from '@store/navigation_store'; import {setTeamLoading} from '@store/team_load_store'; import {isTablet} from '@utils/helpers'; import {logDebug, logInfo} from '@utils/log'; export async function handleFirstConnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) { setExtraSessionProps(serverUrl, groupLabel); autoUpdateTimezone(serverUrl, groupLabel); return doReconnect(serverUrl, groupLabel); } export async function handleReconnect(serverUrl: string, groupLabel: BaseRequestGroupLabel = 'WebSocket Reconnect') { return doReconnect(serverUrl, groupLabel); } async function doReconnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return new Error('cannot find server database'); } const appDatabase = DatabaseManager.appDatabase?.database; if (!appDatabase) { return new Error('cannot find app database'); } const {database} = operator; // Guards against RUNNINGBOARD 0xdead10cc if the app backgrounds mid-sync. const activityToken = await RNUtils.beginDatabaseActivity(serverUrl, 'doReconnect'); try { await SessionAttributesManager.refreshManifest(serverUrl); const lastFullSync = await getLastFullSync(database); const now = Date.now(); const currentTeamId = await getCurrentTeamId(database); const currentChannelId = await getCurrentChannelId(database); setTeamLoading(serverUrl, true); const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastFullSync, groupLabel); if ('error' in entryData) { return entryData.error; } const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData, gmConverted} = entryData; await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId, gmConverted); const dt = Date.now(); if (models?.length) { await operator.batchRecords(models, 'doReconnect'); } logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`); await fetchPostDataIfNeeded(serverUrl, groupLabel); const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!; const license = await getLicense(database); const config = await getConfig(database); handlePlaybookReconnect(serverUrl); handleAgentsReconnect(serverUrl); if (isSupportedServerCalls(config?.Version)) { loadConfigAndCalls(serverUrl, currentUserId, groupLabel); } checkIsAgentsPluginEnabled(serverUrl); fetchAccessControlAttributeFields(serverUrl, true); // Values may have changed while the socket was down, and the events that // would have reported it are gone. Dropping the per-channel dedupe makes // the next visit to each channel refetch them — but the channel already on // screen is never re-entered, so it is refetched explicitly or it would // keep showing what it had when the connection dropped. EphemeralStore.clearChannelAttributeValuesSynced(serverUrl); if (currentChannelId) { fetchChannelAttributeValues(serverUrl, currentChannelId); } await deferredAppEntryActions(serverUrl, lastFullSync, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, meData, initialTeamId, undefined, groupLabel); await setLastFullSync(operator, now); openAllUnreadChannels(serverUrl, groupLabel); doCleanup(serverUrl); AppsManager.refreshAppBindings(serverUrl, groupLabel); return undefined; } finally { setTeamLoading(serverUrl, false); if (activityToken) { RNUtils.endDatabaseActivity(activityToken); } } } async function doCleanup(serverUrl: string) { const dataRetention = await dataRetentionCleanup(serverUrl); const autoCache = await autoCacheCleanup(serverUrl); await expiredBoRPostCleanup(serverUrl); const dataRetentionRan = !dataRetention.skipped && !dataRetention.error; const autoCacheRan = !autoCache.skipped && !autoCache.error; if (dataRetentionRan || autoCacheRan) { await performVacuum(serverUrl); } } async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: RequestGroupLabel) { try { const isActiveServer = (await getActiveServerUrl()) === serverUrl; if (!isActiveServer) { return; } const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const currentChannelId = await getCurrentChannelId(database); const isCRTEnabled = await getIsCRTEnabled(database); const mountedScreens = NavigationStore.getScreensInStack(); const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL); const isThreadScreenMounted = mountedScreens.includes(Screens.THREAD); const tabletDevice = isTablet(); if (isCRTEnabled && isThreadScreenMounted) { // Fetch new posts in the thread only when CRT is enabled, // for non-CRT fetchPostsForChannel includes posts in the thread const rootId = EphemeralStore.getCurrentThreadId(); if (rootId) { const lastPost = await getLastPostInThread(database, rootId); if (lastPost) { if (lastPost) { const options: FetchPaginatedThreadOptions = {}; options.fromCreateAt = lastPost.createAt; options.fromPost = lastPost.id; options.direction = 'down'; await fetchPostThread(serverUrl, rootId, options, false, groupLabel); } } } } if (currentChannelId && (isChannelScreenMounted || tabletDevice)) { await fetchPostsForChannel(serverUrl, currentChannelId, false, false, groupLabel); markChannelAsRead(serverUrl, currentChannelId, false, groupLabel); if (!EphemeralStore.wasNotificationTapped()) { markChannelAsViewed(serverUrl, currentChannelId, true); } EphemeralStore.setNotificationTapped(false); } } catch (error) { logDebug('could not fetch needed post after WS reconnect', error); } }