[Gekidou] app entry (#5609)

* Add channelEntry action

* Refactor appEntry

* Lint fix

* Some duplication removed

* Refactor app entry

* Refactor app entry and add prepareDestroyPermanentlyWithAssociations

* Handle team deletion

* Sync channels

* Lint fix

* Address review comments, take 1

* Address review comments, take 2

* Set initial channel ID to empty

* Fix types

* sort imports

* clean up app entry point

* Fix post header & highlight reply bar

* Style channel list text

* iOS Pods sync

* Rename goToChannel to goToHome

* Sync ios Gemfile.lock

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Miguel Alatzar
2021-09-29 08:38:05 -07:00
committed by GitHub
parent a021e42c80
commit 1d02d6e1e5
21 changed files with 607 additions and 85 deletions

View File

@@ -8,22 +8,26 @@ import {General, Preferences} from '@constants';
import DatabaseManager from '@database/manager';
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
import {selectDefaultTeam} from '@helpers/api/team';
import {DEFAULT_LOCALE} from '@i18n';
import NetworkManager from '@init/network_manager';
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
import {prepareMyPreferences} from '@queries/servers/preference';
import {prepareCommonSystemValues} from '@queries/servers/system';
import {addChannelToTeamHistory, prepareMyTeams} from '@queries/servers/team';
import {queryAllChannelsForTeam, queryChannelsById} from '@queries/servers/channel';
import {prepareModels} from '@queries/servers/entry';
import {prepareCommonSystemValues, queryCommonSystemValues, queryConfig, queryCurrentTeamId, queryWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {addChannelToTeamHistory, deleteMyTeams, queryAvailableTeamIds, queryMyTeams, queryTeamsById} from '@queries/servers/team';
import {queryCurrentUser} from '@queries/servers/user';
import {selectDefaultChannelForTeam} from '@utils/channel';
import {fetchMissingSidebarInfo, fetchMyChannelsForTeam, MyChannelsRequest} from './channel';
import {fetchGroupsForTeam} from './group';
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post';
import {MyPreferencesRequest, fetchMyPreferences} from './preference';
import {fetchRolesIfNeeded} from './role';
import {fetchRoles, fetchRolesIfNeeded} from './role';
import {ConfigAndLicenseRequest, fetchConfigAndLicense} from './systems';
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from './team';
import {fetchMe, MyUserRequest} from './user';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
type AfterLoginArgs = {
serverUrl: string;
@@ -31,10 +35,77 @@ type AfterLoginArgs = {
deviceToken?: string;
}
type AppEntryData = {
initialTeamId: string;
teamData: MyTeamsRequest;
chData?: MyChannelsRequest;
prefData: MyPreferencesRequest;
meData: MyUserRequest;
removeTeamIds?: string[];
removeChannelIds?: string[];
}
type AppEntryError = {
error?: Error | ClientError | string;
}
export const appEntry = async (serverUrl: string) => {
const dt = Date.now();
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
const {database} = operator;
const currentTeamId = await queryCurrentTeamId(database);
const fetchedData = await fetchAppEntryData(serverUrl, currentTeamId);
const fetchedError = (fetchedData as AppEntryError).error;
if (fetchedError) {
return {error: fetchedError, time: Date.now() - dt};
}
const {initialTeamId, teamData, chData, prefData, meData, removeTeamIds, removeChannelIds} = fetchedData as AppEntryData;
if (initialTeamId !== currentTeamId) {
// Immediately set the new team as the current team in the database so that the UI
// renders the correct team.
setCurrentTeamAndChannelId(operator, initialTeamId, '');
}
let removeTeams;
if (removeTeamIds?.length) {
// Immediately delete myTeams so that the UI renders only teams the user is a member of.
removeTeams = await queryTeamsById(database, removeTeamIds);
await deleteMyTeams(operator, removeTeams!);
}
fetchRoles(serverUrl, teamData?.memberships, chData?.memberships, meData?.user);
let removeChannels;
if (removeChannelIds?.length) {
removeChannels = await queryChannelsById(database, removeChannelIds);
}
const modelPromises = await prepareModels({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData});
const models = await Promise.all(modelPromises);
if (models.length) {
await operator.batchRecords(models.flat() as Model[]);
}
const {id: currentUserId, locale: currentUserLocale} = meData.user || (await queryCurrentUser(database))!;
const {config, license} = await queryCommonSystemValues(database);
deferredAppEntryActions(serverUrl, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
const error = teamData.error || chData?.error || prefData.error || meData.error;
return {error, time: Date.now() - dt};
};
export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) => {
const dt = Date.now();
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return {error: `${serverUrl} database not found`};
}
@@ -111,29 +182,7 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs)
}
}
const modelPromises: Array<Promise<Model[]>> = [];
const {operator} = DatabaseManager.serverDatabases[serverUrl];
if (prefData.preferences) {
const prefModel = prepareMyPreferences(operator, prefData.preferences!);
if (prefModel) {
modelPromises.push(prefModel);
}
}
if (teamData.teams) {
const teamModels = prepareMyTeams(operator, teamData.teams!, teamData.memberships!, teamData.unreads!);
if (teamModels) {
modelPromises.push(...teamModels);
}
}
if (chData?.channels?.length) {
const channelModels = await prepareMyChannelsForTeam(operator, initialTeam!.id, chData.channels, chData.memberships!);
if (channelModels) {
modelPromises.push(...channelModels);
}
}
const modelPromises = await prepareModels({operator, teamData, chData, prefData, initialTeamId: initialTeam?.id});
const systemModels = prepareCommonSystemValues(
operator,
@@ -167,7 +216,6 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs)
const error = clData.error || prefData.error || teamData.error || chData?.error;
return {error, time: Date.now() - dt, hasTeams: Boolean((myTeams?.length || 0) > 0 && !teamData.error)};
} catch (error) {
const {operator} = DatabaseManager.serverDatabases[serverUrl];
const systemModels = await prepareCommonSystemValues(operator, {
config: ({} as ClientConfig),
license: ({} as ClientLicense),
@@ -182,6 +230,145 @@ export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs)
}
};
const fetchAppEntryData = async (serverUrl: string, initialTeamId: string): Promise<AppEntryData | AppEntryError> => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
const lastDisconnected = await queryWebSocketLastDisconnected(database);
const includeDeletedChannels = true;
const fetchOnly = true;
// Fetch in parallel teams / team membership / team unreads / channels for current team / user preferences / user
const promises: [Promise<MyTeamsRequest>, Promise<MyChannelsRequest | undefined>, Promise<MyPreferencesRequest>, Promise<MyUserRequest>] = [
fetchMyTeams(serverUrl, fetchOnly),
initialTeamId ? fetchMyChannelsForTeam(serverUrl, initialTeamId, includeDeletedChannels, lastDisconnected, fetchOnly) : Promise.resolve(undefined),
fetchMyPreferences(serverUrl, fetchOnly),
fetchMe(serverUrl, fetchOnly),
];
const resolution = await Promise.all(promises);
const [teamData, , prefData, meData] = resolution;
let [, chData] = resolution;
if (!initialTeamId && teamData.teams?.length && teamData.memberships?.length) {
// If no initial team was set in the database but got teams in the response
const config = await queryConfig(database);
const teamOrderPreference = getPreferenceValue(prefData.preferences || [], Preferences.TEAMS_ORDER, '', '') as string;
const teamMembers = teamData.memberships.map((m) => m.team_id);
const myTeams = teamData.teams!.filter((t) => teamMembers?.includes(t.id));
const defaultTeam = selectDefaultTeam(myTeams, meData.user?.locale || DEFAULT_LOCALE, teamOrderPreference, config.ExperimentalPrimaryTeam);
if (defaultTeam?.id) {
chData = await fetchMyChannelsForTeam(serverUrl, defaultTeam.id, includeDeletedChannels, lastDisconnected, fetchOnly);
}
}
let data: AppEntryData = {
initialTeamId,
teamData,
chData,
prefData,
meData,
};
if (teamData.teams?.length === 0) {
// User is no longer a member of any team
const myTeams = await queryMyTeams(database);
const removeTeamIds: string[] = myTeams?.map((myTeam) => myTeam.id) || [];
return {
...data,
initialTeamId: '',
removeTeamIds,
};
}
const inTeam = teamData.teams?.find((t) => t.id === initialTeamId);
const chError = chData?.error as ClientError | undefined;
if (!inTeam || chError?.status_code === 403) {
// User is no longer a member of the current team
const removeTeamIds = [initialTeamId];
const availableTeamIds = await queryAvailableTeamIds(database, initialTeamId, teamData.teams, prefData.preferences, meData.user?.locale);
const alternateTeamData = await fetchAlternateTeamData(serverUrl, availableTeamIds, removeTeamIds, includeDeletedChannels, lastDisconnected, fetchOnly);
data = {
...data,
...alternateTeamData,
};
}
if (data.chData?.channels) {
const removeChannelIds: string[] = [];
const fetchedChannelIds = data.chData.channels.map((channel) => channel.id);
const channels = await queryAllChannelsForTeam(database, initialTeamId);
for (const channel of channels) {
if (!fetchedChannelIds.includes(channel.id)) {
removeChannelIds.push(channel.id);
}
}
data = {
...data,
removeChannelIds,
};
}
return data;
};
const fetchAlternateTeamData = async (serverUrl: string, availableTeamIds: string[], removeTeamIds: string[], includeDeleted = true, since = 0, fetchOnly = false) => {
let initialTeamId = '';
let chData;
for (const teamId of availableTeamIds) {
// eslint-disable-next-line no-await-in-loop
chData = await fetchMyChannelsForTeam(serverUrl, teamId, includeDeleted, since, fetchOnly);
const chError = chData.error as ClientError | undefined;
if (chError?.status_code === 403) {
removeTeamIds.push(teamId);
} else {
initialTeamId = teamId;
break;
}
}
if (chData) {
return {initialTeamId, chData, removeTeamIds};
}
return {initialTeamId, removeTeamIds};
};
const deferredAppEntryActions = async (
serverUrl: string, currentUserId: string, currentUserLocale: string, preferences: PreferenceType[] | undefined, config: ClientConfig, license: ClientLicense, teamData: MyTeamsRequest,
chData: MyChannelsRequest | undefined, initialTeamId: string) => {
// defer sidebar DM & GM profiles
if (chData?.channels?.length && chData.memberships?.length) {
const directChannels = chData.channels.filter((c) => c.type === General.DM_CHANNEL || c.type === General.GM_CHANNEL);
const channelsToFetchProfiles = new Set<Channel>(directChannels);
if (channelsToFetchProfiles.size) {
const teammateDisplayNameSetting = getTeammateNameDisplaySetting(preferences || [], config, license);
await fetchMissingSidebarInfo(serverUrl, Array.from(channelsToFetchProfiles), currentUserLocale, teammateDisplayNameSetting, currentUserId);
}
// defer fetching posts for unread channels on initial team
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships);
}
// defer groups for team
if (initialTeamId) {
await fetchGroupsForTeam(serverUrl, initialTeamId);
}
// defer fetch channels and unread posts for other teams
if (teamData.teams?.length && teamData.memberships?.length) {
fetchTeamsChannelsAndUnreadPosts(serverUrl, teamData.teams, teamData.memberships, initialTeamId);
}
};
const deferredLoginActions = async (
serverUrl: string, user: UserProfile, prefData: MyPreferencesRequest, clData: ConfigAndLicenseRequest, teamData: MyTeamsRequest,
chData?: MyChannelsRequest, initialTeam?: Team, initialChannel?: Channel) => {

View File

@@ -56,3 +56,30 @@ export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string
return {error};
}
};
export const fetchRoles = async (serverUrl: string, teamMembership?: TeamMembership[], channelMembership?: ChannelMembership[], user?: UserProfile) => {
const rolesToFetch = new Set<string>(user?.roles.split(' ') || []);
if (teamMembership?.length) {
const teamRoles: string[] = [];
const teamMembers: string[] = [];
teamMembership?.forEach((tm) => {
teamRoles.push(...tm.roles.split(' '));
teamMembers.push(tm.team_id);
});
teamRoles.forEach(rolesToFetch.add, rolesToFetch);
}
if (channelMembership?.length) {
for (let i = 0; i < channelMembership!.length; i++) {
const member = channelMembership[i];
member.roles.split(' ').forEach(rolesToFetch.add, rolesToFetch);
}
}
if (rolesToFetch.size > 0) {
fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch));
}
};

View File

@@ -20,6 +20,11 @@ import type {LoadMeArgs} from '@typings/database/database';
import type RoleModel from '@typings/database/models/servers/role';
import type UserModel from '@typings/database/models/servers/user';
export type MyUserRequest = {
user?: UserProfile;
error?: unknown;
}
export type ProfilesPerChannelRequest = {
data?: ProfilesInChannelRequest[];
error?: unknown;
@@ -31,6 +36,31 @@ export type ProfilesInChannelRequest = {
error?: unknown;
}
export const fetchMe = async (serverUrl: string, fetchOnly = false): Promise<MyUserRequest> => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const user = await client.getMe();
if (!fetchOnly) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
operator.handleUsers({users: [user], prepareRecordsOnly: false});
}
}
return {user};
} catch (error) {
await forceLogoutIfNecessary(serverUrl, error as ClientErrorProps);
return {error};
}
};
export const fetchProfilesInChannel = async (serverUrl: string, channelId: string, excludeUserId?: string, fetchOnly = false): Promise<ProfilesInChannelRequest> => {
let client: Client;
try {

View File

@@ -21,6 +21,7 @@ import type SystemModel from '@typings/database/models/servers/system';
type HeaderInputProps = {
config: ClientConfig;
differentThreadSequence: boolean;
license: ClientLicense;
preferences: PreferenceModel[];
post: PostModel;
@@ -33,8 +34,8 @@ const withBaseHeaderProps = withObservables([], ({database}: WithDatabaseArgs &
}));
const withHeaderProps = withObservables(
['preferences', 'post'],
({config, post, license, database, preferences}: WithDatabaseArgs & HeaderInputProps) => {
['preferences', 'post', 'differentThreadSequence'],
({config, post, license, database, preferences, differentThreadSequence}: WithDatabaseArgs & HeaderInputProps) => {
const author = post.author.observe();
const enablePostUsernameOverride = of$(config.EnablePostUsernameOverride === 'true');
const isTimezoneEnabled = of$(config.ExperimentalTimezone === 'true');
@@ -47,13 +48,13 @@ const withHeaderProps = withObservables(
Q.where('delete_at', Q.eq(0)),
),
).observeCount();
const rootPostAuthor = post.root.observe().pipe(switchMap((root) => {
const rootPostAuthor = differentThreadSequence ? post.root.observe().pipe(switchMap((root) => {
if (root.length) {
return root[0].author.observe();
}
return of$(null);
}));
})) : of$(null);
return {
author,

View File

@@ -40,7 +40,7 @@ async function shouldHighlightReplyBar(currentUser: UserModel, post: PostModel,
let rootPost: PostModel | undefined;
const myPosts = await postsInThread.collections.get(POST).query(
Q.and(
Q.where('root_id', post.id || post.rootId),
Q.where('root_id', post.rootId || post.id),
Q.where('create_at', Q.between(postsInThread.earliest, postsInThread.latest)),
Q.where('user_id', currentUser.id),
),
@@ -59,8 +59,8 @@ async function shouldHighlightReplyBar(currentUser: UserModel, post: PostModel,
commentsNotifyLevel = currentUser.notifyProps.comments;
}
const fromCurrentUser = post.userId !== currentUser.id || Boolean(post.props?.from_webhook);
if (!fromCurrentUser) {
const notCurrentUser = post.userId !== currentUser.id || Boolean(post.props?.from_webhook);
if (notCurrentUser) {
if (commentsNotifyLevel === Preferences.COMMENTS_ANY && (threadCreatedByCurrentUser || threadRepliedToByCurrentUser)) {
return true;
} else if (commentsNotifyLevel === Preferences.COMMENTS_ROOT && threadCreatedByCurrentUser) {
@@ -109,8 +109,9 @@ const withPost = withObservables(
return of$(false);
}));
let differentThreadSequence = true;
if (post.rootId) {
const differentThreadSequence = previousPost?.rootId ? previousPost?.rootId !== post.rootId : previousPost?.id !== post.rootId;
differentThreadSequence = previousPost?.rootId ? previousPost?.rootId !== post.rootId : previousPost?.id !== post.rootId;
isFirstReply = of$(differentThreadSequence || (previousPost?.id === post.rootId || previousPost?.rootId === post.rootId));
isLastReply = of$(!(nextPost?.rootId === post.rootId));
}
@@ -130,6 +131,7 @@ const withPost = withObservables(
appsEnabled: of$(appsEnabled(partialConfig)),
canDelete,
currentUser,
differentThreadSequence: of$(differentThreadSequence),
files: post.files.observe(),
highlightReplyBar,
isConsecutivePost,

View File

@@ -32,6 +32,7 @@ type PostProps = {
appsEnabled: boolean;
canDelete: boolean;
currentUser: UserModel;
differentThreadSequence: boolean;
files: FileModel[];
highlight?: boolean;
highlightPinnedOrFlagged?: boolean;
@@ -95,7 +96,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
});
const Post = ({
appsEnabled, canDelete, currentUser, files, highlight, highlightPinnedOrFlagged = true, highlightReplyBar,
appsEnabled, canDelete, currentUser, differentThreadSequence, files, highlight, highlightPinnedOrFlagged = true, highlightReplyBar,
isConsecutivePost, isEphemeral, isFirstReply, isFlagged, isJumboEmoji, isLastReply, isPostAddChannelMember,
location, post, reactionsCount, shouldRenderReplyButton, skipFlaggedHeader, skipPinnedHeader, showAddReaction = true, style,
testID,
@@ -207,6 +208,7 @@ const Post = ({
header = (
<Header
currentUser={currentUser}
differentThreadSequence={differentThreadSequence}
isAutoResponse={isAutoResponder}
isEphemeral={isEphemeral}
isPendingOrFailed={isPendingOrFailed}

View File

@@ -4,11 +4,12 @@
import {Linking} from 'react-native';
import {Notifications} from 'react-native-notifications';
import {appEntry} from '@actions/remote/entry';
import {Screens} from '@constants';
import DatabaseManager from '@database/manager';
import {getActiveServerUrl, getServerCredentials} from '@init/credentials';
import {queryThemeForCurrentTeam} from '@queries/servers/preference';
import {goToScreen, resetToChannel, resetToSelectServer} from '@screens/navigation';
import {goToScreen, resetToHome, resetToSelectServer} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkType, DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
import {parseDeepLink} from '@utils/url';
@@ -69,7 +70,9 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
if (database) {
EphemeralStore.theme = await queryThemeForCurrentTeam(database);
}
launchToChannel({...props, serverUrl}, resetNavigation);
launchToHome({...props, serverUrl}, resetNavigation);
return;
}
}
@@ -77,8 +80,20 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
launchToServer(props, resetNavigation);
};
const launchToChannel = (props: LaunchProps, resetNavigation: Boolean) => {
// TODO: Use LaunchProps to fetch posts for channel and then load user profile, etc...
const launchToHome = (props: LaunchProps, resetNavigation: Boolean) => {
switch (props.launchType) {
case LaunchType.DeepLink:
// TODO:
// deepLinkEntry({props.serverUrl, props.extra});
break;
case LaunchType.Notification: {
// TODO:
// pushNotificationEntry({props.serverUrl, props.extra})
break;
}
default:
appEntry(props.serverUrl!);
}
const passProps = {
skipMetrics: true,
@@ -87,8 +102,8 @@ const launchToChannel = (props: LaunchProps, resetNavigation: Boolean) => {
if (resetNavigation) {
// eslint-disable-next-line no-console
console.log('Launch app in Channel screen');
resetToChannel(passProps);
console.log('Launch app in Home screen');
resetToHome(passProps);
return;
}

View File

@@ -1,14 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {prepareDeletePost} from './post';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type ChannelModel from '@typings/database/models/servers/channel';
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type PostModel from '@typings/database/models/servers/post';
const {SERVER: {CHANNEL, MY_CHANNEL}} = MM_TABLES;
@@ -53,6 +56,41 @@ export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, tea
}
};
export const prepareDeleteChannel = async (channel: ChannelModel): Promise<Model[]> => {
const preparedModels: Model[] = [channel.prepareDestroyPermanently()];
const relations: Array<Relation<Model>> = [channel.membership, channel.info, channel.settings];
for await (const relation of relations) {
try {
const model = await relation.fetch();
if (model) {
preparedModels.push(model.prepareDestroyPermanently());
}
} catch {
// Record not found, do nothing
}
}
const associatedChildren: Array<Query<any>> = [
channel.members,
channel.drafts,
channel.groupsChannel,
channel.postsInChannel,
];
for await (const children of associatedChildren) {
const models = await children.fetch() as Model[];
models.forEach((model) => preparedModels.push(model.prepareDestroyPermanently()));
}
const posts = await channel.posts.fetch() as PostModel[];
for await (const post of posts) {
const preparedPost = await prepareDeletePost(post);
preparedModels.push(...preparedPost);
}
return preparedModels;
};
export const queryAllChannelsForTeam = (database: Database, teamId: string) => {
return database.get(CHANNEL).query(Q.where('team_id', teamId)).fetch() as Promise<ChannelModel[]>;
};
@@ -78,3 +116,12 @@ export const queryChannelByName = async (database: Database, channelName: string
return undefined;
}
};
export const queryChannelsById = async (database: Database, channelIds: string[]): Promise<ChannelModel[]|undefined> => {
try {
const channels = (await database.get(CHANNEL).query(Q.where('id', Q.oneOf(channelIds))).fetch()) as ChannelModel[];
return channels;
} catch {
return undefined;
}
};

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ServerDataOperator from '@database/operator/server_data_operator';
import {prepareDeleteChannel, prepareMyChannelsForTeam} from './channel';
import {prepareMyPreferences} from './preference';
import {prepareDeleteTeam, prepareMyTeams} from './team';
import {prepareUsers} from './user';
import type {MyChannelsRequest} from '@actions/remote/channel';
import type {MyPreferencesRequest} from '@actions/remote/preference';
import type {MyTeamsRequest} from '@actions/remote/team';
import type {MyUserRequest} from '@actions/remote/user';
import type {Model} from '@nozbe/watermelondb';
import type ChannelModel from '@typings/database/models/servers/channel';
import type TeamModel from '@typings/database/models/servers/team';
type PrepareModelsArgs = {
operator: ServerDataOperator;
initialTeamId?: string;
removeTeams?: TeamModel[];
removeChannels?: ChannelModel[];
teamData?: MyTeamsRequest;
chData?: MyChannelsRequest;
prefData?: MyPreferencesRequest;
meData?: MyUserRequest;
}
export const prepareModels = async ({operator, initialTeamId, removeTeams, removeChannels, teamData, chData, prefData, meData}: PrepareModelsArgs): Promise<Array<Promise<Model[]>>> => {
const modelPromises: Array<Promise<Model[]>> = [];
if (removeTeams?.length) {
removeTeams.forEach((team) => {
modelPromises.push(prepareDeleteTeam(team));
});
}
if (removeChannels?.length) {
removeChannels.forEach((channel) => {
modelPromises.push(prepareDeleteChannel(channel));
});
}
if (teamData?.teams?.length) {
const teamModels = prepareMyTeams(operator, teamData.teams, teamData.memberships || [], teamData.unreads || []);
if (teamModels) {
modelPromises.push(...teamModels);
}
}
if (initialTeamId && chData?.channels?.length) {
const channelModels = await prepareMyChannelsForTeam(operator, initialTeamId, chData.channels, chData.memberships || []);
if (channelModels) {
modelPromises.push(...channelModels);
}
}
if (prefData?.preferences?.length) {
const prefModel = prepareMyPreferences(operator, prefData.preferences);
if (prefModel) {
modelPromises.push(prefModel);
}
}
if (meData?.user) {
const userModels = prepareUsers(operator, [meData.user]);
if (userModels) {
modelPromises.push(userModels);
}
}
return modelPromises;
};

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
@@ -10,6 +10,33 @@ import type PostInChannelModel from '@typings/database/models/servers/posts_in_c
const {SERVER: {POST, POSTS_IN_CHANNEL}} = MM_TABLES;
export const prepareDeletePost = async (post: PostModel): Promise<Model[]> => {
const preparedModels: Model[] = [post.prepareDestroyPermanently()];
const relations: Array<Relation<Model> | Query<Model>> = [post.drafts, post.postsInThread];
for await (const relation of relations) {
try {
const model = await relation.fetch();
if (model) {
if (Array.isArray(model)) {
model.forEach((m) => preparedModels.push(m.prepareDestroyPermanently()));
} else {
preparedModels.push(model.prepareDestroyPermanently());
}
}
} catch {
// Record not found, do nothing
}
}
const associatedChildren: Array<Query<any>> = [post.files, post.reactions];
for await (const children of associatedChildren) {
const models = await children.fetch() as Model[];
models.forEach((model) => preparedModels.push(model.prepareDestroyPermanently()));
}
return preparedModels;
};
export const queryPostById = async (database: Database, postId: string) => {
try {
const userRecord = (await database.collections.get(MM_TABLES.SERVER.POST).find(postId)) as PostModel;

View File

@@ -81,6 +81,15 @@ export const queryCommonSystemValues = async (serverDatabase: Database) => {
};
};
export const queryConfig = async (serverDatabase: Database) => {
try {
const config = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CONFIG) as SystemModel;
return (config?.value || {}) as ClientConfig;
} catch {
return {} as ClientConfig;
}
};
export const queryExpandedLinks = async (serverDatabase: Database) => {
try {
const expandedLinks = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.EXPANDED_LINKS) as SystemModel;

View File

@@ -1,11 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {Database, Model, Q, Query, Relation} from '@nozbe/watermelondb';
import {Database as DatabaseConstants} from '@constants';
import {Database as DatabaseConstants, Preferences} from '@constants';
import {getPreferenceValue} from '@helpers/api/preference';
import {selectDefaultTeam} from '@helpers/api/team';
import {prepareDeleteChannel} from './channel';
import {queryPreferencesByCategoryAndName} from './preference';
import {queryConfig} from './system';
import {queryCurrentUser} from './user';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type ChannelModel from '@typings/database/models/servers/channel';
import type MyTeamModel from '@typings/database/models/servers/my_team';
import type TeamModel from '@typings/database/models/servers/team';
import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history';
@@ -58,6 +66,51 @@ export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memb
}
};
export const deleteMyTeams = async (operator: ServerDataOperator, teams: TeamModel[]) => {
const preparedModels: Model[] = [];
for await (const team of teams) {
const myTeam = await team.myTeam.fetch() as MyTeamModel;
preparedModels.push(myTeam.prepareDestroyPermanently());
}
await operator.batchRecords(preparedModels);
};
export const prepareDeleteTeam = async (team: TeamModel): Promise<Model[]> => {
const preparedModels: Model[] = [team.prepareDestroyPermanently()];
const relations: Array<Relation<Model>> = [team.myTeam, team.teamChannelHistory];
for await (const relation of relations) {
try {
const model = await relation.fetch();
if (model) {
preparedModels.push(model.prepareDestroyPermanently());
}
} catch {
// Record not found, do nothing
}
}
const associatedChildren: Array<Query<any>> = [
team.members,
team.groupsTeam,
team.slashCommands,
team.teamSearchHistories,
];
for await (const children of associatedChildren) {
const models = await children.fetch() as Model[];
models.forEach((model) => preparedModels.push(model.prepareDestroyPermanently()));
}
const channels = await team.channels.fetch() as ChannelModel[];
for await (const channel of channels) {
const preparedChannel = await prepareDeleteChannel(channel);
preparedModels.push(...preparedChannel);
}
return preparedModels;
};
export const queryMyTeamById = async (database: Database, teamId: string): Promise<MyTeamModel|undefined> => {
try {
const myTeam = (await database.get(MY_TEAM).find(teamId)) as MyTeamModel;
@@ -76,6 +129,15 @@ export const queryTeamById = async (database: Database, teamId: string): Promise
}
};
export const queryTeamsById = async (database: Database, teamIds: string[]): Promise<TeamModel[]|undefined> => {
try {
const teams = (await database.get(TEAM).query(Q.where('id', Q.oneOf(teamIds))).fetch()) as TeamModel[];
return teams;
} catch {
return undefined;
}
};
export const queryTeamByName = async (database: Database, teamName: string): Promise<TeamModel|undefined> => {
try {
const team = (await database.get(TEAM).query(Q.where('name', teamName)).fetch()) as TeamModel[];
@@ -88,3 +150,39 @@ export const queryTeamByName = async (database: Database, teamName: string): Pro
return undefined;
}
};
export const queryMyTeams = async (database: Database): Promise<MyTeamModel[]|undefined> => {
try {
const teams = (await database.get(MY_TEAM).query().fetch()) as MyTeamModel[];
return teams;
} catch {
return undefined;
}
};
export const queryAvailableTeamIds = async (database: Database, excludeTeamId: string, teams?: Team[], preferences?: PreferenceType[], locale?: string): Promise<string[]> => {
let availableTeamIds: string[] = [];
if (teams) {
let teamOrderPreference;
if (preferences) {
teamOrderPreference = getPreferenceValue(preferences, Preferences.TEAMS_ORDER, '', '') as string;
} else {
const dbPreferences = await queryPreferencesByCategoryAndName(database, Preferences.TEAMS_ORDER, '');
teamOrderPreference = dbPreferences[0].value;
}
const userLocale = locale || (await queryCurrentUser(database))?.locale;
const config = await queryConfig(database);
const defaultTeam = selectDefaultTeam(teams, userLocale, teamOrderPreference, config.ExperimentalPrimaryTeam);
availableTeamIds = [defaultTeam!.id];
} else {
const dbTeams = await queryMyTeams(database);
if (dbTeams) {
availableTeamIds = dbTeams.map((team) => team.id);
}
}
return availableTeamIds.filter((id) => id !== excludeTeamId);
};

View File

@@ -81,7 +81,7 @@ const ChannelListScreen = (props: ChannelProps) => {
<View style={[styles.flex, {alignItems: 'center', justifyContent: 'center'}, tabletSidebarStyle]}>
<Text
onPress={() => goToScreen('Channel', '', undefined, {topBar: {visible: false}})}
style={{fontSize: 20, color: '#fff'}}
style={{fontSize: 20, color: theme.centerChannelColor}}
>
{'Channel List'}
</Text>

View File

@@ -27,7 +27,7 @@ import ErrorText from '@components/error_text';
import FormattedText from '@components/formatted_text';
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
import {t} from '@i18n';
import {goToScreen, resetToChannel} from '@screens/navigation';
import {goToScreen, resetToHome} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -136,13 +136,13 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
console.log('GO TO NO TEAMS');
return;
}
await goToChannel(result.time || 0, result.error as never);
goToHome(result.time || 0, result.error as never);
}
};
const goToChannel = async (time: number, loginError?: never) => {
const goToHome = (time: number, loginError?: never) => {
const hasError = launchError || Boolean(loginError);
resetToChannel({extra, launchError: hasError, launchType, serverUrl, time});
resetToHome({extra, launchError: hasError, launchType, serverUrl, time});
};
const checkLoginResponse = (data: LoginActionResponse) => {
@@ -172,7 +172,7 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
const goToMfa = () => {
const screen = MFA;
const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'});
goToScreen(screen, title, {goToChannel, loginId, password, config, license, serverUrl, theme});
goToScreen(screen, title, {goToHome, loginId, password, config, license, serverUrl, theme});
};
const getLoginErrorMessage = (loginError: string | ClientErrorProps | Error) => {

View File

@@ -115,7 +115,7 @@ describe('Login', () => {
'MFA',
'Multi-factor Authentication',
{
goToChannel: expect.anything(),
goToHome: expect.anything(),
loginId,
password,
config: {EnableSignInWithEmail: 'true', EnableSignInWithUsername: 'true'},

View File

@@ -27,7 +27,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type MFAProps = {
config: Partial<ClientConfig>;
goToChannel: (time: number, error?: never) => void;
goToHome: (time: number, error?: never) => void;
license: Partial<ClientLicense>;
loginId: string;
password: string;
@@ -35,7 +35,7 @@ type MFAProps = {
theme: Theme;
}
const MFA = ({config, goToChannel, license, loginId, password, serverUrl, theme}: MFAProps) => {
const MFA = ({config, goToHome, license, loginId, password, serverUrl, theme}: MFAProps) => {
const intl = useIntl();
const [token, setToken] = useState<string>('');
const [error, setError] = useState<string>('');
@@ -98,7 +98,7 @@ const MFA = ({config, goToChannel, license, loginId, password, serverUrl, theme}
console.log('GO TO NO TEAMS');
return;
}
goToChannel(result.time || 0, result.error as never);
goToHome(result.time || 0, result.error as never);
});
const getProceedView = () => {

View File

@@ -18,7 +18,7 @@ jest.mock('@actions/remote/session', () => {
describe('*** MFA Screen ***', () => {
const baseProps = {
config: {},
goToChannel: jest.fn(),
goToHome: jest.fn(),
loginId: 'loginId',
password: 'passwd',
license: {},
@@ -34,10 +34,10 @@ describe('*** MFA Screen ***', () => {
test('should call login method on submit', async () => {
const props = {
...baseProps,
goToChannel: jest.fn(),
goToHome: jest.fn(),
};
const spyOnGoToChannel = jest.spyOn(props, 'goToChannel');
const spyOnGoToHome = jest.spyOn(props, 'goToHome');
const {getByTestId} = renderWithIntl(<Mfa {...props}/>);
const submitBtn = getByTestId('login_mfa.submit');
const inputText = getByTestId('login_mfa.input');
@@ -47,6 +47,6 @@ describe('*** MFA Screen ***', () => {
fireEvent.press(submitBtn);
});
expect(spyOnGoToChannel).toHaveBeenCalled();
expect(spyOnGoToHome).toHaveBeenCalled();
});
});

View File

@@ -29,7 +29,7 @@ function getThemeFromState() {
return Preferences.THEMES.denim;
}
export function resetToChannel(passProps = {}) {
export function resetToHome(passProps = {}) {
const theme = getThemeFromState();
EphemeralStore.clearNavigationComponents();

View File

@@ -7,7 +7,7 @@ import React, {useState} from 'react';
import {ssoLogin} from '@actions/remote/session';
import ClientError from '@client/rest/error';
import {SSO as SSOEnum} from '@constants';
import {resetToChannel} from '@screens/navigation';
import {resetToHome} from '@screens/navigation';
import {isMinimumServerVersion} from '@utils/helpers';
import SSOWithRedirectURL from './sso_with_redirect_url';
@@ -83,12 +83,12 @@ const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}
console.log('GO TO NO TEAMS');
return;
}
goToChannel(result.time || 0, result.error as never);
goToHome(result.time || 0, result.error as never);
};
const goToChannel = (time: number, error?: never) => {
const goToHome = (time: number, error?: never) => {
const hasError = launchError || Boolean(error);
resetToChannel({extra, launchError: hasError, launchType, serverUrl, time});
resetToHome({extra, launchError: hasError, launchType, serverUrl, time});
};
const isSSOWithRedirectURLAvailable = isMinimumServerVersion(config.Version!, 5, 33, 0);

View File

@@ -1,3 +1,3 @@
source "https://rubygems.org"
gem "cocoapods", "1.10.1"
gem "cocoapods", "1.10.2"

View File

@@ -1,23 +1,24 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.3)
activesupport (5.2.5)
CFPropertyList (3.0.4)
rexml
activesupport (5.2.6)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
algoliasearch (1.27.5)
httpclient (~> 2.8, >= 2.8.3)
json (>= 1.5.1)
atomos (0.1.3)
claide (1.0.3)
cocoapods (1.10.1)
cocoapods (1.10.2)
addressable (~> 2.6)
claide (>= 1.0.2, < 2.0)
cocoapods-core (= 1.10.1)
cocoapods-core (= 1.10.2)
cocoapods-deintegrate (>= 1.0.3, < 2.0)
cocoapods-downloader (>= 1.4.0, < 2.0)
cocoapods-plugins (>= 1.0.0, < 2.0)
@@ -32,7 +33,7 @@ GEM
nap (~> 1.0)
ruby-macho (~> 1.4)
xcodeproj (>= 1.19.0, < 2.0)
cocoapods-core (1.10.1)
cocoapods-core (1.10.2)
activesupport (> 5.0, < 6)
addressable (~> 2.6)
algoliasearch (~> 1.0)
@@ -42,21 +43,21 @@ GEM
netrc (~> 0.11)
public_suffix
typhoeus (~> 1.0)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.4.0)
cocoapods-deintegrate (1.0.5)
cocoapods-downloader (1.5.1)
cocoapods-plugins (1.0.0)
nap
cocoapods-search (1.0.0)
cocoapods-trunk (1.5.0)
cocoapods-search (1.0.1)
cocoapods-trunk (1.6.0)
nap (>= 0.8, < 2.0)
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.1.8)
concurrent-ruby (1.1.9)
escape (0.0.4)
ethon (0.13.0)
ethon (0.14.0)
ffi (>= 1.15.0)
ffi (1.15.0)
ffi (1.15.4)
fourflusher (2.3.1)
fuzzy_match (2.0.4)
gh_inspector (1.1.3)
@@ -70,24 +71,26 @@ GEM
nap (1.1.0)
netrc (0.11.0)
public_suffix (4.0.6)
rexml (3.2.5)
ruby-macho (1.4.0)
thread_safe (0.3.6)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.9)
thread_safe (~> 0.1)
xcodeproj (1.19.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
PLATFORMS
ruby
DEPENDENCIES
cocoapods (= 1.10.1)
cocoapods (= 1.10.2)
BUNDLED WITH
2.1.4