Gekidou MultiServers second part (#5963)

* Edit Server display name

* Lock iPhone to portrait and iPad to landscape

* Create actions for app global to store device token and multi server tutorial

* Add MutliServer tutorial on first use

* WebSocket reconnection priority

* have isRecordGlobalEqualToRaw to not check for value

* Return early on edit server if error is found

* Prepopulate server screen with last logged out server address and name

* Add CompassIcon to circleCI asset generation
This commit is contained in:
Elias Nahum 2022-02-17 10:42:06 -03:00 committed by GitHub
parent 0158b088da
commit 5b9492356b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1012 additions and 240 deletions

View file

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

31
app/actions/app/global.ts Normal file
View file

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

View file

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

View file

@ -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 (
<Modal
visible={visible}
transparent={true}
animationType='fade'
onShow={onShow}
onDismiss={onDismiss}
onRequestClose={onDismiss}
supportedOrientations={[supportedOrientations]}
>
<View
style={StyleSheet.absoluteFill}
pointerEvents='box-none'
/>
<HighlightItem
borderRadius={itemBorderRadius}
itemBounds={itemBounds}
height={height}
onDismiss={onDismiss}
width={width}
/>
{children}
</Modal>
);
};
export default TutorialHighlight;

View file

@ -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 (
<Svg
style={StyleSheet.absoluteFill}
onPress={onDismiss}
>
<G>
<Defs>
<ClipPath id='elementBounds'>
<Path
d={pathD}
clipRule='evenodd'
/>
</ClipPath>
</Defs>
<Rect
x={0}
y={0}
width={width}
height={height}
clipPath='#elementBounds'
fill={isDark ? 'white' : 'black'}
fillOpacity={0.3}
/>
</G>
</Svg>
);
};
export default HighlightItem;

View file

@ -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<ViewStyle>;
message: string;
style?: StyleProp<ViewStyle>;
textStyles?: StyleProp<TextStyle>;
}
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 (
<View
pointerEvents='none'
style={[styles.container, containerStyle]}
>
<View style={[styles.view, style]}>
<HandSwipeLeft fillColor={theme.centerChannelColor}/>
<Text style={[styles.text, textStyles]}>
{message}
</Text>
</View>
</View>
);
};
export default TutorialSwipeLeft;

View file

@ -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 (
<Svg
width={55}
height={48}
viewBox='0 0 55 48'
fill='none'
{...props}
>
<Path
opacity={0.5}
d='M11.92 9h35.883v6H11.921v9L0 12 11.92 0v9z'
fill={fillColor}
/>
<Path
d='M47.664 22.5a2.99 2.99 0 00-1.668.507A3.007 3.007 0 0043.164 21a2.99 2.99 0 00-1.668.507 3.007 3.007 0 00-2.832-2.007c-.546 0-1.06.147-1.5.404V15c0-1.655-1.346-3-3-3-1.655 0-3 1.345-3 3v17.536l-4.124-2.062A4.521 4.521 0 0025.03 30a2.87 2.87 0 00-2.865 2.874c0 .767.298 1.485.84 2.026l9.699 9.7a11.533 11.533 0 008.21 3.4c5.377 0 9.75-4.374 9.75-9.75V25.5c0-1.654-1.345-3-3-3zm1.5 15.75c0 4.55-3.703 8.25-8.252 8.25-2.701 0-5.24-1.052-7.147-2.962l-9.7-9.7c-.259-.256-.401-.6-.401-.973 0-.753.612-1.365 1.365-1.365.463 0 .925.11 1.34.317l5.21 2.603a.75.75 0 001.085-.67V15c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v14.25a.75.75 0 001.5 0V22.5c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v6.75a.75.75 0 001.5 0V24c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v5.25a.75.75 0 001.5 0V25.5c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v12.75z'
fill={fillColor}
/>
<Path
d='M49.164 38.25c0 4.55-3.703 8.25-8.252 8.25-2.701 0-5.24-1.052-7.147-2.962l-9.7-9.7c-.259-.256-.401-.6-.401-.973 0-.753.612-1.365 1.365-1.365.463 0 .925.11 1.34.317l5.21 2.603a.75.75 0 001.085-.67V15c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v14.25a.75.75 0 001.5 0V22.5c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v6.75a.75.75 0 001.5 0V24c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v5.25a.75.75 0 001.5 0V25.5c0-.826.672-1.5 1.5-1.5s1.5.674 1.5 1.5v12.75z'
fill={fillColor}
/>
</Svg>
);
}
export default SwipeLeftHand;

View file

@ -66,6 +66,7 @@ export const SYSTEM_IDENTIFIERS = {
export const GLOBAL_IDENTIFIERS = {
DEVICE_TOKEN: 'deviceToken',
MULTI_SERVER_TUTORIAL: 'multiServerTutorial',
};
export default {

View file

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

View file

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

View file

@ -208,6 +208,16 @@ class DatabaseManager {
return null;
};
public getActiveServerDisplayName = async (): Promise<string|null|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await queryActiveServer(database);
return server?.displayName;
}
return null;
};
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
const database = this.appDatabase?.database;
if (database) {

View file

@ -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<string|null|undefined>}
*/
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
@ -265,6 +265,20 @@ class DatabaseManager {
return null;
};
/**
* getActiveServerDisplayName: Get the server display name for active server database.
* @returns {Promise<string|null|undefined>}
*/
public getActiveServerDisplayName = async (): Promise<string|null|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await queryActiveServer(database);
return server?.displayName;
}
return null;
};
public getServerUrlFromIdentifier = async (identifier: string): Promise<string|undefined> => {
const database = this.appDatabase?.database;
if (database) {

View file

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

View file

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

View file

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

View file

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

View file

@ -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<string, WebSocketClient> = {};
private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {};
private previousAppState: AppStateStatus;
private connectionTimerIDs: Record<string, DebouncedFunc<() => void>> = {};
private isBackgroundTimerRunning = false;
private netConnected = false;
private previousAppState: AppStateStatus;
private statusUpdatesIntervalIDs: Record<string, NodeJS.Timer> = {};
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;

View file

@ -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<KeyboardAwareScrollView | undefined>;
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<FloatingTextInputRef>(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 = (
<Loading
containerStyle={styles.loadingContainerStyle}
style={styles.loading}
/>
);
}
return (
<View style={styles.formContainer}>
<View style={[styles.fullWidth, displayNameError?.length ? styles.error : undefined]}>
<FloatingTextInput
autoCorrect={false}
autoCapitalize={'none'}
enablesReturnKeyAutomatically={true}
error={displayNameError}
label={formatMessage({
id: 'mobile.components.select_server_view.displayName',
defaultMessage: 'Display Name',
})}
onBlur={onBlur}
onChangeText={handleDisplayNameTextChanged}
onFocus={onFocus}
onSubmitEditing={onUpdate}
ref={displayNameRef}
returnKeyType='done'
spellCheck={false}
testID='select_server.server_display_name.input'
theme={theme}
value={displayName}
/>
</View>
{!displayNameError &&
<FormattedText
defaultMessage={'Server: {url}'}
id={'edit_server.display_help'}
style={styles.chooseText}
testID={'edit_server.display_help'}
values={{url: removeProtocol(stripTrailingSlashes(serverUrl))}}
/>
}
<Button
containerStyle={[styles.connectButton, styleButtonBackground]}
disabled={buttonDisabled}
onPress={onUpdate}
testID='select_server.connect.button'
>
{buttonIcon}
<FormattedText
defaultMessage={buttonText}
id={buttonID}
style={styleButtonText}
/>
</Button>
</View>
);
};
export default EditServerForm;

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 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 (
<View style={styles.textContainer}>
<FormattedText
defaultMessage='Edit server name'
id='edit_server.title'
style={styles.title}
testID='edit_server.title'
/>
<FormattedText
defaultMessage='Specify a display name for this server'
id='edit_server.description'
style={styles.description}
testID='edit_server.description'
/>
</View>
);
};
export default EditServerHeader;

View file

@ -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<KeyboardAwareScrollView>();
const [saving, setSaving] = useState(false);
const [displayName, setDisplayName] = useState<string>(server.displayName);
const [buttonDisabled, setButtonDisabled] = useState(true);
const [displayNameError, setDisplayNameError] = useState<string | undefined>();
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 (
<View style={styles.flex}>
<Background theme={theme}/>
<SafeAreaView
key={'server_content'}
style={styles.flex}
testID='select_server.screen'
>
<KeyboardAwareScrollView
bounces={false}
contentContainerStyle={styles.scrollContainer}
enableAutomaticScroll={Platform.OS === 'android'}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraScrollHeight={20}
keyboardDismissMode='on-drag'
keyboardShouldPersistTaps='handled'
// @ts-expect-error legacy ref
ref={keyboardAwareRef}
scrollToOverflowEnabled={true}
style={styles.flex}
>
<Header theme={theme}/>
<Form
buttonDisabled={buttonDisabled}
connecting={saving}
displayName={displayName}
displayNameError={displayNameError}
handleUpdate={handleUpdate}
handleDisplayNameTextChanged={handleDisplayNameTextChanged}
keyboardAwareRef={keyboardAwareRef}
serverUrl={server.url}
theme={theme}
/>
</KeyboardAwareScrollView>
</SafeAreaView>
</View>
);
};
export default EditServer;

View file

@ -41,9 +41,10 @@ const ServerList = ({servers}: Props) => {
addNewServer(theme);
}, [servers]);
const renderServer = useCallback(({item: t}: ListRenderItemInfo<ServersModel>) => {
const renderServer = useCallback(({item: t, index}: ListRenderItemInfo<ServersModel>) => {
return (
<ServerItem
highlight={index === 0}
isActive={t.url === serverUrl}
server={t}
/>

View file

@ -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<GlobalModel>(GLOBAL).findAndObserve(MULTI_SERVER_TUTORIAL).pipe(
switchMap(({value}) => of$(Boolean(value))),
catchError(() => of$(false)),
);
}
return {
server: server.observe(),
tutorialWatched,
};
});
export default enhance(ServerItem);

View file

@ -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<BadgeValues>({isUnread: false, mentions: 0});
const styles = getStyleSheet(theme);
const swipeable = useRef<Swipeable>();
const subscription = useRef<Subscription|undefined>();
const viewRef = useRef<View>(null);
const [showTutorial, setShowTutorial] = useState(false);
const [itemBounds, setItemBounds] = useState<TutorialItemBounds>({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 (
<>
<Swipeable
@ -264,6 +303,7 @@ const ServerItem = ({isActive, server}: Props) => {
>
<View
style={containerStyle}
ref={viewRef}
>
<RectButton
onPress={onServerPressed}
@ -313,6 +353,18 @@ const ServerItem = ({isActive, server}: Props) => {
database={database}
/>
}
{showTutorial &&
<TutorialHighlight
itemBounds={itemBounds}
onDismiss={handleDismissTutorial}
onShow={handleShowTutorial}
>
<TutorialSwipeLeft
message={intl.formatMessage({id: 'server.tutorial.swipe', defaultMessage: 'Swipe left on a server to see more actions'})}
style={isTablet ? styles.tutorialTablet : styles.tutorial}
/>
</TutorialHighlight>
}
</>
);
};

View file

@ -57,72 +57,30 @@ Navigation.setLazyComponentRegistrator((screenName) => {
let screen: any|undefined;
let extraStyles: StyleProp<ViewStyle>;
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) {

View file

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

View file

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

View file

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

31
app/utils/svg/index.ts Normal file
View file

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

View file

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

View file

@ -114,9 +114,11 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>

View file

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

29
package-lock.json generated
View file

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

View file

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

View file

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