Request metrics improvements (#8420)
* Network metrics * update network-client ref * fix unit tests * missing catch error parsing * Replace API's to fetch all teams and channels for a user Update groupLabel to use a type definition Include effective Latency and Average Speed in the metrics Include parallel and sequential request counts * update network library and fix tests * feat(MM-61865): send network request telemetry data to the server (#8436) * feat(MM-61865): send network info telemetry data * unit testing * fix latency vs effectiveLatency * cleanup test * fix failing tests * fix spelling error * fix failing test * more cleanup * fix: improve parallel requests calculation (#8439) * fix: improve parallel requests calculation * fix test issue * multiple parallelGroups testing * calculateAverageSpeedWithCategories test * categorizedRequests in it's own describe * clean up duplicate tests * check for when groupLabel is non-existent * a case when groupLabel is non-existent * more testing with effective latency. * resolveAndFlattenModels with a capital F * add try..catch within resolveAndFlattenModels * update groupLabel to fix failing lint * fix linting issue due to unknown group label * forgot to push changes * fix the indentation problem again * add env var option for COLLECT_NETWORK_METRICS --------- Co-authored-by: Rahim Rahman <rahim.rahman@mattermost.com> Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
a314c6059f
commit
d1d3fda83a
61 changed files with 1795 additions and 891 deletions
|
|
@ -12,6 +12,7 @@ import PushNotifications from '@init/push_notifications';
|
|||
import {
|
||||
prepareDeleteChannel, prepareMyChannelsForTeam, queryAllMyChannel,
|
||||
getMyChannel, getChannelById, queryUsersOnChannel, queryUserChannelsByTypes,
|
||||
prepareAllMyChannels,
|
||||
} from '@queries/servers/channel';
|
||||
import {queryDisplayNamePreferences} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, type PrepareCommonSystemValuesArgs, getCommonSystemValues, getCurrentTeamId, setCurrentChannelId, getCurrentUserId, getConfig, getLicense} from '@queries/servers/system';
|
||||
|
|
@ -23,6 +24,7 @@ import {isTablet} from '@utils/helpers';
|
|||
import {logError, logInfo} from '@utils/log';
|
||||
import {displayGroupMessageName, displayUsername, getUserIdFromChannelName} from '@utils/user';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
|
@ -237,6 +239,43 @@ export async function resetMessageCount(serverUrl: string, channelId: string) {
|
|||
}
|
||||
}
|
||||
|
||||
const resolveAndFlattenModels = async (
|
||||
operator: ServerDataOperator,
|
||||
modelPromises: Array<Promise<Model[]>>,
|
||||
description: string,
|
||||
prepareRecordsOnly: boolean,
|
||||
) => {
|
||||
try {
|
||||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat();
|
||||
|
||||
if (prepareRecordsOnly || !flattenedModels.length) {
|
||||
return {models: flattenedModels, error: undefined};
|
||||
}
|
||||
|
||||
await operator.batchRecords(flattenedModels, description);
|
||||
return {models: flattenedModels, error: undefined};
|
||||
} catch (error) {
|
||||
logError('Failed resolveAndFlattenModels', {error, description});
|
||||
return {models: [], error};
|
||||
}
|
||||
};
|
||||
|
||||
export async function storeAllMyChannels(serverUrl: string, channels: Channel[], memberships: ChannelMembership[], isCRTEnabled: boolean, prepareRecordsOnly = false) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const cs = new Set(channels.map((c) => c.id));
|
||||
const modelPromises: Array<Promise<Model[]>> = [
|
||||
...await prepareAllMyChannels(operator, channels, memberships.filter((m) => cs.has(m.channel_id)), isCRTEnabled),
|
||||
];
|
||||
|
||||
return resolveAndFlattenModels(operator, modelPromises, 'storeAllMyChannels', prepareRecordsOnly);
|
||||
} catch (error) {
|
||||
logError('Failed storeAllMyChannels', {error, serverUrl, channelsCount: channels.length});
|
||||
return {error, models: undefined};
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeMyChannelsForTeam(serverUrl: string, teamId: string, channels: Channel[], memberships: ChannelMembership[], prepareRecordsOnly = false, isCRTEnabled?: boolean) {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -244,25 +283,10 @@ export async function storeMyChannelsForTeam(serverUrl: string, teamId: string,
|
|||
...await prepareMyChannelsForTeam(operator, teamId, channels, memberships, isCRTEnabled),
|
||||
];
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (!models.length) {
|
||||
return {models: []};
|
||||
}
|
||||
|
||||
const flattenedModels = models.flat();
|
||||
|
||||
if (prepareRecordsOnly) {
|
||||
return {models: flattenedModels};
|
||||
}
|
||||
|
||||
if (flattenedModels.length) {
|
||||
await operator.batchRecords(flattenedModels, 'storeMyChannelsForTeam');
|
||||
}
|
||||
|
||||
return {models: flattenedModels};
|
||||
return resolveAndFlattenModels(operator, modelPromises, 'storeMyChannelsForTeam', prepareRecordsOnly);
|
||||
} catch (error) {
|
||||
logError('Failed storeMyChannelsForTeam', error);
|
||||
return {error};
|
||||
return {error, models: undefined};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {addChannelToDefaultCategory, handleConvertedGMCategories, storeCategories} from '@actions/local/category';
|
||||
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {markChannelAsViewed, removeCurrentUserFromChannel, setChannelDeleteAt, storeAllMyChannels, storeMyChannelsForTeam, switchToChannel} from '@actions/local/channel';
|
||||
import {switchToGlobalThreads} from '@actions/local/thread';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {DeepLink, Events, General, Preferences, Screens} from '@constants';
|
||||
|
|
@ -376,7 +376,7 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string,
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) {
|
||||
export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -404,10 +404,74 @@ export async function fetchChannelStats(serverUrl: string, channelId: string, fe
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchAllMyChannelsForAllTeams(serverUrl: string, since: number, isCRTEnabled: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, true);
|
||||
}
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const promises: [Promise<Channel[]>, Promise<ChannelMembership[]>] = [
|
||||
client.getAllChannelsFromAllTeams(since, since > 0, groupLabel),
|
||||
client.getAllMyChannelMembersFromAllTeams(0, General.CHANNEL_MEMBERS_CHUNK_SIZE, groupLabel),
|
||||
];
|
||||
|
||||
const [channels, memberships] = await Promise.all(promises);
|
||||
const remaining = Math.floor(channels.length / General.CHANNEL_MEMBERS_CHUNK_SIZE);
|
||||
const remainingMembershipsPromises = [];
|
||||
const categoriesPromises = [];
|
||||
|
||||
const teamIdsSet = channels.reduce<Set<string>>((set, c) => {
|
||||
if (c.team_id) {
|
||||
set.add(c.team_id);
|
||||
}
|
||||
return set;
|
||||
}, new Set());
|
||||
for (const teamId of teamIdsSet) {
|
||||
categoriesPromises.push(client.getCategories('me', teamId, groupLabel));
|
||||
}
|
||||
|
||||
if (remaining > 0) {
|
||||
for (let i = 1; i <= remaining; i++) {
|
||||
remainingMembershipsPromises.push(client.getAllMyChannelMembersFromAllTeams(i, General.CHANNEL_MEMBERS_CHUNK_SIZE, groupLabel));
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all([...remainingMembershipsPromises, ...categoriesPromises]);
|
||||
const categories: CategoryWithChannels[] = [];
|
||||
for (const result of results) {
|
||||
if (Array.isArray(result)) {
|
||||
memberships.push(...result);
|
||||
} else if ('categories' in result) {
|
||||
categories.push(...result.categories);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fetchOnly) {
|
||||
const {models: chModels} = await storeAllMyChannels(serverUrl, channels, memberships, isCRTEnabled, true);
|
||||
const {models: catModels} = await storeCategories(serverUrl, categories, true, true); // Re-sync
|
||||
const models = (chModels || []).concat(catModels || []);
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models, 'fetchAllMyChannelsForAllTeams');
|
||||
}
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
|
||||
return {channels, memberships, categories};
|
||||
} catch (error) {
|
||||
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
|
||||
if (!fetchOnly) {
|
||||
setTeamLoading(serverUrl, false);
|
||||
}
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMyChannelsForTeam(
|
||||
serverUrl: string, teamId: string, includeDeleted = true,
|
||||
since = 0, fetchOnly = false, excludeDirect = false,
|
||||
isCRTEnabled?: boolean, groupLabel?: string,
|
||||
isCRTEnabled?: boolean, groupLabel?: RequestGroupLabel,
|
||||
): Promise<MyChannelsRequest> {
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
|
|
@ -457,7 +521,7 @@ export async function fetchMyChannelsForTeam(
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise<MyChannelsRequest> {
|
||||
export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyChannelsRequest> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
||||
|
|
@ -484,7 +548,7 @@ export async function fetchMyChannel(serverUrl: string, teamId: string, channelI
|
|||
export async function fetchMissingDirectChannelsInfo(
|
||||
serverUrl: string, directChannels: Channel[], locale?: string,
|
||||
teammateDisplayNameSetting?: string, currentUserId?: string,
|
||||
fetchOnly = false, groupLabel?: string,
|
||||
fetchOnly = false, groupLabel?: RequestGroupLabel,
|
||||
) {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -662,7 +726,7 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string)
|
|||
}
|
||||
}
|
||||
|
||||
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false, groupLabel?: string) {
|
||||
export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.viewMyChannel(channelId, undefined, groupLabel);
|
||||
|
|
@ -1047,7 +1111,7 @@ export async function getChannelTimezones(serverUrl: string, channelId: string)
|
|||
}
|
||||
}
|
||||
|
||||
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, groupLabel?: string) {
|
||||
export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, groupLabel?: RequestGroupLabel) {
|
||||
if (channelId === Screens.GLOBAL_THREADS) {
|
||||
return switchToGlobalThreads(serverUrl, teamId);
|
||||
}
|
||||
|
|
@ -1059,7 +1123,7 @@ export async function switchToChannelById(serverUrl: string, channelId: string,
|
|||
|
||||
DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, true);
|
||||
|
||||
fetchPostsForChannel(serverUrl, channelId, false, groupLabel);
|
||||
fetchPostsForChannel(serverUrl, channelId, false, false, groupLabel);
|
||||
fetchChannelBookmarks(serverUrl, channelId, false, groupLabel);
|
||||
await switchToChannel(serverUrl, channelId, teamId, skipLastUnread);
|
||||
openChannelIfNeeded(serverUrl, channelId, groupLabel);
|
||||
|
|
@ -1258,8 +1322,10 @@ export const handleKickFromChannel = async (serverUrl: string, channelId: string
|
|||
const currentServer = await getActiveServer();
|
||||
if (currentServer?.url === serverUrl) {
|
||||
const channel = await getChannelById(database, channelId);
|
||||
DeviceEventEmitter.emit(event, channel?.displayName);
|
||||
await dismissAllModalsAndPopToRoot();
|
||||
if (channel) {
|
||||
DeviceEventEmitter.emit(event, channel.displayName);
|
||||
await dismissAllModalsAndPopToRoot();
|
||||
}
|
||||
}
|
||||
|
||||
const tabletDevice = isTablet();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {logError} from '@utils/log';
|
|||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) {
|
||||
export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {setLastServerVersionCheck} from '@actions/local/systems';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
|
|
@ -16,6 +17,8 @@ export async function appEntry(serverUrl: string, since = 0) {
|
|||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
PerformanceMetricsManager.startTimeToInteraction();
|
||||
|
||||
if (!since) {
|
||||
if (Object.keys(DatabaseManager.serverDatabases).length === 1) {
|
||||
await setLastServerVersionCheck(serverUrl, true);
|
||||
|
|
@ -28,7 +31,7 @@ export async function appEntry(serverUrl: string, since = 0) {
|
|||
await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId');
|
||||
}
|
||||
|
||||
WebsocketManager.openAll('entry');
|
||||
WebsocketManager.openAll('Cold Start');
|
||||
|
||||
verifyPushProxy(serverUrl);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@
|
|||
import {nativeApplicationVersion} from 'expo-application';
|
||||
import {RESULTS, checkNotifications} from 'react-native-permissions';
|
||||
|
||||
import {fetchMissingDirectChannelsInfo, fetchMyChannelsForTeam, handleKickFromChannel, type MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchAllMyChannelsForAllTeams, fetchMissingDirectChannelsInfo, handleKickFromChannel, type MyChannelsRequest} from '@actions/remote/channel';
|
||||
import {fetchGroupsForMember} from '@actions/remote/groups';
|
||||
import {fetchPostsForUnreadChannels} from '@actions/remote/post';
|
||||
import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
|
||||
import {fetchRoles} from '@actions/remote/role';
|
||||
import {fetchConfigAndLicense, fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||
import {fetchMyTeams, fetchTeamsChannelsThreadsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {fetchMyTeams, fetchTeamsThreads, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
|
||||
import {syncTeamThreads} from '@actions/remote/thread';
|
||||
import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user';
|
||||
import {General, Preferences, Screens} from '@constants';
|
||||
|
|
@ -22,16 +22,16 @@ import {selectDefaultTeam} from '@helpers/api/team';
|
|||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import {getDeviceToken} from '@queries/app/global';
|
||||
import {getChannelById, queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
|
||||
import {prepareModels, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {prepareEntryModels, prepareEntryModelsForDeletion, truncateCrtRelatedTables} from '@queries/servers/entry';
|
||||
import {getHasCRTChanged} from '@queries/servers/preference';
|
||||
import {getConfig, getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId, getConfigValue} from '@queries/servers/system';
|
||||
import {deleteMyTeams, getAvailableTeamIds, getTeamChannelHistory, queryMyTeams, queryMyTeamsByIds, queryTeamsById} from '@queries/servers/team';
|
||||
import {getCurrentChannelId, getCurrentTeamId, getIsDataRetentionEnabled, getPushVerificationStatus, getLastFullSync, setCurrentTeamAndChannelId, getConfigValue} from '@queries/servers/system';
|
||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {isDMorGM, sortChannelsByDisplayName} from '@utils/channel';
|
||||
import {getFullErrorMessage, isErrorWithStatusCode} from '@utils/errors';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {isMinimumServerVersion, isTablet} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {logDebug, logError} from '@utils/log';
|
||||
import {processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import type {Database, Model} from '@nozbe/watermelondb';
|
||||
|
|
@ -66,14 +66,16 @@ export type EntryResponse = {
|
|||
error: unknown;
|
||||
}
|
||||
|
||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise<EntryResponse> => {
|
||||
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: RequestGroupLabel): Promise<EntryResponse> => {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const result = entryRest(serverUrl, teamId, channelId, since, groupLabel);
|
||||
const result = await entryRest(serverUrl, teamId, channelId, since, groupLabel);
|
||||
|
||||
// Fetch data retention policies
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (isDataRetentionEnabled) {
|
||||
fetchDataRetentionPolicy(serverUrl, false, groupLabel);
|
||||
if (!result.error) {
|
||||
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
|
||||
if (isDataRetentionEnabled) {
|
||||
fetchDataRetentionPolicy(serverUrl, false, groupLabel);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -83,7 +85,7 @@ 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,
|
||||
initialTeamId?: string, initialChannelId?: string,
|
||||
groupLabel?: string,
|
||||
groupLabel?: BaseRequestGroupLabel,
|
||||
) {
|
||||
const result = restDeferredAppEntryActions(
|
||||
serverUrl, since, currentUserId, currentUserLocale,
|
||||
|
|
@ -97,242 +99,109 @@ export async function deferredAppEntryActions(
|
|||
return result;
|
||||
}
|
||||
|
||||
const getRemoveTeamIds = async (database: Database, teamData: MyTeamsRequest) => {
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
const joinedTeams = new Set(teamData.memberships?.filter((m) => m.delete_at === 0).map((m) => m.team_id));
|
||||
return myTeams.filter((m) => !joinedTeams.has(m.id)).map((m) => m.id);
|
||||
};
|
||||
const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
let lastDisconnectedAt = since || await getLastFullSync(database);
|
||||
|
||||
const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return [];
|
||||
}
|
||||
const {database} = operator;
|
||||
const [confResp, prefData] = await Promise.all([
|
||||
fetchConfigAndLicense(serverUrl, true, groupLabel),
|
||||
fetchMyPreferences(serverUrl, true, groupLabel),
|
||||
]);
|
||||
|
||||
if (removeTeamIds?.length) {
|
||||
// Immediately delete myTeams so that the UI renders only teams the user is a member of.
|
||||
const removeMyTeams = await queryMyTeamsByIds(database, removeTeamIds).fetch();
|
||||
if (removeMyTeams?.length) {
|
||||
await deleteMyTeams(operator, removeMyTeams);
|
||||
const ids = removeMyTeams.map((m) => m.id);
|
||||
const removeTeams = await queryTeamsById(database, ids).fetch();
|
||||
return removeTeams;
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confResp.config?.CollapsedThreads, confResp.config?.FeatureFlagCollapsedThreads, confResp.config?.Version));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const isSameServer = currentServerUrl === serverUrl;
|
||||
if (isSameServer) {
|
||||
lastDisconnectedAt = 0;
|
||||
}
|
||||
const {error} = await truncateCrtRelatedTables(serverUrl);
|
||||
if (error) {
|
||||
throw new Error(`Resetting CRT on ${serverUrl} failed`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
// let's start fetching in parallel all we can
|
||||
const promises: [Promise<MyTeamsRequest>, Promise<MyUserRequest>, Promise<MyChannelsRequest>] = [
|
||||
fetchMyTeams(serverUrl, true, groupLabel),
|
||||
fetchMe(serverUrl, true, groupLabel),
|
||||
fetchAllMyChannelsForAllTeams(serverUrl, lastDisconnectedAt, isCRTEnabled, true, groupLabel),
|
||||
];
|
||||
|
||||
const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise<EntryResponse> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
const {database} = operator;
|
||||
const [teamData, meData, chData] = await Promise.all(promises);
|
||||
const error = confResp.error || prefData.error || teamData.error || meData.error || chData.error;
|
||||
if (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const lastDisconnectedAt = since || await getLastFullSync(database);
|
||||
fetchRoles(serverUrl, teamData.memberships, chData.memberships, meData.user, false, false, groupLabel);
|
||||
|
||||
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId, channelId, groupLabel);
|
||||
if ('error' in fetchedData) {
|
||||
return {error: fetchedData.error};
|
||||
}
|
||||
let initialTeamId = teamId || '';
|
||||
let initialChannelId = channelId || '';
|
||||
let gmConverted = false;
|
||||
|
||||
const {initialTeamId, initialChannelId: fetchedChannelId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds, isCRTEnabled, gmConverted} = fetchedData;
|
||||
const chError = chData?.error;
|
||||
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
|
||||
// if the user does not have appropriate permissions, which means the user those not belong to the team,
|
||||
// we set it as there is no errors, so that the teams and others can be properly handled
|
||||
chData!.error = undefined;
|
||||
}
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
if (error) {
|
||||
if (channelId) {
|
||||
const existingChannel = await getChannelById(database, channelId);
|
||||
if (existingChannel && existingChannel.type === General.GM_CHANNEL) {
|
||||
// Okay, so now we know the channel existsin in mobile app's database as a GM.
|
||||
// We now need to also check if channel on server is actually a private channel,
|
||||
// and if so, which team does it belong to now. That team will become the
|
||||
// active team on mobile app after this point.
|
||||
|
||||
const serverChannel = chData.channels?.find((c) => c.id === channelId);
|
||||
|
||||
// Although yon can convert GM only to a pirvate channel, a private channel can furthur be converted to a public channel.
|
||||
// So between the mobile app being on the GM and reconnecting,
|
||||
// it may have become either a public or a private channel. So we need to check for both.
|
||||
if (serverChannel?.type === General.PRIVATE_CHANNEL || serverChannel?.type === General.OPEN_CHANNEL) {
|
||||
initialTeamId = serverChannel.team_id;
|
||||
initialChannelId = channelId;
|
||||
gmConverted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!teamData.error && teamData.teams?.length === 0) {
|
||||
initialTeamId = '';
|
||||
}
|
||||
|
||||
const inTeam = teamData.teams?.find((t) => t.id === initialTeamId);
|
||||
if (initialTeamId && !inTeam && !teamData.error) {
|
||||
initialTeamId = '';
|
||||
}
|
||||
|
||||
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
|
||||
// If no initial team was set in the database but got teams in the response
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
|
||||
const myTeams = teamData.teams.filter((t) => teamMembers.has(t.id));
|
||||
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, confResp.config?.ExperimentalPrimaryTeam);
|
||||
if (defaultTeam?.id) {
|
||||
initialTeamId = defaultTeam.id;
|
||||
}
|
||||
}
|
||||
|
||||
initialChannelId = await entryInitialChannelId(database, initialChannelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
|
||||
|
||||
const dt = Date.now();
|
||||
const modelsToDeletePromises = await prepareEntryModelsForDeletion({operator, teamData, chData});
|
||||
const modelPromises = await prepareEntryModels({operator, teamData, chData, prefData, meData, isCRTEnabled});
|
||||
const modelsToDelete = await Promise.all(modelsToDeletePromises);
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (modelsToDelete.length) {
|
||||
models.push(...modelsToDelete);
|
||||
}
|
||||
logDebug('Process models on entry', groupLabel, models.flat().length, `${Date.now() - dt}ms`);
|
||||
|
||||
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData, gmConverted};
|
||||
} catch (error) {
|
||||
logError('entryRest', groupLabel, error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true, false, groupLabel);
|
||||
|
||||
const initialChannelId = await entryInitialChannelId(database, fetchedChannelId, teamId, initialTeamId, meData?.user?.locale || '', chData?.channels, chData?.memberships);
|
||||
|
||||
const removeTeams = await teamsToRemove(serverUrl, removeTeamIds);
|
||||
|
||||
let removeChannels;
|
||||
if (removeChannelIds?.length) {
|
||||
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
|
||||
}
|
||||
|
||||
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled});
|
||||
if (rolesData.roles?.length) {
|
||||
modelPromises.push(operator.handleRole({roles: rolesData.roles, prepareRecordsOnly: true}));
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
|
||||
return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData, gmConverted};
|
||||
};
|
||||
|
||||
const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeamId = '', channelId?: string, groupLabel?: string): Promise<AppEntryData | AppEntryError> => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
let since = sinceArg;
|
||||
const includeDeletedChannels = true;
|
||||
const fetchOnly = true;
|
||||
|
||||
const confReq = await fetchConfigAndLicense(serverUrl, false, groupLabel);
|
||||
const prefData = await fetchMyPreferences(serverUrl, fetchOnly, groupLabel);
|
||||
const isCRTEnabled = Boolean(prefData.preferences && processIsCRTEnabled(prefData.preferences, confReq.config?.CollapsedThreads, confReq.config?.FeatureFlagCollapsedThreads, confReq.config?.Version));
|
||||
if (prefData.preferences) {
|
||||
const crtToggled = await getHasCRTChanged(database, prefData.preferences);
|
||||
if (crtToggled) {
|
||||
const currentServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
const isSameServer = currentServerUrl === serverUrl;
|
||||
if (isSameServer) {
|
||||
since = 0;
|
||||
}
|
||||
const {error} = await truncateCrtRelatedTables(serverUrl);
|
||||
if (error) {
|
||||
return {error: `Resetting CRT on ${serverUrl} failed`};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch in parallel teams / team membership / user preferences / user
|
||||
const promises: [Promise<MyTeamsRequest>, Promise<MyUserRequest>] = [
|
||||
fetchMyTeams(serverUrl, fetchOnly, groupLabel),
|
||||
fetchMe(serverUrl, fetchOnly, groupLabel),
|
||||
];
|
||||
|
||||
const resolution = await Promise.all(promises);
|
||||
const [teamData, meData] = resolution;
|
||||
let chData;
|
||||
|
||||
let initialTeamId = onLoadTeamId;
|
||||
let initialChannelId = channelId;
|
||||
let gmConverted = false;
|
||||
|
||||
if (channelId) {
|
||||
const existingChannel = await getChannelById(database, channelId);
|
||||
if (existingChannel && existingChannel.type === General.GM_CHANNEL) {
|
||||
// Okay, so now we know the channel existsin in mobile app's database as a GM.
|
||||
// We now need to also check if channel on server is actually a private channel,
|
||||
// and if so, which team does it belong to now. That team will become the
|
||||
// active team on mobile app after this point.
|
||||
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const serverChannel = await client.getChannel(channelId, groupLabel);
|
||||
|
||||
// Although yon can convert GM only to a pirvate channel, a private channel can furthur be converted to a public channel.
|
||||
// So between the mobile app being on the GM and reconnecting,
|
||||
// it may have become either a public or a private channel. So we need to check for both.
|
||||
if (serverChannel.type === General.PRIVATE_CHANNEL || serverChannel.type === General.OPEN_CHANNEL) {
|
||||
initialTeamId = serverChannel.team_id;
|
||||
initialChannelId = channelId;
|
||||
gmConverted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initialTeamId) {
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled, groupLabel);
|
||||
}
|
||||
|
||||
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
|
||||
// If no initial team was set in the database but got teams in the response
|
||||
const config = await getConfig(database);
|
||||
const teamOrderPreference = getPreferenceValue<string>(prefData.preferences || [], Preferences.CATEGORIES.TEAMS_ORDER, '', '');
|
||||
const teamMembers = new Set(teamData.memberships.filter((m) => m.delete_at === 0).map((m) => m.team_id));
|
||||
const myTeams = teamData.teams!.filter((t) => teamMembers.has(t.id));
|
||||
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config?.ExperimentalPrimaryTeam);
|
||||
if (defaultTeam?.id) {
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled, groupLabel);
|
||||
}
|
||||
}
|
||||
|
||||
const removeTeamIds = await getRemoveTeamIds(database, teamData);
|
||||
|
||||
let data: AppEntryData = {
|
||||
initialTeamId,
|
||||
teamData,
|
||||
chData,
|
||||
prefData,
|
||||
meData,
|
||||
removeTeamIds,
|
||||
isCRTEnabled,
|
||||
initialChannelId,
|
||||
gmConverted,
|
||||
};
|
||||
|
||||
if (teamData.teams?.length === 0 && !teamData.error) {
|
||||
return {
|
||||
...data,
|
||||
initialTeamId: '',
|
||||
removeTeamIds,
|
||||
};
|
||||
}
|
||||
|
||||
const inTeam = teamData.teams?.find((t) => t.id === initialTeamId);
|
||||
const chError = chData?.error;
|
||||
if ((!inTeam && !teamData.error) || (isErrorWithStatusCode(chError) && chError.status_code === 403)) {
|
||||
// User is no longer a member of the current team
|
||||
if (!removeTeamIds.includes(initialTeamId)) {
|
||||
removeTeamIds.push(initialTeamId);
|
||||
}
|
||||
|
||||
const availableTeamIds = await getAvailableTeamIds(database, initialTeamId, teamData.teams, prefData.preferences, meData.user?.locale);
|
||||
const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly, isCRTEnabled, groupLabel);
|
||||
|
||||
data = {
|
||||
...data,
|
||||
...alternateTeamData,
|
||||
};
|
||||
}
|
||||
|
||||
if (data.chData?.channels) {
|
||||
const removeChannelIds: string[] = [];
|
||||
const fetchedChannelIds = new Set(data.chData.channels.map((channel) => channel.id));
|
||||
|
||||
const channels = await queryAllChannelsForTeam(database, initialTeamId).fetch();
|
||||
for (const channel of channels) {
|
||||
if (!fetchedChannelIds.has(channel.id)) {
|
||||
removeChannelIds.push(channel.id);
|
||||
}
|
||||
}
|
||||
|
||||
data = {
|
||||
...data,
|
||||
removeChannelIds,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const fetchAlternateTeamData = async (
|
||||
serverUrl: string, availableTeamIds: string[], removeTeamIds: string[],
|
||||
includeDeleted = true, since = 0, fetchOnly = false,
|
||||
isCRTEnabled?: boolean, groupLabel?: string,
|
||||
) => {
|
||||
let initialTeamId = '';
|
||||
let chData;
|
||||
|
||||
for await (const teamId of availableTeamIds) {
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled, groupLabel);
|
||||
const chError = chData.error;
|
||||
if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
|
||||
removeTeamIds.push(teamId);
|
||||
} else {
|
||||
initialTeamId = teamId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (chData) {
|
||||
return {initialTeamId, chData, removeTeamIds};
|
||||
}
|
||||
|
||||
return {initialTeamId, removeTeamIds};
|
||||
};
|
||||
|
||||
async function entryInitialChannelId(database: Database, requestedChannelId = '', requestedTeamId = '', initialTeamId: string, locale: string, channels?: Channel[], memberships?: ChannelMember[]) {
|
||||
|
|
@ -376,67 +245,69 @@ async function entryInitialChannelId(database: Database, requestedChannelId = ''
|
|||
async function restDeferredAppEntryActions(
|
||||
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
|
||||
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
|
||||
initialTeamId?: string, initialChannelId?: string, groupLabel?: string,
|
||||
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;
|
||||
|
||||
// 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, groupLabel);
|
||||
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId, false, requestLabel);
|
||||
}
|
||||
|
||||
updateAllUsersSince(serverUrl, since, false, groupLabel);
|
||||
updateAllUsersSince(serverUrl, since, false, requestLabel);
|
||||
updateCanJoinTeams(serverUrl);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (chData?.channels?.length && chData.memberships?.length && initialTeamId) {
|
||||
if (isCRTEnabled && initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId, {groupLabel});
|
||||
}
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, false, groupLabel);
|
||||
}
|
||||
|
||||
// defer fetch channels and unread posts for other teams
|
||||
let mySortedTeams: Team[] = [];
|
||||
if (teamData.teams?.length && teamData.memberships?.length) {
|
||||
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));
|
||||
const teamMap = new Map(teamData.teams.map((t) => [t.id, t]));
|
||||
if (initialTeamId) {
|
||||
sortedTeamIds.delete(initialTeamId);
|
||||
membershipSet.delete(initialTeamId);
|
||||
teamMap.delete(initialTeamId);
|
||||
}
|
||||
|
||||
let myTeams: Team[];
|
||||
if (sortedTeamIds.size) {
|
||||
const mySortedTeams = [...sortedTeamIds].
|
||||
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()));
|
||||
myTeams = [...mySortedTeams, ...extraTeams];
|
||||
mySortedTeams = [...sortedTeams, ...extraTeams];
|
||||
} else {
|
||||
myTeams = teamData.teams.
|
||||
mySortedTeams = teamData.teams.
|
||||
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
|
||||
}
|
||||
|
||||
if (myTeams.length) {
|
||||
fetchTeamsChannelsThreadsAndUnreadPosts(serverUrl, since, myTeams, isCRTEnabled, false, groupLabel + '-additional');
|
||||
const myOtherSortedTeams = mySortedTeams.filter((t) => t.id !== initialTeamId);
|
||||
if (initialTeamId) {
|
||||
const initialTeam = teamMap.get(initialTeamId);
|
||||
if (initialTeam) {
|
||||
mySortedTeams = [initialTeam, ...myOtherSortedTeams];
|
||||
}
|
||||
}
|
||||
|
||||
if (chData?.channels?.length && chData.memberships?.length && initialTeamId) {
|
||||
if (isCRTEnabled && initialTeamId) {
|
||||
await syncTeamThreads(serverUrl, initialTeamId, {groupLabel: requestLabel});
|
||||
}
|
||||
fetchPostsForUnreadChannels(serverUrl, mySortedTeams, chData.channels, chData.memberships, initialChannelId, isCRTEnabled, false, requestLabel);
|
||||
}
|
||||
|
||||
if (myOtherSortedTeams.length) {
|
||||
fetchTeamsThreads(serverUrl, since, myOtherSortedTeams, isCRTEnabled, false, requestLabel);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch groups for current user
|
||||
fetchGroupsForMember(serverUrl, currentUserId, false, groupLabel);
|
||||
fetchGroupsForMember(serverUrl, currentUserId, false, requestLabel);
|
||||
}
|
||||
|
||||
export const setExtraSessionProps = async (serverUrl: string, groupLabel?: string) => {
|
||||
export const setExtraSessionProps = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const serverVersion = await getConfigValue(database, 'Version');
|
||||
|
|
@ -459,7 +330,7 @@ export const setExtraSessionProps = async (serverUrl: string, groupLabel?: strin
|
|||
}
|
||||
};
|
||||
|
||||
export async function verifyPushProxy(serverUrl: string, groupLabel?: string) {
|
||||
export async function verifyPushProxy(serverUrl: string, groupLabel?: RequestGroupLabel) {
|
||||
const deviceId = await getDeviceToken();
|
||||
if (!deviceId) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
|
|||
// sure we don't do this by skipping the load metric here.
|
||||
PerformanceMetricsManager.skipLoadMetric();
|
||||
|
||||
// But still we want to log the TTI
|
||||
PerformanceMetricsManager.startTimeToInteraction();
|
||||
|
||||
try {
|
||||
const clData = await fetchConfigAndLicense(serverUrl, false);
|
||||
if (clData.error) {
|
||||
|
|
@ -31,7 +34,7 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
|
|||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
await WebsocketManager.initializeClient(serverUrl, 'login');
|
||||
await WebsocketManager.initializeClient(serverUrl, 'Login');
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
|||
import type MyTeamModel from '@typings/database/models/servers/my_team';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData, groupLabel?: string) {
|
||||
export async function pushNotificationEntry(serverUrl: string, notification: NotificationData, groupLabel?: BaseRequestGroupLabel) {
|
||||
// We only reach this point if we have a channel Id in the notification payload
|
||||
const channelId = notification.channel_id!;
|
||||
const rootId = notification.root_id!;
|
||||
|
|
@ -33,6 +33,9 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not
|
|||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
PerformanceMetricsManager.startTimeToInteraction();
|
||||
|
||||
const {database} = operator;
|
||||
|
||||
const currentTeamId = await getCurrentTeamId(database);
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) => {
|
||||
export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
|
|
@ -129,7 +129,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false, groupLabel?: string) => {
|
||||
export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const license = await getLicense(database);
|
||||
|
|
@ -191,7 +191,7 @@ export const fetchGroupsForTeamIfConstrained = async (serverUrl: string, teamId:
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) => {
|
||||
export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const channel = await getChannelById(database, channelId);
|
||||
|
|
|
|||
|
|
@ -581,7 +581,7 @@ describe('get posts', () => {
|
|||
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
|
||||
await operator.handleMyChannel({channels: [channel1], myChannels: [channelMember1], prepareRecordsOnly: false});
|
||||
|
||||
const result = await fetchPostsForUnreadChannels(serverUrl, [channel1, {...channel1, id: 'channelid2', total_msg_count: 10}], [{...channelMember1, msg_count: 5}, {...channelMember1, channel_id: 'channelid2', msg_count: 10}], 'testid');
|
||||
const result = await fetchPostsForUnreadChannels(serverUrl, [{id: teamId}] as Team[], [channel1, {...channel1, id: 'channelid2', total_msg_count: 10}], [{...channelMember1, msg_count: 5}, {...channelMember1, channel_id: 'channelid2', msg_count: 10}], 'testid');
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(1); // Only returns the response for the channel with unread messages
|
||||
expect(result?.[0].posts?.[0].channel_id).toBe(channel1.id);
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
|
|||
return {};
|
||||
};
|
||||
|
||||
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise<PostsForChannel> {
|
||||
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false, skipAuthors = false, groupLabel?: RequestGroupLabel): Promise<PostsForChannel> {
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
|
||||
|
|
@ -309,8 +309,10 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
|
|||
}
|
||||
let authors: UserProfile[] = [];
|
||||
if (data.posts?.length && data.order?.length) {
|
||||
const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true, groupLabel);
|
||||
authors = fetchedAuthors || [];
|
||||
if (!skipAuthors) {
|
||||
const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true, groupLabel);
|
||||
authors = fetchedAuthors || [];
|
||||
}
|
||||
|
||||
if (!fetchOnly) {
|
||||
await storePostsForChannel(
|
||||
|
|
@ -333,39 +335,84 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
|
|||
}
|
||||
|
||||
export const fetchPostsForUnreadChannels = async (
|
||||
serverUrl: string, channels: Channel[], memberships: ChannelMembership[],
|
||||
excludeChannelId?: string, fetchOnly = false, groupLabel?: string,
|
||||
serverUrl: string, teams: Team[], channels: Channel[], memberships: ChannelMembership[],
|
||||
excludeChannelId?: string, isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel,
|
||||
): Promise<PostsForChannel[]> => {
|
||||
const membersMap = new Map<string, ChannelMembership>();
|
||||
for (const member of memberships) {
|
||||
membersMap.set(member.channel_id, member);
|
||||
const teamIndexMap = new Map<string, number>();
|
||||
teams.forEach((team, index) => teamIndexMap.set(team.id, index));
|
||||
|
||||
const channelsMap = new Map<string, Channel>();
|
||||
for (const channel of channels) {
|
||||
channelsMap.set(channel.id, channel);
|
||||
}
|
||||
|
||||
const unreadChannelIds = channels.reduce<string[]>((result, channel) => {
|
||||
const member = membersMap.get(channel.id);
|
||||
if (member && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
|
||||
result.push(channel.id);
|
||||
const sortedUnreadchannelIds = memberships.filter((member) => {
|
||||
const channel = channelsMap.get(member.channel_id);
|
||||
if (channel && !channel.delete_at && channel.id !== excludeChannelId) {
|
||||
const unreads = isCRTEnabled ? (channel.total_msg_count_root ?? 0) - (member.msg_count_root ?? 0) : channel.total_msg_count - member.msg_count;
|
||||
return unreads > 0; // Keep only channels with unreads
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return false;
|
||||
}).
|
||||
sort((a, b) => {
|
||||
const channelA = channelsMap.get(a.channel_id);
|
||||
const channelB = channelsMap.get(b.channel_id);
|
||||
|
||||
if (!channelA || !channelB) {
|
||||
return 0; // If either channel is undefined, treat as equal
|
||||
}
|
||||
|
||||
// Priority for channels with an empty team_id
|
||||
if (channelA.team_id === '' && channelB.team_id !== '') {
|
||||
return -1;
|
||||
}
|
||||
if (channelB.team_id === '' && channelA.team_id !== '') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get team indices for sorting
|
||||
const teamIndexA = teamIndexMap.get(channelA.team_id) ?? Number.MAX_SAFE_INTEGER;
|
||||
const teamIndexB = teamIndexMap.get(channelB.team_id) ?? Number.MAX_SAFE_INTEGER;
|
||||
|
||||
// Sort by team index first
|
||||
if (teamIndexA !== teamIndexB) {
|
||||
return teamIndexA - teamIndexB;
|
||||
}
|
||||
|
||||
// If team index is the same, sort by last_viewed_at in descending order
|
||||
return b.last_viewed_at - a.last_viewed_at;
|
||||
}).
|
||||
map((member) => member.channel_id);
|
||||
|
||||
const postsForChannel: PostsForChannel[] = [];
|
||||
const posts: Post[] = [];
|
||||
|
||||
// process 10 unread channels at a time
|
||||
const chunks = chunk(unreadChannelIds, 10);
|
||||
const chunks = chunk(sortedUnreadchannelIds, 10);
|
||||
for await (const channelIds of chunks) {
|
||||
const promises = [];
|
||||
for (const channelId of channelIds) {
|
||||
promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly, groupLabel));
|
||||
promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly, true, groupLabel));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
postsForChannel.push(...results);
|
||||
for (const r of results) {
|
||||
if (r.posts?.length) {
|
||||
posts.push(...r.posts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (posts.length) {
|
||||
fetchPostAuthors(serverUrl, posts, false, groupLabel);
|
||||
}
|
||||
|
||||
return postsForChannel;
|
||||
};
|
||||
|
||||
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false, groupLabel?: string): Promise<PostsRequest> {
|
||||
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<PostsRequest> {
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
|
||||
|
|
@ -465,7 +512,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false, groupLabel?: string): Promise<PostsRequest> {
|
||||
export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<PostsRequest> {
|
||||
try {
|
||||
if (!fetchOnly) {
|
||||
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
|
||||
|
|
@ -512,7 +559,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc
|
|||
}
|
||||
}
|
||||
|
||||
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false, groupLabel?: string): Promise<AuthorsRequest> => {
|
||||
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<AuthorsRequest> => {
|
||||
try {
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -575,7 +622,7 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn
|
|||
}
|
||||
};
|
||||
|
||||
export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false, groupLabel?: string) {
|
||||
export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
setFetchingThreadState(postId, true);
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -746,7 +793,7 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false, groupLabel?: string) {
|
||||
export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export type MyPreferencesRequest = {
|
|||
error?: unknown;
|
||||
};
|
||||
|
||||
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyPreferencesRequest> => {
|
||||
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyPreferencesRequest> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -84,7 +84,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false, groupLabel?: string) => {
|
||||
export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
if (!preferences.length) {
|
||||
return {preferences: []};
|
||||
|
|
@ -141,7 +141,7 @@ export const deleteSavedPost = async (serverUrl: string, postId: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const openChannelIfNeeded = async (serverUrl: string, channelId: string, groupLabel?: string) => {
|
||||
export const openChannelIfNeeded = async (serverUrl: string, channelId: string, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const channel = await getChannelById(database, channelId);
|
||||
|
|
@ -156,7 +156,7 @@ export const openChannelIfNeeded = async (serverUrl: string, channelId: string,
|
|||
}
|
||||
};
|
||||
|
||||
export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: string) => {
|
||||
export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const channels = await queryAllUnreadDMsAndGMsIds(database).fetch();
|
||||
|
|
@ -168,7 +168,7 @@ export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: stri
|
|||
}
|
||||
};
|
||||
|
||||
const openChannels = async (serverUrl: string, channels: ChannelModel[], groupLabel?: string) => {
|
||||
const openChannels = async (serverUrl: string, channels: ChannelModel[], groupLabel?: RequestGroupLabel) => {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export type RolesRequest = {
|
|||
|
||||
export const fetchRolesIfNeeded = async (
|
||||
serverUrl: string, updatedRoles: string[],
|
||||
fetchOnly = false, force = false, groupLabel?: string,
|
||||
fetchOnly = false, force = false, groupLabel?: RequestGroupLabel,
|
||||
): Promise<RolesRequest> => {
|
||||
if (!updatedRoles.length) {
|
||||
return {roles: []};
|
||||
|
|
@ -70,7 +70,7 @@ export const fetchRolesIfNeeded = async (
|
|||
|
||||
export const fetchRoles = async (
|
||||
serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[],
|
||||
user?: UserProfile, fetchOnly = false, force = false, groupLabel?: string,
|
||||
user?: UserProfile, fetchOnly = false, force = false, groupLabel?: RequestGroupLabel,
|
||||
) => {
|
||||
const rolesToFetch = new Set<string>(user?.roles.split(' ') || []);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export type DataRetentionPoliciesRequest = {
|
|||
error?: unknown;
|
||||
}
|
||||
|
||||
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<DataRetentionPoliciesRequest> => {
|
||||
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<DataRetentionPoliciesRequest> => {
|
||||
const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl, groupLabel);
|
||||
const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, undefined, undefined, undefined, groupLabel);
|
||||
const {data: channelPolicies, error: channelPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, true);
|
||||
|
|
@ -45,7 +45,7 @@ export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = fa
|
|||
return data;
|
||||
};
|
||||
|
||||
export const fetchGlobalDataRetentionPolicy = async (serverUrl: string, groupLabel?: string): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => {
|
||||
export const fetchGlobalDataRetentionPolicy = async (serverUrl: string, groupLabel?: RequestGroupLabel): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const data = await client.getGlobalDataRetentionPolicy(groupLabel);
|
||||
|
|
@ -62,7 +62,7 @@ export const fetchAllGranularDataRetentionPolicies = async (
|
|||
isChannel = false,
|
||||
page = 0,
|
||||
policies: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy> = [],
|
||||
groupLabel?: string,
|
||||
groupLabel?: RequestGroupLabel,
|
||||
): Promise<{data?: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy>; error?: unknown}> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -86,7 +86,7 @@ export const fetchAllGranularDataRetentionPolicies = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<ConfigAndLicenseRequest> => {
|
||||
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<ConfigAndLicenseRequest> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const [config, license]: [ClientConfig, ClientLicense] = await Promise.all([
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import {
|
|||
handleKickFromTeam,
|
||||
getTeamMembersByIds,
|
||||
buildTeamIconUrl,
|
||||
fetchTeamsChannelsThreadsAndUnreadPosts,
|
||||
fetchTeamsThreads,
|
||||
} from './team';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
|
|
@ -271,20 +271,20 @@ describe('teams', () => {
|
|||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('fetchTeamsChannelsThreadsAndUnreadPosts - handle not found database', async () => {
|
||||
const result = await fetchTeamsChannelsThreadsAndUnreadPosts('foo', 0, []) as {error: unknown};
|
||||
it('fetchTeamsThreads - handle not found database', async () => {
|
||||
const result = await fetchTeamsThreads('foo', 0, []) as {error: unknown};
|
||||
expect(result?.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('fetchTeamsChannelsThreadsAndUnreadPosts - base case', async () => {
|
||||
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
|
||||
it('fetchTeamsThreads - base case', async () => {
|
||||
const result = await fetchTeamsThreads(
|
||||
serverUrl, 0,
|
||||
[team], true, false);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('fetchTeamsChannelsThreadsAndUnreadPosts - fetch only case', async () => {
|
||||
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
|
||||
it('fetchTeamsThreads - fetch only case', async () => {
|
||||
const result = await fetchTeamsThreads(
|
||||
serverUrl, 0,
|
||||
[team], true, true);
|
||||
expect(result.models).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@
|
|||
import {chunk} from 'lodash';
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {storeCategories} from '@actions/local/category';
|
||||
import {storeMyChannelsForTeam} from '@actions/local/channel';
|
||||
import {storePostsForChannel} from '@actions/local/post';
|
||||
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import {Events} from '@constants';
|
||||
|
|
@ -27,7 +24,7 @@ import {logDebug} from '@utils/log';
|
|||
|
||||
import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
|
||||
import {fetchGroupsForTeamIfConstrained} from './groups';
|
||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels, type PostsForChannel} from './post';
|
||||
import {fetchPostsForChannel} from './post';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {syncThreadsIfNeeded} from './thread';
|
||||
|
|
@ -161,7 +158,7 @@ export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string,
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyTeamsRequest> {
|
||||
export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyTeamsRequest> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -207,7 +204,7 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLa
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: string): Promise<MyTeamsRequest> {
|
||||
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyTeamsRequest> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -247,7 +244,7 @@ export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_P
|
|||
}
|
||||
};
|
||||
|
||||
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number, groupLabel?: string): Promise<boolean> => {
|
||||
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number, groupLabel?: RequestGroupLabel): Promise<boolean> => {
|
||||
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT, false, groupLabel);
|
||||
if (fetchedTeams.find((t) => !myTeamsIds.has(t.id) && t.delete_at === 0)) {
|
||||
return true;
|
||||
|
|
@ -298,7 +295,7 @@ export async function fetchTeamsForComponent(
|
|||
return {teams: alreadyLoaded, hasMore: false, page};
|
||||
}
|
||||
|
||||
export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: string) => {
|
||||
export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -318,9 +315,8 @@ export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: string)
|
|||
}
|
||||
};
|
||||
|
||||
export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
|
||||
serverUrl: string, since: number, teams: Team[],
|
||||
isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: string,
|
||||
export const fetchTeamsThreads = async (
|
||||
serverUrl: string, since: number, teams: Team[], isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: RequestGroupLabel,
|
||||
) => {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -329,62 +325,14 @@ export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
|
|||
// process up to 15 teams at a time
|
||||
const chunks = chunk(teams, 15);
|
||||
for await (const myTeams of chunks) {
|
||||
const promises = [];
|
||||
for (const team of myTeams) {
|
||||
promises.push(fetchMyChannelsForTeam(serverUrl, team.id, true, since, true, true, isCRTEnabled, groupLabel));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
const models: Model[] = [];
|
||||
const channels: Channel[] = [];
|
||||
const members: ChannelMembership[] = [];
|
||||
|
||||
for await (const r of results) {
|
||||
if (!r.error && r.teamId && r.categories && r.channels && r.memberships) {
|
||||
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, r.teamId, r.channels, r.memberships, true, isCRTEnabled);
|
||||
const {models: catModels} = await storeCategories(serverUrl, r.categories, true, true); // Re-sync
|
||||
if (chModels?.length) {
|
||||
models.push(...chModels);
|
||||
}
|
||||
|
||||
if (catModels?.length) {
|
||||
models.push(...catModels);
|
||||
}
|
||||
|
||||
channels.push(...r.channels);
|
||||
members.push(...r.memberships);
|
||||
}
|
||||
}
|
||||
|
||||
const unreadPromise = fetchPostsForUnreadChannels(serverUrl, channels, members, undefined, true, groupLabel);
|
||||
const threadsPromise = syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true, groupLabel);
|
||||
const postPromises: [Promise<PostsForChannel[]>, Promise<{models?: Model[]; error?: unknown}>] = [
|
||||
unreadPromise,
|
||||
threadsPromise,
|
||||
];
|
||||
|
||||
const [unreadPosts, threads] = await Promise.all(postPromises);
|
||||
|
||||
for await (const data of unreadPosts) {
|
||||
if (data.channelId && data.posts?.length && data.order?.length && data.actionType) {
|
||||
const {models: prepared} = await storePostsForChannel(
|
||||
serverUrl, data.channelId,
|
||||
data.posts, data.order, data.previousPostId ?? '',
|
||||
data.actionType, data.authors || [], true,
|
||||
);
|
||||
|
||||
if (prepared?.length) {
|
||||
models.push(...prepared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const threads = await syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true, groupLabel);
|
||||
if (threads.models) {
|
||||
models.push(...threads.models);
|
||||
}
|
||||
|
||||
if (!fetchOnly && models.length) {
|
||||
await operator.batchRecords(removeDuplicatesModels(models), 'fetchTeamsChannelsThreadsAndUnreadPosts');
|
||||
await operator.batchRecords(removeDuplicatesModels(models), 'fetchTeamsThreads');
|
||||
}
|
||||
|
||||
if (models.length) {
|
||||
|
|
@ -394,7 +342,7 @@ export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
|
|||
|
||||
return {error: undefined, models: result};
|
||||
} catch (error) {
|
||||
logDebug('error on fetchTeamsChannelsThreadsAndUnreadPosts', getFullErrorMessage(error));
|
||||
logDebug('error on fetchTeamsThreads', getFullErrorMessage(error));
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ enum Direction {
|
|||
Down,
|
||||
}
|
||||
|
||||
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false, groupLabel?: string) => {
|
||||
export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false, groupLabel?: RequestGroupLabel) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
|
|
@ -214,7 +214,7 @@ export const fetchThreads = async (
|
|||
options: FetchThreadsOptions,
|
||||
direction?: Direction,
|
||||
pages?: number,
|
||||
groupLabel?: string,
|
||||
groupLabel?: RequestGroupLabel,
|
||||
) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
|
|
@ -287,7 +287,7 @@ export const fetchThreads = async (
|
|||
|
||||
export const syncThreadsIfNeeded = async (
|
||||
serverUrl: string, isCRTEnabled: boolean, teams?: Team[],
|
||||
fetchOnly = false, groupLabel?: string,
|
||||
fetchOnly = false, groupLabel?: RequestGroupLabel,
|
||||
) => {
|
||||
try {
|
||||
if (!isCRTEnabled) {
|
||||
|
|
@ -329,7 +329,7 @@ type SyncThreadOptions = {
|
|||
excludeDirect?: boolean;
|
||||
fetchOnly?: boolean;
|
||||
refresh?: boolean;
|
||||
groupLabel?: string;
|
||||
groupLabel?: RequestGroupLabel;
|
||||
}
|
||||
|
||||
export const syncTeamThreads = async (
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export type ProfilesInChannelRequest = {
|
|||
error?: unknown;
|
||||
}
|
||||
|
||||
export const fetchMe = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyUserRequest> => {
|
||||
export const fetchMe = async (serverUrl: string, fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<MyUserRequest> => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -98,7 +98,7 @@ export const refetchCurrentUser = async (serverUrl: string, currentUserId: strin
|
|||
|
||||
export async function fetchProfilesInChannel(
|
||||
serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions,
|
||||
fetchOnly = false, groupLabel?: string,
|
||||
fetchOnly = false, groupLabel?: RequestGroupLabel,
|
||||
): Promise<ProfilesInChannelRequest> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -134,7 +134,7 @@ export async function fetchProfilesInChannel(
|
|||
}
|
||||
}
|
||||
|
||||
export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false, groupLabel?: string): Promise<ProfilesPerChannelRequest> {
|
||||
export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false, groupLabel?: RequestGroupLabel): Promise<ProfilesPerChannelRequest> {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -199,7 +199,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann
|
|||
|
||||
export async function fetchProfilesPerChannels(
|
||||
serverUrl: string, channelIds: string[], excludeUserId?: string,
|
||||
fetchOnly = false, groupLabel?: string,
|
||||
fetchOnly = false, groupLabel?: RequestGroupLabel,
|
||||
): Promise<ProfilesPerChannelRequest> {
|
||||
try {
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -250,7 +250,7 @@ export async function fetchProfilesPerChannels(
|
|||
}
|
||||
}
|
||||
|
||||
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>, groupLabel?: string) => {
|
||||
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
@ -428,7 +428,7 @@ export const fetchUserByIdBatched = async (serverUrl: string, userId: string) =>
|
|||
usersByIdBatch.timeout = setTimeout(processBatch, TIME_TO_BATCH);
|
||||
};
|
||||
|
||||
export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false, groupLabel?: string) => {
|
||||
export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false, groupLabel?: RequestGroupLabel) => {
|
||||
if (!userIds.length) {
|
||||
return {users: [], existingUsers: []};
|
||||
}
|
||||
|
|
@ -632,7 +632,7 @@ export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernam
|
|||
return {users};
|
||||
};
|
||||
|
||||
export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false, groupLabel?: string) {
|
||||
export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false, groupLabel?: RequestGroupLabel) {
|
||||
if (!since) {
|
||||
return {users: []};
|
||||
}
|
||||
|
|
@ -807,7 +807,7 @@ export const buildProfileImageUrlFromUser = (serverUrl: string, user: UserModel
|
|||
return buildProfileImageUrl(serverUrl, user.id, lastPictureUpdate);
|
||||
};
|
||||
|
||||
export const autoUpdateTimezone = async (serverUrl: string, groupLabel?: string) => {
|
||||
export const autoUpdateTimezone = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
|
||||
let database;
|
||||
try {
|
||||
const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
|
|
|
|||
|
|
@ -36,17 +36,17 @@ import {setTeamLoading} from '@store/team_load_store';
|
|||
import {isTablet} from '@utils/helpers';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
|
||||
export async function handleFirstConnect(serverUrl: string, groupLabel?: string) {
|
||||
export async function handleFirstConnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) {
|
||||
setExtraSessionProps(serverUrl, groupLabel);
|
||||
autoUpdateTimezone(serverUrl, groupLabel);
|
||||
return doReconnect(serverUrl, groupLabel);
|
||||
}
|
||||
|
||||
export async function handleReconnect(serverUrl: string) {
|
||||
return doReconnect(serverUrl, 'reconnection');
|
||||
export async function handleReconnect(serverUrl: string, groupLabel: BaseRequestGroupLabel = 'WebSocket Reconnect') {
|
||||
return doReconnect(serverUrl, groupLabel);
|
||||
}
|
||||
|
||||
async function doReconnect(serverUrl: string, groupLabel?: string) {
|
||||
async function doReconnect(serverUrl: string, groupLabel?: BaseRequestGroupLabel) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return new Error('cannot find server database');
|
||||
|
|
@ -82,13 +82,6 @@ async function doReconnect(serverUrl: string, groupLabel?: string) {
|
|||
|
||||
await setLastFullSync(operator, now);
|
||||
|
||||
const tabletDevice = isTablet();
|
||||
const isActiveServer = (await getActiveServerUrl()) === serverUrl;
|
||||
if (isActiveServer && tabletDevice && initialChannelId === currentChannelId) {
|
||||
await markChannelAsRead(serverUrl, initialChannelId, false, groupLabel);
|
||||
markChannelAsViewed(serverUrl, initialChannelId);
|
||||
}
|
||||
|
||||
logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`);
|
||||
setTeamLoading(serverUrl, false);
|
||||
|
||||
|
|
@ -112,7 +105,7 @@ async function doReconnect(serverUrl: string, groupLabel?: string) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: string) {
|
||||
async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: RequestGroupLabel) {
|
||||
try {
|
||||
const isActiveServer = (await getActiveServerUrl()) === serverUrl;
|
||||
if (!isActiveServer) {
|
||||
|
|
@ -146,7 +139,7 @@ async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: string) {
|
|||
}
|
||||
|
||||
if (currentChannelId && (isChannelScreenMounted || tabletDevice)) {
|
||||
await fetchPostsForChannel(serverUrl, currentChannelId, false, groupLabel);
|
||||
await fetchPostsForChannel(serverUrl, currentChannelId, false, false, groupLabel);
|
||||
markChannelAsRead(serverUrl, currentChannelId, false, groupLabel);
|
||||
if (!EphemeralStore.wasNotificationTapped()) {
|
||||
markChannelAsViewed(serverUrl, currentChannelId, true);
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ describe('WebSocket Post Actions', () => {
|
|||
|
||||
mockedGetMyChannel.mockResolvedValueOnce(undefined);
|
||||
mockedFetchMyChannel.mockResolvedValue({teamId: 'team1', memberships: [{user_id: 'user1', channel_id: 'channel1'}]} as MyChannelsRequest);
|
||||
mockedStoreMyChannelsForTeam.mockResolvedValue({models: [myChannelModel]});
|
||||
mockedStoreMyChannelsForTeam.mockResolvedValue({models: [myChannelModel], error: undefined});
|
||||
mockedGetMyChannel.mockResolvedValueOnce(myChannelModel);
|
||||
mockedGetIsCRTEnabled.mockResolvedValue(false);
|
||||
mockedShouldIgnorePost.mockReturnValue(true);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type ClientBase from './base';
|
|||
|
||||
export interface ClientAppsMix {
|
||||
executeAppCall: <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse<Res>>;
|
||||
getAppsBindings: (userID: string, channelID: string, teamID: string, groupLabel?: string) => Promise<AppBinding[]>;
|
||||
getAppsBindings: (userID: string, channelID: string, teamID: string, groupLabel?: RequestGroupLabel) => Promise<AppBinding[]>;
|
||||
}
|
||||
|
||||
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -27,7 +27,7 @@ const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) =>
|
|||
);
|
||||
};
|
||||
|
||||
getAppsBindings = async (userID: string, channelID: string, teamID: string, groupLabel?: string) => {
|
||||
getAppsBindings = async (userID: string, channelID: string, teamID: string, groupLabel?: RequestGroupLabel) => {
|
||||
const params = {
|
||||
user_id: userID,
|
||||
channel_id: channelID,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@
|
|||
import type ClientBase from './base';
|
||||
|
||||
export interface ClientCategoriesMix {
|
||||
getCategories: (userId: string, teamId: string, groupLabel?: string) => Promise<CategoriesWithOrder>;
|
||||
getCategories: (userId: string, teamId: string, groupLabel?: RequestGroupLabel) => Promise<CategoriesWithOrder>;
|
||||
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
|
||||
getCategory: (userId: string, teamId: string, categoryId: string) => Promise<Category>;
|
||||
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
|
||||
}
|
||||
|
||||
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
getCategories = async (userId: string, teamId: string, groupLabel?: string) => {
|
||||
getCategories = async (userId: string, teamId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getCategoriesRoute(userId, teamId)}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ describe('ClientChannelBookmarks', () => {
|
|||
test('getChannelBookmarksForChannel', async () => {
|
||||
const channelId = 'channel_id';
|
||||
const since = 123456;
|
||||
const groupLabel = 'group_label';
|
||||
const groupLabel = 'Login';
|
||||
const expectedUrl = `${client.getChannelBookmarksRoute(channelId)}${buildQueryString({bookmarks_since: since})}`;
|
||||
const expectedOptions = {method: 'get', groupLabel};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface ClientChannelBookmarksMix {
|
|||
updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise<UpdateChannelBookmarkResponse>;
|
||||
updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string): Promise<ChannelBookmarkWithFileInfo[]>;
|
||||
deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string): Promise<ChannelBookmarkWithFileInfo>;
|
||||
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string): Promise<ChannelBookmarkWithFileInfo[]>;
|
||||
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: RequestGroupLabel): Promise<ChannelBookmarkWithFileInfo[]>;
|
||||
}
|
||||
|
||||
const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -57,7 +57,7 @@ const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(superclas
|
|||
);
|
||||
}
|
||||
|
||||
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string) {
|
||||
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: RequestGroupLabel) {
|
||||
return this.doFetch(
|
||||
`${this.getChannelBookmarksRoute(channelId)}${buildQueryString({bookmarks_since: since})}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -19,24 +19,24 @@ export interface ClientChannelsMix {
|
|||
updateChannelPrivacy: (channelId: string, privacy: any) => Promise<Channel>;
|
||||
patchChannel: (channelId: string, channelPatch: Partial<Channel>) => Promise<Channel>;
|
||||
updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => Promise<any>;
|
||||
getChannel: (channelId: string, groupLabel?: string) => Promise<Channel>;
|
||||
getChannel: (channelId: string, groupLabel?: RequestGroupLabel) => Promise<Channel>;
|
||||
getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
|
||||
getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
|
||||
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getSharedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
|
||||
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number, groupLabel?: string) => Promise<Channel[]>;
|
||||
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number, groupLabel?: RequestGroupLabel) => Promise<Channel[]>;
|
||||
getMyChannelMember: (channelId: string) => Promise<ChannelMembership>;
|
||||
getMyChannelMembers: (teamId: string, groupLabel?: string) => Promise<ChannelMembership[]>;
|
||||
getMyChannelMembers: (teamId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelMembership[]>;
|
||||
getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ChannelMembership[]>;
|
||||
getChannelTimezones: (channelId: string) => Promise<string[]>;
|
||||
getChannelMember: (channelId: string, userId: string, groupLabel?: string) => Promise<ChannelMembership>;
|
||||
getChannelMember: (channelId: string, userId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelMembership>;
|
||||
getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise<ChannelMembership[]>;
|
||||
addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise<ChannelMembership>;
|
||||
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
|
||||
getChannelStats: (channelId: string, groupLabel?: string) => Promise<ChannelStats>;
|
||||
getChannelStats: (channelId: string, groupLabel?: RequestGroupLabel) => Promise<ChannelStats>;
|
||||
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
|
||||
viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: string) => Promise<any>;
|
||||
viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: RequestGroupLabel) => Promise<any>;
|
||||
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
|
||||
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
|
||||
|
|
@ -46,6 +46,8 @@ export interface ClientChannelsMix {
|
|||
getMemberInChannel: (channelId: string, userId: string) => Promise<ChannelMembership>;
|
||||
getGroupMessageMembersCommonTeams: (channelId: string) => Promise<Team[]>;
|
||||
convertGroupMessageToPrivateChannel: (channelId: string, teamId: string, displayName: string, name: string) => Promise<Channel>;
|
||||
getAllChannelsFromAllTeams: (lastDeleteAt: number, includeDeleted: boolean, groupLabel?: RequestGroupLabel) => Promise<Channel[]>;
|
||||
getAllMyChannelMembersFromAllTeams: (page: number, perPage: number, groupLabel?: RequestGroupLabel) => Promise<ChannelMembership[]>;
|
||||
}
|
||||
|
||||
const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
|
|
@ -63,6 +65,30 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getAllChannelsFromAllTeams = async (lastDeleteAt: number, includeDeleted: boolean, groupLabel?: RequestGroupLabel) => {
|
||||
const queryData = {
|
||||
last_delete_at: lastDeleteAt,
|
||||
include_deleted: includeDeleted,
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/channels${buildQueryString(queryData)}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getAllMyChannelMembersFromAllTeams = async (page: number, perPage: number, groupLabel?: RequestGroupLabel) => {
|
||||
const queryData = {
|
||||
page,
|
||||
per_page: perPage,
|
||||
};
|
||||
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/channel_members${buildQueryString(queryData)}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
createChannel = async (channel: Channel) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelsRoute()}`,
|
||||
|
|
@ -130,7 +156,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getChannel = async (channelId: string, groupLabel?: string) => {
|
||||
getChannel = async (channelId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
this.getChannelRoute(channelId),
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -180,7 +206,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getMyChannels = async (teamId: string, includeDeleted = false, since = 0, groupLabel?: string) => {
|
||||
getMyChannels = async (teamId: string, includeDeleted = false, since = 0, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({
|
||||
include_deleted: includeDeleted,
|
||||
|
|
@ -197,7 +223,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getMyChannelMembers = async (teamId: string, groupLabel?: string) => {
|
||||
getMyChannelMembers = async (teamId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/${teamId}/channels/members`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -218,7 +244,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getChannelMember = async (channelId: string, userId: string, groupLabel?: string) => {
|
||||
getChannelMember = async (channelId: string, userId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelMemberRoute(channelId, userId)}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -247,7 +273,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
getChannelStats = async (channelId: string, groupLabel?: string) => {
|
||||
getChannelStats = async (channelId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/stats`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -261,7 +287,7 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(superclass: TBase
|
|||
);
|
||||
};
|
||||
|
||||
viewMyChannel = async (channelId: string, prevChannelId?: string, groupLabel?: string) => {
|
||||
viewMyChannel = async (channelId: string, prevChannelId?: string, groupLabel?: RequestGroupLabel) => {
|
||||
// collapsed_threads_supported is not based on user preferences but to know if "CLIENT" supports CRT
|
||||
const data = {channel_id: channelId, prev_channel_id: prevChannelId, collapsed_threads_supported: true};
|
||||
return this.doFetch(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ describe('ClientGeneral', () => {
|
|||
test('ping', async () => {
|
||||
const deviceId = 'device1';
|
||||
const timeoutInterval = 1000;
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'DeepLink';
|
||||
const expectedUrl = `${client.urlVersion}/system/ping?time=${Date.now()}&device_id=${deviceId}`;
|
||||
const expectedOptions = {method: 'get', timeoutInterval, groupLabel};
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ describe('ClientGeneral', () => {
|
|||
});
|
||||
|
||||
test('getClientConfigOld', async () => {
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Notification';
|
||||
const expectedUrl = `${client.urlVersion}/config/client?format=old`;
|
||||
const expectedOptions = {method: 'get', groupLabel};
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ describe('ClientGeneral', () => {
|
|||
});
|
||||
|
||||
test('getClientLicenseOld', async () => {
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Notification';
|
||||
const expectedUrl = `${client.urlVersion}/license/client?format=old`;
|
||||
const expectedOptions = {method: 'get', groupLabel};
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ describe('ClientGeneral', () => {
|
|||
});
|
||||
|
||||
test('getGlobalDataRetentionPolicy', async () => {
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Notification';
|
||||
const expectedUrl = `${client.getGlobalDataRetentionRoute()}/policy`;
|
||||
const expectedOptions = {method: 'get', groupLabel};
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ describe('ClientGeneral', () => {
|
|||
const userId = 'user1';
|
||||
const page = 1;
|
||||
const perPage = 10;
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Notification';
|
||||
const expectedUrl = `${client.getGranularDataRetentionRoute(userId)}/team_policies${buildQueryString({page, per_page: perPage})}`;
|
||||
const expectedOptions = {method: 'get', groupLabel};
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ describe('ClientGeneral', () => {
|
|||
const userId = 'user1';
|
||||
const page = 1;
|
||||
const perPage = 10;
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Notification';
|
||||
const expectedUrl = `${client.getGranularDataRetentionRoute(userId)}/channel_policies${buildQueryString({page, per_page: perPage})}`;
|
||||
const expectedOptions = {method: 'get', groupLabel};
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ describe('ClientGeneral', () => {
|
|||
|
||||
test('getRolesByNames', async () => {
|
||||
const rolesNames = ['role1', 'role2'];
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Notification';
|
||||
const expectedUrl = `${client.getRolesRoute()}/names`;
|
||||
const expectedOptions = {method: 'post', body: rolesNames, groupLabel};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,21 +14,21 @@ type PoliciesResponse<T> = {
|
|||
}
|
||||
|
||||
export interface ClientGeneralMix {
|
||||
ping: (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => Promise<any>;
|
||||
ping: (deviceId?: string, timeoutInterval?: number, groupLabel?: RequestGroupLabel) => Promise<any>;
|
||||
logClientError: (message: string, level?: string) => Promise<any>;
|
||||
getClientConfigOld: (groupLabel?: string) => Promise<ClientConfig>;
|
||||
getClientLicenseOld: (groupLabel?: string) => Promise<ClientLicense>;
|
||||
getClientConfigOld: (groupLabel?: RequestGroupLabel) => Promise<ClientConfig>;
|
||||
getClientLicenseOld: (groupLabel?: RequestGroupLabel) => Promise<ClientLicense>;
|
||||
getTimezones: () => Promise<string[]>;
|
||||
getGlobalDataRetentionPolicy: (groupLabel?: string) => Promise<GlobalDataRetentionPolicy>;
|
||||
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
|
||||
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
|
||||
getRolesByNames: (rolesNames: string[], groupLabel?: string) => Promise<Role[]>;
|
||||
getGlobalDataRetentionPolicy: (groupLabel?: RequestGroupLabel) => Promise<GlobalDataRetentionPolicy>;
|
||||
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: RequestGroupLabel) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
|
||||
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: RequestGroupLabel) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
|
||||
getRolesByNames: (rolesNames: string[], groupLabel?: RequestGroupLabel) => Promise<Role[]>;
|
||||
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
|
||||
sendPerformanceReport: (batch: PerformanceReport) => Promise<{}>;
|
||||
}
|
||||
|
||||
const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
ping = async (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => {
|
||||
ping = async (deviceId?: string, timeoutInterval?: number, groupLabel?: RequestGroupLabel) => {
|
||||
let url = `${this.urlVersion}/system/ping?time=${Date.now()}`;
|
||||
if (deviceId) {
|
||||
url = `${url}&device_id=${deviceId}`;
|
||||
|
|
@ -56,14 +56,14 @@ const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
|||
);
|
||||
};
|
||||
|
||||
getClientConfigOld = async (groupLabel?: string) => {
|
||||
getClientConfigOld = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/config/client?format=old`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getClientLicenseOld = async (groupLabel?: string) => {
|
||||
getClientLicenseOld = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/license/client?format=old`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -77,28 +77,28 @@ const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
|||
);
|
||||
};
|
||||
|
||||
getGlobalDataRetentionPolicy = (groupLabel?: string) => {
|
||||
getGlobalDataRetentionPolicy = (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getGlobalDataRetentionRoute()}/policy`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: string) => {
|
||||
getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getGranularDataRetentionRoute(userId)}/team_policies${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: string) => {
|
||||
getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getGranularDataRetentionRoute(userId)}/channel_policies${buildQueryString({page, per_page: perPage})}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getRolesByNames = async (rolesNames: string[], groupLabel?: string) => {
|
||||
getRolesByNames = async (rolesNames: string[], groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getRolesRoute()}/names`,
|
||||
{method: 'post', body: rolesNames, groupLabel},
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('ClientGroups', () => {
|
|||
|
||||
test('getAllGroupsAssociatedToChannel', async () => {
|
||||
const channelId = 'channel1';
|
||||
const groupLabel = 'testGroup';
|
||||
const groupLabel = 'WebSocket Reconnect';
|
||||
|
||||
await client.getAllGroupsAssociatedToChannel(channelId, undefined, groupLabel);
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ describe('ClientGroups', () => {
|
|||
|
||||
test('getAllGroupsAssociatedToMembership', async () => {
|
||||
const userId = 'user1';
|
||||
const groupLabel = 'testGroup';
|
||||
const groupLabel = 'WebSocket Reconnect';
|
||||
|
||||
await client.getAllGroupsAssociatedToMembership(userId, undefined, groupLabel);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import type ClientBase from './base';
|
|||
|
||||
export interface ClientGroupsMix {
|
||||
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean, groupLabel?: RequestGroupLabel) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean, groupLabel?: RequestGroupLabel) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
|
||||
}
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
|||
);
|
||||
};
|
||||
|
||||
getAllGroupsAssociatedToChannel = async (channelId: string, filterAllowReference = false, groupLabel?: string) => {
|
||||
getAllGroupsAssociatedToChannel = async (channelId: string, filterAllowReference = false, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/channels/${channelId}/groups${buildQueryString({
|
||||
paginate: false,
|
||||
|
|
@ -47,7 +47,7 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
|||
);
|
||||
};
|
||||
|
||||
getAllGroupsAssociatedToMembership = async (userId: string, filterAllowReference = false, groupLabel?: string) => {
|
||||
getAllGroupsAssociatedToMembership = async (userId: string, filterAllowReference = false, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.urlVersion}/users/${userId}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ import type ClientBase from './base';
|
|||
export interface ClientPostsMix {
|
||||
createPost: (post: Post) => Promise<Post>;
|
||||
updatePost: (post: Post) => Promise<Post>;
|
||||
getPost: (postId: string, groupLabel?: string) => Promise<Post>;
|
||||
getPost: (postId: string, groupLabel?: RequestGroupLabel) => Promise<Post>;
|
||||
patchPost: (postPatch: Partial<Post> & {id: string}) => Promise<Post>;
|
||||
deletePost: (postId: string) => Promise<any>;
|
||||
getPostThread: (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => Promise<PostResponse>;
|
||||
getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise<PostResponse>;
|
||||
getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise<PostResponse>;
|
||||
getPostThread: (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: RequestGroupLabel) => Promise<PostResponse>;
|
||||
getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: RequestGroupLabel) => Promise<PostResponse>;
|
||||
getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: RequestGroupLabel) => Promise<PostResponse>;
|
||||
getPostsBefore: (channelId: string, postId?: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
|
||||
getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
|
||||
getFileInfosForPost: (postId: string) => Promise<FileInfo[]>;
|
||||
|
|
@ -51,7 +51,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getPost = async (postId: string, groupLabel?: string) => {
|
||||
getPost = async (postId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getPostRoute(postId)}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -72,7 +72,7 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getPostThread = (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => {
|
||||
getPostThread = (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: RequestGroupLabel) => {
|
||||
const {
|
||||
fetchThreads = true,
|
||||
collapsedThreads = false,
|
||||
|
|
@ -88,14 +88,14 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: string) => {
|
||||
getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getPostsSince = async (channelId: string, since: number, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: string) => {
|
||||
getPostsSince = async (channelId: string, since: number, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getChannelRoute(channelId)}/posts${buildQueryString({since, collapsedThreads, collapsedThreadsExtended})}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ describe('ClientPreferences', () => {
|
|||
test('savePreferences', async () => {
|
||||
const userId = 'user_id';
|
||||
const preferences = [{category: 'category1', name: 'name1', value: 'value1'}] as PreferenceType[];
|
||||
const groupLabel = 'group_label';
|
||||
const groupLabel = 'Server Switch';
|
||||
const expectedUrl = client.getPreferencesRoute(userId);
|
||||
const expectedOptions = {
|
||||
method: 'put',
|
||||
|
|
@ -31,7 +31,7 @@ describe('ClientPreferences', () => {
|
|||
});
|
||||
|
||||
test('getMyPreferences', async () => {
|
||||
const groupLabel = 'group_label';
|
||||
const groupLabel = 'Server Switch';
|
||||
const expectedUrl = client.getPreferencesRoute('me');
|
||||
const expectedOptions = {
|
||||
method: 'get',
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@
|
|||
import type ClientBase from './base';
|
||||
|
||||
export interface ClientPreferencesMix {
|
||||
savePreferences: (userId: string, preferences: PreferenceType[], groupLabel?: string) => Promise<any>;
|
||||
savePreferences: (userId: string, preferences: PreferenceType[], groupLabel?: RequestGroupLabel) => Promise<any>;
|
||||
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
|
||||
getMyPreferences: (groupLabel?: string) => Promise<PreferenceType[]>;
|
||||
getMyPreferences: (groupLabel?: RequestGroupLabel) => Promise<PreferenceType[]>;
|
||||
}
|
||||
|
||||
const ClientPreferences = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
|
||||
savePreferences = async (userId: string, preferences: PreferenceType[], groupLabel?: string) => {
|
||||
savePreferences = async (userId: string, preferences: PreferenceType[], groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute(userId)}`,
|
||||
{method: 'put', body: preferences, groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getMyPreferences = async (groupLabel?: string) => {
|
||||
getMyPreferences = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getPreferencesRoute('me')}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ export interface ClientTeamsMix {
|
|||
deleteTeam: (teamId: string) => Promise<any>;
|
||||
updateTeam: (team: Team) => Promise<Team>;
|
||||
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean, groupLabel?: string) => Promise<Team[]>;
|
||||
getTeam: (teamId: string, groupLabel?: string) => Promise<Team>;
|
||||
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean, groupLabel?: RequestGroupLabel) => Promise<Team[]>;
|
||||
getTeam: (teamId: string, groupLabel?: RequestGroupLabel) => Promise<Team>;
|
||||
getTeamByName: (teamName: string) => Promise<Team>;
|
||||
getMyTeams: (groupLabel?: string) => Promise<Team[]>;
|
||||
getMyTeams: (groupLabel?: RequestGroupLabel) => Promise<Team[]>;
|
||||
getTeamsForUser: (userId: string) => Promise<Team[]>;
|
||||
getMyTeamMembers: (groupLabel?: string) => Promise<TeamMembership[]>;
|
||||
getMyTeamMembers: (groupLabel?: RequestGroupLabel) => Promise<TeamMembership[]>;
|
||||
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
|
||||
getTeamMember: (teamId: string, userId: string, groupLabel?: string) => Promise<TeamMembership>;
|
||||
getTeamMember: (teamId: string, userId: string, groupLabel?: RequestGroupLabel) => Promise<TeamMembership>;
|
||||
getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise<TeamMembership[]>;
|
||||
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
|
||||
addUsersToTeamGracefully: (teamId: string, userIds: string[]) => Promise<TeamMemberWithError[]>;
|
||||
|
|
@ -59,14 +59,14 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false, groupLabel?: string) => {
|
||||
getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`,
|
||||
{method: 'get', groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getTeam = async (teamId: string, groupLabel?: string) => {
|
||||
getTeam = async (teamId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
this.getTeamRoute(teamId),
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -80,7 +80,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getMyTeams = async (groupLabel?: string) => {
|
||||
getMyTeams = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -94,7 +94,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getMyTeamMembers = async (groupLabel?: string) => {
|
||||
getMyTeamMembers = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/teams/members`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -108,7 +108,7 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getTeamMember = async (teamId: string, userId: string, groupLabel?: string) => {
|
||||
getTeamMember = async (teamId: string, userId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getTeamMemberRoute(teamId, userId)}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ describe('ClientThreads', () => {
|
|||
const totalsOnly = true;
|
||||
const serverVersion = '6.0.0';
|
||||
const excludeDirect = true;
|
||||
const groupLabel = 'group1';
|
||||
const groupLabel = 'Cold Start';
|
||||
const queryStringObj = {
|
||||
extended: 'true',
|
||||
before,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface ClientThreadsMix {
|
|||
userId: string, teamId: string,
|
||||
before?: string, after?: string, pageSize?: number,
|
||||
deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string,
|
||||
excludeDirect?: boolean, groupLabel?: string,
|
||||
excludeDirect?: boolean, groupLabel?: RequestGroupLabel,
|
||||
) => Promise<GetUserThreadsResponse>;
|
||||
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
|
||||
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
|
||||
|
|
@ -26,7 +26,7 @@ const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase)
|
|||
userId: string, teamId: string,
|
||||
before = '', after = '', pageSize = PER_PAGE_DEFAULT,
|
||||
deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '',
|
||||
excludeDirect = false, groupLabel?: string) => {
|
||||
excludeDirect = false, groupLabel?: RequestGroupLabel) => {
|
||||
const queryStringObj: Record<string, any> = {
|
||||
extended: 'true',
|
||||
before,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {Events} from '@constants';
|
||||
import test_helper from '@test/test_helper';
|
||||
|
||||
import * as ClientConstants from './constants';
|
||||
import ClientTracking from './tracking';
|
||||
import ClientTracking, {testExports} from './tracking';
|
||||
|
||||
import type ClientError from './error';
|
||||
import type {APIClientInterface, ClientResponseMetrics} from '@mattermost/react-native-network-client';
|
||||
|
||||
type ParallelGroup = typeof testExports.ParallelGroup;
|
||||
|
||||
jest.mock('react-native', () => ({
|
||||
DeviceEventEmitter: {
|
||||
emit: jest.fn(),
|
||||
|
|
@ -50,7 +58,11 @@ jest.mock('@init/credentials', () => ({
|
|||
setServerCredentials: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ClientTraking', () => {
|
||||
jest.mock('@managers/performance_metrics_manager', () => ({
|
||||
collectNetworkRequestData: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ClientTracking', () => {
|
||||
const apiClientMock = {
|
||||
baseUrl: 'https://example.com',
|
||||
get: jest.fn(),
|
||||
|
|
@ -62,11 +74,22 @@ describe('ClientTraking', () => {
|
|||
|
||||
let client: ClientTracking;
|
||||
|
||||
beforeAll(() => {
|
||||
LocalConfig.CollectNetworkMetrics = true;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
client = new ClientTracking(apiClientMock as unknown as APIClientInterface);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
LocalConfig.CollectNetworkMetrics = false;
|
||||
});
|
||||
|
||||
it('should set bearer token', () => {
|
||||
const token = 'testToken';
|
||||
client.setBearerToken(token);
|
||||
|
|
@ -92,60 +115,73 @@ describe('ClientTraking', () => {
|
|||
});
|
||||
|
||||
it('should initialize and track group data', () => {
|
||||
client.initTrackGroup('testGroup');
|
||||
expect(client.requestGroups.has('testGroup')).toBe(true);
|
||||
client.initTrackGroup('Cold Start');
|
||||
expect(client.requestGroups.has('Cold Start')).toBe(true);
|
||||
|
||||
client.trackRequest('testGroup', 'https://example.com/api', {size: 100, compressedSize: 50} as ClientResponseMetrics);
|
||||
const group = client.requestGroups.get('testGroup')!;
|
||||
client.trackRequest('Cold Start', 'https://example.com/api', {size: 100, compressedSize: 50} as ClientResponseMetrics);
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
expect(group.totalSize).toBe(100);
|
||||
expect(group.totalCompressedSize).toBe(50);
|
||||
expect(group.urls['https://example.com/api'].count).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment and decrement request count', () => {
|
||||
client.initTrackGroup('testGroup');
|
||||
client.incrementRequestCount('testGroup');
|
||||
let group = client.requestGroups.get('testGroup')!;
|
||||
client.initTrackGroup('Cold Start');
|
||||
client.incrementRequestCount('Cold Start');
|
||||
let group = client.requestGroups.get('Cold Start')!;
|
||||
expect(group.activeCount).toBe(1);
|
||||
|
||||
client.decrementRequestCount('testGroup');
|
||||
group = client.requestGroups.get('testGroup')!;
|
||||
client.decrementRequestCount('Cold Start');
|
||||
group = client.requestGroups.get('Cold Start')!;
|
||||
expect(group.activeCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle request completion', () => {
|
||||
client.initTrackGroup('testGroup');
|
||||
client.handleRequestCompletion('testGroup');
|
||||
client.initTrackGroup('Cold Start');
|
||||
client.handleRequestCompletion('Cold Start');
|
||||
|
||||
expect(require('@utils/log').logDebug).toHaveBeenCalledWith('Group "testGroup" completed.');
|
||||
expect(require('@utils/log').logDebug).toHaveBeenCalledWith('Group "Cold Start" completed.');
|
||||
});
|
||||
|
||||
it('should clear completion timer', () => {
|
||||
client.initTrackGroup('testGroup');
|
||||
const group = client.requestGroups.get('testGroup')!;
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
group.completionTimer = setTimeout(() => {}, 100);
|
||||
|
||||
client.clearCompletionTimer('testGroup');
|
||||
client.clearCompletionTimer('Cold Start');
|
||||
expect(group.completionTimer).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should check if all requests are completed', () => {
|
||||
client.initTrackGroup('testGroup');
|
||||
const result = client.allRequestsCompleted('testGroup');
|
||||
client.initTrackGroup('Cold Start');
|
||||
const result = client.allRequestsCompleted('Cold Start');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should send telemetry event', () => {
|
||||
client.initTrackGroup('testGroup');
|
||||
const group = client.requestGroups.get('testGroup')!;
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
group.urls = {
|
||||
'https://example.com/api': {
|
||||
count: 2,
|
||||
metrics: {latency: 200, networkType: 'Wi-Fi', tlsCipherSuite: 'none', tlsVersion: 'none', isCached: false, httpVersion: 'h2', compressedSize: 10 * 1024, size: 6 * 1024 * 1024, connectionTime: 0, startTime: 0, endTime: 100, speedInMbps: 1},
|
||||
metrics: {
|
||||
latency: 200,
|
||||
startTime: Date.now(),
|
||||
endTime: Date.now() + 300,
|
||||
speedInMbps: 1,
|
||||
networkType: 'Wi-Fi',
|
||||
tlsCipherSuite: 'none',
|
||||
tlsVersion: 'none',
|
||||
isCached: false,
|
||||
httpVersion: 'h2',
|
||||
compressedSize: 10 * 1024,
|
||||
size: 6 * 1024 * 1024,
|
||||
connectionTime: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
client.sendTelemetryEvent('testGroup', group, 1000);
|
||||
client.sendTelemetryEvent('Cold Start', group, 1000);
|
||||
expect(require('@utils/log').logInfo).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
@ -173,7 +209,7 @@ describe('ClientTraking', () => {
|
|||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
groupLabel: 'testGroup',
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
const result = await client.doFetchWithTracking('https://example.com/api', options);
|
||||
|
|
@ -185,12 +221,98 @@ describe('ClientTraking', () => {
|
|||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
groupLabel: 'testGroup',
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
await expect(client.doFetchWithTracking('https://example.com/api', options)).rejects.toThrow('Received invalid response from the server.');
|
||||
});
|
||||
|
||||
it('should handle non-ok response with error details', async () => {
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: false,
|
||||
data: {
|
||||
message: 'Custom error message',
|
||||
id: 'error_id_123',
|
||||
},
|
||||
code: 400,
|
||||
});
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
try {
|
||||
await client.doFetchWithTracking('https://example.com/api', options);
|
||||
fail('Expected error to be thrown');
|
||||
} catch (error: unknown) {
|
||||
const clientError = error as ClientError;
|
||||
|
||||
expect((clientError.details as {message: string}).message).toBe('Custom error message');
|
||||
expect((clientError.details as {server_error_id: string}).server_error_id).toBe('error_id_123');
|
||||
expect((clientError.details as {status_code: string}).status_code).toBe(400);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle non-ok response without error details', async () => {
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: false,
|
||||
data: {},
|
||||
code: 500,
|
||||
});
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
try {
|
||||
await client.doFetchWithTracking('https://example.com/api', options);
|
||||
fail('Expected error to be thrown');
|
||||
} catch (error: unknown) {
|
||||
const clientError = error as ClientError;
|
||||
|
||||
expect((clientError.details as {message: string}).message).toBe('Response with status code 500');
|
||||
expect((clientError.details as {status_code: string}).status_code).toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle response with bearer token header', async () => {
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: true,
|
||||
data: {success: true},
|
||||
headers: {
|
||||
Token: 'new_bearer_token',
|
||||
},
|
||||
});
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
await client.doFetchWithTracking('https://example.com/api', options);
|
||||
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new_bearer_token`);
|
||||
});
|
||||
|
||||
it('should handle response with lowercase bearer token header', async () => {
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: true,
|
||||
data: {success: true},
|
||||
headers: {
|
||||
token: 'new_lowercase_token',
|
||||
},
|
||||
});
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
await client.doFetchWithTracking('https://example.com/api', options);
|
||||
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new_lowercase_token`);
|
||||
});
|
||||
|
||||
it('should call increment and decrement the same number of times as doFetchWithTracking, and handleRequestCompletion only once', async () => {
|
||||
const incrementSpy = jest.spyOn(client, 'incrementRequestCount');
|
||||
const decrementSpy = jest.spyOn(client, 'decrementRequestCount');
|
||||
|
|
@ -203,67 +325,501 @@ describe('ClientTraking', () => {
|
|||
ok: true,
|
||||
data: {success: true},
|
||||
headers: {},
|
||||
metrics: {latency: 200, size: 500, compressedSize: 300},
|
||||
metrics: {latency: 200, size: 500, compressedSize: 300, startTime: Date.now(), endTime: Date.now() + 100, speedInMbps: 1},
|
||||
});
|
||||
}, 200); // Simulate a 100ms network delay
|
||||
});
|
||||
});
|
||||
|
||||
client.initTrackGroup('testGroup');
|
||||
client.initTrackGroup('Cold Start');
|
||||
|
||||
// Simulate multiple fetch calls
|
||||
const promises = [
|
||||
client.doFetchWithTracking('https://example.com/api1', {method: 'GET', groupLabel: 'testGroup'}),
|
||||
client.doFetchWithTracking('https://example.com/api2', {method: 'GET', groupLabel: 'testGroup'}),
|
||||
client.doFetchWithTracking('https://example.com/api3', {method: 'GET', groupLabel: 'testGroup'}),
|
||||
client.doFetchWithTracking('https://example.com/api1', {method: 'GET', groupLabel: 'Cold Start'}),
|
||||
client.doFetchWithTracking('https://example.com/api2', {method: 'GET', groupLabel: 'Cold Start'}),
|
||||
client.doFetchWithTracking('https://example.com/api3', {method: 'GET', groupLabel: 'Cold Start'}),
|
||||
];
|
||||
|
||||
// Wait for all fetches to resolve
|
||||
await Promise.all(promises);
|
||||
|
||||
const group = client.requestGroups.get('testGroup')!;
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
expect(group.activeCount).toBe(0);
|
||||
|
||||
await test_helper.wait(100);
|
||||
await test_helper.wait(300);
|
||||
|
||||
expect(handleRequestCompletionSpy).toHaveBeenCalledTimes(1);
|
||||
expect(incrementSpy).toHaveBeenCalledTimes(3);
|
||||
expect(decrementSpy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle invalid HTTP method', async () => {
|
||||
const options = {
|
||||
method: 'INVALID' as string,
|
||||
groupLabel: 'Cold Start' as RequestGroupLabel,
|
||||
};
|
||||
|
||||
const result = await client.doFetchWithTracking('https://example.com/api', options) as unknown as {error: string};
|
||||
expect(result.error).toEqual(new Error('Invalid request method'));
|
||||
});
|
||||
|
||||
it('should handle server version changes without cache control', async () => {
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: true,
|
||||
data: {success: true},
|
||||
headers: {
|
||||
'x-version-id': '5.30.0',
|
||||
},
|
||||
});
|
||||
|
||||
await client.doFetchWithTracking('https://example.com/api', {
|
||||
method: 'GET',
|
||||
groupLabel: 'Cold Start',
|
||||
});
|
||||
|
||||
expect(client.serverVersion).toBe('5.30.0');
|
||||
expect(DeviceEventEmitter.emit).toHaveBeenCalledWith(
|
||||
Events.SERVER_VERSION_CHANGED,
|
||||
{serverUrl: 'https://example.com', serverVersion: '5.30.0'},
|
||||
);
|
||||
});
|
||||
|
||||
it('should not update server version when cache control present', async () => {
|
||||
client.serverVersion = '5.20.0';
|
||||
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: true,
|
||||
data: {success: true},
|
||||
headers: {
|
||||
'x-version-id': '5.31.0',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
|
||||
await client.doFetchWithTracking('https://example.com/api', {
|
||||
method: 'GET',
|
||||
groupLabel: 'Cold Start',
|
||||
});
|
||||
|
||||
expect(client.serverVersion).toBe('5.20.0');
|
||||
});
|
||||
|
||||
it('should track duplicate requests correctly and log duplicate details', async () => {
|
||||
const logDebugSpy = jest.spyOn(require('@utils/log'), 'logDebug');
|
||||
apiClientMock.get.mockResolvedValue({
|
||||
ok: true,
|
||||
data: {success: true},
|
||||
headers: {},
|
||||
metrics: {latency: 150, size: 200, compressedSize: 100},
|
||||
metrics: {latency: 150, size: 200, compressedSize: 100, startTime: Date.now(), endTime: Date.now() + 100, speedInMbps: 1},
|
||||
});
|
||||
|
||||
client.initTrackGroup('testGroup');
|
||||
client.initTrackGroup('Cold Start');
|
||||
|
||||
// Simulate multiple fetch calls to the same URL
|
||||
const promises = [
|
||||
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'testGroup'}),
|
||||
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'testGroup'}),
|
||||
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'testGroup'}),
|
||||
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}),
|
||||
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}),
|
||||
client.doFetchWithTracking('https://example.com/api', {method: 'GET', groupLabel: 'Cold Start'}),
|
||||
];
|
||||
|
||||
// Wait for all fetches to resolve
|
||||
await Promise.all(promises);
|
||||
|
||||
// Verify that the group tracked duplicates correctly
|
||||
const group = client.requestGroups.get('testGroup')!;
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
expect(group.urls['https://example.com/api'].count).toBe(3); // URL was requested 3 times
|
||||
expect(group.totalSize).toBe(600); // Total size = 3 * 200
|
||||
expect(group.totalCompressedSize).toBe(300); // Total compressed size = 3 * 100
|
||||
|
||||
await test_helper.wait(100);
|
||||
await test_helper.wait(300);
|
||||
|
||||
// Verify duplicate logging
|
||||
expect(logDebugSpy).toHaveBeenCalledWith(
|
||||
'Duplicate URLs:\n',
|
||||
expect.stringContaining('https://example.com/api'),
|
||||
expect.stringContaining('Duplicate URLs:\n1 - https://example.com/api'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return true when checking allRequestsCompleted for non-existent group', () => {
|
||||
// Try to check completion status for a non-existent group
|
||||
const result = client.allRequestsCompleted('Non Existent Group' as RequestGroupLabel);
|
||||
|
||||
// Should return true since there are no pending requests
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
describe('Request Categorization and Speed Calculations', () => {
|
||||
it('should create multiple parallel groups for non-overlapping requests', () => {
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
const baseTime = Date.now();
|
||||
|
||||
// First parallel group (2 concurrent requests)
|
||||
group.urls = {
|
||||
'https://example.com/api1': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 100,
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + 200,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
'https://example.com/api2': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 150,
|
||||
startTime: baseTime + 50,
|
||||
endTime: baseTime + 250,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
|
||||
// Second parallel group (3 concurrent requests)
|
||||
'https://example.com/api3': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 120,
|
||||
startTime: baseTime + 300,
|
||||
endTime: baseTime + 500,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
'https://example.com/api4': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 180,
|
||||
startTime: baseTime + 320,
|
||||
endTime: baseTime + 520,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
'https://example.com/api5': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 160,
|
||||
startTime: baseTime + 340,
|
||||
endTime: baseTime + 540,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
};
|
||||
|
||||
const result = client.categorizeRequests('Cold Start');
|
||||
|
||||
// Verify parallel groups
|
||||
expect(result.parallelGroups).toHaveLength(2);
|
||||
expect(result.maxConcurrency).toBe(3);
|
||||
|
||||
// First group should have 2 requests
|
||||
expect(result.parallelGroups[0].requests).toHaveLength(2);
|
||||
expect(result.parallelGroups[0].latency).toBe(150); // Max latency of first group
|
||||
|
||||
// Second group should have 3 requests
|
||||
expect(result.parallelGroups[1].requests).toHaveLength(3);
|
||||
expect(result.parallelGroups[1].latency).toBe(180); // Max latency of second group
|
||||
});
|
||||
|
||||
it('should handle overlapping parallel groups correctly', () => {
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
const baseTime = Date.now();
|
||||
|
||||
group.urls = {
|
||||
|
||||
// First request starts early and ends late
|
||||
'https://example.com/api1': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 500,
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + 1000,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
|
||||
// These requests start during the first request
|
||||
'https://example.com/api2': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 200,
|
||||
startTime: baseTime + 100,
|
||||
endTime: baseTime + 300,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
'https://example.com/api3': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 200,
|
||||
startTime: baseTime + 200,
|
||||
endTime: baseTime + 400,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
};
|
||||
|
||||
const result = client.categorizeRequests('Cold Start');
|
||||
|
||||
// Should create a single parallel group since all requests overlap
|
||||
expect(result.parallelGroups).toHaveLength(1);
|
||||
expect(result.maxConcurrency).toBe(3);
|
||||
|
||||
// The group should contain all 3 requests
|
||||
expect(result.parallelGroups[0].requests).toHaveLength(3);
|
||||
expect(result.parallelGroups[0].latency).toBe(500); // Should take the max latency
|
||||
});
|
||||
|
||||
it('should handle requests with latency extending beyond group end time', () => {
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
const baseTime = Date.now();
|
||||
const dataTransferTime = 200;
|
||||
|
||||
group.urls = {
|
||||
|
||||
// First request starts and ends quickly
|
||||
'https://example.com/api1': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 100,
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + dataTransferTime + 100,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
|
||||
// Second request starts at same time but has much longer latency
|
||||
'https://example.com/api2': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 500, // Long latency
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + dataTransferTime + 500, // Ends much later
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
};
|
||||
|
||||
const result = client.categorizeRequests('Cold Start');
|
||||
|
||||
// Should create a single parallel group
|
||||
expect(result.parallelGroups).toHaveLength(1);
|
||||
expect(result.maxConcurrency).toBe(2);
|
||||
|
||||
const group1 = result.parallelGroups[0];
|
||||
|
||||
// Group end time should be the earliest end time
|
||||
expect(group1.endTime).toBe(baseTime + dataTransferTime + 100);
|
||||
|
||||
// But latency should be from the longest request
|
||||
expect(group1.latency).toBe(500);
|
||||
});
|
||||
|
||||
it('should handle requests with latency extending beyond group end time and separate parallel groups', () => {
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
const baseTime = Date.now();
|
||||
const dataTransferTime = 200;
|
||||
const firstGroupMinEndTime = baseTime + dataTransferTime + 100;
|
||||
|
||||
group.urls = {
|
||||
|
||||
// First parallel group
|
||||
'https://example.com/api1': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 100,
|
||||
startTime: baseTime,
|
||||
endTime: firstGroupMinEndTime,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
'https://example.com/api2': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 500,
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + dataTransferTime + 500,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
|
||||
// Second parallel group - starts after first group has ended
|
||||
'https://example.com/api3': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 300,
|
||||
startTime: firstGroupMinEndTime + 1, // Starts after first group
|
||||
endTime: firstGroupMinEndTime + dataTransferTime + 300,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
'https://example.com/api4': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 600,
|
||||
startTime: firstGroupMinEndTime + 1, // Starts with api3
|
||||
endTime: firstGroupMinEndTime + 100 + 600,
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
};
|
||||
|
||||
const result = client.categorizeRequests('Cold Start');
|
||||
|
||||
// Should create two parallel groups
|
||||
expect(result.parallelGroups).toHaveLength(2);
|
||||
expect(result.maxConcurrency).toBe(2);
|
||||
|
||||
const group1 = result.parallelGroups[0];
|
||||
const group2 = result.parallelGroups[1];
|
||||
|
||||
// First group assertions
|
||||
expect(group1.endTime).toBe(baseTime + dataTransferTime + 100);
|
||||
|
||||
// longest latency on the first group is 500, but the first group ends after 300ms, so the effective latency is only 300ms
|
||||
expect(group1.latency).toBe(300);
|
||||
expect(group1.requests).toHaveLength(2);
|
||||
|
||||
// Second group assertions
|
||||
expect(group2.endTime).toBe(firstGroupMinEndTime + dataTransferTime + 300);
|
||||
|
||||
// second group min endTime after 500ms, but it's the last group to end, so the effective latency for the group
|
||||
// is 600ms (longest latency)
|
||||
expect(group2.latency).toBe(600);
|
||||
expect(group2.requests).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should calculate average speed and effective latency for multiple parallel groups', () => {
|
||||
const baseTime = Date.now();
|
||||
|
||||
// Create test parallel groups with known data sizes and timings
|
||||
const parallelGroups: ParallelGroup[] = [
|
||||
{
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + 1000,
|
||||
latency: 200,
|
||||
requests: [
|
||||
{
|
||||
latency: 200,
|
||||
startTime: baseTime,
|
||||
endTime: baseTime + 1000,
|
||||
compressedSize: 1000000, // 1MB
|
||||
} as ClientResponseMetrics,
|
||||
{
|
||||
latency: 150,
|
||||
startTime: baseTime + 100,
|
||||
endTime: baseTime + 900,
|
||||
compressedSize: 500000, // 0.5MB
|
||||
} as ClientResponseMetrics,
|
||||
],
|
||||
},
|
||||
{
|
||||
startTime: baseTime + 1100,
|
||||
endTime: baseTime + 2000,
|
||||
latency: 300,
|
||||
requests: [
|
||||
{
|
||||
latency: 300,
|
||||
startTime: baseTime + 1100,
|
||||
endTime: baseTime + 2000,
|
||||
compressedSize: 2000000, // 2MB
|
||||
} as ClientResponseMetrics,
|
||||
{
|
||||
latency: 250,
|
||||
startTime: baseTime + 1200,
|
||||
endTime: baseTime + 1900,
|
||||
compressedSize: 1500000, // 1.5MB
|
||||
} as ClientResponseMetrics,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Total compressed size: 5MB (5,000,000 bytes)
|
||||
// Total elapsed time: 2 seconds
|
||||
// Total effective latency: 500ms (200ms + 300ms)
|
||||
const result = client.calculateAverageSpeedWithCategories(parallelGroups, 2);
|
||||
|
||||
// Expected average speed:
|
||||
// Total bits = 5,000,000 * 8 = 40,000,000 bits
|
||||
// Data transfer time = 2 seconds - (500ms / 1000) = 1.5 seconds
|
||||
// Speed = 40,000,000 / 1.5 = 26,666,666.67 bps = 26.67 Mbps
|
||||
expect(result.averageSpeedMbps).toBeCloseTo(26.67, 1);
|
||||
expect(result.effectiveLatency).toBe(500); // Sum of max latencies from each group
|
||||
});
|
||||
|
||||
it('should handle zero transfer time in speed calculation', () => {
|
||||
client.initTrackGroup('Cold Start');
|
||||
const group = client.requestGroups.get('Cold Start')!;
|
||||
const now = Date.now();
|
||||
|
||||
// Set up metrics where transfer time would be zero
|
||||
group.urls = {
|
||||
'https://example.com/api': {
|
||||
count: 1,
|
||||
metrics: {
|
||||
latency: 1000,
|
||||
startTime: now,
|
||||
endTime: now + 1000,
|
||||
speedInMbps: 0,
|
||||
networkType: 'Wi-Fi',
|
||||
tlsCipherSuite: 'none',
|
||||
tlsVersion: 'none',
|
||||
isCached: false,
|
||||
httpVersion: 'h2',
|
||||
size: 1000,
|
||||
compressedSize: 500,
|
||||
connectionTime: 0,
|
||||
} as ClientResponseMetrics,
|
||||
},
|
||||
};
|
||||
|
||||
const parallelGroups: ParallelGroup[] = [{
|
||||
startTime: now,
|
||||
endTime: now + 1000,
|
||||
latency: 1000,
|
||||
requests: [group.urls['https://example.com/api'].metrics!],
|
||||
}];
|
||||
|
||||
const result = client.calculateAverageSpeedWithCategories(
|
||||
parallelGroups,
|
||||
1, // 1 second elapsed
|
||||
);
|
||||
|
||||
expect(result.averageSpeedMbps).toBe(0); // Should be 0 since data transfer time is 0/negative
|
||||
expect(result.effectiveLatency).toBe(1000);
|
||||
});
|
||||
|
||||
it('should return empty result when groupData is not found', () => {
|
||||
// Try to categorize requests for a non-existent group
|
||||
const result = client.categorizeRequests('Non Existent Group' as RequestGroupLabel);
|
||||
|
||||
// Verify empty result structure
|
||||
expect(result.parallelGroups).toEqual([]);
|
||||
expect(result.maxConcurrency).toBe(0);
|
||||
});
|
||||
|
||||
it('should return default latency when groupData is not found', () => {
|
||||
// Try to get average latency for a non-existent group
|
||||
const result = client.getAverageLatency('Non Existent Group' as RequestGroupLabel);
|
||||
|
||||
// Should return default latency of 100ms
|
||||
expect(result).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
|
||||
import {DeviceEventEmitter, Platform} from 'react-native';
|
||||
|
||||
import {CollectNetworkMetrics} from '@assets/config.json';
|
||||
import {Events} from '@constants';
|
||||
import {t} from '@i18n';
|
||||
import {setServerCredentials} from '@init/credentials';
|
||||
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
||||
import {NetworkRequestMetrics} from '@managers/performance_metrics_manager/constant';
|
||||
import {getFormattedFileSize} from '@utils/file';
|
||||
import {logDebug, logInfo} from '@utils/log';
|
||||
import {semverFromServerVersion} from '@utils/server';
|
||||
|
|
@ -30,6 +33,30 @@ type GroupData = {
|
|||
completionTimer?: NodeJS.Timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* ParallelGroup
|
||||
* @typedef {Object} ParallelGroup
|
||||
* @property {number} startTime is the start time of all requests in the group
|
||||
* @property {number} endTime is min end time of all requests in the group
|
||||
* @property {number} latency is the max latency (in ms)
|
||||
* @property {ClientResponseMetrics[]} requests is the list of requests in the group
|
||||
*/
|
||||
type ParallelGroup = {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
latency: number;
|
||||
requests: ClientResponseMetrics[];
|
||||
}
|
||||
|
||||
type CategorizedRequestsResult = {
|
||||
parallelGroups: ParallelGroup[];
|
||||
maxConcurrency: number;
|
||||
};
|
||||
|
||||
export const testExports = {
|
||||
ParallelGroup: {} as ParallelGroup,
|
||||
};
|
||||
|
||||
export default class ClientTracking {
|
||||
apiClient: APIClientInterface;
|
||||
csrfToken = '';
|
||||
|
|
@ -63,7 +90,7 @@ export default class ClientTracking {
|
|||
return headers;
|
||||
}
|
||||
|
||||
initTrackGroup(groupLabel: string) {
|
||||
initTrackGroup(groupLabel: RequestGroupLabel) {
|
||||
if (!this.requestGroups.has(groupLabel)) {
|
||||
this.requestGroups.set(groupLabel, {
|
||||
activeCount: 0,
|
||||
|
|
@ -76,7 +103,7 @@ export default class ClientTracking {
|
|||
}
|
||||
}
|
||||
|
||||
trackRequest(groupLabel: string, url: string, metrics?: ClientResponseMetrics) {
|
||||
trackRequest(groupLabel: RequestGroupLabel, url: string, metrics?: ClientResponseMetrics) {
|
||||
this.initTrackGroup(groupLabel);
|
||||
|
||||
const group = this.requestGroups.get(groupLabel)!;
|
||||
|
|
@ -93,32 +120,134 @@ export default class ClientTracking {
|
|||
group.totalCompressedSize += metrics?.compressedSize ?? 0;
|
||||
}
|
||||
|
||||
incrementRequestCount(groupLabel: string) {
|
||||
getAverageLatency(groupLabel: RequestGroupLabel): number {
|
||||
const groupData = this.requestGroups.get(groupLabel);
|
||||
if (!groupData) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
const urlData = Object.entries(groupData.urls);
|
||||
const sumLatency = urlData.reduce((result, url) => (result + (url[1].metrics?.latency ?? 0)), 0);
|
||||
return sumLatency / urlData.length;
|
||||
}
|
||||
|
||||
categorizeRequests(groupLabel: RequestGroupLabel): CategorizedRequestsResult {
|
||||
const groupData = this.requestGroups.get(groupLabel);
|
||||
if (!groupData) {
|
||||
const emptyResult: CategorizedRequestsResult = {
|
||||
parallelGroups: [],
|
||||
maxConcurrency: 0,
|
||||
};
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
const requestsMetrics = Object.entries(groupData.urls).
|
||||
map((e) => e[1].metrics).
|
||||
filter((m) => m != null);
|
||||
requestsMetrics.sort((a, b) => a.startTime - b.startTime);
|
||||
|
||||
const parallelGroups: ParallelGroup[] = [];
|
||||
let maxConcurrency = 0;
|
||||
|
||||
// First pass: group requests
|
||||
for (const metrics of requestsMetrics) {
|
||||
const groupIndex = parallelGroups.findIndex((g) => metrics.startTime <= g.endTime);
|
||||
|
||||
if (groupIndex >= 0) {
|
||||
const currentGroup = parallelGroups[groupIndex];
|
||||
currentGroup.requests.push(metrics);
|
||||
currentGroup.endTime = Math.min(currentGroup.endTime, metrics.endTime);
|
||||
currentGroup.latency = Math.max(currentGroup.latency, metrics.latency);
|
||||
maxConcurrency = Math.max(maxConcurrency, currentGroup.requests.length);
|
||||
} else {
|
||||
// Create a new parallel group
|
||||
parallelGroups.push({
|
||||
startTime: metrics.startTime,
|
||||
endTime: metrics.endTime,
|
||||
latency: metrics.latency,
|
||||
requests: [metrics],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: recalculate max latency for all groups (except the last)
|
||||
// The max latency should only be considered up until the startTime of the next group
|
||||
for (let i = 0; i < parallelGroups.length - 1; i++) {
|
||||
const currentGroup = parallelGroups[i];
|
||||
let maxGroupLatency = 0;
|
||||
|
||||
for (const metrics of currentGroup.requests) {
|
||||
let currentLatency = metrics.latency;
|
||||
|
||||
if ((metrics.startTime + metrics.latency) > currentGroup.endTime) {
|
||||
currentLatency = currentGroup.endTime - metrics.startTime;
|
||||
}
|
||||
maxGroupLatency = Math.max(maxGroupLatency, currentLatency);
|
||||
}
|
||||
|
||||
currentGroup.latency = maxGroupLatency;
|
||||
}
|
||||
|
||||
return {parallelGroups, maxConcurrency};
|
||||
}
|
||||
|
||||
calculateAverageSpeedWithCategories = (
|
||||
parallelGroups: ParallelGroup[],
|
||||
elapsedTimeInSeconds: number, // Observed total elapsed time in seconds
|
||||
): { averageSpeedMbps: number; effectiveLatency: number } => {
|
||||
// Step 1: Calculate total data size in bits
|
||||
const totalDataBits = parallelGroups.reduce((sum, group) => {
|
||||
return sum + group.requests.reduce((groupSum, req) => groupSum + (req.compressedSize * 8), 0);
|
||||
}, 0);
|
||||
|
||||
// Step 2: Calculate effective latency (in ms)
|
||||
const effectiveLatency = parallelGroups.reduce((sum, group) => sum + group.latency, 0);
|
||||
|
||||
// Step 3: Calculate data transfer time
|
||||
const dataTransferTime = elapsedTimeInSeconds - (effectiveLatency / 1000);
|
||||
|
||||
// Handle edge case: if data transfer time is zero or negative.
|
||||
if (dataTransferTime <= 0) {
|
||||
return {averageSpeedMbps: 0, effectiveLatency};
|
||||
}
|
||||
|
||||
// Step 4: Calculate average speed
|
||||
const averageSpeedBps = totalDataBits / dataTransferTime; // Speed in bps
|
||||
const averageSpeedMbps = averageSpeedBps / 1_000_000; // Convert to Mbps
|
||||
|
||||
return {
|
||||
averageSpeedMbps,
|
||||
effectiveLatency,
|
||||
};
|
||||
};
|
||||
|
||||
incrementRequestCount(groupLabel: RequestGroupLabel) {
|
||||
this.initTrackGroup(groupLabel);
|
||||
|
||||
const group = this.requestGroups.get(groupLabel)!;
|
||||
group.activeCount += 1;
|
||||
}
|
||||
|
||||
decrementRequestCount(groupLabel: string) {
|
||||
decrementRequestCount(groupLabel: RequestGroupLabel) {
|
||||
const group = this.requestGroups.get(groupLabel);
|
||||
if (group) {
|
||||
group.activeCount -= 1;
|
||||
|
||||
if (group.activeCount <= 0 && !group.completionFlag) {
|
||||
this.clearCompletionTimer(groupLabel);
|
||||
const latency = this.getAverageLatency(groupLabel);
|
||||
group.completionTimer = setTimeout(() => {
|
||||
if (this.allRequestsCompleted(groupLabel)) {
|
||||
group.completionFlag = true;
|
||||
this.handleRequestCompletion(groupLabel);
|
||||
this.clearCompletionTimer(groupLabel);
|
||||
}
|
||||
}, 100); // Adjust delay as needed (e.g., 100ms) should we set this based on latency or something?
|
||||
}, latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clearCompletionTimer(groupLabel: string) {
|
||||
clearCompletionTimer(groupLabel: RequestGroupLabel) {
|
||||
const group = this.requestGroups.get(groupLabel);
|
||||
if (group?.completionTimer) {
|
||||
clearTimeout(group.completionTimer);
|
||||
|
|
@ -126,7 +255,7 @@ export default class ClientTracking {
|
|||
}
|
||||
}
|
||||
|
||||
handleRequestCompletion(groupLabel: string) {
|
||||
handleRequestCompletion(groupLabel: RequestGroupLabel) {
|
||||
const group = this.requestGroups.get(groupLabel);
|
||||
if (group) {
|
||||
const duration = Date.now() - group.startTime;
|
||||
|
|
@ -138,31 +267,62 @@ export default class ClientTracking {
|
|||
}
|
||||
}
|
||||
|
||||
allRequestsCompleted(groupLabel: string): boolean {
|
||||
allRequestsCompleted(groupLabel: RequestGroupLabel): boolean {
|
||||
const group = this.requestGroups.get(groupLabel);
|
||||
return group ? group.activeCount <= 0 : true;
|
||||
}
|
||||
|
||||
sendTelemetryEvent(groupLabel: string, groupData: GroupData, duration: number) {
|
||||
const urls = Object.keys(groupData.urls);
|
||||
const urlData = Object.entries(groupData.urls);
|
||||
sendTelemetryEvent(groupLabel: RequestGroupLabel, groupData: GroupData, duration: number) {
|
||||
const {totalCompressedSize, totalSize, urls: groupedUrls} = groupData;
|
||||
const urls = Object.keys(groupedUrls);
|
||||
const urlData = Object.entries(groupedUrls);
|
||||
const dupe = urlData.filter((u) => u[1].count > 1);
|
||||
const urlCount = urlData.reduce((result, url) => (result + url[1].count), 0);
|
||||
const sumLatency = urlData.reduce((result, url) => (result + (url[1].metrics?.latency ?? 0)), 0);
|
||||
const latency = sumLatency / urlCount;
|
||||
logInfo(`Telemetry event on ${Platform.OS} for server ${this.apiClient.baseUrl}
|
||||
Group "${groupLabel}"
|
||||
requesting ${urls.length} urls
|
||||
total Compressed size of: ${getFormattedFileSize(groupData.totalCompressedSize)}
|
||||
total size of: ${getFormattedFileSize(groupData.totalSize)}
|
||||
elapsed time: ${duration / 1000} seconds
|
||||
average latency: ${latency} ms`);
|
||||
const latency = this.getAverageLatency(groupLabel);
|
||||
const {parallelGroups, maxConcurrency} = this.categorizeRequests(groupLabel);
|
||||
const elapsedTimeInSeconds = duration / 1000;
|
||||
const {averageSpeedMbps, effectiveLatency} = this.calculateAverageSpeedWithCategories(parallelGroups, elapsedTimeInSeconds);
|
||||
|
||||
logInfo(`Telemetry Event on ${Platform.OS} for Server ${this.apiClient.baseUrl}
|
||||
Group: "${groupLabel}"
|
||||
Total Requests: ${urls.length} URLs
|
||||
Max Concurrency: ${maxConcurrency}
|
||||
Parallel Groups: ${parallelGroups.length}
|
||||
Requests in Each Group: ${parallelGroups.map((g) => g.requests.length).join(', ')}
|
||||
Total Compressed Size: ${getFormattedFileSize(groupData.totalCompressedSize)}
|
||||
Total Size: ${getFormattedFileSize(groupData.totalSize)}
|
||||
Elapsed Time: ${elapsedTimeInSeconds} seconds (${duration} ms)
|
||||
Average Request Latency: ${latency} ms
|
||||
Effective Latency: ${effectiveLatency} ms
|
||||
Average Speed: ${averageSpeedMbps.toFixed(4)} Mbps
|
||||
`,
|
||||
);
|
||||
|
||||
if (dupe.length) {
|
||||
logDebug('Duplicate URLs:\n', dupe.map((d) => `${d[0]} ${JSON.stringify(d[1])}`).join('\n'));
|
||||
logDebug(`Duplicate URLs:\n${dupe.map((d, i) => `${i + 1} - ${d[0]} ${JSON.stringify(d[1])}`).join('\n')}`);
|
||||
}
|
||||
|
||||
// Integrate with telemetry framework here
|
||||
const {collectNetworkRequestData} = PerformanceMetricsManager;
|
||||
|
||||
const commonArguments = {serverUrl: this.apiClient.baseUrl, groupLabel};
|
||||
const metricsData: Array<[NetworkRequestMetrics, number]> = [
|
||||
[NetworkRequestMetrics.AverageSpeed, averageSpeedMbps],
|
||||
[NetworkRequestMetrics.EffectiveLatency, effectiveLatency],
|
||||
[NetworkRequestMetrics.ElapsedTime, elapsedTimeInSeconds],
|
||||
[NetworkRequestMetrics.Latency, latency],
|
||||
[NetworkRequestMetrics.TotalCompressedSize, totalCompressedSize],
|
||||
[NetworkRequestMetrics.TotalRequests, urls.length],
|
||||
[NetworkRequestMetrics.TotalSequentialRequests, parallelGroups.length],
|
||||
[NetworkRequestMetrics.TotalSize, totalSize],
|
||||
];
|
||||
|
||||
metricsData.forEach(([metric, value]) => {
|
||||
collectNetworkRequestData(metric, value, commonArguments);
|
||||
});
|
||||
|
||||
// Send metrics for each parallel group
|
||||
parallelGroups.forEach((group) => {
|
||||
collectNetworkRequestData(NetworkRequestMetrics.TotalParallelRequests, group.requests.length, commonArguments);
|
||||
});
|
||||
}
|
||||
|
||||
buildRequestOptions(options: ClientOptions): RequestOptions {
|
||||
|
|
@ -208,14 +368,14 @@ export default class ClientTracking {
|
|||
})};
|
||||
}
|
||||
|
||||
if (groupLabel) {
|
||||
if (groupLabel && CollectNetworkMetrics) {
|
||||
this.incrementRequestCount(groupLabel);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request!(url, this.buildRequestOptions(options));
|
||||
const headers: ClientHeaders = response.headers || {};
|
||||
if (groupLabel) {
|
||||
if (groupLabel && CollectNetworkMetrics) {
|
||||
this.trackRequest(groupLabel, url, response.metrics);
|
||||
}
|
||||
const serverVersion = semverFromServerVersion(
|
||||
|
|
@ -255,7 +415,7 @@ export default class ClientTracking {
|
|||
details: error,
|
||||
});
|
||||
} finally {
|
||||
if (groupLabel) {
|
||||
if (groupLabel && CollectNetworkMetrics) {
|
||||
this.decrementRequestCount(groupLabel);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type ClientBase from './base';
|
|||
|
||||
export interface ClientUsersMix {
|
||||
createUser: (user: UserProfile, token: string, inviteId: string) => Promise<UserProfile>;
|
||||
patchMe: (userPatch: Partial<UserProfile>, groupLabel?: string) => Promise<UserProfile>;
|
||||
patchMe: (userPatch: Partial<UserProfile>, groupLabel?: RequestGroupLabel) => Promise<UserProfile>;
|
||||
patchUser: (userPatch: Partial<UserProfile> & {id: string}) => Promise<UserProfile>;
|
||||
updateUser: (user: UserProfile) => Promise<UserProfile>;
|
||||
demoteUserToGuest: (userId: string) => Promise<any>;
|
||||
|
|
@ -21,15 +21,15 @@ export interface ClientUsersMix {
|
|||
loginById: (id: string, password: string, token?: string, deviceId?: string) => Promise<UserProfile>;
|
||||
logout: () => Promise<any>;
|
||||
getProfiles: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesByIds: (userIds: string[], options?: Record<string, any>, groupLabel?: string) => Promise<UserProfile[]>;
|
||||
getProfilesByUsernames: (usernames: string[], groupLabel?: string) => Promise<UserProfile[]>;
|
||||
getProfilesByIds: (userIds: string[], options?: Record<string, any>, groupLabel?: RequestGroupLabel) => Promise<UserProfile[]>;
|
||||
getProfilesByUsernames: (usernames: string[], groupLabel?: RequestGroupLabel) => Promise<UserProfile[]>;
|
||||
getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
|
||||
getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
|
||||
getProfilesInChannel: (channelId: string, options?: GetUsersOptions, groupLabel?: string) => Promise<UserProfile[]>;
|
||||
getProfilesInGroupChannels: (channelsIds: string[], groupLabel?: string) => Promise<{[x: string]: UserProfile[]}>;
|
||||
getProfilesInChannel: (channelId: string, options?: GetUsersOptions, groupLabel?: RequestGroupLabel) => Promise<UserProfile[]>;
|
||||
getProfilesInGroupChannels: (channelsIds: string[], groupLabel?: RequestGroupLabel) => Promise<{[x: string]: UserProfile[]}>;
|
||||
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
|
||||
getMe: (groupLabel?: string) => Promise<UserProfile>;
|
||||
getMe: (groupLabel?: RequestGroupLabel) => Promise<UserProfile>;
|
||||
getUser: (userId: string) => Promise<UserProfile>;
|
||||
getUserByUsername: (username: string) => Promise<UserProfile>;
|
||||
getUserByEmail: (email: string) => Promise<UserProfile>;
|
||||
|
|
@ -38,10 +38,10 @@ export interface ClientUsersMix {
|
|||
autocompleteUsers: (name: string, teamId: string, channelId?: string, options?: Record<string, any>) => Promise<{users: UserProfile[]; out_of_channel?: UserProfile[]}>;
|
||||
getSessions: (userId: string) => Promise<Session[]>;
|
||||
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
|
||||
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null, groupLabel?: string) => Promise<{}>;
|
||||
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null, groupLabel?: RequestGroupLabel) => Promise<{}>;
|
||||
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
|
||||
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
|
||||
getStatus: (userId: string, groupLabel?: string) => Promise<UserStatus>;
|
||||
getStatus: (userId: string, groupLabel?: RequestGroupLabel) => Promise<UserStatus>;
|
||||
updateStatus: (status: UserStatus) => Promise<UserStatus>;
|
||||
updateCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
|
||||
unsetCustomStatus: () => Promise<{status: string}>;
|
||||
|
|
@ -66,7 +66,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
patchMe = async (userPatch: Partial<UserProfile>, groupLabel?: string) => {
|
||||
patchMe = async (userPatch: Partial<UserProfile>, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}/patch`,
|
||||
{method: 'put', body: userPatch, groupLabel},
|
||||
|
|
@ -177,14 +177,14 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getProfilesByIds = async (userIds: string[], options = {}, groupLabel?: string) => {
|
||||
getProfilesByIds = async (userIds: string[], options = {}, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/ids${buildQueryString(options)}`,
|
||||
{method: 'post', body: userIds, groupLabel},
|
||||
);
|
||||
};
|
||||
|
||||
getProfilesByUsernames = async (usernames: string[], groupLabel?: string) => {
|
||||
getProfilesByUsernames = async (usernames: string[], groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/usernames`,
|
||||
{method: 'post', body: usernames, groupLabel},
|
||||
|
|
@ -217,7 +217,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getProfilesInChannel = async (channelId: string, options: GetUsersOptions, groupLabel?: string) => {
|
||||
getProfilesInChannel = async (channelId: string, options: GetUsersOptions, groupLabel?: RequestGroupLabel) => {
|
||||
const queryStringObj = {in_channel: channelId, ...options};
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
|
||||
|
|
@ -225,7 +225,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getProfilesInGroupChannels = async (channelsIds: string[], groupLabel?: string) => {
|
||||
getProfilesInGroupChannels = async (channelsIds: string[], groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/group_channels`,
|
||||
{method: 'post', body: channelsIds, groupLabel},
|
||||
|
|
@ -244,7 +244,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getMe = async (groupLabel?: string) => {
|
||||
getMe = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute('me')}`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -332,7 +332,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null, groupLabel?: string) => {
|
||||
setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUsersRoute()}/sessions/device`,
|
||||
{
|
||||
|
|
@ -361,7 +361,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
|
|||
);
|
||||
};
|
||||
|
||||
getStatus = async (userId: string, groupLabel?: string) => {
|
||||
getStatus = async (userId: string, groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getUserRoute(userId)}/status`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default {
|
|||
POST_CHUNK_SIZE: 60,
|
||||
POST_AROUND_CHUNK_SIZE: 10,
|
||||
CHANNELS_CHUNK_SIZE: 50,
|
||||
CHANNEL_MEMBERS_CHUNK_SIZE: 200,
|
||||
CRT_CHUNK_SIZE: 60,
|
||||
STATUS_INTERVAL: 60000,
|
||||
AUTOCOMPLETE_LIMIT_DEFAULT: 25,
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ const launchToHome = async (props: LaunchProps) => {
|
|||
openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local);
|
||||
if (openPushNotification) {
|
||||
await resetToHome(props);
|
||||
return pushNotificationEntry(props.serverUrl!, extra.payload!, 'notification');
|
||||
return pushNotificationEntry(props.serverUrl!, extra.payload!, 'Notification');
|
||||
}
|
||||
|
||||
appEntry(props.serverUrl!);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ class AppsManager {
|
|||
}
|
||||
};
|
||||
|
||||
fetchBindings = async (serverUrl: string, channelId: string, forThread = false, groupLabel?: string) => {
|
||||
fetchBindings = async (serverUrl: string, channelId: string, forThread = false, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const userId = await getCurrentUserId(database);
|
||||
|
|
@ -135,13 +135,14 @@ class AppsManager {
|
|||
}
|
||||
};
|
||||
|
||||
refreshAppBindings = async (serverUrl: string, groupLabel?: string) => {
|
||||
refreshAppBindings = async (serverUrl: string, groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const appsEnabled = (await getConfig(database))?.FeatureFlagAppsEnabled === 'true';
|
||||
const appsEnabled = await this.isAppsEnabled(serverUrl);
|
||||
if (!appsEnabled) {
|
||||
this.getEnabledSubject(serverUrl).next(false);
|
||||
this.clearServer(serverUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const channelId = await getCurrentChannelId(database);
|
||||
|
|
|
|||
13
app/managers/performance_metrics_manager/constant.ts
Normal file
13
app/managers/performance_metrics_manager/constant.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
export enum NetworkRequestMetrics {
|
||||
AverageSpeed = 'mobile_network_requests_average_speed',
|
||||
EffectiveLatency = 'mobile_network_requests_effective_latency',
|
||||
ElapsedTime = 'mobile_network_requests_elapsed_time',
|
||||
Latency = 'mobile_network_requests_latency',
|
||||
TotalCompressedSize = 'mobile_network_requests_total_compressed_size',
|
||||
TotalParallelRequests = 'mobile_network_requests_total_parallel_requests',
|
||||
TotalRequests = 'mobile_network_requests_total_requests',
|
||||
TotalSequentialRequests = 'mobile_network_requests_total_sequential_requests',
|
||||
TotalSize = 'mobile_network_requests_total_size',
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import RNUtils from '@mattermost/rnutils';
|
||||
import {AppState} from 'react-native';
|
||||
import performance from 'react-native-performance';
|
||||
|
||||
import {mockApiClient} from '@test/mock_api_client';
|
||||
|
|
@ -10,6 +11,7 @@ import {logWarning} from '@utils/log';
|
|||
|
||||
import NetworkManager from '../network_manager';
|
||||
|
||||
import {NetworkRequestMetrics} from './constant';
|
||||
import {testExports as batcherTestExports} from './performance_metrics_batcher';
|
||||
import {getBaseReportRequest} from './test_utils';
|
||||
|
||||
|
|
@ -33,263 +35,348 @@ jest.mock('@utils/log', () => ({
|
|||
|
||||
performance.timeOrigin = TEST_EPOCH;
|
||||
|
||||
describe('load metrics', () => {
|
||||
describe('performance_metrics_manager', () => {
|
||||
const serverUrl = 'http://www.someserverurl.com/';
|
||||
const expectedUrl = `${serverUrl}/api/v4/client_perf`;
|
||||
let PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
|
||||
const getMeasure = (timestamp: number, value: number): PerformanceReportMeasure => {
|
||||
return {
|
||||
metric: 'mobile_load',
|
||||
timestamp,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers({doNotFake: [
|
||||
'cancelAnimationFrame',
|
||||
'cancelIdleCallback',
|
||||
'clearImmediate',
|
||||
'clearInterval',
|
||||
'clearTimeout',
|
||||
'hrtime',
|
||||
'nextTick',
|
||||
'queueMicrotask',
|
||||
'requestAnimationFrame',
|
||||
'requestIdleCallback',
|
||||
'setImmediate',
|
||||
'setInterval',
|
||||
]}).setSystemTime(new Date(TEST_EPOCH));
|
||||
|
||||
NetworkManager.createClient(serverUrl);
|
||||
const {operator} = await TestHelper.setupServerDatabase(serverUrl);
|
||||
await operator.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
|
||||
jest.useFakeTimers({doNotFake: [
|
||||
'cancelAnimationFrame',
|
||||
'cancelIdleCallback',
|
||||
'clearImmediate',
|
||||
'clearInterval',
|
||||
'clearTimeout',
|
||||
'hrtime',
|
||||
'nextTick',
|
||||
'queueMicrotask',
|
||||
'requestAnimationFrame',
|
||||
'requestIdleCallback',
|
||||
'setImmediate',
|
||||
'setInterval',
|
||||
]}).setSystemTime(new Date(TEST_EPOCH));
|
||||
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
|
||||
const mockHasRegisteredLoad = {hasRegisteredLoad: false};
|
||||
jest.mocked(RNUtils.setHasRegisteredLoad).mockImplementation(() => {
|
||||
mockHasRegisteredLoad.hasRegisteredLoad = true;
|
||||
});
|
||||
jest.mocked(RNUtils.getHasRegisteredLoad).mockImplementation(() => mockHasRegisteredLoad);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.useRealTimers();
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
await TestHelper.tearDown();
|
||||
performance.clearMarks();
|
||||
});
|
||||
|
||||
it('only load on target', async () => {
|
||||
performance.mark('nativeLaunchStart');
|
||||
const measure = getMeasure(TEST_EPOCH + INTERVAL_TIME, INTERVAL_TIME);
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
jest.clearAllMocks();
|
||||
|
||||
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
|
||||
});
|
||||
|
||||
it('only register load once', async () => {
|
||||
performance.mark('nativeLaunchStart');
|
||||
const measure = getMeasure(TEST_EPOCH, 0);
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
|
||||
PerformanceMetricsManager.setLoadTarget('HOME');
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
|
||||
mockApiClient.post.mockClear();
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retry if the mark is not yet present', async () => {
|
||||
const measure = getMeasure(TEST_EPOCH + (RETRY_TIME * 2), RETRY_TIME);
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
|
||||
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
||||
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(RETRY_TIME);
|
||||
await TestHelper.tick();
|
||||
performance.mark('nativeLaunchStart');
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(RETRY_TIME);
|
||||
await TestHelper.tick();
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
|
||||
});
|
||||
|
||||
it('fail graciously if no measure is set', async () => {
|
||||
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
||||
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(MAX_RETRIES);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await TestHelper.tick();
|
||||
}
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
expect(logWarning).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('other metrics', () => {
|
||||
const serverUrl1 = 'http://www.someserverurl.com/';
|
||||
const expectedUrl1 = `${serverUrl1}/api/v4/client_perf`;
|
||||
|
||||
const serverUrl2 = 'http://www.otherserverurl.com/';
|
||||
const expectedUrl2 = `${serverUrl2}/api/v4/client_perf`;
|
||||
|
||||
const measure1: PerformanceReportMeasure = {
|
||||
metric: 'mobile_channel_switch',
|
||||
timestamp: TEST_EPOCH + 100,
|
||||
value: 100,
|
||||
};
|
||||
|
||||
const measure2: PerformanceReportMeasure = {
|
||||
metric: 'mobile_team_switch',
|
||||
timestamp: TEST_EPOCH + 150 + 50,
|
||||
value: 150,
|
||||
};
|
||||
|
||||
const PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
|
||||
beforeEach(async () => {
|
||||
NetworkManager.createClient(serverUrl1);
|
||||
NetworkManager.createClient(serverUrl2);
|
||||
const {operator: operator1} = await TestHelper.setupServerDatabase(serverUrl1);
|
||||
const {operator: operator2} = await TestHelper.setupServerDatabase(serverUrl2);
|
||||
await operator1.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
|
||||
await operator2.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
|
||||
jest.useFakeTimers({doNotFake: [
|
||||
'cancelAnimationFrame',
|
||||
'cancelIdleCallback',
|
||||
'clearImmediate',
|
||||
'clearInterval',
|
||||
'clearTimeout',
|
||||
'hrtime',
|
||||
'nextTick',
|
||||
'queueMicrotask',
|
||||
'requestAnimationFrame',
|
||||
'requestIdleCallback',
|
||||
'setImmediate',
|
||||
'setInterval',
|
||||
]}).setSystemTime(new Date(TEST_EPOCH));
|
||||
});
|
||||
afterEach(async () => {
|
||||
jest.useRealTimers();
|
||||
NetworkManager.invalidateClient(serverUrl1);
|
||||
NetworkManager.invalidateClient(serverUrl2);
|
||||
await TestHelper.tearDown();
|
||||
performance.clearMarks();
|
||||
});
|
||||
describe('load metrics', () => {
|
||||
const getMeasure = (timestamp: number, value: number): PerformanceReportMeasure => {
|
||||
return {
|
||||
metric: 'mobile_load',
|
||||
timestamp,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
|
||||
const mockHasRegisteredLoad = {hasRegisteredLoad: false};
|
||||
jest.mocked(RNUtils.setHasRegisteredLoad).mockImplementation(() => {
|
||||
mockHasRegisteredLoad.hasRegisteredLoad = true;
|
||||
});
|
||||
jest.mocked(RNUtils.getHasRegisteredLoad).mockImplementation(() => mockHasRegisteredLoad);
|
||||
});
|
||||
afterEach(async () => {
|
||||
performance.clearMarks();
|
||||
});
|
||||
|
||||
it('only load on target', async () => {
|
||||
performance.mark('nativeLaunchStart');
|
||||
const measure = getMeasure(TEST_EPOCH + INTERVAL_TIME, INTERVAL_TIME);
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
|
||||
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
|
||||
});
|
||||
|
||||
it('only register load once', async () => {
|
||||
performance.mark('nativeLaunchStart');
|
||||
const measure = getMeasure(TEST_EPOCH, 0);
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
|
||||
PerformanceMetricsManager.setLoadTarget('HOME');
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
|
||||
mockApiClient.post.mockClear();
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('retry if the mark is not yet present', async () => {
|
||||
const measure = getMeasure(TEST_EPOCH + (RETRY_TIME * 2), RETRY_TIME);
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
|
||||
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
||||
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(RETRY_TIME);
|
||||
await TestHelper.tick();
|
||||
performance.mark('nativeLaunchStart');
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(RETRY_TIME);
|
||||
await TestHelper.tick();
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl, expectedRequest);
|
||||
});
|
||||
|
||||
it('fail graciously if no measure is set', async () => {
|
||||
PerformanceMetricsManager.setLoadTarget('CHANNEL');
|
||||
PerformanceMetricsManager.finishLoad('CHANNEL', serverUrl);
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(MAX_RETRIES);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await TestHelper.tick();
|
||||
}
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
expect(logWarning).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('do not send metrics when we do not start them', async () => {
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
describe('app state changes', () => {
|
||||
const getMeasure = (timestamp: number, value: number): PerformanceReportMeasure => {
|
||||
return {
|
||||
metric: 'mobile_channel_switch',
|
||||
timestamp,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
beforeEach(async () => {
|
||||
AppState.currentState = 'active';
|
||||
|
||||
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
});
|
||||
|
||||
it('forces send on app state change to inactive, or anything other than active', async () => {
|
||||
const appStateSpy = jest.spyOn(AppState, 'addEventListener');
|
||||
|
||||
const measure = getMeasure(TEST_EPOCH, 0);
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl);
|
||||
|
||||
appStateSpy.mock.calls[0][1]('inactive');
|
||||
|
||||
await TestHelper.tick();
|
||||
|
||||
const expectedRequest = getBaseReportRequest(measure.timestamp, measure.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure];
|
||||
expect(mockApiClient.post).toHaveBeenCalled();
|
||||
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(`${serverUrl}/api/v4/client_perf`, expectedRequest);
|
||||
});
|
||||
});
|
||||
|
||||
it('send metric after it has been started', async () => {
|
||||
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure1];
|
||||
describe('time to interaction', () => {
|
||||
beforeEach(() => {
|
||||
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
});
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(100);
|
||||
it('measures time to interaction correctly', () => {
|
||||
PerformanceMetricsManager.startTimeToInteraction();
|
||||
jest.advanceTimersByTime(100);
|
||||
const result = PerformanceMetricsManager.measureTimeToInteraction();
|
||||
expect(result?.duration).toBe(100);
|
||||
});
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
|
||||
it('handles missing TTI mark gracefully', () => {
|
||||
const result = PerformanceMetricsManager.measureTimeToInteraction();
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('a second end metric does not generate a second measure', async () => {
|
||||
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure1];
|
||||
describe('network request metrics', () => {
|
||||
beforeEach(async () => {
|
||||
PerformanceMetricsManager = new PerformanceMetricsManagerClass();
|
||||
});
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(100);
|
||||
afterEach(async () => {
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
await TestHelper.tearDown();
|
||||
});
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(100);
|
||||
it('collects network request metrics', async () => {
|
||||
const timestamp = TEST_EPOCH;
|
||||
jest.setSystemTime(new Date(timestamp));
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
PerformanceMetricsManager.collectNetworkRequestData(
|
||||
NetworkRequestMetrics.ElapsedTime,
|
||||
100,
|
||||
{serverUrl, groupLabel: 'Login'},
|
||||
);
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
|
||||
const expectedRequest = getBaseReportRequest(timestamp, timestamp + 1);
|
||||
expectedRequest.body.histograms = [{
|
||||
metric: NetworkRequestMetrics.ElapsedTime,
|
||||
value: 100,
|
||||
timestamp,
|
||||
label: {network_request_group: 'Login'},
|
||||
}];
|
||||
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(
|
||||
`${serverUrl}/api/v4/client_perf`,
|
||||
expectedRequest,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('different metrics do not interfere', async () => {
|
||||
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure2.timestamp);
|
||||
expectedRequest.body.histograms = [measure1, measure2];
|
||||
describe('other metrics', () => {
|
||||
const serverUrl1 = 'http://www.someserverurl.com/';
|
||||
const expectedUrl1 = `${serverUrl1}/api/v4/client_perf`;
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
PerformanceMetricsManager.startMetric('mobile_team_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
const serverUrl2 = 'http://www.otherserverurl.com/';
|
||||
const expectedUrl2 = `${serverUrl2}/api/v4/client_perf`;
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(100);
|
||||
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
const measure1: PerformanceReportMeasure = {
|
||||
metric: 'mobile_channel_switch',
|
||||
timestamp: TEST_EPOCH + 100,
|
||||
value: 100,
|
||||
};
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
|
||||
});
|
||||
const measure2: PerformanceReportMeasure = {
|
||||
metric: 'mobile_team_switch',
|
||||
timestamp: TEST_EPOCH + 150 + 50,
|
||||
value: 150,
|
||||
};
|
||||
|
||||
it('metrics to different servers do not interfere', async () => {
|
||||
const expectedRequest1 = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
|
||||
expectedRequest1.body.histograms = [measure1];
|
||||
beforeEach(async () => {
|
||||
NetworkManager.createClient(serverUrl1);
|
||||
NetworkManager.createClient(serverUrl2);
|
||||
const {operator: operator1} = await TestHelper.setupServerDatabase(serverUrl1);
|
||||
const {operator: operator2} = await TestHelper.setupServerDatabase(serverUrl2);
|
||||
await operator1.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
|
||||
await operator2.handleConfigs({configs: [{id: 'EnableClientMetrics', value: 'true'}], configsToDelete: [], prepareRecordsOnly: false});
|
||||
});
|
||||
afterEach(async () => {
|
||||
NetworkManager.invalidateClient(serverUrl1);
|
||||
NetworkManager.invalidateClient(serverUrl2);
|
||||
|
||||
const expectedRequest2 = getBaseReportRequest(measure2.timestamp, measure2.timestamp + 1);
|
||||
expectedRequest2.body.histograms = [measure2];
|
||||
performance.clearMarks();
|
||||
});
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
PerformanceMetricsManager.startMetric('mobile_team_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
it('do not send metrics when we do not start them', async () => {
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(100);
|
||||
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl2);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest1);
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl2, expectedRequest2);
|
||||
it('send metric after it has been started', async () => {
|
||||
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure1];
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
|
||||
});
|
||||
|
||||
it('a second end metric does not generate a second measure', async () => {
|
||||
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
|
||||
expectedRequest.body.histograms = [measure1];
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(100);
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
|
||||
});
|
||||
|
||||
it('different metrics do not interfere', async () => {
|
||||
const expectedRequest = getBaseReportRequest(measure1.timestamp, measure2.timestamp);
|
||||
expectedRequest.body.histograms = [measure1, measure2];
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
PerformanceMetricsManager.startMetric('mobile_team_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(100);
|
||||
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest);
|
||||
});
|
||||
|
||||
it('metrics to different servers do not interfere', async () => {
|
||||
const expectedRequest1 = getBaseReportRequest(measure1.timestamp, measure1.timestamp + 1);
|
||||
expectedRequest1.body.histograms = [measure1];
|
||||
|
||||
const expectedRequest2 = getBaseReportRequest(measure2.timestamp, measure2.timestamp + 1);
|
||||
expectedRequest2.body.histograms = [measure2];
|
||||
|
||||
PerformanceMetricsManager.startMetric('mobile_channel_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
PerformanceMetricsManager.startMetric('mobile_team_switch');
|
||||
jest.advanceTimersByTime(50);
|
||||
|
||||
PerformanceMetricsManager.endMetric('mobile_channel_switch', serverUrl1);
|
||||
await TestHelper.tick();
|
||||
jest.advanceTimersByTime(100);
|
||||
PerformanceMetricsManager.endMetric('mobile_team_switch', serverUrl2);
|
||||
await TestHelper.tick();
|
||||
|
||||
jest.advanceTimersByTime(INTERVAL_TIME);
|
||||
await TestHelper.tick();
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl1, expectedRequest1);
|
||||
expect(mockApiClient.post).toHaveBeenCalledWith(expectedUrl2, expectedRequest2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,11 +5,15 @@ import RNUtils from '@mattermost/rnutils';
|
|||
import {AppState, type AppStateStatus} from 'react-native';
|
||||
import performance from 'react-native-performance';
|
||||
|
||||
import {logWarning} from '@utils/log';
|
||||
import {logDebug, logWarning} from '@utils/log';
|
||||
|
||||
import Batcher from './performance_metrics_batcher';
|
||||
|
||||
import type {NetworkRequestMetrics} from './constant';
|
||||
import type {MarkOptions} from 'react-native-performance/lib/typescript/performance';
|
||||
|
||||
type Target = 'HOME' | 'CHANNEL' | 'THREAD' | undefined;
|
||||
|
||||
type MetricName = 'mobile_channel_switch' |
|
||||
'mobile_team_switch';
|
||||
|
||||
|
|
@ -22,7 +26,7 @@ class PerformanceMetricsManager {
|
|||
private lastAppStateIsActive = AppState.currentState === 'active';
|
||||
|
||||
constructor() {
|
||||
AppState.addEventListener('change', (appState) => this.onAppStateChange(appState));
|
||||
AppState.addEventListener('change', (appState: AppStateStatus) => this.onAppStateChange(appState));
|
||||
}
|
||||
|
||||
private onAppStateChange(appState: AppStateStatus) {
|
||||
|
|
@ -104,6 +108,31 @@ class PerformanceMetricsManager {
|
|||
performance.clearMarks(metricName);
|
||||
performance.clearMeasures(measureName);
|
||||
}
|
||||
|
||||
public startTimeToInteraction(options?: MarkOptions) {
|
||||
performance.mark('tti', options);
|
||||
}
|
||||
|
||||
public measureTimeToInteraction() {
|
||||
try {
|
||||
const result = performance.measure('TTI', 'tti');
|
||||
performance.clearMarks('tti');
|
||||
performance.clearMeasures('TTI');
|
||||
logDebug('Time to Interaction', result.duration);
|
||||
return result;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public collectNetworkRequestData = (name: NetworkRequestMetrics, value: number, {serverUrl, groupLabel}: NetworkRequestDataOtherInfo) => {
|
||||
this.ensureBatcher(serverUrl).addToBatch({
|
||||
metric: name,
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
label: {network_request_group: groupLabel},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const testExports = {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,16 @@ class Batcher {
|
|||
this.serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
private async fetchClientPerformanceSetting() {
|
||||
const database = DatabaseManager.serverDatabases[this.serverUrl]?.database;
|
||||
if (!database) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const value = await getConfigValue(database, 'EnableClientMetrics');
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
private started() {
|
||||
return Boolean(this.sendTimeout);
|
||||
}
|
||||
|
|
@ -46,13 +56,7 @@ class Batcher {
|
|||
// Empty the batch as soon as possible to avoid race conditions
|
||||
this.batch = [];
|
||||
|
||||
const database = DatabaseManager.serverDatabases[this.serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientPerformanceSetting = await getConfigValue(database, 'EnableClientMetrics');
|
||||
if (clientPerformanceSetting !== 'true') {
|
||||
if (!await this.fetchClientPerformanceSetting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ type PerformanceReportMeasure = {
|
|||
metric: string;
|
||||
value: number;
|
||||
timestamp: number;
|
||||
label?: Record<string, string>;
|
||||
}
|
||||
|
||||
type PerformanceReport = {
|
||||
|
|
@ -21,3 +22,8 @@ type PerformanceReport = {
|
|||
counters: PerformanceReportMeasure[];
|
||||
histograms: PerformanceReportMeasure[];
|
||||
}
|
||||
|
||||
type NetworkRequestDataOtherInfo = {
|
||||
serverUrl: string;
|
||||
groupLabel: RequestGroupLabel;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class WebsocketManager {
|
|||
}
|
||||
};
|
||||
|
||||
public openAll = async (groupLabel?: string) => {
|
||||
public openAll = async (groupLabel?: BaseRequestGroupLabel) => {
|
||||
let queued = 0;
|
||||
for await (const clientUrl of Object.keys(this.clients)) {
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
|
|
@ -148,7 +148,7 @@ class WebsocketManager {
|
|||
}
|
||||
};
|
||||
|
||||
public initializeClient = async (serverUrl: string, groupLabel = 'reconnection') => {
|
||||
public initializeClient = async (serverUrl: string, groupLabel: BaseRequestGroupLabel = 'WebSocket Reconnect') => {
|
||||
const client: WebSocketClient = this.clients[serverUrl];
|
||||
clearTimeout(this.connectionTimerIDs[serverUrl]);
|
||||
delete this.connectionTimerIDs[serverUrl];
|
||||
|
|
@ -271,7 +271,7 @@ class WebsocketManager {
|
|||
}
|
||||
this.isBackgroundTimerRunning = false;
|
||||
if (this.netConnected) {
|
||||
this.openAll('reconnection');
|
||||
this.openAll('WebSocket Reconnect');
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -390,9 +390,9 @@ describe('Actions.Calls', () => {
|
|||
const {result} = renderHook(() => useCallsConfig('server1'));
|
||||
|
||||
await act(async () => {
|
||||
await CallsActions.loadConfig('server1', false, 'calls');
|
||||
await CallsActions.loadConfig('server1', false, 'Server Switch');
|
||||
});
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledWith('calls');
|
||||
expect(mockClient.getCallsConfig).toHaveBeenCalledWith('Server Switch');
|
||||
assert.equal(result.current.DefaultEnabled, true);
|
||||
assert.equal(result.current.AllowEnableCalls, true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ import type {IntlShape} from 'react-intl';
|
|||
let connection: CallsConnection | null = null;
|
||||
export const getConnectionForTesting = () => connection;
|
||||
|
||||
export const loadConfig = async (serverUrl: string, force = false, groupLabel?: string) => {
|
||||
export const loadConfig = async (serverUrl: string, force = false, groupLabel?: RequestGroupLabel) => {
|
||||
const now = Date.now();
|
||||
const config = getCallsConfig(serverUrl);
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ export const loadConfig = async (serverUrl: string, force = false, groupLabel?:
|
|||
}
|
||||
};
|
||||
|
||||
export const loadCalls = async (serverUrl: string, userId: string, groupLabel?: string) => {
|
||||
export const loadCalls = async (serverUrl: string, userId: string, groupLabel?: RequestGroupLabel) => {
|
||||
let resp: CallChannelState[] = [];
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
|
|
@ -192,7 +192,7 @@ export const createCallAndAddToIds = (channelId: string, call: CallState, ids?:
|
|||
return convertedCall;
|
||||
};
|
||||
|
||||
export const loadConfigAndCalls = async (serverUrl: string, userId: string, groupLabel?: string) => {
|
||||
export const loadConfigAndCalls = async (serverUrl: string, userId: string, groupLabel?: RequestGroupLabel) => {
|
||||
const res = await checkIsCallsPluginEnabled(serverUrl);
|
||||
if (res.data) {
|
||||
loadConfig(serverUrl, true, groupLabel);
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import type {RTCIceServer} from 'react-native-webrtc';
|
|||
|
||||
export interface ClientCallsMix {
|
||||
getEnabled: () => Promise<Boolean>;
|
||||
getCalls: (groupLabel?: string) => Promise<CallChannelState[]>;
|
||||
getCalls: (groupLabel?: RequestGroupLabel) => Promise<CallChannelState[]>;
|
||||
getCallForChannel: (channelId: string) => Promise<CallChannelState>;
|
||||
getCallsConfig: (groupLabel?: string) => Promise<CallsConfig>;
|
||||
getVersion: (groupLabel?: string) => Promise<CallsVersion>;
|
||||
getCallsConfig: (groupLabel?: RequestGroupLabel) => Promise<CallsConfig>;
|
||||
getVersion: (groupLabel?: RequestGroupLabel) => Promise<CallsVersion>;
|
||||
enableChannelCalls: (channelId: string, enable: boolean) => Promise<CallChannelState>;
|
||||
endCall: (channelId: string) => Promise<ApiResp>;
|
||||
genTURNCredentials: () => Promise<RTCIceServer[]>;
|
||||
|
|
@ -38,7 +38,7 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
|||
}
|
||||
};
|
||||
|
||||
getCalls = async (groupLabel?: string) => {
|
||||
getCalls = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/channels?mobilev2=true`,
|
||||
{method: 'get', groupLabel},
|
||||
|
|
@ -52,14 +52,14 @@ const ClientCalls = (superclass: any) => class extends superclass {
|
|||
);
|
||||
};
|
||||
|
||||
getCallsConfig = async (groupLabel?: string) => {
|
||||
getCallsConfig = async (groupLabel?: RequestGroupLabel) => {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/config`,
|
||||
{method: 'get', groupLabel},
|
||||
) as CallsConfig;
|
||||
};
|
||||
|
||||
getVersion = async (groupLabel?: string) => {
|
||||
getVersion = async (groupLabel?: RequestGroupLabel) => {
|
||||
try {
|
||||
return this.doFetch(
|
||||
`${this.getCallsRoute()}/version`,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ export const CallNotification = ({
|
|||
const onContainerPress = useCallback(async () => {
|
||||
if (incomingCall.serverUrl !== serverUrl) {
|
||||
await DatabaseManager.setActiveServerDatabase(incomingCall.serverUrl);
|
||||
await WebsocketManager.initializeClient(incomingCall.serverUrl, 'calls');
|
||||
await WebsocketManager.initializeClient(incomingCall.serverUrl, 'Server Switch');
|
||||
}
|
||||
switchToChannelById(incomingCall.serverUrl, incomingCall.channelID);
|
||||
}, [incomingCall, serverUrl]);
|
||||
|
|
|
|||
|
|
@ -466,7 +466,7 @@ const CallScreen = ({
|
|||
await popTopScreen(Screens.THREAD);
|
||||
}
|
||||
await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl);
|
||||
WebsocketManager.initializeClient(currentCall.serverUrl, 'calls');
|
||||
WebsocketManager.initializeClient(currentCall.serverUrl, 'Server Switch');
|
||||
await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId});
|
||||
}, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]);
|
||||
|
||||
|
|
|
|||
|
|
@ -276,7 +276,9 @@ describe('prepareMyChannelsForTeam', () => {
|
|||
(operator.handleMyChannel as jest.Mock).mockReturnValue(myChannelRecords);
|
||||
(operator.handleMyChannelSettings as jest.Mock).mockReturnValue(myChannelSettingsRecords);
|
||||
|
||||
query = jest.fn().mockResolvedValueOnce(Object.entries(allChannelsForTeam).map((c) => c[1])).mockResolvedValueOnce(Object.entries(allChannelsInfoForTeam).map((i) => i[1]));
|
||||
const result = await prepareMyChannelsForTeam(operator, teamId, channels, channelMembers, true);
|
||||
query = jest.fn();
|
||||
|
||||
expect(result).toEqual([channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords]);
|
||||
expect(operator.handleChannel).toHaveBeenCalledWith({channels, prepareRecordsOnly: true});
|
||||
|
|
@ -967,7 +969,7 @@ describe('Channel Functions', () => {
|
|||
Q.where('team_id', teamId),
|
||||
Q.where('delete_at', Q.eq(0)),
|
||||
Q.where('type', General.OPEN_CHANNEL),
|
||||
Q.where('channel_id', Q.notEq('ignoreid')),
|
||||
Q.where('id', Q.notEq('ignoreid')),
|
||||
),
|
||||
Q.sortBy('display_name', Q.asc),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -81,37 +81,30 @@ export function prepareMissingChannelsForAllTeams(operator: ServerDataOperator,
|
|||
return prepareChannels(operator, channels, channelInfos, memberships, memberships, isCRTEnabled);
|
||||
}
|
||||
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean) => {
|
||||
const {database} = operator;
|
||||
|
||||
const channelsQuery = queryAllChannelsForTeam(database, teamId);
|
||||
const allChannelsForTeam = (await channelsQuery.fetch()).
|
||||
reduce((map: Record<string, ChannelModel>, channel) => {
|
||||
map[channel.id] = channel;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const channelInfosQuery = queryAllChannelsInfoForTeam(database, teamId);
|
||||
const allChannelsInfoForTeam = (await channelInfosQuery.fetch()).
|
||||
reduce((map: Record<string, ChannelInfoModel>, info) => {
|
||||
map[info.id] = info;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const buildChannelInfos = async (database: Database, channels: Channel[]) => {
|
||||
const channelInfos: ChannelInfo[] = [];
|
||||
const memberships = channelMembers.map((cm) => {
|
||||
return {...cm, id: cm.channel_id};
|
||||
});
|
||||
|
||||
const channelsQuery = await queryAllChannels(database);
|
||||
const storedChannelsMap = channelsQuery.reduce<Record<string, ChannelModel>>((map, channel) => {
|
||||
map[channel.id] = channel;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const channelInfosQuery = await queryAllChannelsInfo(database);
|
||||
const storedChannelInfosMap = channelInfosQuery.reduce<Record<string, ChannelInfoModel>>((map, info) => {
|
||||
map[info.id] = info;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
for (const c of channels) {
|
||||
const storedChannel = allChannelsForTeam[c.id];
|
||||
const storedChannel = storedChannelsMap[c.id];
|
||||
let storedInfo: ChannelInfoModel | undefined;
|
||||
let member_count = 0;
|
||||
let guest_count = 0;
|
||||
let pinned_post_count = 0;
|
||||
let files_count = 0;
|
||||
if (storedChannel) {
|
||||
storedInfo = allChannelsInfoForTeam[c.id];
|
||||
storedInfo = storedChannelInfosMap[c.id];
|
||||
if (storedInfo) {
|
||||
member_count = storedInfo.memberCount;
|
||||
guest_count = storedInfo.guestCount;
|
||||
|
|
@ -131,6 +124,28 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea
|
|||
});
|
||||
}
|
||||
|
||||
return channelInfos;
|
||||
};
|
||||
|
||||
export const prepareAllMyChannels = async (operator: ServerDataOperator, channels: Channel[], channelMemberships: ChannelMembership[], isCRTEnabled?: boolean) => {
|
||||
const {database} = operator;
|
||||
|
||||
const fetchedChannelIds = new Set(channels.map((c) => c.id));
|
||||
const memberships = channelMemberships.filter((m) => fetchedChannelIds.has(m.channel_id)).map((m) => ({...m, id: m.channel_id}));
|
||||
|
||||
const channelInfos = await buildChannelInfos(database, channels);
|
||||
|
||||
return prepareChannels(operator, channels, channelInfos, channelMemberships, memberships, isCRTEnabled);
|
||||
};
|
||||
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[], isCRTEnabled?: boolean) => {
|
||||
const {database} = operator;
|
||||
|
||||
const channelInfos = await buildChannelInfos(database, channels);
|
||||
const memberships = channelMembers.map((cm) => {
|
||||
return {...cm, id: cm.channel_id};
|
||||
});
|
||||
|
||||
return prepareChannels(operator, channels, channelInfos, channelMembers, memberships, isCRTEnabled);
|
||||
};
|
||||
|
||||
|
|
@ -316,7 +331,7 @@ export const getDefaultChannelForTeam = async (database: Database, teamId: strin
|
|||
];
|
||||
|
||||
if (ignoreId) {
|
||||
clauses.push(Q.where('channel_id', Q.notEq(ignoreId)));
|
||||
clauses.push(Q.where('id', Q.notEq(ignoreId)));
|
||||
}
|
||||
|
||||
const myChannels = await database.get<ChannelModel>(CHANNEL).query(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import {MM_TABLES} from '@constants/database';
|
|||
import DatabaseManager from '@database/manager';
|
||||
|
||||
import {prepareCategoriesAndCategoriesChannels} from './categories';
|
||||
import {prepareDeleteChannel, prepareMyChannelsForTeam} from './channel';
|
||||
import {prepareAllMyChannels, prepareDeleteChannel, queryAllChannels, queryChannelsById} from './channel';
|
||||
import {prepareMyPreferences} from './preference';
|
||||
import {resetLastFullSync} from './system';
|
||||
import {prepareDeleteTeam, prepareMyTeams} from './team';
|
||||
import {prepareDeleteTeam, prepareMyTeams, queryMyTeams, queryTeamsById} from './team';
|
||||
import {prepareUsers} from './user';
|
||||
|
||||
import type {MyChannelsRequest} from '@actions/remote/channel';
|
||||
|
|
@ -18,13 +18,10 @@ import type {MyUserRequest} from '@actions/remote/user';
|
|||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type PrepareModelsArgs = {
|
||||
operator: ServerDataOperator;
|
||||
initialTeamId?: string;
|
||||
removeTeams?: TeamModel[];
|
||||
removeChannels?: ChannelModel[];
|
||||
teamData?: MyTeamsRequest;
|
||||
chData?: MyChannelsRequest;
|
||||
prefData?: MyPreferencesRequest;
|
||||
|
|
@ -32,6 +29,13 @@ type PrepareModelsArgs = {
|
|||
isCRTEnabled?: boolean;
|
||||
}
|
||||
|
||||
type PrepareModelsForDeletionArgs = {
|
||||
operator: ServerDataOperator;
|
||||
initialTeamId?: string;
|
||||
teamData?: MyTeamsRequest;
|
||||
chData?: MyChannelsRequest;
|
||||
}
|
||||
|
||||
const {
|
||||
CHANNEL_BOOKMARK,
|
||||
POST,
|
||||
|
|
@ -44,21 +48,48 @@ const {
|
|||
MY_CHANNEL,
|
||||
} = MM_TABLES.SERVER;
|
||||
|
||||
export async function prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData, isCRTEnabled}: PrepareModelsArgs): Promise<Array<Promise<Model[]>>> {
|
||||
export async function prepareEntryModelsForDeletion({operator, teamData, chData}: PrepareModelsForDeletionArgs): Promise<Array<Promise<Model[]>>> {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const {database} = operator;
|
||||
let teamIdsToDelete: Set<string>|undefined;
|
||||
|
||||
if (teamData?.teams?.length && teamData.memberships?.length) {
|
||||
const myTeams = await queryMyTeams(database).fetch();
|
||||
const joinedTeams = new Set(teamData.memberships?.filter((m) => m.delete_at === 0).map((m) => m.team_id));
|
||||
const myTeamsToDelete = myTeams.filter((m) => !joinedTeams.has(m.id));
|
||||
teamIdsToDelete = new Set(myTeamsToDelete.map((m) => m.id));
|
||||
const removeTeams = await queryTeamsById(database, Array.from(teamIdsToDelete)).fetch();
|
||||
|
||||
if (removeTeams?.length) {
|
||||
removeTeams.forEach((team) => {
|
||||
modelPromises.push(prepareDeleteTeam(team));
|
||||
});
|
||||
}
|
||||
|
||||
if (removeChannels?.length) {
|
||||
removeChannels.forEach((channel) => {
|
||||
modelPromises.push(prepareDeleteChannel(channel));
|
||||
});
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
const {channels} = chData;
|
||||
const fetchedChannelIds = new Set(channels.map((c) => c.id));
|
||||
const channelsQuery = await queryAllChannels(database);
|
||||
const storedChannelsMap = channelsQuery.reduce<Record<string, ChannelModel>>((map, channel) => {
|
||||
map[channel.id] = channel;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const removeChannelIds: string[] = Object.keys(storedChannelsMap).filter((id) => !fetchedChannelIds.has(id) && !teamIdsToDelete?.has(storedChannelsMap[id]?.teamId));
|
||||
let removeChannels: ChannelModel[]|undefined;
|
||||
if (removeChannelIds?.length) {
|
||||
removeChannels = await queryChannelsById(database, removeChannelIds).fetch();
|
||||
removeChannels.forEach((c) => {
|
||||
modelPromises.push(prepareDeleteChannel(c));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return modelPromises;
|
||||
}
|
||||
|
||||
export async function prepareEntryModels({operator, teamData, chData, prefData, meData, isCRTEnabled}: PrepareModelsArgs): Promise<Array<Promise<Model[]>>> {
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
|
||||
if (teamData?.teams?.length && teamData.memberships?.length) {
|
||||
modelPromises.push(...prepareMyTeams(operator, teamData.teams, teamData.memberships));
|
||||
}
|
||||
|
|
@ -68,9 +99,8 @@ export async function prepareModels({operator, initialTeamId, removeTeams, remov
|
|||
}
|
||||
|
||||
if (chData?.channels?.length && chData.memberships?.length) {
|
||||
if (initialTeamId) {
|
||||
modelPromises.push(...await prepareMyChannelsForTeam(operator, initialTeamId, chData.channels, chData.memberships, isCRTEnabled));
|
||||
}
|
||||
const {channels, memberships} = chData;
|
||||
modelPromises.push(...await prepareAllMyChannels(operator, channels, memberships, isCRTEnabled));
|
||||
}
|
||||
|
||||
if (prefData?.preferences?.length) {
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
}
|
||||
}
|
||||
return false;
|
||||
}, [intl]);
|
||||
}, [intl, navigation]);
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
if (!isFocused) {
|
||||
|
|
@ -136,7 +136,7 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
if (!props.hasTeams) {
|
||||
resetToTeams();
|
||||
}
|
||||
}, [Boolean(props.hasTeams)]);
|
||||
}, [props.hasTeams]);
|
||||
|
||||
useEffect(() => {
|
||||
const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||
|
|
@ -172,6 +172,7 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
PerformanceMetricsManager.finishLoad('HOME', serverUrl);
|
||||
PerformanceMetricsManager.measureTimeToInteraction();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
|
@ -201,7 +202,7 @@ const ChannelListScreen = (props: ChannelProps) => {
|
|||
moreThanOneTeam={props.hasMoreThanOneTeam}
|
||||
hasChannels={props.hasChannels}
|
||||
/>
|
||||
{isTablet &&
|
||||
{isTablet && props.hasChannels &&
|
||||
<AdditionalTabletView/>
|
||||
}
|
||||
{props.showIncomingCalls && !isTablet &&
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ const ServerItem = ({
|
|||
setBadge({isUnread, mentions});
|
||||
};
|
||||
|
||||
const logoutServer = async () => {
|
||||
const logoutServer = useCallback(async () => {
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
await logout(server.url);
|
||||
|
||||
|
|
@ -188,14 +188,14 @@ const ServerItem = ({
|
|||
} else {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
|
||||
}
|
||||
};
|
||||
}, [isActive, server.url]);
|
||||
|
||||
const removeServer = async () => {
|
||||
const removeServer = useCallback(async () => {
|
||||
const skipLogoutFromServer = server.lastActiveAt === 0;
|
||||
await dismissBottomSheet();
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
await logout(server.url, skipLogoutFromServer, true);
|
||||
};
|
||||
}, [server.lastActiveAt, server.url]);
|
||||
|
||||
const startTutorial = () => {
|
||||
viewRef.current?.measureInWindow((x, y, w, h) => {
|
||||
|
|
@ -222,7 +222,7 @@ const ServerItem = ({
|
|||
setShowTutorial(true);
|
||||
});
|
||||
}
|
||||
}, [showTutorial]);
|
||||
}, [highlight, isTablet, tutorialWatched]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (showTutorial && !tutorialShown.current) {
|
||||
|
|
@ -239,7 +239,7 @@ const ServerItem = ({
|
|||
}
|
||||
|
||||
return style;
|
||||
}, [isActive]);
|
||||
}, [isActive, styles.active, styles.container]);
|
||||
|
||||
const serverStyle = useMemo(() => {
|
||||
const style: StyleProp<ViewStyle> = [styles.row];
|
||||
|
|
@ -248,7 +248,7 @@ const ServerItem = ({
|
|||
}
|
||||
|
||||
return style;
|
||||
}, [server.lastActiveAt]);
|
||||
}, [server.lastActiveAt, styles.offline, styles.row]);
|
||||
|
||||
const handleLogin = useCallback(async () => {
|
||||
swipeable.current?.close();
|
||||
|
|
@ -275,7 +275,7 @@ const ServerItem = ({
|
|||
|
||||
canReceiveNotifications(server.url, result.canReceiveNotifications as string, intl);
|
||||
loginToServer(theme, server.url, displayName, data.config!, data.license!);
|
||||
}, [server, theme, intl]);
|
||||
}, [server.url, intl, theme, displayName]);
|
||||
|
||||
const handleDismissTutorial = useCallback(() => {
|
||||
swipeable.current?.close();
|
||||
|
|
@ -286,15 +286,15 @@ const ServerItem = ({
|
|||
const handleEdit = useCallback(() => {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
|
||||
editServer(theme, server);
|
||||
}, [server]);
|
||||
}, [server, theme]);
|
||||
|
||||
const handleLogout = useCallback(async () => {
|
||||
alertServerLogout(server.displayName, logoutServer, intl);
|
||||
}, [isActive, intl, server]);
|
||||
}, [server.displayName, logoutServer, intl]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
alertServerRemove(server.displayName, removeServer, intl);
|
||||
}, [isActive, server, intl]);
|
||||
}, [server.displayName, removeServer, intl]);
|
||||
|
||||
const handleShowTutorial = useCallback(() => {
|
||||
swipeable.current?.openRight();
|
||||
|
|
@ -311,12 +311,12 @@ const ServerItem = ({
|
|||
await dismissBottomSheet();
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
DatabaseManager.setActiveServerDatabase(server.url);
|
||||
WebsocketManager.initializeClient(server.url, 'entry');
|
||||
WebsocketManager.initializeClient(server.url, 'Server Switch');
|
||||
return;
|
||||
}
|
||||
|
||||
handleLogin();
|
||||
}, [server, isActive, theme, intl]);
|
||||
}, [isActive, server.lastActiveAt, server.url, handleLogin]);
|
||||
|
||||
const onSwipeableWillOpen = useCallback(() => {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, server.url);
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ describe('handleDeepLink', () => {
|
|||
const result = await handleDeepLink('https://existingserver.com/team/channels/town-square');
|
||||
expect(dismissAllModalsAndPopToRoot).toHaveBeenCalled();
|
||||
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('https://existingserver.com');
|
||||
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'deeplink');
|
||||
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'DeepLink');
|
||||
expect(result).toEqual({error: false});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export async function handleDeepLink(deepLinkUrl: string, intlShape?: IntlShape,
|
|||
if (existingServerUrl !== currentServerUrl && NavigationStore.getVisibleScreen()) {
|
||||
await dismissAllModalsAndPopToRoot();
|
||||
DatabaseManager.setActiveServerDatabase(existingServerUrl);
|
||||
WebsocketManager.initializeClient(existingServerUrl, 'deeplink');
|
||||
WebsocketManager.initializeClient(existingServerUrl, 'DeepLink');
|
||||
await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,13 @@ jest.mock('@utils/log', () => ({
|
|||
jest.mock('@mattermost/rnutils', () => ({
|
||||
getRealFilePath: jest.fn(),
|
||||
isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}),
|
||||
getConstants: jest.fn().mockReturnValue({
|
||||
appGroupIdentifier: 'group.mattermost.rnbeta',
|
||||
appGroupSharedDirectory: {
|
||||
sharedDirectory: '',
|
||||
databasePath: '',
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('FilePickerUtil', () => {
|
||||
|
|
|
|||
|
|
@ -181,6 +181,11 @@ lane :configure do
|
|||
json['ShowOnboarding'] = true
|
||||
end
|
||||
|
||||
# Configure Show Onboarding
|
||||
if ENV['COLLECT_NETWORK_METRICS'] == 'true'
|
||||
json['CollectNetworkMetrics'] = true
|
||||
end
|
||||
|
||||
# Configure the auth schemes
|
||||
auth_scheme = ENV['APP_AUTH_SCHEME'].to_s.empty? ? 'mmauth' : ENV['APP_AUTH_SCHEME']
|
||||
auth_scheme_dev = ENV['APP_AUTH_SCHEME_BETA'].to_s.empty? ? 'mmauthbeta' : ENV['APP_AUTH_SCHEME_BETA']
|
||||
|
|
|
|||
5
types/api/client.d.ts
vendored
5
types/api/client.d.ts
vendored
|
|
@ -3,13 +3,16 @@
|
|||
|
||||
type logLevel = 'ERROR' | 'WARNING' | 'INFO';
|
||||
|
||||
type BaseRequestGroupLabel = 'Login' | 'Cold Start' | 'Notification' | 'DeepLink' | 'WebSocket Reconnect' | 'Server Switch';
|
||||
type RequestGroupLabel = BaseRequestGroupLabel | `${BaseRequestGroupLabel} Deferred`;
|
||||
|
||||
type ClientOptions = {
|
||||
body?: any;
|
||||
method?: string;
|
||||
noRetry?: boolean;
|
||||
timeoutInterval?: number;
|
||||
headers?: Record<string, any>;
|
||||
groupLabel?: string;
|
||||
groupLabel?: RequestGroupLabel;
|
||||
};
|
||||
|
||||
type ClientErrorIntl =
|
||||
|
|
|
|||
Loading…
Reference in a new issue