From 9f84ab79ce9d31daa4da48f41dbfc6f03b59c501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 23 Feb 2023 10:11:34 +0100 Subject: [PATCH] Only call app entry on websocket reconnect (#7065) * Only call app entry on websocket reconnect * Handle notification on its own entry and run app entry on websocket initialization * Fix notification entry issues * Fix login entry and add retry on entry failure * feedback review * Put back handleEntryAfterLoadNavigation before the batching --------- Co-authored-by: Elias Nahum --- app/actions/remote/entry/app.ts | 65 +------- app/actions/remote/entry/common.ts | 127 +++------------ app/actions/remote/entry/login.ts | 63 +------- app/actions/remote/entry/notification.ts | 144 +++++++----------- app/actions/remote/notifications.ts | 2 +- app/actions/remote/session.ts | 82 ++++------ app/actions/websocket/index.ts | 60 +++----- app/actions/websocket/threads.ts | 2 +- app/client/websocket/index.ts | 33 ++-- .../touchable_emoji/skinned_emoji.tsx | 4 +- app/init/app.ts | 4 +- app/init/launch.ts | 40 ++--- app/managers/websocket_manager.ts | 50 +++--- .../calls/screens/call_screen/call_screen.tsx | 4 +- app/screens/bottom_sheet/index.tsx | 2 +- app/screens/channel/header/header.tsx | 2 +- .../home/channel_list/channel_list.tsx | 1 - .../servers_list/server_item/server_item.tsx | 4 +- app/screens/home/index.tsx | 3 +- app/screens/login/form.tsx | 14 +- app/screens/mfa/index.tsx | 10 +- app/screens/sso/index.tsx | 12 +- app/utils/config.ts | 12 ++ app/utils/deep_link/index.ts | 4 +- app/utils/notification/index.ts | 16 +- assets/base/i18n/en.json | 4 +- index.ts | 2 +- types/api/session.d.ts | 2 - types/launch/index.ts | 1 - 29 files changed, 269 insertions(+), 500 deletions(-) create mode 100644 app/utils/config.ts diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index 217fb3182a..232665e180 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -1,87 +1,38 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {dataRetentionCleanup, setLastServerVersionCheck} from '@actions/local/systems'; +import {setLastServerVersionCheck} from '@actions/local/systems'; import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; -import {prepareCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, getCurrentChannelId, getConfig, getLicense} from '@queries/servers/system'; -import {getCurrentUser} from '@queries/servers/user'; -import {setTeamLoading} from '@store/team_load_store'; +import WebsocketManager from '@managers/websocket_manager'; +import {prepareCommonSystemValues} from '@queries/servers/system'; import {deleteV1Data} from '@utils/file'; -import {isTablet} from '@utils/helpers'; -import {logInfo} from '@utils/log'; -import {handleEntryAfterLoadNavigation, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common'; -import {deferredAppEntryActions, entry} from './gql_common'; +import {verifyPushProxy} from './common'; -export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) { +export async function appEntry(serverUrl: string, since = 0) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; } if (!since) { - registerDeviceToken(serverUrl); if (Object.keys(DatabaseManager.serverDatabases).length === 1) { await setLastServerVersionCheck(serverUrl, true); } } - // Run data retention cleanup - await dataRetentionCleanup(serverUrl); - // clear lastUnreadChannelId const removeLastUnreadChannelId = await prepareCommonSystemValues(operator, {lastUnreadChannelId: ''}); if (removeLastUnreadChannelId) { await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId'); } - const {database} = operator; - - const currentTeamId = await getCurrentTeamId(database); - const currentChannelId = await getCurrentChannelId(database); - const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since; - - setTeamLoading(serverUrl, true); - const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since); - if ('error' in entryData) { - setTeamLoading(serverUrl, false); - return {error: entryData.error}; - } - - const {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData} = entryData; - if (isUpgrade && meData?.user) { - const isTabletDevice = await isTablet(); - const me = await prepareCommonSystemValues(operator, { - currentUserId: meData.user.id, - currentTeamId: initialTeamId, - currentChannelId: isTabletDevice ? initialChannelId : undefined, - }); - if (me?.length) { - await operator.batchRecords(me, 'appEntry - upgrade store me'); - } - } - - await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId, currentChannelId, initialTeamId, initialChannelId); - - const dt = Date.now(); - await operator.batchRecords(models, 'appEntry'); - logInfo('ENTRY MODELS BATCHING TOOK', `${Date.now() - dt}ms`); - setTeamLoading(serverUrl, false); - - const {id: currentUserId, locale: currentUserLocale} = meData?.user || (await getCurrentUser(database))!; - const config = await getConfig(database); - const license = await getLicense(database); - await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); - - if (!since) { - // Load data from other servers - syncOtherServers(serverUrl); - } + WebsocketManager.openAll(); verifyPushProxy(serverUrl); - return {userId: currentUserId}; + return {}; } export async function upgradeEntry(serverUrl: string) { @@ -89,7 +40,7 @@ export async function upgradeEntry(serverUrl: string) { try { const configAndLicense = await fetchConfigAndLicense(serverUrl, false); - const entryData = await appEntry(serverUrl, 0, true); + const entryData = await appEntry(serverUrl, 0); const error = configAndLicense.error || entryData.error; if (!error) { diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index e206ac0b07..7919c790ef 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -2,7 +2,6 @@ // See LICENSE.txt for license information. import {markChannelAsViewed} from '@actions/local/channel'; -import {dataRetentionCleanup} from '@actions/local/systems'; import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel'; import {fetchGroupsForMember} from '@actions/remote/groups'; import {fetchPostsForUnreadChannels} from '@actions/remote/post'; @@ -11,8 +10,7 @@ import {fetchRoles} from '@actions/remote/role'; import {fetchConfigAndLicense} from '@actions/remote/systems'; import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team'; import {syncTeamThreads} from '@actions/remote/thread'; -import {autoUpdateTimezone, fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; -import {gqlAllChannels} from '@client/graphQL/entry'; +import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; import {General, Preferences, Screens} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy'; @@ -22,16 +20,13 @@ import {selectDefaultTeam} from '@helpers/api/team'; import {DEFAULT_LOCALE} from '@i18n'; import NetworkManager from '@managers/network_manager'; import {getDeviceToken} from '@queries/app/global'; -import {getAllServers} from '@queries/app/servers'; -import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel'; +import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel'; import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry'; import {getHasCRTChanged} from '@queries/servers/preference'; -import {getConfig, getCurrentChannelId, getCurrentTeamId, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {getConfig, getCurrentChannelId, getCurrentTeamId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team'; -import {getIsCRTEnabled} from '@queries/servers/thread'; import NavigationStore from '@store/navigation_store'; import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel'; -import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql'; import {isTablet} from '@utils/helpers'; import {logDebug} from '@utils/log'; import {processIsCRTEnabled} from '@utils/thread'; @@ -112,6 +107,12 @@ export const entryRest = async (serverUrl: string, teamId?: string, channelId?: } const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled} = fetchedData; + const chError = chData?.error as ClientError | undefined; + if (chError?.status_code === 403) { + // if the user does not have appropriate permissions, which means the user those not belong to the team, + // we set it as there is no errors, so that the teams and others can be properly handled + chData!.error = undefined; + } const error = teamData.error || chData?.error || prefData.error || meData.error; if (error) { return {error}; @@ -372,107 +373,6 @@ export const registerDeviceToken = async (serverUrl: string) => { return {error: undefined}; }; -export const syncOtherServers = async (serverUrl: string) => { - const servers = await getAllServers(); - for (const server of servers) { - if (server.url !== serverUrl && server.lastActiveAt > 0) { - registerDeviceToken(server.url); - syncAllChannelMembersAndThreads(server.url).then(() => { - dataRetentionCleanup(server.url); - }); - autoUpdateTimezone(server.url); - } - } -}; - -const syncAllChannelMembersAndThreads = async (serverUrl: string) => { - const database = DatabaseManager.serverDatabases[serverUrl]?.database; - if (!database) { - return; - } - - const config = await getConfig(database); - - if (config?.FeatureFlagGraphQL === 'true') { - const error = await graphQLSyncAllChannelMembers(serverUrl); - if (error) { - logDebug('failed graphQL, falling back to rest', error); - restSyncAllChannelMembers(serverUrl); - } - } else { - restSyncAllChannelMembers(serverUrl); - } -}; - -const graphQLSyncAllChannelMembers = async (serverUrl: string) => { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return 'Server database not found'; - } - - const {database} = operator; - - const response = await gqlAllChannels(serverUrl); - if ('error' in response) { - return response.error; - } - - if (response.errors) { - return response.errors[0].message; - } - - const userId = await getCurrentUserId(database); - - const channels = getMemberChannelsFromGQLQuery(response.data); - const memberships = response.data.channelMembers?.map((m) => gqlToClientChannelMembership(m, userId)); - - if (channels && memberships) { - const modelPromises = await prepareMyChannelsForTeam(operator, '', channels, memberships, undefined, true); - const models = (await Promise.all(modelPromises)).flat(); - if (models.length) { - await operator.batchRecords(models, 'graphQLSyncAllChannelMembers'); - } - } - - const isCRTEnabled = await getIsCRTEnabled(database); - if (isCRTEnabled) { - const myTeams = await queryMyTeams(operator.database).fetch(); - for await (const myTeam of myTeams) { - // need to await here since GM/DM threads in different teams overlap - await syncTeamThreads(serverUrl, myTeam.id); - } - } - - return ''; -}; - -const restSyncAllChannelMembers = async (serverUrl: string) => { - let client; - try { - client = NetworkManager.getClient(serverUrl); - } catch { - return; - } - - try { - const myTeams = await client.getMyTeams(); - const preferences = await client.getMyPreferences(); - const config = await client.getClientConfigOld(); - - let excludeDirect = false; - for await (const myTeam of myTeams) { - fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect); - excludeDirect = true; - if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) { - // need to await here since GM/DM threads in different teams overlap - await syncTeamThreads(serverUrl, myTeam.id); - } - } - } catch { - // Do nothing - } -}; - export async function verifyPushProxy(serverUrl: string) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { @@ -537,7 +437,14 @@ export async function handleEntryAfterLoadNavigation( const isThreadsMounted = mountedScreens.includes(Screens.THREAD); const tabletDevice = await isTablet(); - if (currentTeamIdAfterLoad !== currentTeamId) { + if (!currentTeamIdAfterLoad) { + // First load or no team + if (tabletDevice) { + await setCurrentTeamAndChannelId(operator, initialTeamId, ''); + } else { + await setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId); + } + } else if (currentTeamIdAfterLoad !== currentTeamId) { // Switched teams while loading if (!teamMembers.find((t) => t.team_id === currentTeamIdAfterLoad && t.delete_at === 0)) { await handleKickFromTeam(serverUrl, currentTeamIdAfterLoad); diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index e7673dc4ca..6d45f02ef5 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -1,81 +1,34 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {switchToChannelById} from '@actions/remote/channel'; import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; -import NetworkManager from '@managers/network_manager'; -import {setCurrentTeamAndChannelId} from '@queries/servers/system'; -import {setTeamLoading} from '@store/team_load_store'; -import {isTablet} from '@utils/helpers'; - -import {deferredAppEntryActions, entry} from './gql_common'; - -import type {Client} from '@client/rest'; +import {getServerCredentials} from '@init/credentials'; +import WebsocketManager from '@managers/websocket_manager'; type AfterLoginArgs = { serverUrl: string; - user: UserProfile; - deviceToken?: string; } -export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> { - const dt = Date.now(); - +export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?: any}> { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; } - let client: Client; - try { - client = NetworkManager.getClient(serverUrl); - } catch (error) { - return {error}; - } - - if (deviceToken) { - try { - client.attachDevice(deviceToken); - } catch { - // do nothing, the token could've failed to attach to the session but is not a blocker - } - } - try { const clData = await fetchConfigAndLicense(serverUrl, false); if (clData.error) { return {error: clData.error}; } - setTeamLoading(serverUrl, true); - const entryData = await entry(serverUrl, '', ''); - - if ('error' in entryData) { - setTeamLoading(serverUrl, false); - return {error: entryData.error}; + const credentials = await getServerCredentials(serverUrl); + if (credentials?.token) { + WebsocketManager.createClient(serverUrl, credentials.token); + await WebsocketManager.initializeClient(serverUrl); } - const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; - - const isTabletDevice = await isTablet(); - - let switchToChannel = false; - if (initialChannelId && isTabletDevice) { - switchToChannel = true; - switchToChannelById(serverUrl, initialChannelId, initialTeamId); - } else { - setCurrentTeamAndChannelId(operator, initialTeamId, ''); - } - - await operator.batchRecords(models, 'loginEntry'); - setTeamLoading(serverUrl, false); - - const config = clData.config || {} as ClientConfig; - const license = clData.license || {} as ClientLicense; - deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined); - - return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)}; + return {}; } catch (error) { return {error}; } diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index 8ce004b7bd..442aeea61d 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -1,47 +1,44 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {switchToChannelById} from '@actions/remote/channel'; +import {fetchMyChannel, switchToChannelById} from '@actions/remote/channel'; +import {fetchPostById} from '@actions/remote/post'; +import {fetchMyTeam} from '@actions/remote/team'; import {fetchAndSwitchToThread} from '@actions/remote/thread'; -import {Screens} from '@constants'; import {getDefaultThemeByAppearance} from '@context/theme'; import DatabaseManager from '@database/manager'; +import WebsocketManager from '@managers/websocket_manager'; import {getMyChannel} from '@queries/servers/channel'; +import {getPostById} from '@queries/servers/post'; import {queryThemePreferences} from '@queries/servers/preference'; -import {getConfig, getCurrentTeamId, getLicense, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {getCurrentTeamId} from '@queries/servers/system'; import {getMyTeamById} from '@queries/servers/team'; 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 {emitNotificationError} from '@utils/notification'; import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme'; -import {syncOtherServers} from './common'; -import {deferredAppEntryActions, entry} from './gql_common'; +import type ClientError from '@client/rest/error'; +import type MyChannelModel from '@typings/database/models/servers/my_channel'; +import type MyTeamModel from '@typings/database/models/servers/my_team'; +import type PostModel from '@typings/database/models/servers/post'; -export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) { +export async function pushNotificationEntry(serverUrl: string, notification: NotificationData) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; } // We only reach this point if we have a channel Id in the notification payload - const channelId = notification.payload!.channel_id!; - const rootId = notification.payload!.root_id!; + const channelId = notification.channel_id!; + const rootId = notification.root_id!; const {database} = operator; const currentTeamId = await getCurrentTeamId(database); const currentServerUrl = await DatabaseManager.getActiveServerUrl(); - const lastDisconnectedAt = await getWebSocketLastDisconnected(database); - let isDirectChannel = false; - - let teamId = notification.payload?.team_id; + let teamId = notification.team_id; if (!teamId) { // If the notification payload does not have a teamId we assume is a DM/GM - isDirectChannel = true; teamId = currentTeamId; } @@ -61,91 +58,62 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not updateThemeIfNeeded(theme, true); } - await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME); - // To make the switch faster we determine if we already have the team & channel - const myChannel = await getMyChannel(database, channelId); - const myTeam = await getMyTeamById(database, teamId); + let myChannel: MyChannelModel | ChannelMembership | undefined = await getMyChannel(database, channelId); + let myTeam: MyTeamModel | TeamMembership | undefined = await getMyTeamById(database, teamId); + + if (!myTeam) { + const resp = await fetchMyTeam(serverUrl, teamId); + if (resp.error) { + if ((resp.error as ClientError).status_code === 403) { + emitNotificationError('Team'); + } else { + emitNotificationError('Connection'); + } + } else { + myTeam = resp.memberships?.[0]; + } + } + + if (!myChannel) { + const resp = await fetchMyChannel(serverUrl, teamId, channelId); + if (resp.error) { + if ((resp.error as ClientError).status_code === 403) { + emitNotificationError('Channel'); + } else { + emitNotificationError('Connection'); + } + } else { + myChannel = resp.memberships?.[0]; + } + } const isCRTEnabled = await getIsCRTEnabled(database); const isThreadNotification = isCRTEnabled && Boolean(rootId); - let switchedToScreen = false; - let switchedToChannel = false; if (myChannel && myTeam) { if (isThreadNotification) { - await fetchAndSwitchToThread(serverUrl, rootId, true); - } else { - switchedToChannel = true; - await switchToChannelById(serverUrl, channelId, teamId); - } - switchedToScreen = true; - } + let post: PostModel | Post | undefined = await getPostById(database, rootId); + if (!post) { + const resp = await fetchPostById(serverUrl, rootId); + post = resp.post; + } - setTeamLoading(serverUrl, true); - const entryData = await entry(serverUrl, teamId, channelId); - if ('error' in entryData) { - setTeamLoading(serverUrl, false); - return {error: entryData.error}; - } - const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; + const actualRootId = post && ('root_id' in post ? post.root_id : post.rootId); - // There is a chance that after the above request returns - // the user is no longer part of the team or channel - // that triggered the notification (rare but possible) - let selectedTeamId = teamId; - let selectedChannelId = channelId; - if (initialTeamId !== teamId) { - // We are no longer a part of the team that the notification belongs to - // Immediately set the new team as the current team in the database so that the UI - // renders the correct team. - selectedTeamId = initialTeamId; - if (!isDirectChannel) { - selectedChannelId = initialChannelId; - } - } - - if (!switchedToScreen) { - const isTabletDevice = await isTablet(); - if (isTabletDevice || (channelId === selectedChannelId)) { - // Make switch again to get the missing data and make sure the team is the correct one - switchedToScreen = true; - if (isThreadNotification) { + if (actualRootId) { + await fetchAndSwitchToThread(serverUrl, actualRootId, true); + } else if (post) { await fetchAndSwitchToThread(serverUrl, rootId, true); } else { - switchedToChannel = true; - await switchToChannelById(serverUrl, channelId, teamId); + emitNotificationError('Post'); } - } else if (teamId !== selectedTeamId || channelId !== selectedChannelId) { - // If in the end the selected team or channel is different than the one from the notification - // we switch again - await setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId); + } else { + await switchToChannelById(serverUrl, channelId, teamId); } } - if (teamId !== selectedTeamId) { - emitNotificationError('Team'); - } else if (channelId !== selectedChannelId) { - emitNotificationError('Channel'); - } + WebsocketManager.openAll(); - // Waiting for the screen to display fixes a race condition when fetching and storing data - if (switchedToChannel) { - await NavigationStore.waitUntilScreenHasLoaded(Screens.CHANNEL); - } else if (switchedToScreen && isThreadNotification) { - await NavigationStore.waitUntilScreenHasLoaded(Screens.THREAD); - } - - await operator.batchRecords(models, 'pushNotificationEntry'); - setTeamLoading(serverUrl, false); - - const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!; - const config = await getConfig(database); - const license = await getLicense(database); - - await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId); - - syncOtherServers(serverUrl); - - return {userId: currentUserId}; + return {}; } diff --git a/app/actions/remote/notifications.ts b/app/actions/remote/notifications.ts index ebf61eda0c..b4ad2fdfb7 100644 --- a/app/actions/remote/notifications.ts +++ b/app/actions/remote/notifications.ts @@ -137,7 +137,7 @@ export const backgroundNotification = async (serverUrl: string, notification: No serverUrl, teamId, channel ? [channel] : [], myChannel ? [myChannel] : [], - true, isCRTEnabled, + false, isCRTEnabled, ); if (data.categoryChannels?.length && channel) { diff --git a/app/actions/remote/session.ts b/app/actions/remote/session.ts index 69b042de2a..d586feb825 100644 --- a/app/actions/remote/session.ts +++ b/app/actions/remote/session.ts @@ -7,16 +7,14 @@ import {DeviceEventEmitter, Platform} from 'react-native'; import {Database, Events} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; -import {getServerCredentials} from '@init/credentials'; import PushNotifications from '@init/push_notifications'; import NetworkManager from '@managers/network_manager'; -import WebsocketManager from '@managers/websocket_manager'; import {getDeviceToken} from '@queries/app/global'; import {getServerDisplayName} from '@queries/app/servers'; -import {getCurrentUserId, getExpiredSession, getConfig, getLicense} from '@queries/servers/system'; +import {getCurrentUserId, getExpiredSession} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; -import {logWarning, logError} from '@utils/log'; +import {logWarning, logError, logDebug} from '@utils/log'; import {scheduleExpiredNotification} from '@utils/notification'; import {getCSRFFromCookie} from '@utils/security'; @@ -27,47 +25,25 @@ import type {LoginArgs} from '@typings/database/database'; const HTTP_UNAUTHORIZED = 401; -export const completeLogin = async (serverUrl: string) => { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return {error: `${serverUrl} database not found`}; +export const addPushProxyVerificationStateFromLogin = async (serverUrl: string) => { + try { + const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + + const systems: IdValue[] = []; + + // Set push proxy verification + const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl); + if (ppVerification) { + systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification}); + } + + if (systems.length) { + await operator.handleSystem({systems, prepareRecordsOnly: false}); + } + } catch (error) { + logDebug('error setting the push proxy verification state on login', error); } - - const {database} = operator; - const license = await getLicense(database); - const config = await getConfig(database); - - if (!Object.keys(config)?.length || !license || !Object.keys(license)?.length) { - return null; - } - - await DatabaseManager.setActiveServerDatabase(serverUrl); - - const systems: IdValue[] = []; - - // Set push proxy verification - const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl); - if (ppVerification) { - systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification}); - } - - // Start websocket - const credentials = await getServerCredentials(serverUrl); - if (credentials?.token) { - WebsocketManager.createClient(serverUrl, credentials.token); - systems.push({ - id: SYSTEM_IDENTIFIERS.WEBSOCKET, - value: 0, - }); - } - - if (systems.length) { - operator.handleSystem({systems, prepareRecordsOnly: false}); - } - - return null; }; - export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientErrorProps) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { @@ -151,11 +127,12 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo } try { - const {error, hasTeams, time} = await loginEntry({serverUrl, user}); - completeLogin(serverUrl); - return {error: error as ClientError, failed: false, hasTeams, time}; + await addPushProxyVerificationStateFromLogin(serverUrl); + const {error} = await loginEntry({serverUrl}); + await DatabaseManager.setActiveServerDatabase(serverUrl); + return {error: error as ClientError, failed: false}; } catch (error) { - return {error: error as ClientError, failed: false, time: 0}; + return {error: error as ClientError, failed: false}; } }; @@ -251,7 +228,6 @@ export const sendPasswordResetEmail = async (serverUrl: string, email: string) = }; export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise => { - let deviceToken; let user; const database = DatabaseManager.appDatabase?.database; @@ -279,7 +255,6 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser displayName: serverDisplayName, }, }); - deviceToken = await getDeviceToken(); user = await client.getMe(); await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false}); await server?.operator.handleSystem({ @@ -294,11 +269,12 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser } try { - const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken}); - completeLogin(serverUrl); - return {error: error as ClientError, failed: false, hasTeams, time}; + await addPushProxyVerificationStateFromLogin(serverUrl); + const {error} = await loginEntry({serverUrl}); + await DatabaseManager.setActiveServerDatabase(serverUrl); + return {error: error as ClientError, failed: false}; } catch (error) { - return {error: error as ClientError, failed: false, time: 0}; + return {error: error as ClientError, failed: false}; } }; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index d541af523e..917aa43dac 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -2,11 +2,12 @@ // See LICENSE.txt for license information. import {markChannelAsViewed} from '@actions/local/channel'; +import {dataRetentionCleanup} from '@actions/local/systems'; import {markChannelAsRead} from '@actions/remote/channel'; -import {handleEntryAfterLoadNavigation} from '@actions/remote/entry/common'; +import {handleEntryAfterLoadNavigation, registerDeviceToken} from '@actions/remote/entry/common'; import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common'; import {fetchPostsForChannel, fetchPostThread} from '@actions/remote/post'; -import {fetchStatusByIds} from '@actions/remote/user'; +import {autoUpdateTimezone} from '@actions/remote/user'; import {loadConfigAndCalls} from '@calls/actions/calls'; import { handleCallChannelDisabled, @@ -32,17 +33,15 @@ import {Screens, WebsocketEvents} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; import AppsManager from '@managers/apps_manager'; -import {getCurrentChannel} from '@queries/servers/channel'; import {getLastPostInThread} from '@queries/servers/post'; import { getConfig, getCurrentChannelId, - getCurrentUserId, + getCurrentTeamId, getLicense, getWebSocketLastDisconnected, resetWebSocketLastDisconnected, } from '@queries/servers/system'; -import {getCurrentTeam} from '@queries/servers/team'; import {getIsCRTEnabled} from '@queries/servers/thread'; import {getCurrentUser} from '@queries/servers/user'; import EphemeralStore from '@store/ephemeral_store'; @@ -72,36 +71,14 @@ import {handleLeaveTeamEvent, handleUserAddedToTeamEvent, handleUpdateTeamEvent, import {handleThreadUpdatedEvent, handleThreadReadChangedEvent, handleThreadFollowChangedEvent} from './threads'; import {handleUserUpdatedEvent, handleUserTypingEvent} from './users'; -// ESR: 5.37 -const alreadyConnected = new Set(); - 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)) { - await handleReconnect(serverUrl); - return; - } - - alreadyConnected.add(serverUrl); - resetWebSocketLastDisconnected(operator); - fetchStatusByIds(serverUrl, ['me']); - - if (isSupportedServerCalls(config?.Version)) { - const currentUserId = await getCurrentUserId(database); - loadConfigAndCalls(serverUrl, currentUserId); - } + registerDeviceToken(serverUrl); + autoUpdateTimezone(serverUrl); + return doReconnect(serverUrl); } export async function handleReconnect(serverUrl: string) { - await doReconnect(serverUrl); + return doReconnect(serverUrl); } export async function handleClose(serverUrl: string, lastDisconnect: number) { @@ -123,12 +100,12 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) { async function doReconnect(serverUrl: string) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { - return; + return new Error('cannot find server database'); } const appDatabase = DatabaseManager.appDatabase?.database; if (!appDatabase) { - return; + return new Error('cannot find app database'); } const {database} = operator; @@ -136,21 +113,23 @@ async function doReconnect(serverUrl: string) { const lastDisconnectedAt = await getWebSocketLastDisconnected(database); resetWebSocketLastDisconnected(operator); - const currentTeam = await getCurrentTeam(database); - const currentChannel = await getCurrentChannel(database); + const currentTeamId = await getCurrentTeamId(database); + const currentChannelId = await getCurrentChannelId(database); setTeamLoading(serverUrl, true); - const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt); + const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastDisconnectedAt); if ('error' in entryData) { setTeamLoading(serverUrl, false); - return; + return entryData.error; } const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; - await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeam?.id || '', currentChannel?.id || '', initialTeamId, initialChannelId); + await handleEntryAfterLoadNavigation(serverUrl, teamData.memberships || [], chData?.memberships || [], currentTeamId || '', currentChannelId || '', initialTeamId, initialChannelId); const dt = Date.now(); - await operator.batchRecords(models, 'doReconnect'); + if (models?.length) { + await operator.batchRecords(models, 'doReconnect'); + } logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`); setTeamLoading(serverUrl, false); @@ -166,7 +145,10 @@ async function doReconnect(serverUrl: string) { await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); + dataRetentionCleanup(serverUrl); + AppsManager.refreshAppBindings(serverUrl); + return undefined; } export async function handleEvent(serverUrl: string, msg: WebSocketMessage) { diff --git a/app/actions/websocket/threads.ts b/app/actions/websocket/threads.ts index 7ccea72256..b4b90b35bf 100644 --- a/app/actions/websocket/threads.ts +++ b/app/actions/websocket/threads.ts @@ -2,8 +2,8 @@ // See LICENSE.txt for license information. import {markTeamThreadsAsRead, processReceivedThreads, updateThread} from '@actions/local/thread'; -import {getCurrentTeamId} from '@app/queries/servers/system'; import DatabaseManager from '@database/manager'; +import {getCurrentTeamId} from '@queries/servers/system'; import EphemeralStore from '@store/ephemeral_store'; export async function handleThreadUpdatedEvent(serverUrl: string, msg: WebSocketMessage): Promise { diff --git a/app/client/websocket/index.ts b/app/client/websocket/index.ts index 80ec319559..6b9ddb34ea 100644 --- a/app/client/websocket/index.ts +++ b/app/client/websocket/index.ts @@ -7,16 +7,18 @@ import {Platform} from 'react-native'; import {WebsocketEvents} from '@constants'; import DatabaseManager from '@database/manager'; import {getConfig} from '@queries/servers/system'; +import {hasReliableWebsocket} from '@utils/config'; +import {toMilliseconds} from '@utils/datetime'; import {logError, logInfo, logWarning} from '@utils/log'; const MAX_WEBSOCKET_FAILS = 7; -const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec - -const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins +const WEBSOCKET_TIMEOUT = toMilliseconds({seconds: 30}); +const MIN_WEBSOCKET_RETRY_TIME = toMilliseconds({seconds: 3}); +const MAX_WEBSOCKET_RETRY_TIME = toMilliseconds({minutes: 5}); export default class WebSocketClient { private conn?: WebSocketClientInterface; - private connectionTimeout: any; + private connectionTimeout: NodeJS.Timeout | undefined; private connectionId: string; private token: string; @@ -43,6 +45,7 @@ export default class WebSocketClient { private url = ''; private serverUrl: string; + private hasReliablyReconnect = false; constructor(serverUrl: string, token: string, lastDisconnect = 0) { this.connectionId = ''; @@ -98,7 +101,7 @@ export default class WebSocketClient { this.url = connectionUrl; - const reliableWebSockets = config.EnableReliableWebSockets === 'true'; + const reliableWebSockets = hasReliableWebsocket(config); if (reliableWebSockets) { // Add connection id, and last_sequence_number to the query param. // We cannot also send it as part of the auth_challenge, because the session cookie is already sent with the request. @@ -125,7 +128,7 @@ export default class WebSocketClient { // iOS is using he underlying cookieJar headers.Authorization = `Bearer ${this.token}`; } - const {client} = await getOrCreateWebSocketClient(this.url, {headers}); + const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT}); this.conn = client; } catch (error) { return; @@ -154,6 +157,7 @@ export default class WebSocketClient { if (this.serverSequence && this.missedEventsCallback) { this.missedEventsCallback(); } + this.hasReliablyReconnect = true; } } else if (this.firstConnectCallback) { logInfo('websocket connected to', this.url); @@ -171,6 +175,7 @@ export default class WebSocketClient { this.conn = undefined; this.responseSequence = 1; + this.hasReliablyReconnect = false; if (this.connectFailCount === 0) { logInfo('websocket closed', this.url); @@ -203,7 +208,9 @@ export default class WebSocketClient { this.connectionTimeout = setTimeout( () => { if (this.stop) { - clearTimeout(this.connectionTimeout); + if (this.connectionTimeout) { + clearTimeout(this.connectionTimeout); + } return; } this.initialize(opts); @@ -214,6 +221,7 @@ export default class WebSocketClient { this.conn!.onError((evt: any) => { if (evt.url === this.url) { + this.hasReliablyReconnect = false; if (this.connectFailCount <= 1) { logError('websocket error', this.url); logError('WEBSOCKET ERROR EVENT', evt); @@ -243,11 +251,17 @@ export default class WebSocketClient { // If we already have a connectionId present, and server sends a different one, // that means it's either a long timeout, or server restart, or sequence number is not found. + // If the server is not available the first time we try to connect, we won't have a connection id + // but still we need to sync. // Then we do the sync calls, and reset sequence number to 0. - if (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) { - logInfo(this.url, 'long timeout, or server restart, or sequence number is not found.'); + if ( + (this.connectionId !== '' && this.connectionId !== msg.data.connection_id) || + (this.hasReliablyReconnect && this.connectionId === '') + ) { + logInfo(this.url, 'long timeout, or server restart, or sequence number is not found, or first connect after failure.'); this.reconnectCallback(); this.serverSequence = 0; + this.hasReliablyReconnect = false; } // If it's a fresh connection, we have to set the connectionId regardless. @@ -315,6 +329,7 @@ export default class WebSocketClient { this.stop = stop; this.connectFailCount = 0; this.responseSequence = 1; + this.hasReliablyReconnect = false; if (this.conn && this.conn.readyState === WebSocketReadyState.OPEN) { this.conn.close(); diff --git a/app/components/touchable_emoji/skinned_emoji.tsx b/app/components/touchable_emoji/skinned_emoji.tsx index 7fce9e6ecd..f6ba48a0df 100644 --- a/app/components/touchable_emoji/skinned_emoji.tsx +++ b/app/components/touchable_emoji/skinned_emoji.tsx @@ -4,12 +4,12 @@ import React, {useCallback, useMemo} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; -import {useEmojiSkinTone} from '@app/hooks/emoji_category_bar'; -import {preventDoubleTap} from '@app/utils/tap'; import Emoji from '@components/emoji'; import TouchableWithFeedback from '@components/touchable_with_feedback'; +import {useEmojiSkinTone} from '@hooks/emoji_category_bar'; import {skinCodes} from '@utils/emoji'; import {isValidNamedEmoji} from '@utils/emoji/helpers'; +import {preventDoubleTap} from '@utils/tap'; type Props = { name: string; diff --git a/app/init/app.ts b/app/init/app.ts index d4d1665008..0b8e8a7fba 100644 --- a/app/init/app.ts +++ b/app/init/app.ts @@ -52,6 +52,6 @@ export async function start() { registerNavigationListeners(); registerScreens(); - await initialLaunch(); - WebsocketManager.init(serverCredentials); + await WebsocketManager.init(serverCredentials); + initialLaunch(); } diff --git a/app/init/launch.ts b/app/init/launch.ts index 9a23ff34a3..3f48028d88 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -27,8 +27,7 @@ const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, Pu export const initialLaunch = async () => { const deepLinkUrl = await Linking.getInitialURL(); if (deepLinkUrl) { - await launchAppFromDeepLink(deepLinkUrl, true); - return; + return launchAppFromDeepLink(deepLinkUrl, true); } const notification = await Notifications.getInitialNotification(); @@ -43,11 +42,10 @@ export const initialLaunch = async () => { tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null; } if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) { - await launchAppFromNotification(convertToNotificationData(notification!), true); - return; + return launchAppFromNotification(convertToNotificationData(notification!), true); } - await launchApp({launchType: Launch.Normal, coldStart: true}); + return launchApp({launchType: Launch.Normal, coldStart: notification ? tapped : true}); }; const launchAppFromDeepLink = async (deepLinkUrl: string, coldStart = false) => { @@ -162,14 +160,17 @@ const launchToHome = async (props: LaunchProps) => { const extra = props.extra as NotificationWithData; openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local); if (openPushNotification) { - pushNotificationEntry(props.serverUrl!, extra); - } else { - appEntry(props.serverUrl!); + await resetToHome(props); + return pushNotificationEntry(props.serverUrl!, extra.payload!); } + + appEntry(props.serverUrl!); break; } case Launch.Normal: - appEntry(props.serverUrl!); + if (props.coldStart) { + appEntry(props.serverUrl!); + } break; } @@ -202,16 +203,21 @@ export const getLaunchPropsFromNotification = async (notification: NotificationW const {payload} = notification; launchProps.extra = notification; + let serverUrl: string | undefined; - if (payload?.server_url) { - launchProps.serverUrl = payload.server_url; - } else if (payload?.server_id) { - const serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id); - if (serverUrl) { - launchProps.serverUrl = serverUrl; - } else { - launchProps.launchError = true; + try { + if (payload?.server_url) { + DatabaseManager.getServerDatabaseAndOperator(payload.server_url); + serverUrl = payload.server_url; + } else if (payload?.server_id) { + serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id); } + } catch { + launchProps.launchError = true; + } + + if (serverUrl) { + launchProps.serverUrl = serverUrl; } else { launchProps.launchError = true; } diff --git a/app/managers/websocket_manager.ts b/app/managers/websocket_manager.ts index 5358df848e..9b71e32b8a 100644 --- a/app/managers/websocket_manager.ts +++ b/app/managers/websocket_manager.ts @@ -33,6 +33,7 @@ class WebsocketManager { private previousActiveState: boolean; private statusUpdatesIntervalIDs: Record = {}; private backgroundIntervalId: number | undefined; + private firstConnectionSynced: Record = {}; constructor() { this.previousActiveState = AppState.currentState === 'active'; @@ -40,21 +41,15 @@ class WebsocketManager { public init = async (serverCredentials: ServerCredential[]) => { this.netConnected = Boolean((await NetInfo.fetch()).isConnected); - await Promise.all( - serverCredentials.map( - async ({serverUrl, token}) => { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return; - } - - try { - this.createClient(serverUrl, token, 0); - } catch (error) { - logError('WebsocketManager init error', error); - } - }, - ), + serverCredentials.forEach( + ({serverUrl, token}) => { + try { + DatabaseManager.getServerDatabaseAndOperator(serverUrl); + this.createClient(serverUrl, token, 0); + } catch (error) { + logError('WebsocketManager init error', error); + } + }, ); AppState.addEventListener('change', this.onAppStateChange); @@ -68,6 +63,7 @@ class WebsocketManager { this.connectionTimerIDs[serverUrl].cancel(); } delete this.clients[serverUrl]; + delete this.firstConnectionSynced[serverUrl]; this.getConnectedSubject(serverUrl).next('not_connected'); delete this.connectedSubjects[serverUrl]; @@ -84,9 +80,6 @@ class WebsocketManager { client.setReliableReconnectCallback(() => this.onReliableReconnect(serverUrl)); client.setCloseCallback((connectFailCount: number, lastDisconnect: number) => this.onWebsocketClose(serverUrl, connectFailCount, lastDisconnect)); - if (this.netConnected && ['unknown', 'active'].includes(AppState.currentState)) { - client.initialize(); - } this.clients[serverUrl] = client; return this.clients[serverUrl]; @@ -143,25 +136,34 @@ class WebsocketManager { } }; - private initializeClient = (serverUrl: string) => { + public initializeClient = async (serverUrl: string) => { const client: WebSocketClient = this.clients[serverUrl]; - if (!client?.isConnected()) { - client.initialize(); - } this.connectionTimerIDs[serverUrl]?.cancel(); delete this.connectionTimerIDs[serverUrl]; + if (!client?.isConnected()) { + client.initialize(); + if (!this.firstConnectionSynced[serverUrl]) { + const error = await handleFirstConnect(serverUrl); + if (error) { + client.close(false); + } + this.firstConnectionSynced[serverUrl] = true; + } + } }; private onFirstConnect = (serverUrl: string) => { this.startPeriodicStatusUpdates(serverUrl); this.getConnectedSubject(serverUrl).next('connected'); - handleFirstConnect(serverUrl); }; private onReconnect = async (serverUrl: string) => { this.startPeriodicStatusUpdates(serverUrl); this.getConnectedSubject(serverUrl).next('connected'); - await handleReconnect(serverUrl); + const error = await handleReconnect(serverUrl); + if (error) { + this.getClient(serverUrl)?.close(false); + } }; private onReliableReconnect = async (serverUrl: string) => { diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index 679bd37074..b74f6054ea 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -18,7 +18,6 @@ import {Navigation} from 'react-native-navigation'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {RTCView} from 'react-native-webrtc'; -import {appEntry} from '@actions/remote/entry'; import {leaveCall, muteMyself, setSpeakerphoneOn, unmuteMyself} from '@calls/actions'; import {startCallRecording, stopCallRecording} from '@calls/actions/calls'; import {recordingAlert, recordingWillBePostedAlert, recordingErrorAlert} from '@calls/alerts'; @@ -41,6 +40,7 @@ import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {useIsTablet} from '@hooks/device'; +import WebsocketManager from '@managers/websocket_manager'; import { bottomSheet, dismissAllModalsAndPopToScreen, @@ -357,7 +357,7 @@ const CallScreen = ({ await popTopScreen(Screens.THREAD); } await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl); - await appEntry(currentCall.serverUrl, Date.now()); + WebsocketManager.initializeClient(currentCall.serverUrl); await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId}); }, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]); diff --git a/app/screens/bottom_sheet/index.tsx b/app/screens/bottom_sheet/index.tsx index 32be1d4578..ca9e85838b 100644 --- a/app/screens/bottom_sheet/index.tsx +++ b/app/screens/bottom_sheet/index.tsx @@ -5,11 +5,11 @@ import BottomSheetM, {BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheet import React, {ReactNode, useCallback, useEffect, useMemo, useRef} from 'react'; import {DeviceEventEmitter, Handle, InteractionManager, Keyboard, StyleProp, View, ViewStyle} from 'react-native'; -import useNavButtonPressed from '@app/hooks/navigation_button_pressed'; import {Events} from '@constants'; import {useTheme} from '@context/theme'; import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {useIsTablet} from '@hooks/device'; +import useNavButtonPressed from '@hooks/navigation_button_pressed'; import {dismissModal} from '@screens/navigation'; import {hapticFeedback} from '@utils/general'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx index 230b1d8bb5..d93ba122cc 100644 --- a/app/screens/channel/header/header.tsx +++ b/app/screens/channel/header/header.tsx @@ -6,7 +6,6 @@ import {useIntl} from 'react-intl'; import {Keyboard, Platform, Text, View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {bottomSheetSnapPoint} from '@app/utils/helpers'; import {CHANNEL_ACTIONS_OPTIONS_HEIGHT} from '@components/channel_actions/channel_actions'; import CompassIcon from '@components/compass_icon'; import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; @@ -19,6 +18,7 @@ import {useIsTablet} from '@hooks/device'; import {useDefaultHeaderHeight} from '@hooks/header'; import {bottomSheet, popTopScreen, showModal} from '@screens/navigation'; import {isTypeDMorGM} from '@utils/channel'; +import {bottomSheetSnapPoint} from '@utils/helpers'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; diff --git a/app/screens/home/channel_list/channel_list.tsx b/app/screens/home/channel_list/channel_list.tsx index f9c3e01847..ab521a5e86 100644 --- a/app/screens/home/channel_list/channel_list.tsx +++ b/app/screens/home/channel_list/channel_list.tsx @@ -32,7 +32,6 @@ type ChannelProps = { channelsCount: number; isCRTEnabled: boolean; teamsCount: number; - time?: number; isLicensed: boolean; showToS: boolean; launchType: LaunchType; diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx index 0a8eb6b8e0..0b95a874cf 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx @@ -9,7 +9,6 @@ import Swipeable from 'react-native-gesture-handler/Swipeable'; import {Navigation} from 'react-native-navigation'; import {storeMultiServerTutorial} from '@actions/app/global'; -import {appEntry} from '@actions/remote/entry'; import {doPing} from '@actions/remote/general'; import {logout} from '@actions/remote/session'; import {fetchConfigAndLicense} from '@actions/remote/systems'; @@ -24,6 +23,7 @@ import {useTheme} from '@context/theme'; import DatabaseManager from '@database/manager'; import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads'; import {useIsTablet} from '@hooks/device'; +import WebsocketManager from '@managers/websocket_manager'; import {getServerByIdentifier} from '@queries/app/servers'; import {dismissBottomSheet} from '@screens/navigation'; import {canReceiveNotifications} from '@utils/push_proxy'; @@ -296,7 +296,7 @@ const ServerItem = ({ await dismissBottomSheet(); Navigation.updateProps(Screens.HOME, {extra: undefined}); DatabaseManager.setActiveServerDatabase(server.url); - await appEntry(server.url, Date.now()); + WebsocketManager.initializeClient(server.url); return; } diff --git a/app/screens/home/index.tsx b/app/screens/home/index.tsx index 78598d743b..dae234e2ce 100644 --- a/app/screens/home/index.tsx +++ b/app/screens/home/index.tsx @@ -36,7 +36,6 @@ enableFreeze(true); type HomeProps = LaunchProps & { componentId: string; - time?: number; }; const Tab = createBottomTabNavigator(); @@ -46,7 +45,7 @@ export default function HomeScreen(props: HomeProps) { const intl = useIntl(); useEffect(() => { - const listener = DeviceEventEmitter.addListener(Events.NOTIFICATION_ERROR, (value: 'Team' | 'Channel') => { + const listener = DeviceEventEmitter.addListener(Events.NOTIFICATION_ERROR, (value: 'Team' | 'Channel' | 'Post' | 'Connection') => { notificationError(intl, value); }); diff --git a/app/screens/login/form.tsx b/app/screens/login/form.tsx index 960ec4b1d9..a9d91d902f 100644 --- a/app/screens/login/form.tsx +++ b/app/screens/login/form.tsx @@ -8,14 +8,14 @@ import {Keyboard, TextInput, TouchableOpacity, View} from 'react-native'; import Button from 'react-native-button'; import {login} from '@actions/remote/session'; -import CompassIcon from '@app/components/compass_icon'; import ClientError from '@client/rest/error'; +import CompassIcon from '@components/compass_icon'; import FloatingTextInput from '@components/floating_text_input_label'; import FormattedText from '@components/formatted_text'; import Loading from '@components/loading'; import {FORGOT_PASSWORD, MFA} from '@constants/screens'; import {t} from '@i18n'; -import {goToScreen, loginAnimationOptions, resetToHome, resetToTeams} from '@screens/navigation'; +import {goToScreen, loginAnimationOptions, resetToHome} from '@screens/navigation'; import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; import {isServerError} from '@utils/errors'; import {preventDoubleTap} from '@utils/tap'; @@ -99,17 +99,13 @@ const LoginForm = ({config, extra, serverDisplayName, launchError, launchType, l const signIn = async () => { const result: LoginActionResponse = await login(serverUrl!, {serverDisplayName, loginId: loginId.toLowerCase(), password, config, license}); if (checkLoginResponse(result)) { - if (!result.hasTeams && !result.error) { - resetToTeams(); - return; - } - goToHome(result.time || 0, result.error as never); + goToHome(result.error as never); } }; - const goToHome = (time: number, loginError?: never) => { + const goToHome = (loginError?: never) => { const hasError = launchError || Boolean(loginError); - resetToHome({extra, launchError: hasError, launchType, serverUrl, time}); + resetToHome({extra, launchError: hasError, launchType, serverUrl}); }; const checkLoginResponse = (data: LoginActionResponse) => { diff --git a/app/screens/mfa/index.tsx b/app/screens/mfa/index.tsx index d2f8edefd9..134631f41c 100644 --- a/app/screens/mfa/index.tsx +++ b/app/screens/mfa/index.tsx @@ -19,7 +19,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {useIsTablet} from '@hooks/device'; import {t} from '@i18n'; import Background from '@screens/background'; -import {popTopScreen, resetToTeams} from '@screens/navigation'; +import {popTopScreen} from '@screens/navigation'; import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -32,7 +32,7 @@ import type {AvailableScreens} from '@typings/screens/navigation'; type MFAProps = { componentId: AvailableScreens; config: Partial; - goToHome: (time: number, error?: never) => void; + goToHome: (error?: never) => void; license: Partial; loginId: string; password: string; @@ -156,11 +156,7 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD setError(result.error.message); return; } - if (!result.hasTeams && !result.error) { - resetToTeams(); - return; - } - goToHome(result.time || 0, result.error as never); + goToHome(result.error as never); }), [token]); const transform = useAnimatedStyle(() => { diff --git a/app/screens/sso/index.tsx b/app/screens/sso/index.tsx index 84e0338aeb..c0b06bffd6 100644 --- a/app/screens/sso/index.tsx +++ b/app/screens/sso/index.tsx @@ -15,7 +15,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import useNavButtonPressed from '@hooks/navigation_button_pressed'; import NetworkManager from '@managers/network_manager'; import Background from '@screens/background'; -import {dismissModal, popTopScreen, resetToHome, resetToTeams} from '@screens/navigation'; +import {dismissModal, popTopScreen, resetToHome} from '@screens/navigation'; import {logWarning} from '@utils/log'; import SSOWithRedirectURL from './sso_with_redirect_url'; @@ -105,16 +105,12 @@ const SSO = ({ onLoadEndError(result.error); return; } - if (!result.hasTeams && !result.error) { - resetToTeams(); - return; - } - goToHome(result.time || 0, result.error as never); + goToHome(result.error as never); }; - const goToHome = (time: number, error?: never) => { + const goToHome = (error?: never) => { const hasError = launchError || Boolean(error); - resetToHome({extra, launchError: hasError, launchType, serverUrl, time}); + resetToHome({extra, launchError: hasError, launchType, serverUrl}); }; const dismiss = () => { diff --git a/app/utils/config.ts b/app/utils/config.ts new file mode 100644 index 0000000000..ed9aacfff9 --- /dev/null +++ b/app/utils/config.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {isMinimumServerVersion} from './helpers'; + +export function hasReliableWebsocket(config: ClientConfig) { + if (isMinimumServerVersion(config.Version, 6, 5)) { + return true; + } + + return config.EnableReliableWebSockets === 'true'; +} diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index e3f5b242d2..a8addc47dd 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -5,13 +5,13 @@ import {createIntl, IntlShape} from 'react-intl'; import urlParse from 'url-parse'; import {makeDirectChannel, switchToChannelByName} from '@actions/remote/channel'; -import {appEntry} from '@actions/remote/entry'; import {showPermalink} from '@actions/remote/permalink'; import {fetchUsersByUsernames} from '@actions/remote/user'; import {DeepLink, Launch, Screens} from '@constants'; import {getDefaultThemeByAppearance} from '@context/theme'; import DatabaseManager from '@database/manager'; import {DEFAULT_LOCALE, getTranslations} from '@i18n'; +import WebsocketManager from '@managers/websocket_manager'; import {getActiveServerUrl} from '@queries/app/servers'; import {getCurrentUser, queryUsersByUsername} from '@queries/servers/user'; import {dismissAllModalsAndPopToRoot} from '@screens/navigation'; @@ -48,7 +48,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape, if (existingServerUrl !== currentServerUrl && NavigationStore.getVisibleScreen()) { await dismissAllModalsAndPopToRoot(); DatabaseManager.setActiveServerDatabase(existingServerUrl); - appEntry(existingServerUrl, Date.now()); + WebsocketManager.initializeClient(existingServerUrl); await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME); } diff --git a/app/utils/notification/index.ts b/app/utils/notification/index.ts index f72e5d8db2..d4a9d02d9e 100644 --- a/app/utils/notification/index.ts +++ b/app/utils/notification/index.ts @@ -47,7 +47,7 @@ export const convertToNotificationData = (notification: Notification, tapped = t return notificationData; }; -export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel') => { +export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel' | 'Connection' | 'Post') => { const title = intl.formatMessage({id: 'notification.message_not_found', defaultMessage: 'Message not found'}); let message; switch (type) { @@ -63,13 +63,25 @@ export const notificationError = (intl: IntlShape, type: 'Team' | 'Channel') => defaultMessage: 'This message belongs to a team where you are not a member.', }); break; + case 'Post': + message = intl.formatMessage({ + id: 'notification.no_post', + defaultMessage: 'The message has not been found.', + }); + break; + case 'Connection': + message = intl.formatMessage({ + id: 'notification.no_connection', + defaultMessage: 'The server is unreachable and we were not able to retrieve the notification channel / team.', + }); + break; } Alert.alert(title, message); popToRoot(); }; -export const emitNotificationError = (type: 'Team' | 'Channel') => { +export const emitNotificationError = (type: 'Team' | 'Channel' | 'Post' | 'Connection') => { const req = setTimeout(() => { DeviceEventEmitter.emit(Events.NOTIFICATION_ERROR, type); clearTimeout(req); diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 5184c6ea6f..2941462c47 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -340,6 +340,7 @@ "invite.send_invite": "Send", "invite.sendInvitationsTo": "Send invitations to…", "invite.shareLink": "Share link", + "invite.summary.back": "Go back", "invite.summary.done": "Done", "invite.summary.email_invite": "An invitation email has been sent", "invite.summary.error": "{invitationsCount, plural, one {Invitation} other {Invitations}} could not be sent successfully", @@ -535,7 +536,6 @@ "mobile.login_options.saml": "SAML", "mobile.login_options.select_option": "Select a login option below.", "mobile.login_options.separator_text": "or log in with", - "mobile.login_options.sso_continue": "Continue with", "mobile.manage_members.admin": "Admin", "mobile.manage_members.cancel": "Cancel", "mobile.manage_members.change_role.error": "An error occurred while trying to update the role. Please check your connection and try again.", @@ -726,6 +726,8 @@ "notification_settings.threads_start": "Threads that I start", "notification_settings.threads_start_participate": "Threads that I start or participate in", "notification.message_not_found": "Message not found", + "notification.no_connection": "The server is unreachable and we were not able to retrieve the notification channel / team.", + "notification.no_post": "The message has not been found.", "notification.not_channel_member": "This message belongs to a channel where you are not a member.", "notification.not_team_member": "This message belongs to a team where you are not a member.", "onboarding.calls": "Start secure audio calls instantly", diff --git a/index.ts b/index.ts index 69fbefa2df..27f15a4411 100644 --- a/index.ts +++ b/index.ts @@ -57,5 +57,5 @@ if (Platform.OS === 'android') { Navigation.events().registerAppLaunchedListener(async () => { await initialize(); - await start(); + start(); }); diff --git a/types/api/session.d.ts b/types/api/session.d.ts index 3cd6e8c026..3a2c2ff80d 100644 --- a/types/api/session.d.ts +++ b/types/api/session.d.ts @@ -15,7 +15,5 @@ interface Session { interface LoginActionResponse { error?: ClientErrorProps | Error | string; - hasTeams?: boolean; failed: boolean; - time?: number; } diff --git a/types/launch/index.ts b/types/launch/index.ts index 1d4986cae0..334e9c3922 100644 --- a/types/launch/index.ts +++ b/types/launch/index.ts @@ -44,6 +44,5 @@ export interface LaunchProps { launchError?: Boolean; serverUrl?: string; displayName?: string; - time?: number; coldStart?: boolean; }