From fb57c423c76961a9788b9298ced0b2cd6e0c1cd9 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 17 Dec 2024 19:51:26 +0800 Subject: [PATCH] Network metrics (#8390) * Network metrics * update network-client ref * fix unit tests * missing catch error parsing * add client tracking unit tests * fix typos --- app/actions/remote/channel.ts | 52 ++-- app/actions/remote/channel_bookmark.ts | 4 +- app/actions/remote/entry/app.ts | 2 +- app/actions/remote/entry/common.ts | 73 ++--- app/actions/remote/entry/login.ts | 2 +- app/actions/remote/entry/notification.ts | 16 +- app/actions/remote/groups.ts | 12 +- app/actions/remote/notifications.test.ts | 5 + app/actions/remote/post.ts | 45 +-- app/actions/remote/preference.ts | 20 +- app/actions/remote/role.ts | 14 +- app/actions/remote/systems.ts | 23 +- app/actions/remote/team.ts | 30 +- app/actions/remote/thread.ts | 37 ++- app/actions/remote/user.ts | 38 +-- app/actions/websocket/index.ts | 34 +-- app/client/rest/apps.ts | 6 +- app/client/rest/base.ts | 140 +-------- app/client/rest/categories.ts | 6 +- app/client/rest/channel_bookmark.ts | 6 +- app/client/rest/channels.ts | 36 +-- app/client/rest/general.ts | 42 +-- app/client/rest/groups.ts | 12 +- app/client/rest/posts.ts | 24 +- app/client/rest/preferences.ts | 12 +- app/client/rest/teams.ts | 30 +- app/client/rest/threads.ts | 15 +- app/client/rest/tracking.test.ts | 269 ++++++++++++++++++ app/client/rest/tracking.ts | 263 +++++++++++++++++ app/client/rest/users.ts | 55 ++-- app/helpers/database/index.ts | 4 + app/init/launch.ts | 2 +- app/managers/apps_manager.ts | 10 +- app/managers/network_manager.ts | 2 + app/managers/websocket_manager.ts | 12 +- app/products/calls/actions/calls.test.ts | 4 +- app/products/calls/actions/calls.ts | 16 +- app/products/calls/client/rest.ts | 18 +- .../call_notification/call_notification.tsx | 2 +- .../calls/screens/call_screen/call_screen.tsx | 2 +- .../servers_list/server_item/server_item.tsx | 2 +- app/utils/deep_link/index.test.ts | 2 +- app/utils/deep_link/index.ts | 2 +- assets/base/config.json | 3 +- package-lock.json | 6 +- package.json | 2 +- types/api/client.d.ts | 1 + 47 files changed, 946 insertions(+), 467 deletions(-) create mode 100644 app/client/rest/tracking.test.ts create mode 100644 app/client/rest/tracking.ts diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index e3e15f9f19c..76105f360aa 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -376,12 +376,12 @@ export async function fetchChannelCreator(serverUrl: string, channelId: string, } } -export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false) { +export async function fetchChannelStats(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) { try { const client = NetworkManager.getClient(serverUrl); const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const stats = await client.getChannelStats(channelId); + const stats = await client.getChannelStats(channelId, groupLabel); if (!fetchOnly) { const channel = await getChannelById(database, channelId); if (channel) { @@ -404,7 +404,11 @@ export async function fetchChannelStats(serverUrl: string, channelId: string, fe } } -export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, includeDeleted = true, since = 0, fetchOnly = false, excludeDirect = false, isCRTEnabled?: boolean): Promise { +export async function fetchMyChannelsForTeam( + serverUrl: string, teamId: string, includeDeleted = true, + since = 0, fetchOnly = false, excludeDirect = false, + isCRTEnabled?: boolean, groupLabel?: string, +): Promise { try { if (!fetchOnly) { setTeamLoading(serverUrl, true); @@ -412,9 +416,9 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, const client = NetworkManager.getClient(serverUrl); const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const [allChannels, channelMemberships, categoriesWithOrder] = await Promise.all([ - client.getMyChannels(teamId, includeDeleted, since), - client.getMyChannelMembers(teamId), - client.getCategories('me', teamId), + client.getMyChannels(teamId, includeDeleted, since, groupLabel), + client.getMyChannelMembers(teamId, groupLabel), + client.getCategories('me', teamId, groupLabel), ]); let channels = allChannels; @@ -453,13 +457,13 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string, } } -export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false): Promise { +export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise { try { const client = NetworkManager.getClient(serverUrl); const [channel, member] = await Promise.all([ - client.getChannel(channelId), - client.getChannelMember(channelId, 'me'), + client.getChannel(channelId, groupLabel), + client.getChannelMember(channelId, 'me', groupLabel), ]); if (!fetchOnly) { @@ -477,7 +481,11 @@ 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) { +export async function fetchMissingDirectChannelsInfo( + serverUrl: string, directChannels: Channel[], locale?: string, + teammateDisplayNameSetting?: string, currentUserId?: string, + fetchOnly = false, groupLabel?: string, +) { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const displayNameByChannel: Record = {}; @@ -510,8 +518,8 @@ export async function fetchMissingDirectChannelsInfo(serverUrl: string, directCh const membersCount = await getMembersCountByChannelsId(database, dmIds); const profileChannelsToFetch = dmIds.filter((id) => membersCount[id] <= 1 && dmWithoutDisplayName.has(id)); const results = await Promise.all([ - profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false) : Promise.resolve({data: undefined}), - fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false), + profileChannelsToFetch.length ? fetchProfilesPerChannels(serverUrl, profileChannelsToFetch, currentUserId, false, groupLabel) : Promise.resolve({data: undefined}), + fetchProfilesInGroupChannels(serverUrl, gms.map((c) => c.id), false, groupLabel), ]); const profileRequests = results.flat(); @@ -654,10 +662,10 @@ export async function joinChannelIfNeeded(serverUrl: string, channelId: string) } } -export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false) { +export async function markChannelAsRead(serverUrl: string, channelId: string, updateLocal = false, groupLabel?: string) { try { const client = NetworkManager.getClient(serverUrl); - await client.viewMyChannel(channelId); + await client.viewMyChannel(channelId, undefined, groupLabel); if (updateLocal) { await markChannelAsViewed(serverUrl, channelId, true); @@ -1039,7 +1047,7 @@ export async function getChannelTimezones(serverUrl: string, channelId: string) } } -export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false) { +export async function switchToChannelById(serverUrl: string, channelId: string, teamId?: string, skipLastUnread = false, groupLabel?: string) { if (channelId === Screens.GLOBAL_THREADS) { return switchToGlobalThreads(serverUrl, teamId); } @@ -1051,18 +1059,18 @@ export async function switchToChannelById(serverUrl: string, channelId: string, DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, true); - fetchPostsForChannel(serverUrl, channelId); - fetchChannelBookmarks(serverUrl, channelId); + fetchPostsForChannel(serverUrl, channelId, false, groupLabel); + fetchChannelBookmarks(serverUrl, channelId, false, groupLabel); await switchToChannel(serverUrl, channelId, teamId, skipLastUnread); - openChannelIfNeeded(serverUrl, channelId); - markChannelAsRead(serverUrl, channelId); - fetchChannelStats(serverUrl, channelId); - fetchGroupsForChannelIfConstrained(serverUrl, channelId); + openChannelIfNeeded(serverUrl, channelId, groupLabel); + markChannelAsRead(serverUrl, channelId, false, groupLabel); + fetchChannelStats(serverUrl, channelId, false, groupLabel); + fetchGroupsForChannelIfConstrained(serverUrl, channelId, false, groupLabel); DeviceEventEmitter.emit(Events.CHANNEL_SWITCH, false); if (await AppsManager.isAppsEnabled(serverUrl)) { - AppsManager.fetchBindings(serverUrl, channelId); + AppsManager.fetchBindings(serverUrl, channelId, false, groupLabel); } return {}; diff --git a/app/actions/remote/channel_bookmark.ts b/app/actions/remote/channel_bookmark.ts index 825295ad5b8..b9640e60377 100644 --- a/app/actions/remote/channel_bookmark.ts +++ b/app/actions/remote/channel_bookmark.ts @@ -11,7 +11,7 @@ import {logError} from '@utils/log'; import {forceLogoutIfNecessary} from './session'; -export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false) { +export async function fetchChannelBookmarks(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) { try { const client = NetworkManager.getClient(serverUrl); const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -22,7 +22,7 @@ export async function fetchChannelBookmarks(serverUrl: string, channelId: string } const since = await getBookmarksSince(database, channelId); - const bookmarks = await client.getChannelBookmarksForChannel(channelId, since); + const bookmarks = await client.getChannelBookmarksForChannel(channelId, since, groupLabel); if (!fetchOnly && bookmarks.length) { await operator.handleChannelBookmark({bookmarks, prepareRecordsOnly: false}); diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index 232665e180b..c79a2236e9f 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -28,7 +28,7 @@ export async function appEntry(serverUrl: string, since = 0) { await operator.batchRecords(removeLastUnreadChannelId, 'appEntry - removeLastUnreadChannelId'); } - WebsocketManager.openAll(); + WebsocketManager.openAll('entry'); verifyPushProxy(serverUrl); diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 8a68e7e6be7..d8e7c18547f 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -66,14 +66,14 @@ export type EntryResponse = { error: unknown; } -export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise => { +export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise => { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const result = entryRest(serverUrl, teamId, channelId, since); + const result = entryRest(serverUrl, teamId, channelId, since, groupLabel); // Fetch data retention policies const isDataRetentionEnabled = await getIsDataRetentionEnabled(database); if (isDataRetentionEnabled) { - fetchDataRetentionPolicy(serverUrl); + fetchDataRetentionPolicy(serverUrl, false, groupLabel); } return result; @@ -82,10 +82,17 @@ export const entry = async (serverUrl: string, teamId?: string, channelId?: stri 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) { - const result = restDeferredAppEntryActions(serverUrl, since, currentUserId, currentUserLocale, preferences, config, license, teamData, chData, initialTeamId, initialChannelId); + initialTeamId?: string, initialChannelId?: string, + groupLabel?: string, +) { + const result = restDeferredAppEntryActions( + serverUrl, since, currentUserId, currentUserLocale, + preferences, config, license, + teamData, chData, initialTeamId, initialChannelId, + groupLabel, + ); - autoUpdateTimezone(serverUrl); + autoUpdateTimezone(serverUrl, groupLabel); return result; } @@ -117,7 +124,7 @@ const teamsToRemove = async (serverUrl: string, removeTeamIds?: string[]) => { return []; }; -const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise => { +const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return {error: `${serverUrl} database not found`}; @@ -126,7 +133,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, const lastDisconnectedAt = since || await getLastFullSync(database); - const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId, channelId); + const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, teamId, channelId, groupLabel); if ('error' in fetchedData) { return {error: fetchedData.error}; } @@ -143,7 +150,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, return {error}; } - const rolesData = await fetchRoles(serverUrl, teamData.memberships, chData?.memberships, meData.user, true); + 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); @@ -164,7 +171,7 @@ const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, return {models: models.flat(), initialChannelId, initialTeamId, prefData, teamData, chData, meData, gmConverted}; }; -const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeamId = '', channelId?: string): Promise => { +const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeamId = '', channelId?: string, groupLabel?: string): Promise => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; @@ -173,8 +180,8 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam const includeDeletedChannels = true; const fetchOnly = true; - const confReq = await fetchConfigAndLicense(serverUrl); - const prefData = await fetchMyPreferences(serverUrl, fetchOnly); + 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); @@ -193,8 +200,8 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam // Fetch in parallel teams / team membership / user preferences / user const promises: [Promise, Promise] = [ - fetchMyTeams(serverUrl, fetchOnly), - fetchMe(serverUrl, fetchOnly), + fetchMyTeams(serverUrl, fetchOnly, groupLabel), + fetchMe(serverUrl, fetchOnly, groupLabel), ]; const resolution = await Promise.all(promises); @@ -214,7 +221,7 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam // active team on mobile app after this point. const client = NetworkManager.getClient(serverUrl); - const serverChannel = await client.getChannel(channelId); + 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, @@ -228,7 +235,7 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam } if (initialTeamId) { - chData = await fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled); + chData = await fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled, groupLabel); } if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) { @@ -239,7 +246,7 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam 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); + chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, since, fetchOnly, false, isCRTEnabled, groupLabel); } } @@ -274,7 +281,7 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam } const availableTeamIds = await getAvailableTeamIds(database, initialTeamId, teamData.teams, prefData.preferences, meData.user?.locale); - const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly, isCRTEnabled); + const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, since, fetchOnly, isCRTEnabled, groupLabel); data = { ...data, @@ -304,12 +311,14 @@ const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeam const fetchAlternateTeamData = async ( serverUrl: string, availableTeamIds: string[], removeTeamIds: string[], - includeDeleted = true, since = 0, fetchOnly = false, isCRTEnabled?: boolean) => { + 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); + 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); @@ -367,7 +376,7 @@ 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, + initialTeamId?: string, initialChannelId?: string, groupLabel?: string, ) { const isCRTEnabled = (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) || false; const directChannels = chData?.channels?.filter(isDMorGM); @@ -376,21 +385,21 @@ async function restDeferredAppEntryActions( // sidebar DM & GM profiles if (channelsToFetchProfiles.size) { const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license); - fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId); + fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId, false, groupLabel); } - updateAllUsersSince(serverUrl, since); + updateAllUsersSince(serverUrl, since, false, groupLabel); updateCanJoinTeams(serverUrl); - // defer fetch channels and unread posts for other teams setTimeout(async () => { if (chData?.channels?.length && chData.memberships?.length && initialTeamId) { if (isCRTEnabled && initialTeamId) { - await syncTeamThreads(serverUrl, initialTeamId); + await syncTeamThreads(serverUrl, initialTeamId, false, false, groupLabel); } - fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId); + fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId, false, groupLabel); } + // defer fetch channels and unread posts for other teams 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(',')); @@ -418,16 +427,16 @@ async function restDeferredAppEntryActions( } if (myTeams.length) { - fetchTeamsChannelsThreadsAndUnreadPosts(serverUrl, since, myTeams, isCRTEnabled); + fetchTeamsChannelsThreadsAndUnreadPosts(serverUrl, since, myTeams, isCRTEnabled, false, groupLabel + '-additional'); } } }); // Fetch groups for current user - fetchGroupsForMember(serverUrl, currentUserId); + fetchGroupsForMember(serverUrl, currentUserId, false, groupLabel); } -export const setExtraSessionProps = async (serverUrl: string) => { +export const setExtraSessionProps = async (serverUrl: string, groupLabel?: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const serverVersion = await getConfigValue(database, 'Version'); @@ -441,7 +450,7 @@ export const setExtraSessionProps = async (serverUrl: string) => { const res = await checkNotifications(); const granted = res.status === RESULTS.GRANTED || res.status === RESULTS.LIMITED; const client = NetworkManager.getClient(serverUrl); - client.setExtraSessionProps(deviceToken, !granted, nativeApplicationVersion); + client.setExtraSessionProps(deviceToken, !granted, nativeApplicationVersion, groupLabel); } return {}; } catch (error) { @@ -450,7 +459,7 @@ export const setExtraSessionProps = async (serverUrl: string) => { } }; -export async function verifyPushProxy(serverUrl: string) { +export async function verifyPushProxy(serverUrl: string, groupLabel?: string) { const deviceId = await getDeviceToken(); if (!deviceId) { return; @@ -468,7 +477,7 @@ export async function verifyPushProxy(serverUrl: string) { } const client = NetworkManager.getClient(serverUrl); - const response = await client.ping(deviceId); + const response = await client.ping(deviceId, undefined, groupLabel); const canReceiveNotifications = response?.data?.CanReceiveNotifications; switch (canReceiveNotifications) { case PUSH_PROXY_RESPONSE_NOT_AVAILABLE: diff --git a/app/actions/remote/entry/login.ts b/app/actions/remote/entry/login.ts index 48b7349141a..c59acb423bd 100644 --- a/app/actions/remote/entry/login.ts +++ b/app/actions/remote/entry/login.ts @@ -31,7 +31,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); + await WebsocketManager.initializeClient(serverUrl, 'login'); } return {}; diff --git a/app/actions/remote/entry/notification.ts b/app/actions/remote/entry/notification.ts index 2876c801526..c78a64ce1ae 100644 --- a/app/actions/remote/entry/notification.ts +++ b/app/actions/remote/entry/notification.ts @@ -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) { +export async function pushNotificationEntry(serverUrl: string, notification: NotificationData, groupLabel?: string) { // 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!; @@ -65,7 +65,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not let myTeam: MyTeamModel | TeamMembership | undefined = await getMyTeamById(database, teamId); if (!myTeam) { - const resp = await fetchMyTeam(serverUrl, teamId); + const resp = await fetchMyTeam(serverUrl, teamId, false, groupLabel); if (resp.error) { if (isErrorWithStatusCode(resp.error) && resp.error.status_code === 403) { emitNotificationError('Team'); @@ -78,7 +78,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not } if (!myChannel) { - const resp = await fetchMyChannel(serverUrl, teamId, channelId); + const resp = await fetchMyChannel(serverUrl, teamId, channelId, false, groupLabel); if (resp.error) { if (isErrorWithStatusCode(resp.error) && resp.error.status_code === 403) { emitNotificationError('Channel'); @@ -97,7 +97,7 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not if (isThreadNotification) { let post: PostModel | Post | undefined = await getPostById(database, rootId); if (!post) { - const resp = await fetchPostById(serverUrl, rootId); + const resp = await fetchPostById(serverUrl, rootId, false, groupLabel); post = resp.post; } @@ -105,20 +105,20 @@ export async function pushNotificationEntry(serverUrl: string, notification: Not if (actualRootId) { PerformanceMetricsManager.setLoadTarget('THREAD'); - await fetchAndSwitchToThread(serverUrl, actualRootId, true); + await fetchAndSwitchToThread(serverUrl, actualRootId, true, groupLabel); } else if (post) { PerformanceMetricsManager.setLoadTarget('THREAD'); - await fetchAndSwitchToThread(serverUrl, rootId, true); + await fetchAndSwitchToThread(serverUrl, rootId, true, groupLabel); } else { emitNotificationError('Post'); } } else { PerformanceMetricsManager.setLoadTarget('CHANNEL'); - await switchToChannelById(serverUrl, channelId, teamId); + await switchToChannelById(serverUrl, channelId, teamId, false, groupLabel); } } - WebsocketManager.openAll(); + WebsocketManager.openAll(groupLabel); return {}; } diff --git a/app/actions/remote/groups.ts b/app/actions/remote/groups.ts index 50d6a5fca42..fc2d38256a7 100644 --- a/app/actions/remote/groups.ts +++ b/app/actions/remote/groups.ts @@ -66,7 +66,7 @@ export const fetchGroupsByNames = async (serverUrl: string, names: string[], fet } }; -export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false) => { +export const fetchGroupsForChannel = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) => { try { const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const license = await getLicense(database); @@ -75,7 +75,7 @@ export const fetchGroupsForChannel = async (serverUrl: string, channelId: string } const client = NetworkManager.getClient(serverUrl); - const response = await client.getAllGroupsAssociatedToChannel(channelId); + const response = await client.getAllGroupsAssociatedToChannel(channelId, undefined, groupLabel); if (!response.groups.length) { return {groups: [], groupChannels: []}; @@ -129,7 +129,7 @@ export const fetchGroupsForTeam = async (serverUrl: string, teamId: string, fetc } }; -export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false) => { +export const fetchGroupsForMember = async (serverUrl: string, userId: string, fetchOnly = false, groupLabel?: string) => { try { const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const license = await getLicense(database); @@ -138,7 +138,7 @@ export const fetchGroupsForMember = async (serverUrl: string, userId: string, fe } const client: Client = NetworkManager.getClient(serverUrl); - const response = await client.getAllGroupsAssociatedToMembership(userId); + const response = await client.getAllGroupsAssociatedToMembership(userId, false, groupLabel); if (!response.length) { return {groups: [], groupMemberships: []}; @@ -191,13 +191,13 @@ export const fetchGroupsForTeamIfConstrained = async (serverUrl: string, teamId: } }; -export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false) => { +export const fetchGroupsForChannelIfConstrained = async (serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const channel = await getChannelById(database, channelId); if (channel?.isGroupConstrained) { - return fetchGroupsForChannel(serverUrl, channelId, fetchOnly); + return fetchGroupsForChannel(serverUrl, channelId, fetchOnly, groupLabel); } return {}; diff --git a/app/actions/remote/notifications.test.ts b/app/actions/remote/notifications.test.ts index 72b2cdced49..ad3d3467d54 100644 --- a/app/actions/remote/notifications.test.ts +++ b/app/actions/remote/notifications.test.ts @@ -107,6 +107,11 @@ afterEach(async () => { await DatabaseManager.destroyServerDatabase(serverUrl); }); +afterAll(() => { + // This removes the timer set to log the results of the network metrics + jest.clearAllTimers(); +}); + describe('notifications', () => { it('fetchNotificationData - handle no channel id', async () => { const result = await fetchNotificationData(serverUrl, {} as NotificationWithData); diff --git a/app/actions/remote/post.ts b/app/actions/remote/post.ts index fd3193178d6..acf8e3c4b29 100644 --- a/app/actions/remote/post.ts +++ b/app/actions/remote/post.ts @@ -284,7 +284,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => { return {}; }; -export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false): Promise { +export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise { try { if (!fetchOnly) { EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId); @@ -296,10 +296,10 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string, const postsInChannel = await getRecentPostsInChannel(database, channelId); const since = myChannel?.lastFetchedAt || postsInChannel?.[0]?.createAt || 0; if (since) { - postAction = fetchPostsSince(serverUrl, channelId, since, true); + postAction = fetchPostsSince(serverUrl, channelId, since, true, groupLabel); actionType = ActionType.POSTS.RECEIVED_SINCE; } else { - postAction = fetchPosts(serverUrl, channelId, 0, General.POST_CHUNK_SIZE, true); + postAction = fetchPosts(serverUrl, channelId, 0, General.POST_CHUNK_SIZE, true, groupLabel); actionType = ActionType.POSTS.RECEIVED_IN_CHANNEL; } @@ -309,7 +309,7 @@ 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); + const {authors: fetchedAuthors} = await fetchPostAuthors(serverUrl, data.posts, true, groupLabel); authors = fetchedAuthors || []; if (!fetchOnly) { @@ -332,7 +332,10 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string, } } -export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string, fetchOnly = false): Promise => { +export const fetchPostsForUnreadChannels = async ( + serverUrl: string, channels: Channel[], memberships: ChannelMembership[], + excludeChannelId?: string, fetchOnly = false, groupLabel?: string, +): Promise => { const membersMap = new Map(); for (const member of memberships) { membersMap.set(member.channel_id, member); @@ -353,7 +356,7 @@ export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: C for await (const channelIds of chunks) { const promises = []; for (const channelId of channelIds) { - promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly)); + promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly, groupLabel)); } const results = await Promise.all(promises); @@ -362,7 +365,7 @@ export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: C return postsForChannel; }; -export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false): Promise { +export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false, groupLabel?: string): Promise { try { if (!fetchOnly) { EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId); @@ -370,7 +373,7 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0, const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const client = NetworkManager.getClient(serverUrl); const isCRTEnabled = await getIsCRTEnabled(database); - const data = await client.getPosts(channelId, page, perPage, isCRTEnabled, isCRTEnabled); + const data = await client.getPosts(channelId, page, perPage, isCRTEnabled, isCRTEnabled, groupLabel); const result = processPostsFetched(data); if (!fetchOnly && result.posts.length) { const models = await operator.handlePosts({ @@ -379,7 +382,7 @@ export async function fetchPosts(serverUrl: string, channelId: string, page = 0, prepareRecordsOnly: true, }); - const {authors} = await fetchPostAuthors(serverUrl, result.posts, true); + const {authors} = await fetchPostAuthors(serverUrl, result.posts, true, groupLabel); if (authors?.length) { const userModels = await operator.handleUsers({ users: authors, @@ -462,7 +465,7 @@ export async function fetchPostsBefore(serverUrl: string, channelId: string, pos } } -export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false): Promise { +export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false, groupLabel?: string): Promise { try { if (!fetchOnly) { EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId); @@ -471,7 +474,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const isCRTEnabled = await getIsCRTEnabled(database); - const data = await client.getPostsSince(channelId, since, isCRTEnabled, isCRTEnabled); + const data = await client.getPostsSince(channelId, since, isCRTEnabled, isCRTEnabled, groupLabel); const result = processPostsFetched(data); if (!fetchOnly) { const models = await operator.handlePosts({ @@ -480,7 +483,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc prepareRecordsOnly: true, }); - const {authors} = await fetchPostAuthors(serverUrl, result.posts, true); + const {authors} = await fetchPostAuthors(serverUrl, result.posts, true, groupLabel); if (authors?.length) { const userModels = await operator.handleUsers({ users: authors, @@ -509,7 +512,7 @@ export async function fetchPostsSince(serverUrl: string, channelId: string, sinc } } -export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false): Promise => { +export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false, groupLabel?: string): Promise => { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const client = NetworkManager.getClient(serverUrl); @@ -537,11 +540,11 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn } const promises: Array> = []; if (userIdsToLoad.size) { - promises.push(client.getProfilesByIds(Array.from(userIdsToLoad))); + promises.push(client.getProfilesByIds(Array.from(userIdsToLoad), {}, groupLabel)); } if (usernamesToLoad.size) { - promises.push(client.getProfilesByUsernames(Array.from(usernamesToLoad))); + promises.push(client.getProfilesByUsernames(Array.from(usernamesToLoad), groupLabel)); } if (promises.length) { @@ -572,7 +575,7 @@ export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOn } }; -export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false) { +export async function fetchPostThread(serverUrl: string, postId: string, options?: FetchPaginatedThreadOptions, fetchOnly = false, groupLabel?: string) { try { setFetchingThreadState(postId, true); const client = NetworkManager.getClient(serverUrl); @@ -586,7 +589,7 @@ export async function fetchPostThread(serverUrl: string, postId: string, options collapsedThreads: isCRTEnabled, collapsedThreadsExtended: isCRTEnabled, ...options, - }); + }, groupLabel); const result = processPostsFetched(data); let posts: Model[] = []; if (result.posts.length && !fetchOnly) { @@ -598,7 +601,7 @@ export async function fetchPostThread(serverUrl: string, postId: string, options }); models.push(...posts); - const {authors} = await fetchPostAuthors(serverUrl, result.posts, true); + const {authors} = await fetchPostAuthors(serverUrl, result.posts, true, groupLabel); if (authors?.length) { const userModels = await operator.handleUsers({ users: authors, @@ -743,14 +746,14 @@ export async function fetchMissingChannelsFromPosts(serverUrl: string, posts: Po } } -export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false) { +export async function fetchPostById(serverUrl: string, postId: string, fetchOnly = false, groupLabel?: string) { try { const client = NetworkManager.getClient(serverUrl); const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const post = await client.getPost(postId); + const post = await client.getPost(postId, groupLabel); if (!fetchOnly) { const models: Model[] = []; - const {authors} = await fetchPostAuthors(serverUrl, [post], true); + const {authors} = await fetchPostAuthors(serverUrl, [post], true, groupLabel); const posts = await operator.handlePosts({ actionType: ActionType.POSTS.RECEIVED_NEW, order: [post.id], diff --git a/app/actions/remote/preference.ts b/app/actions/remote/preference.ts index fd1b4b4db74..5b16b4ff039 100644 --- a/app/actions/remote/preference.ts +++ b/app/actions/remote/preference.ts @@ -28,12 +28,12 @@ export type MyPreferencesRequest = { error?: unknown; }; -export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false): Promise => { +export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise => { try { const client = NetworkManager.getClient(serverUrl); const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const preferences = await client.getMyPreferences(); + const preferences = await client.getMyPreferences(groupLabel); if (!fetchOnly) { await operator.handlePreferences({ @@ -84,7 +84,7 @@ export const savePostPreference = async (serverUrl: string, postId: string) => { } }; -export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false) => { +export const savePreference = async (serverUrl: string, preferences: PreferenceType[], prepareRecordsOnly = false, groupLabel?: string) => { try { if (!preferences.length) { return {preferences: []}; @@ -97,7 +97,7 @@ export const savePreference = async (serverUrl: string, preferences: PreferenceT const chunkSize = 100; const chunks = chunk(preferences, chunkSize); chunks.forEach((c: PreferenceType[]) => { - client.savePreferences(userId, c); + client.savePreferences(userId, c, groupLabel); }); const preferenceModels = await operator.handlePreferences({ preferences, @@ -141,14 +141,14 @@ export const deleteSavedPost = async (serverUrl: string, postId: string) => { } }; -export const openChannelIfNeeded = async (serverUrl: string, channelId: string) => { +export const openChannelIfNeeded = async (serverUrl: string, channelId: string, groupLabel?: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const channel = await getChannelById(database, channelId); if (!channel || !isDMorGM(channel)) { return {}; } - const res = await openChannels(serverUrl, [channel]); + const res = await openChannels(serverUrl, [channel], groupLabel); return res; } catch (error) { forceLogoutIfNecessary(serverUrl, error); @@ -156,11 +156,11 @@ export const openChannelIfNeeded = async (serverUrl: string, channelId: string) } }; -export const openAllUnreadChannels = async (serverUrl: string) => { +export const openAllUnreadChannels = async (serverUrl: string, groupLabel?: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const channels = await queryAllUnreadDMsAndGMsIds(database).fetch(); - const res = await openChannels(serverUrl, channels); + const res = await openChannels(serverUrl, channels, groupLabel); return res; } catch (error) { forceLogoutIfNecessary(serverUrl, error); @@ -168,7 +168,7 @@ export const openAllUnreadChannels = async (serverUrl: string) => { } }; -const openChannels = async (serverUrl: string, channels: ChannelModel[]) => { +const openChannels = async (serverUrl: string, channels: ChannelModel[], groupLabel?: string) => { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const userId = await getCurrentUserId(database); @@ -202,7 +202,7 @@ const openChannels = async (serverUrl: string, channels: ChannelModel[]) => { ); } - return savePreference(serverUrl, prefs); + return savePreference(serverUrl, prefs, false, groupLabel); }; export const setDirectChannelVisible = async (serverUrl: string, channelId: string, visible = true) => { diff --git a/app/actions/remote/role.ts b/app/actions/remote/role.ts index 8ffb1ed840f..875661e4f4a 100644 --- a/app/actions/remote/role.ts +++ b/app/actions/remote/role.ts @@ -15,7 +15,10 @@ export type RolesRequest = { roles?: Role[]; } -export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string[], fetchOnly = false, force = false): Promise => { +export const fetchRolesIfNeeded = async ( + serverUrl: string, updatedRoles: string[], + fetchOnly = false, force = false, groupLabel?: string, +): Promise => { if (!updatedRoles.length) { return {roles: []}; } @@ -46,7 +49,7 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string const getRolesRequests = []; for (let i = 0; i < newRoles.length; i += General.MAX_GET_ROLES_BY_NAMES) { const chunk = newRoles.slice(i, i + General.MAX_GET_ROLES_BY_NAMES); - getRolesRequests.push(client.getRolesByNames(chunk)); + getRolesRequests.push(client.getRolesByNames(chunk, groupLabel)); } const roles = (await Promise.all(getRolesRequests)).flat(); @@ -65,7 +68,10 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string } }; -export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], user?: UserProfile, fetchOnly = false, force = false) => { +export const fetchRoles = async ( + serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], + user?: UserProfile, fetchOnly = false, force = false, groupLabel?: string, +) => { const rolesToFetch = new Set(user?.roles.split(' ') || []); if (teamMembership?.length) { @@ -87,7 +93,7 @@ export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembers rolesToFetch.delete(''); if (rolesToFetch.size > 0) { - return fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), fetchOnly, force); + return fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch), fetchOnly, force, groupLabel); } return {roles: []}; diff --git a/app/actions/remote/systems.ts b/app/actions/remote/systems.ts index 0a4c8a8b8a9..6f365c8216a 100644 --- a/app/actions/remote/systems.ts +++ b/app/actions/remote/systems.ts @@ -22,9 +22,9 @@ export type DataRetentionPoliciesRequest = { error?: unknown; } -export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false): Promise => { - const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl); - const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl); +export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise => { + 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); const error = globalPolicyError || teamPoliciesError || channelPoliciesError; @@ -45,10 +45,10 @@ export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = fa return data; }; -export const fetchGlobalDataRetentionPolicy = async (serverUrl: string): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => { +export const fetchGlobalDataRetentionPolicy = async (serverUrl: string, groupLabel?: string): Promise<{data?: GlobalDataRetentionPolicy; error?: unknown}> => { try { const client = NetworkManager.getClient(serverUrl); - const data = await client.getGlobalDataRetentionPolicy(); + const data = await client.getGlobalDataRetentionPolicy(groupLabel); return {data}; } catch (error) { logDebug('error on fetchGlobalDataRetentionPolicy', getFullErrorMessage(error)); @@ -62,6 +62,7 @@ export const fetchAllGranularDataRetentionPolicies = async ( isChannel = false, page = 0, policies: Array = [], + groupLabel?: string, ): Promise<{data?: Array; error?: unknown}> => { try { const client = NetworkManager.getClient(serverUrl); @@ -70,13 +71,13 @@ export const fetchAllGranularDataRetentionPolicies = async ( const currentUserId = await getCurrentUserId(database); let data; if (isChannel) { - data = await client.getChannelDataRetentionPolicies(currentUserId, page); + data = await client.getChannelDataRetentionPolicies(currentUserId, page, undefined, groupLabel); } else { - data = await client.getTeamDataRetentionPolicies(currentUserId, page); + data = await client.getTeamDataRetentionPolicies(currentUserId, page, undefined, groupLabel); } policies.push(...data.policies); if (policies.length < data.total_count) { - await fetchAllGranularDataRetentionPolicies(serverUrl, isChannel, page + 1, policies); + await fetchAllGranularDataRetentionPolicies(serverUrl, isChannel, page + 1, policies, groupLabel); } return {data: policies}; } catch (error) { @@ -85,12 +86,12 @@ export const fetchAllGranularDataRetentionPolicies = async ( } }; -export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false): Promise => { +export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise => { try { const client = NetworkManager.getClient(serverUrl); const [config, license]: [ClientConfig, ClientLicense] = await Promise.all([ - client.getClientConfigOld(), - client.getClientLicenseOld(), + client.getClientConfigOld(groupLabel), + client.getClientLicenseOld(groupLabel), ]); if (!fetchOnly) { diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index 364c35a40af..59b202196da 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -161,14 +161,14 @@ export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string, } } -export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promise { +export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLabel?: string): Promise { try { const client = NetworkManager.getClient(serverUrl); const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const [teams, memberships]: [Team[], TeamMembership[]] = await Promise.all([ - client.getMyTeams(), - client.getMyTeamMembers(), + client.getMyTeams(groupLabel), + client.getMyTeamMembers(groupLabel), ]); if (!fetchOnly) { @@ -207,14 +207,14 @@ export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promis } } -export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false): Promise { +export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: string): Promise { try { const client = NetworkManager.getClient(serverUrl); const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const [team, membership] = await Promise.all([ - client.getTeam(teamId), - client.getTeamMember(teamId, 'me'), + client.getTeam(teamId, groupLabel), + client.getTeamMember(teamId, 'me', groupLabel), ]); if (!fetchOnly) { const modelPromises = prepareMyTeams(operator, [team], [membership]); @@ -247,14 +247,14 @@ export const fetchAllTeams = async (serverUrl: string, page = 0, perPage = PER_P } }; -const recCanJoinTeams = async (client: Client, myTeamsIds: Set, page: number): Promise => { - const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT); +const recCanJoinTeams = async (client: Client, myTeamsIds: Set, page: number, groupLabel?: string): Promise => { + 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; } if (fetchedTeams.length === PER_PAGE_DEFAULT) { - return recCanJoinTeams(client, myTeamsIds, page + 1); + return recCanJoinTeams(client, myTeamsIds, page + 1, groupLabel); } return false; @@ -298,7 +298,7 @@ export async function fetchTeamsForComponent( return {teams: alreadyLoaded, hasMore: false, page}; } -export const updateCanJoinTeams = async (serverUrl: string) => { +export const updateCanJoinTeams = async (serverUrl: string, groupLabel?: string) => { try { const client = NetworkManager.getClient(serverUrl); const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -306,7 +306,7 @@ export const updateCanJoinTeams = async (serverUrl: string) => { const myTeams = await queryMyTeams(database).fetch(); const myTeamsIds = new Set(myTeams.map((m) => m.id)); - const canJoin = await recCanJoinTeams(client, myTeamsIds, 0); + const canJoin = await recCanJoinTeams(client, myTeamsIds, 0, groupLabel); EphemeralStore.setCanJoinOtherTeams(serverUrl, canJoin); return {}; @@ -320,7 +320,7 @@ export const updateCanJoinTeams = async (serverUrl: string) => { export const fetchTeamsChannelsThreadsAndUnreadPosts = async ( serverUrl: string, since: number, teams: Team[], - isCRTEnabled?: boolean, fetchOnly = false, + isCRTEnabled?: boolean, fetchOnly = false, groupLabel?: string, ) => { try { const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -331,7 +331,7 @@ export const fetchTeamsChannelsThreadsAndUnreadPosts = async ( for await (const myTeams of chunks) { const promises = []; for (const team of myTeams) { - promises.push(fetchMyChannelsForTeam(serverUrl, team.id, true, since, true, true, isCRTEnabled)); + promises.push(fetchMyChannelsForTeam(serverUrl, team.id, true, since, true, true, isCRTEnabled, groupLabel)); } const results = await Promise.all(promises); @@ -356,8 +356,8 @@ export const fetchTeamsChannelsThreadsAndUnreadPosts = async ( } } - const unreadPromise = fetchPostsForUnreadChannels(serverUrl, channels, members, undefined, true); - const threadsPromise = syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true); + const unreadPromise = fetchPostsForUnreadChannels(serverUrl, channels, members, undefined, true, groupLabel); + const threadsPromise = syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true, groupLabel); const postPromises: [Promise, Promise<{models?: Model[]; error?: unknown}>] = [ unreadPromise, threadsPromise, diff --git a/app/actions/remote/thread.ts b/app/actions/remote/thread.ts index 0549c6ae1de..84270d35b6b 100644 --- a/app/actions/remote/thread.ts +++ b/app/actions/remote/thread.ts @@ -39,14 +39,14 @@ enum Direction { Down, } -export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false) => { +export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, isFromNotification = false, groupLabel?: string) => { const database = DatabaseManager.serverDatabases[serverUrl]?.database; if (!database) { return {error: `${serverUrl} database not found`}; } // Load thread before we open to the thread modal - fetchPostThread(serverUrl, rootId); + fetchPostThread(serverUrl, rootId, undefined, false, groupLabel); // Mark thread as read const isCRTEnabled = await getIsCRTEnabled(database); @@ -71,7 +71,7 @@ export const fetchAndSwitchToThread = async (serverUrl: string, rootId: string, if (currentChannelId === post?.channelId) { AppsManager.copyMainBindingsToThread(serverUrl, currentChannelId); } else { - AppsManager.fetchBindings(serverUrl, post.channelId, true); + AppsManager.fetchBindings(serverUrl, post.channelId, true, groupLabel); } } } @@ -214,6 +214,7 @@ export const fetchThreads = async ( options: FetchThreadsOptions, direction?: Direction, pages?: number, + groupLabel?: string, ) => { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { @@ -243,7 +244,12 @@ export const fetchThreads = async ( const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since, excludeDirect = false} = opts; currentPage++; - const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version, excludeDirect); + const {threads} = await client.getThreads( + currentUser.id, teamId, + before, after, perPage, + deleted, unread, since, false, version, + excludeDirect, groupLabel, + ); if (threads.length) { // Mark all fetched threads as following for (const thread of threads) { @@ -279,7 +285,10 @@ export const fetchThreads = async ( return {error: false, threads: threadsData}; }; -export const syncThreadsIfNeeded = async (serverUrl: string, isCRTEnabled: boolean, teams?: Team[], fetchOnly = false) => { +export const syncThreadsIfNeeded = async ( + serverUrl: string, isCRTEnabled: boolean, teams?: Team[], + fetchOnly = false, groupLabel?: string, +) => { try { if (!isCRTEnabled) { return {models: []}; @@ -291,7 +300,7 @@ export const syncThreadsIfNeeded = async (serverUrl: string, isCRTEnabled: boole if (teams?.length) { for (const team of teams) { - promises.push(syncTeamThreads(serverUrl, team.id, true, true)); + promises.push(syncTeamThreads(serverUrl, team.id, true, true, groupLabel)); } } @@ -304,10 +313,9 @@ export const syncThreadsIfNeeded = async (serverUrl: string, isCRTEnabled: boole } } - const flat = models.flat(); + const flat = removeDuplicatesModels(models.flat()); if (!fetchOnly && flat.length) { - const uniqueArray = removeDuplicatesModels(flat); - await operator.batchRecords(uniqueArray, 'syncThreadsIfNeeded'); + await operator.batchRecords(flat, 'syncThreadsIfNeeded'); } return {models: flat}; @@ -317,7 +325,10 @@ export const syncThreadsIfNeeded = async (serverUrl: string, isCRTEnabled: boole } }; -export const syncTeamThreads = async (serverUrl: string, teamId: string, excludeDirect = false, fetchOnly = false) => { +export const syncTeamThreads = async ( + serverUrl: string, teamId: string, + excludeDirect = false, fetchOnly = false, groupLabel?: string, +) => { try { const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const syncData = await getTeamThreadsSyncData(database, teamId); @@ -341,6 +352,8 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, exclude teamId, {unread: true, excludeDirect}, Direction.Down, + undefined, + groupLabel, ), fetchThreads( serverUrl, @@ -348,6 +361,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, exclude {excludeDirect}, undefined, 1, + groupLabel, ), ]); if (allUnreadThreads.error || latestThreads.error) { @@ -373,6 +387,9 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, exclude serverUrl, teamId, {deleted: true, since: syncData.latest + 1, excludeDirect}, + undefined, + undefined, + groupLabel, ); if (allNewThreads.error) { return {error: allNewThreads.error}; diff --git a/app/actions/remote/user.ts b/app/actions/remote/user.ts index ee683d28eed..5b102594885 100644 --- a/app/actions/remote/user.ts +++ b/app/actions/remote/user.ts @@ -43,12 +43,12 @@ export type ProfilesInChannelRequest = { error?: unknown; } -export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise => { +export const fetchMe = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise => { try { const client = NetworkManager.getClient(serverUrl); const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const resultSettled = await Promise.allSettled([client.getMe(), client.getStatus('me')]); + const resultSettled = await Promise.allSettled([client.getMe(groupLabel), client.getStatus('me', groupLabel)]); let user: UserProfile|undefined; let userStatus: UserStatus|undefined; for (const result of resultSettled) { @@ -96,12 +96,15 @@ export const refetchCurrentUser = async (serverUrl: string, currentUserId: strin setCurrentUserId(operator, user.id); }; -export async function fetchProfilesInChannel(serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, fetchOnly = false): Promise { +export async function fetchProfilesInChannel( + serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions, + fetchOnly = false, groupLabel?: string, +): Promise { try { const client = NetworkManager.getClient(serverUrl); const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const users = await client.getProfilesInChannel(channelId, options); + const users = await client.getProfilesInChannel(channelId, options, groupLabel); const uniqueUsers = Array.from(new Set(users)); const filteredUsers = uniqueUsers.filter((u) => u.id !== excludeUserId); if (!fetchOnly) { @@ -131,7 +134,7 @@ export async function fetchProfilesInChannel(serverUrl: string, channelId: strin } } -export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false): Promise { +export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false, groupLabel?: string): Promise { try { const client = NetworkManager.getClient(serverUrl); const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -147,7 +150,7 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann const gms = chunk(channelsToFetch, 50); const data: ProfilesInChannelRequest[] = []; - const requests = gms.map((cIds) => client.getProfilesInGroupChannels(cIds)); + const requests = gms.map((cIds) => client.getProfilesInGroupChannels(cIds, groupLabel)); const response = await Promise.all(requests); for (const r of response) { for (const id in r) { @@ -194,7 +197,10 @@ export async function fetchProfilesInGroupChannels(serverUrl: string, groupChann } } -export async function fetchProfilesPerChannels(serverUrl: string, channelIds: string[], excludeUserId?: string, fetchOnly = false): Promise { +export async function fetchProfilesPerChannels( + serverUrl: string, channelIds: string[], excludeUserId?: string, + fetchOnly = false, groupLabel?: string, +): Promise { try { const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -203,7 +209,7 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st const data: ProfilesInChannelRequest[] = []; for await (const cIds of channels) { - const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, undefined, true)); + const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, undefined, true, groupLabel)); const response = await Promise.all(requests); data.push(...response); } @@ -244,12 +250,12 @@ export async function fetchProfilesPerChannels(serverUrl: string, channelIds: st } } -export const updateMe = async (serverUrl: string, user: Partial) => { +export const updateMe = async (serverUrl: string, user: Partial, groupLabel?: string) => { try { const client = NetworkManager.getClient(serverUrl); const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); - const data = await client.patchMe(user); + const data = await client.patchMe(user, groupLabel); if (data) { operator.handleUsers({prepareRecordsOnly: false, users: [data]}); @@ -422,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) => { +export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetchOnly = false, groupLabel?: string) => { if (!userIds.length) { return {users: [], existingUsers: []}; } @@ -444,7 +450,7 @@ export const fetchUsersByIds = async (serverUrl: string, userIds: string[], fetc if (usersToLoad.size === 0) { return {users: [], existingUsers}; } - const users = await client.getProfilesByIds([...new Set(usersToLoad)]); + const users = await client.getProfilesByIds([...new Set(usersToLoad)], {}, groupLabel); if (!fetchOnly && users.length) { await operator.handleUsers({ users, @@ -626,7 +632,7 @@ export const fetchMissingProfilesByUsernames = async (serverUrl: string, usernam return {users}; }; -export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false) { +export async function updateAllUsersSince(serverUrl: string, since: number, fetchOnly = false, groupLabel?: string) { if (!since) { return {users: []}; } @@ -638,7 +644,7 @@ export async function updateAllUsersSince(serverUrl: string, since: number, fetc const currentUserId = await getCurrentUserId(database); const userIds = (await queryAllUsers(database).fetchIds()).filter((id) => id !== currentUserId); - userUpdates = await client.getProfilesByIds(userIds, {since}); + userUpdates = await client.getProfilesByIds(userIds, {since}, groupLabel); if (userUpdates.length && !fetchOnly) { const modelsToBatch: Model[] = []; const userModels = await operator.handleUsers({users: userUpdates, prepareRecordsOnly: true}); @@ -801,7 +807,7 @@ export const buildProfileImageUrlFromUser = (serverUrl: string, user: UserModel return buildProfileImageUrl(serverUrl, user.id, lastPictureUpdate); }; -export const autoUpdateTimezone = async (serverUrl: string) => { +export const autoUpdateTimezone = async (serverUrl: string, groupLabel?: string) => { let database; try { const result = DatabaseManager.getServerDatabaseAndOperator(serverUrl); @@ -824,7 +830,7 @@ export const autoUpdateTimezone = async (serverUrl: string) => { if (currentTimezone.useAutomaticTimezone && newTimezoneExists) { const timezone = {useAutomaticTimezone: 'true', automaticTimezone: deviceTimezone, manualTimezone: currentTimezone.manualTimezone}; - await updateMe(serverUrl, {timezone}); + await updateMe(serverUrl, {timezone}, groupLabel); } return {}; diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 39177ce7447..acc1be7768d 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -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) { - setExtraSessionProps(serverUrl); - autoUpdateTimezone(serverUrl); - return doReconnect(serverUrl); +export async function handleFirstConnect(serverUrl: string, groupLabel?: string) { + setExtraSessionProps(serverUrl, groupLabel); + autoUpdateTimezone(serverUrl, groupLabel); + return doReconnect(serverUrl, groupLabel); } export async function handleReconnect(serverUrl: string) { - return doReconnect(serverUrl); + return doReconnect(serverUrl, 'reconnection'); } -async function doReconnect(serverUrl: string) { +async function doReconnect(serverUrl: string, groupLabel?: string) { const operator = DatabaseManager.serverDatabases[serverUrl]?.operator; if (!operator) { return new Error('cannot find server database'); @@ -66,7 +66,7 @@ async function doReconnect(serverUrl: string) { const currentChannelId = await getCurrentChannelId(database); setTeamLoading(serverUrl, true); - const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastFullSync); + const entryData = await entry(serverUrl, currentTeamId, currentChannelId, lastFullSync, groupLabel); if ('error' in entryData) { setTeamLoading(serverUrl, false); return entryData.error; @@ -85,34 +85,34 @@ async function doReconnect(serverUrl: string) { const tabletDevice = isTablet(); const isActiveServer = (await getActiveServerUrl()) === serverUrl; if (isActiveServer && tabletDevice && initialChannelId === currentChannelId) { - await markChannelAsRead(serverUrl, initialChannelId); + await markChannelAsRead(serverUrl, initialChannelId, false, groupLabel); markChannelAsViewed(serverUrl, initialChannelId); } logInfo('WEBSOCKET RECONNECT MODELS BATCHING TOOK', `${Date.now() - dt}ms`); setTeamLoading(serverUrl, false); - await fetchPostDataIfNeeded(serverUrl); + await fetchPostDataIfNeeded(serverUrl, groupLabel); const {id: currentUserId, locale: currentUserLocale} = (await getCurrentUser(database))!; const license = await getLicense(database); const config = await getConfig(database); if (isSupportedServerCalls(config?.Version)) { - loadConfigAndCalls(serverUrl, currentUserId); + loadConfigAndCalls(serverUrl, currentUserId, groupLabel); } - await deferredAppEntryActions(serverUrl, lastFullSync, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId); + await deferredAppEntryActions(serverUrl, lastFullSync, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId, undefined, groupLabel); - openAllUnreadChannels(serverUrl); + openAllUnreadChannels(serverUrl, groupLabel); dataRetentionCleanup(serverUrl); - AppsManager.refreshAppBindings(serverUrl); + AppsManager.refreshAppBindings(serverUrl, groupLabel); return undefined; } -async function fetchPostDataIfNeeded(serverUrl: string) { +async function fetchPostDataIfNeeded(serverUrl: string, groupLabel?: string) { try { const isActiveServer = (await getActiveServerUrl()) === serverUrl; if (!isActiveServer) { @@ -139,15 +139,15 @@ async function fetchPostDataIfNeeded(serverUrl: string) { options.fromCreateAt = lastPost.createAt; options.fromPost = lastPost.id; options.direction = 'down'; - await fetchPostThread(serverUrl, rootId, options); + await fetchPostThread(serverUrl, rootId, options, false, groupLabel); } } } } if (currentChannelId && (isChannelScreenMounted || tabletDevice)) { - await fetchPostsForChannel(serverUrl, currentChannelId); - markChannelAsRead(serverUrl, currentChannelId); + await fetchPostsForChannel(serverUrl, currentChannelId, false, groupLabel); + markChannelAsRead(serverUrl, currentChannelId, false, groupLabel); if (!EphemeralStore.wasNotificationTapped()) { markChannelAsViewed(serverUrl, currentChannelId, true); } diff --git a/app/client/rest/apps.ts b/app/client/rest/apps.ts index e80712460ed..12f705d92d1 100644 --- a/app/client/rest/apps.ts +++ b/app/client/rest/apps.ts @@ -7,7 +7,7 @@ import type ClientBase from './base'; export interface ClientAppsMix { executeAppCall: (call: AppCallRequest, trackAsSubmit: boolean) => Promise>; - getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise; + getAppsBindings: (userID: string, channelID: string, teamID: string, groupLabel?: string) => Promise; } const ClientApps = >(superclass: TBase) => class extends superclass { @@ -27,7 +27,7 @@ const ClientApps = >(superclass: TBase) => ); }; - getAppsBindings = async (userID: string, channelID: string, teamID: string) => { + getAppsBindings = async (userID: string, channelID: string, teamID: string, groupLabel?: string) => { const params = { user_id: userID, channel_id: channelID, @@ -37,7 +37,7 @@ const ClientApps = >(superclass: TBase) => return this.doFetch( `${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; }; diff --git a/app/client/rest/base.ts b/app/client/rest/base.ts index 0240d3f6761..d66c2c7f08a 100644 --- a/app/client/rest/base.ts +++ b/app/client/rest/base.ts @@ -1,33 +1,16 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {DeviceEventEmitter} from 'react-native'; - -import {Events, Calls} from '@constants'; -import {t} from '@i18n'; -import {setServerCredentials} from '@init/credentials'; -import {semverFromServerVersion} from '@utils/server'; +import {Calls} from '@constants'; import * as ClientConstants from './constants'; -import ClientError from './error'; - -import type { - APIClientInterface, - ClientHeaders, - ClientResponse, - RequestOptions, -} from '@mattermost/react-native-network-client'; - -export default class ClientBase { - apiClient: APIClientInterface; - csrfToken = ''; - requestHeaders: {[x: string]: string} = {}; - serverVersion = ''; - urlVersion = '/api/v4'; - enableLogging = false; +import ClientTracking from './tracking'; + +import type {APIClientInterface} from '@mattermost/react-native-network-client'; +export default class ClientBase extends ClientTracking { constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) { - this.apiClient = apiClient; + super(apiClient); if (bearerToken) { this.setBearerToken(bearerToken); @@ -54,16 +37,6 @@ export default class ClientBase { return this.apiClient.baseUrl + baseUrl; } - getRequestHeaders(requestMethod: string) { - const headers = {...this.requestHeaders}; - - if (this.csrfToken && requestMethod.toLowerCase() !== 'get') { - headers[ClientConstants.HEADER_X_CSRF_TOKEN] = this.csrfToken; - } - - return headers; - } - getWebSocketUrl = () => { return `${this.urlVersion}/websocket`; }; @@ -72,15 +45,6 @@ export default class ClientBase { this.requestHeaders[ClientConstants.HEADER_ACCEPT_LANGUAGE] = locale; } - setBearerToken(bearerToken: string) { - this.requestHeaders[ClientConstants.HEADER_AUTH] = `${ClientConstants.HEADER_BEARER} ${bearerToken}`; - setServerCredentials(this.apiClient.baseUrl, bearerToken); - } - - setCSRFToken(csrfToken: string) { - this.csrfToken = csrfToken; - } - // Routes getUsersRoute() { @@ -236,96 +200,6 @@ export default class ClientBase { } doFetch = async (url: string, options: ClientOptions, returnDataOnly = true) => { - let request; - const method = options.method?.toLowerCase(); - switch (method) { - case 'get': - request = this.apiClient!.get; - break; - case 'put': - request = this.apiClient!.put; - break; - case 'post': - request = this.apiClient!.post; - break; - case 'patch': - request = this.apiClient!.patch; - break; - case 'delete': - request = this.apiClient!.delete; - break; - default: - throw new ClientError(this.apiClient.baseUrl, { - message: 'Invalid request method', - intl: { - id: t('mobile.request.invalid_request_method'), - defaultMessage: 'Invalid request method', - }, - url, - }); - } - - const requestOptions: RequestOptions = { - body: options.body, - headers: this.getRequestHeaders(method), - }; - if (options.noRetry) { - requestOptions.retryPolicyConfiguration = { - retryLimit: 0, - }; - } - if (options.timeoutInterval) { - requestOptions.timeoutInterval = options.timeoutInterval; - } - - if (options.headers) { - if (requestOptions.headers) { - requestOptions.headers = { - ...requestOptions.headers, - ...options.headers, - }; - } else { - requestOptions.headers = options.headers; - } - } - - let response: ClientResponse; - try { - response = await request!(url, requestOptions); - } catch (error) { - throw new ClientError(this.apiClient.baseUrl, { - message: 'Received invalid response from the server.', - intl: { - id: t('mobile.request.invalid_response'), - defaultMessage: 'Received invalid response from the server.', - }, - url, - details: error, - }); - } - - const headers: ClientHeaders = response.headers || {}; - const serverVersion = semverFromServerVersion(headers[ClientConstants.HEADER_X_VERSION_ID] || headers[ClientConstants.HEADER_X_VERSION_ID.toLowerCase()]); - const hasCacheControl = Boolean(headers[ClientConstants.HEADER_CACHE_CONTROL] || headers[ClientConstants.HEADER_CACHE_CONTROL.toLowerCase()]); - if (serverVersion && !hasCacheControl && this.serverVersion !== serverVersion) { - this.serverVersion = serverVersion; - DeviceEventEmitter.emit(Events.SERVER_VERSION_CHANGED, {serverUrl: this.apiClient.baseUrl, serverVersion}); - } - - const bearerToken = headers[ClientConstants.HEADER_TOKEN] || headers[ClientConstants.HEADER_TOKEN.toLowerCase()]; - if (bearerToken) { - this.setBearerToken(bearerToken); - } - - if (response.ok) { - return returnDataOnly ? (response.data || {}) : response; - } - - throw new ClientError(this.apiClient.baseUrl, { - message: response.data?.message as string || `Response with status code ${response.code}`, - server_error_id: response.data?.id as string, - status_code: response.code, - url, - }); + return this.doFetchWithTracking(url, options, returnDataOnly); }; } diff --git a/app/client/rest/categories.ts b/app/client/rest/categories.ts index 73e15820c5d..fd1485573a3 100644 --- a/app/client/rest/categories.ts +++ b/app/client/rest/categories.ts @@ -4,17 +4,17 @@ import type ClientBase from './base'; export interface ClientCategoriesMix { - getCategories: (userId: string, teamId: string) => Promise; + getCategories: (userId: string, teamId: string, groupLabel?: string) => Promise; getCategoriesOrder: (userId: string, teamId: string) => Promise; getCategory: (userId: string, teamId: string, categoryId: string) => Promise; updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise; } const ClientCategories = >(superclass: TBase) => class extends superclass { - getCategories = async (userId: string, teamId: string) => { + getCategories = async (userId: string, teamId: string, groupLabel?: string) => { return this.doFetch( `${this.getCategoriesRoute(userId, teamId)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; getCategoriesOrder = async (userId: string, teamId: string) => { diff --git a/app/client/rest/channel_bookmark.ts b/app/client/rest/channel_bookmark.ts index 07dd0a27e7a..39e8665750f 100644 --- a/app/client/rest/channel_bookmark.ts +++ b/app/client/rest/channel_bookmark.ts @@ -10,7 +10,7 @@ export interface ClientChannelBookmarksMix { updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise; updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string): Promise; deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string): Promise; - getChannelBookmarksForChannel(channelId: string, since: number): Promise; + getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string): Promise; } const ClientChannelBookmarks = >(superclass: TBase) => class extends superclass { @@ -57,10 +57,10 @@ const ClientChannelBookmarks = >(superclas ); } - getChannelBookmarksForChannel(channelId: string, since: number) { + getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string) { return this.doFetch( `${this.getChannelBookmarksRoute(channelId)}${buildQueryString({bookmarks_since: since})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); } }; diff --git a/app/client/rest/channels.ts b/app/client/rest/channels.ts index 2d909b80b68..6900d1dfbc1 100644 --- a/app/client/rest/channels.ts +++ b/app/client/rest/channels.ts @@ -19,24 +19,24 @@ export interface ClientChannelsMix { updateChannelPrivacy: (channelId: string, privacy: any) => Promise; patchChannel: (channelId: string, channelPatch: Partial) => Promise; updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => Promise; - getChannel: (channelId: string) => Promise; + getChannel: (channelId: string, groupLabel?: string) => Promise; getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise; getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise; getChannels: (teamId: string, page?: number, perPage?: number) => Promise; getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise; getSharedChannels: (teamId: string, page?: number, perPage?: number) => Promise; - getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number) => Promise; + getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number, groupLabel?: string) => Promise; getMyChannelMember: (channelId: string) => Promise; - getMyChannelMembers: (teamId: string) => Promise; + getMyChannelMembers: (teamId: string, groupLabel?: string) => Promise; getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise; getChannelTimezones: (channelId: string) => Promise; - getChannelMember: (channelId: string, userId: string) => Promise; + getChannelMember: (channelId: string, userId: string, groupLabel?: string) => Promise; getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise; addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise; removeFromChannel: (userId: string, channelId: string) => Promise; - getChannelStats: (channelId: string) => Promise; + getChannelStats: (channelId: string, groupLabel?: string) => Promise; getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise; - viewMyChannel: (channelId: string, prevChannelId?: string) => Promise; + viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: string) => Promise; autocompleteChannels: (teamId: string, name: string) => Promise; autocompleteChannelsForSearch: (teamId: string, name: string) => Promise; searchChannels: (teamId: string, term: string) => Promise; @@ -130,10 +130,10 @@ const ClientChannels = >(superclass: TBase ); }; - getChannel = async (channelId: string) => { + getChannel = async (channelId: string, groupLabel?: string) => { return this.doFetch( this.getChannelRoute(channelId), - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -180,13 +180,13 @@ const ClientChannels = >(superclass: TBase ); }; - getMyChannels = async (teamId: string, includeDeleted = false, since = 0) => { + getMyChannels = async (teamId: string, includeDeleted = false, since = 0, groupLabel?: string) => { return this.doFetch( `${this.getUserRoute('me')}/teams/${teamId}/channels${buildQueryString({ include_deleted: includeDeleted, last_delete_at: since, })}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -197,10 +197,10 @@ const ClientChannels = >(superclass: TBase ); }; - getMyChannelMembers = async (teamId: string) => { + getMyChannelMembers = async (teamId: string, groupLabel?: string) => { return this.doFetch( `${this.getUserRoute('me')}/teams/${teamId}/channels/members`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -218,10 +218,10 @@ const ClientChannels = >(superclass: TBase ); }; - getChannelMember = async (channelId: string, userId: string) => { + getChannelMember = async (channelId: string, userId: string, groupLabel?: string) => { return this.doFetch( `${this.getChannelMemberRoute(channelId, userId)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -247,10 +247,10 @@ const ClientChannels = >(superclass: TBase ); }; - getChannelStats = async (channelId: string) => { + getChannelStats = async (channelId: string, groupLabel?: string) => { return this.doFetch( `${this.getChannelRoute(channelId)}/stats`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -261,12 +261,12 @@ const ClientChannels = >(superclass: TBase ); }; - viewMyChannel = async (channelId: string, prevChannelId?: string) => { + viewMyChannel = async (channelId: string, prevChannelId?: string, groupLabel?: string) => { // 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( `${this.getChannelsRoute()}/members/me/view`, - {method: 'post', body: data}, + {method: 'post', body: data, groupLabel}, ); }; diff --git a/app/client/rest/general.ts b/app/client/rest/general.ts index 76d85a9cf93..943144005fd 100644 --- a/app/client/rest/general.ts +++ b/app/client/rest/general.ts @@ -14,28 +14,28 @@ type PoliciesResponse = { } export interface ClientGeneralMix { - ping: (deviceId?: string, timeoutInterval?: number) => Promise; + ping: (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => Promise; logClientError: (message: string, level?: string) => Promise; - getClientConfigOld: () => Promise; - getClientLicenseOld: () => Promise; + getClientConfigOld: (groupLabel?: string) => Promise; + getClientLicenseOld: (groupLabel?: string) => Promise; getTimezones: () => Promise; - getGlobalDataRetentionPolicy: () => Promise; - getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number) => Promise>; - getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number) => Promise>; - getRolesByNames: (rolesNames: string[]) => Promise; + getGlobalDataRetentionPolicy: (groupLabel?: string) => Promise; + getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise>; + getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise>; + getRolesByNames: (rolesNames: string[], groupLabel?: string) => Promise; getRedirectLocation: (urlParam: string) => Promise>; sendPerformanceReport: (batch: PerformanceReport) => Promise<{}>; } const ClientGeneral = >(superclass: TBase) => class extends superclass { - ping = async (deviceId?: string, timeoutInterval?: number) => { + ping = async (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => { let url = `${this.urlVersion}/system/ping?time=${Date.now()}`; if (deviceId) { url = `${url}&device_id=${deviceId}`; } return this.doFetch( url, - {method: 'get', timeoutInterval}, + {method: 'get', timeoutInterval, groupLabel}, false, ); }; @@ -56,17 +56,17 @@ const ClientGeneral = >(superclass: TBase) ); }; - getClientConfigOld = async () => { + getClientConfigOld = async (groupLabel?: string) => { return this.doFetch( `${this.urlVersion}/config/client?format=old`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getClientLicenseOld = async () => { + getClientLicenseOld = async (groupLabel?: string) => { return this.doFetch( `${this.urlVersion}/license/client?format=old`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -77,31 +77,31 @@ const ClientGeneral = >(superclass: TBase) ); }; - getGlobalDataRetentionPolicy = () => { + getGlobalDataRetentionPolicy = (groupLabel?: string) => { return this.doFetch( `${this.getGlobalDataRetentionRoute()}/policy`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + getTeamDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: string) => { return this.doFetch( `${this.getGranularDataRetentionRoute(userId)}/team_policies${buildQueryString({page, per_page: perPage})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT) => { + getChannelDataRetentionPolicies = (userId: string, page = 0, perPage = PER_PAGE_DEFAULT, groupLabel?: string) => { return this.doFetch( `${this.getGranularDataRetentionRoute(userId)}/channel_policies${buildQueryString({page, per_page: perPage})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getRolesByNames = async (rolesNames: string[]) => { + getRolesByNames = async (rolesNames: string[], groupLabel?: string) => { return this.doFetch( `${this.getRolesRoute()}/names`, - {method: 'post', body: rolesNames}, + {method: 'post', body: rolesNames, groupLabel}, ); }; diff --git a/app/client/rest/groups.ts b/app/client/rest/groups.ts index 88ef1eaccde..c1b68286bc3 100644 --- a/app/client/rest/groups.ts +++ b/app/client/rest/groups.ts @@ -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; - getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>; - getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise; + getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<{groups: Group[]; total_group_count: number}>; + getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise; getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>; } @@ -29,14 +29,14 @@ const ClientGroups = >(superclass: TBase) ); }; - getAllGroupsAssociatedToChannel = async (channelId: string, filterAllowReference = false) => { + getAllGroupsAssociatedToChannel = async (channelId: string, filterAllowReference = false, groupLabel?: string) => { return this.doFetch( `${this.urlVersion}/channels/${channelId}/groups${buildQueryString({ paginate: false, filter_allow_reference: filterAllowReference, include_member_count: true, })}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -47,10 +47,10 @@ const ClientGroups = >(superclass: TBase) ); }; - getAllGroupsAssociatedToMembership = async (userId: string, filterAllowReference = false) => { + getAllGroupsAssociatedToMembership = async (userId: string, filterAllowReference = false, groupLabel?: string) => { return this.doFetch( `${this.urlVersion}/users/${userId}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; }; diff --git a/app/client/rest/posts.ts b/app/client/rest/posts.ts index 32bd2a96508..f7b2c856609 100644 --- a/app/client/rest/posts.ts +++ b/app/client/rest/posts.ts @@ -10,12 +10,12 @@ import type ClientBase from './base'; export interface ClientPostsMix { createPost: (post: Post) => Promise; updatePost: (post: Post) => Promise; - getPost: (postId: string) => Promise; + getPost: (postId: string, groupLabel?: string) => Promise; patchPost: (postPatch: Partial & {id: string}) => Promise; deletePost: (postId: string) => Promise; - getPostThread: (postId: string, options: FetchPaginatedThreadOptions) => Promise; - getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; - getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; + getPostThread: (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => Promise; + getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise; + getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise; getPostsBefore: (channelId: string, postId?: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise; getFileInfosForPost: (postId: string) => Promise; @@ -51,10 +51,10 @@ const ClientPosts = >(superclass: TBase) = ); }; - getPost = async (postId: string) => { + getPost = async (postId: string, groupLabel?: string) => { return this.doFetch( `${this.getPostRoute(postId)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -72,7 +72,7 @@ const ClientPosts = >(superclass: TBase) = ); }; - getPostThread = (postId: string, options: FetchPaginatedThreadOptions) => { + getPostThread = (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => { const { fetchThreads = true, collapsedThreads = false, @@ -84,21 +84,21 @@ const ClientPosts = >(superclass: TBase) = } = options; return this.doFetch( `${this.getPostRoute(postId)}/thread${buildQueryString({skipFetchThreads: !fetchThreads, collapsedThreads, collapsedThreadsExtended, direction, perPage, ...rest})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false) => { + getPosts = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: string) => { return this.doFetch( `${this.getChannelRoute(channelId)}/posts${buildQueryString({page, per_page: perPage, collapsedThreads, collapsedThreadsExtended})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getPostsSince = async (channelId: string, since: number, collapsedThreads = false, collapsedThreadsExtended = false) => { + getPostsSince = async (channelId: string, since: number, collapsedThreads = false, collapsedThreadsExtended = false, groupLabel?: string) => { return this.doFetch( `${this.getChannelRoute(channelId)}/posts${buildQueryString({since, collapsedThreads, collapsedThreadsExtended})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; diff --git a/app/client/rest/preferences.ts b/app/client/rest/preferences.ts index 16adfb0b88c..dbb40183edd 100644 --- a/app/client/rest/preferences.ts +++ b/app/client/rest/preferences.ts @@ -4,23 +4,23 @@ import type ClientBase from './base'; export interface ClientPreferencesMix { - savePreferences: (userId: string, preferences: PreferenceType[]) => Promise; + savePreferences: (userId: string, preferences: PreferenceType[], groupLabel?: string) => Promise; deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise; - getMyPreferences: () => Promise; + getMyPreferences: (groupLabel?: string) => Promise; } const ClientPreferences = >(superclass: TBase) => class extends superclass { - savePreferences = async (userId: string, preferences: PreferenceType[]) => { + savePreferences = async (userId: string, preferences: PreferenceType[], groupLabel?: string) => { return this.doFetch( `${this.getPreferencesRoute(userId)}`, - {method: 'put', body: preferences}, + {method: 'put', body: preferences, groupLabel}, ); }; - getMyPreferences = async () => { + getMyPreferences = async (groupLabel?: string) => { return this.doFetch( `${this.getPreferencesRoute('me')}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; diff --git a/app/client/rest/teams.ts b/app/client/rest/teams.ts index cd6f802906c..c2136b2561e 100644 --- a/app/client/rest/teams.ts +++ b/app/client/rest/teams.ts @@ -12,14 +12,14 @@ export interface ClientTeamsMix { deleteTeam: (teamId: string) => Promise; updateTeam: (team: Team) => Promise; patchTeam: (team: Partial & {id: string}) => Promise; - getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise; - getTeam: (teamId: string) => Promise; + getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean, groupLabel?: string) => Promise; + getTeam: (teamId: string, groupLabel?: string) => Promise; getTeamByName: (teamName: string) => Promise; - getMyTeams: () => Promise; + getMyTeams: (groupLabel?: string) => Promise; getTeamsForUser: (userId: string) => Promise; - getMyTeamMembers: () => Promise; + getMyTeamMembers: (groupLabel?: string) => Promise; getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise; - getTeamMember: (teamId: string, userId: string) => Promise; + getTeamMember: (teamId: string, userId: string, groupLabel?: string) => Promise; getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise; addToTeam: (teamId: string, userId: string) => Promise; addUsersToTeamGracefully: (teamId: string, userIds: string[]) => Promise; @@ -59,17 +59,17 @@ const ClientTeams = >(superclass: TBase) = ); }; - getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false) => { + getTeams = async (page = 0, perPage = PER_PAGE_DEFAULT, includeTotalCount = false, groupLabel?: string) => { return this.doFetch( `${this.getTeamsRoute()}${buildQueryString({page, per_page: perPage, include_total_count: includeTotalCount})}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getTeam = async (teamId: string) => { + getTeam = async (teamId: string, groupLabel?: string) => { return this.doFetch( this.getTeamRoute(teamId), - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -80,10 +80,10 @@ const ClientTeams = >(superclass: TBase) = ); }; - getMyTeams = async () => { + getMyTeams = async (groupLabel?: string) => { return this.doFetch( `${this.getUserRoute('me')}/teams`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -94,10 +94,10 @@ const ClientTeams = >(superclass: TBase) = ); }; - getMyTeamMembers = async () => { + getMyTeamMembers = async (groupLabel?: string) => { return this.doFetch( `${this.getUserRoute('me')}/teams/members`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -108,10 +108,10 @@ const ClientTeams = >(superclass: TBase) = ); }; - getTeamMember = async (teamId: string, userId: string) => { + getTeamMember = async (teamId: string, userId: string, groupLabel?: string) => { return this.doFetch( `${this.getTeamMemberRoute(teamId, userId)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; diff --git a/app/client/rest/threads.ts b/app/client/rest/threads.ts index 5a5c79cb1ea..4239f156bf6 100644 --- a/app/client/rest/threads.ts +++ b/app/client/rest/threads.ts @@ -8,7 +8,12 @@ import {PER_PAGE_DEFAULT} from './constants'; import type ClientBase from './base'; export interface ClientThreadsMix { - getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string, excludeDirect?: boolean) => Promise; + getThreads: ( + userId: string, teamId: string, + before?: string, after?: string, pageSize?: number, + deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string, + excludeDirect?: boolean, groupLabel?: string, + ) => Promise; getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise; markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise; markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise; @@ -17,7 +22,11 @@ export interface ClientThreadsMix { } const ClientThreads = >(superclass: TBase) => class extends superclass { - getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '', excludeDirect = false) => { + getThreads = async ( + userId: string, teamId: string, + before = '', after = '', pageSize = PER_PAGE_DEFAULT, + deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '', + excludeDirect = false, groupLabel?: string) => { const queryStringObj: Record = { extended: 'true', before, @@ -35,7 +44,7 @@ const ClientThreads = >(superclass: TBase) } return this.doFetch( `${this.getThreadsRoute(userId, teamId)}${buildQueryString(queryStringObj)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; diff --git a/app/client/rest/tracking.test.ts b/app/client/rest/tracking.test.ts new file mode 100644 index 00000000000..84543ee9982 --- /dev/null +++ b/app/client/rest/tracking.test.ts @@ -0,0 +1,269 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import test_helper from '@test/test_helper'; + +import * as ClientConstants from './constants'; +import ClientTracking from './tracking'; + +import type {APIClientInterface, ClientResponseMetrics} from '@mattermost/react-native-network-client'; + +jest.mock('react-native', () => ({ + DeviceEventEmitter: { + emit: jest.fn(), + }, + Platform: { + OS: 'ios', + select: jest.fn(({ios, default: def}: any) => { + return ios ?? def; + }), + }, +})); + +jest.mock('@mattermost/rnutils', () => ({ + getIOSAppGroupDetails: jest.fn().mockRejectedValue(''), + isRunningInSplitView: jest.fn().mockReturnValue({isSplit: false, isTablet: false}), +})); + +jest.mock('expo-crypto', () => ({ + randomUUID: jest.fn(() => '12345678-1234-1234-1234-1234567890ab'), +})); + +jest.mock('@i18n', () => ({ + t: jest.fn((key) => key), +})); + +jest.mock('@utils/file', () => ({ + getFormattedFileSize: jest.fn((size) => `${size} bytes`), +})); + +jest.mock('@utils/log', () => ({ + logDebug: jest.fn(), + logInfo: jest.fn(), +})); + +jest.mock('@utils/server', () => ({ + semverFromServerVersion: jest.fn((version) => version), +})); + +jest.mock('@init/credentials', () => ({ + setServerCredentials: jest.fn(), +})); + +describe('ClientTraking', () => { + const apiClientMock = { + baseUrl: 'https://example.com', + get: jest.fn(), + post: jest.fn(), + put: jest.fn(), + patch: jest.fn(), + delete: jest.fn(), + }; + + let client: ClientTracking; + + beforeEach(() => { + jest.clearAllMocks(); + client = new ClientTracking(apiClientMock as unknown as APIClientInterface); + }); + + it('should set bearer token', () => { + const token = 'testToken'; + client.setBearerToken(token); + + expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} ${token}`); + expect(require('@init/credentials').setServerCredentials).toHaveBeenCalledWith(apiClientMock.baseUrl, token); + }); + + it('should set CSRF token', () => { + const token = 'csrfToken'; + client.setCSRFToken(token); + + expect(client.csrfToken).toBe(token); + }); + + it('should get request headers', () => { + client.setCSRFToken('csrfToken'); + client.setBearerToken('testToken'); + + const headers = client.getRequestHeaders('POST'); + expect(headers[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} testToken`); + expect(headers[ClientConstants.HEADER_X_CSRF_TOKEN]).toBe('csrfToken'); + }); + + it('should initialize and track group data', () => { + client.initTrackGroup('testGroup'); + expect(client.requestGroups.has('testGroup')).toBe(true); + + client.trackRequest('testGroup', 'https://example.com/api', {size: 100, compressedSize: 50} as ClientResponseMetrics); + const group = client.requestGroups.get('testGroup')!; + 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')!; + expect(group.activeCount).toBe(1); + + client.decrementRequestCount('testGroup'); + group = client.requestGroups.get('testGroup')!; + expect(group.activeCount).toBe(0); + }); + + it('should handle request completion', () => { + client.initTrackGroup('testGroup'); + client.handleRequestCompletion('testGroup'); + + expect(require('@utils/log').logDebug).toHaveBeenCalledWith('Group "testGroup" completed.'); + }); + + it('should clear completion timer', () => { + client.initTrackGroup('testGroup'); + const group = client.requestGroups.get('testGroup')!; + group.completionTimer = setTimeout(() => {}, 100); + + client.clearCompletionTimer('testGroup'); + expect(group.completionTimer).toBeUndefined(); + }); + + it('should check if all requests are completed', () => { + client.initTrackGroup('testGroup'); + const result = client.allRequestsCompleted('testGroup'); + expect(result).toBe(true); + }); + + it('should send telemetry event', () => { + client.initTrackGroup('testGroup'); + const group = client.requestGroups.get('testGroup')!; + 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}, + }, + }; + + client.sendTelemetryEvent('testGroup', group, 1000); + expect(require('@utils/log').logInfo).toHaveBeenCalled(); + }); + + it('should build request options', () => { + const options = { + body: {key: 'value'}, + method: 'POST', + noRetry: true, + timeoutInterval: 5000, + headers: {custom: 'header'}, + }; + + const result = client.buildRequestOptions(options); + expect(result.headers?.custom).toBe('header'); + expect(result.retryPolicyConfiguration?.retryLimit).toBe(0); + expect(result.timeoutInterval).toBe(5000); + }); + + it('should perform fetch with tracking', async () => { + apiClientMock.get.mockResolvedValue({ + ok: true, + data: {success: true}, + headers: {}, + }); + + const options = { + method: 'GET', + groupLabel: 'testGroup', + }; + + const result = await client.doFetchWithTracking('https://example.com/api', options); + expect(result).toEqual({success: true}); + }); + + it('should handle fetch errors', async () => { + apiClientMock.get.mockRejectedValue(new Error('Request failed')); + + const options = { + method: 'GET', + groupLabel: 'testGroup', + }; + + await expect(client.doFetchWithTracking('https://example.com/api', options)).rejects.toThrow('Received invalid response from the server.'); + }); + + 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'); + const handleRequestCompletionSpy = jest.spyOn(client, 'handleRequestCompletion'); + + apiClientMock.get.mockImplementation(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + ok: true, + data: {success: true}, + headers: {}, + metrics: {latency: 200, size: 500, compressedSize: 300}, + }); + }, 200); // Simulate a 100ms network delay + }); + }); + + client.initTrackGroup('testGroup'); + + // 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'}), + ]; + + // Wait for all fetches to resolve + await Promise.all(promises); + + const group = client.requestGroups.get('testGroup')!; + expect(group.activeCount).toBe(0); + + await test_helper.wait(100); + + expect(handleRequestCompletionSpy).toHaveBeenCalledTimes(1); + expect(incrementSpy).toHaveBeenCalledTimes(3); + expect(decrementSpy).toHaveBeenCalledTimes(3); + }); + + 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}, + }); + + client.initTrackGroup('testGroup'); + + // 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'}), + ]; + + // Wait for all fetches to resolve + await Promise.all(promises); + + // Verify that the group tracked duplicates correctly + const group = client.requestGroups.get('testGroup')!; + 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); + + // Verify duplicate logging + expect(logDebugSpy).toHaveBeenCalledWith( + 'Duplicate URLs:\n', + expect.stringContaining('https://example.com/api'), + ); + }); +}); diff --git a/app/client/rest/tracking.ts b/app/client/rest/tracking.ts new file mode 100644 index 00000000000..cedbfe32e84 --- /dev/null +++ b/app/client/rest/tracking.ts @@ -0,0 +1,263 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {DeviceEventEmitter, Platform} from 'react-native'; + +import {Events} from '@constants'; +import {t} from '@i18n'; +import {setServerCredentials} from '@init/credentials'; +import {getFormattedFileSize} from '@utils/file'; +import {logDebug, logInfo} from '@utils/log'; +import {semverFromServerVersion} from '@utils/server'; + +import * as ClientConstants from './constants'; +import ClientError from './error'; + +import type {APIClientInterface, ClientHeaders, ClientResponseMetrics, RequestOptions} from '@mattermost/react-native-network-client'; + +type UrlData = { + count: number; + metrics?: ClientResponseMetrics; +} + +type GroupData = { + activeCount: number; + startTime: number; + totalSize: number; + totalCompressedSize: number; + urls: Record; + completionFlag: boolean; + completionTimer?: NodeJS.Timeout; +} + +export default class ClientTracking { + apiClient: APIClientInterface; + csrfToken = ''; + requestHeaders: {[x: string]: string} = {}; + serverVersion = ''; + urlVersion = '/api/v4'; + enableLogging = false; + + requestGroups: Map = new Map(); + + constructor(apiClient: APIClientInterface) { + this.apiClient = apiClient; + } + + setBearerToken(bearerToken: string) { + this.requestHeaders[ClientConstants.HEADER_AUTH] = `${ClientConstants.HEADER_BEARER} ${bearerToken}`; + setServerCredentials(this.apiClient.baseUrl, bearerToken); + } + + setCSRFToken(csrfToken: string) { + this.csrfToken = csrfToken; + } + + getRequestHeaders(requestMethod: string) { + const headers = {...this.requestHeaders}; + + if (this.csrfToken && requestMethod.toLowerCase() !== 'get') { + headers[ClientConstants.HEADER_X_CSRF_TOKEN] = this.csrfToken; + } + + return headers; + } + + initTrackGroup(groupLabel: string) { + if (!this.requestGroups.has(groupLabel)) { + this.requestGroups.set(groupLabel, { + activeCount: 0, + startTime: Date.now(), + totalSize: 0, + totalCompressedSize: 0, + urls: {}, + completionFlag: false, + }); + } + } + + trackRequest(groupLabel: string, url: string, metrics?: ClientResponseMetrics) { + this.initTrackGroup(groupLabel); + + const group = this.requestGroups.get(groupLabel)!; + + if (group.urls[url]) { + group.urls[url].count += 1; + } else { + group.urls[url] = { + count: 1, + metrics, + }; + } + group.totalSize += metrics?.size ?? 0; + group.totalCompressedSize += metrics?.compressedSize ?? 0; + } + + incrementRequestCount(groupLabel: string) { + this.initTrackGroup(groupLabel); + + const group = this.requestGroups.get(groupLabel)!; + group.activeCount += 1; + } + + decrementRequestCount(groupLabel: string) { + const group = this.requestGroups.get(groupLabel); + if (group) { + group.activeCount -= 1; + + if (group.activeCount <= 0 && !group.completionFlag) { + this.clearCompletionTimer(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? + } + } + } + + clearCompletionTimer(groupLabel: string) { + const group = this.requestGroups.get(groupLabel); + if (group?.completionTimer) { + clearTimeout(group.completionTimer); + group.completionTimer = undefined; + } + } + + handleRequestCompletion(groupLabel: string) { + const group = this.requestGroups.get(groupLabel); + if (group) { + const duration = Date.now() - group.startTime; + + logDebug(`Group "${groupLabel}" completed.`); + this.sendTelemetryEvent(groupLabel, group, duration); + + this.requestGroups.delete(groupLabel); + } + } + + allRequestsCompleted(groupLabel: string): 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); + 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`); + + if (dupe.length) { + logDebug('Duplicate URLs:\n', dupe.map((d) => `${d[0]} ${JSON.stringify(d[1])}`).join('\n')); + } + + // Integrate with telemetry framework here + } + + buildRequestOptions(options: ClientOptions): RequestOptions { + const requestOptions: RequestOptions = { + body: options.body, + headers: this.getRequestHeaders(options.method!.toLowerCase()), + }; + if (options.noRetry) { + requestOptions.retryPolicyConfiguration = {retryLimit: 0}; + } + if (options.timeoutInterval) { + requestOptions.timeoutInterval = options.timeoutInterval; + } + if (options.headers) { + requestOptions.headers = {...requestOptions.headers, ...options.headers}; + } + return requestOptions; + } + + doFetchWithTracking = async (url: string, options: ClientOptions, returnDataOnly = true) => { + let request; + const {groupLabel} = options; + const method = options.method?.toLowerCase(); + switch (method) { + case 'get': request = this.apiClient!.get; + break; + case 'put': request = this.apiClient!.put; + break; + case 'post': request = this.apiClient!.post; + break; + case 'patch': request = this.apiClient!.patch; + break; + case 'delete': request = this.apiClient!.delete; + break; + default: + return {error: new ClientError(this.apiClient.baseUrl, { + message: 'Invalid request method', + intl: { + id: t('mobile.request.invalid_request_method'), + defaultMessage: 'Invalid request method', + }, + url, + })}; + } + + if (groupLabel) { + this.incrementRequestCount(groupLabel); + } + + try { + const response = await request!(url, this.buildRequestOptions(options)); + const headers: ClientHeaders = response.headers || {}; + if (groupLabel) { + this.trackRequest(groupLabel, url, response.metrics); + } + const serverVersion = semverFromServerVersion( + headers[ClientConstants.HEADER_X_VERSION_ID] || headers[ClientConstants.HEADER_X_VERSION_ID.toLowerCase()], + ); + const hasCacheControl = Boolean( + headers[ClientConstants.HEADER_CACHE_CONTROL] || headers[ClientConstants.HEADER_CACHE_CONTROL.toLowerCase()], + ); + if (serverVersion && !hasCacheControl && this.serverVersion !== serverVersion) { + this.serverVersion = serverVersion; + DeviceEventEmitter.emit(Events.SERVER_VERSION_CHANGED, {serverUrl: this.apiClient.baseUrl, serverVersion}); + } + + const bearerToken = headers[ClientConstants.HEADER_TOKEN] || headers[ClientConstants.HEADER_TOKEN.toLowerCase()]; + if (bearerToken) { + this.setBearerToken(bearerToken); + } + + if (response.ok) { + return returnDataOnly ? (response.data || {}) : response; + } + + throw new ClientError(this.apiClient.baseUrl, { + message: response.data?.message as string || `Response with status code ${response.code}`, + server_error_id: response.data?.id as string, + status_code: response.code, + url, + }); + } catch (error) { + throw new ClientError(this.apiClient.baseUrl, { + message: 'Received invalid response from the server.', + intl: { + id: t('mobile.request.invalid_response'), + defaultMessage: 'Received invalid response from the server.', + }, + url, + details: error, + }); + } finally { + if (groupLabel) { + this.decrementRequestCount(groupLabel); + } + } + }; +} diff --git a/app/client/rest/users.ts b/app/client/rest/users.ts index 79eeef851ef..5b0fcac9b25 100644 --- a/app/client/rest/users.ts +++ b/app/client/rest/users.ts @@ -10,7 +10,7 @@ import type ClientBase from './base'; export interface ClientUsersMix { createUser: (user: UserProfile, token: string, inviteId: string) => Promise; - patchMe: (userPatch: Partial) => Promise; + patchMe: (userPatch: Partial, groupLabel?: string) => Promise; patchUser: (userPatch: Partial & {id: string}) => Promise; updateUser: (user: UserProfile) => Promise; demoteUserToGuest: (userId: string) => Promise; @@ -21,15 +21,15 @@ export interface ClientUsersMix { loginById: (id: string, password: string, token?: string, deviceId?: string) => Promise; logout: () => Promise; getProfiles: (page?: number, perPage?: number, options?: Record) => Promise; - getProfilesByIds: (userIds: string[], options?: Record) => Promise; - getProfilesByUsernames: (usernames: string[]) => Promise; + getProfilesByIds: (userIds: string[], options?: Record, groupLabel?: string) => Promise; + getProfilesByUsernames: (usernames: string[], groupLabel?: string) => Promise; getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record) => Promise; getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise; getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record) => Promise; - getProfilesInChannel: (channelId: string, options?: GetUsersOptions) => Promise; - getProfilesInGroupChannels: (channelsIds: string[]) => Promise<{[x: string]: UserProfile[]}>; + getProfilesInChannel: (channelId: string, options?: GetUsersOptions, groupLabel?: string) => Promise; + getProfilesInGroupChannels: (channelsIds: string[], groupLabel?: string) => Promise<{[x: string]: UserProfile[]}>; getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise; - getMe: () => Promise; + getMe: (groupLabel?: string) => Promise; getUser: (userId: string) => Promise; getUserByUsername: (username: string) => Promise; getUserByEmail: (email: string) => Promise; @@ -38,10 +38,10 @@ export interface ClientUsersMix { autocompleteUsers: (name: string, teamId: string, channelId?: string, options?: Record) => Promise<{users: UserProfile[]; out_of_channel?: UserProfile[]}>; getSessions: (userId: string) => Promise; checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>; - setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null) => Promise<{}>; + setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null, groupLabel?: string) => Promise<{}>; searchUsers: (term: string, options: SearchUserOptions) => Promise; getStatusesByIds: (userIds: string[]) => Promise; - getStatus: (userId: string) => Promise; + getStatus: (userId: string, groupLabel?: string) => Promise; updateStatus: (status: UserStatus) => Promise; updateCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>; unsetCustomStatus: () => Promise<{status: string}>; @@ -66,10 +66,10 @@ const ClientUsers = >(superclass: TBase) = ); }; - patchMe = async (userPatch: Partial) => { + patchMe = async (userPatch: Partial, groupLabel?: string) => { return this.doFetch( `${this.getUserRoute('me')}/patch`, - {method: 'put', body: userPatch}, + {method: 'put', body: userPatch, groupLabel}, ); }; @@ -127,7 +127,7 @@ const ClientUsers = >(superclass: TBase) = body.ldap_only = 'true'; } - const {data} = await this.doFetch( + const resp = await this.doFetch( `${this.getUsersRoute()}/login`, { method: 'post', @@ -137,7 +137,7 @@ const ClientUsers = >(superclass: TBase) = false, ); - return data; + return resp?.data; }; loginById = async (id: string, password: string, token = '', deviceId = '') => { @@ -148,7 +148,7 @@ const ClientUsers = >(superclass: TBase) = token, }; - const {data} = await this.doFetch( + const resp = await this.doFetch( `${this.getUsersRoute()}/login`, { method: 'post', @@ -158,7 +158,7 @@ const ClientUsers = >(superclass: TBase) = false, ); - return data; + return resp?.data; }; logout = async () => { @@ -177,17 +177,17 @@ const ClientUsers = >(superclass: TBase) = ); }; - getProfilesByIds = async (userIds: string[], options = {}) => { + getProfilesByIds = async (userIds: string[], options = {}, groupLabel?: string) => { return this.doFetch( `${this.getUsersRoute()}/ids${buildQueryString(options)}`, - {method: 'post', body: userIds}, + {method: 'post', body: userIds, groupLabel}, ); }; - getProfilesByUsernames = async (usernames: string[]) => { + getProfilesByUsernames = async (usernames: string[], groupLabel?: string) => { return this.doFetch( `${this.getUsersRoute()}/usernames`, - {method: 'post', body: usernames}, + {method: 'post', body: usernames, groupLabel}, ); }; @@ -217,18 +217,18 @@ const ClientUsers = >(superclass: TBase) = ); }; - getProfilesInChannel = async (channelId: string, options: GetUsersOptions) => { + getProfilesInChannel = async (channelId: string, options: GetUsersOptions, groupLabel?: string) => { const queryStringObj = {in_channel: channelId, ...options}; return this.doFetch( `${this.getUsersRoute()}${buildQueryString(queryStringObj)}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; - getProfilesInGroupChannels = async (channelsIds: string[]) => { + getProfilesInGroupChannels = async (channelsIds: string[], groupLabel?: string) => { return this.doFetch( `${this.getUsersRoute()}/group_channels`, - {method: 'post', body: channelsIds}, + {method: 'post', body: channelsIds, groupLabel}, ); }; @@ -244,10 +244,10 @@ const ClientUsers = >(superclass: TBase) = ); }; - getMe = async () => { + getMe = async (groupLabel?: string) => { return this.doFetch( `${this.getUserRoute('me')}`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -325,7 +325,7 @@ const ClientUsers = >(superclass: TBase) = ); }; - setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null) => { + setExtraSessionProps = async (deviceId: string, deviceNotificationDisabled: boolean, version: string | null, groupLabel?: string) => { return this.doFetch( `${this.getUsersRoute()}/sessions/device`, { @@ -335,6 +335,7 @@ const ClientUsers = >(superclass: TBase) = device_notification_disabled: deviceNotificationDisabled ? 'true' : 'false', mobile_version: version || '', }, + groupLabel, }, ); }; @@ -353,10 +354,10 @@ const ClientUsers = >(superclass: TBase) = ); }; - getStatus = async (userId: string) => { + getStatus = async (userId: string, groupLabel?: string) => { return this.doFetch( `${this.getUserRoute(userId)}/status`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; diff --git a/app/helpers/database/index.ts b/app/helpers/database/index.ts index 0fd7082d14d..330296729f5 100644 --- a/app/helpers/database/index.ts +++ b/app/helpers/database/index.ts @@ -94,6 +94,10 @@ const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g'); export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_'); export function removeDuplicatesModels(array: Model[]) { + if (!array.length) { + return array; + } + const seen = new Set(); return array.filter((item) => { const key = `${item.collection.table}-${item.id}`; diff --git a/app/init/launch.ts b/app/init/launch.ts index 2fee4a45def..ddaf0801286 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -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!); + return pushNotificationEntry(props.serverUrl!, extra.payload!, 'notification'); } appEntry(props.serverUrl!); diff --git a/app/managers/apps_manager.ts b/app/managers/apps_manager.ts index aa2c4f48ba9..d98462e8865 100644 --- a/app/managers/apps_manager.ts +++ b/app/managers/apps_manager.ts @@ -103,7 +103,7 @@ class AppsManager { } }; - fetchBindings = async (serverUrl: string, channelId: string, forThread = false) => { + fetchBindings = async (serverUrl: string, channelId: string, forThread = false, groupLabel?: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const userId = await getCurrentUserId(database); @@ -114,7 +114,7 @@ class AppsManager { } const client = NetworkManager.getClient(serverUrl); - const fetchedBindings = await client.getAppsBindings(userId, channelId, teamId); + const fetchedBindings = await client.getAppsBindings(userId, channelId, teamId, groupLabel); const validatedBindings = validateBindings(fetchedBindings); const bindingsToStore = validatedBindings.length ? validatedBindings : emptyBindings; @@ -135,7 +135,7 @@ class AppsManager { } }; - refreshAppBindings = async (serverUrl: string) => { + refreshAppBindings = async (serverUrl: string, groupLabel?: string) => { try { const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const appsEnabled = (await getConfig(database))?.FeatureFlagAppsEnabled === 'true'; @@ -147,11 +147,11 @@ class AppsManager { const channelId = await getCurrentChannelId(database); // We await here, since errors on this call may clear the thread bindings - await this.fetchBindings(serverUrl, channelId); + await this.fetchBindings(serverUrl, channelId, false, groupLabel); const threadChannelId = this.getThreadsBindingsSubject(serverUrl).value.channelId; if (threadChannelId) { - await this.fetchBindings(serverUrl, threadChannelId, true); + await this.fetchBindings(serverUrl, threadChannelId, true, groupLabel); } } catch (error) { logDebug('Error refreshing apps', error); diff --git a/app/managers/network_manager.ts b/app/managers/network_manager.ts index b5bd94e1b4a..bd6fe1ff645 100644 --- a/app/managers/network_manager.ts +++ b/app/managers/network_manager.ts @@ -62,6 +62,7 @@ class NetworkManager { waitsForConnectivity: false, httpMaximumConnectionsPerHost: 100, cancelRequestsOnUnauthorized: true, + collectMetrics: false, }, retryPolicyConfiguration: { type: RetryTypes.EXPONENTIAL_RETRY, @@ -134,6 +135,7 @@ class NetworkManager { timeoutIntervalForRequest: managedConfig?.timeout ? parseInt(managedConfig.timeout, 10) : this.DEFAULT_CONFIG.sessionConfiguration?.timeoutIntervalForRequest, timeoutIntervalForResource: managedConfig?.timeoutVPN ? parseInt(managedConfig.timeoutVPN, 10) : this.DEFAULT_CONFIG.sessionConfiguration?.timeoutIntervalForResource, waitsForConnectivity: managedConfig?.useVPN === 'true', + collectMetrics: LocalConfig.CollectNetworkMetrics, }, headers, }; diff --git a/app/managers/websocket_manager.ts b/app/managers/websocket_manager.ts index f26913595bd..95ab1a722c4 100644 --- a/app/managers/websocket_manager.ts +++ b/app/managers/websocket_manager.ts @@ -109,16 +109,16 @@ class WebsocketManager { } }; - public openAll = async () => { + public openAll = async (groupLabel?: string) => { let queued = 0; for await (const clientUrl of Object.keys(this.clients)) { const activeServerUrl = await DatabaseManager.getActiveServerUrl(); if (clientUrl === activeServerUrl) { - this.initializeClient(clientUrl); + this.initializeClient(clientUrl, groupLabel); } else { queued += 1; this.getConnectedSubject(clientUrl).next('connecting'); - this.connectionTimerIDs[clientUrl] = setTimeout(() => this.initializeClient(clientUrl), WAIT_UNTIL_NEXT * queued); + this.connectionTimerIDs[clientUrl] = setTimeout(() => this.initializeClient(clientUrl, groupLabel), WAIT_UNTIL_NEXT * queued); } } }; @@ -148,7 +148,7 @@ class WebsocketManager { } }; - public initializeClient = async (serverUrl: string) => { + public initializeClient = async (serverUrl: string, groupLabel = 'reconnection') => { const client: WebSocketClient = this.clients[serverUrl]; clearTimeout(this.connectionTimerIDs[serverUrl]); delete this.connectionTimerIDs[serverUrl]; @@ -156,7 +156,7 @@ class WebsocketManager { const hasSynced = this.firstConnectionSynced[serverUrl]; client.initialize({}, !hasSynced); if (!hasSynced) { - const error = await handleFirstConnect(serverUrl); + const error = await handleFirstConnect(serverUrl, groupLabel); if (error) { // This will try to reconnect and try to sync again client.close(false); @@ -271,7 +271,7 @@ class WebsocketManager { } this.isBackgroundTimerRunning = false; if (this.netConnected) { - this.openAll(); + this.openAll('reconnection'); } return; diff --git a/app/products/calls/actions/calls.test.ts b/app/products/calls/actions/calls.test.ts index 5be7c259c02..9590e4804ce 100644 --- a/app/products/calls/actions/calls.test.ts +++ b/app/products/calls/actions/calls.test.ts @@ -390,9 +390,9 @@ describe('Actions.Calls', () => { const {result} = renderHook(() => useCallsConfig('server1')); await act(async () => { - await CallsActions.loadConfig('server1'); + await CallsActions.loadConfig('server1', false, 'calls'); }); - expect(mockClient.getCallsConfig).toHaveBeenCalledWith(); + expect(mockClient.getCallsConfig).toHaveBeenCalledWith('calls'); assert.equal(result.current.DefaultEnabled, true); assert.equal(result.current.AllowEnableCalls, true); }); diff --git a/app/products/calls/actions/calls.ts b/app/products/calls/actions/calls.ts index 2c7d6b2947a..860ce378b8e 100644 --- a/app/products/calls/actions/calls.ts +++ b/app/products/calls/actions/calls.ts @@ -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) => { +export const loadConfig = async (serverUrl: string, force = false, groupLabel?: string) => { const now = Date.now(); const config = getCallsConfig(serverUrl); @@ -66,7 +66,7 @@ export const loadConfig = async (serverUrl: string, force = false) => { try { const client = NetworkManager.getClient(serverUrl); - const configs = await Promise.all([client.getCallsConfig(), client.getVersion()]); + const configs = await Promise.all([client.getCallsConfig(groupLabel), client.getVersion(groupLabel)]); const nextConfig = {...configs[0], version: configs[1], last_retrieved_at: now}; setConfig(serverUrl, nextConfig); return {data: nextConfig}; @@ -77,11 +77,11 @@ export const loadConfig = async (serverUrl: string, force = false) => { } }; -export const loadCalls = async (serverUrl: string, userId: string) => { +export const loadCalls = async (serverUrl: string, userId: string, groupLabel?: string) => { let resp: CallChannelState[] = []; try { const client = NetworkManager.getClient(serverUrl); - resp = await client.getCalls() || []; + resp = await client.getCalls(groupLabel) || []; } catch (error) { logDebug('error on loadCalls', getFullErrorMessage(error)); await forceLogoutIfNecessary(serverUrl, error); @@ -104,7 +104,7 @@ export const loadCalls = async (serverUrl: string, userId: string) => { // Batch load user models async because we'll need them later if (ids.size > 0) { - fetchUsersByIds(serverUrl, Array.from(ids)); + fetchUsersByIds(serverUrl, Array.from(ids), false, groupLabel); } setCalls(serverUrl, userId, callsResults, enabledChannels); @@ -192,11 +192,11 @@ export const createCallAndAddToIds = (channelId: string, call: CallState, ids?: return convertedCall; }; -export const loadConfigAndCalls = async (serverUrl: string, userId: string) => { +export const loadConfigAndCalls = async (serverUrl: string, userId: string, groupLabel?: string) => { const res = await checkIsCallsPluginEnabled(serverUrl); if (res.data) { - loadConfig(serverUrl, true); - loadCalls(serverUrl, userId); + loadConfig(serverUrl, true, groupLabel); + loadCalls(serverUrl, userId, groupLabel); } }; diff --git a/app/products/calls/client/rest.ts b/app/products/calls/client/rest.ts index 22eb1153d8c..9937bec4592 100644 --- a/app/products/calls/client/rest.ts +++ b/app/products/calls/client/rest.ts @@ -7,10 +7,10 @@ import type {RTCIceServer} from 'react-native-webrtc'; export interface ClientCallsMix { getEnabled: () => Promise; - getCalls: () => Promise; + getCalls: (groupLabel?: string) => Promise; getCallForChannel: (channelId: string) => Promise; - getCallsConfig: () => Promise; - getVersion: () => Promise; + getCallsConfig: (groupLabel?: string) => Promise; + getVersion: (groupLabel?: string) => Promise; enableChannelCalls: (channelId: string, enable: boolean) => Promise; endCall: (channelId: string) => Promise; genTURNCredentials: () => Promise; @@ -38,10 +38,10 @@ const ClientCalls = (superclass: any) => class extends superclass { } }; - getCalls = async () => { + getCalls = async (groupLabel?: string) => { return this.doFetch( `${this.getCallsRoute()}/channels?mobilev2=true`, - {method: 'get'}, + {method: 'get', groupLabel}, ); }; @@ -52,18 +52,18 @@ const ClientCalls = (superclass: any) => class extends superclass { ); }; - getCallsConfig = async () => { + getCallsConfig = async (groupLabel?: string) => { return this.doFetch( `${this.getCallsRoute()}/config`, - {method: 'get'}, + {method: 'get', groupLabel}, ) as CallsConfig; }; - getVersion = async () => { + getVersion = async (groupLabel?: string) => { try { return this.doFetch( `${this.getCallsRoute()}/version`, - {method: 'get'}, + {method: 'get', groupLabel}, ); } catch (e) { return {}; diff --git a/app/products/calls/components/call_notification/call_notification.tsx b/app/products/calls/components/call_notification/call_notification.tsx index 81491f46b1a..fbd47a66cbe 100644 --- a/app/products/calls/components/call_notification/call_notification.tsx +++ b/app/products/calls/components/call_notification/call_notification.tsx @@ -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); + await WebsocketManager.initializeClient(incomingCall.serverUrl, 'calls'); } switchToChannelById(incomingCall.serverUrl, incomingCall.channelID); }, [incomingCall, serverUrl]); diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index 9af21a5eb97..15b099dd9a3 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -466,7 +466,7 @@ const CallScreen = ({ await popTopScreen(Screens.THREAD); } await DatabaseManager.setActiveServerDatabase(currentCall.serverUrl); - WebsocketManager.initializeClient(currentCall.serverUrl); + WebsocketManager.initializeClient(currentCall.serverUrl, 'calls'); await goToScreen(Screens.THREAD, callThreadOptionTitle, {rootId: currentCall.threadId}); }, [currentCall?.serverUrl, currentCall?.threadId, fromThreadScreen, componentId, callThreadOptionTitle]); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx index 795f52d1cd5..27d0c6efd14 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx @@ -311,7 +311,7 @@ const ServerItem = ({ await dismissBottomSheet(); Navigation.updateProps(Screens.HOME, {extra: undefined}); DatabaseManager.setActiveServerDatabase(server.url); - WebsocketManager.initializeClient(server.url); + WebsocketManager.initializeClient(server.url, 'entry'); return; } diff --git a/app/utils/deep_link/index.test.ts b/app/utils/deep_link/index.test.ts index ec18e833e68..e2d97452c84 100644 --- a/app/utils/deep_link/index.test.ts +++ b/app/utils/deep_link/index.test.ts @@ -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'); + expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('https://existingserver.com', 'deeplink'); expect(result).toEqual({error: false}); }); diff --git a/app/utils/deep_link/index.ts b/app/utils/deep_link/index.ts index ac29edf7312..1e522342052 100644 --- a/app/utils/deep_link/index.ts +++ b/app/utils/deep_link/index.ts @@ -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); + WebsocketManager.initializeClient(existingServerUrl, 'deeplink'); await NavigationStore.waitUntilScreenHasLoaded(Screens.HOME); } diff --git a/assets/base/config.json b/assets/base/config.json index d5a44b4cc4c..2ae360213a6 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -28,5 +28,6 @@ "ShowOnboarding": false, "ExperimentalNormalizeMarkdownLinks": false, - "CustomRequestHeaders": {} + "CustomRequestHeaders": {}, + "CollectNetworkMetrics": false } diff --git a/package-lock.json b/package-lock.json index 3db8b9e0836..1c1fc44f973 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@mattermost/compass-icons": "0.1.45", "@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard", "@mattermost/react-native-emm": "1.5.0", - "@mattermost/react-native-network-client": "1.7.3", + "@mattermost/react-native-network-client": "github:mattermost/react-native-network-client#ad7b88412f41c5a1024420a4f4c7461883cd0e63", "@mattermost/react-native-paste-input": "0.8.0", "@mattermost/react-native-turbo-log": "github:mattermost/react-native-turbo-log#d8ddf5e7974546aff3e83b2c563907eb609ac5f4", "@mattermost/rnshare": "file:./libraries/@mattermost/rnshare", @@ -5891,8 +5891,8 @@ }, "node_modules/@mattermost/react-native-network-client": { "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.7.3.tgz", - "integrity": "sha512-gJSQ4Prf5jcbPlxVylhEtBGafSBYZSat+T21LIDHksGOfeY+jvPL3p8dS+cq+0D1AOHQ3tHNBkZ7RaY6IdUadw==", + "resolved": "git+ssh://git@github.com/mattermost/react-native-network-client.git#ad7b88412f41c5a1024420a4f4c7461883cd0e63", + "integrity": "sha512-vCKDx7hQowcY0AkhKr4vk1CTMXHaOiPuA4fqvDAepD3RjdfoMdnz1VYXizSuj5pjHKWbuFp+D+L/sL/1G/FUlw==", "license": "MIT", "dependencies": { "validator": "13.12.0", diff --git a/package.json b/package.json index cdfb35fbd84..63b8be0eabb 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@mattermost/compass-icons": "0.1.45", "@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard", "@mattermost/react-native-emm": "1.5.0", - "@mattermost/react-native-network-client": "1.7.3", + "@mattermost/react-native-network-client": "github:mattermost/react-native-network-client#ad7b88412f41c5a1024420a4f4c7461883cd0e63", "@mattermost/react-native-paste-input": "0.8.0", "@mattermost/react-native-turbo-log": "github:mattermost/react-native-turbo-log#d8ddf5e7974546aff3e83b2c563907eb609ac5f4", "@mattermost/rnshare": "file:./libraries/@mattermost/rnshare", diff --git a/types/api/client.d.ts b/types/api/client.d.ts index eeceeb39ae9..33ca82f8d14 100644 --- a/types/api/client.d.ts +++ b/types/api/client.d.ts @@ -9,6 +9,7 @@ type ClientOptions = { noRetry?: boolean; timeoutInterval?: number; headers?: Record; + groupLabel?: string; }; type ClientErrorIntl =