forked from Ivasoft/mattermost-mobile
[Gekidou] Login entry point (#5568)
* Login entry point * feedback review * sort imports * Fix model relations * Handle when no current team or current channel has been selected * Fix MFA unit test * update prepareCommonSystemValues arguments
This commit is contained in:
@@ -29,6 +29,7 @@
|
||||
"no-shadow": "off",
|
||||
"react/display-name": [2, { "ignoreTranspilerName": false }],
|
||||
"react/jsx-filename-extension": 0,
|
||||
"react-hooks/exhaustive-deps": 0,
|
||||
"camelcase": [
|
||||
0,
|
||||
{
|
||||
|
||||
@@ -136,7 +136,6 @@ android {
|
||||
missingDimensionStrategy "RNNotifications.reactNativeVersion", "reactNative60"
|
||||
versionCode 333
|
||||
versionName "2.0.0"
|
||||
multiDexEnabled = true
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
@@ -2,25 +2,14 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import {IntlShape} from 'react-intl';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getSessions} from '@actions/remote/session';
|
||||
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import {queryCommonSystemValues} from '@app/queries/servers/system';
|
||||
import {getSessions} from '@actions/remote/user';
|
||||
|
||||
const sortByNewest = (a: Session, b: Session) => {
|
||||
if (a.create_at > b.create_at) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
export const scheduleExpiredNotification = async (serverUrl: string, intl: IntlShape) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl].database;
|
||||
const {currentUserId, config}: {currentUserId: string; config: Partial<ClientConfig>} = await queryCommonSystemValues(database);
|
||||
import {sortByNewest} from '@utils/general';
|
||||
import {createIntl} from 'react-intl';
|
||||
|
||||
export const scheduleExpiredNotification = async (serverUrl: string, config: Partial<ClientConfig>, userId: string, locale = DEFAULT_LOCALE) => {
|
||||
if (config.ExtendSessionLengthWithActivity === 'true') {
|
||||
PushNotifications.cancelAllLocalNotifications();
|
||||
return null;
|
||||
@@ -31,7 +20,7 @@ export const scheduleExpiredNotification = async (serverUrl: string, intl: IntlS
|
||||
let sessions: Session[]|undefined;
|
||||
|
||||
try {
|
||||
sessions = await getSessions(serverUrl, currentUserId);
|
||||
sessions = await getSessions(serverUrl, userId);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Failed to get user sessions', e);
|
||||
@@ -43,23 +32,19 @@ export const scheduleExpiredNotification = async (serverUrl: string, intl: IntlS
|
||||
}
|
||||
|
||||
const session = sessions.sort(sortByNewest)[0];
|
||||
const expiresAt = session?.expires_at ? parseInt(session.expires_at as string, 10) : 0;
|
||||
const expiresAt = session?.expires_at || 0;
|
||||
const expiresInDays = Math.ceil(Math.abs(moment.duration(moment().diff(moment(expiresAt))).asDays()));
|
||||
const message = intl.formatMessage(
|
||||
{
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.'},
|
||||
{
|
||||
siteName: config.SiteName,
|
||||
daysCount: expiresInDays,
|
||||
},
|
||||
);
|
||||
const intl = createIntl({locale, messages: getTranslations(locale)});
|
||||
const body = intl.formatMessage({
|
||||
id: 'mobile.session_expired',
|
||||
defaultMessage: 'Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.',
|
||||
}, {siteName: config.SiteName, daysCount: expiresInDays});
|
||||
|
||||
if (expiresAt) {
|
||||
if (expiresAt && body) {
|
||||
//@ts-expect-error: Does not need to set all Notification properties
|
||||
PushNotifications.scheduleNotification({
|
||||
fireDate: expiresAt,
|
||||
body: message,
|
||||
body,
|
||||
payload: {
|
||||
userInfo: {
|
||||
local: true,
|
||||
@@ -23,7 +23,7 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use
|
||||
return {error: `No database present for ${serverUrl}`};
|
||||
}
|
||||
|
||||
const currentUser = await queryUserById({userId, database}) ?? null;
|
||||
const currentUser = await queryUserById(database, userId) ?? null;
|
||||
|
||||
if (!currentUser) {
|
||||
return null;
|
||||
|
||||
109
app/actions/remote/channel.ts
Normal file
109
app/actions/remote/channel.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {displayGroupMessageName, displayUsername} from '@utils/user';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
import {fetchProfilesPerChannels} from './user';
|
||||
|
||||
export type MyChannelsRequest = {
|
||||
channels?: Channel[];
|
||||
memberships?: ChannelMembership[];
|
||||
error?: never;
|
||||
}
|
||||
|
||||
export const fetchMyChannelsForTeam = async (serverUrl: string, teamId: string, includeDeleted = true, since = 0, fetchOnly = false, excludeDirect = false): Promise<MyChannelsRequest> => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
let [channels, memberships] = await Promise.all<Channel[], ChannelMembership[]>([
|
||||
client.getMyChannels(teamId, includeDeleted, since),
|
||||
client.getMyChannelMembers(teamId),
|
||||
]);
|
||||
|
||||
if (excludeDirect) {
|
||||
channels = channels.filter((c) => c.type !== General.GM_CHANNEL && c.type !== General.DM_CHANNEL);
|
||||
}
|
||||
|
||||
const channelIds = new Set<string>(channels.map((c) => c.id));
|
||||
memberships = memberships.reduce((result: ChannelMembership[], m: ChannelMembership) => {
|
||||
if (channelIds.has(m.channel_id)) {
|
||||
result.push(m);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
if (operator) {
|
||||
const prepare = await prepareMyChannelsForTeam(operator, teamId, channels, memberships);
|
||||
if (prepare) {
|
||||
modelPromises.push(...prepare);
|
||||
}
|
||||
if (modelPromises.length) {
|
||||
const models = await Promise.all(modelPromises);
|
||||
const flattenedModels = models.flat() as Model[];
|
||||
if (flattenedModels?.length > 0) {
|
||||
try {
|
||||
await operator.batchRecords(flattenedModels);
|
||||
} catch {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FAILED TO BATCH CHANNELS');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {channels, memberships};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, exludeUserId?: string) => {
|
||||
const channelIds = directChannels.map((dc) => dc.id);
|
||||
const result = await fetchProfilesPerChannels(serverUrl, channelIds, exludeUserId, false);
|
||||
if (result.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayNameByChannel: Record<string, string> = {};
|
||||
|
||||
if (result.data) {
|
||||
result.data.forEach((data) => {
|
||||
if (data.users) {
|
||||
if (data.users.length > 1) {
|
||||
displayNameByChannel[data.channelId] = displayGroupMessageName(data.users, locale, teammateDisplayNameSetting, exludeUserId);
|
||||
} else {
|
||||
displayNameByChannel[data.channelId] = displayUsername(data.users[0], locale, teammateDisplayNameSetting);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
directChannels.forEach((c) => {
|
||||
const displayName = displayNameByChannel[c.id];
|
||||
if (displayName) {
|
||||
c.display_name = displayName;
|
||||
}
|
||||
});
|
||||
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
operator.handleChannel({channels: directChannels, prepareRecordsOnly: false});
|
||||
}
|
||||
};
|
||||
214
app/actions/remote/entry.ts
Normal file
214
app/actions/remote/entry.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import {scheduleExpiredNotification} from '@actions/local/push_notification';
|
||||
import {General, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
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 {selectDefaultChannelForTeam} from '@utils/channel';
|
||||
|
||||
import {fetchMissingSidebarInfo, fetchMyChannelsForTeam, MyChannelsRequest} from './channel';
|
||||
import {fetchPostsForChannel, fetchPostsForUnreadChannels} from './post';
|
||||
import {MyPreferencesRequest, fetchMyPreferences} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {ConfigAndLicenseRequest, fetchConfigAndLicense} from './systems';
|
||||
import {fetchMyTeams, fetchTeamsChannelsAndUnreadPosts, MyTeamsRequest} from './team';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
type AfterLoginArgs = {
|
||||
serverUrl: string;
|
||||
user: UserProfile;
|
||||
deviceToken?: string;
|
||||
}
|
||||
|
||||
export const loginEntry = async ({serverUrl, user, deviceToken}: AfterLoginArgs) => {
|
||||
const dt = Date.now();
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
if (deviceToken) {
|
||||
try {
|
||||
client.attachDevice(deviceToken);
|
||||
} catch {
|
||||
// do nothing, the token could've failed to attach to the session but is not a blocker
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let initialTeam: Team|undefined;
|
||||
let initialChannel: Channel|undefined;
|
||||
let myTeams: Team[]|undefined;
|
||||
|
||||
// Fetch in parallel server config & license / user preferences / teams / team membership / team unreads
|
||||
const promises: [Promise<ConfigAndLicenseRequest>, Promise<MyPreferencesRequest>, Promise<MyTeamsRequest>] = [
|
||||
fetchConfigAndLicense(serverUrl, true),
|
||||
fetchMyPreferences(serverUrl, true),
|
||||
fetchMyTeams(serverUrl, true),
|
||||
];
|
||||
|
||||
const [clData, prefData, teamData] = await Promise.all(promises);
|
||||
let chData: MyChannelsRequest|undefined;
|
||||
|
||||
// schedule local push notification if needed
|
||||
if (clData.config) {
|
||||
scheduleExpiredNotification(serverUrl, clData.config, user.id, user.locale);
|
||||
}
|
||||
|
||||
// select initial team
|
||||
if (!clData.error && !prefData.error && !teamData.error) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences!, Preferences.TEAMS_ORDER, '', '') as string;
|
||||
const teamRoles: string[] = [];
|
||||
const teamMembers: string[] = [];
|
||||
|
||||
teamData.memberships?.forEach((tm) => {
|
||||
teamRoles.push(...tm.roles.split(' '));
|
||||
teamMembers.push(tm.team_id);
|
||||
});
|
||||
|
||||
myTeams = teamData.teams!.filter((t) => teamMembers?.includes(t.id));
|
||||
initialTeam = selectDefaultTeam(myTeams, user.locale, teamOrderPreference, clData.config?.ExperimentalPrimaryTeam);
|
||||
|
||||
if (initialTeam) {
|
||||
const rolesToFetch = new Set<string>([...user.roles.split(' '), ...teamRoles]);
|
||||
|
||||
// fetch channels / channel membership for initial team
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, initialTeam.id, false, 0, true);
|
||||
if (chData.channels?.length && chData.memberships?.length) {
|
||||
const {channels, memberships} = chData;
|
||||
const channelIds = new Set(channels?.map((c) => c.id));
|
||||
for (let i = 0; i < memberships!.length; i++) {
|
||||
const member = memberships[i];
|
||||
if (channelIds.has(member.channel_id)) {
|
||||
member.roles.split(' ').forEach(rolesToFetch.add, rolesToFetch);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch user roles
|
||||
const rData = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch));
|
||||
|
||||
// select initial channel
|
||||
initialChannel = selectDefaultChannelForTeam(channels!, memberships!, initialTeam!.id, rData.roles, user.locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 systemModels = prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config: clData.config || ({} as ClientConfig),
|
||||
license: clData.license || ({} as ClientLicense),
|
||||
currentTeamId: initialTeam?.id || '',
|
||||
currentChannelId: initialChannel?.id || '',
|
||||
},
|
||||
);
|
||||
if (systemModels) {
|
||||
modelPromises.push(systemModels);
|
||||
}
|
||||
|
||||
if (initialTeam && initialChannel) {
|
||||
try {
|
||||
const tch = addChannelToTeamHistory(operator, initialTeam.id, initialChannel.id, true);
|
||||
modelPromises.push(tch);
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models.flat() as Model[]);
|
||||
}
|
||||
|
||||
deferredLoginActions(serverUrl, user, prefData, clData, teamData, chData, initialTeam, initialChannel);
|
||||
|
||||
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),
|
||||
currentTeamId: '',
|
||||
currentChannelId: '',
|
||||
});
|
||||
if (systemModels) {
|
||||
await operator.batchRecords(systemModels);
|
||||
}
|
||||
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
const deferredLoginActions = async (
|
||||
serverUrl: string, user: UserProfile, prefData: MyPreferencesRequest, clData: ConfigAndLicenseRequest, teamData: MyTeamsRequest,
|
||||
chData?: MyChannelsRequest, initialTeam?: Team, initialChannel?: Channel) => {
|
||||
// defer fetching posts for initial channel
|
||||
if (initialChannel) {
|
||||
fetchPostsForChannel(serverUrl, initialChannel.id);
|
||||
}
|
||||
|
||||
// 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(prefData.preferences || [], clData.config, clData.license);
|
||||
await fetchMissingSidebarInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
// defer fetching posts for unread channels on initial team
|
||||
fetchPostsForUnreadChannels(serverUrl, chData.channels, chData.memberships, initialChannel?.id);
|
||||
}
|
||||
|
||||
// defer groups for team
|
||||
// if (initialTeam) {
|
||||
// await fetchGroupsForTeam(serverUrl, initialTeam.id);
|
||||
// }
|
||||
|
||||
// defer fetch channels and unread posts for other teams
|
||||
if (teamData.teams?.length && teamData.memberships?.length) {
|
||||
fetchTeamsChannelsAndUnreadPosts(serverUrl, teamData.teams, teamData.memberships, initialTeam?.id);
|
||||
}
|
||||
};
|
||||
@@ -1,17 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryCurrentUserId} from '@queries/servers/system';
|
||||
|
||||
import type {ClientResponse} from '@mattermost/react-native-network-client';
|
||||
import type {Client4Error} from '@typings/api/client';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const doPing = async (serverUrl: string) => {
|
||||
const client = await NetworkManager.createClient(serverUrl);
|
||||
@@ -49,32 +41,3 @@ export const doPing = async (serverUrl: string) => {
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const forceLogoutIfNecessary = async (serverUrl: string, err: Client4Error) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const currentUserId = await queryCurrentUserId(database);
|
||||
|
||||
if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) {
|
||||
await logout(serverUrl);
|
||||
}
|
||||
|
||||
return {error: null};
|
||||
};
|
||||
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false) => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.logout();
|
||||
} catch (error) {
|
||||
// We want to log the user even if logging out from the server failed
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('An error ocurred loging out from the server', serverUrl, error);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceEventEmitter.emit(General.SERVER_LOGOUT, serverUrl);
|
||||
};
|
||||
|
||||
227
app/actions/remote/post.ts
Normal file
227
app/actions/remote/post.ts
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ActionType, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getNeededAtMentionedUsernames} from '@helpers/api/user';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryRecentPostsInChannel} from '@queries/servers/post';
|
||||
import {queryCurrentUserId, queryCurrentChannelId} from '@queries/servers/system';
|
||||
import {queryAllUsers} from '@queries/servers/user';
|
||||
|
||||
import type {Client} from '@client/rest';
|
||||
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
type PostsRequest = {
|
||||
error?: never;
|
||||
order?: string[];
|
||||
posts?: Post[];
|
||||
previousPostId?: string;
|
||||
}
|
||||
|
||||
type AuthorsRequest = {
|
||||
authors?: UserProfile[];
|
||||
error?: unknown;
|
||||
}
|
||||
|
||||
export const fetchPostsForCurrentChannel = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const currentChannelId = await queryCurrentChannelId(database);
|
||||
return fetchPostsForChannel(serverUrl, currentChannelId);
|
||||
};
|
||||
|
||||
export const fetchPostsForChannel = async (serverUrl: string, channelId: string) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let postAction: Promise<PostsRequest>|undefined;
|
||||
let actionType: string|undefined;
|
||||
const postsInChannel = await queryRecentPostsInChannel(operator.database, channelId);
|
||||
if (!postsInChannel || postsInChannel.length < General.POST_CHUNK_SIZE) {
|
||||
postAction = fetchPosts(serverUrl, channelId, 0, General.POST_CHUNK_SIZE, true);
|
||||
actionType = ActionType.POSTS.RECEIVED_IN_CHANNEL;
|
||||
} else {
|
||||
const since = postsInChannel[0]?.createAt || 0;
|
||||
postAction = fetchPostsSince(serverUrl, channelId, since, true);
|
||||
actionType = ActionType.POSTS.RECEIVED_SINCE;
|
||||
}
|
||||
|
||||
const data = await postAction;
|
||||
if (data.error) {
|
||||
// Here we should emit an event that fetching posts failed.
|
||||
}
|
||||
|
||||
if (data.posts?.length && data.order?.length) {
|
||||
try {
|
||||
await fetchPostAuthors(serverUrl, data.posts, false);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('FETCH AUTHORS ERROR', error);
|
||||
}
|
||||
|
||||
operator.handlePosts({
|
||||
actionType,
|
||||
order: data.order,
|
||||
posts: data.posts,
|
||||
previousPostId: data.previousPostId,
|
||||
});
|
||||
}
|
||||
|
||||
return {posts: data.posts};
|
||||
};
|
||||
|
||||
export const fetchPostsForUnreadChannels = async (serverUrl: string, channels: Channel[], memberships: ChannelMembership[], excludeChannelId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
for (const member of memberships) {
|
||||
const channel = channels.find((c) => c.id === member.channel_id);
|
||||
if (channel && (channel.total_msg_count - member.msg_count) > 0 && channel.id !== excludeChannelId) {
|
||||
fetchPostsForChannel(serverUrl, channel.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const fetchPosts = async (serverUrl: string, channelId: string, page = 0, perPage = General.POST_CHUNK_SIZE, fetchOnly = false): Promise<PostsRequest> => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.getPosts(channelId, page, perPage);
|
||||
return processPostsFetched(serverUrl, ActionType.POSTS.RECEIVED_IN_CHANNEL, data, fetchOnly);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchPostsSince = async (serverUrl: string, channelId: string, since: number, fetchOnly = false): Promise<PostsRequest> => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await client.getPostsSince(channelId, since);
|
||||
return processPostsFetched(serverUrl, ActionType.POSTS.RECEIVED_SINCE, data, fetchOnly);
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchPostAuthors = async (serverUrl: string, posts: Post[], fetchOnly = false): Promise<AuthorsRequest> => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
const currentUserId = await queryCurrentUserId(operator.database);
|
||||
const users = await queryAllUsers(operator.database);
|
||||
const existingUserIds = new Set<string>();
|
||||
const existingUserNames = new Set<string>();
|
||||
let excludeUsername;
|
||||
users.forEach((u) => {
|
||||
existingUserIds.add(u.id);
|
||||
existingUserNames.add(u.username);
|
||||
if (u.id === currentUserId) {
|
||||
excludeUsername = u.username;
|
||||
}
|
||||
});
|
||||
|
||||
const usernamesToLoad = getNeededAtMentionedUsernames(existingUserNames, posts, excludeUsername);
|
||||
const userIdsToLoad = new Set<string>();
|
||||
posts.forEach((p) => {
|
||||
const userId = p.user_id;
|
||||
|
||||
if (userId === currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existingUserIds.has(userId)) {
|
||||
userIdsToLoad.add(userId);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const promises: Array<Promise<UserProfile[]>> = [];
|
||||
if (userIdsToLoad.size) {
|
||||
promises.push(client.getProfilesByIds(Array.from(userIdsToLoad)));
|
||||
}
|
||||
|
||||
if (usernamesToLoad.size) {
|
||||
promises.push(client.getProfilesByUsernames(Array.from(usernamesToLoad)));
|
||||
}
|
||||
|
||||
if (promises.length) {
|
||||
const result = await Promise.all(promises);
|
||||
const authors = result.flat();
|
||||
|
||||
if (!fetchOnly && authors.length) {
|
||||
operator.handleUsers({
|
||||
users: authors,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
}
|
||||
|
||||
return {authors};
|
||||
}
|
||||
|
||||
return {authors: [] as UserProfile[]};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
const processPostsFetched = (serverUrl: string, actionType: string, data: {order: string[]; posts: Post[]; prev_post_id?: string}, fetchOnly = false) => {
|
||||
const order = data.order;
|
||||
const posts = Object.values(data.posts) as Post[];
|
||||
const previousPostId = data.prev_post_id;
|
||||
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
operator.handlePosts({
|
||||
actionType,
|
||||
order,
|
||||
posts,
|
||||
previousPostId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
posts,
|
||||
order,
|
||||
previousPostId,
|
||||
};
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
|
||||
import {forceLogoutIfNecessary} from './general';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
export type MyPreferencesRequest = {
|
||||
preferences?: PreferenceType[];
|
||||
|
||||
236
app/actions/remote/retry.ts
Normal file
236
app/actions/remote/retry.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {prepareMyChannelsForTeam} from '@queries/servers/channel';
|
||||
import {prepareMyPreferences, queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {prepareCommonSystemValues, queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {prepareMyTeams} from '@queries/servers/team';
|
||||
import {queryCurrentUser} from '@queries/servers/user';
|
||||
import {selectDefaultChannelForTeam} from '@utils/channel';
|
||||
|
||||
import {fetchMissingSidebarInfo, fetchMyChannelsForTeam, MyChannelsRequest} from './channel';
|
||||
import {fetchPostsForChannel} from './post';
|
||||
import {fetchMyPreferences, MyPreferencesRequest} from './preference';
|
||||
import {fetchRolesIfNeeded} from './role';
|
||||
import {ConfigAndLicenseRequest, fetchConfigAndLicense} from './systems';
|
||||
import {fetchMyTeams, MyTeamsRequest} from './team';
|
||||
|
||||
import type {Model} from '@nozbe/watermelondb';
|
||||
|
||||
export const retryInitialTeamAndChannel = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
let initialTeam: Team|undefined;
|
||||
let initialChannel: Channel|undefined;
|
||||
|
||||
const user = await queryCurrentUser(database);
|
||||
if (!user) {
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
// Fetch in parallel server config & license / user preferences / teams / team membership / team unreads
|
||||
const promises: [Promise<ConfigAndLicenseRequest>, Promise<MyPreferencesRequest>, Promise<MyTeamsRequest>] = [
|
||||
fetchConfigAndLicense(serverUrl, true),
|
||||
fetchMyPreferences(serverUrl, true),
|
||||
fetchMyTeams(serverUrl, true),
|
||||
];
|
||||
|
||||
const [clData, prefData, teamData] = await Promise.all(promises);
|
||||
let chData: MyChannelsRequest|undefined;
|
||||
|
||||
// select initial team
|
||||
if (!clData.error && !prefData.error && !teamData.error) {
|
||||
const teamOrderPreference = getPreferenceValue(prefData.preferences!, Preferences.TEAMS_ORDER, '', '') as string;
|
||||
const teamRoles: string[] = [];
|
||||
const teamMembers: string[] = [];
|
||||
|
||||
teamData.memberships?.forEach((tm) => {
|
||||
teamRoles.push(...tm.roles.split(' '));
|
||||
teamMembers.push(tm.team_id);
|
||||
});
|
||||
|
||||
const myTeams = teamData.teams!.filter((t) => teamMembers?.includes(t.id));
|
||||
initialTeam = selectDefaultTeam(myTeams, user.locale, teamOrderPreference, clData.config?.ExperimentalPrimaryTeam);
|
||||
|
||||
if (initialTeam) {
|
||||
const rolesToFetch = new Set<string>([...user.roles.split(' '), ...teamRoles]);
|
||||
|
||||
// fetch channels / channel membership for initial team
|
||||
chData = await fetchMyChannelsForTeam(serverUrl, initialTeam.id, false, 0, true);
|
||||
if (chData.channels?.length && chData.memberships?.length) {
|
||||
const {channels, memberships} = chData;
|
||||
const channelIds = new Set(channels?.map((c) => c.id));
|
||||
for (let i = 0; i < memberships!.length; i++) {
|
||||
const member = memberships[i];
|
||||
if (channelIds.has(member.channel_id)) {
|
||||
member.roles.split(' ').forEach(rolesToFetch.add, rolesToFetch);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch user roles
|
||||
const rData = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch));
|
||||
|
||||
// select initial channel
|
||||
initialChannel = selectDefaultChannelForTeam(channels!, memberships!, initialTeam!.id, rData.roles, user.locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!initialTeam || !initialChannel) {
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const {operator} = DatabaseManager.serverDatabases[serverUrl];
|
||||
|
||||
const prefModel = prepareMyPreferences(operator, prefData.preferences!);
|
||||
if (prefModel) {
|
||||
modelPromises.push(prefModel);
|
||||
}
|
||||
|
||||
const teamModels = prepareMyTeams(operator, teamData.teams!, teamData.memberships!, teamData.unreads!);
|
||||
if (teamModels) {
|
||||
modelPromises.push(...teamModels);
|
||||
}
|
||||
|
||||
const channelModels = await prepareMyChannelsForTeam(operator, initialTeam!.id, chData!.channels!, chData!.memberships!);
|
||||
if (channelModels) {
|
||||
modelPromises.push(...channelModels);
|
||||
}
|
||||
|
||||
const systemModels = prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
config: clData.config!,
|
||||
license: clData.license!,
|
||||
currentTeamId: initialTeam?.id,
|
||||
currentChannelId: initialChannel?.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (systemModels) {
|
||||
modelPromises.push(systemModels);
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
|
||||
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(prefData.preferences || [], clData.config, clData.license);
|
||||
fetchMissingSidebarInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
fetchPostsForChannel(serverUrl, initialChannel.id);
|
||||
|
||||
return {error: false};
|
||||
} catch (error) {
|
||||
return {error: true};
|
||||
}
|
||||
};
|
||||
|
||||
export const retryInitialChannel = async (serverUrl: string, teamId: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
try {
|
||||
NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
try {
|
||||
let initialChannel: Channel|undefined;
|
||||
const rolesToFetch = new Set<string>();
|
||||
|
||||
const user = await queryCurrentUser(database);
|
||||
if (!user) {
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
const prefs = await queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT);
|
||||
const preferences: PreferenceType[] = prefs.map((p) => ({
|
||||
category: p.category,
|
||||
name: p.name,
|
||||
user_id: p.userId,
|
||||
value: p.value,
|
||||
}));
|
||||
const {config, license} = await queryCommonSystemValues(database);
|
||||
|
||||
// fetch channels / channel membership for initial team
|
||||
const chData = await fetchMyChannelsForTeam(serverUrl, teamId, false, 0, true);
|
||||
if (chData.channels?.length && chData.memberships?.length) {
|
||||
const {channels, memberships} = chData;
|
||||
const channelIds = new Set(channels?.map((c) => c.id));
|
||||
for (let i = 0; i < memberships!.length; i++) {
|
||||
const member = memberships[i];
|
||||
if (channelIds.has(member.channel_id)) {
|
||||
member.roles.split(' ').forEach(rolesToFetch.add, rolesToFetch);
|
||||
}
|
||||
}
|
||||
|
||||
// fetch user roles
|
||||
const rData = await fetchRolesIfNeeded(serverUrl, Array.from(rolesToFetch));
|
||||
|
||||
// select initial channel
|
||||
initialChannel = selectDefaultChannelForTeam(channels!, memberships!, teamId, rData.roles, user.locale);
|
||||
}
|
||||
|
||||
if (!initialChannel) {
|
||||
return {error: true};
|
||||
}
|
||||
|
||||
const modelPromises: Array<Promise<Model[]>> = [];
|
||||
const {operator} = DatabaseManager.serverDatabases[serverUrl];
|
||||
|
||||
const channelModels = await prepareMyChannelsForTeam(operator, teamId, chData!.channels!, chData!.memberships!);
|
||||
if (channelModels) {
|
||||
modelPromises.push(...channelModels);
|
||||
}
|
||||
|
||||
const systemModels = prepareCommonSystemValues(
|
||||
operator,
|
||||
{
|
||||
currentChannelId: initialChannel?.id,
|
||||
},
|
||||
);
|
||||
|
||||
if (systemModels) {
|
||||
modelPromises.push(systemModels);
|
||||
}
|
||||
|
||||
const models = await Promise.all(modelPromises);
|
||||
await operator.batchRecords(models.flat());
|
||||
|
||||
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);
|
||||
fetchMissingSidebarInfo(serverUrl, Array.from(channelsToFetchProfiles), user.locale, teammateDisplayNameSetting, user.id);
|
||||
}
|
||||
|
||||
fetchPostsForChannel(serverUrl, initialChannel.id);
|
||||
|
||||
return {error: false};
|
||||
} catch (error) {
|
||||
return {error: true};
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,18 @@ import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryRoles} from '@queries/servers/role';
|
||||
|
||||
export const loadRolesIfNeeded = async (serverUrl: string, updatedRoles: string[]) => {
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
export type RolesRequest = {
|
||||
error?: never;
|
||||
roles?: Role[];
|
||||
}
|
||||
|
||||
export const fetchRolesIfNeeded = async (serverUrl: string, updatedRoles: string[]): Promise<RolesRequest> => {
|
||||
if (!updatedRoles.length) {
|
||||
return {roles: []};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
@@ -25,16 +36,23 @@ export const loadRolesIfNeeded = async (serverUrl: string, updatedRoles: string[
|
||||
return !roleNames.includes(newRole);
|
||||
});
|
||||
|
||||
if (!newRoles.length) {
|
||||
return {roles: []};
|
||||
}
|
||||
|
||||
try {
|
||||
const roles = await client.getRolesByNames(newRoles);
|
||||
|
||||
await operator.handleRole({
|
||||
roles,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
if (roles.length) {
|
||||
await operator.handleRole({
|
||||
roles,
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
}
|
||||
|
||||
return {roles};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {error};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
223
app/actions/remote/session.ts
Normal file
223
app/actions/remote/session.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@actions/local/timezone';
|
||||
import {General, Database} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryDeviceToken} from '@queries/app/global';
|
||||
import {queryCurrentUserId, queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
|
||||
import type {LoginArgs} from '@typings/database/database';
|
||||
|
||||
import {logError} from './error';
|
||||
import {loginEntry} from './entry';
|
||||
import {fetchDataRetentionPolicy} from './systems';
|
||||
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {config, license}: { config: Partial<ClientConfig>; license: Partial<ClientLicense> } = await queryCommonSystemValues(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set timezone
|
||||
if (isTimezoneEnabled(config)) {
|
||||
const timezone = getDeviceTimezone();
|
||||
await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id});
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const forceLogoutIfNecessary = async (serverUrl: string, err: ClientErrorProps) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const currentUserId = await queryCurrentUserId(database);
|
||||
|
||||
if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) {
|
||||
await logout(serverUrl);
|
||||
}
|
||||
|
||||
return {error: null};
|
||||
};
|
||||
|
||||
export const getSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return await client.getSessions(currentUserId);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await forceLogoutIfNecessary(serverUrl, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password}: LoginArgs): Promise<LoginActionResponse> => {
|
||||
let deviceToken;
|
||||
let user: UserProfile;
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return {error: 'App database not found.', failed: true};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error, failed: true};
|
||||
}
|
||||
|
||||
try {
|
||||
deviceToken = await queryDeviceToken(appDatabase);
|
||||
user = await client.login(
|
||||
loginId,
|
||||
password,
|
||||
mfaToken,
|
||||
deviceToken,
|
||||
ldapOnly,
|
||||
);
|
||||
|
||||
const server = await DatabaseManager.createServerDatabase({
|
||||
config: {
|
||||
dbName: serverUrl,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await server?.operator.handleSystem({
|
||||
systems: [{
|
||||
id: Database.SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: user.id,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
const csrfToken = await getCSRFFromCookie(serverUrl);
|
||||
client.setCSRFToken(csrfToken);
|
||||
} catch (error) {
|
||||
return {error, failed: true};
|
||||
}
|
||||
|
||||
try {
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user});
|
||||
completeLogin(serverUrl, user);
|
||||
return {error, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
|
||||
export const logout = async (serverUrl: string, skipServerLogout = false) => {
|
||||
if (!skipServerLogout) {
|
||||
try {
|
||||
const client = NetworkManager.getClient(serverUrl);
|
||||
await client.logout();
|
||||
} catch (error) {
|
||||
// We want to log the user even if logging out from the server failed
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('An error ocurred loging out from the server', serverUrl, error);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceEventEmitter.emit(General.SERVER_LOGOUT, serverUrl);
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await client.sendPasswordResetEmail(email);
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
error: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const ssoLogin = async (serverUrl: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
|
||||
let deviceToken;
|
||||
let user;
|
||||
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
if (!database) {
|
||||
return {error: 'App database not found', failed: true};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error, failed: true};
|
||||
}
|
||||
|
||||
client.setBearerToken(bearerToken);
|
||||
client.setCSRFToken(csrfToken);
|
||||
|
||||
// Setting up active database for this SSO login flow
|
||||
try {
|
||||
const server = await DatabaseManager.createServerDatabase({
|
||||
config: {
|
||||
dbName: serverUrl,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
deviceToken = await queryDeviceToken(database);
|
||||
user = await client.getMe();
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await server?.operator.handleSystem({
|
||||
systems: [{
|
||||
id: Database.SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: user.id,
|
||||
}],
|
||||
prepareRecordsOnly: false,
|
||||
});
|
||||
} catch (e) {
|
||||
return {error: e, failed: true};
|
||||
}
|
||||
|
||||
try {
|
||||
const {error, hasTeams, time} = await loginEntry({serverUrl, user, deviceToken});
|
||||
completeLogin(serverUrl, user);
|
||||
return {error, failed: false, hasTeams, time};
|
||||
} catch (error) {
|
||||
return {error, failed: false, time: 0};
|
||||
}
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {logError} from '@actions/remote/error';
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/general';
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
|
||||
@@ -5,9 +5,12 @@ import {Model} from '@nozbe/watermelondb';
|
||||
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryWebSocketLastDisconnected} from '@queries/servers/system';
|
||||
import {prepareMyTeams} from '@queries/servers/team';
|
||||
|
||||
import {forceLogoutIfNecessary} from './general';
|
||||
import {fetchMyChannelsForTeam} from './channel';
|
||||
import {fetchPostsForUnreadChannels} from './post';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
export type MyTeamsRequest = {
|
||||
teams?: Team[];
|
||||
@@ -55,3 +58,23 @@ export const fetchMyTeams = async (serverUrl: string, fetchOnly = false): Promis
|
||||
return {error};
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchTeamsChannelsAndUnreadPosts = async (serverUrl: string, teams: Team[], memberships: TeamMembership[], excludeTeamId?: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const myTeams = teams.filter((t) => memberships.find((m) => m.team_id === t.id && t.id !== excludeTeamId));
|
||||
const since = await queryWebSocketLastDisconnected(database);
|
||||
|
||||
for await (const team of myTeams) {
|
||||
const {channels, memberships: members} = await fetchMyChannelsForTeam(serverUrl, team.id, since > 0, since, false, true);
|
||||
|
||||
if (channels?.length && members?.length) {
|
||||
fetchPostsForUnreadChannels(serverUrl, channels, members);
|
||||
}
|
||||
}
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
@@ -1,124 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@actions/local/timezone';
|
||||
import {logError} from '@actions/remote/error';
|
||||
import {loadRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchDataRetentionPolicy} from '@actions/remote/systems';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {Database} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import analytics from '@init/analytics';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryDeviceToken} from '@queries/app/global';
|
||||
import {queryCommonSystemValues} from '@queries/servers/system';
|
||||
import {getCSRFFromCookie} from '@utils/security';
|
||||
import {prepareUsers} from '@queries/servers/user';
|
||||
|
||||
import type {Client4Error} from '@typings/api/client';
|
||||
import type {LoadMeArgs, LoginArgs} from '@typings/database/database';
|
||||
import type {Client} from '@client/rest';
|
||||
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';
|
||||
|
||||
import {forceLogoutIfNecessary} from './general';
|
||||
import {forceLogoutIfNecessary} from './session';
|
||||
|
||||
// import {initAfterLogin} from './init';
|
||||
|
||||
type LoadedUser = {
|
||||
currentUser?: UserProfile;
|
||||
error?: Client4Error;
|
||||
export type ProfilesPerChannelRequest = {
|
||||
data?: ProfilesInChannelRequest[];
|
||||
error?: never;
|
||||
}
|
||||
|
||||
export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
export type ProfilesInChannelRequest = {
|
||||
users?: UserProfile[];
|
||||
channelId: string;
|
||||
error?: never;
|
||||
}
|
||||
|
||||
const {config, license}: { config: Partial<ClientConfig>; license: Partial<ClientLicense> } = await queryCommonSystemValues(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set timezone
|
||||
if (isTimezoneEnabled(config)) {
|
||||
const timezone = getDeviceTimezone();
|
||||
await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id});
|
||||
}
|
||||
|
||||
// Data retention
|
||||
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getSessions = async (serverUrl: string, currentUserId: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return await client.getSessions(currentUserId);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
await forceLogoutIfNecessary(serverUrl, e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password}: LoginArgs) => {
|
||||
let deviceToken;
|
||||
let user: UserProfile;
|
||||
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (!appDatabase) {
|
||||
return {error: 'App database not found.'};
|
||||
}
|
||||
|
||||
let client;
|
||||
export const fetchProfilesInChannel = async (serverUrl: string, channelId: string, excludeUserId?: string, fetchOnly = false): Promise<ProfilesInChannelRequest> => {
|
||||
let client: Client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
return {channelId, error};
|
||||
}
|
||||
|
||||
try {
|
||||
deviceToken = await queryDeviceToken(appDatabase);
|
||||
user = await client.login(
|
||||
loginId,
|
||||
password,
|
||||
mfaToken,
|
||||
deviceToken,
|
||||
ldapOnly,
|
||||
);
|
||||
const users = await client.getProfilesInChannel(channelId);
|
||||
const uniqueUsers = Array.from(new Set(users));
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
const prepare = prepareUsers(operator, uniqueUsers.filter((u) => u.id !== excludeUserId));
|
||||
if (prepare) {
|
||||
const models = await prepare;
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await DatabaseManager.createServerDatabase({
|
||||
config: {
|
||||
dbName: serverUrl,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
const csrfToken = await getCSRFFromCookie(serverUrl);
|
||||
client.setCSRFToken(csrfToken);
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
return {channelId, users: uniqueUsers};
|
||||
} catch (error) {
|
||||
forceLogoutIfNecessary(serverUrl, error);
|
||||
return {channelId, error};
|
||||
}
|
||||
};
|
||||
|
||||
const result = await loadMe(serverUrl, {user, deviceToken});
|
||||
export const fetchProfilesPerChannels = async (serverUrl: string, channelIds: string[], excludeUserId?: string, fetchOnly = false): Promise<ProfilesPerChannelRequest> => {
|
||||
try {
|
||||
const requests = channelIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true));
|
||||
const data = await Promise.all(requests);
|
||||
|
||||
if (!result?.error) {
|
||||
await completeLogin(serverUrl, user);
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (operator) {
|
||||
const users = new Set<UserProfile>();
|
||||
for (const item of data) {
|
||||
if (item.users?.length) {
|
||||
item.users.forEach(users.add, users);
|
||||
}
|
||||
}
|
||||
const prepare = prepareUsers(operator, Array.from(users).filter((u) => u.id !== excludeUserId));
|
||||
if (prepare) {
|
||||
const models = await prepare;
|
||||
if (models.length) {
|
||||
await operator.batchRecords(models);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {data};
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
// initAfterLogin({serverUrl, user, deviceToken});
|
||||
|
||||
return {error: undefined};
|
||||
};
|
||||
|
||||
export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs) => {
|
||||
@@ -259,74 +226,6 @@ export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs)
|
||||
return {currentUser, error: undefined};
|
||||
};
|
||||
|
||||
export const ssoLogin = async (serverUrl: string, bearerToken: string, csrfToken: string) => {
|
||||
let deviceToken;
|
||||
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
if (!database) {
|
||||
return {error: 'App database not found'};
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
client.setBearerToken(bearerToken);
|
||||
client.setCSRFToken(csrfToken);
|
||||
|
||||
// Setting up active database for this SSO login flow
|
||||
try {
|
||||
await DatabaseManager.createServerDatabase({
|
||||
config: {
|
||||
dbName: serverUrl,
|
||||
serverUrl,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
deviceToken = await queryDeviceToken(database);
|
||||
} catch (e) {
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = (await loadMe(serverUrl, {deviceToken}) as unknown) as LoadedUser;
|
||||
if (!result?.error && result?.currentUser) {
|
||||
await completeLogin(serverUrl, result.currentUser);
|
||||
}
|
||||
} catch (e) {
|
||||
return {error: undefined};
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await client.sendPasswordResetEmail(email);
|
||||
} catch (e) {
|
||||
return {
|
||||
error: e,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: response.data,
|
||||
error: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateMe = async (serverUrl: string, user: UserModel) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -349,11 +248,13 @@ export const updateMe = async (serverUrl: string, user: UserModel) => {
|
||||
return {error: e};
|
||||
}
|
||||
|
||||
operator.handleUsers({prepareRecordsOnly: false, users: [data]});
|
||||
if (data) {
|
||||
operator.handleUsers({prepareRecordsOnly: false, users: [data]});
|
||||
|
||||
const updatedRoles: string[] = data.roles.split(' ');
|
||||
if (updatedRoles.length) {
|
||||
await loadRolesIfNeeded(serverUrl, updatedRoles);
|
||||
const updatedRoles: string[] = data.roles.split(' ');
|
||||
if (updatedRoles.length) {
|
||||
await fetchRolesIfNeeded(serverUrl, updatedRoles);
|
||||
}
|
||||
}
|
||||
|
||||
return {data};
|
||||
|
||||
@@ -15,8 +15,6 @@ import type {
|
||||
RequestOptions,
|
||||
} from '@mattermost/react-native-network-client';
|
||||
|
||||
import type {ClientOptions} from '@typings/api/client';
|
||||
|
||||
import * as ClientConstants from './constants';
|
||||
import ClientError from './error';
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@ import {buildQueryString} from '@utils/helpers';
|
||||
import {PER_PAGE_DEFAULT} from './constants';
|
||||
|
||||
export interface ClientGroupsMix {
|
||||
getGroups: (filterAllowReference?: boolean, page?: number, perPage?: number) => Promise<Group[]>;
|
||||
getGroups: (filterAllowReference?: boolean, page?: number, perPage?: number, since?: number) => Promise<Group[]>;
|
||||
getGroupsByUserId: (userID: string) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToChannelsInTeam: (teamID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
getAllGroupsAssociatedToTeam: (teamID: string, filterAllowReference?: boolean) => Promise<{groups: Group[]; tota_group_count: number}>;
|
||||
getAllGroupsAssociatedToChannelsInTeam: (teamID: string, filterAllowReference?: boolean) => Promise<{groups: Record<string, Group[]>}>;
|
||||
getAllGroupsAssociatedToChannel: (channelID: string, filterAllowReference?: boolean) => Promise<Group[]>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,10 @@ import React from 'react';
|
||||
import {StyleProp, Text, TextStyle, ViewStyle} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {ClientError} from '@utils/client_error';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
export type ClientErrorWithIntl = ClientError & {intl: {values?: Record<string, any>}}
|
||||
|
||||
type ErrorProps = {
|
||||
error: ClientErrorWithIntl | string;
|
||||
error: Partial<ClientErrorProps> | string;
|
||||
testID?: string;
|
||||
textStyle?: StyleProp<ViewStyle> | StyleProp<TextStyle>;
|
||||
theme: Theme;
|
||||
|
||||
36
app/components/failed_action/cloud_svg.tsx
Normal file
36
app/components/failed_action/cloud_svg.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import Svg, {Path} from 'react-native-svg';
|
||||
|
||||
type CloudSvgProps = {
|
||||
color: string;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
||||
const CloudSvg = ({color, height, width}: CloudSvgProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{height, width, alignItems: 'flex-start'}}
|
||||
testID='failed_network_action.cloud_icon'
|
||||
>
|
||||
<Svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox='0 0 72 47'
|
||||
>
|
||||
<Path
|
||||
d='M58.464,19.072c0,-5.181 -1.773,-9.599 -5.316,-13.249c-3.545,-3.649 -7.854,-5.474 -12.932,-5.474c-3.597,0 -6.902,0.979 -9.917,2.935c-3.014,1.959 -5.263,4.523 -6.743,7.696c-1.483,-0.739 -2.856,-1.111 -4.126,-1.111c-2.328,0 -4.363,0.769 -6.109,2.301c-1.745,1.535 -2.831,3.466 -3.252,5.792c-2.856,0.952 -5.185,2.672 -6.982,5.156c-1.8,2.487 -2.697,5.316 -2.697,8.489c0,3.915 1.4,7.299 4.204,10.155c2.802,2.857 6.161,4.285 10.076,4.285l43.794,0c3.595,0 6.664,-1.295 9.203,-3.888c2.538,-2.591 3.808,-5.685 3.808,-9.282c0,-3.702 -1.27,-6.848 -3.808,-9.441c-2.539,-2.591 -5.608,-3.888 -9.203,-3.888l0,-0.476Zm-31.294,16.424l17.17,0c-0.842,-1.62 -2.02,-2.92 -3.535,-3.898c-1.515,-0.977 -3.198,-1.467 -5.05,-1.467c-1.852,0 -3.535,0.49 -5.05,1.467c-1.515,0.978 -2.693,2.278 -3.535,3.898l0,0Zm17.338,-12.407c0,-0.782 -0.252,-1.411 -0.757,-1.886c-0.505,-0.474 -1.124,-0.713 -1.852,-0.713c-0.73,0 -1.347,0.239 -1.852,0.713c-0.505,0.475 -0.757,1.104 -0.757,1.886c0,0.783 0.252,1.412 0.757,1.886c0.505,0.476 1.122,0.713 1.852,0.713c0.728,0 1.347,-0.237 1.852,-0.713c0.505,-0.474 0.757,-1.103 0.757,-1.886Zm-12.288,0c0,-0.782 -0.253,-1.411 -0.758,-1.886c-0.505,-0.474 -1.123,-0.713 -1.851,-0.713c-0.73,0 -1.347,0.239 -1.852,0.713c-0.505,0.475 -0.757,1.104 -0.757,1.886c0,0.783 0.252,1.412 0.757,1.886c0.505,0.476 1.122,0.713 1.852,0.713c0.728,0 1.346,-0.237 1.851,-0.713c0.505,-0.474 0.758,-1.103 0.758,-1.886Z'
|
||||
fillRule='evenodd'
|
||||
strokeLinejoin='round'
|
||||
fill={color}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CloudSvg;
|
||||
96
app/components/failed_action/index.tsx
Normal file
96
app/components/failed_action/index.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Text, View} from 'react-native';
|
||||
import Button from 'react-native-button';
|
||||
|
||||
import {View as ViewConstants} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import CloudSvg from './cloud_svg';
|
||||
|
||||
type FailedActionProps = {
|
||||
action?: string;
|
||||
message: string;
|
||||
title: string;
|
||||
onAction: () => void;
|
||||
}
|
||||
|
||||
const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: ViewConstants.INDICATOR_BAR_HEIGHT,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
title: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.8),
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 15,
|
||||
marginTop: 10,
|
||||
},
|
||||
description: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.4),
|
||||
fontSize: 17,
|
||||
lineHeight: 25,
|
||||
textAlign: 'center',
|
||||
},
|
||||
link: {
|
||||
color: theme.buttonColor,
|
||||
fontSize: 15,
|
||||
},
|
||||
buttonContainer: {
|
||||
backgroundColor: theme.buttonBg,
|
||||
borderRadius: 5,
|
||||
height: 42,
|
||||
justifyContent: 'center',
|
||||
marginTop: 20,
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const FailedAction = ({action, message, title, onAction}: FailedActionProps) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
|
||||
const text = action || intl.formatMessage({id: 'failed_action.try_again', defaultMessage: 'Try again'});
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CloudSvg
|
||||
color={changeOpacity(theme.centerChannelColor, 0.15)}
|
||||
height={76}
|
||||
width={76}
|
||||
/>
|
||||
<Text
|
||||
style={style.title}
|
||||
testID='error_title'
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
style={style.description}
|
||||
testID='error_text'
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
<Button
|
||||
containerStyle={style.buttonContainer}
|
||||
onPress={onAction}
|
||||
>
|
||||
<Text style={style.link}>{text}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default FailedAction;
|
||||
59
app/components/server_version/index.tsx
Normal file
59
app/components/server_version/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {View} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {unsupportedServer} from '@utils/supported_server/supported_server';
|
||||
import {isSystemAdmin} from '@utils/user';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type WithUserArgs = WithDatabaseArgs & {
|
||||
currentUserId: SystemModel;
|
||||
}
|
||||
|
||||
type ServerVersionProps = WithDatabaseArgs & {
|
||||
config: SystemModel;
|
||||
user: UserModel;
|
||||
};
|
||||
|
||||
const {SERVER: {SYSTEM, USER}} = MM_TABLES;
|
||||
|
||||
const ServerVersion = ({config, user}: ServerVersionProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
const serverVersion = (config.value?.Version) || '';
|
||||
|
||||
if (serverVersion) {
|
||||
const {RequiredServer: {MAJOR_VERSION, MIN_VERSION, PATCH_VERSION}} = View;
|
||||
const isSupportedServer = isMinimumServerVersion(serverVersion, MAJOR_VERSION, MIN_VERSION, PATCH_VERSION);
|
||||
|
||||
if (!isSupportedServer) {
|
||||
// Only display the Alert if the TOS does not need to show first
|
||||
unsupportedServer(isSystemAdmin(user.roles), intl.formatMessage);
|
||||
}
|
||||
}
|
||||
}, [config.value?.Version, user.roles]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const withSystem = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUserId: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID),
|
||||
config: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG),
|
||||
}));
|
||||
|
||||
const withUser = withObservables(['currentUserId'], ({currentUserId, database}: WithUserArgs) => ({
|
||||
user: database.collections.get(USER).findAndObserve(currentUserId.value),
|
||||
}));
|
||||
|
||||
export default withDatabase(withSystem(withUser(ServerVersion)));
|
||||
@@ -54,6 +54,7 @@ export const SYSTEM_IDENTIFIERS = {
|
||||
CURRENT_USER_ID: 'currentUserId',
|
||||
DATA_RETENTION_POLICIES: 'dataRetentionPolicies',
|
||||
LICENSE: 'license',
|
||||
WEBSOCKET: 'WebSocket',
|
||||
};
|
||||
|
||||
export const GLOBAL_IDENTIFIERS = {
|
||||
|
||||
@@ -131,4 +131,5 @@ export default {
|
||||
NotificationLevels,
|
||||
SidebarSectionTypes,
|
||||
IOS_HORIZONTAL_LANDSCAPE: 44,
|
||||
INDICATOR_BAR_HEIGHT,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q, Query, Relation} from '@nozbe/watermelondb';
|
||||
import {children, field, immutableRelation, lazy} from '@nozbe/watermelondb/decorators';
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {children, field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
@@ -42,9 +42,6 @@ export default class ChannelModel extends Model {
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL is associated with only one CHANNEL_INFO (relationship is 1:1) */
|
||||
[CHANNEL_INFO]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
/** A CHANNEL can be associated with multiple CHANNEL_MEMBERSHIP (relationship is 1:N) */
|
||||
[CHANNEL_MEMBERSHIP]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
@@ -54,12 +51,6 @@ export default class ChannelModel extends Model {
|
||||
/** A CHANNEL can be associated with multiple GROUPS_IN_CHANNEL (relationship is 1:N) */
|
||||
[GROUPS_IN_CHANNEL]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
/** A CHANNEL is associated with only one MY_CHANNEL (relationship is 1:1) */
|
||||
[MY_CHANNEL]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
/** A CHANNEL is associated to only one MY_CHANNEL_SETTINGS (relationship is 1:1) */
|
||||
[MY_CHANNEL_SETTINGS]: {type: 'has_many', foreignKey: 'channel_id'},
|
||||
|
||||
/** A CHANNEL can be associated with multiple POSTS_IN_CHANNEL (relationship is 1:N) */
|
||||
[POSTS_IN_CHANNEL]: {type: 'has_many', foreignKey: 'id'},
|
||||
|
||||
@@ -125,11 +116,12 @@ export default class ChannelModel extends Model {
|
||||
@immutableRelation(USER, 'creator_id') creator!: Relation<UserModel>;
|
||||
|
||||
/** info : Query returning extra information about this channel from CHANNEL_INFO table */
|
||||
@lazy info = this.collections.get(CHANNEL_INFO).query(Q.on(CHANNEL, 'id', this.id)) as Query<ChannelInfoModel>;
|
||||
// @lazy info = this.collections.get(CHANNEL_INFO).query(Q.on(CHANNEL, 'id', this.id)) as Query<ChannelInfoModel>;
|
||||
@immutableRelation(CHANNEL_INFO, 'id') info!: Relation<ChannelInfoModel>;
|
||||
|
||||
/** membership : Query returning the membership data for the current user if it belongs to this channel */
|
||||
@lazy membership = this.collections.get(MY_CHANNEL).query(Q.on(CHANNEL, 'id', this.id)) as Query<MyChannelModel>;
|
||||
@immutableRelation(MY_CHANNEL, 'id') membership!: Relation<MyChannelModel>;
|
||||
|
||||
/** settings: User specific settings/preferences for this channel */
|
||||
@lazy settings = this.collections.get(MY_CHANNEL_SETTINGS).query(Q.on(CHANNEL, 'id', this.id)) as Query<MyChannelSettingsModel>;
|
||||
@immutableRelation(MY_CHANNEL_SETTINGS, 'id') settings!: Relation<MyChannelSettingsModel>;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
@@ -20,13 +20,6 @@ export default class ChannelInfoModel extends Model {
|
||||
/** table (name) : ChannelInfo */
|
||||
static table = CHANNEL_INFO;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL is associated with only one CHANNEL_INFO (relationship is 1:1) */
|
||||
[CHANNEL]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** guest_count : The number of guest in this channel */
|
||||
@field('guest_count') guestCount!: number;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
@@ -18,13 +18,6 @@ export default class MyChannelModel extends Model {
|
||||
/** table (name) : MyChannel */
|
||||
static table = MY_CHANNEL;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL can be associated to only one record from the MY_CHANNEL table (relationship is 1:1) */
|
||||
[CHANNEL]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** last_post_at : The timestamp for any last post on this channel */
|
||||
@field('last_post_at') lastPostAt!: number;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {immutableRelation, json} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
@@ -20,13 +20,6 @@ export default class MyChannelSettingsModel extends Model {
|
||||
/** table (name) : MyChannelSettings */
|
||||
static table = MY_CHANNEL_SETTINGS;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A CHANNEL is related to only one MY_CHANNEL_SETTINGS (relationship is 1:1) */
|
||||
[CHANNEL]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** notify_props : Configurations with regards to this channel */
|
||||
@json('notify_props', safeParseJSON) notifyProps!: ChannelNotifyProps;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {field, relation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
@@ -18,13 +18,6 @@ export default class MyTeamModel extends Model {
|
||||
/** table (name) : MyTeam */
|
||||
static table = MY_TEAM;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** TEAM and MY_TEAM have a 1:1 relationship. */
|
||||
[TEAM]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** is_unread : Boolean flag for unread messages on team level */
|
||||
@field('is_unread') isUnread!: boolean;
|
||||
|
||||
@@ -35,5 +28,5 @@ export default class MyTeamModel extends Model {
|
||||
@field('roles') roles!: string;
|
||||
|
||||
/** team : The relation to the TEAM, that this user belongs to */
|
||||
@relation(MY_TEAM, 'id') team!: Relation<TeamModel>;
|
||||
@relation(TEAM, 'id') team!: Relation<TeamModel>;
|
||||
}
|
||||
|
||||
@@ -40,9 +40,6 @@ export default class PostModel extends Model {
|
||||
/** A POST can have multiple POSTS_IN_THREAD. (relationship is 1:N)*/
|
||||
[POSTS_IN_THREAD]: {type: 'has_many', foreignKey: 'id'},
|
||||
|
||||
/** A POST can have POST_METADATA. (relationship is 1:1)*/
|
||||
[POST_METADATA]: {type: 'has_many', foreignKey: 'id'},
|
||||
|
||||
/** A POST can have multiple REACTION. (relationship is 1:N)*/
|
||||
[REACTION]: {type: 'has_many', foreignKey: 'post_id'},
|
||||
|
||||
@@ -102,7 +99,7 @@ export default class PostModel extends Model {
|
||||
@children(FILE) files!: FileModel[];
|
||||
|
||||
/** metadata: All the extra data associated with this Post */
|
||||
@children(POST_METADATA) metadata!: PostMetadataModel[];
|
||||
@immutableRelation(POST_METADATA, 'id') metadata!: Relation<PostMetadataModel>;
|
||||
|
||||
/** reactions: All the reactions associated with this Post */
|
||||
@children(REACTION) reactions!: ReactionModel[];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {immutableRelation, json} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
@@ -19,13 +19,6 @@ export default class PostMetadataModel extends Model {
|
||||
/** table (name) : PostMetadata */
|
||||
static table = POST_METADATA;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A POST can have multiple POST_METADATA.(relationship is 1:N)*/
|
||||
[POST]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** data : Different types of data ranging from embeds to images. */
|
||||
@json('data', safeParseJSON) data!: PostMetadata;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Q, Query} from '@nozbe/watermelondb';
|
||||
import {children, field, lazy} from '@nozbe/watermelondb/decorators';
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {children, field, immutableRelation} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
@@ -42,15 +42,9 @@ export default class TeamModel extends Model {
|
||||
/** A TEAM has a 1:N relationship with GROUPS_IN_TEAM. A TEAM can possess multiple groups */
|
||||
[GROUPS_IN_TEAM]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
/** A TEAM has a 1:1 relationship with MY_TEAM. */
|
||||
[MY_TEAM]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
/** A TEAM has a 1:N relationship with SLASH_COMMAND. A TEAM can possess multiple slash commands */
|
||||
[SLASH_COMMAND]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
/** A TEAM has a 1:1 relationship with TEAM_CHANNEL_HISTORY.*/
|
||||
[TEAM_CHANNEL_HISTORY]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
/** A TEAM has a 1:N relationship with TEAM_MEMBERSHIP. A TEAM can regroup multiple users */
|
||||
[TEAM_MEMBERSHIP]: {type: 'has_many', foreignKey: 'team_id'},
|
||||
|
||||
@@ -91,14 +85,14 @@ export default class TeamModel extends Model {
|
||||
/** groupsInTeam : All the groups associated with this team */
|
||||
@children(GROUPS_IN_TEAM) groupsInTeam!: GroupsInTeamModel[];
|
||||
|
||||
/** myTeam : Retrieves additional information about the team that this user is possibly part of. This query might yield no result if the user isn't part of a team. */
|
||||
@lazy myTeam = this.collections.get(MY_TEAM).query(Q.on(TEAM, 'id', this.id)) as Query<MyTeamModel>;
|
||||
/** myTeam : Retrieves additional information about the team that this user is possibly part of. */
|
||||
@immutableRelation(MY_TEAM, 'id') myTeam!: Relation<MyTeamModel>;
|
||||
|
||||
/** slashCommands : All the slash commands associated with this team */
|
||||
@children(SLASH_COMMAND) slashCommands!: SlashCommandModel[];
|
||||
|
||||
/** teamChannelHistory : A history of the channels in this team that has been visited, ordered by the most recent and capped to the last 5 */
|
||||
@lazy teamChannelHistory = this.collections.get(TEAM_CHANNEL_HISTORY).query(Q.on(TEAM, 'id', this.id)) as Query<TeamChannelHistoryModel>;
|
||||
@immutableRelation(TEAM_CHANNEL_HISTORY, 'id') teamChannelHistory!: Relation<TeamChannelHistoryModel>;
|
||||
|
||||
/** members : All the users associated with this team */
|
||||
@children(TEAM_MEMBERSHIP) members!: TeamMembershipModel[];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import {immutableRelation, json} from '@nozbe/watermelondb/decorators';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {safeParseJSON} from '@utils/helpers';
|
||||
@@ -20,13 +20,6 @@ export default class TeamChannelHistoryModel extends Model {
|
||||
/** table (name) : TeamChannelHistory */
|
||||
static table = TEAM_CHANNEL_HISTORY;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations = {
|
||||
|
||||
/** A TEAM and TEAM_CHANNEL_HISTORY share a 1:1 relationship */
|
||||
[TEAM]: {type: 'belongs_to', key: 'id'},
|
||||
};
|
||||
|
||||
/** channel_ids : An array containing the last 5 channels visited within this team order by recency */
|
||||
@json('channel_ids', safeParseJSON) channelIds!: string[];
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
import {Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {Database} from '@constants';
|
||||
import {getRawRecordPairs, retrieveRecords} from '@database/operator/utils/general';
|
||||
import {getRawRecordPairs, getValidRecordsForUpdate, retrieveRecords} from '@database/operator/utils/general';
|
||||
import {transformPostInThreadRecord} from '@database/operator/server_data_operator/transformers/post';
|
||||
import {getPostListEdges} from '@database//operator/utils/post';
|
||||
|
||||
import type {RecordPair} from '@typings/database/database';
|
||||
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
|
||||
|
||||
export interface PostsInThreadHandlerMix {
|
||||
@@ -23,7 +24,7 @@ const PostsInThreadHandler = (superclass: any) => class extends superclass {
|
||||
return [];
|
||||
}
|
||||
|
||||
const update: PostsInThread[] = [];
|
||||
const update: RecordPair[] = [];
|
||||
const create: PostsInThread[] = [];
|
||||
const ids = Object.keys(postsMap);
|
||||
for await (const rootId of ids) {
|
||||
@@ -36,11 +37,16 @@ const PostsInThreadHandler = (superclass: any) => class extends superclass {
|
||||
|
||||
if (chunks.length) {
|
||||
const chunk = chunks[0];
|
||||
update.push({
|
||||
const newValue = {
|
||||
id: rootId,
|
||||
earliest: Math.min(chunk.earliest, firstPost.create_at),
|
||||
latest: Math.max(chunk.latest, lastPost.create_at),
|
||||
});
|
||||
};
|
||||
update.push(getValidRecordsForUpdate({
|
||||
tableName: POSTS_IN_THREAD,
|
||||
newValue,
|
||||
existingRecord: chunk,
|
||||
}));
|
||||
} else {
|
||||
// create chunk
|
||||
create.push({
|
||||
@@ -53,7 +59,7 @@ const PostsInThreadHandler = (superclass: any) => class extends superclass {
|
||||
|
||||
const postInThreadRecords = (await this.prepareRecords({
|
||||
createRaws: getRawRecordPairs(create),
|
||||
updateRaws: getRawRecordPairs(update),
|
||||
updateRaws: update,
|
||||
transformer: transformPostInThreadRecord,
|
||||
tableName: POSTS_IN_THREAD,
|
||||
})) as PostsInThreadModel[];
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General, Preferences} from '@constants';
|
||||
|
||||
export function getPreferenceValue(preferences: PreferenceType[], category: string, name: string, defaultValue: unknown = '') {
|
||||
const pref = preferences.find((p) => p.category === category && p.name === name);
|
||||
|
||||
@@ -24,3 +26,15 @@ export function getPreferenceAsInt(preferences: PreferenceType[], category: stri
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export function getTeammateNameDisplaySetting(preferences: PreferenceType[], config?: ClientConfig, license?: ClientLicense) {
|
||||
const useAdminTeammateNameDisplaySetting = license?.LockTeammateNameDisplay === 'true' && config?.LockTeammateNameDisplay === 'true';
|
||||
const preference = getPreferenceValue(preferences, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.NAME_NAME_FORMAT, '') as string;
|
||||
if (preference && !useAdminTeammateNameDisplaySetting) {
|
||||
return preference;
|
||||
} else if (config?.TeammateNameDisplay) {
|
||||
return config.TeammateNameDisplay;
|
||||
}
|
||||
|
||||
return General.TEAMMATE_NAME_DISPLAY.SHOW_USERNAME;
|
||||
}
|
||||
|
||||
35
app/helpers/api/user.ts
Normal file
35
app/helpers/api/user.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
|
||||
export const getNeededAtMentionedUsernames = (usernames: Set<string>, posts: Post[], excludeUsername?: string) => {
|
||||
const usernamesToLoad = new Set<string>();
|
||||
|
||||
const pattern = /\B@(([a-z0-9_.-]*[a-z0-9_])[.-]*)/gi;
|
||||
|
||||
posts.forEach((p) => {
|
||||
let match;
|
||||
while ((match = pattern.exec(p.message)) !== null) {
|
||||
// match[1] is the matched mention including trailing punctuation
|
||||
// match[2] is the matched mention without trailing punctuation
|
||||
if (General.SPECIAL_MENTIONS.indexOf(match[2]) !== -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (match[1] === excludeUsername || match[2] === excludeUsername) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (usernames.has(match[1]) || usernames.has(match[2])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If there's no trailing punctuation, this will only add 1 item to the set
|
||||
usernamesToLoad.add(match[1]);
|
||||
usernamesToLoad.add(match[2]);
|
||||
}
|
||||
});
|
||||
|
||||
return usernamesToLoad;
|
||||
};
|
||||
@@ -206,18 +206,18 @@ export function getLocaleFromLanguage(lang: string) {
|
||||
}
|
||||
|
||||
export function resetMomentLocale(locale?: string) {
|
||||
moment.locale(locale || DEFAULT_LOCALE);
|
||||
moment.locale(locale || DEFAULT_LOCALE.split('-')[0]);
|
||||
}
|
||||
|
||||
export function getTranslations(locale?: string) {
|
||||
return loadTranslation(locale);
|
||||
}
|
||||
|
||||
export function getLocalizedMessage(lang: string, id: string) {
|
||||
export function getLocalizedMessage(lang: string, id: string, defaultMessage?: string) {
|
||||
const locale = getLocaleFromLanguage(lang);
|
||||
const translations = getTranslations(locale);
|
||||
|
||||
return translations[id];
|
||||
return translations[id] || defaultMessage;
|
||||
}
|
||||
|
||||
export function t(v: string): string {
|
||||
|
||||
@@ -147,8 +147,7 @@ class PushNotifications {
|
||||
|
||||
if (payload?.server_url) {
|
||||
const database = DatabaseManager.serverDatabases[payload.server_url]?.database;
|
||||
const currentChannelId = await queryCurrentChannelId(database);
|
||||
const channelId = currentChannelId?.value;
|
||||
const channelId = await queryCurrentChannelId(database);
|
||||
|
||||
if (channelId && payload.channel_id !== channelId) {
|
||||
const screen = 'Notification';
|
||||
|
||||
57
app/queries/servers/channel.ts
Normal file
57
app/queries/servers/channel.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
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';
|
||||
|
||||
const {SERVER: {CHANNEL}} = MM_TABLES;
|
||||
|
||||
export const prepareMyChannelsForTeam = async (operator: ServerDataOperator, teamId: string, channels: Channel[], channelMembers: ChannelMembership[]) => {
|
||||
const allChannelsForTeam = await queryAllChannelsForTeam(operator.database, teamId);
|
||||
const channelInfos: ChannelInfo[] = [];
|
||||
const memberships = channelMembers.map((cm) => ({...cm, id: cm.channel_id}));
|
||||
|
||||
for await (const c of channels) {
|
||||
const storedChannel = allChannelsForTeam.find((sc) => sc.id === c.id);
|
||||
let storedInfo: ChannelInfoModel;
|
||||
let member_count = 0;
|
||||
let guest_count = 0;
|
||||
let pinned_post_count = 0;
|
||||
if (storedChannel) {
|
||||
storedInfo = (await storedChannel.info.fetch()) as ChannelInfoModel;
|
||||
member_count = storedInfo.memberCount;
|
||||
guest_count = storedInfo.guestCount;
|
||||
pinned_post_count = storedInfo.pinnedPostCount;
|
||||
}
|
||||
|
||||
channelInfos.push({
|
||||
id: c.id,
|
||||
header: c.header,
|
||||
purpose: c.purpose,
|
||||
guest_count,
|
||||
member_count,
|
||||
pinned_post_count,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const channelRecords = operator.handleChannel({channels, prepareRecordsOnly: true});
|
||||
const channelInfoRecords = operator.handleChannelInfo({channelInfos, prepareRecordsOnly: true});
|
||||
const membershipRecords = operator.handleChannelMembership({channelMemberships: memberships, prepareRecordsOnly: true});
|
||||
const myChannelRecords = operator.handleMyChannel({myChannels: memberships, prepareRecordsOnly: true});
|
||||
const myChannelSettingsRecords = operator.handleMyChannelSettings({settings: memberships, prepareRecordsOnly: true});
|
||||
|
||||
return [channelRecords, channelInfoRecords, membershipRecords, myChannelRecords, myChannelSettingsRecords];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryAllChannelsForTeam = (database: Database, teamId: string) => {
|
||||
return database.get(CHANNEL).query(Q.where('team_id', teamId)).fetch() as Promise<ChannelModel[]>;
|
||||
};
|
||||
50
app/queries/servers/post.ts
Normal file
50
app/queries/servers/post.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||
|
||||
const {SERVER: {POST, POSTS_IN_CHANNEL}} = MM_TABLES;
|
||||
|
||||
export const queryPostsInChannel = (database: Database, channelId: string): Promise<PostInChannelModel[]> => {
|
||||
try {
|
||||
return database.get(POSTS_IN_CHANNEL).query(
|
||||
Q.where('channel_id', channelId),
|
||||
Q.experimentalSortBy('latest', Q.desc),
|
||||
).fetch() as Promise<PostInChannelModel[]>;
|
||||
} catch {
|
||||
return Promise.resolve([] as PostInChannelModel[]);
|
||||
}
|
||||
};
|
||||
|
||||
export const queryPostsChunk = (database: Database, channelId: string, earlies: number, latest: number): Promise<PostModel[]> => {
|
||||
try {
|
||||
return database.get(POST).query(
|
||||
Q.and(
|
||||
Q.where('channel_id', channelId),
|
||||
Q.where('create_at', Q.between(earlies, latest)),
|
||||
Q.where('delete_at', Q.eq(0)),
|
||||
),
|
||||
Q.experimentalSortBy('create_at', Q.desc),
|
||||
).fetch() as Promise<PostModel[]>;
|
||||
} catch {
|
||||
return Promise.resolve([] as PostModel[]);
|
||||
}
|
||||
};
|
||||
|
||||
export const queryRecentPostsInChannel = async (database: Database, channelId: string): Promise<PostModel[]> => {
|
||||
try {
|
||||
const chunks = await queryPostsInChannel(database, channelId);
|
||||
if (chunks.length) {
|
||||
const recent = chunks[0];
|
||||
return queryPostsChunk(database, channelId, recent.earliest, recent.latest);
|
||||
}
|
||||
return Promise.resolve([] as PostModel[]);
|
||||
} catch {
|
||||
return Promise.resolve([] as PostModel[]);
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database, Q} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
||||
export const prepareMyPreferences = (operator: ServerDataOperator, preferences: PreferenceType[]) => {
|
||||
try {
|
||||
@@ -13,3 +18,10 @@ export const prepareMyPreferences = (operator: ServerDataOperator, preferences:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryPreferencesByCategoryAndName = (database: Database, category: string, name: string) => {
|
||||
return database.get(MM_TABLES.SERVER.PREFERENCE).query(
|
||||
Q.where('category', category),
|
||||
Q.where('name', name),
|
||||
).fetch() as Promise<PreferenceModel[]>;
|
||||
};
|
||||
|
||||
@@ -8,12 +8,20 @@ import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
type PrepareCommonSystemValuesArgs = {
|
||||
config?: ClientConfig;
|
||||
currentChannelId?: string;
|
||||
currentTeamId?: string;
|
||||
currentUserId?: string;
|
||||
license?: ClientLicense;
|
||||
}
|
||||
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
|
||||
export const queryCurrentChannelId = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const currentChannelId = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID) as SystemModel;
|
||||
return currentChannelId?.value || '';
|
||||
return (currentChannelId?.value || '') as string;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -22,14 +30,14 @@ export const queryCurrentChannelId = async (serverDatabase: Database) => {
|
||||
export const queryCurrentUserId = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const currentUserId = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CURRENT_USER_ID) as SystemModel;
|
||||
return currentUserId?.value || '';
|
||||
return (currentUserId?.value || '') as string;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const queryCommonSystemValues = async (database: Database) => {
|
||||
const systemRecords = (await database.collections.get(SYSTEM).query().fetch()) as SystemModel[];
|
||||
export const queryCommonSystemValues = async (serverDatabase: Database) => {
|
||||
const systemRecords = (await serverDatabase.collections.get(SYSTEM).query().fetch()) as SystemModel[];
|
||||
let config = {};
|
||||
let license = {};
|
||||
let currentChannelId = '';
|
||||
@@ -50,7 +58,7 @@ export const queryCommonSystemValues = async (database: Database) => {
|
||||
currentUserId = systemRecord.value;
|
||||
break;
|
||||
case SYSTEM_IDENTIFIERS.LICENSE:
|
||||
license = systemRecord.value;
|
||||
license = systemRecord.value as ClientLicense;
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -59,38 +67,62 @@ export const queryCommonSystemValues = async (database: Database) => {
|
||||
currentChannelId,
|
||||
currentTeamId,
|
||||
currentUserId,
|
||||
config,
|
||||
license,
|
||||
config: (config as ClientConfig),
|
||||
license: (license as ClientLicense),
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareCommonSystemValues = (
|
||||
operator: ServerDataOperator, config: ClientConfig, license: ClientLicense,
|
||||
currentUserId: string, currentTeamId: string, currentChannelId: string) => {
|
||||
export const queryWebSocketLastDisconnected = async (serverDatabase: Database) => {
|
||||
try {
|
||||
const websocketLastDisconnected = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.WEBSOCKET) as SystemModel;
|
||||
return (parseInt(websocketLastDisconnected?.value || 0, 10) || 0);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareCommonSystemValues = (
|
||||
operator: ServerDataOperator, values: PrepareCommonSystemValuesArgs) => {
|
||||
try {
|
||||
const {config, currentChannelId, currentTeamId, currentUserId, license} = values;
|
||||
const systems: IdValue[] = [];
|
||||
if (config !== undefined) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
});
|
||||
}
|
||||
|
||||
if (license !== undefined) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
});
|
||||
}
|
||||
|
||||
if (currentUserId !== undefined) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: currentUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (currentTeamId !== undefined) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
|
||||
value: currentTeamId,
|
||||
});
|
||||
}
|
||||
|
||||
if (currentChannelId !== undefined) {
|
||||
systems.push({
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID,
|
||||
value: currentChannelId,
|
||||
});
|
||||
}
|
||||
|
||||
return operator.handleSystem({
|
||||
systems: [
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CONFIG,
|
||||
value: JSON.stringify(config),
|
||||
},
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.LICENSE,
|
||||
value: JSON.stringify(license),
|
||||
},
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
|
||||
value: currentUserId,
|
||||
},
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID,
|
||||
value: currentTeamId,
|
||||
},
|
||||
{
|
||||
id: SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID,
|
||||
value: currentChannelId,
|
||||
},
|
||||
],
|
||||
systems,
|
||||
prepareRecordsOnly: true,
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -1,14 +1,44 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {Database as DatabaseConstants} from '@constants';
|
||||
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type TeamChannelHistoryModel from '@typings/database/models/servers/team_channel_history';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
const {TEAM, TEAM_CHANNEL_HISTORY} = DatabaseConstants.MM_TABLES.SERVER;
|
||||
|
||||
export const addChannelToTeamHistory = async (operator: ServerDataOperator, teamId: string, channelId: string, prepareRecordsOnly = false) => {
|
||||
let tch: TeamChannelHistory|undefined;
|
||||
|
||||
try {
|
||||
const teamChannelHistory = (await operator.database.get(TEAM_CHANNEL_HISTORY).find(teamId)) as TeamChannelHistoryModel;
|
||||
const channelIds = teamChannelHistory.channelIds;
|
||||
channelIds.unshift(channelId);
|
||||
tch = {
|
||||
id: teamId,
|
||||
channel_ids: channelIds.slice(0, 5),
|
||||
};
|
||||
} catch {
|
||||
tch = {
|
||||
id: teamId,
|
||||
channel_ids: [channelId],
|
||||
};
|
||||
}
|
||||
|
||||
return operator.handleTeamChannelHistory({teamChannelHistories: [tch], prepareRecordsOnly});
|
||||
};
|
||||
|
||||
export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memberships: TeamMembership[], unreads: TeamUnread[]) => {
|
||||
try {
|
||||
const teamRecords = operator.handleTeam({prepareRecordsOnly: true, teams});
|
||||
const teamMembershipRecords = operator.handleTeamMemberships({prepareRecordsOnly: true, teamMemberships: memberships});
|
||||
const teamMemberships = memberships.filter((m) => teams.find((t) => t.id === m.team_id));
|
||||
const teamMembershipRecords = operator.handleTeamMemberships({prepareRecordsOnly: true, teamMemberships});
|
||||
const myTeams: MyTeam[] = unreads.map((unread) => {
|
||||
const matchingTeam = memberships.find((team) => team.team_id === unread.team_id);
|
||||
const matchingTeam = teamMemberships.find((team) => team.team_id === unread.team_id);
|
||||
return {id: unread.team_id, roles: matchingTeam?.roles ?? '', is_unread: unread.msg_count > 0, mentions_count: unread.mention_count};
|
||||
});
|
||||
const myTeamRecords = operator.handleMyTeam({
|
||||
@@ -21,3 +51,12 @@ export const prepareMyTeams = (operator: ServerDataOperator, teams: Team[], memb
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const queryTeamById = async (database: Database, teamId: string): Promise<TeamModel|undefined> => {
|
||||
try {
|
||||
const team = (await database.get(TEAM).find(teamId)) as TeamModel;
|
||||
return team;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,13 +5,14 @@ import {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
|
||||
import type User from '@typings/database/models/servers/user';
|
||||
|
||||
import {queryCurrentUserId} from './system';
|
||||
|
||||
export const queryUserById = async ({userId, database}: { userId: string; database: Database}) => {
|
||||
import type ServerDataOperator from '@database/operator/server_data_operator';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
export const queryUserById = async (database: Database, userId: string) => {
|
||||
try {
|
||||
const userRecord = (await database.collections.get(MM_TABLES.SERVER.USER).find(userId)) as User;
|
||||
const userRecord = (await database.collections.get(MM_TABLES.SERVER.USER).find(userId)) as UserModel;
|
||||
return userRecord;
|
||||
} catch {
|
||||
return undefined;
|
||||
@@ -21,8 +22,29 @@ export const queryUserById = async ({userId, database}: { userId: string; databa
|
||||
export const queryCurrentUser = async (database: Database) => {
|
||||
const currentUserId = await queryCurrentUserId(database);
|
||||
if (currentUserId) {
|
||||
return queryUserById({userId: currentUserId, database});
|
||||
return queryUserById(database, currentUserId);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const queryAllUsers = async (database: Database): Promise<UserModel[]> => {
|
||||
try {
|
||||
const userRecords = (await database.get(MM_TABLES.SERVER.USER).query().fetch()) as UserModel[];
|
||||
return userRecords;
|
||||
} catch {
|
||||
return Promise.resolve([] as UserModel[]);
|
||||
}
|
||||
};
|
||||
|
||||
export const prepareUsers = (operator: ServerDataOperator, users: UserProfile[]) => {
|
||||
try {
|
||||
if (users.length) {
|
||||
return operator.handleUsers({users, prepareRecordsOnly: true});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,14 +6,13 @@ import {Text} from 'react-native';
|
||||
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {General} from '@constants';
|
||||
import {t} from '@utils/i18n';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type ChannelDisplayNameProps = {
|
||||
channelType: string;
|
||||
currentUserId: string;
|
||||
displayName: string;
|
||||
teammateId: string;
|
||||
teammateId?: string;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
@@ -33,7 +32,7 @@ const ChannelDisplayName = ({channelType, currentUserId, displayName, teammateId
|
||||
>
|
||||
{isSelfDMChannel ? (
|
||||
<FormattedText
|
||||
id={t('channel_header.directchannel.you')}
|
||||
id={'channel_header.directchannel.you'}
|
||||
defaultMessage={'{displayname} (you)'}
|
||||
values={{displayname: displayName}}
|
||||
/>) : displayName
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {isGuest as isTeammateGuest} from '@utils/user';
|
||||
import React from 'react';
|
||||
import {TouchableOpacity, View} from 'react-native';
|
||||
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {General} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getUserIdFromChannelName, isGuest as isTeammateGuest} from '@utils/user';
|
||||
|
||||
import ChannelDisplayName from './channel_display_name';
|
||||
import ChannelGuestLabel from './channel_guest_label';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelInfoModel from '@typings/database/models/servers/channel_info';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type MyChannelSettingsModel from '@typings/database/models/servers/my_channel_settings';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
type WithChannelArgs = WithDatabaseArgs & {
|
||||
currentUserId: SystemModel;
|
||||
channel: ChannelModel;
|
||||
}
|
||||
|
||||
type ChannelTitleInputProps = {
|
||||
canHaveSubtitle: boolean;
|
||||
channel: ChannelModel;
|
||||
currentUserId: string;
|
||||
teammateId?: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
type ChannelTitleProps = ChannelTitleInputProps & {
|
||||
channelInfo: ChannelInfoModel;
|
||||
channelSettings: MyChannelSettingsModel;
|
||||
database: Database;
|
||||
teammate?: UserModel;
|
||||
teammateId: string;
|
||||
};
|
||||
|
||||
const ConnectedChannelTitle = ({
|
||||
const ChannelTitle = ({
|
||||
canHaveSubtitle,
|
||||
channel,
|
||||
channelInfo,
|
||||
@@ -48,7 +50,6 @@ const ConnectedChannelTitle = ({
|
||||
currentUserId,
|
||||
onPress,
|
||||
teammate,
|
||||
teammateId,
|
||||
}: ChannelTitleProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -57,12 +58,11 @@ const ConnectedChannelTitle = ({
|
||||
const isArchived = channel.deleteAt !== 0;
|
||||
const isChannelMuted = channelSettings.notifyProps?.mark_unread === 'mention';
|
||||
const isChannelShared = false; // todo: Read this value from ChannelModel when implemented
|
||||
|
||||
const hasGuests = channelInfo.guestCount > 0;
|
||||
const teammateRoles = teammate?.roles ?? '';
|
||||
const isGuest = channelType === General.DM_CHANNEL && isTeammateGuest(teammateRoles);
|
||||
|
||||
const showGuestLabel = (canHaveSubtitle || (isGuest && hasGuests) || (channelType === General.DM_CHANNEL && isGuest));
|
||||
const showGuestLabel = (canHaveSubtitle && ((isGuest && hasGuests) || (channelType === General.DM_CHANNEL && isGuest)));
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -81,7 +81,7 @@ const ConnectedChannelTitle = ({
|
||||
channelType={channelType}
|
||||
currentUserId={currentUserId}
|
||||
displayName={channel.displayName}
|
||||
teammateId={teammateId}
|
||||
teammateId={teammate?.id}
|
||||
theme={theme}
|
||||
/>
|
||||
{isChannelShared && (
|
||||
@@ -175,16 +175,21 @@ const getStyle = makeStyleSheetFromTheme((theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
const ChannelTitle: React.FunctionComponent<ChannelTitleInputProps> =
|
||||
withDatabase(
|
||||
withObservables(['channel', 'teammateId'], ({channel, teammateId, database}: { channel: ChannelModel; teammateId: string; database: Database }) => {
|
||||
return {
|
||||
channelInfo: database.collections.get(MM_TABLES.SERVER.CHANNEL_INFO).findAndObserve(channel.id),
|
||||
channelSettings: database.collections.get(MM_TABLES.SERVER.MY_CHANNEL_SETTINGS).findAndObserve(channel.id),
|
||||
...(teammateId && channel.displayName && {teammate: database.collections.get(MM_TABLES.SERVER.USER).findAndObserve(teammateId)}),
|
||||
};
|
||||
},
|
||||
)(ConnectedChannelTitle),
|
||||
);
|
||||
const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentUserId: database.collections.get(MM_TABLES.SERVER.SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID),
|
||||
}));
|
||||
|
||||
export default ChannelTitle;
|
||||
const enhancedChannelTitle = withObservables(['channel', 'currentUserId'], ({channel, currentUserId, database}: WithChannelArgs) => {
|
||||
let teammateId;
|
||||
if (channel.type === General.DM_CHANNEL && channel.displayName) {
|
||||
teammateId = getUserIdFromChannelName(currentUserId.value, channel.name);
|
||||
}
|
||||
|
||||
return {
|
||||
channelInfo: channel.info.observe(),
|
||||
channelSettings: channel.settings.observe(),
|
||||
...(teammateId && {teammate: database.collections.get(MM_TABLES.SERVER.USER).findAndObserve(teammateId)}),
|
||||
};
|
||||
});
|
||||
|
||||
export default withDatabase(withSystemIds(enhancedChannelTitle(ChannelTitle)));
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {DeviceEventEmitter, LayoutChangeEvent, Platform, useWindowDimensions, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import VIEWS from '@constants/view';
|
||||
import DEVICE from '@constants/device';
|
||||
import {General} from '@constants';
|
||||
import {getUserIdFromChannelName} from '@utils/user';
|
||||
import {Device, View as ViewConstants} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import ChannelTitle from './channel_title';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
|
||||
type ChannelNavBar = {
|
||||
channel: ChannelModel;
|
||||
currentUserId: string;
|
||||
onPress: () => void;
|
||||
config: ClientConfig;
|
||||
}
|
||||
|
||||
const ChannelNavBar = ({currentUserId, channel, onPress}: ChannelNavBar) => {
|
||||
const ChannelNavBar = ({channel, onPress}: ChannelNavBar) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const theme = useTheme();
|
||||
const style = getStyleFromTheme(theme);
|
||||
@@ -40,47 +39,40 @@ const ChannelNavBar = ({currentUserId, channel, onPress}: ChannelNavBar) => {
|
||||
height = layoutHeight;
|
||||
}
|
||||
|
||||
DeviceEventEmitter.emit(VIEWS.CHANNEL_NAV_BAR_CHANGED, layoutHeight);
|
||||
DeviceEventEmitter.emit(ViewConstants.CHANNEL_NAV_BAR_CHANGED, layoutHeight);
|
||||
};
|
||||
|
||||
switch (Platform.OS) {
|
||||
case 'android':
|
||||
height = VIEWS.ANDROID_TOP_PORTRAIT;
|
||||
if (DEVICE.IS_TABLET) {
|
||||
height = VIEWS.ANDROID_TOP_LANDSCAPE;
|
||||
height = ViewConstants.ANDROID_TOP_PORTRAIT;
|
||||
if (Device.IS_TABLET) {
|
||||
height = ViewConstants.ANDROID_TOP_LANDSCAPE;
|
||||
}
|
||||
break;
|
||||
case 'ios':
|
||||
height = VIEWS.IOS_TOP_PORTRAIT - VIEWS.STATUS_BAR_HEIGHT;
|
||||
if (DEVICE.IS_TABLET && isLandscape) {
|
||||
height = ViewConstants.IOS_TOP_PORTRAIT - ViewConstants.STATUS_BAR_HEIGHT;
|
||||
if (Device.IS_TABLET && isLandscape) {
|
||||
height -= 1;
|
||||
} else if (isLandscape) {
|
||||
height = VIEWS.IOS_TOP_LANDSCAPE;
|
||||
height = ViewConstants.IOS_TOP_LANDSCAPE;
|
||||
canHaveSubtitle = false;
|
||||
}
|
||||
|
||||
if (DEVICE.IS_IPHONE_WITH_INSETS && isLandscape) {
|
||||
if (Device.IS_IPHONE_WITH_INSETS && isLandscape) {
|
||||
canHaveSubtitle = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
let teammateId: string | undefined;
|
||||
if (channel?.type === General.DM_CHANNEL) {
|
||||
teammateId = getUserIdFromChannelName(currentUserId, channel.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
onLayout={onLayout}
|
||||
style={[style.header, {height: height + insets.top, paddingTop: insets.top, paddingLeft: insets.left, paddingRight: insets.right}]}
|
||||
>
|
||||
<ChannelTitle
|
||||
currentUserId={currentUserId}
|
||||
channel={channel}
|
||||
onPress={onPress}
|
||||
canHaveSubtitle={canHaveSubtitle}
|
||||
teammateId={teammateId}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
@@ -105,4 +97,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default ChannelNavBar;
|
||||
const withChannel = withObservables(['channelId'], ({channelId, database}: {channelId: string } & WithDatabaseArgs) => ({
|
||||
channel: database.get(MM_TABLES.SERVER.CHANNEL).findAndObserve(channelId),
|
||||
}));
|
||||
|
||||
export default withDatabase(withChannel(ChannelNavBar));
|
||||
|
||||
56
app/screens/channel/failed_channels/index.tsx
Normal file
56
app/screens/channel/failed_channels/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {retryInitialChannel} from '@actions/remote/retry';
|
||||
import FailedAction from '@components/failed_action';
|
||||
import Loading from '@components/loading';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type TeamModel from '@typings/database/models/servers/team';
|
||||
|
||||
type FailedChannelsProps = {
|
||||
team: TeamModel;
|
||||
}
|
||||
|
||||
const FailedChannels = ({team}: FailedChannelsProps) => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const title = intl.formatMessage({id: 'failed_action.something_wrong', defaultMessage: 'Something went wrong'});
|
||||
const message = intl.formatMessage({id: 'failed_action.fetch_channels', defaultMessage: 'Channels could not be loaded for {teamName}.'}, {teamName: team.displayName});
|
||||
|
||||
const onAction = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const {error} = await retryInitialChannel(serverUrl, team.id);
|
||||
|
||||
if (error) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FailedAction
|
||||
message={message}
|
||||
title={title}
|
||||
onAction={onAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const withTeam = withObservables(['teamId'], ({teamId, database}: {teamId: string} & WithDatabaseArgs) => ({
|
||||
team: database.get(MM_TABLES.SERVER.TEAM).findAndObserve(teamId),
|
||||
}));
|
||||
|
||||
export default withDatabase(withTeam(FailedChannels));
|
||||
42
app/screens/channel/failed_teams/index.tsx
Normal file
42
app/screens/channel/failed_teams/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {retryInitialTeamAndChannel} from '@actions/remote/retry';
|
||||
import FailedAction from '@components/failed_action';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
|
||||
const FailedTeams = () => {
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const title = intl.formatMessage({id: 'failed_action.something_wrong', defaultMessage: 'Something went wrong'});
|
||||
const message = intl.formatMessage({id: 'failed_action.fetch_teams', defaultMessage: 'An error ocurred while loading the teams of this server'});
|
||||
|
||||
const onAction = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const {error} = await retryInitialTeamAndChannel(serverUrl);
|
||||
|
||||
if (error) {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Loading/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FailedAction
|
||||
message={message}
|
||||
title={title}
|
||||
onAction={onAction}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default FailedTeams;
|
||||
@@ -1,49 +1,53 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Database} from '@nozbe/watermelondb';
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React, {useEffect} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import React, {useMemo} from 'react';
|
||||
import {Text, View} from 'react-native';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {logout} from '@actions/remote/general';
|
||||
import {logout} from '@actions/remote/session';
|
||||
import ServerVersion from '@components/server_version';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import ViewTypes from '@constants/view';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {useServerUrl} from '@context/server_url';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {unsupportedServer} from '@utils/supported_server/supported_server';
|
||||
import {isSystemAdmin as isUserSystemAdmin} from '@utils/user';
|
||||
import {Colors} from 'react-native/Libraries/NewAppScreen';
|
||||
|
||||
import ChannelNavBar from './channel_nav_bar';
|
||||
|
||||
import type ChannelModel from '@typings/database/models/servers/channel';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
import type {LaunchType} from '@typings/launch';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {Text, View} from 'react-native';
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const {SERVER: {CHANNEL, SYSTEM, USER}} = MM_TABLES;
|
||||
import FailedChannels from './failed_channels';
|
||||
import FailedTeams from './failed_teams';
|
||||
|
||||
type WithDatabaseArgs = { database: Database }
|
||||
type WithChannelAndThemeArgs = WithDatabaseArgs & {
|
||||
type ChannelProps = WithDatabaseArgs & LaunchProps & {
|
||||
currentChannelId: SystemModel;
|
||||
currentUserId: SystemModel;
|
||||
}
|
||||
type ChannelProps = WithDatabaseArgs & {
|
||||
channel: ChannelModel;
|
||||
config: SystemModel;
|
||||
launchType: LaunchType;
|
||||
user: UserModel;
|
||||
currentUserId: SystemModel;
|
||||
currentTeamId: SystemModel;
|
||||
time?: number;
|
||||
};
|
||||
|
||||
const Channel = ({channel, user, config, currentUserId}: ChannelProps) => {
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
sectionContainer: {
|
||||
marginTop: 32,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
}));
|
||||
|
||||
const Channel = ({currentChannelId, currentTeamId, time}: ChannelProps) => {
|
||||
// TODO: If we have LaunchProps, ensure we load the correct channel/post/modal.
|
||||
// TODO: If LaunchProps.error is true, use the LaunchProps.launchType to determine which
|
||||
// error message to display. For example:
|
||||
@@ -58,51 +62,47 @@ const Channel = ({channel, user, config, currentUserId}: ChannelProps) => {
|
||||
|
||||
//todo: https://mattermost.atlassian.net/browse/MM-37266
|
||||
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
useEffect(() => {
|
||||
const serverVersion = (config.value?.Version) || '';
|
||||
|
||||
const isSystemAdmin = isUserSystemAdmin(user.roles);
|
||||
|
||||
if (serverVersion) {
|
||||
const {RequiredServer: {MAJOR_VERSION, MIN_VERSION, PATCH_VERSION}} = ViewTypes;
|
||||
const isSupportedServer = isMinimumServerVersion(serverVersion, MAJOR_VERSION, MIN_VERSION, PATCH_VERSION);
|
||||
|
||||
if (!isSupportedServer) {
|
||||
// Only display the Alert if the TOS does not need to show first
|
||||
unsupportedServer(isSystemAdmin, intl.formatMessage);
|
||||
}
|
||||
}
|
||||
}, [config.value?.Version, intl.formatMessage, user.roles]);
|
||||
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const doLogout = () => {
|
||||
logout(serverUrl!);
|
||||
};
|
||||
|
||||
const renderComponent = useMemo(() => {
|
||||
if (!currentTeamId.value) {
|
||||
return <FailedTeams/>;
|
||||
}
|
||||
|
||||
if (!currentChannelId.value) {
|
||||
return <FailedChannels teamId={currentTeamId.value}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ChannelNavBar
|
||||
channelId={currentChannelId.value}
|
||||
onPress={() => null}
|
||||
/>
|
||||
);
|
||||
}, [currentTeamId.value, currentChannelId.value]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={styles.flex}
|
||||
mode='margin'
|
||||
edges={['left', 'right', 'bottom']}
|
||||
>
|
||||
<ServerVersion/>
|
||||
<StatusBar theme={theme}/>
|
||||
<ChannelNavBar
|
||||
currentUserId={currentUserId.value}
|
||||
channel={channel}
|
||||
onPress={() => null}
|
||||
config={config.value}
|
||||
/>
|
||||
{renderComponent}
|
||||
<View style={styles.sectionContainer}>
|
||||
<Text
|
||||
onPress={doLogout}
|
||||
style={styles.sectionTitle}
|
||||
>
|
||||
{`Logout from ${serverUrl}`}
|
||||
{`Loaded in: ${time || 0}ms. Logout from ${serverUrl}`}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -110,30 +110,9 @@ const Channel = ({channel, user, config, currentUserId}: ChannelProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme(() => ({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
sectionContainer: {
|
||||
marginTop: 32,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '600',
|
||||
color: Colors.black,
|
||||
},
|
||||
}));
|
||||
|
||||
export const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
const withSystemIds = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
currentChannelId: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID),
|
||||
currentUserId: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_USER_ID),
|
||||
config: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CONFIG),
|
||||
currentTeamId: database.collections.get(SYSTEM).findAndObserve(SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID),
|
||||
}));
|
||||
|
||||
const withChannelAndUser = withObservables(['currentChannelId'], ({currentChannelId, currentUserId, database}: WithChannelAndThemeArgs) => ({
|
||||
channel: database.collections.get(CHANNEL).findAndObserve(currentChannelId.value),
|
||||
user: database.collections.get(USER).findAndObserve(currentUserId.value),
|
||||
}));
|
||||
|
||||
export default withDatabase(withSystemIds(withChannelAndUser(Channel)));
|
||||
export default withDatabase(withSystemIds(Channel));
|
||||
|
||||
@@ -5,7 +5,7 @@ import {act, waitFor} from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import * as UserAPICalls from '@actions/remote/user';
|
||||
import * as SessionAPICalls from '@actions/remote/session';
|
||||
import {renderWithIntl, fireEvent} from '@test/intl-test-helper';
|
||||
|
||||
import ForgotPassword from './index';
|
||||
@@ -37,7 +37,7 @@ describe('ForgotPassword', () => {
|
||||
});
|
||||
|
||||
test('Should show password link sent texts', async () => {
|
||||
const spyOnResetAPICall = jest.spyOn(UserAPICalls, 'sendPasswordResetEmail');
|
||||
const spyOnResetAPICall = jest.spyOn(SessionAPICalls, 'sendPasswordResetEmail');
|
||||
const {getByTestId} = renderWithIntl(<ForgotPassword {...baseProps}/>);
|
||||
const emailTextInput = getByTestId('forgot.password.email');
|
||||
const resetButton = getByTestId('forgot.password.button');
|
||||
|
||||
@@ -9,7 +9,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import ErrorText from '@components/error_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {sendPasswordResetEmail} from '@actions/remote/user';
|
||||
import {sendPasswordResetEmail} from '@actions/remote/session';
|
||||
import {isEmail} from '@utils/helpers';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
||||
@@ -21,12 +21,11 @@ import Button from 'react-native-button';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
|
||||
import {NavigationFunctionComponent} from 'react-native-navigation';
|
||||
|
||||
import ErrorText, {ClientErrorWithIntl} from '@components/error_text';
|
||||
import ErrorText from '@components/error_text';
|
||||
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import {scheduleExpiredNotification} from '@actions/remote/push_notification';
|
||||
import {login} from '@actions/remote/user';
|
||||
import {login} from '@actions/remote/session';
|
||||
import {goToScreen, resetToChannel} from '@screens/navigation';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
@@ -53,7 +52,7 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<ClientErrorWithIntl | string | undefined | null>();
|
||||
const [error, setError] = useState<Partial<ClientErrorProps> | string | undefined | null>();
|
||||
|
||||
const [loginId, setLoginId] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
@@ -130,25 +129,35 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
|
||||
});
|
||||
|
||||
const signIn = async () => {
|
||||
const result = await login(serverUrl!, {loginId: loginId.toLowerCase(), password, config, license});
|
||||
const result: LoginActionResponse = await login(serverUrl!, {loginId: loginId.toLowerCase(), password, config, license});
|
||||
if (checkLoginResponse(result)) {
|
||||
await goToChannel();
|
||||
if (!result.hasTeams && !result.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GO TO NO TEAMS');
|
||||
return;
|
||||
}
|
||||
await goToChannel(result.time || 0, result.error as never);
|
||||
}
|
||||
};
|
||||
|
||||
const goToChannel = async () => {
|
||||
await scheduleExpiredNotification(serverUrl!, intl);
|
||||
resetToChannel({extra, launchError, launchType, serverUrl});
|
||||
const goToChannel = async (time: number, loginError?: never) => {
|
||||
const hasError = launchError || Boolean(loginError);
|
||||
resetToChannel({extra, launchError: hasError, launchType, serverUrl, time});
|
||||
};
|
||||
|
||||
const checkLoginResponse = (data: any) => {
|
||||
if (MFA_EXPECTED_ERRORS.includes(data?.error?.server_error_id)) {
|
||||
const checkLoginResponse = (data: LoginActionResponse) => {
|
||||
let errorId = '';
|
||||
if (typeof data.error === 'object' && data.error.server_error_id) {
|
||||
errorId = data.error.server_error_id;
|
||||
}
|
||||
|
||||
if (data.failed && MFA_EXPECTED_ERRORS.includes(errorId)) {
|
||||
goToMfa();
|
||||
setIsLoading(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data?.error) {
|
||||
if (data?.error && data.failed) {
|
||||
setIsLoading(false);
|
||||
setError(getLoginErrorMessage(data.error));
|
||||
return false;
|
||||
@@ -165,11 +174,15 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
|
||||
goToScreen(screen, title, {goToChannel, loginId, password, config, license, serverUrl, theme});
|
||||
};
|
||||
|
||||
const getLoginErrorMessage = (loginError: any) => {
|
||||
return (getServerErrorForLogin(loginError) || loginError);
|
||||
const getLoginErrorMessage = (loginError: string | ClientErrorProps) => {
|
||||
if (typeof loginError === 'string') {
|
||||
return loginError;
|
||||
}
|
||||
|
||||
return getServerErrorForLogin(loginError);
|
||||
};
|
||||
|
||||
const getServerErrorForLogin = (serverError: any) => {
|
||||
const getServerErrorForLogin = (serverError?: ClientErrorProps) => {
|
||||
if (!serverError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import {waitFor, renderWithIntl, fireEvent} from '@test/intl-test-helper';
|
||||
|
||||
import Login from './index';
|
||||
|
||||
jest.mock('@actions/remote/user', () => {
|
||||
jest.mock('@actions/remote/session', () => {
|
||||
return {
|
||||
login: () => {
|
||||
return {
|
||||
data: undefined,
|
||||
failed: true,
|
||||
error: {
|
||||
server_error_id: 'mfa.validate_token.authenticate.app_error',
|
||||
},
|
||||
|
||||
@@ -18,14 +18,14 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import ErrorText from '@components/error_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {login} from '@actions/remote/user';
|
||||
import {login} from '@actions/remote/session';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type MFAProps = {
|
||||
config: Partial<ClientConfig>;
|
||||
goToChannel: () => void;
|
||||
goToChannel: (time: number, error?: never) => void;
|
||||
license: Partial<ClientLicense>;
|
||||
loginId: string;
|
||||
password: string;
|
||||
@@ -34,6 +34,7 @@ type MFAProps = {
|
||||
}
|
||||
|
||||
const MFA = ({config, goToChannel, license, loginId, password, serverUrl, theme}: MFAProps) => {
|
||||
const intl = useIntl();
|
||||
const [token, setToken] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
@@ -74,13 +75,27 @@ const MFA = ({config, goToChannel, license, loginId, password, serverUrl, theme}
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
const result = await login(serverUrl, {loginId, password, mfaToken: token, config, license});
|
||||
const result: LoginActionResponse = await login(serverUrl, {loginId, password, mfaToken: token, config, license});
|
||||
setIsLoading(false);
|
||||
if (result?.error) {
|
||||
setError(result?.error);
|
||||
if (result?.error && result.failed) {
|
||||
if (typeof result.error == 'string') {
|
||||
setError(result?.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.error.intl) {
|
||||
setError(intl.formatMessage({id: result.error.intl.id, defaultMessage: result.error.intl.defaultMessage}, result.error.intl.values));
|
||||
return;
|
||||
}
|
||||
|
||||
setError(result.error.message);
|
||||
}
|
||||
if (!result.hasTeams && !result.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GO TO NO TEAMS');
|
||||
return;
|
||||
}
|
||||
goToChannel();
|
||||
goToChannel(result.time || 0, result.error as never);
|
||||
});
|
||||
|
||||
const getProceedView = () => {
|
||||
|
||||
@@ -9,9 +9,9 @@ import {renderWithIntl} from '@test/intl-test-helper';
|
||||
|
||||
import Mfa from './index';
|
||||
|
||||
jest.mock('@actions/remote/user', () => {
|
||||
jest.mock('@actions/remote/session', () => {
|
||||
return {
|
||||
login: jest.fn(),
|
||||
login: jest.fn().mockResolvedValue({error: undefined, hasTeams: true}),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {doPing} from '@actions/remote/general';
|
||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import AppVersion from '@components/app_version';
|
||||
import ErrorText, {ClientErrorWithIntl} from '@components/error_text';
|
||||
import ErrorText from '@components/error_text';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {Screens} from '@constants';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
@@ -47,7 +47,7 @@ const Server: NavigationFunctionComponent = ({componentId, extra, launchType, la
|
||||
id: 'mobile.launchError.notification',
|
||||
defaultMessage: 'Did not find a server for this notification',
|
||||
}) : undefined;
|
||||
const [error, setError] = useState<ClientErrorWithIntl|string|undefined>(initialError);
|
||||
const [error, setError] = useState<Partial<ClientErrorProps>|string|undefined>(initialError);
|
||||
|
||||
const [url, setUrl] = useState<string>('');
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
|
||||
import {useManagedConfig} from '@mattermost/react-native-emm';
|
||||
import React, {useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
import {SSO as SSOEnum} from '@constants';
|
||||
import {scheduleExpiredNotification} from '@actions/remote/push_notification';
|
||||
import {ssoLogin} from '@actions/remote/user';
|
||||
import {ssoLogin} from '@actions/remote/session';
|
||||
import {resetToChannel} from '@screens/navigation';
|
||||
import {ErrorApi} from '@typings/api/client';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
@@ -25,7 +22,6 @@ interface SSOProps extends LaunchProps {
|
||||
}
|
||||
|
||||
const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}: SSOProps) => {
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig();
|
||||
|
||||
const [loginError, setLoginError] = useState<string>('');
|
||||
@@ -61,8 +57,13 @@ const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}
|
||||
break;
|
||||
}
|
||||
|
||||
const onLoadEndError = (e: ErrorApi) => {
|
||||
const onLoadEndError = (e: ClientErrorProps | string) => {
|
||||
console.warn('Failed to set store from local data', e); // eslint-disable-line no-console
|
||||
if (typeof e === 'string') {
|
||||
setLoginError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
let errorMessage = e.message;
|
||||
if (e.url) {
|
||||
errorMessage += `\nURL: ${e.url}`;
|
||||
@@ -71,18 +72,22 @@ const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}
|
||||
};
|
||||
|
||||
const doSSOLogin = async (bearerToken: string, csrfToken: string) => {
|
||||
const {error = undefined} = await ssoLogin(serverUrl!, bearerToken, csrfToken);
|
||||
if (error) {
|
||||
onLoadEndError(error);
|
||||
setLoginError(error);
|
||||
const result: LoginActionResponse = await ssoLogin(serverUrl!, bearerToken, csrfToken);
|
||||
if (result?.error && result.failed) {
|
||||
onLoadEndError(result.error);
|
||||
return;
|
||||
}
|
||||
goToChannel();
|
||||
if (!result.hasTeams && !result.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('GO TO NO TEAMS');
|
||||
return;
|
||||
}
|
||||
goToChannel(result.time || 0, result.error as never);
|
||||
};
|
||||
|
||||
const goToChannel = () => {
|
||||
scheduleExpiredNotification(serverUrl!, intl);
|
||||
resetToChannel({extra, launchError, launchType, serverUrl});
|
||||
const goToChannel = (time: number, error?: never) => {
|
||||
const hasError = launchError || Boolean(error);
|
||||
resetToChannel({extra, launchError: hasError, launchType, serverUrl, time});
|
||||
};
|
||||
|
||||
const isSSOWithRedirectURLAvailable = isMinimumServerVersion(config.Version!, 5, 33, 0);
|
||||
|
||||
36
app/utils/channel/index.ts
Normal file
36
app/utils/channel/index.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General, Permissions} from '@constants';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import {hasPermission} from '@utils/role';
|
||||
|
||||
export function selectDefaultChannelForTeam(channels: Channel[], memberships: ChannelMembership[], teamId: string, roles?: Role[], locale = DEFAULT_LOCALE) {
|
||||
let channel: Channel|undefined;
|
||||
let canIJoinPublicChannelsInTeam = false;
|
||||
|
||||
if (roles) {
|
||||
canIJoinPublicChannelsInTeam = hasPermission(roles, Permissions.JOIN_PUBLIC_CHANNELS, true);
|
||||
}
|
||||
const defaultChannel = channels?.find((c) => c.name === General.DEFAULT_CHANNEL);
|
||||
const iAmMemberOfTheTeamDefaultChannel = Boolean(defaultChannel && memberships?.find((m) => m.channel_id === defaultChannel.id));
|
||||
const myFirstTeamChannel = channels?.filter((c) => c.team_id === teamId && c.type === General.OPEN_CHANNEL && Boolean(memberships?.find((m) => c.id === m.channel_id))).
|
||||
sort(sortChannelsByDisplayName.bind(null, locale))[0];
|
||||
|
||||
if (iAmMemberOfTheTeamDefaultChannel || canIJoinPublicChannelsInTeam) {
|
||||
channel = defaultChannel;
|
||||
} else {
|
||||
channel = myFirstTeamChannel || defaultChannel;
|
||||
}
|
||||
|
||||
return channel;
|
||||
}
|
||||
|
||||
export function sortChannelsByDisplayName(locale: string, a: Channel, b: Channel): number {
|
||||
// if both channels have the display_name defined
|
||||
if (a.display_name && b.display_name && a.display_name !== b.display_name) {
|
||||
return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true});
|
||||
}
|
||||
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
type SortByCreatAt = (Session | Channel | Team | Post) & {
|
||||
create_at: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function emptyFunction(e?: any) {
|
||||
// eslint-disable-line no-empty-function, @typescript-eslint/no-unused-vars
|
||||
export function emptyFunction(..._args: any[]) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// Generates a RFC-4122 version 4 compliant globally unique identifier.
|
||||
@@ -25,3 +29,11 @@ export const generateId = (): string => {
|
||||
});
|
||||
return id;
|
||||
};
|
||||
|
||||
export const sortByNewest = (a: SortByCreatAt, b: SortByCreatAt) => {
|
||||
if (a.create_at > b.create_at) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export default {
|
||||
initialLoad: 0,
|
||||
channelSwitch: 0,
|
||||
teamSwitch: 0,
|
||||
};
|
||||
@@ -1,11 +1,59 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {General} from '@constants';
|
||||
import {General, Preferences} from '@constants';
|
||||
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
|
||||
|
||||
export function getUserIdFromChannelName(userId: string, channelName: string): string {
|
||||
export function displayUsername(user?: UserProfile, locale?: string, teammateDisplayNameSetting?: string, useFallbackUsername = true) {
|
||||
let name = useFallbackUsername ? getLocalizedMessage(locale || DEFAULT_LOCALE, t('channel_loader.someone'), 'Someone') : '';
|
||||
|
||||
if (user) {
|
||||
if (teammateDisplayNameSetting === Preferences.DISPLAY_PREFER_NICKNAME) {
|
||||
name = user.nickname || getFullName(user);
|
||||
} else if (teammateDisplayNameSetting === Preferences.DISPLAY_PREFER_FULL_NAME) {
|
||||
name = getFullName(user);
|
||||
} else {
|
||||
name = user.username;
|
||||
}
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
name = user.username;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
export function displayGroupMessageName(users: UserProfile[], locale?: string, teammateDisplayNameSetting?: string, excludeUserId?: string) {
|
||||
const names: string[] = [];
|
||||
const sortUsernames = (a: string, b: string) => {
|
||||
return a.localeCompare(b, locale || DEFAULT_LOCALE, {numeric: true});
|
||||
};
|
||||
|
||||
users.forEach((u) => {
|
||||
if (u.id !== excludeUserId) {
|
||||
names.push(displayUsername(u, locale, teammateDisplayNameSetting));
|
||||
}
|
||||
});
|
||||
|
||||
return names.sort(sortUsernames).join(', ');
|
||||
}
|
||||
|
||||
export function getFullName(user: UserProfile): string {
|
||||
if (user.first_name && user.last_name) {
|
||||
return `${user.first_name} ${user.last_name}`;
|
||||
} else if (user.first_name) {
|
||||
return user.first_name;
|
||||
} else if (user.last_name) {
|
||||
return user.last_name;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function getUserIdFromChannelName(knownUserId: string, channelName: string): string {
|
||||
const ids = channelName.split('__');
|
||||
if (ids[0] === userId) {
|
||||
if (ids[0] === knownUserId) {
|
||||
return ids[1];
|
||||
}
|
||||
return ids[0];
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"channel_header.directchannel.you": "{displayname} (you)",
|
||||
"channel_loader.someone": "Someone",
|
||||
"channel.channelHasGuests": "This channel has guests",
|
||||
"channel.hasGuests": "This group message has guests",
|
||||
"channel.isGuest": "This person is a guest",
|
||||
"failed_action.fetch_channels": "Channels could not be loaded for {teamName}.",
|
||||
"failed_action.fetch_teams": "An error ocurred while loading the teams of this server",
|
||||
"failed_action.something_wrong": "Something went wrong",
|
||||
"failed_action.try_again": "Try again",
|
||||
"login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator",
|
||||
"login_mfa.token": "MFA Token",
|
||||
"login_mfa.tokenReq": "Please enter an MFA token",
|
||||
@@ -30,6 +39,8 @@
|
||||
"mobile.error_handler.description": "\nTap relaunch to open the app again. After restart, you can report the problem from the settings menu.\n",
|
||||
"mobile.error_handler.title": "Unexpected error occurred",
|
||||
"mobile.launchError.notification": "Did not find a server for this notification",
|
||||
"mobile.link.error.text": "Unable to open the link.",
|
||||
"mobile.link.error.title": "Error",
|
||||
"mobile.login_options.choose_title": "Choose your login method",
|
||||
"mobile.managed.blocked_by": "Blocked by {vendor}",
|
||||
"mobile.managed.exit": "Exit",
|
||||
@@ -55,13 +66,19 @@
|
||||
"mobile.routes.loginOptions": "Login Chooser",
|
||||
"mobile.routes.mfa": "Multi-factor Authentication",
|
||||
"mobile.routes.sso": "Single Sign-On",
|
||||
"mobile.server_upgrade.alert_description": "This server version is unsupported and users will be exposed to compatibility issues that cause crashes or severe bugs breaking core functionality of the app. Upgrading to server version {serverVersion} or later is required.",
|
||||
"mobile.server_upgrade.button": "OK",
|
||||
"mobile.server_upgrade.description": "\nA server upgrade is required to use the Mattermost app. Please ask your System Administrator for details.\n",
|
||||
"mobile.server_upgrade.dismiss": "Dismiss",
|
||||
"mobile.server_upgrade.learn_more": "Learn More",
|
||||
"mobile.server_upgrade.title": "Server upgrade required",
|
||||
"mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server",
|
||||
"mobile.server_url.empty": "Please enter a valid server URL",
|
||||
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
|
||||
"mobile.session_expired": "Session Expired: Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
|
||||
"mobile.unsupported_server.message": "Attachments, link previews, reactions and embed data may not be displayed correctly. If this issue persists contact your System Administrator to upgrade your Mattermost server.",
|
||||
"mobile.unsupported_server.ok": "OK",
|
||||
"mobile.unsupported_server.title": "Unsupported server version",
|
||||
"password_form.title": "Password Reset",
|
||||
"password_send.checkInbox": "Please check your inbox.",
|
||||
"password_send.description": "To reset your password, enter the email address you used to sign up",
|
||||
|
||||
@@ -706,7 +706,7 @@ SPEC CHECKSUMS:
|
||||
EXFileSystem: 0a04aba8da751b9ac954065911bcf166503f8267
|
||||
ExpoModulesCore: 2734852616127a6c1fc23012197890a6f3763dc7
|
||||
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
|
||||
FBReactNativeSpec: d35931295aacfe996e833c01a3701d4aa7a80cb4
|
||||
FBReactNativeSpec: cef0cc6d50abc92e8cf52f140aa22b5371cfec0b
|
||||
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
|
||||
jail-monkey: feb2bdedc4d67312cd41a455c22661d804bba985
|
||||
libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0
|
||||
|
||||
16088
package-lock.json
generated
16088
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
types/api/client.d.ts
vendored
39
types/api/client.d.ts
vendored
@@ -1,30 +1,21 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type logLevel = 'ERROR' | 'WARNING' | 'INFO';
|
||||
export type GenericClientResponse = {
|
||||
response: any;
|
||||
headers: Map<string, string>;
|
||||
data: any;
|
||||
};
|
||||
export type ErrorOffline = {
|
||||
message: string;
|
||||
url: string;
|
||||
};
|
||||
export type ErrorInvalidResponse = {
|
||||
intl: {
|
||||
id: string;
|
||||
defaultMessage: string;
|
||||
};
|
||||
};
|
||||
export type ErrorApi = {
|
||||
message: string;
|
||||
server_error_id: string;
|
||||
status_code: number;
|
||||
url: string;
|
||||
};
|
||||
export type Client4Error = ErrorOffline | ErrorInvalidResponse | ErrorApi;
|
||||
export type ClientOptions = {
|
||||
type logLevel = 'ERROR' | 'WARNING' | 'INFO';
|
||||
|
||||
type ClientOptions = {
|
||||
body?: any;
|
||||
method?: string;
|
||||
};
|
||||
|
||||
interface ClientErrorProps extends Error {
|
||||
details: Error;
|
||||
intl?:
|
||||
{defaultMessage?: string; id: string; values?: Record<string, any>} |
|
||||
{ defaultMessage?: string; id: string; values?: Record<string, any> } |
|
||||
{ id: string; defaultMessage?: string; values?: Record<string, any> } |
|
||||
{ id: string; defaultMessage?: string; values?: Record<string, any> };
|
||||
url: string;
|
||||
server_error_id?: string;
|
||||
status_code?: number;
|
||||
}
|
||||
|
||||
10
types/api/client_error.d.ts
vendored
10
types/api/client_error.d.ts
vendored
@@ -1,10 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
interface ClientErrorProps extends Error {
|
||||
details: Error;
|
||||
intl?: {defaultMessage?: string; id: string} | { defaultMessage?: string; id: string } | { id: string; defaultMessage?: string; values: any } | { id: string; defaultMessage?: string };
|
||||
url: string;
|
||||
server_error_id?: string | number;
|
||||
status_code?: number;
|
||||
}
|
||||
11
types/api/session.d.ts
vendored
11
types/api/session.d.ts
vendored
@@ -3,8 +3,15 @@
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
create_at: string|number;
|
||||
create_at: number;
|
||||
device_id?: string;
|
||||
expires_at: string|number;
|
||||
expires_at: number;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
interface LoginActionResponse {
|
||||
error?: ClientErrorProps | string;
|
||||
hasTeams?: boolean;
|
||||
failed: boolean;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
2
types/database/database.d.ts
vendored
2
types/database/database.d.ts
vendored
@@ -16,6 +16,8 @@ import type System from '@typings/database/models/servers/system';
|
||||
|
||||
import {DatabaseType} from './enums';
|
||||
|
||||
export type WithDatabaseArgs = { database: Database }
|
||||
|
||||
export type CreateServerDatabaseConfig = {
|
||||
dbName: string;
|
||||
dbType?: DatabaseType.DEFAULT | DatabaseType.SERVER;
|
||||
|
||||
10
types/database/models/servers/channel.d.ts
vendored
10
types/database/models/servers/channel.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Query, Relation} from '@nozbe/watermelondb';
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ export default class ChannelModel extends Model {
|
||||
deleteAt: number;
|
||||
|
||||
/** update_at : The timestamp to when this channel was last updated on the server */
|
||||
updateAt!: number;
|
||||
updateAt!: number;
|
||||
|
||||
/** display_name : The channel display name (e.g. Town Square ) */
|
||||
displayName: string;
|
||||
@@ -63,11 +63,11 @@ export default class ChannelModel extends Model {
|
||||
creator: Relation<UserModel>;
|
||||
|
||||
/** info : Query returning extra information about this channel from the CHANNEL_INFO table */
|
||||
info: Query<ChannelInfoModel>;
|
||||
info: Relation<ChannelInfoModel>;
|
||||
|
||||
/** membership : Query returning the membership data for the current user if it belongs to this channel */
|
||||
membership: Query<MyChannelModel>;
|
||||
membership: Relation<MyChannelModel>;
|
||||
|
||||
/** settings: User specific settings/preferences for this channel */
|
||||
settings: Query<MyChannelSettingsModel>;
|
||||
settings: Relation<MyChannelSettingsModel>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* ChannelInfo is an extension of the information contained in the Channel table.
|
||||
@@ -13,9 +13,6 @@ export default class ChannelInfoModel extends Model {
|
||||
/** table (name) : ChannelInfo */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** guest_count : The number of guest in this channel */
|
||||
guestCount: number;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* MyChannel is an extension of the Channel model but it lists only the Channels the app's user belongs to
|
||||
@@ -11,9 +11,6 @@ export default class MyChannelModel extends Model {
|
||||
/** table (name) : MyChannel */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** last_post_at : The timestamp for any last post on this channel */
|
||||
lastPostAt: number;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* The MyChannelSettings model represents the specific user's configuration to
|
||||
@@ -12,9 +12,6 @@ export default class MyChannelSettingsModel extends Model {
|
||||
/** table (name) : MyChannelSettings */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** notify_props : Configurations with regards to this channel */
|
||||
notifyProps: Partial<ChannelNotifyProps>;
|
||||
|
||||
|
||||
5
types/database/models/servers/my_team.d.ts
vendored
5
types/database/models/servers/my_team.d.ts
vendored
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* MyTeam represents only the teams that the current user belongs to
|
||||
@@ -11,9 +11,6 @@ export default class MyTeamModel extends Model {
|
||||
/** table (name) : MyTeam */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** is_unread : Boolean flag for unread messages on team level */
|
||||
isUnread: boolean;
|
||||
|
||||
|
||||
2
types/database/models/servers/post.d.ts
vendored
2
types/database/models/servers/post.d.ts
vendored
@@ -66,7 +66,7 @@ export default class PostModel extends Model {
|
||||
postsInThread: PostInThreadModel[];
|
||||
|
||||
/** metadata: All the extra data associated with this Post */
|
||||
metadata: PostMetadataModel[];
|
||||
metadata: Relation<PostMetadataModel>;
|
||||
|
||||
/** reactions: All the reactions associated with this Post */
|
||||
reactions: ReactionModel[];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* PostMetadata provides additional information on a POST
|
||||
@@ -11,9 +11,6 @@ export default class PostMetadataModel extends Model {
|
||||
/** table (name) : PostMetadata */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** data : Different types of data ranging from arrays, emojis, files to images and reactions. */
|
||||
data: PostMetadata;
|
||||
|
||||
|
||||
6
types/database/models/servers/team.d.ts
vendored
6
types/database/models/servers/team.d.ts
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Query} from '@nozbe/watermelondb';
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
@@ -48,13 +48,13 @@ export default class TeamModel extends Model {
|
||||
groupsInTeam: GroupsInTeamModel[];
|
||||
|
||||
/** myTeam : Retrieves additional information about the team that this user is possibly part of. This query might yield no result if the user isn't part of a team. */
|
||||
myTeam: Query<MyTeamModel>;
|
||||
myTeam: Relation<MyTeamModel>;
|
||||
|
||||
/** slashCommands : All the slash commands associated with this team */
|
||||
slashCommands: SlashCommandModel[];
|
||||
|
||||
/** teamChannelHistory : A history of the channels in this team that has been visited, ordered by the most recent and capped to the last 5 */
|
||||
teamChannelHistory: Query<TeamChannelHistoryModel>;
|
||||
teamChannelHistory: Relation<TeamChannelHistoryModel>;
|
||||
|
||||
/** members : All the users associated with this team */
|
||||
members: TeamMembershipModel[];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Relation} from '@nozbe/watermelondb';
|
||||
import Model, {Associations} from '@nozbe/watermelondb/Model';
|
||||
import Model from '@nozbe/watermelondb/Model';
|
||||
|
||||
/**
|
||||
* The TeamChannelHistory model helps keeping track of the last channel visited
|
||||
@@ -12,9 +12,6 @@ export default class TeamChannelHistoryModel extends Model {
|
||||
/** table (name) : TeamChannelHistory */
|
||||
static table: string;
|
||||
|
||||
/** associations : Describes every relationship to this table. */
|
||||
static associations: Associations;
|
||||
|
||||
/** channel_ids : An array containing the last 5 channels visited within this team order by recency */
|
||||
channelIds: string[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user