Login improvement (#8306)

* refactor to improve requests on login

* include in team sidebar order by display name teams not present in the order preference

* Fix iOS reload while developing

* fix code causing tests to fail

* feedback review

* update network-client
This commit is contained in:
Elias Nahum 2024-11-13 08:06:12 +08:00 committed by GitHub
parent f6bc8ae50b
commit ea2cb45a18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 364 additions and 118 deletions

View file

@ -105,13 +105,13 @@ const mockClient = {
getUser: jest.fn((userId: string) => ({...user, id: userId})), getUser: jest.fn((userId: string) => ({...user, id: userId})),
getMyChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])), getMyChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])),
getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: ''}])), getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: ''}])),
getCategories: jest.fn((userId: string, teamId: string) => ({categories: [{id: 'categoryid', channel_id: [channelId], team_id: teamId}], order: ['categoryid']})), getCategories: jest.fn((userId: string, teamId: string) => ({categories: [{id: 'categoryid', channel_ids: [channelId], team_id: teamId}], order: ['categoryid']})),
viewMyChannel: jest.fn(), viewMyChannel: jest.fn(),
getTeamByName: jest.fn((teamName: string) => ({id: teamId, name: teamName})), getTeamByName: jest.fn((teamName: string) => ({id: teamId, name: teamName})),
getTeam: jest.fn((id: string) => ({id, name: 'teamname'})), getTeam: jest.fn((id: string) => ({id, name: 'teamname'})),
addToTeam: jest.fn((teamId: string, userId: string) => ({id: userId + '-' + teamId, user_id: userId, team_id: teamId, roles: ''})), addToTeam: jest.fn((teamId: string, userId: string) => ({id: userId + '-' + teamId, user_id: userId, team_id: teamId, roles: ''})),
getUserByUsername: jest.fn((username: string) => ({...user, id: 'userid2', username})), getUserByUsername: jest.fn((username: string) => ({...user, id: 'userid2', username})),
createDirectChannel: jest.fn((userId1: string, userId2: string) => ({id: userId1 + '__' + userId2, team_id: '', type: 'D', display_name: 'displayname'})), createDirectChannel: jest.fn((userIds: string[]) => ({id: userIds[0] + '__' + userIds[1], team_id: '', type: 'D', display_name: 'displayname'})),
getChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])), getChannels: jest.fn((teamId: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: teamId}])),
getArchivedChannels: jest.fn((teamId: string) => ([{id: channelId + 'old', name: 'channel1old', creatorId: user.id, team_id: teamId, delete_at: 1}])), getArchivedChannels: jest.fn((teamId: string) => ([{id: channelId + 'old', name: 'channel1old', creatorId: user.id, team_id: teamId, delete_at: 1}])),
createGroupChannel: jest.fn(() => ({id: 'groupid', team_id: '', type: 'G', display_name: 'displayname'})), createGroupChannel: jest.fn(() => ({id: 'groupid', team_id: '', type: 'G', display_name: 'displayname'})),
@ -129,6 +129,11 @@ const mockClient = {
convertChannelToPrivate: jest.fn(), convertChannelToPrivate: jest.fn(),
getGroupMessageMembersCommonTeams: jest.fn(() => ({id: teamId, name: 'teamname'})), getGroupMessageMembersCommonTeams: jest.fn(() => ({id: teamId, name: 'teamname'})),
convertGroupMessageToPrivateChannel: jest.fn((channelId: string) => ({id: channelId, name: 'channel1', creatorId: user.id, type: 'P'})), convertGroupMessageToPrivateChannel: jest.fn((channelId: string) => ({id: channelId, name: 'channel1', creatorId: user.id, type: 'P'})),
getPosts: jest.fn(() => ({
order: [],
posts: [],
previousPostId: '',
})),
}; };
const teamId = 'teamid1'; const teamId = 'teamid1';

View file

@ -45,6 +45,7 @@ import type ChannelModel from '@typings/database/models/servers/channel';
import type {IntlShape} from 'react-intl'; import type {IntlShape} from 'react-intl';
export type MyChannelsRequest = { export type MyChannelsRequest = {
teamId?: string;
categories?: CategoryWithChannels[]; categories?: CategoryWithChannels[];
channels?: Channel[]; channels?: Channel[];
memberships?: ChannelMembership[]; memberships?: ChannelMembership[];
@ -441,7 +442,7 @@ export async function fetchMyChannelsForTeam(serverUrl: string, teamId: string,
setTeamLoading(serverUrl, false); setTeamLoading(serverUrl, false);
} }
return {channels, memberships, categories}; return {channels, memberships, categories, teamId};
} catch (error) { } catch (error) {
logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error)); logDebug('error on fetchMyChannelsForTeam', getFullErrorMessage(error));
if (!fetchOnly) { if (!fetchOnly) {

View file

@ -10,7 +10,7 @@ import {fetchPostsForUnreadChannels} from '@actions/remote/post';
import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference'; import {type MyPreferencesRequest, fetchMyPreferences} from '@actions/remote/preference';
import {fetchRoles} from '@actions/remote/role'; import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense, fetchDataRetentionPolicy} from '@actions/remote/systems'; import {fetchConfigAndLicense, fetchDataRetentionPolicy} from '@actions/remote/systems';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team'; import {fetchMyTeams, fetchTeamsChannelsThreadsAndUnreadPosts, handleKickFromTeam, type MyTeamsRequest, updateCanJoinTeams} from '@actions/remote/team';
import {syncTeamThreads} from '@actions/remote/thread'; import {syncTeamThreads} from '@actions/remote/thread';
import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user'; import {fetchMe, type MyUserRequest, updateAllUsersSince, autoUpdateTimezone} from '@actions/remote/user';
import {General, Preferences, Screens} from '@constants'; import {General, Preferences, Screens} from '@constants';
@ -66,9 +66,6 @@ export type EntryResponse = {
error: unknown; error: unknown;
} }
const FETCH_MISSING_DM_TIMEOUT = 2500;
export const FETCH_UNREADS_TIMEOUT = 2500;
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): Promise<EntryResponse> => {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const result = entryRest(serverUrl, teamId, channelId, since); const result = entryRest(serverUrl, teamId, channelId, since);
@ -311,8 +308,7 @@ const fetchAlternateTeamData = async (
let initialTeamId = ''; let initialTeamId = '';
let chData; let chData;
for (const teamId of availableTeamIds) { for await (const teamId of availableTeamIds) {
// eslint-disable-next-line no-await-in-loop
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled); chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly, false, isCRTEnabled);
const chError = chData.error; const chError = chData.error;
if (isErrorWithStatusCode(chError) && chError.status_code === 403) { if (isErrorWithStatusCode(chError) && chError.status_code === 403) {
@ -371,49 +367,56 @@ async function entryInitialChannelId(database: Database, requestedChannelId = ''
async function restDeferredAppEntryActions( async function restDeferredAppEntryActions(
serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, serverUrl: string, since: number, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined,
config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined, config: ClientConfig, license: ClientLicense | undefined, teamData: MyTeamsRequest, chData: MyChannelsRequest | undefined,
initialTeamId?: string, initialChannelId?: string) { initialTeamId?: string, initialChannelId?: string,
setTimeout(async () => { ) {
if (chData?.channels?.length && chData.memberships?.length) { const isCRTEnabled = (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) || false;
// defer fetching posts for unread channels on initial team const directChannels = chData?.channels?.filter(isDMorGM);
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId); const channelsToFetchProfiles = new Set<Channel>(directChannels);
}
}, FETCH_UNREADS_TIMEOUT); // sidebar DM & GM profiles
if (channelsToFetchProfiles.size) {
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
}
updateAllUsersSince(serverUrl, since);
updateCanJoinTeams(serverUrl);
// defer fetch channels and unread posts for other teams // defer fetch channels and unread posts for other teams
if (teamData.teams?.length && teamData.memberships?.length) { setTimeout(async () => {
fetchTeamsChannelsAndUnreadPosts(serverUrl, since, teamData.teams, teamData.memberships, initialTeamId); if (chData?.channels?.length && chData.memberships?.length && initialTeamId) {
} if (isCRTEnabled && initialTeamId) {
await syncTeamThreads(serverUrl, initialTeamId);
if (preferences && processIsCRTEnabled(preferences, config.CollapsedThreads, config.FeatureFlagCollapsedThreads, config.Version)) {
if (initialTeamId) {
await syncTeamThreads(serverUrl, initialTeamId);
}
if (teamData.teams?.length) {
for await (const team of teamData.teams) {
if (team.id !== initialTeamId) {
// need to await here since GM/DM threads in different teams overlap
await syncTeamThreads(serverUrl, team.id);
}
} }
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannelId);
} }
}
updateCanJoinTeams(serverUrl); if (teamData.teams?.length && teamData.memberships?.length) {
await updateAllUsersSince(serverUrl, since); const teamsOrder = preferences?.find((p) => p.category === Preferences.CATEGORIES.TEAMS_ORDER);
const sortedTeamIds = new Set(teamsOrder?.value.split(','));
const membershipSet = new Set(teamData.memberships.map((m) => m.team_id));
const teamMap = new Map(teamData.teams.map((t) => [t.id, t]));
let myTeams: Team[];
if (sortedTeamIds.size) {
const mySortedTeams = [...sortedTeamIds].
filter((id) => membershipSet.has(id) && teamMap.has(id)).
map((id) => teamMap.get(id)!);
const extraTeams = [...membershipSet].
filter((id) => !sortedTeamIds.has(id) && teamMap.get(id)).
map((id) => teamMap.get(id)!).
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
myTeams = [...mySortedTeams, ...extraTeams];
} else {
myTeams = teamData.teams.
sort((a, b) => a.display_name.toLocaleLowerCase().localeCompare(b.display_name.toLocaleLowerCase()));
}
fetchTeamsChannelsThreadsAndUnreadPosts(serverUrl, since, myTeams, isCRTEnabled);
}
});
// Fetch groups for current user // Fetch groups for current user
fetchGroupsForMember(serverUrl, currentUserId); fetchGroupsForMember(serverUrl, currentUserId);
// defer sidebar DM & GM profiles
setTimeout(async () => {
const directChannels = chData?.channels?.filter(isDMorGM);
const channelsToFetchProfiles = new Set<Channel>(directChannels);
if (channelsToFetchProfiles.size) {
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config.LockTeammateNameDisplay, config.TeammateNameDisplay, license);
fetchMissingDirectChannelsInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
}
}, FETCH_MISSING_DM_TIMEOUT);
} }
export const setExtraSessionProps = async (serverUrl: string) => { export const setExtraSessionProps = async (serverUrl: string) => {

View file

@ -4,6 +4,8 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import {chunk} from 'lodash';
import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel'; import {markChannelAsUnread, updateLastPostAt} from '@actions/local/channel';
import {addPostAcknowledgement, removePost, removePostAcknowledgement, storePostsForChannel} from '@actions/local/post'; import {addPostAcknowledgement, removePost, removePostAcknowledgement, storePostsForChannel} from '@actions/local/post';
import {addRecentReaction} from '@actions/local/reactions'; import {addRecentReaction} from '@actions/local/reactions';
@ -40,6 +42,13 @@ type PostsRequest = {
previousPostId?: string; previousPostId?: string;
} }
export type PostsForChannel = PostsRequest & {
actionType?: string;
authors?: UserProfile[];
channelId?: string;
error?: unknown;
};
type PostsObjectsRequest = { type PostsObjectsRequest = {
error?: unknown; error?: unknown;
order?: string[]; order?: string[];
@ -275,7 +284,7 @@ export const retryFailedPost = async (serverUrl: string, post: PostModel) => {
return {}; return {};
}; };
export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false) { export async function fetchPostsForChannel(serverUrl: string, channelId: string, fetchOnly = false): Promise<PostsForChannel> {
try { try {
if (!fetchOnly) { if (!fetchOnly) {
EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId); EphemeralStore.addLoadingMessagesForChannel(serverUrl, channelId);
@ -312,7 +321,7 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
} }
} }
return {posts: data.posts, order: data.order, authors, actionType, previousPostId: data.previousPostId}; return {posts: data.posts, order: data.order, authors, actionType, previousPostId: data.previousPostId, channelId};
} catch (error) { } catch (error) {
logDebug('error on fetchPostsForChannel', getFullErrorMessage(error)); logDebug('error on fetchPostsForChannel', getFullErrorMessage(error));
return {error}; return {error};
@ -323,15 +332,34 @@ export async function fetchPostsForChannel(serverUrl: string, channelId: string,
} }
} }
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => { export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string, fetchOnly = false): Promise<PostsForChannel[]> => {
const promises = []; const membersMap = new Map<string, ChannelMembership>();
for (const member of memberships) { for (const member of memberships) {
const channel = channels.find((c) => c.id === member.channel_id); membersMap.set(member.channel_id, member);
if (channel && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
promises.push(fetchPostsForChannel(serverUrl, channel.id));
}
} }
return Promise.all(promises);
const unreadChannelIds = channels.reduce<string[]>((result, channel) => {
const member = membersMap.get(channel.id);
if (member && !channel.delete_at && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
result.push(channel.id);
}
return result;
}, []);
const postsForChannel: PostsForChannel[] = [];
// process 10 unread channels at a time
const chunks = chunk(unreadChannelIds, 10);
for await (const channelIds of chunks) {
const promises = [];
for (const channelId of channelIds) {
promises.push(fetchPostsForChannel(serverUrl, channelId, fetchOnly));
}
const results = await Promise.all(promises);
postsForChannel.push(...results);
}
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): Promise<PostsRequest> {

View file

@ -18,7 +18,6 @@ import {
fetchAllTeams, fetchAllTeams,
fetchTeamsForComponent, fetchTeamsForComponent,
updateCanJoinTeams, updateCanJoinTeams,
fetchTeamsChannelsAndUnreadPosts,
fetchTeamByName, fetchTeamByName,
removeCurrentUserFromTeam, removeCurrentUserFromTeam,
removeUserFromTeam, removeUserFromTeam,
@ -26,6 +25,7 @@ import {
handleKickFromTeam, handleKickFromTeam,
getTeamMembersByIds, getTeamMembersByIds,
buildTeamIconUrl, buildTeamIconUrl,
fetchTeamsChannelsThreadsAndUnreadPosts,
} from './team'; } from './team';
import type ServerDataOperator from '@database/operator/server_data_operator'; import type ServerDataOperator from '@database/operator/server_data_operator';
@ -81,12 +81,38 @@ const mockClient = {
getMyTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])), getMyTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])),
getMyTeamMembers: jest.fn(() => ([{id: 'userid1-' + teamId, user_id: 'userid1', team_id: teamId, roles: ''}])), getMyTeamMembers: jest.fn(() => ([{id: 'userid1-' + teamId, user_id: 'userid1', team_id: teamId, roles: ''}])),
getTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])), getTeams: jest.fn(() => ([{id: teamId, name: 'team1'}])),
getMyChannels: jest.fn((id: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: id}])), getMyChannels: jest.fn((id: string) => ([{id: channelId, name: 'channel1', creatorId: user.id, team_id: id, total_msg_count: 1}])),
getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: ''}])), getMyChannelMembers: jest.fn(() => ([{id: user.id + '-' + channelId, user_id: user.id, channel_id: channelId, roles: '', msg_count: 0}])),
getCategories: jest.fn((userId: string, id: string) => ({categories: [{id: 'categoryid', channel_id: [channelId], team_id: id}], order: ['categoryid']})), getCategories: jest.fn((userId: string, id: string) => ({categories: [{id: 'categoryid', channel_ids: [channelId], team_id: id}], order: ['categoryid']})),
getTeamByName: jest.fn((name: string) => ({id: teamId, name})), getTeamByName: jest.fn((name: string) => ({id: teamId, name})),
removeFromTeam: jest.fn(), removeFromTeam: jest.fn(),
getTeamMembersByIds: jest.fn((id: string, userIds: string[]) => (userIds.map((userId) => ({id: userId + '-' + id, user_id: userId, team_id: id, roles: ''})))), getTeamMembersByIds: jest.fn((id: string, userIds: string[]) => (userIds.map((userId) => ({id: userId + '-' + id, user_id: userId, team_id: id, roles: ''})))),
getPosts: jest.fn(() => ({
order: ['yocj9xgkh78exk1uhx9yny1zxy', 'ad6yoisgh385fy7rkph49zpfqa', 'o5qqa4ntdigp7rbnf75f8hgeaw'],
posts: [{
channel_id: channelId,
create_at: 1726107604522,
delete_at: 0,
edit_at: 0,
hashtags: '',
id: 'yocj9xgkh78exk1uhx9yny1zxy',
is_pinned: false,
last_reply_at: 0,
message: 'Message ead863',
metadata: {},
original_id: '',
participants: null,
pending_post_id: '',
props: {},
remote_id: '',
reply_count: 0,
root_id: '',
type: '',
update_at: 1726107604522,
user_id: 'k479wsypafgjddz8cerqx4m1ha',
}],
previousPostId: '',
})),
}; };
beforeAll(() => { beforeAll(() => {
@ -245,16 +271,25 @@ describe('teams', () => {
expect(result).toBeDefined(); expect(result).toBeDefined();
}); });
it('fetchTeamsChannelsAndUnreadPosts - handle not found database', async () => { it('fetchTeamsChannelsThreadsAndUnreadPosts - handle not found database', async () => {
const result = await fetchTeamsChannelsAndUnreadPosts('foo', 0, [], []) as {error: unknown}; const result = await fetchTeamsChannelsThreadsAndUnreadPosts('foo', 0, []) as {error: unknown};
expect(result?.error).toBeDefined(); expect(result?.error).toBeDefined();
}); });
it('fetchTeamsChannelsAndUnreadPosts - base case', async () => { it('fetchTeamsChannelsThreadsAndUnreadPosts - base case', async () => {
const result = await fetchTeamsChannelsAndUnreadPosts(serverUrl, 0, [team], [{team_id: teamId, user_id: 'userid1'} as TeamMembership]); const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
serverUrl, 0,
[team], true, false);
expect(result).toBeDefined(); expect(result).toBeDefined();
}); });
it('fetchTeamsChannelsThreadsAndUnreadPosts - fetch only case', async () => {
const result = await fetchTeamsChannelsThreadsAndUnreadPosts(
serverUrl, 0,
[team], true, true);
expect(result.models).toBeDefined();
});
it('fetchTeamByName - handle not found database', async () => { it('fetchTeamByName - handle not found database', async () => {
const result = await fetchTeamByName('foo', '') as {error: unknown}; const result = await fetchTeamByName('foo', '') as {error: unknown};
expect(result?.error).toBeDefined(); expect(result?.error).toBeDefined();

View file

@ -1,12 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {chunk} from 'lodash';
import {DeviceEventEmitter} from 'react-native'; import {DeviceEventEmitter} from 'react-native';
import {storeCategories} from '@actions/local/category';
import {storeMyChannelsForTeam} from '@actions/local/channel';
import {storePostsForChannel} from '@actions/local/post';
import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team'; import {removeUserFromTeam as localRemoveUserFromTeam} from '@actions/local/team';
import {PER_PAGE_DEFAULT} from '@client/rest/constants'; import {PER_PAGE_DEFAULT} from '@client/rest/constants';
import {Events} from '@constants'; import {Events} from '@constants';
import DatabaseManager from '@database/manager'; import DatabaseManager from '@database/manager';
import {removeDuplicatesModels} from '@helpers/database';
import NetworkManager from '@managers/network_manager'; import NetworkManager from '@managers/network_manager';
import {getActiveServerUrl} from '@queries/app/servers'; import {getActiveServerUrl} from '@queries/app/servers';
import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories'; import {prepareCategoriesAndCategoriesChannels} from '@queries/servers/categories';
@ -22,9 +27,10 @@ import {logDebug} from '@utils/log';
import {fetchMyChannelsForTeam, switchToChannelById} from './channel'; import {fetchMyChannelsForTeam, switchToChannelById} from './channel';
import {fetchGroupsForTeamIfConstrained} from './groups'; import {fetchGroupsForTeamIfConstrained} from './groups';
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post'; import {fetchPostsForChannel, fetchPostsForUnreadChannels, type PostsForChannel} from './post';
import {fetchRolesIfNeeded} from './role'; import {fetchRolesIfNeeded} from './role';
import {forceLogoutIfNecessary} from './session'; import {forceLogoutIfNecessary} from './session';
import {syncThreadsIfNeeded} from './thread';
import type {Client} from '@client/rest'; import type {Client} from '@client/rest';
import type {Model} from '@nozbe/watermelondb'; import type {Model} from '@nozbe/watermelondb';
@ -312,24 +318,85 @@ export const updateCanJoinTeams = async (serverUrl: string) => {
} }
}; };
export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, since: number, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => { export const fetchTeamsChannelsThreadsAndUnreadPosts = async (
const database = DatabaseManager.serverDatabases[serverUrl]?.database; serverUrl: string, since: number, teams: Team[],
if (!database) { isCRTEnabled?: boolean, fetchOnly = false,
return {error: `${serverUrl} database not found`}; ) => {
} try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const result: Model[] = [];
const membershipSet = new Set(memberships.map((m) => m.team_id)); // process up to 15 teams at a time
const myTeams = teams.filter((t) => membershipSet.has(t.id) && t.id !== excludeTeamId); const chunks = chunk(teams, 15);
for await (const myTeams of chunks) {
const promises = [];
for (const team of myTeams) {
promises.push(fetchMyChannelsForTeam(serverUrl, team.id, true, since, true, true, isCRTEnabled));
}
for await (const team of myTeams) { const results = await Promise.all(promises);
const {channels, memberships: members} = await fetchMyChannelsForTeam(serverUrl, team.id, true, since, false, true); const models: Model[] = [];
const channels: Channel[] = [];
const members: ChannelMembership[] = [];
if (channels?.length && members?.length) { for await (const r of results) {
fetchPostsForUnreadChannels(serverUrl, channels, members); if (!r.error && r.teamId && r.categories && r.channels && r.memberships) {
const {models: chModels} = await storeMyChannelsForTeam(serverUrl, r.teamId, r.channels, r.memberships, true, isCRTEnabled);
const {models: catModels} = await storeCategories(serverUrl, r.categories, true, true); // Re-sync
if (chModels?.length) {
models.push(...chModels);
}
if (catModels?.length) {
models.push(...catModels);
}
channels.push(...r.channels);
members.push(...r.memberships);
}
}
const unreadPromise = fetchPostsForUnreadChannels(serverUrl, channels, members, undefined, true);
const threadsPromise = syncThreadsIfNeeded(serverUrl, isCRTEnabled ?? false, myTeams, true);
const postPromises: [Promise<PostsForChannel[]>, Promise<{models?: Model[]; error?: unknown}>] = [
unreadPromise,
threadsPromise,
];
const [unreadPosts, threads] = await Promise.all(postPromises);
for await (const data of unreadPosts) {
if (data.channelId && data.posts?.length && data.order?.length && data.actionType) {
const {models: prepared} = await storePostsForChannel(
serverUrl, data.channelId,
data.posts, data.order, data.previousPostId ?? '',
data.actionType, data.authors || [], true,
);
if (prepared?.length) {
models.push(...prepared);
}
}
}
if (threads.models) {
models.push(...threads.models);
}
if (!fetchOnly && models.length) {
await operator.batchRecords(removeDuplicatesModels(models), 'fetchTeamsChannelsThreadsAndUnreadPosts');
}
if (models.length) {
result.push(...models);
}
} }
}
return {}; return {error: undefined, models: result};
} catch (error) {
logDebug('error on fetchTeamsChannelsThreadsAndUnreadPosts', getFullErrorMessage(error));
return {error};
}
}; };
export async function fetchTeamByName(serverUrl: string, teamName: string, fetchOnly = false) { export async function fetchTeamByName(serverUrl: string, teamName: string, fetchOnly = false) {

View file

@ -19,6 +19,7 @@ import {
syncTeamThreads, syncTeamThreads,
loadEarlierThreads, loadEarlierThreads,
fetchAndSwitchToThread, fetchAndSwitchToThread,
syncThreadsIfNeeded,
} from './thread'; } from './thread';
import type ServerDataOperator from '@database/operator/server_data_operator'; import type ServerDataOperator from '@database/operator/server_data_operator';
@ -150,7 +151,7 @@ describe('get threads', () => {
expect(result).toBeDefined(); expect(result).toBeDefined();
expect(result.error).toBeFalsy(); expect(result.error).toBeFalsy();
expect(result.models).toBeDefined(); expect(result.models).toBeDefined();
expect(result.models?.length).toBe(5); // 1 thread, 1 thread participant, 1 read thread in team, 1 unread thread in team, 1 team thread sync expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 latest thread in team, 1 team thread sync (the unread thread is actually the latest so it the duplicate is removed)
// Sync again after first sync // Sync again after first sync
const result2 = await syncTeamThreads(serverUrl, team.id); const result2 = await syncTeamThreads(serverUrl, team.id);
@ -160,6 +161,23 @@ describe('get threads', () => {
expect(result2.models?.length).toBe(1); // 1 team thread sync expect(result2.models?.length).toBe(1); // 1 team thread sync
}); });
it('syncThreadsIfNeeded - handle error', async () => {
const result = await syncThreadsIfNeeded('foo', true, []);
expect(result).toBeDefined();
expect(result.error).toBeTruthy();
});
it('syncThreadsIfNeeded - base case', async () => {
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, value: user1.id}], prepareRecordsOnly: false});
await operator.handleUsers({users: [user1], prepareRecordsOnly: false});
const result = await syncThreadsIfNeeded(serverUrl, true, [team]);
expect(result).toBeDefined();
expect(result.error).toBeFalsy();
expect(result.models).toBeDefined();
expect(result.models?.length).toBe(4); // 1 thread, 1 thread participant, 1 latest thread in team, 1 team thread sync (the unread thread is actually the latest so it the duplicate is removed)
});
it('loadEarlierThreads - handle error', async () => { it('loadEarlierThreads - handle error', async () => {
const result = await loadEarlierThreads('foo', '', ''); const result = await loadEarlierThreads('foo', '', '');
expect(result).toBeDefined(); expect(result).toBeDefined();

View file

@ -5,6 +5,7 @@ import {markTeamThreadsAsRead, markThreadAsViewed, processReceivedThreads, switc
import {fetchPostThread} from '@actions/remote/post'; import {fetchPostThread} from '@actions/remote/post';
import {General} from '@constants'; import {General} from '@constants';
import DatabaseManager from '@database/manager'; import DatabaseManager from '@database/manager';
import {removeDuplicatesModels} from '@helpers/database';
import PushNotifications from '@init/push_notifications'; import PushNotifications from '@init/push_notifications';
import AppsManager from '@managers/apps_manager'; import AppsManager from '@managers/apps_manager';
import NetworkManager from '@managers/network_manager'; import NetworkManager from '@managers/network_manager';
@ -13,7 +14,8 @@ import {getConfigValue, getCurrentChannelId, getCurrentTeamId} from '@queries/se
import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread'; import {getIsCRTEnabled, getThreadById, getTeamThreadsSyncData} from '@queries/servers/thread';
import {getCurrentUser} from '@queries/servers/user'; import {getCurrentUser} from '@queries/servers/user';
import {getFullErrorMessage} from '@utils/errors'; import {getFullErrorMessage} from '@utils/errors';
import {logDebug} from '@utils/log'; import {isMinimumServerVersion} from '@utils/helpers';
import {logDebug, logError} from '@utils/log';
import {showThreadFollowingSnackbar} from '@utils/snack_bar'; import {showThreadFollowingSnackbar} from '@utils/snack_bar';
import {getThreadsListEdges} from '@utils/thread'; import {getThreadsListEdges} from '@utils/thread';
@ -30,6 +32,7 @@ type FetchThreadsOptions = {
unread?: boolean; unread?: boolean;
since?: number; since?: number;
totalsOnly?: boolean; totalsOnly?: boolean;
excludeDirect?: boolean;
}; };
enum Direction { enum Direction {
@ -238,10 +241,10 @@ export const fetchThreads = async (
let currentPage = 0; let currentPage = 0;
const fetchThreadsFunc = async (opts: FetchThreadsOptions) => { const fetchThreadsFunc = async (opts: FetchThreadsOptions) => {
const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since} = opts; const {before, after, perPage = General.CRT_CHUNK_SIZE, deleted, unread, since, excludeDirect = false} = opts;
currentPage++; currentPage++;
const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version); const {threads} = await client.getThreads(currentUser.id, teamId, before, after, perPage, deleted, unread, since, false, version, excludeDirect);
if (threads.length) { if (threads.length) {
// Mark all fetched threads as following // Mark all fetched threads as following
for (const thread of threads) { for (const thread of threads) {
@ -277,7 +280,50 @@ export const fetchThreads = async (
return {error: false, threads: threadsData}; return {error: false, threads: threadsData};
}; };
export const syncTeamThreads = async (serverUrl: string, teamId: string, prepareRecordsOnly = false) => { export const syncThreadsIfNeeded = async (serverUrl: string, isCRTEnabled: boolean, teams?: Team[], fetchOnly = false) => {
try {
if (!isCRTEnabled) {
return {models: []};
}
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const promises = [];
const models: Model[][] = [];
// this is to keep backwards compatibility with servers that send the
// threads for DM / GM regardless for every team
const version = await getConfigValue(database, 'Version');
const hasThreadExclusions = isMinimumServerVersion(version, 10, 2, 0);
if (teams?.length) {
for (const team of teams) {
promises.push(syncTeamThreads(serverUrl, team.id, true, true));
}
}
if (promises.length) {
const results = await Promise.all(promises);
for (const r of results) {
if (r.models?.length) {
models.push(r.models);
}
}
}
const flat = models.flat();
if (!fetchOnly && flat.length) {
const uniqueArray = hasThreadExclusions ? flat : removeDuplicatesModels(flat);
await operator.batchRecords(uniqueArray, 'syncThreadsIfNeeded');
}
return {models: flat};
} catch (error) {
logError('syncThreadsIfNeeded: Error', error);
return {error, models: undefined};
}
};
export const syncTeamThreads = async (serverUrl: string, teamId: string, excludeDirect = false, fetchOnly = false) => {
try { try {
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl); const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const syncData = await getTeamThreadsSyncData(database, teamId); const syncData = await getTeamThreadsSyncData(database, teamId);
@ -299,13 +345,13 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
fetchThreads( fetchThreads(
serverUrl, serverUrl,
teamId, teamId,
{unread: true}, {unread: true, excludeDirect},
Direction.Down, Direction.Down,
), ),
fetchThreads( fetchThreads(
serverUrl, serverUrl,
teamId, teamId,
{}, {excludeDirect},
undefined, undefined,
1, 1,
), ),
@ -313,6 +359,9 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
if (allUnreadThreads.error || latestThreads.error) { if (allUnreadThreads.error || latestThreads.error) {
return {error: allUnreadThreads.error || latestThreads.error}; return {error: allUnreadThreads.error || latestThreads.error};
} }
const dedupe = new Set(latestThreads.threads?.map((t) => t.id));
if (latestThreads.threads?.length) { if (latestThreads.threads?.length) {
// We are fetching the threads for the first time. We get "latest" and "earliest" values. // We are fetching the threads for the first time. We get "latest" and "earliest" values.
const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads); const {earliestThread, latestThread} = getThreadsListEdges(latestThreads.threads);
@ -322,13 +371,14 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
threads.push(...latestThreads.threads); threads.push(...latestThreads.threads);
} }
if (allUnreadThreads.threads?.length) { if (allUnreadThreads.threads?.length) {
threads.push(...allUnreadThreads.threads); const unread = allUnreadThreads.threads.filter((u) => !dedupe.has(u.id));
threads.push(...unread);
} }
} else { } else {
const allNewThreads = await fetchThreads( const allNewThreads = await fetchThreads(
serverUrl, serverUrl,
teamId, teamId,
{deleted: true, since: syncData.latest + 1}, {deleted: true, since: syncData.latest + 1, excludeDirect},
); );
if (allNewThreads.error) { if (allNewThreads.error) {
return {error: allNewThreads.error}; return {error: allNewThreads.error};
@ -361,7 +411,7 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
} }
} }
if (!prepareRecordsOnly && models?.length) { if (!fetchOnly && models?.length) {
try { try {
await operator.batchRecords(models, 'syncTeamThreads'); await operator.batchRecords(models, 'syncTeamThreads');
} catch (err) { } catch (err) {
@ -372,7 +422,6 @@ export const syncTeamThreads = async (serverUrl: string, teamId: string, prepare
} }
} }
} }
return {error: false, models}; return {error: false, models};
} catch (error) { } catch (error) {
return {error}; return {error};

View file

@ -314,11 +314,11 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
} }
const {posts, order, authors, actionType, previousPostId} = await fetchPostsForChannel(serverUrl, channelId, true); const {posts, order, authors, actionType, previousPostId} = await fetchPostsForChannel(serverUrl, channelId, true);
if (posts?.length && order?.length) { if (posts?.length && order?.length && actionType) {
const {models: prepared} = await storePostsForChannel( const {models: prepared} = await storePostsForChannel(
serverUrl, channelId, serverUrl, channelId,
posts, order, previousPostId ?? '', posts, order, previousPostId ?? '',
actionType, authors, true, actionType, authors || [], true,
); );
if (prepared?.length) { if (prepared?.length) {

View file

@ -8,7 +8,7 @@ import {PER_PAGE_DEFAULT} from './constants';
import type ClientBase from './base'; import type ClientBase from './base';
export interface ClientThreadsMix { export interface ClientThreadsMix {
getThreads: (userId: string, teamId: string, before?: string, after?: string, pageSize?: number, deleted?: boolean, unread?: boolean, since?: number, totalsOnly?: boolean, serverVersion?: string) => 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) => Promise<GetUserThreadsResponse>;
getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>; getThread: (userId: string, teamId: string, threadId: string, extended?: boolean) => Promise<any>;
markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>; markThreadAsRead: (userId: string, teamId: string, threadId: string, timestamp: number) => Promise<any>;
markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise<any>; markThreadAsUnread: (userId: string, teamId: string, threadId: string, postId: string) => Promise<any>;
@ -17,7 +17,7 @@ export interface ClientThreadsMix {
} }
const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase) => class extends superclass { 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 = '') => { getThreads = async (userId: string, teamId: string, before = '', after = '', pageSize = PER_PAGE_DEFAULT, deleted = false, unread = false, since = 0, totalsOnly = false, serverVersion = '', excludeDirect = false) => {
const queryStringObj: Record<string, any> = { const queryStringObj: Record<string, any> = {
extended: 'true', extended: 'true',
before, before,
@ -26,6 +26,7 @@ const ClientThreads = <TBase extends Constructor<ClientBase>>(superclass: TBase)
unread, unread,
since, since,
totalsOnly, totalsOnly,
excludeDirect,
}; };
if (serverVersion && isMinimumServerVersion(serverVersion, 6, 0)) { if (serverVersion && isMinimumServerVersion(serverVersion, 6, 0)) {
queryStringObj.per_page = pageSize; queryStringObj.per_page = pageSize;

View file

@ -16,6 +16,11 @@ import TeamList from './team_list';
import type {WithDatabaseArgs} from '@typings/database/database'; import type {WithDatabaseArgs} from '@typings/database/database';
import type MyTeamModel from '@typings/database/models/servers/my_team'; import type MyTeamModel from '@typings/database/models/servers/my_team';
interface TeamWithLowerName {
myTeam: MyTeamModel;
lowerName?: string;
}
const withTeams = withObservables([], ({database}: WithDatabaseArgs) => { const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
const myTeams = queryMyTeams(database).observe(); const myTeams = queryMyTeams(database).observe();
const teamIds = queryJoinedTeams(database).observe().pipe( const teamIds = queryJoinedTeams(database).observe().pipe(
@ -26,26 +31,41 @@ const withTeams = withObservables([], ({database}: WithDatabaseArgs) => {
switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))), switchMap((p) => (p.length ? of$(p[0].value.split(',')) : of$([]))),
); );
const myOrderedTeams = combineLatest([myTeams, order, teamIds]).pipe( const myOrderedTeams = combineLatest([myTeams, order, teamIds]).pipe(
map(([ts, o, tids]) => { map(([memberships, o, teams]) => {
let ids: string[] = o; const sortedTeamIds = new Set(o);
if (!o.length) { const membershipMap = new Map(memberships.map((m) => [m.id, m]));
ids = tids.
sort((a, b) => a.displayName.toLocaleLowerCase().localeCompare(b.displayName.toLocaleLowerCase())). if (sortedTeamIds.size) {
map((t) => t.id); const mySortedTeams = [...sortedTeamIds].
filter((id) => membershipMap.has(id)).
map((id) => membershipMap.get(id)!);
const extraTeams = teams.
filter((t) => !sortedTeamIds.has(t.id) && membershipMap.has(t.id)).
map((t) => ({
myTeam: membershipMap.get(t.id)!,
lowerName: t.displayName.toLocaleLowerCase(),
} as TeamWithLowerName)).
sort((a, b) => a.lowerName!.localeCompare(b.lowerName!)).
map((t) => {
delete t.lowerName;
return t;
});
return [...mySortedTeams, ...extraTeams];
} }
const teamMap = ts.reduce<{[x: string]: MyTeamModel}>((result, team) => { return teams.
result[team.id] = team; filter((t) => membershipMap.has(t.id)).
return result; map((t) => ({
}, {}); myTeam: membershipMap.get(t.id)!,
lowerName: t.displayName.toLocaleLowerCase(),
return ids.reduce<MyTeamModel[]>((result, id) => { } as TeamWithLowerName)).
const t = teamMap[id]; sort((a, b) => a.lowerName!.localeCompare(b.lowerName!)).
if (t) { map((t) => {
result.push(t); delete t.lowerName;
} return t.myTeam;
return result; });
}, []);
}), }),
); );
return { return {

View file

@ -63,9 +63,12 @@ export const transformThreadRecord = ({action, database, value}: TransformerArgs
*/ */
export const transformThreadParticipantRecord = ({action, database, value}: TransformerArgs): Promise<ThreadParticipantModel> => { export const transformThreadParticipantRecord = ({action, database, value}: TransformerArgs): Promise<ThreadParticipantModel> => {
const raw = value.raw as ThreadParticipant; const raw = value.raw as ThreadParticipant;
const record = value.record as ThreadParticipantModel;
const isCreateAction = action === OperationType.CREATE;
// id of participant comes from server response // id of participant comes from server response
const fieldsMapper = (participant: ThreadParticipantModel) => { const fieldsMapper = (participant: ThreadParticipantModel) => {
participant._raw.id = isCreateAction ? `${raw.thread_id}-${raw.id}` : record.id;
participant.threadId = raw.thread_id; participant.threadId = raw.thread_id;
participant.userId = raw.id; participant.userId = raw.id;
}; };
@ -81,8 +84,11 @@ export const transformThreadParticipantRecord = ({action, database, value}: Tran
export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => { export const transformThreadInTeamRecord = ({action, database, value}: TransformerArgs): Promise<ThreadInTeamModel> => {
const raw = value.raw as ThreadInTeam; const raw = value.raw as ThreadInTeam;
const record = value.record as ThreadInTeamModel;
const isCreateAction = action === OperationType.CREATE;
const fieldsMapper = (threadInTeam: ThreadInTeamModel) => { const fieldsMapper = (threadInTeam: ThreadInTeamModel) => {
threadInTeam._raw.id = isCreateAction ? `${raw.thread_id}-${raw.team_id}` : record.id;
threadInTeam.threadId = raw.thread_id; threadInTeam.threadId = raw.thread_id;
threadInTeam.teamId = raw.team_id; threadInTeam.teamId = raw.team_id;
}; };

View file

@ -5,6 +5,7 @@ import xRegExp from 'xregexp';
import {General} from '@constants'; import {General} from '@constants';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel'; import type ChannelModel from '@typings/database/models/servers/channel';
import type MyChannelModel from '@typings/database/models/servers/my_channel'; import type MyChannelModel from '@typings/database/models/servers/my_channel';
@ -91,3 +92,15 @@ export const filterAndSortMyChannels = ([myChannels, channels, notifyProps]: Fil
// Matches letters from any alphabet and numbers // Matches letters from any alphabet and numbers
const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g'); const sqliteLikeStringRegex = xRegExp('[^\\p{L}\\p{Nd}]', 'g');
export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_'); export const sanitizeLikeString = (value: string) => value.replace(sqliteLikeStringRegex, '_');
export function removeDuplicatesModels(array: Model[]) {
const seen = new Set();
return array.filter((item) => {
const key = `${item.collection.table}-${item.id}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}

View file

@ -60,7 +60,7 @@ class NetworkManager {
sessionConfiguration: { sessionConfiguration: {
allowsCellularAccess: true, allowsCellularAccess: true,
waitsForConnectivity: false, waitsForConnectivity: false,
httpMaximumConnectionsPerHost: 10, httpMaximumConnectionsPerHost: 100,
cancelRequestsOnUnauthorized: true, cancelRequestsOnUnauthorized: true,
}, },
retryPolicyConfiguration: { retryPolicyConfiguration: {

View file

@ -1162,7 +1162,7 @@ PODS:
- Yoga - Yoga
- react-native-netinfo (11.3.2): - react-native-netinfo (11.3.2):
- React-Core - React-Core
- react-native-network-client (1.7.2): - react-native-network-client (1.7.3):
- Alamofire (~> 5.9.1) - Alamofire (~> 5.9.1)
- DoubleConversion - DoubleConversion
- glog - glog
@ -2043,7 +2043,7 @@ SPEC CHECKSUMS:
react-native-emm: ecab44d78fb1cc7d7b7901914f48fb6309c46ff2 react-native-emm: ecab44d78fb1cc7d7b7901914f48fb6309c46ff2
react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7 react-native-image-picker: c3afe5472ef870d98a4b28415fc0b928161ee5f7
react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc
react-native-network-client: 5173c47230b5f497cdef469cba8e6e1b3df687eb react-native-network-client: 82ef7c000c5fd3bfd1ad9e78cd920af33040f9f3
react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4 react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4
react-native-paste-input: 011a9916ef3acf809a7da122847c61ca0f93a60e react-native-paste-input: 011a9916ef3acf809a7da122847c61ca0f93a60e
react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d react-native-performance: ff93f8af3b2ee9519fd7879896aa9b8b8272691d

View file

@ -6,7 +6,7 @@ import React
@objc private var hasRegisteredLoad = false @objc private var hasRegisteredLoad = false
deinit { deinit {
DispatchQueue.main.async { DispatchQueue.main.sync {
guard let w = UIApplication.shared.delegate?.window, let window = w else { return } guard let w = UIApplication.shared.delegate?.window, let window = w else { return }
window.removeObserver(self, forKeyPath: "frame") window.removeObserver(self, forKeyPath: "frame")
} }

10
package-lock.json generated
View file

@ -6,7 +6,7 @@
"packages": { "packages": {
"": { "": {
"name": "mattermost-mobile", "name": "mattermost-mobile",
"version": "2.21.0", "version": "2.22.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache 2.0", "license": "Apache 2.0",
"dependencies": { "dependencies": {
@ -22,7 +22,7 @@
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard", "@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker", "@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
"@mattermost/react-native-emm": "1.5.0", "@mattermost/react-native-emm": "1.5.0",
"@mattermost/react-native-network-client": "1.7.2", "@mattermost/react-native-network-client": "1.7.3",
"@mattermost/react-native-paste-input": "0.8.0", "@mattermost/react-native-paste-input": "0.8.0",
"@mattermost/react-native-turbo-log": "0.4.0", "@mattermost/react-native-turbo-log": "0.4.0",
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare", "@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",
@ -5908,9 +5908,9 @@
} }
}, },
"node_modules/@mattermost/react-native-network-client": { "node_modules/@mattermost/react-native-network-client": {
"version": "1.7.2", "version": "1.7.3",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.7.2.tgz", "resolved": "https://registry.npmjs.org/@mattermost/react-native-network-client/-/react-native-network-client-1.7.3.tgz",
"integrity": "sha512-UaFhUaQkmb6Hq2LOyutiwL0HeMVHmhBBpgojLi83zXXXB5OgNWDgqbDBo6Y986c5TwhCy1/LxENdHBc+mI4RZA==", "integrity": "sha512-gJSQ4Prf5jcbPlxVylhEtBGafSBYZSat+T21LIDHksGOfeY+jvPL3p8dS+cq+0D1AOHQ3tHNBkZ7RaY6IdUadw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"validator": "13.12.0", "validator": "13.12.0",

View file

@ -23,7 +23,7 @@
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard", "@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker", "@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
"@mattermost/react-native-emm": "1.5.0", "@mattermost/react-native-emm": "1.5.0",
"@mattermost/react-native-network-client": "1.7.2", "@mattermost/react-native-network-client": "1.7.3",
"@mattermost/react-native-paste-input": "0.8.0", "@mattermost/react-native-paste-input": "0.8.0",
"@mattermost/react-native-turbo-log": "0.4.0", "@mattermost/react-native-turbo-log": "0.4.0",
"@mattermost/rnshare": "file:./libraries/@mattermost/rnshare", "@mattermost/rnshare": "file:./libraries/@mattermost/rnshare",