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:
parent
0158b088da
commit
5b9492356b
34 changed files with 1012 additions and 240 deletions
|
|
@ -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
31
app/actions/app/global.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
57
app/components/tutorial_highlight/index.tsx
Normal file
57
app/components/tutorial_highlight/index.tsx
Normal 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;
|
||||
57
app/components/tutorial_highlight/item.tsx
Normal file
57
app/components/tutorial_highlight/item.tsx
Normal 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;
|
||||
62
app/components/tutorial_highlight/swipe_left.tsx
Normal file
62
app/components/tutorial_highlight/swipe_left.tsx
Normal 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;
|
||||
38
app/components/tutorial_highlight/swipe_left_hand.tsx
Normal file
38
app/components/tutorial_highlight/swipe_left_hand.tsx
Normal 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;
|
||||
|
||||
|
|
@ -66,6 +66,7 @@ export const SYSTEM_IDENTIFIERS = {
|
|||
|
||||
export const GLOBAL_IDENTIFIERS = {
|
||||
DEVICE_TOKEN: 'deviceToken',
|
||||
MULTI_SERVER_TUTORIAL: 'multiServerTutorial',
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
198
app/screens/edit_server/form.tsx
Normal file
198
app/screens/edit_server/form.tsx
Normal 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;
|
||||
55
app/screens/edit_server/header.tsx
Normal file
55
app/screens/edit_server/header.tsx
Normal 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;
|
||||
137
app/screens/edit_server/index.tsx
Normal file
137
app/screens/edit_server/index.tsx
Normal 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;
|
||||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
31
app/utils/svg/index.ts
Normal 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(' ');
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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
29
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
9
types/components/tutorial_highlight.d.ts
vendored
Normal file
9
types/components/tutorial_highlight.d.ts
vendored
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue