From 0464dd34eef270de464520795656965c85fe38d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Thu, 19 May 2022 11:24:59 +0200 Subject: [PATCH] Unify entry functions (#6231) * Unify entry functions * Handle switched to channel on websocket reconnect * Simplify global threads by moving it to switchByChannelId * Keep in mind team when changing to global threads view * Avoid unneeded channel switches on notification entry * Fix effects on home screen * Address feedback --- app/actions/local/thread.ts | 19 ++- app/actions/remote/channel.ts | 7 +- app/actions/remote/entry/app.ts | 130 +++------------- app/actions/remote/entry/common.ts | 136 ++++++++++++++-- app/actions/remote/entry/login.ts | 119 +++----------- app/actions/remote/entry/notification.ts | 86 +++-------- app/actions/remote/team.ts | 9 +- app/actions/websocket/channel.ts | 18 ++- app/actions/websocket/index.ts | 189 ++++++----------------- app/screens/home/index.tsx | 12 +- app/utils/navigation/index.ts | 17 ++ 11 files changed, 298 insertions(+), 444 deletions(-) diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index 8456237b4..460ed4136 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -8,7 +8,7 @@ import DatabaseManager from '@database/manager'; import {getTranslations, t} from '@i18n'; import {getChannelById} from '@queries/servers/channel'; import {getPostById} from '@queries/servers/post'; -import {getCurrentTeamId, getCurrentUserId, setCurrentChannelId} from '@queries/servers/system'; +import {getCurrentTeamId, getCurrentUserId, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {addChannelToTeamHistory} from '@queries/servers/team'; import {getIsCRTEnabled, getThreadById, prepareThreadsFromReceivedPosts, queryThreadsInTeam} from '@queries/servers/thread'; import {getCurrentUser} from '@queries/servers/user'; @@ -19,7 +19,7 @@ import {changeOpacity} from '@utils/theme'; import type Model from '@nozbe/watermelondb/Model'; -export const switchToGlobalThreads = async (serverUrl: string, prepareRecordsOnly = false) => { +export const switchToGlobalThreads = async (serverUrl: string, teamId?: string, prepareRecordsOnly = false) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -27,10 +27,19 @@ export const switchToGlobalThreads = async (serverUrl: string, prepareRecordsOnl const {database} = operator; const models: Model[] = []; + + let teamIdToUse = teamId; + if (!teamId) { + teamIdToUse = await getCurrentTeamId(database); + } + + if (!teamIdToUse) { + return {error: 'no team to switch to'}; + } + try { - await setCurrentChannelId(operator, ''); - const currentTeamId = await getCurrentTeamId(database); - const history = await addChannelToTeamHistory(operator, currentTeamId, Screens.GLOBAL_THREADS, true); + await setCurrentTeamAndChannelId(operator, teamId, ''); + const history = await addChannelToTeamHistory(operator, teamIdToUse, Screens.GLOBAL_THREADS, true); models.push(...history); const isTabletDevice = await isTablet(); if (isTabletDevice) { diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 1389c15d1..11ad7d489 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -971,6 +971,10 @@ export async function getChannelTimezones(serverUrl: string, channelId: string) } export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false) { + if (channelId === Screens.GLOBAL_THREADS) { + return switchToGlobalThreads(serverUrl, teamId); + } + const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -993,9 +997,6 @@ export async function switchToPenultimateChannel(serverUrl: string) { try { const currentTeam = await getCurrentTeamId(database); const channelId = await getNthLastChannelFromTeam(database, currentTeam, 1); - if (channelId === Screens.GLOBAL_THREADS) { - return switchToGlobalThreads(serverUrl); - } return switchToChannelById(serverUrl, channelId); } catch (error) { return {error}; diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index e96e90aae..e127768a9 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -1,25 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {switchToGlobalThreads} from '@actions/local/thread'; import {switchToChannelById} from '@actions/remote/channel'; -import {fetchRoles} from '@actions/remote/role'; import {fetchConfigAndLicense} from '@actions/remote/systems'; -import {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'; import DatabaseManager from '@database/manager'; -import NetworkManager from '@managers/network_manager'; -import {getDeviceToken} from '@queries/app/global'; -import {queryChannelsById, getDefaultChannelForTeam} from '@queries/servers/channel'; -import {prepareModels} from '@queries/servers/entry'; -import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getPushVerificationStatus} from '@queries/servers/system'; -import {getNthLastChannelFromTeam} from '@queries/servers/team'; +import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getCurrentChannelId} from '@queries/servers/system'; import {getCurrentUser} from '@queries/servers/user'; import {deleteV1Data} from '@utils/file'; import {isTablet} from '@utils/helpers'; -import {deferredAppEntryActions, fetchAppEntryData, registerDeviceToken, syncOtherServers, teamsToRemove} from './common'; +import {deferredAppEntryActions, entry, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common'; export async function appEntry(serverUrl: string, since = 0) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -41,58 +31,31 @@ export async function appEntry(serverUrl: string, since = 0) { const tabletDevice = await isTablet(); const currentTeamId = await getCurrentTeamId(database); + const currentChannelId = await getCurrentChannelId(database); const lastDisconnectedAt = (await getWebSocketLastDisconnected(database)) || since; - const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, currentTeamId); - if ('error' in fetchedData) { - return {error: fetchedData.error}; + const entryData = await entry(serverUrl, currentTeamId, currentChannelId, since); + if ('error' in entryData) { + return {error: entryData.error}; } + const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; - const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData; - const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true, true); + let switchToChannel = false; - if (initialTeamId === currentTeamId) { - if (tabletDevice) { - const cId = await getNthLastChannelFromTeam(database, currentTeamId); - if (cId === Screens.GLOBAL_THREADS) { - switchToGlobalThreads(serverUrl); - } else { - switchToChannelById(serverUrl, cId, initialTeamId); - } - } + // Immediately set the new team as the current team in the database so that the UI + // renders the correct team. + if (tabletDevice && initialChannelId) { + switchToChannel = true; + switchToChannelById(serverUrl, initialChannelId, initialTeamId); } else { - // Immediately set the new team as the current team in the database so that the UI - // renders the correct team. - let channelId = ''; - if (tabletDevice) { - const channel = await getDefaultChannelForTeam(database, initialTeamId); - channelId = channel?.id || ''; - } - if (channelId) { - switchToChannelById(serverUrl, channelId, initialTeamId); - } else { - setCurrentTeamAndChannelId(operator, initialTeamId, channelId); - } + setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId); } - const removeTeams = await teamsToRemove(serverUrl, removeTeamIds); + await operator.batchRecords(models); - 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})); - } - - const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); - - const {id: currentUserId, locale: currentUserLocale} = meData.user || (await getCurrentUser(database))!; + const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!; const {config, license} = await getCommonSystemValues(database); - await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); + await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined); if (!since) { // Load data from other servers @@ -101,58 +64,7 @@ export async function appEntry(serverUrl: string, since = 0) { verifyPushProxy(serverUrl); - const error = teamData.error || chData?.error || prefData.error || meData.error; - return {error, userId: meData?.user?.id}; -} - -export async function verifyPushProxy(serverUrl: string) { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return; - } - - const {database} = operator; - - const ppVerification = await getPushVerificationStatus(database); - if ( - ppVerification !== PUSH_PROXY_STATUS_UNKNOWN && - ppVerification !== '' - ) { - return; - } - - const appDatabase = DatabaseManager.appDatabase?.database; - if (!appDatabase) { - return; - } - - const deviceId = await getDeviceToken(appDatabase); - if (!deviceId) { - return; - } - - let client; - try { - client = NetworkManager.getClient(serverUrl); - } catch (err) { - return; - } - - try { - const response = await client.ping(deviceId); - const canReceiveNotifications = response?.data?.CanReceiveNotifications; - switch (canReceiveNotifications) { - case PUSH_PROXY_RESPONSE_NOT_AVAILABLE: - operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_NOT_AVAILABLE}], prepareRecordsOnly: false}); - return; - case PUSH_PROXY_RESPONSE_UNKNOWN: - return; - default: - operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}], prepareRecordsOnly: false}); - } - } catch (err) { - // Do nothing - } + return {userId: currentUserId}; } export async function upgradeEntry(serverUrl: string) { @@ -164,12 +76,12 @@ export async function upgradeEntry(serverUrl: string) { try { const configAndLicense = await fetchConfigAndLicense(serverUrl, false); - const entry = await appEntry(serverUrl); + const entryData = await appEntry(serverUrl); - const error = configAndLicense.error || entry.error; + const error = configAndLicense.error || entryData.error; if (!error) { - const models = await prepareCommonSystemValues(operator, {currentUserId: entry.userId}); + const models = await prepareCommonSystemValues(operator, {currentUserId: entryData.userId}); if (models?.length) { await operator.batchRecords(models); } diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 00a97a195..7adb5b519 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -1,13 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {fetchChannelStats, fetchMissingSidebarInfo, fetchMyChannelsForTeam, markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel'; -import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post'; +import {Model} from '@nozbe/watermelondb'; + +import {fetchMissingSidebarInfo, fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel'; +import {fetchPostsForUnreadChannels} from '@actions/remote/post'; import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; +import {fetchRoles} from '@actions/remote/role'; import {fetchConfigAndLicense} from '@actions/remote/systems'; import {fetchAllTeams, fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from '@actions/remote/team'; +import {fetchNewThreads} from '@actions/remote/thread'; import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; import {Preferences} 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'; import DatabaseManager from '@database/manager'; import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference'; import {selectDefaultTeam} from '@helpers/api/team'; @@ -15,14 +21,13 @@ import {DEFAULT_LOCALE} from '@i18n'; import NetworkManager from '@managers/network_manager'; import {getDeviceToken} from '@queries/app/global'; import {queryAllServers} from '@queries/app/servers'; -import {queryAllChannelsForTeam} from '@queries/servers/channel'; -import {getConfig} from '@queries/servers/system'; -import {deleteMyTeams, getAvailableTeamIds, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team'; +import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel'; +import {prepareModels} from '@queries/servers/entry'; +import {getConfig, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system'; +import {deleteMyTeams, getAvailableTeamIds, getNthLastChannelFromTeam, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team'; import {isDMorGM} from '@utils/channel'; import {isCRTEnabled} from '@utils/thread'; -import {fetchNewThreads} from '../thread'; - import type ClientError from '@client/rest/error'; export type AppEntryData = { @@ -60,7 +65,65 @@ export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) return undefined; }; -export const fetchAppEntryData = async (serverUrl: string, since: number, initialTeamId: string): Promise => { +export type EntryResponse = { + models: Model[]; + initialTeamId: string; + initialChannelId: string; + prefData: MyPreferencesRequest; + teamData: MyTeamsRequest; + chData?: MyChannelsRequest; +} | { + error: unknown; +} +export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise => { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return {error: `${serverUrl} database not found`}; + } + + const {database} = operator; + + const lastDisconnectedAt = since || await getWebSocketLastDisconnected(database); + + const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId); + if ('error' in fetchedData) { + return {error: fetchedData.error}; + } + + const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData; + const error = teamData.error || chData?.error || prefData.error || meData.error; + if (error) { + return {error}; + } + + const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true); + + let initialChannelId = channelId; + if (!chData?.channels?.find((c) => c.id === channelId)) { + initialChannelId = ''; + } + if (initialTeamId !== teamId || !initialChannelId) { + initialChannelId = await getNthLastChannelFromTeam(database, initialTeamId); + } + + 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})); + } + + const models = await Promise.all(modelPromises); + + return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData}; +}; + +export const fetchAppEntryData = async (serverUrl: string, since: number, initialTeamId = ''): Promise => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -188,13 +251,6 @@ export async function deferredAppEntryActions( serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, initialTeamId?: string, initialChannelId?: string) { - // defer fetching posts for initial channel - if (initialChannelId) { - fetchPostsForChannel(serverUrl, initialChannelId); - markChannelAsRead(serverUrl, initialChannelId); - fetchChannelStats(serverUrl, initialChannelId); - } - // defer sidebar DM & GM profiles if (chData?.channels?.length && chData.memberships?.length) { const directChannels = chData.channels.filter(isDMorGM); @@ -288,3 +344,53 @@ const syncAllChannelMembers = async (serverUrl: string) => { // Do nothing } }; + +export async function verifyPushProxy(serverUrl: string) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return; + } + + const {database} = operator; + + const ppVerification = await getPushVerificationStatus(database); + if ( + ppVerification !== PUSH_PROXY_STATUS_UNKNOWN && + ppVerification !== '' + ) { + return; + } + + const appDatabase = DatabaseManager.appDatabase?.database; + if (!appDatabase) { + return; + } + + const deviceId = await getDeviceToken(appDatabase); + if (!deviceId) { + return; + } + + let client; + try { + client = NetworkManager.getClient(serverUrl); + } catch (err) { + return; + } + + try { + const response = await client.ping(deviceId); + const canReceiveNotifications = response?.data?.CanReceiveNotifications; + switch (canReceiveNotifications) { + case PUSH_PROXY_RESPONSE_NOT_AVAILABLE: + operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_NOT_AVAILABLE}], prepareRecordsOnly: false}); + return; + case PUSH_PROXY_RESPONSE_UNKNOWN: + return; + default: + operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}], prepareRecordsOnly: false}); + } + } catch (err) { + // Do nothing + } +} diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index 9fd484c3e..4558b2164 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -1,28 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel'; -import {MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; -import {fetchRolesIfNeeded, RolesRequest} from '@actions/remote/role'; +import {switchToChannelById} from '@actions/remote/channel'; import {getSessions} from '@actions/remote/session'; import {ConfigAndLicenseRequest, fetchConfigAndLicense} from '@actions/remote/systems'; -import {fetchMyTeams, MyTeamsRequest} from '@actions/remote/team'; -import {Preferences} from '@constants'; import DatabaseManager from '@database/manager'; -import {getPreferenceValue} from '@helpers/api/preference'; -import {selectDefaultTeam} from '@helpers/api/team'; import NetworkManager from '@managers/network_manager'; -import {prepareModels} from '@queries/servers/entry'; -import {prepareCommonSystemValues} from '@queries/servers/system'; -import {addChannelToTeamHistory, addTeamToTeamHistory} from '@queries/servers/team'; -import {selectDefaultChannelForTeam} from '@utils/channel'; +import {prepareCommonSystemValues, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {isTablet} from '@utils/helpers'; import {scheduleExpiredNotification} from '@utils/notification'; -import {deferredAppEntryActions} from './common'; +import {deferredAppEntryActions, entry, EntryResponse} from './common'; import type {Client} from '@client/rest'; -import type TeamModel from '@typings/database/models/servers/team'; type AfterLoginArgs = { serverUrl: string; @@ -54,20 +44,18 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) try { const isTabletDevice = await isTablet(); - let initialTeam: Team|TeamModel|undefined; - let initialChannel: Channel|undefined; - let myTeams: Team[]|undefined; // Fetch in parallel server config & license / user preferences / teams / team membership - const promises: [Promise, Promise, Promise] = [ + const promises: [Promise, Promise] = [ fetchConfigAndLicense(serverUrl, true), - fetchMyPreferences(serverUrl, true), - fetchMyTeams(serverUrl, true), + entry(serverUrl, '', ''), ]; - const [clData, prefData, teamData] = await Promise.all(promises); - let chData: MyChannelsRequest|undefined; - let rData: RolesRequest|undefined; + const [clData, entryData] = await Promise.all(promises); + + if ('error' in entryData) { + return {error: entryData.error}; + } // schedule local push notification if needed if (clData.config) { @@ -91,89 +79,22 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) } } - // select initial team - if (!clData.error && !prefData.error && !teamData.error) { - const teamOrderPreference = getPreferenceValue(prefData.preferences!, Preferences.TEAMS_ORDER, '', '') as string; - const teamRoles: string[] = []; - const teamMembers = new Set(); - - teamData.memberships?.forEach((tm) => { - teamRoles.push(...tm.roles.split(' ')); - teamMembers.add(tm.team_id); - }); - - myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id)); - initialTeam = selectDefaultTeam(myTeams, user.locale, teamOrderPreference, clData.config?.ExperimentalPrimaryTeam); - - if (initialTeam) { - const rolesToFetch = new Set([...user.roles.split(' '), ...teamRoles]); - - // fetch channels / channel membership for initial team - chData = await fetchMyChannelsForTeam(serverUrl, initialTeam.id, false, 0, true); - if (chData.channels?.length && chData.memberships?.length) { - const {channels, memberships} = chData; - const channelIds = new Set(channels?.map((c) => c.id)); - for (let i = 0; i < memberships!.length; i++) { - const member = memberships[i]; - if (channelIds.has(member.channel_id)) { - member.roles.split(' ').forEach(rolesToFetch.add, rolesToFetch); - } - } - - // fetch user roles - rData = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), true); - - // select initial channel only on Tablets - if (isTabletDevice) { - initialChannel = selectDefaultChannelForTeam(channels!, memberships!, initialTeam!.id, rData.roles, user.locale); - } - } - } + let switchToChannel = false; + const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; + if (initialChannelId && isTabletDevice) { + switchToChannel = true; + switchToChannelById(serverUrl, initialChannelId, initialTeamId); + } else { + setCurrentTeamAndChannelId(operator, initialTeamId, ''); } - const modelPromises = await prepareModels({operator, teamData, chData, prefData, initialTeamId: initialTeam?.id}); - - const systemModels = prepareCommonSystemValues( - operator, - { - config: clData.config || ({} as ClientConfig), - license: clData.license || ({} as ClientLicense), - currentTeamId: initialTeam?.id || '', - currentChannelId: initialChannel?.id || '', - }, - ); - if (systemModels) { - modelPromises.push(systemModels); - } - - if (initialTeam) { - const th = addTeamToTeamHistory(operator, initialTeam.id, true); - modelPromises.push(th); - } - - if (initialTeam && initialChannel) { - try { - const tch = addChannelToTeamHistory(operator, initialTeam.id, initialChannel.id, true); - modelPromises.push(tch); - } catch { - // do nothing - } - } - - if (rData?.roles?.length) { - const roles = operator.handleRole({roles: rData.roles, prepareRecordsOnly: true}); - modelPromises.push(roles); - } - - const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); + await operator.batchRecords(models); 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, initialTeam?.id, initialChannel?.id); + deferredAppEntryActions(serverUrl, 0, user.id, user.locale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined); - const error = clData.error || prefData.error || teamData.error || chData?.error; - return {error, time: Date.now() - dt, hasTeams: Boolean((myTeams?.length || 0) > 0 && !teamData.error)}; + return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)}; } catch (error) { const systemModels = await prepareCommonSystemValues(operator, { config: ({} as ClientConfig), diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index bb336aec3..773de8b21 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -2,12 +2,10 @@ // See LICENSE.txt for license information. import {switchToChannel} from '@actions/local/channel'; -import {markChannelAsRead} from '@actions/remote/channel'; -import {fetchRoles} from '@actions/remote/role'; +import {markChannelAsRead, switchToChannelById} from '@actions/remote/channel'; import {Screens} from '@constants'; import DatabaseManager from '@database/manager'; -import {queryChannelsById, getDefaultChannelForTeam, getMyChannel} from '@queries/servers/channel'; -import {prepareModels} from '@queries/servers/entry'; +import {getMyChannel} from '@queries/servers/channel'; import {getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; import {getMyTeamById} from '@queries/servers/team'; import {getCurrentUser} from '@queries/servers/user'; @@ -15,7 +13,7 @@ import EphemeralStore from '@store/ephemeral_store'; import {isTablet} from '@utils/helpers'; import {emitNotificationError} from '@utils/notification'; -import {deferredAppEntryActions, fetchAppEntryData, syncOtherServers, teamsToRemove} from './common'; +import {deferredAppEntryActions, entry, syncOtherServers} from './common'; export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -23,9 +21,10 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not return {error: `${serverUrl} database not found`}; } + const isTabletDevice = await isTablet(); + // We only reach this point if we have a channel Id in the notification payload const channelId = notification.payload!.channel_id!; - const isTabletDevice = await isTablet(); const {database} = operator; const currentTeamId = await getCurrentTeamId(database); const lastDisconnectedAt = await getWebSocketLastDisconnected(database); @@ -46,22 +45,17 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not // 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 switchedToTeamAndChanel = false; if (myChannel && myTeam) { - switchedToTeamAndChanel = true; await EphemeralStore.waitUntilScreenHasLoaded(Screens.HOME); markChannelAsRead(serverUrl, channelId); await switchToChannel(serverUrl, channelId, teamId); } - const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId); - - if ('error' in fetchedData) { - return {error: fetchedData.error}; + const entryData = await entry(serverUrl, teamId, channelId); + if ('error' in entryData) { + return {error: entryData.error}; } - - const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData; - const rolesData = await fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user, true); + const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; // There is a chance that after the above request returns // the user is no longer part of the team or channel @@ -72,68 +66,36 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not // 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) { - if (isTabletDevice) { - const channel = await getDefaultChannelForTeam(operator.database, selectedTeamId); - selectedChannelId = channel?.id || ''; - } else { - selectedChannelId = ''; - } + selectedChannelId = initialChannelId; } } - if (removeChannelIds?.includes(channelId)) { - // We are no longer a part of the channel that the notification belongs to - // Immediately set the new channel as the current channel in the database so that the UI - // renders the correct channel. - - if (isTabletDevice) { - const channel = await getDefaultChannelForTeam(operator.database, selectedTeamId); - selectedChannelId = channel?.id || ''; - } else { - selectedChannelId = ''; - } - } - - // If in the end the selected team or channel is different than the one from the notification - // we switch again - if (selectedTeamId !== teamId || selectedChannelId !== channelId) { + let switchedToChannel = false; + if (isTabletDevice || (selectedChannelId === channelId)) { + // Make switch again to get the missing data and make sure the team is the correct one + switchedToChannel = true; + switchToChannelById(serverUrl, selectedChannelId, selectedTeamId); + } else if (selectedTeamId !== teamId || selectedChannelId !== channelId) { + // If in the end the selected team or channel is different than the one from the notification + // we switch again setCurrentTeamAndChannelId(operator, selectedTeamId, selectedChannelId); } if (selectedTeamId !== teamId) { emitNotificationError('Team'); - } else if (selectedChannelId === channelId) { - if (!switchedToTeamAndChanel) { - markChannelAsRead(serverUrl, channelId); - switchToChannel(serverUrl, channelId, teamId); - } - } else { + } else if (selectedChannelId !== channelId) { emitNotificationError('Channel'); } - const removeTeams = await teamsToRemove(serverUrl, removeTeamIds); + await operator.batchRecords(models); - let removeChannels; - if (removeChannelIds?.length) { - removeChannels = await queryChannelsById(operator.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})); - } - - const models = await Promise.all(modelPromises); - await operator.batchRecords(models.flat()); - - const {id: currentUserId, locale: currentUserLocale} = meData.user || (await getCurrentUser(operator.database))!; + const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!; const {config, license} = await getCommonSystemValues(operator.database); - await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId); + await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, switchedToChannel ? selectedChannelId : undefined); syncOtherServers(serverUrl); - const error = teamData.error || chData?.error || prefData.error || meData.error; - return {error, userId: meData?.user?.id}; + + return {userId: currentUserId}; } diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index ae68f0446..20fea85fe 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -5,8 +5,7 @@ import {Model} from '@nozbe/watermelondb'; import {DeviceEventEmitter} from 'react-native'; import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team'; -import {switchToGlobalThreads} from '@actions/local/thread'; -import {Events, Screens} from '@constants'; +import {Events} from '@constants'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {prepareCategories, prepareCategoryChannels} from '@queries/servers/categories'; @@ -273,11 +272,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) { if (await isTablet()) { channelId = await getNthLastChannelFromTeam(database, teamId); if (channelId) { - if (channelId === Screens.GLOBAL_THREADS) { - await switchToGlobalThreads(serverUrl); - } else { - await switchToChannelById(serverUrl, channelId, teamId); - } + await switchToChannelById(serverUrl, channelId, teamId); DeviceEventEmitter.emit(Events.TEAM_SWITCH, false); return; } diff --git a/app/actions/websocket/channel.ts b/app/actions/websocket/channel.ts index ac06763af..4e453cdd1 100644 --- a/app/actions/websocket/channel.ts +++ b/app/actions/websocket/channel.ts @@ -22,7 +22,7 @@ import {Events, Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {queryActiveServer} from '@queries/app/servers'; import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel'; -import {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentChannelId} from '@queries/servers/system'; +import {prepareCommonSystemValues, getConfig, setCurrentChannelId, getCurrentChannelId, getCurrentTeamId} from '@queries/servers/system'; import {getNthLastChannelFromTeam} from '@queries/servers/team'; import {getCurrentUser, getTeammateNameDisplay, getUserById} from '@queries/servers/user'; import {dismissAllModals, popToRoot} from '@screens/navigation'; @@ -354,10 +354,14 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg: await popToRoot(); if (await isTablet()) { - const channelToJumpTo = await getNthLastChannelFromTeam(database, channel?.teamId); + let tId = channel.teamId; + if (!tId) { + tId = await getCurrentTeamId(database); + } + const channelToJumpTo = await getNthLastChannelFromTeam(database, tId); if (channelToJumpTo) { if (channelToJumpTo === Screens.GLOBAL_THREADS) { - const {models: switchToGlobalThreadsModels} = await switchToGlobalThreads(serverUrl, true); + const {models: switchToGlobalThreadsModels} = await switchToGlobalThreads(serverUrl, tId, true); if (switchToGlobalThreadsModels) { models.push(...switchToGlobalThreadsModels); } @@ -422,10 +426,14 @@ export async function handleChannelDeletedEvent(serverUrl: string, msg: WebSocke await popToRoot(); if (await isTablet()) { - const channelToJumpTo = await getNthLastChannelFromTeam(database, currentChannel?.teamId); + let tId = currentChannel.teamId; + if (!tId) { + tId = await getCurrentTeamId(database); + } + const channelToJumpTo = await getNthLastChannelFromTeam(database, tId); if (channelToJumpTo) { if (channelToJumpTo === Screens.GLOBAL_THREADS) { - switchToGlobalThreads(serverUrl); + switchToGlobalThreads(serverUrl, tId); return; } switchToChannel(serverUrl, channelToJumpTo); diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index ff034fe23..475197855 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -1,26 +1,22 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {markChannelAsViewed} from '@actions/local/channel'; -import {fetchMissingSidebarInfo, markChannelAsRead, switchToChannelById} from '@actions/remote/channel'; -import {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 {Screens, WebsocketEvents} from '@constants'; +import {DeviceEventEmitter} from 'react-native'; + +import {switchToChannelById} from '@actions/remote/channel'; +import {deferredAppEntryActions, entry} from '@actions/remote/entry/common'; +import {fetchStatusByIds} from '@actions/remote/user'; +import {Events, Screens, 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 {queryActiveServer} from '@queries/app/servers'; +import {getCurrentChannel} from '@queries/servers/channel'; +import {getCommonSystemValues, getConfig, getWebSocketLastDisconnected, resetWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; +import {getCurrentTeam} from '@queries/servers/team'; +import {getCurrentUser} from '@queries/servers/user'; +import {dismissAllModals, popToRoot} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; -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, @@ -91,140 +87,57 @@ async function doReconnect(serverUrl: string) { 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); - - if ('error' in fetchedData) { + const appDatabase = DatabaseManager.appDatabase?.database; + if (!appDatabase) { return; } - const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData; - const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true); - const profiles: UserProfile[] = []; + const {database} = operator; + const tabletDevice = await isTablet(); + const lastDisconnectedAt = await getWebSocketLastDisconnected(database); + resetWebSocketLastDisconnected(operator); + const currentTeam = await getCurrentTeam(database); + const currentChannel = await getCurrentChannel(database); - 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]; - }, [[], []]); - - 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); - } - } + const entryData = await entry(serverUrl, currentTeam?.id, currentChannel?.id, lastDisconnectedAt); + if ('error' in entryData) { + return; } + const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; - // 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); + let switchedToChannel = false; + // if no longer a member of the current team or the current channel + if (initialTeamId !== currentTeam?.id || initialChannelId !== currentChannel?.id) { + const currentServer = await queryActiveServer(appDatabase); const isChannelScreenMounted = EphemeralStore.getNavigationComponents().includes(Screens.CHANNEL); - - if (isChannelScreenMounted || tabletDevice) { - markChannelAsRead(serverUrl, currentChannelId); - markChannelAsViewed(serverUrl, currentChannelId); - } - - // 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); - } + if (serverUrl === currentServer?.url) { + if (currentTeam && initialTeamId !== currentTeam.id) { + DeviceEventEmitter.emit(Events.LEAVE_TEAM, {displayName: currentTeam.displayName}); + await dismissAllModals(); + await popToRoot(); + } else if (currentChannel && initialChannelId !== currentChannel.id && isChannelScreenMounted) { + DeviceEventEmitter.emit(Events.LEAVE_CHANNEL, {displayName: currentChannel?.displayName}); + await dismissAllModals(); + await popToRoot(); } + + if (tabletDevice && initialChannelId) { + switchedToChannel = true; + switchToChannelById(serverUrl, initialChannelId, initialTeamId); + } else { + setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId); + } + } else { + setCurrentTeamAndChannelId(operator, initialTeamId, initialChannelId); } } - fetchAllTeams(serverUrl); - updateAllUsersSince(serverUrl, lastDisconnectedAt); + await operator.batchRecords(models); + + const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!; + const {config, license} = await getCommonSystemValues(database); + await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchedToChannel ? initialChannelId : undefined); // https://mattermost.atlassian.net/browse/MM-41520 } diff --git a/app/screens/home/index.tsx b/app/screens/home/index.tsx index e3aef911d..37896302d 100644 --- a/app/screens/home/index.tsx +++ b/app/screens/home/index.tsx @@ -13,7 +13,7 @@ import {Events, Screens} from '@constants'; import {useTheme} from '@context/theme'; import {findChannels} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; -import {alertTeamRemove} from '@utils/navigation'; +import {alertChannelRemove, alertTeamRemove} from '@utils/navigation'; import {notificationError} from '@utils/notification'; import Account from './account'; @@ -62,6 +62,16 @@ export default function HomeScreen(props: HomeProps) { }; }, [intl.locale]); + useEffect(() => { + const listener = DeviceEventEmitter.addListener(Events.LEAVE_CHANNEL, (displayName: string) => { + alertChannelRemove(displayName, intl); + }); + + return () => { + listener.remove(); + }; + }, [intl.locale]); + useEffect(() => { const listener = HWKeyboardEvent.onHWKeyPressed((keyEvent: {pressedKey: string}) => { const screen = EphemeralStore.getAllNavigationComponents(); diff --git a/app/utils/navigation/index.ts b/app/utils/navigation/index.ts index a2d7023d9..b7e522b99 100644 --- a/app/utils/navigation/index.ts +++ b/app/utils/navigation/index.ts @@ -29,3 +29,20 @@ export async function alertTeamRemove(displayName: string, intl: IntlShape) { }], ); } + +export async function alertChannelRemove(displayName: string, intl: IntlShape) { + Alert.alert( + intl.formatMessage({ + id: 'alert.removed_from_channel.title', + defaultMessage: 'Removed from channel', + }), + intl.formatMessage({ + id: 'alert.removed_from_channel.description', + defaultMessage: 'You have been removed from channel {displayName}.', + }, {displayName}), + [{ + style: 'cancel', + text: intl.formatMessage({id: 'mobile.oauth.something_wrong.okButton', defaultMessage: 'OK'}), + }], + ); +}