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:
Elias Nahum
2022-02-14 16:39:29 -03:00
committed by GitHub
parent 9f5f73b264
commit d35eac8bd3
36 changed files with 652 additions and 279 deletions

View File

@@ -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) {

View File

@@ -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};

View File

@@ -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};
};

View File

@@ -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
}
};

View File

@@ -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};
};

View File

@@ -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;

View File

@@ -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});

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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';

View File

@@ -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>
);

View File

@@ -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,

View File

@@ -26,7 +26,8 @@ type Props = {
const styles = StyleSheet.create({
badge: {
left: 25,
left: 13,
top: -8,
},
unread: {
left: 18,

View File

@@ -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';

View File

@@ -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}>

View File

@@ -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});
};
/**

View File

@@ -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}) => {

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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]);

View File

@@ -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>) => {

View File

@@ -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}
/>
}
</>
);
};

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();
};
}, []);

View File

@@ -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',

View File

@@ -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);
}

View File

@@ -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='Lets 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={'Lets 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>
);

View File

@@ -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
View 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);
}

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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;
}