forked from Ivasoft/mattermost-mobile
Gekidou servers (#5960)
* Servers logout and websocket state * addNewServer uility and rename file * add LaunchType for add new server * added time to LaunchProps type * Remove unnecessary props for launchToHome * Fix local action updateLastPostAt * Batch fetchProfilesPerChannels requests in chunks of 50 * WS handleUserAddedToChannelEvent to return early if no channelId is set * WS handleNewPostEvent to batch update last_post_at * add common actions to sync other servers * Entry actions to sync other servers data * Do not attempt to fetch notification data if payload does not contain a channelId * Set database as default at the end of the login flow * Handle logout when other servers remain * Handle Server options * Show alert when logging out from the account screen * Add workaround to have Lottie animate the loading component * Fix badge position in ServerIcon component * Server screen to support adding new server * Fix login screen to display error when credentials do not match * add localization strings * fix DatabaseProvider to update on server switch * Fix home icon and server icon subscriptions and badge display * Add dependencies to onLogout callback * feedback * Only updateLastPostAt if needed
This commit is contained in:
@@ -288,17 +288,17 @@ export const updateLastPostAt = async (serverUrl: string, channelId: string, las
|
||||
member.prepareUpdate((m) => {
|
||||
m.lastPostAt = lastPostAt;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
try {
|
||||
if (!prepareRecordsOnly) {
|
||||
await operator.batchRecords([member]);
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
} catch (error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
return {member};
|
||||
return {member: undefined};
|
||||
};
|
||||
|
||||
export async function updateChannelsDisplayName(serverUrl: string, channels: ChannelModel[], users: UserProfile[], prepareRecordsOnly = false) {
|
||||
|
||||
@@ -259,7 +259,7 @@ export const fetchMyChannel = async (serverUrl: string, teamId: string, channelI
|
||||
};
|
||||
|
||||
export const fetchMissingSidebarInfo = async (serverUrl: string, directChannels: Channel[], locale?: string, teammateDisplayNameSetting?: string, exludeUserId?: string, fetchOnly = false) => {
|
||||
const channelIds = directChannels.map((dc) => dc.id);
|
||||
const channelIds = directChannels.sort((a, b) => b.last_post_at - a.last_post_at).map((dc) => dc.id);
|
||||
const result = await fetchProfilesPerChannels(serverUrl, channelIds, exludeUserId, false);
|
||||
if (result.error) {
|
||||
return {error: result.error};
|
||||
|
||||
@@ -13,9 +13,9 @@ import {queryCurrentUser} from '@queries/servers/user';
|
||||
import {deleteV1Data} from '@utils/file';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
|
||||
import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData} from './common';
|
||||
import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData, syncOtherServers} from './common';
|
||||
|
||||
export const appEntry = async (serverUrl: string) => {
|
||||
export const appEntry = async (serverUrl: string, since = 0) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
@@ -24,7 +24,7 @@ export const appEntry = async (serverUrl: string) => {
|
||||
|
||||
const tabletDevice = await isTablet();
|
||||
const currentTeamId = await queryCurrentTeamId(database);
|
||||
const lastDisconnectedAt = await queryWebSocketLastDisconnected(database);
|
||||
const lastDisconnectedAt = (await queryWebSocketLastDisconnected(database)) || since;
|
||||
const fetchedData = await fetchAppEntryData(serverUrl, lastDisconnectedAt, currentTeamId);
|
||||
const fetchedError = (fetchedData as AppEntryError).error;
|
||||
|
||||
@@ -87,6 +87,11 @@ export const appEntry = async (serverUrl: string) => {
|
||||
const {config, license} = await queryCommonSystemValues(database);
|
||||
deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, initialTeamId);
|
||||
|
||||
if (!since) {
|
||||
// Load data from other servers
|
||||
syncOtherServers(serverUrl);
|
||||
}
|
||||
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
return {error, userId: meData?.user?.id};
|
||||
};
|
||||
|
||||
@@ -13,6 +13,8 @@ import DatabaseManager from '@database/manager';
|
||||
import {getPreferenceValue, getTeammateNameDisplaySetting} from '@helpers/api/preference';
|
||||
import {selectDefaultTeam} from '@helpers/api/team';
|
||||
import {DEFAULT_LOCALE} from '@i18n';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryAllServers} from '@queries/app/servers';
|
||||
import {queryAllChannelsForTeam} from '@queries/servers/channel';
|
||||
import {queryConfig} from '@queries/servers/system';
|
||||
import {queryAvailableTeamIds, queryMyTeams} from '@queries/servers/team';
|
||||
@@ -193,3 +195,40 @@ export const deferredAppEntryActions = async (
|
||||
|
||||
updateAllUsersSince(serverUrl, since);
|
||||
};
|
||||
|
||||
export const syncOtherServers = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.appDatabase?.database;
|
||||
if (database) {
|
||||
const servers = await queryAllServers(database);
|
||||
for (const server of servers) {
|
||||
if (server.url !== serverUrl && server.lastActiveAt > 0) {
|
||||
syncAllChannelMembers(server.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const syncAllChannelMembers = async (serverUrl: string) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
let client;
|
||||
try {
|
||||
client = NetworkManager.getClient(serverUrl);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const myTeams = await client.getMyTeams();
|
||||
let excludeDirect = false;
|
||||
for (const myTeam of myTeams) {
|
||||
fetchMyChannelsForTeam(serverUrl, myTeam.id, false, 0, false, excludeDirect);
|
||||
excludeDirect = true;
|
||||
}
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import EphemeralStore from '@store/ephemeral_store';
|
||||
import {isTablet} from '@utils/helpers';
|
||||
import {emitNotificationError} from '@utils/notification';
|
||||
|
||||
import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData} from './common';
|
||||
import {AppEntryData, AppEntryError, deferredAppEntryActions, fetchAppEntryData, syncOtherServers} from './common';
|
||||
|
||||
export const pushNotificationEntry = async (serverUrl: string, notification: NotificationWithData) => {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
@@ -143,6 +143,7 @@ export const pushNotificationEntry = async (serverUrl: string, notification: Not
|
||||
const {config, license} = await queryCommonSystemValues(operator.database);
|
||||
|
||||
deferredAppEntryActions(serverUrl, lastDisconnectedAt, currentUserId, currentUserLocale, prefData.preferences, config, license, teamData, chData, selectedTeamId, selectedChannelId);
|
||||
syncOtherServers(serverUrl);
|
||||
const error = teamData.error || chData?.error || prefData.error || meData.error;
|
||||
return {error, userId: meData?.user?.id};
|
||||
};
|
||||
|
||||
@@ -27,7 +27,12 @@ const fetchNotificationData = async (serverUrl: string, notification: Notificati
|
||||
}
|
||||
|
||||
try {
|
||||
const channelId = notification.payload!.channel_id!;
|
||||
const channelId = notification.payload?.channel_id;
|
||||
|
||||
if (!channelId) {
|
||||
return {error: 'No chanel Id was specified'};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const system = await queryCommonSystemValues(database);
|
||||
let teamId = notification.payload?.team_id;
|
||||
|
||||
@@ -5,6 +5,7 @@ import {DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@actions/local/timezone';
|
||||
import {Database, Events} from '@constants';
|
||||
import {SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
@@ -23,11 +24,12 @@ import type {LoginArgs} from '@typings/database/database';
|
||||
const HTTP_UNAUTHORIZED = 401;
|
||||
|
||||
export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
if (!operator) {
|
||||
return {error: `${serverUrl} database not found`};
|
||||
}
|
||||
|
||||
const {database} = operator;
|
||||
const {config, license}: { config: Partial<ClientConfig>; license: Partial<ClientLicense> } = await queryCommonSystemValues(database);
|
||||
|
||||
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
|
||||
@@ -45,10 +47,17 @@ export const completeLogin = async (serverUrl: string, user: UserProfile) => {
|
||||
fetchDataRetentionPolicy(serverUrl);
|
||||
}
|
||||
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
|
||||
// Start websocket
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
return operator.handleSystem({systems: [{
|
||||
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
|
||||
value: 0,
|
||||
}],
|
||||
prepareRecordsOnly: false});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -120,7 +129,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
|
||||
displayName: serverDisplayName,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
await server?.operator.handleSystem({
|
||||
systems: [{
|
||||
@@ -208,7 +217,6 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
|
||||
displayName: serverDisplayName,
|
||||
},
|
||||
});
|
||||
await DatabaseManager.setActiveServerDatabase(serverUrl);
|
||||
deviceToken = await queryDeviceToken(database);
|
||||
user = await client.getMe();
|
||||
await server?.operator.handleUsers({users: [user], prepareRecordsOnly: false});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Model, Q} from '@nozbe/watermelondb';
|
||||
import {chunk} from 'lodash';
|
||||
|
||||
import {updateChannelsDisplayName} from '@actions/local/channel';
|
||||
import {updateRecentCustomStatuses, updateLocalUser} from '@actions/local/user';
|
||||
@@ -114,8 +115,14 @@ export const fetchProfilesInChannel = async (serverUrl: string, channelId: strin
|
||||
|
||||
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);
|
||||
// Batch fetching profiles per channel by chunks of 50
|
||||
const channels = chunk(channelIds, 50);
|
||||
const data: ProfilesInChannelRequest[] = [];
|
||||
for await (const cIds of channels) {
|
||||
const requests = cIds.map((id) => fetchProfilesInChannel(serverUrl, id, excludeUserId, true));
|
||||
const response = await Promise.all(requests);
|
||||
data.push(...response);
|
||||
}
|
||||
|
||||
if (!fetchOnly) {
|
||||
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
|
||||
|
||||
@@ -25,7 +25,6 @@ export async function handleUserAddedToChannelEvent(serverUrl: string, msg: any)
|
||||
}
|
||||
const currentUser = await queryCurrentUser(database.database);
|
||||
const {team_id: teamId, channel_id: channelId, user_id: userId} = msg.data;
|
||||
|
||||
const models: Model[] = [];
|
||||
|
||||
try {
|
||||
|
||||
@@ -61,7 +61,10 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
// Ensure the channel membership
|
||||
let myChannel = await queryMyChannel(operator.database, post.channel_id);
|
||||
if (myChannel) {
|
||||
await updateLastPostAt(serverUrl, post.channel_id, post.create_at, false);
|
||||
const {member} = await updateLastPostAt(serverUrl, post.channel_id, post.create_at, false);
|
||||
if (member) {
|
||||
myChannel = member;
|
||||
}
|
||||
} else {
|
||||
const myChannelRequest = await fetchMyChannel(serverUrl, '', post.channel_id, true);
|
||||
if (myChannelRequest.error) {
|
||||
@@ -142,7 +145,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
|
||||
models.push(viewedAt);
|
||||
}
|
||||
} else {
|
||||
const hasMentions = msg.data.mentions.includes(currentUserId);
|
||||
const hasMentions = msg.data.mentions?.includes(currentUserId);
|
||||
preparedMyChannelHack(myChannel);
|
||||
const {member: unreadAt} = await markChannelAsUnread(
|
||||
serverUrl,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Events} from '@constants';
|
||||
import {t} from '@i18n';
|
||||
import {Analytics, create} from '@init/analytics';
|
||||
import {setServerCredentials} from '@init/credentials';
|
||||
import {semverFromServerVersion} from '@utils/supported_server';
|
||||
import {semverFromServerVersion} from '@utils/server';
|
||||
|
||||
import * as ClientConstants from './constants';
|
||||
import ClientError from './error';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import LottieView from 'lottie-react-native';
|
||||
import React from 'react';
|
||||
import React, {useEffect, useRef} from 'react';
|
||||
import {StyleSheet, View, ViewStyle} from 'react-native';
|
||||
|
||||
type LoadingProps = {
|
||||
@@ -12,14 +12,28 @@ type LoadingProps = {
|
||||
}
|
||||
|
||||
const Loading = ({containerStyle, style, color}: LoadingProps) => {
|
||||
const lottieRef = useRef<LottieView|null>(null);
|
||||
|
||||
// This is a workaround as it seems that autoPlay does not work properly on
|
||||
// newer versions of RN
|
||||
useEffect(() => {
|
||||
const animationFrame = requestAnimationFrame(() => {
|
||||
lottieRef.current?.reset();
|
||||
lottieRef.current?.play();
|
||||
});
|
||||
|
||||
return () => cancelAnimationFrame(animationFrame);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<LottieView
|
||||
source={require('./spinner.json')}
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
style={[styles.lottie, style]}
|
||||
colorFilters={color ? [{color, keypath: 'Shape Layer 1'}] : undefined}
|
||||
loop={true}
|
||||
ref={lottieRef}
|
||||
source={require('./spinner.json')}
|
||||
style={[styles.lottie, style]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
|
||||
"fontFamily": "OpenSans-Bold",
|
||||
"fontSize": 12,
|
||||
"height": 22,
|
||||
"left": 25,
|
||||
"left": 13,
|
||||
"lineHeight": 16.5,
|
||||
"minWidth": 26,
|
||||
"opacity": 1,
|
||||
@@ -84,7 +84,7 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1
|
||||
"paddingHorizontal": 5,
|
||||
"position": "absolute",
|
||||
"textAlign": "center",
|
||||
"top": -1,
|
||||
"top": -8,
|
||||
"transform": Array [
|
||||
Object {
|
||||
"scale": 1,
|
||||
|
||||
@@ -26,7 +26,8 @@ type Props = {
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
badge: {
|
||||
left: 25,
|
||||
left: 13,
|
||||
top: -8,
|
||||
},
|
||||
unread: {
|
||||
left: 18,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {map, switchMap} from 'rxjs/operators';
|
||||
import {SupportedServer} from '@constants';
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
import {isMinimumServerVersion} from '@utils/helpers';
|
||||
import {unsupportedServer} from '@utils/supported_server';
|
||||
import {unsupportedServer} from '@utils/server';
|
||||
import {isSystemAdmin} from '@utils/user';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
@@ -29,14 +29,16 @@ export function withServerDatabase<T>(Component: ComponentType<T>): ComponentTyp
|
||||
) : undefined;
|
||||
|
||||
if (server) {
|
||||
const serverDatabase =
|
||||
DatabaseManager.serverDatabases[server?.url]?.database;
|
||||
const database =
|
||||
DatabaseManager.serverDatabases[server.url]?.database;
|
||||
|
||||
setState({
|
||||
database: serverDatabase,
|
||||
serverUrl: server?.url,
|
||||
serverDisplayName: server?.displayName,
|
||||
});
|
||||
if (database) {
|
||||
setState({
|
||||
database,
|
||||
serverUrl: server.url,
|
||||
serverDisplayName: server.displayName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setState(undefined);
|
||||
}
|
||||
@@ -55,7 +57,10 @@ export function withServerDatabase<T>(Component: ComponentType<T>): ComponentTyp
|
||||
}
|
||||
|
||||
return (
|
||||
<DatabaseProvider database={state.database}>
|
||||
<DatabaseProvider
|
||||
database={state.database}
|
||||
key={state.serverUrl}
|
||||
>
|
||||
<UserLocaleProvider database={state.database}>
|
||||
<ServerProvider server={{displayName: state.serverDisplayName, url: state.serverUrl}}>
|
||||
<ThemeProvider database={state.database}>
|
||||
|
||||
@@ -376,9 +376,9 @@ class DatabaseManager {
|
||||
const databaseShm = `${androidFilesDir}${databaseName}.db-shm`;
|
||||
const databaseWal = `${androidFilesDir}${databaseName}.db-wal`;
|
||||
|
||||
FileSystem.deleteAsync(databaseFile);
|
||||
FileSystem.deleteAsync(databaseShm);
|
||||
FileSystem.deleteAsync(databaseWal);
|
||||
FileSystem.deleteAsync(databaseFile, {idempotent: true});
|
||||
FileSystem.deleteAsync(databaseShm, {idempotent: true});
|
||||
FileSystem.deleteAsync(databaseWal, {idempotent: true});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -90,6 +90,8 @@ class GlobalEventHandler {
|
||||
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
WebsocketManager.invalidateClient(serverUrl);
|
||||
|
||||
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
|
||||
await DatabaseManager.deleteServerDatabase(serverUrl);
|
||||
|
||||
const analyticsClient = analytics.get(serverUrl);
|
||||
@@ -102,11 +104,15 @@ class GlobalEventHandler {
|
||||
this.clearCookiesForServer(serverUrl);
|
||||
deleteFileCache(serverUrl);
|
||||
|
||||
if (!Object.keys(DatabaseManager.serverDatabases).length) {
|
||||
EphemeralStore.theme = undefined;
|
||||
}
|
||||
if (activeServerUrl === serverUrl) {
|
||||
let launchType: LaunchType = LaunchType.AddServer;
|
||||
if (!Object.keys(DatabaseManager.serverDatabases).length) {
|
||||
EphemeralStore.theme = undefined;
|
||||
launchType = LaunchType.Normal;
|
||||
}
|
||||
|
||||
relaunchApp({launchType: LaunchType.Normal}, true);
|
||||
relaunchApp({launchType}, true);
|
||||
}
|
||||
};
|
||||
|
||||
onServerConfigChanged = ({serverUrl, config}: {serverUrl: string; config: ClientConfig}) => {
|
||||
|
||||
@@ -142,14 +142,9 @@ const launchToHome = async (props: LaunchProps) => {
|
||||
break;
|
||||
}
|
||||
|
||||
const passProps = {
|
||||
skipMetrics: true,
|
||||
...props,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Launch app in Home screen');
|
||||
resetToHome(passProps);
|
||||
resetToHome(props);
|
||||
};
|
||||
|
||||
const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {TextStyle, View} from 'react-native';
|
||||
|
||||
import {logout} from '@actions/remote/session';
|
||||
import DrawerItem from '@components/drawer_item';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {queryServer} from '@queries/app/servers';
|
||||
import {useServerDisplayName, useServerUrl} from '@context/server';
|
||||
import {alertServerLogout} from '@utils/server';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -34,22 +34,19 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
|
||||
const Settings = ({style, theme}: Props) => {
|
||||
const styles = getStyleSheet(theme);
|
||||
const intl = useIntl();
|
||||
const serverUrl = useServerUrl();
|
||||
const [serverName, setServerName] = useState(serverUrl);
|
||||
const onLogout = useCallback(preventDoubleTap(() => {
|
||||
logout(serverUrl);
|
||||
}), []);
|
||||
const serverDisplayName = useServerDisplayName();
|
||||
|
||||
useEffect(() => {
|
||||
const appDatabase = DatabaseManager.appDatabase?.database;
|
||||
if (appDatabase) {
|
||||
queryServer(appDatabase, serverUrl).then((server) => {
|
||||
if (server) {
|
||||
setServerName(server.displayName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [serverUrl]);
|
||||
const onLogout = useCallback(preventDoubleTap(() => {
|
||||
alertServerLogout(
|
||||
serverDisplayName,
|
||||
() => {
|
||||
logout(serverUrl);
|
||||
},
|
||||
intl,
|
||||
);
|
||||
}), [serverDisplayName, serverUrl, intl]);
|
||||
|
||||
return (
|
||||
<DrawerItem
|
||||
@@ -64,7 +61,7 @@ const Settings = ({style, theme}: Props) => {
|
||||
<FormattedText
|
||||
id={'account.logout_from'}
|
||||
defaultMessage={'Log out of {serverName}'}
|
||||
values={{serverName}}
|
||||
values={{serverName: serverDisplayName}}
|
||||
style={styles.logOutFrom}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -92,6 +92,7 @@ export default function Servers() {
|
||||
for (const [key, map] of subscriptionsToRemove) {
|
||||
map.subscription?.unsubscribe();
|
||||
subscriptions.delete(key);
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
@@ -103,9 +104,10 @@ export default function Servers() {
|
||||
};
|
||||
subscriptions.set(url, unreads);
|
||||
unreads.subscription = subscribeUnreadAndMentionsByServer(url, unreadsSubscription);
|
||||
} else if (subscriptions.has(url)) {
|
||||
} else if ((!lastActiveAt || url === currentServerUrl) && subscriptions.has(url)) {
|
||||
subscriptions.get(url)?.subscription?.unsubscribe();
|
||||
subscriptions.delete(url);
|
||||
updateTotal();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -129,7 +131,7 @@ export default function Servers() {
|
||||
renderContent,
|
||||
snapPoints,
|
||||
theme,
|
||||
title: intl.formatMessage({id: 'servers.create_button', defaultMessage: 'Add a server'}),
|
||||
title: intl.formatMessage({id: 'your.servers', defaultMessage: 'Your servers'}),
|
||||
});
|
||||
}
|
||||
}, [isTablet, theme]);
|
||||
|
||||
@@ -7,8 +7,10 @@ import {ListRenderItemInfo, StyleSheet, View} from 'react-native';
|
||||
import {FlatList} from 'react-native-gesture-handler';
|
||||
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import BottomSheetContent from '@screens/bottom_sheet/content';
|
||||
import {addNewServer} from '@utils/server';
|
||||
|
||||
import ServerItem from './server_item';
|
||||
|
||||
@@ -33,10 +35,10 @@ const ServerList = ({servers}: Props) => {
|
||||
const intl = useIntl();
|
||||
const isTablet = useIsTablet();
|
||||
const serverUrl = useServerUrl();
|
||||
const theme = useTheme();
|
||||
|
||||
const onAddServer = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Lets add a server');
|
||||
const onAddServer = useCallback(async () => {
|
||||
addNewServer(theme);
|
||||
}, [servers]);
|
||||
|
||||
const renderServer = useCallback(({item: t}: ListRenderItemInfo<ServersModel>) => {
|
||||
|
||||
@@ -7,17 +7,23 @@ import {DeviceEventEmitter, Text, View} from 'react-native';
|
||||
import {RectButton} from 'react-native-gesture-handler';
|
||||
import Swipeable from 'react-native-gesture-handler/Swipeable';
|
||||
|
||||
import {appEntry} from '@actions/remote/entry';
|
||||
import {logout} from '@actions/remote/session';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Loading from '@components/loading';
|
||||
import ServerIcon from '@components/server_icon';
|
||||
import {Events} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {subscribeServerUnreadAndMentions} from '@database/subscription/unreads';
|
||||
import WebsocketManager from '@init/websocket_manager';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {addNewServer, alertServerLogout, alertServerRemove} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
|
||||
|
||||
import Options from './options';
|
||||
import WebSocket from './websocket';
|
||||
|
||||
import type ServersModel from '@typings/database/models/app/servers';
|
||||
import type MyChannelModel from '@typings/database/models/servers/my_channel';
|
||||
@@ -63,15 +69,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
logout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
borderRadius: 8,
|
||||
height: 16,
|
||||
left: 40,
|
||||
height: 18,
|
||||
left: 42,
|
||||
position: 'absolute',
|
||||
top: 11,
|
||||
width: 16,
|
||||
},
|
||||
nameContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
width: 18,
|
||||
},
|
||||
name: {
|
||||
color: theme.centerChannelColor,
|
||||
@@ -80,7 +82,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
offline: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
row: {flexDirection: 'row'},
|
||||
row: {flexDirection: 'row', alignItems: 'center'},
|
||||
serverIcon: {
|
||||
borderColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
height: 72,
|
||||
justifyContent: 'center',
|
||||
width: 45,
|
||||
},
|
||||
unread: {
|
||||
top: -2,
|
||||
left: 25,
|
||||
@@ -89,21 +98,24 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
color: changeOpacity(theme.centerChannelColor, 0.72),
|
||||
...typography('Body', 75, 'Regular'),
|
||||
},
|
||||
websocket: {
|
||||
marginLeft: 7,
|
||||
marginTop: 4,
|
||||
switching: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
const ServerItem = ({isActive, server}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const [badge, setBadge] = useState<BadgeValues>({isUnread: false, mentions: 0});
|
||||
const styles = getStyleSheet(theme);
|
||||
const swipeable = useRef<Swipeable>();
|
||||
const subscription = useRef<Subscription|undefined>();
|
||||
const websocketError = server.lastActiveAt ? !WebsocketManager.isConnected(server.url) : false;
|
||||
const database = DatabaseManager.serverDatabases[server.url]?.database;
|
||||
let displayName = server.displayName;
|
||||
|
||||
if (server.url === server.displayName) {
|
||||
displayName = intl.formatMessage({id: 'servers.default', defaultMessage: 'Default Server'});
|
||||
}
|
||||
@@ -119,6 +131,30 @@ const ServerItem = ({isActive, server}: Props) => {
|
||||
setBadge({isUnread, mentions});
|
||||
};
|
||||
|
||||
const logoutServer = async () => {
|
||||
await logout(server.url);
|
||||
|
||||
if (isActive) {
|
||||
dismissBottomSheet();
|
||||
} else {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
|
||||
}
|
||||
};
|
||||
|
||||
const removeServer = async () => {
|
||||
if (server.lastActiveAt > 0) {
|
||||
await logout(server.url);
|
||||
}
|
||||
|
||||
if (isActive) {
|
||||
dismissBottomSheet();
|
||||
} else {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, '');
|
||||
}
|
||||
|
||||
await DatabaseManager.destroyServerDatabase(server.url);
|
||||
};
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
const style = [styles.container];
|
||||
if (isActive) {
|
||||
@@ -137,23 +173,39 @@ const ServerItem = ({isActive, server}: Props) => {
|
||||
return style;
|
||||
}, [server.lastActiveAt]);
|
||||
|
||||
const onServerPressed = useCallback(() => {
|
||||
const handleLogin = useCallback(() => {
|
||||
addNewServer(theme, server.url, displayName);
|
||||
}, [server, theme, intl]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ON EDIT');
|
||||
}, [server]);
|
||||
|
||||
const handleLogout = useCallback(async () => {
|
||||
alertServerLogout(server.displayName, logoutServer, intl);
|
||||
}, [isActive, intl, server]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
alertServerRemove(server.displayName, removeServer, intl);
|
||||
}, [isActive, server, intl]);
|
||||
|
||||
const onServerPressed = useCallback(async () => {
|
||||
if (isActive) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ACTIVE SERVER', server.displayName);
|
||||
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
||||
return;
|
||||
}
|
||||
|
||||
if (server.lastActiveAt) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SWITCH TO SERVER', server.displayName);
|
||||
setSwitching(true);
|
||||
await appEntry(server.url, Date.now());
|
||||
await dismissBottomSheet();
|
||||
DatabaseManager.setActiveServerDatabase(server.url);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('LOGIN TO SERVER', server.displayName);
|
||||
}, [server]);
|
||||
handleLogin();
|
||||
}, [server, isActive, theme, intl]);
|
||||
|
||||
const onSwipeableWillOpen = useCallback(() => {
|
||||
DeviceEventEmitter.emit(Events.SWIPEABLE, server.url);
|
||||
@@ -162,11 +214,15 @@ const ServerItem = ({isActive, server}: Props) => {
|
||||
const renderActions = useCallback((progress) => {
|
||||
return (
|
||||
<Options
|
||||
onEdit={handleEdit}
|
||||
onLogin={handleLogin}
|
||||
onLogout={handleLogout}
|
||||
onRemove={handleRemove}
|
||||
progress={progress}
|
||||
server={server}
|
||||
/>
|
||||
);
|
||||
}, [server]);
|
||||
}, [isActive, server, theme, intl]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = DeviceEventEmitter.addListener(Events.SWIPEABLE, (url: string) => {
|
||||
@@ -182,9 +238,10 @@ const ServerItem = ({isActive, server}: Props) => {
|
||||
if (!isActive) {
|
||||
if (server.lastActiveAt && !subscription.current) {
|
||||
subscription.current = subscribeServerUnreadAndMentions(server.url, unreadsSubscription);
|
||||
} else if (!server.lastActiveAt && subscription.current) {
|
||||
subscription.current.unsubscribe();
|
||||
} else if (!server.lastActiveAt) {
|
||||
subscription.current?.unsubscribe();
|
||||
subscription.current = undefined;
|
||||
setBadge({isUnread: false, mentions: 0});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,62 +252,68 @@ const ServerItem = ({isActive, server}: Props) => {
|
||||
}, [server.lastActiveAt, isActive]);
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
renderRightActions={renderActions}
|
||||
friction={2}
|
||||
onSwipeableWillOpen={onSwipeableWillOpen}
|
||||
<>
|
||||
<Swipeable
|
||||
renderRightActions={renderActions}
|
||||
friction={2}
|
||||
onSwipeableWillOpen={onSwipeableWillOpen}
|
||||
|
||||
// @ts-expect-error legacy ref
|
||||
ref={swipeable}
|
||||
rightThreshold={40}
|
||||
>
|
||||
<View
|
||||
style={containerStyle}
|
||||
// @ts-expect-error legacy ref
|
||||
ref={swipeable}
|
||||
rightThreshold={40}
|
||||
>
|
||||
<RectButton
|
||||
onPress={onServerPressed}
|
||||
style={styles.button}
|
||||
rippleColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
<View
|
||||
style={containerStyle}
|
||||
>
|
||||
{!server.lastActiveAt &&
|
||||
<View style={styles.logout}>
|
||||
<CompassIcon
|
||||
name='minus-circle'
|
||||
size={16}
|
||||
color={theme.dndIndicator}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
<View style={serverStyle}>
|
||||
<ServerIcon
|
||||
badgeBackgroundColor={theme.mentionColor}
|
||||
badgeBorderColor={theme.mentionBg}
|
||||
badgeColor={theme.mentionBg}
|
||||
badgeStyle={styles.badge}
|
||||
iconColor={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
hasUnreads={badge.isUnread}
|
||||
mentionCount={badge.mentions}
|
||||
size={36}
|
||||
unreadStyle={styles.unread}
|
||||
/>
|
||||
<View style={styles.details}>
|
||||
<View style={styles.nameContainer}>
|
||||
<RectButton
|
||||
onPress={onServerPressed}
|
||||
style={styles.button}
|
||||
rippleColor={changeOpacity(theme.centerChannelColor, 0.16)}
|
||||
>
|
||||
<View style={serverStyle}>
|
||||
{!switching &&
|
||||
<ServerIcon
|
||||
badgeBackgroundColor={theme.mentionColor}
|
||||
badgeBorderColor={theme.mentionBg}
|
||||
badgeColor={theme.mentionBg}
|
||||
badgeStyle={styles.badge}
|
||||
iconColor={changeOpacity(theme.centerChannelColor, 0.56)}
|
||||
hasUnreads={badge.isUnread}
|
||||
mentionCount={badge.mentions}
|
||||
size={36}
|
||||
unreadStyle={styles.unread}
|
||||
style={styles.serverIcon}
|
||||
/>
|
||||
}
|
||||
{switching &&
|
||||
<Loading
|
||||
style={styles.swithing}
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
}
|
||||
<View style={styles.details}>
|
||||
<Text style={styles.name}>{displayName}</Text>
|
||||
{websocketError &&
|
||||
<CompassIcon
|
||||
name='alert-circle-outline'
|
||||
size={14.4}
|
||||
color={theme.dndIndicator}
|
||||
style={styles.websocket}
|
||||
/>
|
||||
}
|
||||
<Text style={styles.url}>{removeProtocol(stripTrailingSlashes(server.url))}</Text>
|
||||
</View>
|
||||
<Text style={styles.url}>{removeProtocol(stripTrailingSlashes(server.url))}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</RectButton>
|
||||
</View>
|
||||
</Swipeable>
|
||||
{!server.lastActiveAt && !switching &&
|
||||
<View style={styles.logout}>
|
||||
<CompassIcon
|
||||
name='alert-circle-outline'
|
||||
size={18}
|
||||
color={changeOpacity(theme.centerChannelColor, 0.64)}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
</RectButton>
|
||||
</View>
|
||||
</Swipeable>
|
||||
{Boolean(database) && server.lastActiveAt > 0 &&
|
||||
<WebSocket
|
||||
database={database}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import React from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {Animated, StyleSheet, View} from 'react-native';
|
||||
|
||||
@@ -13,6 +13,10 @@ import Option, {OPTION_SIZE} from './option';
|
||||
import type ServersModel from '@typings/database/models/app/servers';
|
||||
|
||||
type Props = {
|
||||
onEdit: () => void;
|
||||
onLogin: () => void;
|
||||
onLogout: () => void;
|
||||
onRemove: () => void;
|
||||
progress: Animated.AnimatedInterpolation;
|
||||
server: ServersModel;
|
||||
}
|
||||
@@ -27,7 +31,7 @@ const styles = StyleSheet.create({
|
||||
right: {borderTopRightRadius: 8, borderBottomRightRadius: 8},
|
||||
});
|
||||
|
||||
const ServerOptions = ({progress, server}: Props) => {
|
||||
const ServerOptions = ({onEdit, onLogin, onLogout, onRemove, progress, server}: Props) => {
|
||||
const intl = useIntl();
|
||||
const theme = useTheme();
|
||||
const isLoggedIn = server.lastActiveAt > 0;
|
||||
@@ -41,26 +45,6 @@ const ServerOptions = ({progress, server}: Props) => {
|
||||
defaultMessage: 'Log in',
|
||||
});
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ON EDIT');
|
||||
}, [server]);
|
||||
|
||||
const onLogin = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ON Login');
|
||||
}, [server]);
|
||||
|
||||
const onLogout = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ON Logout');
|
||||
}, [server]);
|
||||
|
||||
const onRemove = useCallback(() => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('ON Remove');
|
||||
}, [server]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Option
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
|
||||
|
||||
import WebSocket from './websocket';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type SystemModel from '@typings/database/models/servers/system';
|
||||
|
||||
const {SERVER: {SYSTEM}} = MM_TABLES;
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
isConnected: database.get<SystemModel>(SYSTEM).
|
||||
findAndObserve(SYSTEM_IDENTIFIERS.WEBSOCKET).
|
||||
pipe(
|
||||
switchMap(({value}) => of$(parseInt(value || 0, 10) > 0)),
|
||||
),
|
||||
}));
|
||||
|
||||
export default enhanced(WebSocket);
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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 CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
marginBottom: 12,
|
||||
top: -4,
|
||||
},
|
||||
unreachable: {
|
||||
color: theme.dndIndicator,
|
||||
marginLeft: 5,
|
||||
...typography('Body', 75, 'Regular'),
|
||||
},
|
||||
}));
|
||||
|
||||
const WebSocket = ({isConnected}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!isConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<CompassIcon
|
||||
name='alert-outline'
|
||||
color={theme.dndIndicator}
|
||||
size={14.4}
|
||||
/>
|
||||
<FormattedText
|
||||
id='server.websocket.unreachable'
|
||||
defaultMessage='Server is unreachable.'
|
||||
style={style.unreachable}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocket;
|
||||
@@ -75,6 +75,7 @@ const Home = ({isFocused, theme}: Props) => {
|
||||
for (const [key, map] of subscriptionsToRemove) {
|
||||
map.subscription?.unsubscribe();
|
||||
subscriptions.delete(key);
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
for (const server of servers) {
|
||||
@@ -86,9 +87,10 @@ const Home = ({isFocused, theme}: Props) => {
|
||||
};
|
||||
subscriptions.set(url, unreads);
|
||||
unreads.subscription = subscribeUnreadAndMentionsByServer(url, unreadsSubscription);
|
||||
} else if (subscriptions.has(url)) {
|
||||
} else if (!lastActiveAt && subscriptions.has(url)) {
|
||||
subscriptions.get(url)?.subscription?.unsubscribe();
|
||||
subscriptions.delete(url);
|
||||
updateTotal();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -101,6 +103,7 @@ const Home = ({isFocused, theme}: Props) => {
|
||||
subscriptions.forEach((unreads) => {
|
||||
unreads.subscription?.unsubscribe();
|
||||
});
|
||||
subscriptions.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -177,11 +177,11 @@ const LoginForm = ({config, extra, keyboardAwareRef, numberSSOs, serverDisplayNa
|
||||
|
||||
if (loginError instanceof ClientError) {
|
||||
const errorId = loginError.server_error_id;
|
||||
if (!errorId) {
|
||||
if (!errorId && loginError.message) {
|
||||
return loginError.message;
|
||||
}
|
||||
|
||||
if (errorId === 'api.user.login.invalid_credentials_email_username') {
|
||||
if (errorId === 'api.user.login.invalid_credentials_email_username' || !errorId) {
|
||||
return intl.formatMessage({
|
||||
id: 'login.invalid_credentials',
|
||||
defaultMessage: 'The email and password combination is incorrect',
|
||||
|
||||
@@ -9,15 +9,14 @@ import {Navigation, Options, OptionsModalPresentationStyle} from 'react-native-n
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Device, Screens} from '@constants';
|
||||
import {Device, Events, Screens} from '@constants';
|
||||
import NavigationConstants from '@constants/navigation';
|
||||
import {getDefaultThemeByAppearance} from '@context/theme';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {LaunchProps, LaunchType} from '@typings/launch';
|
||||
import {NavButtons} from '@typings/screens/navigation';
|
||||
import {changeOpacity, setNavigatorStyles} from '@utils/theme';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
export const appearanceControlledScreens = [Screens.SERVER, Screens.LOGIN, Screens.FORGOT_PASSWORD, Screens.MFA, Screens.SSO];
|
||||
@@ -105,11 +104,17 @@ function getThemeFromState(): Theme {
|
||||
return getDefaultThemeByAppearance();
|
||||
}
|
||||
|
||||
export function resetToHome(passProps = {}) {
|
||||
export function resetToHome(passProps: LaunchProps = {launchType: LaunchType.Normal}) {
|
||||
const theme = getThemeFromState();
|
||||
const isDark = tinyColor(theme.sidebarBg).isDark();
|
||||
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
|
||||
|
||||
if (passProps.launchType === LaunchType.AddServer) {
|
||||
dismissModal({componentId: Screens.SERVER});
|
||||
dismissModal({componentId: Screens.BOTTOM_SHEET});
|
||||
return;
|
||||
}
|
||||
|
||||
EphemeralStore.clearNavigationComponents();
|
||||
|
||||
const stack = {
|
||||
@@ -594,3 +599,8 @@ export async function bottomSheet({title, renderContent, snapPoints, initialSnap
|
||||
}, {modal: {swipeToDismiss: true}});
|
||||
}
|
||||
}
|
||||
|
||||
export async function dismissBottomSheet() {
|
||||
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
||||
await EphemeralStore.waitUntilScreensIsRemoved(Screens.BOTTOM_SHEET);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
additionalServer: boolean;
|
||||
theme: Theme;
|
||||
};
|
||||
|
||||
@@ -28,7 +29,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
connect: {
|
||||
width: 270,
|
||||
letterSpacing: -1,
|
||||
color: theme.mentionColor,
|
||||
color: theme.centerChannelColor,
|
||||
marginVertical: 12,
|
||||
...typography('Heading', 1000, 'SemiBold'),
|
||||
},
|
||||
@@ -41,29 +42,47 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const ServerHeader = ({theme}: Props) => {
|
||||
const ServerHeader = ({additionalServer, theme}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
let title;
|
||||
if (additionalServer) {
|
||||
title = (
|
||||
<FormattedText
|
||||
defaultMessage='Add a server'
|
||||
id='servers.create_button'
|
||||
style={[styles.connect, isTablet ? styles.connectTablet : undefined]}
|
||||
testID='mobile.components.select_server_view.add_server'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<FormattedText
|
||||
defaultMessage='Let’s Connect to a Server'
|
||||
id='mobile.components.select_server_view.msg_connect'
|
||||
style={[styles.connect, isTablet ? styles.connectTablet : undefined]}
|
||||
testID='mobile.components.select_server_view.msg_connect'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.textContainer}>
|
||||
{!additionalServer &&
|
||||
<FormattedText
|
||||
defaultMessage={'Welcome'}
|
||||
id={'mobile.components.select_server_view.msg_welcome'}
|
||||
testID={'mobile.components.select_server_view.msg_welcome'}
|
||||
defaultMessage='Welcome'
|
||||
id='mobile.components.select_server_view.msg_welcome'
|
||||
testID='mobile.components.select_server_view.msg_welcome'
|
||||
style={styles.welcome}
|
||||
/>
|
||||
}
|
||||
{title}
|
||||
<FormattedText
|
||||
defaultMessage={'Let’s Connect to a Server'}
|
||||
id={'mobile.components.select_server_view.msg_connect'}
|
||||
style={[styles.connect, isTablet ? styles.connectTablet : undefined]}
|
||||
testID={'mobile.components.select_server_view.msg_connect'}
|
||||
/>
|
||||
<FormattedText
|
||||
defaultMessage={"A Server is your team's communication hub which is accessed through a unique URL"}
|
||||
id={'mobile.components.select_server_view.msg_description'}
|
||||
defaultMessage="A Server is your team's communication hub which is accessed through a unique URL"
|
||||
id='mobile.components.select_server_view.msg_description'
|
||||
style={styles.description}
|
||||
testID={'mobile.components.select_server_view.msg_description'}
|
||||
testID='mobile.components.select_server_view.msg_description'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ import {t} from '@i18n';
|
||||
import NetworkManager from '@init/network_manager';
|
||||
import {queryServerByDisplayName, queryServerByIdentifier} from '@queries/app/servers';
|
||||
import Background from '@screens/background';
|
||||
import {goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||
import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||
import {DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
|
||||
import {getErrorMessage} from '@utils/client_error';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -31,6 +31,7 @@ import ServerForm from './form';
|
||||
import ServerHeader from './header';
|
||||
|
||||
interface ServerProps extends LaunchProps {
|
||||
closeButtonId?: string;
|
||||
componentId: string;
|
||||
theme: Theme;
|
||||
}
|
||||
@@ -44,7 +45,16 @@ const defaultServerUrlMessage = {
|
||||
|
||||
const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
|
||||
|
||||
const Server = ({componentId, extra, launchType, launchError, theme}: ServerProps) => {
|
||||
const Server = ({
|
||||
closeButtonId,
|
||||
componentId,
|
||||
displayName: defaultDisplayName,
|
||||
extra,
|
||||
launchType,
|
||||
launchError,
|
||||
serverUrl: defaultServerUrl,
|
||||
theme,
|
||||
}: ServerProps) => {
|
||||
const intl = useIntl();
|
||||
const managedConfig = useManagedConfig<ManagedConfig>();
|
||||
const dimensions = useWindowDimensions();
|
||||
@@ -60,7 +70,7 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp
|
||||
const {formatMessage} = intl;
|
||||
|
||||
useEffect(() => {
|
||||
const serverName = managedConfig?.serverName || LocalConfig.DefaultServerName;
|
||||
let serverName = managedConfig?.serverName || LocalConfig.DefaultServerName;
|
||||
let serverUrl = managedConfig?.serverUrl || LocalConfig.DefaultServerUrl;
|
||||
let autoconnect = managedConfig?.allowOtherServers === 'false' || LocalConfig.AutoSelectServerUrl;
|
||||
|
||||
@@ -78,6 +88,9 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp
|
||||
autoconnect = true;
|
||||
serverUrl = deepLinkServerUrl;
|
||||
}
|
||||
} else if (launchType === LaunchType.AddServer) {
|
||||
serverName = defaultDisplayName;
|
||||
serverUrl = defaultServerUrl;
|
||||
}
|
||||
|
||||
if (serverUrl) {
|
||||
@@ -120,6 +133,16 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp
|
||||
return () => unsubscribe.remove();
|
||||
}, [componentId, url, dimensions]);
|
||||
|
||||
useEffect(() => {
|
||||
const navigationEvents = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => {
|
||||
if (closeButtonId && buttonId === closeButtonId) {
|
||||
dismissModal({componentId});
|
||||
}
|
||||
});
|
||||
|
||||
return () => navigationEvents.remove();
|
||||
}, []);
|
||||
|
||||
const displayLogin = (serverUrl: string, config: ClientConfig, license: ClientLicense) => {
|
||||
const isLicensed = license.IsLicensed === 'true';
|
||||
const samlEnabled = config.EnableSaml === 'true' && isLicensed && license.SAML === 'true';
|
||||
@@ -310,7 +333,10 @@ const Server = ({componentId, extra, launchType, launchError, theme}: ServerProp
|
||||
scrollToOverflowEnabled={true}
|
||||
style={styles.flex}
|
||||
>
|
||||
<ServerHeader theme={theme}/>
|
||||
<ServerHeader
|
||||
additionalServer={launchType === LaunchType.AddServer}
|
||||
theme={theme}
|
||||
/>
|
||||
<ServerForm
|
||||
buttonDisabled={buttonDisabled}
|
||||
connecting={connecting}
|
||||
|
||||
173
app/utils/server/index.ts
Normal file
173
app/utils/server/index.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert, AlertButton, DeviceEventEmitter} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {Events, Screens, SupportedServer} from '@constants';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {LaunchType} from '@typings/launch';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
export function unsupportedServer(isSystemAdmin: boolean, intl: IntlShape) {
|
||||
if (isSystemAdmin) {
|
||||
return unsupportedServerAdminAlert(intl);
|
||||
}
|
||||
return unsupportedServerAlert(intl);
|
||||
}
|
||||
|
||||
export function semverFromServerVersion(value: string) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const split = value.split('.');
|
||||
|
||||
const major = parseInt(split[0], 10);
|
||||
const minor = parseInt(split[1] || '0', 10);
|
||||
const patch = parseInt(split[2] || '0', 10);
|
||||
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
export async function addNewServer(theme: Theme, serverUrl?: string, displayName?: string) {
|
||||
DeviceEventEmitter.emit(Events.CLOSE_BOTTOM_SHEET);
|
||||
|
||||
await EphemeralStore.waitUntilScreensIsRemoved(Screens.BOTTOM_SHEET);
|
||||
const closeButton = CompassIcon.getImageSourceSync('close', 24, changeOpacity(theme.centerChannelColor, 0.56));
|
||||
const closeButtonId = 'close-server';
|
||||
const props = {
|
||||
closeButtonId,
|
||||
displayName,
|
||||
launchType: LaunchType.AddServer,
|
||||
serverUrl,
|
||||
theme,
|
||||
};
|
||||
const options = {
|
||||
layout: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
componentBackgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
modal: {swipeToDismiss: false},
|
||||
topBar: {
|
||||
visible: true,
|
||||
drawBehind: true,
|
||||
translucient: true,
|
||||
noBorder: true,
|
||||
elevation: 0,
|
||||
background: {color: 'transparent'},
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
icon: closeButton,
|
||||
testID: 'close.server.button',
|
||||
}],
|
||||
leftButtonColor: undefined,
|
||||
title: {color: theme.sidebarHeaderTextColor},
|
||||
scrollEdgeAppearance: {
|
||||
active: true,
|
||||
noBorder: true,
|
||||
translucid: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
showModal(Screens.SERVER, '', props, options);
|
||||
}
|
||||
|
||||
export async function alertServerLogout(displayName: string, onPress: () => void, intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'server.logout.alert_title',
|
||||
defaultMessage: 'Are you sure you want to log out of {displayName}?',
|
||||
}, {displayName}),
|
||||
intl.formatMessage({
|
||||
id: 'server.logout.alert_description',
|
||||
defaultMessage: 'All associated data will be removed',
|
||||
}),
|
||||
[{
|
||||
style: 'cancel',
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
}, {
|
||||
style: 'destructive',
|
||||
text: intl.formatMessage({id: 'servers.logout', defaultMessage: 'Log out'}),
|
||||
onPress,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
export async function alertServerRemove(displayName: string, onPress: () => void, intl: IntlShape) {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'server.remove.alert_title',
|
||||
defaultMessage: 'Are you sure you want to remove {displayName}?',
|
||||
}, {displayName}),
|
||||
intl.formatMessage({
|
||||
id: 'server.remove.alert_description',
|
||||
defaultMessage: 'This will remove it from your list of servers. All associated data will be removed',
|
||||
}),
|
||||
[{
|
||||
style: 'cancel',
|
||||
text: intl.formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'}),
|
||||
}, {
|
||||
style: 'destructive',
|
||||
text: intl.formatMessage({id: 'servers.remove', defaultMessage: 'Remove'}),
|
||||
onPress,
|
||||
}],
|
||||
);
|
||||
}
|
||||
|
||||
function unsupportedServerAdminAlert(intl: IntlShape) {
|
||||
const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
|
||||
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.alert_description',
|
||||
defaultMessage: '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.',
|
||||
}, {serverVersion: SupportedServer.FULL_VERSION});
|
||||
|
||||
const cancel: AlertButton = {
|
||||
text: intl.formatMessage({id: 'mobile.server_upgrade.dismiss', defaultMessage: 'Dismiss'}),
|
||||
style: 'default',
|
||||
};
|
||||
|
||||
const learnMore: AlertButton = {
|
||||
text: intl.formatMessage({id: 'mobile.server_upgrade.learn_more', defaultMessage: 'Learn More'}),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
const url = 'https://docs.mattermost.com/administration/release-lifecycle.html';
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'mobile.link.error.title', defaultMessage: 'Error'}),
|
||||
intl.formatMessage({id: 'mobile.link.error.text', defaultMessage: 'Unable to open the link.'}),
|
||||
);
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
},
|
||||
};
|
||||
const buttons: AlertButton[] = [cancel, learnMore];
|
||||
const options = {cancelable: false};
|
||||
|
||||
Alert.alert(title, message, buttons, options);
|
||||
}
|
||||
|
||||
function unsupportedServerAlert(intl: IntlShape) {
|
||||
const title = intl.formatMessage({id: 'mobile.unsupported_server.title', defaultMessage: 'Unsupported server version'});
|
||||
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.unsupported_server.message',
|
||||
defaultMessage: '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.',
|
||||
});
|
||||
|
||||
const okButton: AlertButton = {
|
||||
text: intl.formatMessage({id: 'mobile.unsupported_server.ok', defaultMessage: 'OK'}),
|
||||
style: 'default',
|
||||
};
|
||||
|
||||
const buttons: AlertButton[] = [okButton];
|
||||
const options = {cancelable: false};
|
||||
|
||||
Alert.alert(title, message, buttons, options);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {IntlShape} from 'react-intl';
|
||||
import {Alert, AlertButton} from 'react-native';
|
||||
|
||||
import {SupportedServer} from '@constants';
|
||||
import {tryOpenURL} from '@utils/url';
|
||||
|
||||
export function unsupportedServer(isSystemAdmin: boolean, intl: IntlShape) {
|
||||
if (isSystemAdmin) {
|
||||
return unsupportedServerAdminAlert(intl);
|
||||
}
|
||||
return unsupportedServerAlert(intl);
|
||||
}
|
||||
|
||||
export function semverFromServerVersion(value: string) {
|
||||
if (!value || typeof value !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const split = value.split('.');
|
||||
|
||||
const major = parseInt(split[0], 10);
|
||||
const minor = parseInt(split[1] || '0', 10);
|
||||
const patch = parseInt(split[2] || '0', 10);
|
||||
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
function unsupportedServerAdminAlert(intl: IntlShape) {
|
||||
const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'});
|
||||
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.server_upgrade.alert_description',
|
||||
defaultMessage: '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.',
|
||||
}, {serverVersion: SupportedServer.FULL_VERSION});
|
||||
|
||||
const cancel: AlertButton = {
|
||||
text: intl.formatMessage({id: 'mobile.server_upgrade.dismiss', defaultMessage: 'Dismiss'}),
|
||||
style: 'default',
|
||||
};
|
||||
|
||||
const learnMore: AlertButton = {
|
||||
text: intl.formatMessage({id: 'mobile.server_upgrade.learn_more', defaultMessage: 'Learn More'}),
|
||||
style: 'cancel',
|
||||
onPress: () => {
|
||||
const url = 'https://docs.mattermost.com/administration/release-lifecycle.html';
|
||||
const onError = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({id: 'mobile.link.error.title', defaultMessage: 'Error'}),
|
||||
intl.formatMessage({id: 'mobile.link.error.text', defaultMessage: 'Unable to open the link.'}),
|
||||
);
|
||||
};
|
||||
|
||||
tryOpenURL(url, onError);
|
||||
},
|
||||
};
|
||||
const buttons: AlertButton[] = [cancel, learnMore];
|
||||
const options = {cancelable: false};
|
||||
|
||||
Alert.alert(title, message, buttons, options);
|
||||
}
|
||||
|
||||
function unsupportedServerAlert(intl: IntlShape) {
|
||||
const title = intl.formatMessage({id: 'mobile.unsupported_server.title', defaultMessage: 'Unsupported server version'});
|
||||
|
||||
const message = intl.formatMessage({
|
||||
id: 'mobile.unsupported_server.message',
|
||||
defaultMessage: '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.',
|
||||
});
|
||||
|
||||
const okButton: AlertButton = {
|
||||
text: intl.formatMessage({id: 'mobile.unsupported_server.ok', defaultMessage: 'OK'}),
|
||||
style: 'default',
|
||||
};
|
||||
|
||||
const buttons: AlertButton[] = [okButton];
|
||||
const options = {cancelable: false};
|
||||
|
||||
Alert.alert(title, message, buttons, options);
|
||||
}
|
||||
@@ -379,6 +379,11 @@
|
||||
"screens.channel_details": "Channel Details",
|
||||
"screens.channel_edit": "Edit Channel",
|
||||
"search_bar.search": "Search",
|
||||
"server.logout.alert_description": "All associated data will be removed",
|
||||
"server.logout.alert_title": "Are you sure you want to log out of {displayName}?",
|
||||
"server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed",
|
||||
"server.remove.alert_title": "Are you sure you want to remove {displayName}?",
|
||||
"server.websocket.unreachable": "Server is unreachable.",
|
||||
"servers.create_button": "Add a server",
|
||||
"servers.default": "Default Server",
|
||||
"servers.edit": "Edit",
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface DeepLinkWithData {
|
||||
}
|
||||
|
||||
export const LaunchType = {
|
||||
AddServer: 'add-server',
|
||||
Normal: 'normal',
|
||||
DeepLink: 'deeplink',
|
||||
Notification: 'notification',
|
||||
@@ -56,4 +57,6 @@ export interface LaunchProps {
|
||||
launchType: LaunchType;
|
||||
launchError?: Boolean;
|
||||
serverUrl?: string;
|
||||
displayName?: string;
|
||||
time?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user