mattermost-mobile/app/actions/remote/entry/deferred.ts
Rahim Rahman 882097a668
feat(MM-63937): fetch initial team data only on entry (#8894)
* feat(MM-63936): add mark function in performance metrics manager
* feat(MM-63563): fetch initial team data ONLY on entry
* remove all performance mark()
* revert changes to processEntryModels
* using processNextTeam to download team in sequence
* separate restDeferredAppEntryActions into two parts
* remove console.log
* fixed issue with conflict post.id
* regression from fetchPostsForUnreadChannels
* extracted deferred functions into deferred.ts
* missing mock
* extract combineChannelsData to it's own function
* remove isDelete
* use logDebug vs logInfo
* add logDebug and logWarning in the mocking
* add combineChannelsData test
* revert the use of logDebug
* spelling errors
* undo logDebug changes
* moving fetchMissingDirectChannelsInfo into setTimeout saved quite a bit of time when it comes to TTI
* changes to accommodate other recent changes
* keep the channelsMap smaller by moving delete_at.
* using for loop instead of recursive
* making this function more readable
* add jsdoc
* changes based on review
* team will not get omitted from the sidebar if there is an error
* fix test as well
* added a new test
* remove comments on categories
* add categories to fix failing test
* fix failing test
2025-08-18 10:06:15 -06:00

203 lines
8.9 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, type MyChannelsRequest} from '@actions/remote/channel';
import {fetchGroupsForMember} from '@actions/remote/groups';
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {fetchRoles} from '@actions/remote/role';
import {fetchScheduledPosts} from '@actions/remote/scheduled_post';
import {fetchTeamsThreads, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread';
import {autoUpdateTimezone, updateAllUsersSince, type MyUserRequest} from '@actions/remote/user';
import {Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {processEntryModels, processEntryModelsForDeletion} from '@queries/servers/entry';
import {isDMorGM} from '@utils/channel';
import {logError} from '@utils/log';
import {processIsCRTEnabled} from '@utils/thread';
export async function deferredAppEntryActions(
serverUrl: string,
since: number,
currentUserId: string, currentUserLocale: string,
preferences: PreferenceType[] | undefined,
config: ClientConfig,
license: ClientLicense | undefined,
teamData: MyTeamsRequest,
chData: MyChannelsRequest | undefined,
meData: MyUserRequest | undefined,
initialTeamId?: string, initialChannelId?: string,
groupLabel?: BaseRequestGroupLabel,
) {
const result = restDeferredAppEntryActions(
serverUrl,
since,
currentUserId,
currentUserLocale,
preferences,
config,
license,
teamData,
chData,
meData,
initialTeamId,
initialChannelId,
groupLabel,
);
autoUpdateTimezone(serverUrl, groupLabel);
return result;
}
export async function restDeferredAppEntryActions(
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, meData: MyUserRequest | undefined,
initialTeamId?: string, initialChannelId?: string, groupLabel?: BaseRequestGroupLabel,
) {
const isCRTEnabled = (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) || false;
const directChannels = chData?.channels?.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);
const requestLabel: RequestGroupLabel|undefined = groupLabel ? `${groupLabel} Deferred` : undefined;
setTimeout(async () => {
// sidebar DM & GM profiles
if (channelsToFetchProfiles.size) {
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId, false, requestLabel);
}
updateAllUsersSince(serverUrl, since, false, requestLabel);
updateCanJoinTeams(serverUrl);
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const teamMap = new Map(teamData.teams?.map((t) => [t.id, t]) || []);
let mySortedTeams: Team[] = [];
let myOtherSortedTeams: Team[] = [];
let teamQueue: Team[] = [];
const combinedChannelsData: MyChannelsRequest = {
channels: [],
memberships: [],
...chData,
};
const processTeams = async () => {
for (const team of teamQueue) {
let data: MyChannelsRequest = {};
try {
/* eslint-disable-next-line no-await-in-loop */
data = await fetchMyChannelsForTeam(serverUrl, team.id, false, since, true, false, isCRTEnabled, requestLabel);
combineChannelsData(combinedChannelsData, data);
} catch (error) {
logError('Error fetching channels for team', groupLabel, error);
}
const currentTeamData: MyTeamsRequest = {
teams: [team],
memberships: teamData?.memberships?.filter((m) => m.team_id === team.id),
};
/* eslint-disable-next-line no-await-in-loop */
await processEntryModels({operator, teamData: currentTeamData, chData: data, isCRTEnabled});
}
const uniqueChannelsData: MyChannelsRequest = {
channels: Array.from(new Map(combinedChannelsData.channels?.map((c) => [c.id, c])).values()),
memberships: Array.from(new Map(combinedChannelsData.memberships?.map((m) => [m.channel_id, m])).values()),
categories: Array.from(new Map(combinedChannelsData.categories?.map((c) => [c.id, c])).values()),
};
await processFinalInitializationTasks(uniqueChannelsData);
};
const processFinalInitializationTasks = async (uniqueChannelsData: MyChannelsRequest) => {
try {
fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData?.user, false, false, groupLabel);
if (initialTeamId) {
const initialTeam = teamMap.get(initialTeamId);
if (initialTeam) {
mySortedTeams = [initialTeam, ...myOtherSortedTeams];
}
}
// previously we were deleting the models via processEntryModels, but we don't want to delete any
// teams or channels until we have fetched all the data for the team, which now is later in the code
processEntryModelsForDeletion({serverUrl, operator, teamData, chData: uniqueChannelsData});
if (uniqueChannelsData?.channels?.length && uniqueChannelsData.memberships?.length && initialTeamId) {
if (isCRTEnabled && initialTeamId) {
await syncTeamThreads(serverUrl, initialTeamId, {groupLabel: requestLabel});
}
fetchPostsForUnreadChannels(serverUrl, mySortedTeams, uniqueChannelsData.channels, uniqueChannelsData.memberships, initialChannelId, isCRTEnabled, requestLabel);
}
if (myOtherSortedTeams.length) {
fetchTeamsThreads(serverUrl, since, myOtherSortedTeams, isCRTEnabled, false, requestLabel);
}
// Fetch groups for current user
await fetchGroupsForMember(serverUrl, currentUserId, false, requestLabel);
if (initialTeamId) {
await fetchScheduledPosts(serverUrl, initialTeamId, true, groupLabel);
}
} catch (error) {
logError('Error in processFinalInitializationTasks', groupLabel, error);
}
};
if (teamData.teams?.length && teamData.memberships?.length) {
mySortedTeams = sortTeamsByPreferences(teamData, preferences, teamMap);
myOtherSortedTeams = mySortedTeams.filter((t) => t.id !== initialTeamId);
teamQueue = [...myOtherSortedTeams];
}
processTeams();
});
}
function sortTeamsByPreferences(teamData: MyTeamsRequest, preferences: PreferenceType[] | undefined, teamMap: Map<string, Team>): Team[] {
const teamsOrder = preferences?.find((p) => p.category === Preferences.CATEGORIES.TEAMS_ORDER);
const sortedTeamIds = new Set(teamsOrder?.value.split(','));
const membershipSet = new Set(teamData.memberships?.map((m) => m.team_id) || []);
if (sortedTeamIds.size) {
const sortedTeams = [...sortedTeamIds].
filter((id) => membershipSet.has(id) && teamMap.has(id)).
map((id) => teamMap.get(id)!);
const extraTeams = [...membershipSet].
filter((id) => !sortedTeamIds.has(id) && teamMap.get(id)).
map((id) => teamMap.get(id)!).
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
return [...sortedTeams, ...extraTeams];
}
return teamData.teams?.
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase())) || [];
}
function combineChannelsData(target: MyChannelsRequest, source: MyChannelsRequest) {
if (source.channels) {
target.channels = target.channels?.concat(source.channels);
}
if (source.memberships) {
target.memberships = target.memberships?.concat(source.memberships);
}
if (source.categories) {
target.categories = target.categories?.concat(source.categories);
}
}
export const testExports = {
combineChannelsData,
sortTeamsByPreferences,
};