diff --git a/app/actions/local/systems.ts b/app/actions/local/systems.ts new file mode 100644 index 000000000..3a192d990 --- /dev/null +++ b/app/actions/local/systems.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import deepEqual from 'deep-equal'; + +import {SYSTEM_IDENTIFIERS} from '@constants/database'; +import DatabaseManager from '@database/manager'; +import {getServerCredentials} from '@init/credentials'; +import {getCommonSystemValues} from '@queries/servers/system'; +import {logError} from '@utils/log'; + +export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) { + try { + // If we have credentials for this server then update the values in the database + const credentials = await getServerCredentials(serverUrl); + if (credentials) { + const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const current = await getCommonSystemValues(operator.database); + const systems: IdValue[] = []; + if (!deepEqual(config, current.config)) { + systems.push({ + id: SYSTEM_IDENTIFIERS.CONFIG, + value: JSON.stringify(config), + }); + } + + if (!deepEqual(license, current.license)) { + systems.push({ + id: SYSTEM_IDENTIFIERS.LICENSE, + value: JSON.stringify(license), + }); + } + + if (systems.length) { + await operator.handleSystem({systems, prepareRecordsOnly: false}); + } + } + } catch (error) { + logError('An error occurred while saving config & license', error); + } +} diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index a04bb97c9..7040e45c0 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -4,14 +4,14 @@ import {switchToChannelById} from '@actions/remote/channel'; import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; -import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getConfig, getCurrentChannelId} from '@queries/servers/system'; +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 {logDebug, logInfo} from '@utils/log'; +import {logInfo} from '@utils/log'; -import {deferredAppEntryActions, entry, registerDeviceToken, syncOtherServers, verifyPushProxy} from './common'; -import {graphQLCommon} from './gql_common'; +import {registerDeviceToken, syncOtherServers, verifyPushProxy} from './common'; +import {deferredAppEntryActions, entry} from './gql_common'; export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -19,8 +19,6 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) return {error: `${serverUrl} database not found`}; } - const {database} = operator; - if (!since) { registerDeviceToken(serverUrl); } @@ -31,34 +29,6 @@ export async function appEntry(serverUrl: string, since = 0, isUpgrade = false) operator.batchRecords(removeLastUnreadChannelId); } - const config = await getConfig(database); - let result; - if (config?.FeatureFlagGraphQL === 'true') { - const {currentTeamId, currentChannelId} = await getCommonSystemValues(database); - result = await graphQLCommon(serverUrl, true, currentTeamId, currentChannelId, '', isUpgrade); - if (result.error) { - logDebug('Error using GraphQL, trying REST', result.error); - result = restAppEntry(serverUrl, since, isUpgrade); - } - } else { - result = restAppEntry(serverUrl, since, isUpgrade); - } - - if (!since) { - // Load data from other servers - syncOtherServers(serverUrl); - } - - verifyPushProxy(serverUrl); - - return result; -} - -async function restAppEntry(serverUrl: string, since = 0, isUpgrade = false) { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return {error: `${serverUrl} database not found`}; - } const {database} = operator; const tabletDevice = await isTablet(); @@ -98,6 +68,13 @@ async function restAppEntry(serverUrl: string, since = 0, isUpgrade = false) { const {config, license} = await getCommonSystemValues(database); await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, switchToChannel ? initialChannelId : undefined); + if (!since) { + // Load data from other servers + syncOtherServers(serverUrl); + } + + verifyPushProxy(serverUrl); + return {userId: currentUserId}; } diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 483b9a22e..a57a9eb87 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -1,18 +1,19 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Model} from '@nozbe/watermelondb'; +import {Database, Model} from '@nozbe/watermelondb'; -import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, MyChannelsRequest, switchToChannelById} from '@actions/remote/channel'; +import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, MyChannelsRequest} from '@actions/remote/channel'; +import {fetchGroupsForMember} from '@actions/remote/groups'; 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 {fetchAndSwitchToThread, fetchNewThreads} from '@actions/remote/thread'; +import {fetchNewThreads} from '@actions/remote/thread'; import {fetchMe, MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; import {gqlAllChannels} from '@client/graphQL/entry'; -import {Preferences, Screens} from '@constants'; +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'; import DatabaseManager from '@database/manager'; @@ -22,22 +23,16 @@ import {DEFAULT_LOCALE} from '@i18n'; import NetworkManager from '@managers/network_manager'; import {getDeviceToken} from '@queries/app/global'; import {queryAllServers} from '@queries/app/servers'; -import {getMyChannel, prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel'; +import {prepareMyChannelsForTeam, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel'; import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry'; import {getHasCRTChanged} from '@queries/servers/preference'; -import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system'; -import {deleteMyTeams, getAvailableTeamIds, getMyTeamById, getNthLastChannelFromTeam, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team'; -import {getIsCRTEnabled} from '@queries/servers/thread'; -import NavigationStore from '@store/navigation_store'; -import {isDMorGM} from '@utils/channel'; +import {getConfig, getCurrentUserId, getPushVerificationStatus, getWebSocketLastDisconnected} from '@queries/servers/system'; +import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team'; +import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel'; import {getMemberChannelsFromGQLQuery, gqlToClientChannelMembership} from '@utils/graphql'; -import {isTablet} from '@utils/helpers'; import {logDebug} from '@utils/log'; -import {emitNotificationError} from '@utils/notification'; import {processIsCRTEnabled} from '@utils/thread'; -import {fetchGroupsForMember} from '../groups'; - import type ClientError from '@client/rest/error'; export type AppEntryData = { @@ -91,7 +86,7 @@ export const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) return []; }; -export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise => { +export const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -114,13 +109,7 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri 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 initialChannelId = await entryInitialChannelId(database, channelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships); const removeTeams = await teamsToRemove(serverUrl, removeTeamIds); @@ -279,7 +268,45 @@ export const fetchAlternateTeamData = async ( return {initialTeamId, removeTeamIds}; }; -export async function deferredAppEntryActions( +export async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) { + const membershipIds = new Set(memberships?.map((m) => m.channel_id)); + const requestedChannel = channels?.find((c) => (c.id === requestedChannelId) && membershipIds.has(c.id)); + + // If team and channel are the requested, return the channel + if (initialTeamId === requestedTeamId && requestedChannel) { + return requestedChannelId; + } + + // DM or GMs don't care about changes in teams, so return directly + if (requestedChannel && isDMorGM(requestedChannel)) { + return requestedChannelId; + } + + // Check if we are still members of any channel on the history + const teamChannelHistory = await getTeamChannelHistory(database, initialTeamId); + for (const c of teamChannelHistory) { + if (membershipIds.has(c) || c === Screens.GLOBAL_THREADS) { + return c; + } + } + + // Check if we are member of the default channel. + const defaultChannel = channels?.find((c) => c.name === General.DEFAULT_CHANNEL && c.team_id === initialTeamId); + const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannel && membershipIds.has(defaultChannel.id)); + if (iAmMemberOfTheTeamDefaultChannel) { + return defaultChannel!.id; + } + + // Get the first channel of the list, based on the locale. + const myFirstTeamChannel = channels?.filter((c) => + c.team_id === requestedTeamId && + c.type === General.OPEN_CHANNEL && + membershipIds.has(c.id), + ).sort(sortChannelsByDisplayName.bind(null, locale))[0]; + return myFirstTeamChannel?.id || ''; +} + +export async function restDeferredAppEntryActions( serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, initialTeamId?: string, initialChannelId?: string) { @@ -486,61 +513,3 @@ export async function verifyPushProxy(serverUrl: string) { // Do nothing } } - -export async function handleNotificationNavigation(serverUrl: string, selectedChannelId: string, selectedTeamId: string, notificationChannelId: string, notificationTeamId: string, rootId = '') { - try { - const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - - const myChannel = await getMyChannel(database, selectedChannelId); - const myTeam = await getMyTeamById(database, selectedTeamId); - 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, selectedChannelId, selectedTeamId); - } - switchedToScreen = true; - } - - if (!switchedToScreen) { - const isTabletDevice = await isTablet(); - if (isTabletDevice || (notificationChannelId === selectedChannelId)) { - // Make switch again to get the missing data and make sure the team is the correct one - switchedToScreen = true; - if (isThreadNotification) { - await fetchAndSwitchToThread(serverUrl, rootId, true); - } else { - switchedToChannel = true; - await switchToChannelById(serverUrl, notificationChannelId, notificationTeamId); - } - } else if (notificationTeamId !== selectedTeamId || notificationChannelId !== 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); - } - } - - if (notificationTeamId !== selectedTeamId) { - emitNotificationError('Team'); - } else if (notificationChannelId !== selectedChannelId) { - emitNotificationError('Channel'); - } - - // 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); - } - - return {}; - } catch (error) { - return {error}; - } -} diff --git a/app/actions/remote/entry/gql_common.ts b/app/actions/remote/entry/gql_common.ts index 797b3df06..9de797bde 100644 --- a/app/actions/remote/entry/gql_common.ts +++ b/app/actions/remote/entry/gql_common.ts @@ -3,12 +3,13 @@ import {Database} from '@nozbe/watermelondb'; -import {markChannelAsRead, MyChannelsRequest} from '@actions/remote/channel'; +import {storeConfigAndLicense} from '@actions/local/systems'; +import {MyChannelsRequest} from '@actions/remote/channel'; import {fetchGroupsForMember} from '@actions/remote/groups'; -import {fetchPostsForChannel, fetchPostsForUnreadChannels} from '@actions/remote/post'; +import {fetchPostsForUnreadChannels} from '@actions/remote/post'; import {MyTeamsRequest} from '@actions/remote/team'; import {fetchNewThreads} from '@actions/remote/thread'; -import {MyUserRequest, updateAllUsersSince} from '@actions/remote/user'; +import {updateAllUsersSince} from '@actions/remote/user'; import {gqlEntry, gqlEntryChannels, gqlOtherChannels} from '@client/graphQL/entry'; import {Preferences} from '@constants'; import DatabaseManager from '@database/manager'; @@ -17,14 +18,13 @@ import {selectDefaultTeam} from '@helpers/api/team'; import {queryAllChannels, queryAllChannelsForTeam} from '@queries/servers/channel'; import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry'; import {getHasCRTChanged} from '@queries/servers/preference'; -import {prepareCommonSystemValues} from '@queries/servers/system'; -import {addChannelToTeamHistory, addTeamToTeamHistory, queryMyTeams} from '@queries/servers/team'; -import {selectDefaultChannelForTeam} from '@utils/channel'; +import {getConfig} from '@queries/servers/system'; +import {queryMyTeams} from '@queries/servers/team'; import {filterAndTransformRoles, getMemberChannelsFromGQLQuery, getMemberTeamsFromGQLQuery, gqlToClientChannelMembership, gqlToClientPreference, gqlToClientSidebarCategory, gqlToClientTeamMembership, gqlToClientUser} from '@utils/graphql'; -import {isTablet} from '@utils/helpers'; +import {logDebug} from '@utils/log'; import {processIsCRTEnabled} from '@utils/thread'; -import {teamsToRemove, FETCH_UNREADS_TIMEOUT, handleNotificationNavigation} from './common'; +import {teamsToRemove, FETCH_UNREADS_TIMEOUT, entryRest, EntryResponse, entryInitialChannelId, restDeferredAppEntryActions} from './common'; import type ClientError from '@client/rest/error'; import type ChannelModel from '@typings/database/models/servers/channel'; @@ -33,14 +33,13 @@ import type TeamModel from '@typings/database/models/servers/team'; export async function deferredAppEntryGraphQLActions( serverUrl: string, since: number, - meData: MyUserRequest, + currentUserId: string, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, - isTabletDevice: boolean, + preferences: PreferenceType[] | undefined, + config: ClientConfig, initialTeamId?: string, initialChannelId?: string, - isCRTEnabled = false, - syncDatabase?: boolean, ) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { @@ -48,12 +47,6 @@ export async function deferredAppEntryGraphQLActions( } const {database} = operator; - // defer fetching posts for initial channel - if (initialChannelId && isTabletDevice) { - fetchPostsForChannel(serverUrl, initialChannelId); - markChannelAsRead(serverUrl, initialChannelId); - } - setTimeout(() => { if (chData?.channels?.length && chData.memberships?.length) { // defer fetching posts for unread channels on initial team @@ -61,7 +54,7 @@ export async function deferredAppEntryGraphQLActions( } }, FETCH_UNREADS_TIMEOUT); - if (isCRTEnabled) { + if (preferences && processIsCRTEnabled(preferences, config)) { if (initialTeamId) { await fetchNewThreads(serverUrl, initialTeamId, false); } @@ -77,16 +70,19 @@ export async function deferredAppEntryGraphQLActions( } if (initialTeamId) { - const result = await getChannelData(serverUrl, initialTeamId, meData.user!.id, true); + const result = await getChannelData(serverUrl, initialTeamId, currentUserId, true); if ('error' in result) { return result; } - const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false, syncDatabase); + const removeChannels = await getRemoveChannels(database, result.chData, initialTeamId, false); const modelPromises = await prepareModels({operator, removeChannels, chData: result.chData}, true); - modelPromises.push(operator.handleRole({roles: filterAndTransformRoles(result.roles), prepareRecordsOnly: true})); + const roles = filterAndTransformRoles(result.roles); + if (roles.length) { + modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true})); + } const models = (await Promise.all(modelPromises)).flat(); operator.batchRecords(models); @@ -98,30 +94,26 @@ export async function deferredAppEntryGraphQLActions( }, FETCH_UNREADS_TIMEOUT); } - if (meData.user?.id) { - // Fetch groups for current user - fetchGroupsForMember(serverUrl, meData.user?.id); - } + // Fetch groups for current user + fetchGroupsForMember(serverUrl, currentUserId); updateAllUsersSince(serverUrl, since); - return {}; + return {error: undefined}; } -const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean, syncDatabase?: boolean) => { +const getRemoveChannels = async (database: Database, chData: MyChannelsRequest | undefined, initialTeamId: string, singleTeam: boolean) => { const removeChannels: ChannelModel[] = []; - if (syncDatabase) { - if (chData?.channels) { - const fetchedChannelIds = chData.channels?.map((channel) => channel.id); + if (chData?.channels) { + const fetchedChannelIds = chData.channels?.map((channel) => channel.id); - const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database); - const channels = await query.fetch(); + const query = singleTeam ? queryAllChannelsForTeam(database, initialTeamId) : queryAllChannels(database); + const channels = await query.fetch(); - for (const channel of channels) { - const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== ''; - if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) { - removeChannels.push(channel); - } + for (const channel of channels) { + const excludeCondition = singleTeam ? true : channel.teamId !== initialTeamId && channel.teamId !== ''; + if (excludeCondition && !fetchedChannelIds?.includes(channel.id)) { + removeChannels.push(channel); } } } @@ -158,17 +150,13 @@ const getChannelData = async (serverUrl: string, initialTeamId: string, userId: return {chData, roles}; }; -export const graphQLCommon = async (serverUrl: string, syncDatabase: boolean, currentTeamId: string, currentChannelId: string, rootId?: string, isUpgrade = false, isNotificationEntry = false) => { - const dt = Date.now(); - +export const entryGQL = async (serverUrl: string, currentTeamId?: string, currentChannelId?: string): Promise => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; } const {database} = operator; - const isTabletDevice = await isTablet(); - let response; try { response = await gqlEntry(serverUrl); @@ -188,6 +176,7 @@ export const graphQLCommon = async (serverUrl: string, syncDatabase: boolean, cu const config = fetchedData.config || {} as ClientConfig; const license = fetchedData.license || {} as ClientLicense; + await storeConfigAndLicense(serverUrl, config, license); const meData = { user: gqlToClientUser(fetchedData.user!), @@ -212,13 +201,6 @@ export const graphQLCommon = async (serverUrl: string, syncDatabase: boolean, cu } } - if (isUpgrade && meData?.user) { - const me = await prepareCommonSystemValues(operator, {currentUserId: meData.user.id}); - if (me?.length) { - await operator.batchRecords(me); - } - } - let initialTeamId = currentTeamId; if (!teamData.teams.length) { initialTeamId = ''; @@ -244,72 +226,64 @@ export const graphQLCommon = async (serverUrl: string, syncDatabase: boolean, cu const roles = filterAndTransformRoles(gqlRoles); - let initialChannelId = currentChannelId; - if (initialTeamId !== currentTeamId || !chData?.channels?.find((c) => c.id === currentChannelId)) { - initialChannelId = ''; - if (isTabletDevice && chData?.channels && chData.memberships) { - initialChannelId = selectDefaultChannelForTeam(chData.channels, chData.memberships, initialTeamId, roles, meData.user.locale)?.id || ''; - } - } - + const initialChannelId = await entryInitialChannelId(database, currentChannelId, currentTeamId, initialTeamId, meData.user.id, chData?.channels, chData?.memberships); let removeTeams: TeamModel[] = []; - const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true, syncDatabase); + const removeChannels = await getRemoveChannels(database, chData, initialTeamId, true); - if (syncDatabase) { - const removeTeamIds = []; + const removeTeamIds = []; - const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0); - if (removedFromTeam?.length) { - removeTeamIds.push(...removedFromTeam.map((m) => m.team_id)); - } - - if (teamData.teams?.length === 0) { - // User is no longer a member of any team - const myTeams = await queryMyTeams(database).fetch(); - removeTeamIds.push(...(myTeams?.map((myTeam) => myTeam.id) || [])); - } - - removeTeams = await teamsToRemove(serverUrl, removeTeamIds); + const removedFromTeam = teamData.memberships?.filter((m) => m.delete_at > 0); + if (removedFromTeam?.length) { + removeTeamIds.push(...removedFromTeam.map((m) => m.team_id)); } + if (teamData.teams?.length === 0) { + // User is no longer a member of any team + const myTeams = await queryMyTeams(database).fetch(); + removeTeamIds.push(...(myTeams?.map((myTeam) => myTeam.id) || [])); + } + + removeTeams = await teamsToRemove(serverUrl, removeTeamIds); + const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}, true); - modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true})); - modelPromises.push(prepareCommonSystemValues( - operator, - { - config, - license, - currentTeamId: initialTeamId, - currentChannelId: initialChannelId, - }, - )); - - if (initialTeamId && initialTeamId !== currentTeamId) { - const th = addTeamToTeamHistory(operator, initialTeamId, true); - modelPromises.push(th); + if (roles.length) { + modelPromises.push(operator.handleRole({roles, prepareRecordsOnly: true})); } - - if (initialTeamId !== currentTeamId && initialChannelId) { - try { - const tch = addChannelToTeamHistory(operator, initialTeamId, initialChannelId, true); - modelPromises.push(tch); - } catch { - // do nothing - } - } - - const models = await Promise.all(modelPromises); - if (models.length) { - await operator.batchRecords(models.flat()); - } - - if (isNotificationEntry) { - await handleNotificationNavigation(serverUrl, initialChannelId, initialTeamId, currentChannelId, currentTeamId, rootId); - } - - const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, config)); - deferredAppEntryGraphQLActions(serverUrl, 0, meData, teamData, chData, isTabletDevice, initialTeamId, initialChannelId, isCRTEnabled, syncDatabase); - - const timeElapsed = Date.now() - dt; - return {time: timeElapsed, hasTeams: Boolean(teamData.teams.length), userId: meData.user.id, error: undefined}; + const models = (await Promise.all(modelPromises)).flat(); + return {models, initialTeamId, initialChannelId, prefData, teamData, chData, meData}; }; + +export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise => { + const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); + const config = await getConfig(database); + let result; + if (config?.FeatureFlagGraphQL === 'true') { + result = await entryGQL(serverUrl, teamId, channelId); + if ('error' in result) { + logDebug('Error using GraphQL, trying REST', result.error); + result = entryRest(serverUrl, teamId, channelId, since); + } + } else { + result = entryRest(serverUrl, teamId, channelId, since); + } + + return result; +}; + +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) { + let result; + if (config?.FeatureFlagGraphQL === 'true') { + result = await deferredAppEntryGraphQLActions(serverUrl, since, currentUserId, teamData, chData, preferences, config, initialTeamId, initialChannelId); + if (result.error) { + logDebug('Error using GraphQL, trying REST', result.error); + result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId); + } + } else { + result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId); + } + + return result; +} diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index 05945e93c..4b34396fc 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -3,16 +3,15 @@ import {switchToChannelById} from '@actions/remote/channel'; import {getSessions} from '@actions/remote/session'; -import {ConfigAndLicenseRequest, fetchConfigAndLicense} from '@actions/remote/systems'; +import {fetchConfigAndLicense} from '@actions/remote/systems'; import DatabaseManager from '@database/manager'; import NetworkManager from '@managers/network_manager'; import {setCurrentTeamAndChannelId} from '@queries/servers/system'; import {isTablet} from '@utils/helpers'; -import {logDebug, logWarning} from '@utils/log'; +import {logWarning} from '@utils/log'; import {scheduleExpiredNotification} from '@utils/notification'; -import {deferredAppEntryActions, entry} from './common'; -import {graphQLCommon} from './gql_common'; +import {deferredAppEntryActions, entry} from './gql_common'; import type {Client} from '@client/rest'; @@ -22,13 +21,9 @@ type AfterLoginArgs = { deviceToken?: string; } -type SpecificAfterLoginArgs = { - serverUrl: string; - user: UserProfile; - clData: ConfigAndLicenseRequest; -} - export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs): Promise<{error?: any; hasTeams?: boolean; time?: number}> { + const dt = Date.now(); + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -50,7 +45,7 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) } try { - const clData = await fetchConfigAndLicense(serverUrl, true); + const clData = await fetchConfigAndLicense(serverUrl, false); if (clData.error) { return {error: clData.error}; } @@ -76,51 +71,32 @@ export async function loginEntry({serverUrl, user, deviceToken}: AfterLoginArgs) } } - if (clData.config?.FeatureFlagGraphQL === 'true') { - const result = await graphQLCommon(serverUrl, false, '', ''); - if (!result.error) { - return result; - } - logDebug('Error using GraphQL, trying REST', result.error); + const entryData = await entry(serverUrl, '', ''); + + if ('error' in entryData) { + return {error: entryData.error}; } - return restLoginEntry({serverUrl, user, clData}); + 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); + + 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)}; } catch (error) { return {error}; } } - -const restLoginEntry = async ({serverUrl, user, clData}: SpecificAfterLoginArgs) => { - const dt = Date.now(); - - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return {error: `${serverUrl} database not found`}; - } - - const entryData = await entry(serverUrl, '', ''); - - if ('error' in entryData) { - return {error: entryData.error}; - } - - const isTabletDevice = await isTablet(); - - const {models, initialTeamId, initialChannelId, prefData, teamData, chData} = entryData; - - let switchToChannel = false; - if (initialChannelId && isTabletDevice) { - switchToChannel = true; - switchToChannelById(serverUrl, initialChannelId, initialTeamId); - } else { - setCurrentTeamAndChannelId(operator, initialTeamId, ''); - } - - 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, initialTeamId, switchToChannel ? initialChannelId : undefined); - - return {time: Date.now() - dt, hasTeams: Boolean(teamData.teams?.length)}; -}; diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index ef7535943..5365afa8a 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -1,19 +1,25 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {switchToChannelById} from '@actions/remote/channel'; +import {fetchAndSwitchToThread} from '@actions/remote/thread'; import {Preferences, Screens} from '@constants'; import {getDefaultThemeByAppearance} from '@context/theme'; import DatabaseManager from '@database/manager'; +import {getMyChannel} from '@queries/servers/channel'; import {queryPreferencesByCategoryAndName} from '@queries/servers/preference'; -import {getCommonSystemValues, getConfig, getCurrentTeamId, getWebSocketLastDisconnected} from '@queries/servers/system'; +import {getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} 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 {logDebug} from '@utils/log'; +import {isTablet} from '@utils/helpers'; +import {emitNotificationError} from '@utils/notification'; import {setThemeDefaults, updateThemeIfNeeded} from '@utils/theme'; -import {deferredAppEntryActions, entry, handleNotificationNavigation, syncOtherServers} from './common'; -import {graphQLCommon} from './gql_common'; +import {syncOtherServers} from './common'; +import {deferredAppEntryActions, entry} from './gql_common'; export async function pushNotificationEntry(serverUrl: string, notification: NotificationWithData) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; @@ -27,6 +33,8 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not 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; @@ -54,30 +62,25 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME); - const config = await getConfig(database); - let result; - if (config?.FeatureFlagGraphQL === 'true') { - result = await graphQLCommon(serverUrl, true, teamId, channelId, rootId, false, true); - if (result.error) { - logDebug('Error using GraphQL, trying REST', result.error); - result = restNotificationEntry(serverUrl, teamId, channelId, rootId, isDirectChannel); + // 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); + + 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); } - } else { - result = restNotificationEntry(serverUrl, teamId, channelId, rootId, isDirectChannel); + switchedToScreen = true; } - syncOtherServers(serverUrl); - - return result; -} - -const restNotificationEntry = async (serverUrl: string, teamId: string, channelId: string, rootId: string, isDirectChannel: boolean) => { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return {error: `${serverUrl} database not found`}; - } - const {database} = operator; - const entryData = await entry(serverUrl, teamId, channelId); if ('error' in entryData) { return {error: entryData.error}; @@ -99,16 +102,45 @@ const restNotificationEntry = async (serverUrl: string, teamId: string, channelI } } - await operator.batchRecords(models); + 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) { + await fetchAndSwitchToThread(serverUrl, rootId, true); + } else { + switchedToChannel = true; + await switchToChannelById(serverUrl, channelId, teamId); + } + } 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); + } + } - await handleNotificationNavigation(serverUrl, selectedChannelId, selectedTeamId, channelId, teamId, rootId); + if (teamId !== selectedTeamId) { + emitNotificationError('Team'); + } else if (channelId !== selectedChannelId) { + emitNotificationError('Channel'); + } + + // 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); const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(operator.database))!; const {config, license} = await getCommonSystemValues(operator.database); - const lastDisconnectedAt = await getWebSocketLastDisconnected(database); - await deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId); + syncOtherServers(serverUrl); + return {userId: currentUserId}; -}; +} diff --git a/app/actions/remote/groups.ts b/app/actions/remote/groups.ts index f0919f39e..0ea353c85 100644 --- a/app/actions/remote/groups.ts +++ b/app/actions/remote/groups.ts @@ -30,6 +30,10 @@ export const fetchGroupsForAutocomplete = async (serverUrl: string, query: strin const client: Client = NetworkManager.getClient(serverUrl); const response = await client.getGroups({query, includeMemberCount: true}); + if (!response.length) { + return []; + } + return operator.handleGroups({groups: response, prepareRecordsOnly: fetchOnly}); } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); @@ -51,6 +55,10 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet const groups = (await Promise.all(promises)).flat(); // Save locally + if (!groups.length) { + return []; + } + return operator.handleGroups({groups, prepareRecordsOnly: fetchOnly}); } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); @@ -64,6 +72,10 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string const client = NetworkManager.getClient(serverUrl); const response = await client.getAllGroupsAssociatedToChannel(channelId); + if (!response.groups.length) { + return {groups: [], groupChannels: []}; + } + const [groups, groupChannels] = await Promise.all([ operator.handleGroups({groups: response.groups, prepareRecordsOnly: true}), operator.handleGroupChannelsForChannel({groups: response.groups, channelId, prepareRecordsOnly: true}), @@ -87,6 +99,10 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc const client: Client = NetworkManager.getClient(serverUrl); const response = await client.getAllGroupsAssociatedToTeam(teamId); + if (!response.groups.length) { + return {groups: [], groupTeams: []}; + } + const [groups, groupTeams] = await Promise.all([ operator.handleGroups({groups: response.groups, prepareRecordsOnly: true}), operator.handleGroupTeamsForTeam({groups: response.groups, teamId, prepareRecordsOnly: true}), @@ -109,6 +125,10 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe const client: Client = NetworkManager.getClient(serverUrl); const response = await client.getAllGroupsAssociatedToMembership(userId); + if (!response.length) { + return {groups: [], groupMemberships: []}; + } + const [groups, groupMemberships] = await Promise.all([ operator.handleGroups({groups: response, prepareRecordsOnly: true}), operator.handleGroupMembershipsForMember({groups: response, userId, prepareRecordsOnly: true}), diff --git a/app/actions/remote/systems.ts b/app/actions/remote/systems.ts index 3fc329c7d..d8525f2e4 100644 --- a/app/actions/remote/systems.ts +++ b/app/actions/remote/systems.ts @@ -1,14 +1,11 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import deepEqual from 'deep-equal'; - +import {storeConfigAndLicense} from '@actions/local/systems'; import {forceLogoutIfNecessary} from '@actions/remote/session'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; -import {getServerCredentials} from '@init/credentials'; import NetworkManager from '@managers/network_manager'; -import {getCommonSystemValues} from '@queries/servers/system'; import {logError} from '@utils/log'; import type ClientError from '@client/rest/error'; @@ -65,34 +62,8 @@ export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false client.getClientLicenseOld(), ]); - // If we have credentials for this server then update the values in the database if (!fetchOnly) { - const credentials = await getServerCredentials(serverUrl); - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (credentials && operator) { - const current = await getCommonSystemValues(operator.database); - const systems: IdValue[] = []; - if (!deepEqual(config, current.config)) { - systems.push({ - id: SYSTEM_IDENTIFIERS.CONFIG, - value: JSON.stringify(config), - }); - } - - if (!deepEqual(license, current.license)) { - systems.push({ - id: SYSTEM_IDENTIFIERS.LICENSE, - value: JSON.stringify(license), - }); - } - - if (systems.length) { - await operator.handleSystem({systems, prepareRecordsOnly: false}). - catch((error) => { - logError('An error occurred while saving config & license', error); - }); - } - } + storeConfigAndLicense(serverUrl, config, license); } return {config, license}; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 085abb1fd..fa9d98197 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -4,9 +4,7 @@ import {DeviceEventEmitter} from 'react-native'; import {switchToChannelById} from '@actions/remote/channel'; -import {deferredAppEntryActions, entry} from '@actions/remote/entry/common'; -import {graphQLCommon} from '@actions/remote/entry/gql_common'; -import {fetchConfigAndLicense} from '@actions/remote/systems'; +import {deferredAppEntryActions, entry} from '@actions/remote/entry/gql_common'; import {fetchStatusByIds} from '@actions/remote/user'; import {loadConfigAndCalls} from '@calls/actions/calls'; import { @@ -29,7 +27,6 @@ import {isSupportedServerCalls} from '@calls/utils'; import {Events, Screens, WebsocketEvents} from '@constants'; import {SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; -import ServerDataOperator from '@database/operator/server_data_operator'; import {getActiveServerUrl, queryActiveServer} from '@queries/app/servers'; import {getCurrentChannel} from '@queries/servers/channel'; import { @@ -45,7 +42,7 @@ import {getCurrentUser} from '@queries/servers/user'; import {dismissAllModals, popToRoot} from '@screens/navigation'; import NavigationStore from '@store/navigation_store'; import {isTablet} from '@utils/helpers'; -import {logDebug, logInfo} from '@utils/log'; +import {logInfo} from '@utils/log'; import {handleCategoryCreatedEvent, handleCategoryDeletedEvent, handleCategoryOrderUpdatedEvent, handleCategoryUpdatedEvent} from './category'; import {handleChannelConvertedEvent, handleChannelCreatedEvent, @@ -116,13 +113,22 @@ export async function handleClose(serverUrl: string, lastDisconnect: number) { }); } -async function doReconnectRest(serverUrl: string, operator: ServerDataOperator, currentTeamId: string, currentUserId: string, config: ClientConfig, license: ClientLicense, lastDisconnectedAt: number) { +async function doReconnect(serverUrl: string) { + const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; + if (!operator) { + return; + } + const appDatabase = DatabaseManager.appDatabase?.database; if (!appDatabase) { return; } const {database} = operator; + + const lastDisconnectedAt = await getWebSocketLastDisconnected(database); + resetWebSocketLastDisconnected(operator); + const currentTeam = await getCurrentTeam(database); const currentChannel = await getCurrentChannel(database); const currentActiveServerUrl = await getActiveServerUrl(DatabaseManager.appDatabase!.database); @@ -170,51 +176,15 @@ async function doReconnectRest(serverUrl: string, operator: ServerDataOperator, await operator.batchRecords(models); logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`); - const {locale: currentUserLocale} = (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, switchedToChannel ? initialChannelId : undefined); - // https://mattermost.atlassian.net/browse/MM-41520 -} - -async function doReconnect(serverUrl: string) { - const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; - if (!operator) { - return; - } - - const {database} = operator; - 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 currentActiveServerUrl = await getActiveServerUrl(DatabaseManager.appDatabase!.database); - if (serverUrl === currentActiveServerUrl) { - DeviceEventEmitter.emit(Events.FETCHING_POSTS, true); - } - - if (config.FeatureFlagGraphQL === 'true') { - const {error} = await graphQLCommon(serverUrl, true, system.currentTeamId, system.currentChannelId); - if (error) { - logDebug('Error using GraphQL, trying REST', error); - await doReconnectRest(serverUrl, operator, system.currentTeamId, system.currentUserId, config, license, lastDisconnectedAt); - } - } else { - await doReconnectRest(serverUrl, operator, system.currentTeamId, system.currentUserId, config, license, lastDisconnectedAt); - } - - // Calls is not set up for GraphQL yet if (isSupportedServerCalls(config?.Version)) { - loadConfigAndCalls(serverUrl, system.currentUserId); + loadConfigAndCalls(serverUrl, currentUserId); } + + // https://mattermost.atlassian.net/browse/MM-41520 } export async function handleEvent(serverUrl: string, msg: WebSocketMessage) { diff --git a/app/client/graphQL/entry.ts b/app/client/graphQL/entry.ts index 241b8eb2f..eb1625b93 100644 --- a/app/client/graphQL/entry.ts +++ b/app/client/graphQL/entry.ts @@ -1,10 +1,9 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Client} from '@client/rest'; import {MEMBERS_PER_PAGE} from '@constants/graphql'; import NetworkManager from '@managers/network_manager'; -import {Client} from '../rest'; - import QueryNames from './constants'; const doGQLQuery = async (serverUrl: string, query: string, variables: {[name: string]: any}, operationName: string) => { @@ -321,6 +320,11 @@ query ${QueryNames.QUERY_ALL_CHANNELS}($perPage: Int!){ schemeAdmin lastViewedAt notifyProps + roles { + id + name + permissions + } channel { id header @@ -358,6 +362,11 @@ query ${QueryNames.QUERY_ALL_CHANNELS_NEXT}($perPage: Int!, $cursor: String!) { schemeAdmin lastViewedAt notifyProps + roles { + id + name + permissions + } channel { id header