Refactor entry to better share between rest and GQL (#6638)

* Refactor entry to better share between rest and GQL

* Address feedback
This commit is contained in:
Daniel Espino García 2022-09-12 17:27:47 +02:00 committed by GitHub
parent a32022ba5c
commit 260e1cfde7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 328 additions and 389 deletions

View file

@ -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);
}
}

View file

@ -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};
}

View file

@ -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<EntryResponse> => {
export const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
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};
}
}

View file

@ -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<EntryResponse> => {
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<EntryResponse> => {
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;
}

View file

@ -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)};
};

View file

@ -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};
};
}

View file

@ -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}),

View file

@ -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};

View file

@ -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) {

View file

@ -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