diff --git a/.circleci/config.yml b/.circleci/config.yml
index b753e3dcf..292774343 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -83,6 +83,13 @@ commands:
- run:
name: Generate assets
command: node ./scripts/generate-assets.js
+ - run:
+ name: Compass Icons
+ environment:
+ COMPASS_ICONS: "node_modules/@mattermost/compass-icons/font/compass-icons.ttf"
+ command: |
+ cp "$COMPASS_ICONS" "assets/fonts/"
+ cp "$COMPASS_ICONS" "android/app/src/main/assets/fonts"
- save_cache:
name: Save assets cache
key: v1-assets-{{ checksum "assets/base/config.json" }}-{{ arch }}
diff --git a/app/actions/app/global.ts b/app/actions/app/global.ts
new file mode 100644
index 000000000..81d83458b
--- /dev/null
+++ b/app/actions/app/global.ts
@@ -0,0 +1,31 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {GLOBAL_IDENTIFIERS} from '@constants/database';
+import DatabaseManager from '@database/manager';
+
+export const storeDeviceToken = async (token: string, prepareRecordsOnly = false) => {
+ const operator = DatabaseManager.appDatabase?.operator;
+
+ if (!operator) {
+ return {error: 'No App database found'};
+ }
+
+ return operator.handleGlobal({
+ global: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: token}],
+ prepareRecordsOnly,
+ });
+};
+
+export const storeMultiServerTutorial = async (prepareRecordsOnly = false) => {
+ const operator = DatabaseManager.appDatabase?.operator;
+
+ if (!operator) {
+ return {error: 'No App database found'};
+ }
+
+ return operator.handleGlobal({
+ global: [{id: GLOBAL_IDENTIFIERS.MULTI_SERVER_TUTORIAL, value: 'true'}],
+ prepareRecordsOnly,
+ });
+};
diff --git a/app/actions/remote/session.ts b/app/actions/remote/session.ts
index afb5febd6..b5ddabbf1 100644
--- a/app/actions/remote/session.ts
+++ b/app/actions/remote/session.ts
@@ -153,7 +153,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
}
};
-export const logout = async (serverUrl: string, skipServerLogout = false) => {
+export const logout = async (serverUrl: string, skipServerLogout = false, removeServer = false) => {
if (!skipServerLogout) {
try {
const client = NetworkManager.getClient(serverUrl);
@@ -165,7 +165,7 @@ export const logout = async (serverUrl: string, skipServerLogout = false) => {
}
}
- DeviceEventEmitter.emit(Events.SERVER_LOGOUT, serverUrl);
+ DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl, removeServer});
};
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
diff --git a/app/components/tutorial_highlight/index.tsx b/app/components/tutorial_highlight/index.tsx
new file mode 100644
index 000000000..5dc9775a8
--- /dev/null
+++ b/app/components/tutorial_highlight/index.tsx
@@ -0,0 +1,57 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useEffect, useState} from 'react';
+import {Modal, StyleSheet, useWindowDimensions, View} from 'react-native';
+
+import HighlightItem from './item';
+
+type Props = {
+ children?: React.ReactNode;
+ itemBounds: TutorialItemBounds;
+ itemBorderRadius?: number;
+ onDismiss: () => void;
+ onShow: () => void;
+}
+
+const TutorialHighlight = ({children, itemBounds, itemBorderRadius, onDismiss, onShow}: Props) => {
+ const {width, height} = useWindowDimensions();
+ const [visible, setIsVisible] = useState(false);
+ const isLandscape = width > height;
+ const supportedOrientations = isLandscape ? 'landscape' : 'portrait';
+
+ useEffect(() => {
+ const t = setTimeout(() => {
+ setIsVisible(true);
+ }, 500);
+
+ return () => clearTimeout(t);
+ }, []);
+
+ return (
+
+
+
+ {children}
+
+ );
+};
+
+export default TutorialHighlight;
diff --git a/app/components/tutorial_highlight/item.tsx b/app/components/tutorial_highlight/item.tsx
new file mode 100644
index 000000000..de98bb12a
--- /dev/null
+++ b/app/components/tutorial_highlight/item.tsx
@@ -0,0 +1,57 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useMemo} from 'react';
+import {StyleSheet} from 'react-native';
+import Svg, {ClipPath, Defs, G, Path, Rect} from 'react-native-svg';
+import tinyColor from 'tinycolor2';
+
+import {useTheme} from '@context/theme';
+import {constructRectangularPathWithBorderRadius} from '@utils/svg';
+
+type Props = {
+ borderRadius?: number;
+ height: number;
+ itemBounds: TutorialItemBounds;
+ onDismiss: () => void;
+ width: number;
+}
+
+const HighlightItem = ({height, itemBounds, onDismiss, borderRadius = 0, width}: Props) => {
+ const theme = useTheme();
+ const isDark = tinyColor(theme.centerChannelBg).isDark();
+
+ const pathD = useMemo(() => {
+ const parent = {startX: 0, startY: 0, endX: width, endY: height};
+ return constructRectangularPathWithBorderRadius(parent, itemBounds, borderRadius);
+ }, [borderRadius, itemBounds, width]);
+
+ return (
+
+ );
+};
+
+export default HighlightItem;
diff --git a/app/components/tutorial_highlight/swipe_left.tsx b/app/components/tutorial_highlight/swipe_left.tsx
new file mode 100644
index 000000000..505116f6a
--- /dev/null
+++ b/app/components/tutorial_highlight/swipe_left.tsx
@@ -0,0 +1,62 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle} from 'react-native';
+
+import {useTheme} from '@context/theme';
+import {makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+import HandSwipeLeft from './swipe_left_hand';
+
+type Props = {
+ containerStyle?: StyleProp;
+ message: string;
+ style?: StyleProp;
+ textStyles?: StyleProp;
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ container: {
+ ...StyleSheet.absoluteFillObject,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ view: {
+ alignItems: 'center',
+ backgroundColor: theme.centerChannelBg,
+ borderRadius: 8,
+ height: 136,
+ padding: 16,
+ width: 247,
+ },
+ text: {
+ ...typography('Heading', 200),
+ color: theme.centerChannelColor,
+ marginTop: 8,
+ paddingHorizontal: 12,
+ textAlign: 'center',
+ },
+}));
+
+const TutorialSwipeLeft = ({containerStyle, message, style, textStyles}: Props) => {
+ const theme = useTheme();
+ const styles = getStyleSheet(theme);
+
+ return (
+
+
+
+
+ {message}
+
+
+
+ );
+};
+
+export default TutorialSwipeLeft;
diff --git a/app/components/tutorial_highlight/swipe_left_hand.tsx b/app/components/tutorial_highlight/swipe_left_hand.tsx
new file mode 100644
index 000000000..4030ca6f3
--- /dev/null
+++ b/app/components/tutorial_highlight/swipe_left_hand.tsx
@@ -0,0 +1,38 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import * as React from 'react';
+import Svg, {Path} from 'react-native-svg';
+
+type Props = {
+ fillColor: string;
+}
+
+function SwipeLeftHand({fillColor, ...props}: Props) {
+ return (
+
+ );
+}
+
+export default SwipeLeftHand;
+
diff --git a/app/constants/database.ts b/app/constants/database.ts
index 8a3e63f0d..07e867f87 100644
--- a/app/constants/database.ts
+++ b/app/constants/database.ts
@@ -66,6 +66,7 @@ export const SYSTEM_IDENTIFIERS = {
export const GLOBAL_IDENTIFIERS = {
DEVICE_TOKEN: 'deviceToken',
+ MULTI_SERVER_TUTORIAL: 'multiServerTutorial',
};
export default {
diff --git a/app/constants/screens.ts b/app/constants/screens.ts
index 553bf17e2..d1b235161 100644
--- a/app/constants/screens.ts
+++ b/app/constants/screens.ts
@@ -14,6 +14,7 @@ export const CHANNEL_EDIT = 'ChannelEdit';
export const CUSTOM_STATUS_CLEAR_AFTER = 'CustomStatusClearAfter';
export const CUSTOM_STATUS = 'CustomStatus';
export const EDIT_PROFILE = 'EditProfile';
+export const EDIT_SERVER = 'EditServer';
export const FORGOT_PASSWORD = 'ForgotPassword';
export const HOME = 'Home';
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
@@ -44,6 +45,7 @@ export default {
CUSTOM_STATUS_CLEAR_AFTER,
CUSTOM_STATUS,
EDIT_PROFILE,
+ EDIT_SERVER,
FORGOT_PASSWORD,
HOME,
INTEGRATION_SELECTOR,
diff --git a/app/context/theme/index.tsx b/app/context/theme/index.tsx
index c0cdf7daf..e45e02b73 100644
--- a/app/context/theme/index.tsx
+++ b/app/context/theme/index.tsx
@@ -38,6 +38,15 @@ export function getDefaultThemeByAppearance(): Theme {
export const ThemeContext = createContext(getDefaultThemeByAppearance());
const {Consumer, Provider} = ThemeContext;
+const updateThemeIfNeeded = (theme: Theme) => {
+ if (theme !== EphemeralStore.theme) {
+ EphemeralStore.theme = theme;
+ requestAnimationFrame(() => {
+ setNavigationStackStyles(theme);
+ });
+ }
+};
+
const ThemeProvider = ({currentTeamId, children, themes}: Props) => {
const getTheme = (): Theme => {
if (currentTeamId) {
@@ -45,12 +54,8 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => {
if (teamTheme?.value) {
try {
const theme = setThemeDefaults(JSON.parse(teamTheme.value) as Theme);
- if (theme !== EphemeralStore.theme) {
- EphemeralStore.theme = theme;
- requestAnimationFrame(() => {
- setNavigationStackStyles(theme);
- });
- }
+ updateThemeIfNeeded(theme);
+
return theme;
} catch {
// no theme change
@@ -59,10 +64,7 @@ const ThemeProvider = ({currentTeamId, children, themes}: Props) => {
}
const defaultTheme = getDefaultThemeByAppearance();
- EphemeralStore.theme = defaultTheme;
- requestAnimationFrame(() => {
- setNavigationStackStyles(defaultTheme);
- });
+ updateThemeIfNeeded(defaultTheme);
return defaultTheme;
};
diff --git a/app/database/manager/__mocks__/index.ts b/app/database/manager/__mocks__/index.ts
index 7c1cb1528..3af7e380f 100644
--- a/app/database/manager/__mocks__/index.ts
+++ b/app/database/manager/__mocks__/index.ts
@@ -208,6 +208,16 @@ class DatabaseManager {
return null;
};
+ public getActiveServerDisplayName = async (): Promise => {
+ const database = this.appDatabase?.database;
+ if (database) {
+ const server = await queryActiveServer(database);
+ return server?.displayName;
+ }
+
+ return null;
+ };
+
public getServerUrlFromIdentifier = async (identifier: string): Promise => {
const database = this.appDatabase?.database;
if (database) {
diff --git a/app/database/manager/index.ts b/app/database/manager/index.ts
index e2c1ebb04..6dd5a6281 100644
--- a/app/database/manager/index.ts
+++ b/app/database/manager/index.ts
@@ -252,7 +252,7 @@ class DatabaseManager {
};
/**
- * getActiveServerUrl: Get the record for active server database.
+ * getActiveServerUrl: Get the server url for active server database.
* @returns {Promise}
*/
public getActiveServerUrl = async (): Promise => {
@@ -265,6 +265,20 @@ class DatabaseManager {
return null;
};
+ /**
+ * getActiveServerDisplayName: Get the server display name for active server database.
+ * @returns {Promise}
+ */
+ public getActiveServerDisplayName = async (): Promise => {
+ const database = this.appDatabase?.database;
+ if (database) {
+ const server = await queryActiveServer(database);
+ return server?.displayName;
+ }
+
+ return null;
+ };
+
public getServerUrlFromIdentifier = async (identifier: string): Promise => {
const database = this.appDatabase?.database;
if (database) {
diff --git a/app/database/operator/app_data_operator/comparator/index.ts b/app/database/operator/app_data_operator/comparator/index.ts
index 686d470b4..8cc261af7 100644
--- a/app/database/operator/app_data_operator/comparator/index.ts
+++ b/app/database/operator/app_data_operator/comparator/index.ts
@@ -9,5 +9,5 @@ export const isRecordInfoEqualToRaw = (record: InfoModel, raw: AppInfo) => {
};
export const isRecordGlobalEqualToRaw = (record: GlobalModel, raw: IdValue) => {
- return raw.id === record.id && raw.value === record.value;
+ return raw.id === record.id;
};
diff --git a/app/database/subscription/servers.ts b/app/database/subscription/servers.ts
index c95cbbdb4..7064add6a 100644
--- a/app/database/subscription/servers.ts
+++ b/app/database/subscription/servers.ts
@@ -15,7 +15,7 @@ export const subscribeActiveServers = (observer: (servers: ServersModel[]) => vo
return db?.
get(SERVERS).
query(Q.where('identifier', Q.notEq(''))).
- observeWithColumns(['last_active_at']).
+ observeWithColumns(['display_name', 'last_active_at']).
subscribe(observer);
};
diff --git a/app/init/global_event_handler.ts b/app/init/global_event_handler.ts
index 5c32e20a7..034b7a486 100644
--- a/app/init/global_event_handler.ts
+++ b/app/init/global_event_handler.ts
@@ -23,6 +23,11 @@ import {deleteFileCache} from '@utils/file';
type LinkingCallbackArg = {url: string};
+type LogoutCallbackArg = {
+ serverUrl: string;
+ removeServer: boolean;
+}
+
class GlobalEventHandler {
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
@@ -83,7 +88,7 @@ class GlobalEventHandler {
}
};
- onLogout = async (serverUrl: string) => {
+ onLogout = async ({serverUrl, removeServer}: LogoutCallbackArg) => {
await removeServerCredentials(serverUrl);
const channelIds = await selectAllMyChannelIds(serverUrl);
PushNotifications.cancelChannelsNotifications(channelIds);
@@ -92,7 +97,12 @@ class GlobalEventHandler {
WebsocketManager.invalidateClient(serverUrl);
const activeServerUrl = await DatabaseManager.getActiveServerUrl();
- await DatabaseManager.deleteServerDatabase(serverUrl);
+ const activeServerDisplayName = await DatabaseManager.getActiveServerDisplayName();
+ if (removeServer) {
+ await DatabaseManager.destroyServerDatabase(serverUrl);
+ } else {
+ await DatabaseManager.deleteServerDatabase(serverUrl);
+ }
const analyticsClient = analytics.get(serverUrl);
if (analyticsClient) {
@@ -105,13 +115,18 @@ class GlobalEventHandler {
deleteFileCache(serverUrl);
if (activeServerUrl === serverUrl) {
+ let displayName = '';
let launchType: LaunchType = LaunchType.AddServer;
if (!Object.keys(DatabaseManager.serverDatabases).length) {
EphemeralStore.theme = undefined;
launchType = LaunchType.Normal;
+
+ if (activeServerDisplayName) {
+ displayName = activeServerDisplayName;
+ }
}
- relaunchApp({launchType}, true);
+ relaunchApp({launchType, serverUrl, displayName}, true);
}
};
@@ -154,7 +169,7 @@ class GlobalEventHandler {
const credentials = await getServerCredentials(serverUrl);
if (credentials) {
- this.onLogout(serverUrl);
+ this.onLogout({serverUrl, removeServer: false});
}
};
}
diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts
index 0c2ce6d31..21932bce6 100644
--- a/app/init/push_notifications.ts
+++ b/app/init/push_notifications.ts
@@ -14,10 +14,10 @@ import {
Registered,
} from 'react-native-notifications';
+import {storeDeviceToken} from '@actions/app/global';
import {markChannelAsViewed} from '@actions/local/channel';
import {backgroundNotification, openNotification} from '@actions/remote/notifications';
import {Device, Events, Navigation, Screens} from '@constants';
-import {GLOBAL_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import NativeNotifications from '@notifications';
@@ -187,7 +187,7 @@ class PushNotifications {
const serverUrl = await this.getServerUrlFromNotification(notification);
if (serverUrl) {
- DeviceEventEmitter.emit(Events.SERVER_LOGOUT, serverUrl);
+ DeviceEventEmitter.emit(Events.SERVER_LOGOUT, {serverUrl});
}
};
@@ -257,16 +257,7 @@ class PushNotifications {
prefix = Device.PUSH_NOTIFY_ANDROID_REACT_NATIVE;
}
- const operator = DatabaseManager.appDatabase?.operator;
-
- if (!operator) {
- return {error: 'No App database found'};
- }
-
- operator.handleGlobal({
- global: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: `${prefix}:${deviceToken}`}],
- prepareRecordsOnly: false,
- });
+ storeDeviceToken(`${prefix}:${deviceToken}`);
// Store the device token in the default database
this.requestNotificationReplyPermissions();
diff --git a/app/init/websocket_manager.ts b/app/init/websocket_manager.ts
index cf2448813..149a52028 100644
--- a/app/init/websocket_manager.ts
+++ b/app/init/websocket_manager.ts
@@ -2,7 +2,9 @@
// See LICENSE.txt for license information.
import NetInfo, {NetInfoState} from '@react-native-community/netinfo';
-import {AppState, AppStateStatus, DeviceEventEmitter, Platform} from 'react-native';
+import {debounce, DebouncedFunc} from 'lodash';
+import {AppState, AppStateStatus} from 'react-native';
+import BackgroundTimer from 'react-native-background-timer';
import {setCurrentUserStatusOffline} from '@actions/local/user';
import {fetchStatusByIds} from '@actions/remote/user';
@@ -15,11 +17,16 @@ import {queryAllUsers} from '@queries/servers/user';
import type {ServerCredential} from '@typings/credentials';
+const WAIT_TO_CLOSE = 15 * 1000;
+const WAIT_UNTIL_NEXT = 5 * 1000;
+
class WebsocketManager {
private clients: Record = {};
- private statusUpdatesIntervalIDs: Record = {};
- private previousAppState: AppStateStatus;
+ private connectionTimerIDs: Record void>> = {};
+ private isBackgroundTimerRunning = false;
private netConnected = false;
+ private previousAppState: AppStateStatus;
+ private statusUpdatesIntervalIDs: Record = {};
constructor() {
this.previousAppState = AppState.currentState;
@@ -46,17 +53,14 @@ class WebsocketManager {
AppState.addEventListener('change', this.onAppStateChange);
NetInfo.addEventListener(this.onNetStateChange);
-
- if (Platform.OS === 'android') {
- DeviceEventEmitter.addListener('windowFocusChanged', ({appState}: {appState: AppStateStatus}) => {
- this.onAppStateChange(appState);
- });
- }
};
public invalidateClient = (serverUrl: string) => {
this.clients[serverUrl]?.close();
this.clients[serverUrl]?.invalidate();
+ if (this.connectionTimerIDs[serverUrl]) {
+ this.connectionTimerIDs[serverUrl].cancel();
+ }
delete this.clients[serverUrl];
};
@@ -80,14 +84,21 @@ class WebsocketManager {
public closeAll = () => {
for (const client of Object.values(this.clients)) {
- client.close(true);
+ if (client.isConnected()) {
+ client.close(true);
+ }
}
};
- public openAll = () => {
- for (const client of Object.values(this.clients)) {
- if (!client.isConnected()) {
- client.initialize();
+ public openAll = async () => {
+ for await (const clientUrl of Object.keys(this.clients)) {
+ const activeServerUrl = await DatabaseManager.getActiveServerUrl();
+ if (clientUrl === activeServerUrl) {
+ this.initializeClient(clientUrl);
+ } else {
+ const bounce = debounce(this.initializeClient.bind(this, clientUrl), WAIT_UNTIL_NEXT);
+ this.connectionTimerIDs[clientUrl] = bounce;
+ bounce();
}
}
};
@@ -96,6 +107,24 @@ class WebsocketManager {
return this.clients[serverUrl]?.isConnected();
};
+ private cancelAllConnections = () => {
+ for (const url in this.connectionTimerIDs) {
+ if (this.connectionTimerIDs[url]) {
+ this.connectionTimerIDs[url].cancel();
+ delete this.connectionTimerIDs[url];
+ }
+ }
+ };
+
+ private initializeClient = (serverUrl: string) => {
+ const client: WebSocketClient = this.clients[serverUrl];
+ if (!client?.isConnected()) {
+ client.initialize();
+ }
+ this.connectionTimerIDs[serverUrl]?.cancel();
+ delete this.connectionTimerIDs[serverUrl];
+ };
+
private onFirstConnect = (serverUrl: string) => {
this.startPeriodicStatusUpdates(serverUrl);
handleFirstConnect(serverUrl);
@@ -156,13 +185,22 @@ class WebsocketManager {
return;
}
- if (appState !== 'active') {
- this.closeAll();
+ this.cancelAllConnections();
+ if (appState === 'background' && !this.isBackgroundTimerRunning) {
+ this.isBackgroundTimerRunning = true;
+ this.cancelAllConnections();
+ BackgroundTimer.runBackgroundTimer(() => {
+ this.closeAll();
+ BackgroundTimer.stopBackgroundTimer();
+ this.isBackgroundTimerRunning = false;
+ }, WAIT_TO_CLOSE);
this.previousAppState = appState;
return;
}
if (appState === 'active' && this.netConnected) { // Reopen the websockets only if there is connection
+ BackgroundTimer.stopBackgroundTimer();
+ this.isBackgroundTimerRunning = false;
this.openAll();
this.previousAppState = appState;
return;
diff --git a/app/screens/edit_server/form.tsx b/app/screens/edit_server/form.tsx
new file mode 100644
index 000000000..1a5c83317
--- /dev/null
+++ b/app/screens/edit_server/form.tsx
@@ -0,0 +1,198 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {MutableRefObject, useCallback, useEffect, useRef} from 'react';
+import {useIntl} from 'react-intl';
+import {Keyboard, Platform, useWindowDimensions, View} from 'react-native';
+import Button from 'react-native-button';
+import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
+
+import FloatingTextInput, {FloatingTextInputRef} from '@components/floating_text_input_label';
+import FormattedText from '@components/formatted_text';
+import Loading from '@components/loading';
+import {useIsTablet} from '@hooks/device';
+import {t} from '@i18n';
+import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+import {removeProtocol, stripTrailingSlashes} from '@utils/url';
+
+type Props = {
+ buttonDisabled: boolean;
+ connecting: boolean;
+ displayName?: string;
+ displayNameError?: string;
+ handleUpdate: () => void;
+ handleDisplayNameTextChanged: (text: string) => void;
+ keyboardAwareRef: MutableRefObject;
+ serverUrl: string;
+ theme: Theme;
+};
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ formContainer: {
+ alignItems: 'center',
+ maxWidth: 600,
+ width: '100%',
+ paddingHorizontal: 20,
+ },
+ fullWidth: {
+ width: '100%',
+ },
+ error: {
+ marginBottom: 18,
+ },
+ chooseText: {
+ alignSelf: 'flex-start',
+ color: changeOpacity(theme.centerChannelColor, 0.64),
+ marginTop: 8,
+ ...typography('Body', 75, 'Regular'),
+ },
+ connectButton: {
+ backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
+ width: '100%',
+ marginTop: 32,
+ marginLeft: 20,
+ marginRight: 20,
+ padding: 15,
+ },
+ loadingContainerStyle: {
+ marginRight: 10,
+ padding: 0,
+ top: -2,
+ },
+ loading: {
+ height: 20,
+ width: 20,
+ },
+}));
+
+const EditServerForm = ({
+ buttonDisabled,
+ connecting,
+ displayName = '',
+ displayNameError,
+ handleUpdate,
+ handleDisplayNameTextChanged,
+ keyboardAwareRef,
+ serverUrl,
+ theme,
+}: Props) => {
+ const {formatMessage} = useIntl();
+ const isTablet = useIsTablet();
+ const dimensions = useWindowDimensions();
+ const displayNameRef = useRef(null);
+ const styles = getStyleSheet(theme);
+
+ const onBlur = useCallback(() => {
+ if (Platform.OS === 'ios') {
+ const reset = !displayNameRef.current?.isFocused();
+ if (reset) {
+ keyboardAwareRef.current?.scrollToPosition(0, 0);
+ }
+ }
+ }, []);
+
+ const onUpdate = useCallback(() => {
+ Keyboard.dismiss();
+ handleUpdate();
+ }, [handleUpdate]);
+
+ const onFocus = useCallback(() => {
+ // For iOS we set the position of the input instead of
+ // having the KeyboardAwareScrollView figure it out by itself
+ // on Android KeyboardAwareScrollView does nothing as is handled
+ // by the OS
+ if (Platform.OS === 'ios') {
+ let offsetY = 160;
+ if (isTablet) {
+ const {width, height} = dimensions;
+ const isLandscape = width > height;
+ offsetY = isLandscape ? 230 : 100;
+ }
+ requestAnimationFrame(() => {
+ keyboardAwareRef.current?.scrollToPosition(0, offsetY);
+ });
+ }
+ }, [dimensions, isTablet]);
+
+ useEffect(() => {
+ if (Platform.OS === 'ios' && isTablet) {
+ if (displayNameRef.current?.isFocused()) {
+ onFocus();
+ } else {
+ keyboardAwareRef.current?.scrollToPosition(0, 0);
+ }
+ }
+ }, [onFocus]);
+
+ const buttonType = buttonDisabled ? 'disabled' : 'default';
+ const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
+ const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
+
+ let buttonID = t('edit_server.save');
+ let buttonText = 'Save';
+ let buttonIcon;
+
+ if (connecting) {
+ buttonID = t('edit_server.saving');
+ buttonText = 'Saving';
+ buttonIcon = (
+
+ );
+ }
+
+ return (
+
+
+
+
+ {!displayNameError &&
+
+ }
+
+
+ );
+};
+
+export default EditServerForm;
diff --git a/app/screens/edit_server/header.tsx b/app/screens/edit_server/header.tsx
new file mode 100644
index 000000000..ce81ac2ba
--- /dev/null
+++ b/app/screens/edit_server/header.tsx
@@ -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 FormattedText from '@components/formatted_text';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+type Props = {
+ theme: Theme;
+};
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ textContainer: {
+ marginBottom: 32,
+ maxWidth: 600,
+ width: '100%',
+ paddingHorizontal: 20,
+ },
+ title: {
+ letterSpacing: -1,
+ color: theme.centerChannelColor,
+ marginVertical: 12,
+ ...typography('Heading', 1000, 'SemiBold'),
+ },
+ description: {
+ color: changeOpacity(theme.centerChannelColor, 0.64),
+ ...typography('Body', 200, 'Regular'),
+ },
+}));
+
+const EditServerHeader = ({theme}: Props) => {
+ const styles = getStyleSheet(theme);
+
+ return (
+
+
+
+
+ );
+};
+
+export default EditServerHeader;
diff --git a/app/screens/edit_server/index.tsx b/app/screens/edit_server/index.tsx
new file mode 100644
index 000000000..036cd01ed
--- /dev/null
+++ b/app/screens/edit_server/index.tsx
@@ -0,0 +1,137 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React, {useCallback, useEffect, useRef, useState} from 'react';
+import {useIntl} from 'react-intl';
+import {Platform, View} from 'react-native';
+import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
+import {Navigation} from 'react-native-navigation';
+import {SafeAreaView} from 'react-native-safe-area-context';
+
+import DatabaseManager from '@database/manager';
+import {queryServerByDisplayName} from '@queries/app/servers';
+import Background from '@screens/background';
+import {dismissModal} from '@screens/navigation';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+
+import Form from './form';
+import Header from './header';
+
+import type ServersModel from '@typings/database/models/app/servers';
+
+interface ServerProps {
+ closeButtonId?: string;
+ componentId: string;
+ server: ServersModel;
+ theme: Theme;
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ appInfo: {
+ color: changeOpacity(theme.centerChannelColor, 0.56),
+ },
+ flex: {
+ flex: 1,
+ },
+ scrollContainer: {
+ alignItems: 'center',
+ height: '100%',
+ justifyContent: 'center',
+ },
+}));
+
+const EditServer = ({closeButtonId, componentId, server, theme}: ServerProps) => {
+ const {formatMessage} = useIntl();
+ const keyboardAwareRef = useRef();
+ const [saving, setSaving] = useState(false);
+ const [displayName, setDisplayName] = useState(server.displayName);
+ const [buttonDisabled, setButtonDisabled] = useState(true);
+ const [displayNameError, setDisplayNameError] = useState();
+ const styles = getStyleSheet(theme);
+
+ useEffect(() => {
+ setButtonDisabled(Boolean(displayName && displayName !== server.displayName));
+ }, [displayName]);
+
+ useEffect(() => {
+ const navigationEvents = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => {
+ if (closeButtonId && buttonId === closeButtonId) {
+ dismissModal({componentId});
+ }
+ });
+
+ return () => navigationEvents.remove();
+ }, []);
+
+ const handleUpdate = useCallback(async () => {
+ if (buttonDisabled) {
+ return;
+ }
+
+ if (displayNameError) {
+ setDisplayNameError(undefined);
+ }
+
+ setSaving(true);
+ const knownServer = await queryServerByDisplayName(DatabaseManager.appDatabase!.database, displayName);
+ if (knownServer && knownServer.lastActiveAt > 0 && knownServer.url !== server.url) {
+ setButtonDisabled(true);
+ setDisplayNameError(formatMessage({
+ id: 'mobile.server_name.exists',
+ defaultMessage: 'You are using this name for another server.',
+ }));
+ setSaving(false);
+ return;
+ }
+
+ await DatabaseManager.updateServerDisplayName(server.url, displayName);
+ dismissModal({componentId});
+ }, [!buttonDisabled && displayName, !buttonDisabled && displayNameError]);
+
+ const handleDisplayNameTextChanged = useCallback((text: string) => {
+ setDisplayName(text);
+ setDisplayNameError(undefined);
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default EditServer;
diff --git a/app/screens/home/channel_list/servers/servers_list/index.tsx b/app/screens/home/channel_list/servers/servers_list/index.tsx
index 76730ab77..715129c7a 100644
--- a/app/screens/home/channel_list/servers/servers_list/index.tsx
+++ b/app/screens/home/channel_list/servers/servers_list/index.tsx
@@ -41,9 +41,10 @@ const ServerList = ({servers}: Props) => {
addNewServer(theme);
}, [servers]);
- const renderServer = useCallback(({item: t}: ListRenderItemInfo) => {
+ const renderServer = useCallback(({item: t, index}: ListRenderItemInfo) => {
return (
diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts
new file mode 100644
index 000000000..3c09e0d80
--- /dev/null
+++ b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts
@@ -0,0 +1,33 @@
+// 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 {catchError, switchMap} from 'rxjs/operators';
+
+import {GLOBAL_IDENTIFIERS, MM_TABLES} from '@constants/database';
+
+import ServerItem from './server_item';
+
+import type GlobalModel from '@typings/database/models/app/global';
+import type ServersModel from '@typings/database/models/app/servers';
+
+const {MULTI_SERVER_TUTORIAL} = GLOBAL_IDENTIFIERS;
+const {APP: {GLOBAL}} = MM_TABLES;
+
+const enhance = withObservables(['highlight'], ({highlight, server}: {highlight: boolean; server: ServersModel}) => {
+ let tutorialWatched = of$(false);
+ if (highlight) {
+ tutorialWatched = server.collections.get(GLOBAL).findAndObserve(MULTI_SERVER_TUTORIAL).pipe(
+ switchMap(({value}) => of$(Boolean(value))),
+ catchError(() => of$(false)),
+ );
+ }
+
+ return {
+ server: server.observe(),
+ tutorialWatched,
+ };
+});
+
+export default enhance(ServerItem);
diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/index.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx
similarity index 79%
rename from app/screens/home/channel_list/servers/servers_list/server_item/index.tsx
rename to app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx
index c4d78f285..bd99497f7 100644
--- a/app/screens/home/channel_list/servers/servers_list/server_item/index.tsx
+++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx
@@ -7,17 +7,21 @@ import {DeviceEventEmitter, Text, View} from 'react-native';
import {RectButton} from 'react-native-gesture-handler';
import Swipeable from 'react-native-gesture-handler/Swipeable';
+import {storeMultiServerTutorial} from '@actions/app/global';
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 TutorialHighlight from '@components/tutorial_highlight';
+import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left';
import {Events} from '@constants';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {subscribeServerUnreadAndMentions} from '@database/subscription/unreads';
+import {useIsTablet} from '@hooks/device';
import {dismissBottomSheet} from '@screens/navigation';
-import {addNewServer, alertServerLogout, alertServerRemove} from '@utils/server';
+import {addNewServer, alertServerLogout, alertServerRemove, editServer} from '@utils/server';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
@@ -30,8 +34,10 @@ import type MyChannelModel from '@typings/database/models/servers/my_channel';
import type {Subscription} from 'rxjs';
type Props = {
+ highlight: boolean;
isActive: boolean;
server: ServersModel;
+ tutorialWatched: boolean;
}
type BadgeValues = {
@@ -103,16 +109,26 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
width: 40,
justifyContent: 'center',
},
+ tutorial: {
+ top: -30,
+ },
+ tutorialTablet: {
+ top: -80,
+ },
}));
-const ServerItem = ({isActive, server}: Props) => {
+const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
const intl = useIntl();
const theme = useTheme();
+ const isTablet = useIsTablet();
const [switching, setSwitching] = useState(false);
const [badge, setBadge] = useState({isUnread: false, mentions: 0});
const styles = getStyleSheet(theme);
const swipeable = useRef();
const subscription = useRef();
+ const viewRef = useRef(null);
+ const [showTutorial, setShowTutorial] = useState(false);
+ const [itemBounds, setItemBounds] = useState({startX: 0, startY: 0, endX: 0, endY: 0});
const database = DatabaseManager.serverDatabases[server.url]?.database;
let displayName = server.displayName;
@@ -142,17 +158,22 @@ const ServerItem = ({isActive, server}: Props) => {
};
const removeServer = async () => {
- if (server.lastActiveAt > 0) {
- await logout(server.url);
- }
+ const skipLogoutFromServer = server.lastActiveAt === 0;
+ await dismissBottomSheet();
+ await logout(server.url, skipLogoutFromServer, true);
+ };
- if (isActive) {
- dismissBottomSheet();
- } else {
- DeviceEventEmitter.emit(Events.SWIPEABLE, '');
- }
-
- await DatabaseManager.destroyServerDatabase(server.url);
+ const startTutorial = () => {
+ viewRef.current?.measureInWindow((x, y, w, h) => {
+ const bounds: TutorialItemBounds = {
+ startX: x - 20,
+ startY: y - 5,
+ endX: x + w + 20,
+ endY: y + h + 5,
+ };
+ setShowTutorial(true);
+ setItemBounds(bounds);
+ });
};
const containerStyle = useMemo(() => {
@@ -177,9 +198,15 @@ const ServerItem = ({isActive, server}: Props) => {
addNewServer(theme, server.url, displayName);
}, [server, theme, intl]);
+ const handleDismissTutorial = useCallback(() => {
+ swipeable.current?.close();
+ setShowTutorial(false);
+ storeMultiServerTutorial();
+ }, []);
+
const handleEdit = useCallback(() => {
- // eslint-disable-next-line no-console
- console.log('ON EDIT');
+ DeviceEventEmitter.emit(Events.SWIPEABLE, '');
+ editServer(theme, server);
}, [server]);
const handleLogout = useCallback(async () => {
@@ -190,6 +217,10 @@ const ServerItem = ({isActive, server}: Props) => {
alertServerRemove(server.displayName, removeServer, intl);
}, [isActive, server, intl]);
+ const handleShowTutorial = useCallback(() => {
+ swipeable.current?.openRight();
+ }, []);
+
const onServerPressed = useCallback(async () => {
if (isActive) {
dismissBottomSheet();
@@ -251,6 +282,14 @@ const ServerItem = ({isActive, server}: Props) => {
};
}, [server.lastActiveAt, isActive]);
+ useEffect(() => {
+ let time: NodeJS.Timeout;
+ if (highlight && !tutorialWatched) {
+ time = setTimeout(startTutorial, 300);
+ }
+ return () => clearTimeout(time);
+ }, [highlight, tutorialWatched]);
+
return (
<>
{
>
{
database={database}
/>
}
+ {showTutorial &&
+
+
+
+ }
>
);
};
diff --git a/app/screens/index.tsx b/app/screens/index.tsx
index 37e299506..357c5e854 100644
--- a/app/screens/index.tsx
+++ b/app/screens/index.tsx
@@ -57,72 +57,30 @@ Navigation.setLazyComponentRegistrator((screenName) => {
let screen: any|undefined;
let extraStyles: StyleProp;
switch (screenName) {
- case Screens.ABOUT:
+ case Screens.ABOUT:
screen = withServerDatabase(require('@screens/about').default);
break;
- // case 'AdvancedSettings':
- // screen = require('@screens/settings/advanced_settings').default;
- // break;
- case Screens.BOTTOM_SHEET:
+ case Screens.BOTTOM_SHEET:
screen = withServerDatabase(require('@screens/bottom_sheet').default);
break;
- case Screens.CHANNEL:
+ case Screens.CHANNEL:
screen = withServerDatabase(require('@screens/channel').default);
break;
- case Screens.CUSTOM_STATUS:
+ case Screens.CUSTOM_STATUS:
screen = withServerDatabase(require('@screens/custom_status').default);
break;
- case Screens.CUSTOM_STATUS_CLEAR_AFTER:
+ case Screens.CUSTOM_STATUS_CLEAR_AFTER:
screen = withServerDatabase(require('@screens/custom_status_clear_after').default);
break;
- case Screens.EMOJI_PICKER:
+ case Screens.EMOJI_PICKER:
screen = withServerDatabase(require('@screens/emoji_picker').default);
break;
- // case 'ChannelAddMembers':
- // screen = require('@screens/channel_add_members').default;
- // break;
- // case 'ChannelInfo':
- // screen = require('@screens/channel_info').default;
- // break;
- // case 'ChannelMembers':
- // screen = require('@screens/channel_members').default;
- // break;
- // case 'ChannelNotificationPreference':
- // screen = require('@screens/channel_notification_preference').default;
- // break;
- // case 'ClientUpgrade':
- // screen = require('@screens/client_upgrade').default;
- // break;
- // case 'ClockDisplaySettings':
- // screen = require('@screens/settings/clock_display').default;
- // break;
- // case 'Code':
- // screen = require('@screens/code').default;
- // break;
- // case 'CreateChannel':
- // screen = require('@screens/create_channel').default;
- // break;
- // case 'DisplaySettings':
- // screen = require('@screens/settings/display_settings').default;
- // break;
- // case 'EditChannel':
- // screen = require('@screens/edit_channel').default;
- // break;
- // case 'EditPost':
- // screen = require('@screens/edit_post').default;
- // break;
case Screens.EDIT_PROFILE:
screen = withServerDatabase((require('@screens/edit_profile').default));
break;
- // case 'ErrorTeamsList':
- // screen = require('@screens/error_teams_list').default;
- // break;
- // case 'ExpandedAnnouncementBanner':
- // screen = require('@screens/expanded_announcement_banner').default;
- // break;
- // case 'FlaggedPosts':
- // screen = require('@screens/flagged_posts').default;
- // break;
+ case Screens.EDIT_SERVER:
+ screen = withIntl(require('@screens/edit_server').default);
+ break;
case Screens.FORGOT_PASSWORD:
screen = withIntl(require('@screens/forgot_password').default);
break;
@@ -134,109 +92,21 @@ Navigation.setLazyComponentRegistrator((screenName) => {
}));
return;
}
- // case 'Gallery':
- // screen = require('@screens/gallery').default;
- // break;
- // case 'InteractiveDialog':
- // screen = require('@screens/interactive_dialog').default;
- // break;
case Screens.LOGIN:
screen = withIntl(require('@screens/login').default);
break;
- // case 'LongPost':
- // screen = require('@screens/long_post').default;
- // break;
- // case 'MainSidebar':
- // screen = require('app/components/sidebars/main').default;
- // break;
case Screens.MFA:
screen = withIntl(require('@screens/mfa').default);
break;
case Screens.BROWSE_CHANNELS:
screen = withServerDatabase(require('@screens/browse_channels').default);
break;
- // case 'MoreDirectMessages':
- // screen = require('@screens/more_dms').default;
- // break;
- // case 'Notification':
- // extraStyles = Platform.select({android: {flex: undefined, height: 100}});
- // screen = require('@screens/notification/index.tsx').default;
- // break;
- // case 'NotificationSettings':
- // screen = require('@screens/settings/notification_settings').default;
- // break;
- // case 'NotificationSettingsAutoResponder':
- // screen = require('@screens/settings/notification_settings_auto_responder').default;
- // break;
- // case 'NotificationSettingsEmail':
- // screen = require('@screens/settings/notification_settings_email').default;
- // break;
- // case 'NotificationSettingsMentions':
- // screen = require('@screens/settings/notification_settings_mentions').default;
- // break;
- // case 'NotificationSettingsMentionsKeywords':
- // screen = require('@screens/settings/notification_settings_mentions_keywords').default;
- // break;
- // case 'NotificationSettingsMobile':
- // screen = require('@screens/settings/notification_settings_mobile').default;
- // break;
- // case 'Permalink':
- // screen = require('@screens/permalink').default;
- // break;
- // case 'PinnedPosts':
- // screen = require('@screens/pinned_posts').default;
- // break;
case Screens.POST_OPTIONS:
screen = withServerDatabase(require('@screens/post_options').default);
break;
- // case 'ReactionList':
- // screen = require('@screens/reaction_list').default;
- // break;
- // case 'RecentMentions':
- // screen = require('@screens/recent_mentions').default;
- // break;
- // case 'Search':
- // screen = require('@screens/search').default;
- // break;
- // case 'SelectorScreen':
- // screen = require('@screens/selector_screen').default;
- // break;
- // case 'SelectTeam':
- // screen = require('@screens/select_team').default;
- // break;
- // case 'SelectTimezone':
- // screen = require('@screens/settings/timezone/select_timezone').default;
- // break;
- // case 'Settings':
- // screen = require('@screens/settings/general').default;
- // break;
- // case 'SettingsSidebar':
- // screen = require('app/components/sidebars/settings').default;
- // break;
- // case 'SidebarSettings':
- // screen = require('@screens/settings/sidebar').default;
- // break;
- case Screens.SSO:
- screen = withIntl(require('@screens/sso').default);
- break;
- // case 'Table':
- // screen = require('@screens/table').default;
- // break;
- // case 'TermsOfService':
- // screen = require('@screens/terms_of_service').default;
- // break;
- // case 'ThemeSettings':
- // screen = require('@screens/settings/theme').default;
- // break;
- // case 'Thread':
- // screen = require('@screens/thread').default;
- // break;
- // case 'TimezoneSettings':
- // screen = require('@screens/settings/timezone').default;
- // break;
- // case 'UserProfile':
- // screen = require('@screens/user_profile').default;
- // break;
+ case Screens.SSO:
+ screen = withIntl(require('@screens/sso').default);
+ break;
}
if (screen) {
diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts
index c979a2984..24bbeb8de 100644
--- a/app/screens/navigation.ts
+++ b/app/screens/navigation.ts
@@ -99,11 +99,11 @@ export const bottomSheetModalOptions = (theme: Theme, closeButtonId: string) =>
};
};
+// This locks phones to portrait for all screens while keeps
+// all orientations available for Tablets.
Navigation.setDefaultOptions({
layout: {
-
- //@ts-expect-error all not defined in type definition
- orientation: [Device.IS_TABLET ? 'all' : 'portrait'],
+ orientation: Device.IS_TABLET ? undefined : ['portrait'],
},
});
diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx
index 45d8135ba..c6af68160 100644
--- a/app/screens/server/index.tsx
+++ b/app/screens/server/index.tsx
@@ -70,8 +70,8 @@ const Server = ({
const {formatMessage} = intl;
useEffect(() => {
- let serverName = managedConfig?.serverName || LocalConfig.DefaultServerName;
- let serverUrl = managedConfig?.serverUrl || LocalConfig.DefaultServerUrl;
+ let serverName = defaultDisplayName || managedConfig?.serverName || LocalConfig.DefaultServerName;
+ let serverUrl = defaultServerUrl || managedConfig?.serverUrl || LocalConfig.DefaultServerUrl;
let autoconnect = managedConfig?.allowOtherServers === 'false' || LocalConfig.AutoSelectServerUrl;
if (launchType === LaunchType.DeepLink) {
diff --git a/app/utils/server/index.ts b/app/utils/server/index.ts
index 64a9e5f46..7417f1a96 100644
--- a/app/utils/server/index.ts
+++ b/app/utils/server/index.ts
@@ -11,6 +11,8 @@ import {LaunchType} from '@typings/launch';
import {changeOpacity} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
+import type ServersModel from '@typings/database/models/app/servers';
+
export function unsupportedServer(isSystemAdmin: boolean, intl: IntlShape) {
if (isSystemAdmin) {
return unsupportedServerAdminAlert(intl);
@@ -34,7 +36,6 @@ export function semverFromServerVersion(value: string) {
export async function addNewServer(theme: Theme, serverUrl?: string, displayName?: string) {
await dismissBottomSheet();
- const closeButton = CompassIcon.getImageSourceSync('close', 24, changeOpacity(theme.centerChannelColor, 0.56));
const closeButtonId = 'close-server';
const props = {
closeButtonId,
@@ -43,37 +44,23 @@ export async function addNewServer(theme: Theme, serverUrl?: string, displayName
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,
- },
- },
- };
+ const options = buildServerModalOptions(theme, closeButtonId);
showModal(Screens.SERVER, '', props, options);
}
+export async function editServer(theme: Theme, server: ServersModel) {
+ const closeButtonId = 'close-server-edit';
+ const props = {
+ closeButtonId,
+ server,
+ theme,
+ };
+ const options = buildServerModalOptions(theme, closeButtonId);
+
+ showModal(Screens.EDIT_SERVER, '', props, options);
+}
+
export async function alertServerLogout(displayName: string, onPress: () => void, intl: IntlShape) {
Alert.alert(
intl.formatMessage({
@@ -168,3 +155,34 @@ function unsupportedServerAlert(intl: IntlShape) {
Alert.alert(title, message, buttons, options);
}
+
+function buildServerModalOptions(theme: Theme, closeButtonId: string) {
+ const closeButton = CompassIcon.getImageSourceSync('close', 24, changeOpacity(theme.centerChannelColor, 0.56));
+ return {
+ 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: closeButtonId,
+ }],
+ leftButtonColor: undefined,
+ title: {color: theme.sidebarHeaderTextColor},
+ scrollEdgeAppearance: {
+ active: true,
+ noBorder: true,
+ translucid: true,
+ },
+ },
+ };
+}
diff --git a/app/utils/svg/index.ts b/app/utils/svg/index.ts
new file mode 100644
index 000000000..622d79b6e
--- /dev/null
+++ b/app/utils/svg/index.ts
@@ -0,0 +1,31 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+export const svgM = (x: number, y: number) => `M ${x} ${y}`;
+export const svgL = (x: number, y: number) => `L ${x} ${y}`;
+export const svgArc = (toX: number, toY: number, radius: number) => `A ${radius},${radius} 0 0 0 ${toX},${toY}`;
+export const z = 'z';
+
+export const constructRectangularPathWithBorderRadius = (
+ parentBounds: TutorialItemBounds,
+ itemBounds: TutorialItemBounds,
+ borderRadius = 0,
+): string => {
+ const {startX, startY, endX, endY} = itemBounds;
+ return [
+ svgM(parentBounds.startX, parentBounds.startY),
+ svgL(parentBounds.startX, parentBounds.endY),
+ svgL(parentBounds.endX, parentBounds.endY),
+ svgL(parentBounds.endX, parentBounds.startY),
+ z,
+ svgM(startX, startY + borderRadius),
+ svgL(startX, endY - borderRadius),
+ svgArc(startX + borderRadius, endY, borderRadius),
+ svgL(endX - borderRadius, endY),
+ svgArc(endX, endY - borderRadius, borderRadius),
+ svgL(endX, startY + borderRadius),
+ svgArc(endX - borderRadius, startY, borderRadius),
+ svgL(startX + borderRadius, startY),
+ svgArc(startX, startY + borderRadius, borderRadius),
+ ].join(' ');
+};
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 2f06323ae..828012ded 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -101,6 +101,11 @@
"custom_status.suggestions.working_from_home": "Working from home",
"date_separator.today": "Today",
"date_separator.yesterday": "Yesterday",
+ "edit_server.description": "Specify a display name for this server",
+ "edit_server.display_help": "Server: {url}",
+ "edit_server.save": "Save",
+ "edit_server.saving": "Saving",
+ "edit_server.title": "Edit server name",
"emoji_picker.activities": "Activities",
"emoji_picker.animals-nature": "Animals & Nature",
"emoji_picker.custom": "Custom",
@@ -120,8 +125,8 @@
"emoji_skin.medium_light_skin_tone": "medium light skin tone",
"emoji_skin.medium_skin_tone": "medium skin tone",
"file_upload.fileAbove": "Files must be less than {max}",
- "home.header.plus_menu": "Options",
"get_post_link_modal.title": "Copy Link",
+ "home.header.plus_menu": "Options",
"intro.add_people": "Add People",
"intro.channel_details": "Details",
"intro.created_by": "created by {creator} on {date}.",
@@ -397,6 +402,7 @@
"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.tutorial.swipe": "Swipe left on a server to see more actions",
"server.websocket.unreachable": "Server is unreachable.",
"servers.create_button": "Add a server",
"servers.default": "Default Server",
diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist
index c0d89541c..b166ce4fd 100644
--- a/ios/Mattermost/Info.plist
+++ b/ios/Mattermost/Info.plist
@@ -114,9 +114,11 @@
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
- UIInterfaceOrientationPortraitUpsideDown
UIViewControllerBasedStatusBarAppearance
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 224e06a39..940b0120d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -260,6 +260,8 @@ PODS:
- React-jsinspector (0.67.2)
- React-logger (0.67.2):
- glog
+ - react-native-background-timer (2.4.1):
+ - React-Core
- react-native-cameraroll (4.1.2):
- React-Core
- react-native-cookies (6.0.11):
@@ -498,6 +500,7 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
+ - react-native-background-timer (from `../node_modules/react-native-background-timer`)
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
- "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)"
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
@@ -627,6 +630,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
+ react-native-background-timer:
+ :path: "../node_modules/react-native-background-timer"
react-native-cameraroll:
:path: "../node_modules/@react-native-community/cameraroll"
react-native-cookies:
@@ -773,6 +778,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: 52beb652bbc61201bd70cbe4f0b8edb607e8da4f
React-jsinspector: 595f76eba2176ebd8817a1fffd47b84fbdab9383
React-logger: 23de8ea0f44fa00ee77e96060273225607fd4d78
+ react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866
react-native-cookies: cd92f3824ed1e32a20802e8185101e14bb5b76da
react-native-document-picker: 429972f7ece4463aa5bcdd789622b3a674a3c5d1
diff --git a/package-lock.json b/package-lock.json
index e07fa2df9..e9f3e3db8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,6 @@
"requires": true,
"packages": {
"": {
- "name": "mattermost-mobile",
"version": "2.0.0",
"hasInstallScript": true,
"license": "Apache 2.0",
@@ -52,6 +51,7 @@
"react-intl": "5.24.4",
"react-native": "0.67.2",
"react-native-android-open-settings": "1.3.0",
+ "react-native-background-timer": "2.4.1",
"react-native-button": "3.0.1",
"react-native-calendars": "1.1276.0",
"react-native-device-info": "8.4.8",
@@ -114,6 +114,7 @@
"@types/lodash": "4.14.178",
"@types/react": "17.0.38",
"@types/react-native": "0.66.15",
+ "@types/react-native-background-timer": "2.0.0",
"@types/react-native-button": "3.0.1",
"@types/react-native-share": "3.3.3",
"@types/react-native-video": "5.0.12",
@@ -5874,6 +5875,12 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-native-background-timer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz",
+ "integrity": "sha512-y5VW82dL/ESOLg+5QQHyBdsFVA4ZklENxmOyxv8o06T+3HBG2JOSuz/CIPz1vKdB7dmWDGPZNuPosdtnp+xv2A==",
+ "dev": true
+ },
"node_modules/@types/react-native-button": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/react-native-button/-/react-native-button-3.0.1.tgz",
@@ -19917,6 +19924,14 @@
"resolved": "https://registry.npmjs.org/react-native-android-open-settings/-/react-native-android-open-settings-1.3.0.tgz",
"integrity": "sha512-h4FTWRtTLRVNS7RK4Bg2gAXe8G5bFZL8Jtmfk2rG2a/N/wJR+v1rAY2BxRkC48lQTqwQCIAQRbWgLVkJ8XmaLQ=="
},
+ "node_modules/react-native-background-timer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz",
+ "integrity": "sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==",
+ "peerDependencies": {
+ "react-native": ">=0.47.0"
+ }
+ },
"node_modules/react-native-button": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-button/-/react-native-button-3.0.1.tgz",
@@ -28981,6 +28996,12 @@
"@types/react": "*"
}
},
+ "@types/react-native-background-timer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/react-native-background-timer/-/react-native-background-timer-2.0.0.tgz",
+ "integrity": "sha512-y5VW82dL/ESOLg+5QQHyBdsFVA4ZklENxmOyxv8o06T+3HBG2JOSuz/CIPz1vKdB7dmWDGPZNuPosdtnp+xv2A==",
+ "dev": true
+ },
"@types/react-native-button": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/react-native-button/-/react-native-button-3.0.1.tgz",
@@ -40101,6 +40122,12 @@
"resolved": "https://registry.npmjs.org/react-native-android-open-settings/-/react-native-android-open-settings-1.3.0.tgz",
"integrity": "sha512-h4FTWRtTLRVNS7RK4Bg2gAXe8G5bFZL8Jtmfk2rG2a/N/wJR+v1rAY2BxRkC48lQTqwQCIAQRbWgLVkJ8XmaLQ=="
},
+ "react-native-background-timer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/react-native-background-timer/-/react-native-background-timer-2.4.1.tgz",
+ "integrity": "sha512-TE4Kiy7jUyv+hugxDxitzu38sW1NqjCk4uE5IgU2WevLv7sZacaBc6PZKOShNRPGirLl1NWkaG3LDEkdb9Um5g==",
+ "requires": {}
+ },
"react-native-button": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-button/-/react-native-button-3.0.1.tgz",
diff --git a/package.json b/package.json
index 0d2276789..17d472911 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
"react-intl": "5.24.4",
"react-native": "0.67.2",
"react-native-android-open-settings": "1.3.0",
+ "react-native-background-timer": "2.4.1",
"react-native-button": "3.0.1",
"react-native-calendars": "1.1276.0",
"react-native-device-info": "8.4.8",
@@ -111,6 +112,7 @@
"@types/lodash": "4.14.178",
"@types/react": "17.0.38",
"@types/react-native": "0.66.15",
+ "@types/react-native-background-timer": "2.0.0",
"@types/react-native-button": "3.0.1",
"@types/react-native-share": "3.3.3",
"@types/react-native-video": "5.0.12",
diff --git a/types/components/tutorial_highlight.d.ts b/types/components/tutorial_highlight.d.ts
new file mode 100644
index 000000000..df302b0a3
--- /dev/null
+++ b/types/components/tutorial_highlight.d.ts
@@ -0,0 +1,9 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+type TutorialItemBounds = {
+ startX: number;
+ startY: number;
+ endX: number;
+ endY: number;
+}