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:
parent
f6bc8ae50b
commit
ea2cb45a18
18 changed files with 364 additions and 118 deletions
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
10
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue