Network metrics (#8390)

* Network metrics

* update network-client ref

* fix unit tests

* missing catch error parsing

* add client tracking unit tests

* fix typos
This commit is contained in:
Elias Nahum 2024-12-17 19:51:26 +08:00 committed by GitHub
parent 640ff93e01
commit fb57c423c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 945 additions and 466 deletions

View file

@ -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<MyChannelsRequest> {
export async function fetchMyChannelsForTeam(
serverUrl: string, teamId: string, includeDeleted = true,
since = 0, fetchOnly = false, excludeDirect = false,
isCRTEnabled?: boolean, groupLabel?: string,
): Promise<MyChannelsRequest> {
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<MyChannelsRequest> {
export async function fetchMyChannel(serverUrl: string, teamId: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise<MyChannelsRequest> {
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<string, string> = {};
@ -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 {};

View file

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

View file

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

View file

@ -66,14 +66,14 @@ export type EntryResponse = {
error: unknown;
}
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0): Promise<EntryResponse> => {
export const entry = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise<EntryResponse> => {
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<EntryResponse> => {
const entryRest = async (serverUrl: string, teamId?: string, channelId?: string, since = 0, groupLabel?: string): Promise<EntryResponse> => {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
@ -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<AppEntryData | AppEntryError> => {
const fetchAppEntryData = async (serverUrl: string, sinceArg: number, onLoadTeamId = '', channelId?: string, groupLabel?: string): Promise<AppEntryData | AppEntryError> => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
@ -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<MyTeamsRequest>, Promise<MyUserRequest>] = [
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:

View file

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

View file

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

View file

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

View file

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

View file

@ -284,7 +284,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {};
};
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false): Promise<PostsForChannel> {
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false, groupLabel?: string): Promise<PostsForChannel> {
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<PostsForChannel[]> => {
export const fetchPostsForUnreadChannels = async (
serverUrl: string, channels: Channel[], memberships: ChannelMembership[],
excludeChannelId?: string, fetchOnly = false, groupLabel?: string,
): Promise<PostsForChannel[]> => {
const membersMap = new Map<string, ChannelMembership>();
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<PostsRequest> {
export async function fetchPosts(serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false, groupLabel?: string): Promise<PostsRequest> {
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<PostsRequest> {
export async function fetchPostsSince(serverUrl: string, channelId: string, since: number, fetchOnly = false, groupLabel?: string): Promise<PostsRequest> {
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<AuthorsRequest> => {
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false, groupLabel?: string): Promise<AuthorsRequest> => {
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<Promise<UserProfile[]>> = [];
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],

View file

@ -28,12 +28,12 @@ export type MyPreferencesRequest = {
error?: unknown;
};
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false): Promise<MyPreferencesRequest> => {
export const fetchMyPreferences = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyPreferencesRequest> => {
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) => {

View file

@ -15,7 +15,10 @@ export type RolesRequest = {
roles?: Role[];
}
export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string[], fetchOnly = false, force = false): Promise<RolesRequest> => {
export const fetchRolesIfNeeded = async (
serverUrl: string, updatedRoles: string[],
fetchOnly = false, force = false, groupLabel?: string,
): Promise<RolesRequest> => {
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<string>(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: []};

View file

@ -22,9 +22,9 @@ export type DataRetentionPoliciesRequest = {
error?: unknown;
}
export const fetchDataRetentionPolicy = async (serverUrl: string, fetchOnly = false): Promise<DataRetentionPoliciesRequest> => {
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<DataRetentionPoliciesRequest> => {
const {data: globalPolicy, error: globalPolicyError} = await fetchGlobalDataRetentionPolicy(serverUrl, groupLabel);
const {data: teamPolicies, error: teamPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, undefined, undefined, undefined, groupLabel);
const {data: channelPolicies, error: channelPoliciesError} = await fetchAllGranularDataRetentionPolicies(serverUrl, true);
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<TeamDataRetentionPolicy | ChannelDataRetentionPolicy> = [],
groupLabel?: string,
): Promise<{data?: Array<TeamDataRetentionPolicy | ChannelDataRetentionPolicy>; 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<ConfigAndLicenseRequest> => {
export const fetchConfigAndLicense = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<ConfigAndLicenseRequest> => {
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) {

View file

@ -161,14 +161,14 @@ export async function sendEmailInvitesToTeam(serverUrl: string, teamId: string,
}
}
export async function fetchMyTeams(serverUrl: string, fetchOnly = false): Promise<MyTeamsRequest> {
export async function fetchMyTeams(serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyTeamsRequest> {
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<MyTeamsRequest> {
export async function fetchMyTeam(serverUrl: string, teamId: string, fetchOnly = false, groupLabel?: string): Promise<MyTeamsRequest> {
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<string>, page: number): Promise<boolean> => {
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT);
const recCanJoinTeams = async (client: Client, myTeamsIds: Set<string>, page: number, groupLabel?: string): Promise<boolean> => {
const fetchedTeams = await client.getTeams(page, PER_PAGE_DEFAULT, false, groupLabel);
if (fetchedTeams.find((t) => !myTeamsIds.has(t.id) && t.delete_at === 0)) {
return true;
}
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<PostsForChannel[]>, Promise<{models?: Model[]; error?: unknown}>] = [
unreadPromise,
threadsPromise,

View file

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

View file

@ -43,12 +43,12 @@ export type ProfilesInChannelRequest = {
error?: unknown;
}
export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyUserRequest> => {
export const fetchMe = async (serverUrl: string, fetchOnly = false, groupLabel?: string): Promise<MyUserRequest> => {
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<ProfilesInChannelRequest> {
export async function fetchProfilesInChannel(
serverUrl: string, channelId: string, excludeUserId?: string, options?: GetUsersOptions,
fetchOnly = false, groupLabel?: string,
): Promise<ProfilesInChannelRequest> {
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<ProfilesPerChannelRequest> {
export async function fetchProfilesInGroupChannels(serverUrl: string, groupChannelIds: string[], fetchOnly = false, groupLabel?: string): Promise<ProfilesPerChannelRequest> {
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<ProfilesPerChannelRequest> {
export async function fetchProfilesPerChannels(
serverUrl: string, channelIds: string[], excludeUserId?: string,
fetchOnly = false, groupLabel?: string,
): Promise<ProfilesPerChannelRequest> {
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<UserProfile>) => {
export const updateMe = async (serverUrl: string, user: Partial<UserProfile>, 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 {};

View file

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

View file

@ -7,7 +7,7 @@ import type ClientBase from './base';
export interface ClientAppsMix {
executeAppCall: <Res = unknown>(call: AppCallRequest, trackAsSubmit: boolean) => Promise<AppCallResponse<Res>>;
getAppsBindings: (userID: string, channelID: string, teamID: string) => Promise<AppBinding[]>;
getAppsBindings: (userID: string, channelID: string, teamID: string, groupLabel?: string) => Promise<AppBinding[]>;
}
const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
@ -27,7 +27,7 @@ const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) =>
);
};
getAppsBindings = async (userID: string, channelID: string, teamID: string) => {
getAppsBindings = async (userID: string, channelID: string, teamID: string, groupLabel?: string) => {
const params = {
user_id: userID,
channel_id: channelID,
@ -37,7 +37,7 @@ const ClientApps = <TBase extends Constructor<ClientBase>>(superclass: TBase) =>
return this.doFetch(
`${this.getAppsProxyRoute()}/api/v1/bindings${buildQueryString(params)}`,
{method: 'get'},
{method: 'get', groupLabel},
);
};
};

View file

@ -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 ClientTracking from './tracking';
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 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);
};
}

View file

@ -4,17 +4,17 @@
import type ClientBase from './base';
export interface ClientCategoriesMix {
getCategories: (userId: string, teamId: string) => Promise<CategoriesWithOrder>;
getCategories: (userId: string, teamId: string, groupLabel?: string) => Promise<CategoriesWithOrder>;
getCategoriesOrder: (userId: string, teamId: string) => Promise<string[]>;
getCategory: (userId: string, teamId: string, categoryId: string) => Promise<Category>;
updateChannelCategories: (userId: string, teamId: string, categories: CategoryWithChannels[]) => Promise<CategoriesWithOrder>;
}
const ClientCategories = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
getCategories = async (userId: string, teamId: string) => {
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) => {

View file

@ -10,7 +10,7 @@ export interface ClientChannelBookmarksMix {
updateChannelBookmark(channelId: string, bookmark: ChannelBookmark, connectionId?: string): Promise<UpdateChannelBookmarkResponse>;
updateChannelBookmarkSortOrder(channelId: string, bookmarkId: string, newSortOrder: number, connectionId?: string): Promise<ChannelBookmarkWithFileInfo[]>;
deleteChannelBookmark(channelId: string, bookmarkId: string, connectionId?: string): Promise<ChannelBookmarkWithFileInfo>;
getChannelBookmarksForChannel(channelId: string, since: number): Promise<ChannelBookmarkWithFileInfo[]>;
getChannelBookmarksForChannel(channelId: string, since: number, groupLabel?: string): Promise<ChannelBookmarkWithFileInfo[]>;
}
const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
@ -57,10 +57,10 @@ const ClientChannelBookmarks = <TBase extends Constructor<ClientBase>>(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},
);
}
};

View file

@ -19,24 +19,24 @@ export interface ClientChannelsMix {
updateChannelPrivacy: (channelId: string, privacy: any) => Promise<Channel>;
patchChannel: (channelId: string, channelPatch: Partial<Channel>) => Promise<Channel>;
updateChannelNotifyProps: (props: ChannelNotifyProps & {channel_id: string; user_id: string}) => Promise<any>;
getChannel: (channelId: string) => Promise<Channel>;
getChannel: (channelId: string, groupLabel?: string) => Promise<Channel>;
getChannelByName: (teamId: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
getChannelByNameAndTeamName: (teamName: string, channelName: string, includeDeleted?: boolean) => Promise<Channel>;
getChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getArchivedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getSharedChannels: (teamId: string, page?: number, perPage?: number) => Promise<Channel[]>;
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number) => Promise<Channel[]>;
getMyChannels: (teamId: string, includeDeleted?: boolean, lastDeleteAt?: number, groupLabel?: string) => Promise<Channel[]>;
getMyChannelMember: (channelId: string) => Promise<ChannelMembership>;
getMyChannelMembers: (teamId: string) => Promise<ChannelMembership[]>;
getMyChannelMembers: (teamId: string, groupLabel?: string) => Promise<ChannelMembership[]>;
getChannelMembers: (channelId: string, page?: number, perPage?: number) => Promise<ChannelMembership[]>;
getChannelTimezones: (channelId: string) => Promise<string[]>;
getChannelMember: (channelId: string, userId: string) => Promise<ChannelMembership>;
getChannelMember: (channelId: string, userId: string, groupLabel?: string) => Promise<ChannelMembership>;
getChannelMembersByIds: (channelId: string, userIds: string[]) => Promise<ChannelMembership[]>;
addToChannel: (userId: string, channelId: string, postRootId?: string) => Promise<ChannelMembership>;
removeFromChannel: (userId: string, channelId: string) => Promise<any>;
getChannelStats: (channelId: string) => Promise<ChannelStats>;
getChannelStats: (channelId: string, groupLabel?: string) => Promise<ChannelStats>;
getChannelMemberCountsByGroup: (channelId: string, includeTimezones: boolean) => Promise<ChannelMemberCountByGroup[]>;
viewMyChannel: (channelId: string, prevChannelId?: string) => Promise<any>;
viewMyChannel: (channelId: string, prevChannelId?: string, groupLabel?: string) => Promise<any>;
autocompleteChannels: (teamId: string, name: string) => Promise<Channel[]>;
autocompleteChannelsForSearch: (teamId: string, name: string) => Promise<Channel[]>;
searchChannels: (teamId: string, term: string) => Promise<Channel[]>;
@ -130,10 +130,10 @@ const ClientChannels = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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},
);
};

View file

@ -14,28 +14,28 @@ type PoliciesResponse<T> = {
}
export interface ClientGeneralMix {
ping: (deviceId?: string, timeoutInterval?: number) => Promise<any>;
ping: (deviceId?: string, timeoutInterval?: number, groupLabel?: string) => Promise<any>;
logClientError: (message: string, level?: string) => Promise<any>;
getClientConfigOld: () => Promise<ClientConfig>;
getClientLicenseOld: () => Promise<ClientLicense>;
getClientConfigOld: (groupLabel?: string) => Promise<ClientConfig>;
getClientLicenseOld: (groupLabel?: string) => Promise<ClientLicense>;
getTimezones: () => Promise<string[]>;
getGlobalDataRetentionPolicy: () => Promise<GlobalDataRetentionPolicy>;
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
getGlobalDataRetentionPolicy: (groupLabel?: string) => Promise<GlobalDataRetentionPolicy>;
getTeamDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise<PoliciesResponse<TeamDataRetentionPolicy>>;
getChannelDataRetentionPolicies: (userId: string, page?: number, perPage?: number, groupLabel?: string) => Promise<PoliciesResponse<ChannelDataRetentionPolicy>>;
getRolesByNames: (rolesNames: string[], groupLabel?: string) => Promise<Role[]>;
getRedirectLocation: (urlParam: string) => Promise<Record<string, string>>;
sendPerformanceReport: (batch: PerformanceReport) => Promise<{}>;
}
const ClientGeneral = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass {
ping = async (deviceId?: string, timeoutInterval?: number) => {
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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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},
);
};

View file

@ -9,8 +9,8 @@ import type ClientBase from './base';
export interface ClientGroupsMix {
getGroups: (params: {query?: string; filterAllowReference?: boolean; page?: number; perPage?: number; since?: number; includeMemberCount?: boolean}) => Promise<Group[]>;
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean) => Promise<Group[]>;
getAllGroupsAssociatedToChannel: (channelId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<{groups: Group[]; total_group_count: number}>;
getAllGroupsAssociatedToMembership: (userId: string, filterAllowReference?: boolean, groupLabel?: string) => Promise<Group[]>;
getAllGroupsAssociatedToTeam: (teamId: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; total_group_count: number}>;
}
@ -29,14 +29,14 @@ const ClientGroups = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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},
);
};
};

View file

@ -10,12 +10,12 @@ import type ClientBase from './base';
export interface ClientPostsMix {
createPost: (post: Post) => Promise<Post>;
updatePost: (post: Post) => Promise<Post>;
getPost: (postId: string) => Promise<Post>;
getPost: (postId: string, groupLabel?: string) => Promise<Post>;
patchPost: (postPatch: Partial<Post> & {id: string}) => Promise<Post>;
deletePost: (postId: string) => Promise<any>;
getPostThread: (postId: string, options: FetchPaginatedThreadOptions) => Promise<PostResponse>;
getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
getPostThread: (postId: string, options: FetchPaginatedThreadOptions, groupLabel?: string) => Promise<PostResponse>;
getPosts: (channelId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise<PostResponse>;
getPostsSince: (channelId: string, since: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean, groupLabel?: string) => Promise<PostResponse>;
getPostsBefore: (channelId: string, postId?: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
getPostsAfter: (channelId: string, postId: string, page?: number, perPage?: number, collapsedThreads?: boolean, collapsedThreadsExtended?: boolean) => Promise<PostResponse>;
getFileInfosForPost: (postId: string) => Promise<FileInfo[]>;
@ -51,10 +51,10 @@ const ClientPosts = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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},
);
};

View file

@ -4,23 +4,23 @@
import type ClientBase from './base';
export interface ClientPreferencesMix {
savePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
savePreferences: (userId: string, preferences: PreferenceType[], groupLabel?: string) => Promise<any>;
deletePreferences: (userId: string, preferences: PreferenceType[]) => Promise<any>;
getMyPreferences: () => Promise<PreferenceType[]>;
getMyPreferences: (groupLabel?: string) => Promise<PreferenceType[]>;
}
const ClientPreferences = <TBase extends Constructor<ClientBase>>(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},
);
};

View file

@ -12,14 +12,14 @@ export interface ClientTeamsMix {
deleteTeam: (teamId: string) => Promise<any>;
updateTeam: (team: Team) => Promise<Team>;
patchTeam: (team: Partial<Team> & {id: string}) => Promise<Team>;
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean) => Promise<Team[]>;
getTeam: (teamId: string) => Promise<Team>;
getTeams: (page?: number, perPage?: number, includeTotalCount?: boolean, groupLabel?: string) => Promise<Team[]>;
getTeam: (teamId: string, groupLabel?: string) => Promise<Team>;
getTeamByName: (teamName: string) => Promise<Team>;
getMyTeams: () => Promise<Team[]>;
getMyTeams: (groupLabel?: string) => Promise<Team[]>;
getTeamsForUser: (userId: string) => Promise<Team[]>;
getMyTeamMembers: () => Promise<TeamMembership[]>;
getMyTeamMembers: (groupLabel?: string) => Promise<TeamMembership[]>;
getTeamMembers: (teamId: string, page?: number, perPage?: number) => Promise<TeamMembership[]>;
getTeamMember: (teamId: string, userId: string) => Promise<TeamMembership>;
getTeamMember: (teamId: string, userId: string, groupLabel?: string) => Promise<TeamMembership>;
getTeamMembersByIds: (teamId: string, userIds: string[]) => Promise<TeamMembership[]>;
addToTeam: (teamId: string, userId: string) => Promise<TeamMembership>;
addUsersToTeamGracefully: (teamId: string, userIds: string[]) => Promise<TeamMemberWithError[]>;
@ -59,17 +59,17 @@ const ClientTeams = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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},
);
};

View file

@ -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<GetUserThreadsResponse>;
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<GetUserThreadsResponse>;
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise<any>;
@ -17,7 +22,11 @@ export interface ClientThreadsMix {
}
const ClientThreads = <TBase extends Constructor<ClientBase>>(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<string, any> = {
extended: 'true',
before,
@ -35,7 +44,7 @@ const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase)
}
return this.doFetch(
`${this.getThreadsRoute(userId, teamId)}${buildQueryString(queryStringObj)}`,
{method: 'get'},
{method: 'get', groupLabel},
);
};

View file

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

263
app/client/rest/tracking.ts Normal file
View file

@ -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<string, UrlData>;
completionFlag: boolean;
completionTimer?: NodeJS.Timeout;
}
export default class ClientTracking {
apiClient: APIClientInterface;
csrfToken = '';
requestHeaders: {[x: string]: string} = {};
serverVersion = '';
urlVersion = '/api/v4';
enableLogging = false;
requestGroups: Map<string, GroupData> = 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);
}
}
};
}

View file

@ -10,7 +10,7 @@ import type ClientBase from './base';
export interface ClientUsersMix {
createUser: (user: UserProfile, token: string, inviteId: string) => Promise<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>) => Promise<UserProfile>;
patchMe: (userPatch: Partial<UserProfile>, groupLabel?: string) => Promise<UserProfile>;
patchUser: (userPatch: Partial<UserProfile> & {id: string}) => Promise<UserProfile>;
updateUser: (user: UserProfile) => Promise<UserProfile>;
demoteUserToGuest: (userId: string) => Promise<any>;
@ -21,15 +21,15 @@ export interface ClientUsersMix {
loginById: (id: string, password: string, token?: string, deviceId?: string) => Promise<UserProfile>;
logout: () => Promise<any>;
getProfiles: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesByIds: (userIds: string[], options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesByUsernames: (usernames: string[]) => Promise<UserProfile[]>;
getProfilesByIds: (userIds: string[], options?: Record<string, any>, groupLabel?: string) => Promise<UserProfile[]>;
getProfilesByUsernames: (usernames: string[], groupLabel?: string) => Promise<UserProfile[]>;
getProfilesInTeam: (teamId: string, page?: number, perPage?: number, sort?: string, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesNotInTeam: (teamId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
getProfilesWithoutTeam: (page?: number, perPage?: number, options?: Record<string, any>) => Promise<UserProfile[]>;
getProfilesInChannel: (channelId: string, options?: GetUsersOptions) => Promise<UserProfile[]>;
getProfilesInGroupChannels: (channelsIds: string[]) => Promise<{[x: string]: UserProfile[]}>;
getProfilesInChannel: (channelId: string, options?: GetUsersOptions, groupLabel?: string) => Promise<UserProfile[]>;
getProfilesInGroupChannels: (channelsIds: string[], groupLabel?: string) => Promise<{[x: string]: UserProfile[]}>;
getProfilesNotInChannel: (teamId: string, channelId: string, groupConstrained: boolean, page?: number, perPage?: number) => Promise<UserProfile[]>;
getMe: () => Promise<UserProfile>;
getMe: (groupLabel?: string) => Promise<UserProfile>;
getUser: (userId: string) => Promise<UserProfile>;
getUserByUsername: (username: string) => Promise<UserProfile>;
getUserByEmail: (email: string) => Promise<UserProfile>;
@ -38,10 +38,10 @@ export interface ClientUsersMix {
autocompleteUsers: (name: string, teamId: string, channelId?: string, options?: Record<string, any>) => Promise<{users: UserProfile[]; out_of_channel?: UserProfile[]}>;
getSessions: (userId: string) => Promise<Session[]>;
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null) => Promise<{}>;
setExtraSessionProps: (deviceId: string, notificationsEnabled: boolean, version: string | null, groupLabel?: string) => Promise<{}>;
searchUsers: (term: string, options: SearchUserOptions) => Promise<UserProfile[]>;
getStatusesByIds: (userIds: string[]) => Promise<UserStatus[]>;
getStatus: (userId: string) => Promise<UserStatus>;
getStatus: (userId: string, groupLabel?: string) => Promise<UserStatus>;
updateStatus: (status: UserStatus) => Promise<UserStatus>;
updateCustomStatus: (customStatus: UserCustomStatus) => Promise<{status: string}>;
unsetCustomStatus: () => Promise<{status: string}>;
@ -66,10 +66,10 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
patchMe = async (userPatch: Partial<UserProfile>) => {
patchMe = async (userPatch: Partial<UserProfile>, groupLabel?: string) => {
return this.doFetch(
`${this.getUserRoute('me')}/patch`,
{method: 'put', body: userPatch},
{method: 'put', body: userPatch, groupLabel},
);
};
@ -127,7 +127,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
false,
);
return data;
return resp?.data;
};
loginById = async (id: string, password: string, token = '', deviceId = '') => {
@ -148,7 +148,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
token,
};
const {data} = await this.doFetch(
const resp = await this.doFetch(
`${this.getUsersRoute()}/login`,
{
method: 'post',
@ -158,7 +158,7 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
false,
);
return data;
return resp?.data;
};
logout = async () => {
@ -177,17 +177,17 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(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 = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
device_notification_disabled: deviceNotificationDisabled ? 'true' : 'false',
mobile_version: version || '',
},
groupLabel,
},
);
};
@ -353,10 +354,10 @@ const ClientUsers = <TBase extends Constructor<ClientBase>>(superclass: TBase) =
);
};
getStatus = async (userId: string) => {
getStatus = async (userId: string, groupLabel?: string) => {
return this.doFetch(
`${this.getUserRoute(userId)}/status`,
{method: 'get'},
{method: 'get', groupLabel},
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,10 +7,10 @@ import type {RTCIceServer} from 'react-native-webrtc';
export interface ClientCallsMix {
getEnabled: () => Promise<Boolean>;
getCalls: () => Promise<CallChannelState[]>;
getCalls: (groupLabel?: string) => Promise<CallChannelState[]>;
getCallForChannel: (channelId: string) => Promise<CallChannelState>;
getCallsConfig: () => Promise<CallsConfig>;
getVersion: () => Promise<CallsVersion>;
getCallsConfig: (groupLabel?: string) => Promise<CallsConfig>;
getVersion: (groupLabel?: string) => Promise<CallsVersion>;
enableChannelCalls: (channelId: string, enable: boolean) => Promise<CallChannelState>;
endCall: (channelId: string) => Promise<ApiResp>;
genTURNCredentials: () => Promise<RTCIceServer[]>;
@ -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 {};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,5 +28,6 @@
"ShowOnboarding": false,
"ExperimentalNormalizeMarkdownLinks": false,
"CustomRequestHeaders": {}
"CustomRequestHeaders": {},
"CollectNetworkMetrics": false
}

6
package-lock.json generated
View file

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

View file

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

View file

@ -9,6 +9,7 @@ type ClientOptions = {
noRetry?: boolean;
timeoutInterval?: number;
headers?: Record<string, any>;
groupLabel?: string;
};
type ClientErrorIntl =