Draft - Add Server Screen Gekidou Updates (#5663)

* initial checkin

* all text is added

* add fonts

* wip

* get fonts from differnt site suggestion from matt

* use actual apostrophe

* add react-native-paper dependency

* wip

* add project info for metropolis fonts

* correct the display name help text alignment with stretch
use paper theme to color and make the the placeholder opaque
remove placeholder prop because spec wants the box to be empty on focus

* fix Metropolis font reference

* Add more styling.  Clean up code later

* input box text theme color

* separate padding for two textinput boxes
share the input theme

* Use ActivityIndicator from react-native-paper

* wrap formatText components in a view so they scroll up inline with the
text inputs and button

* clean up styles section. no empty lines and no comments

* Use Colors library instead of harcoding #FFFFFF

* Move error message into helper directly under the server url input box

* need to handle the error so it can be presented in the modal

* set state for button is disabled or not
reset errors before every connect attempt

* when url is not valid show the error message and disable the connect
button until some text in display or the url input changes

* s/generic/default/

* group formatted messages together

* Add icon to the error message
fix clipped helper messages

* when urlError is not '', make the input border red
after connecting creates an error, typing in the input boxes will
   * enable the connect button
   * reset the error messages
   * change outline from red to blue

* After connecting and the server url has an error, allow the user to
click the display input and keep the url border red.  The only time the
red border and error reset is when the user types in that field again

* remove duplicate button component. only get the prop values and
construct after all values defined

* use correct icon and add spacing between icon and text

* fix styling for Android
New styling from figma

* initial add of background svg.  RHS is off a bit

* sync with gekidou changes

* sync with gekidou

* sync with gekidou

* sync with gekidou

* sync with gekidou

* use new FloatingTextInput component

* Added widgets/text_settings

* wip

* wip

* Update index.tsx

* work in progress

* fix text settings component

* fix text settings component

* crash fix

* code clean up

* Update index.tsx

* Fix testSettings

* Wrap with View so the text moves with the text inputs when keyboard
appears

* update styling
when user starts typing in server url after a failed connect, dismiss
the error

* use utils/buttonStyles

* leave as it was to minimize irrelevant PR diffs

* nit

* Align all components
Add vertical margin to styles
Add subcontainer and container props to text_setting component
Use FormattedText for choosing a display name text instead of
text_setting helper text

* revert midnight change

* reset the connecing error when a user starts modifying the server url

* formatting
remove svg
repalce <Text> with <FormattedText>

* fix lint

* remove svg file

* Listening for appearance to set the theme for non connected screens

* Pass the theme as a prop for TextSettings

* Fix Server screen layout and pass the displayName for DB creation

* Tablet layout

* Persist keyboard on tap

* Change position & opacity for app version

* added background SVG

* Fix non theme control screens status bar color

* Split Server screen into smaller components

* fixed svg background layout for tablet

* Bring back form & header and remove some extra background color and styles

* Remove duplicate font reference in Info.plist

* Final UI tweaks

* Fix error display on url field

* margin bottom to app version

* Add ClientError utility to extract error

* update snapshot

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
Co-authored-by: Avinash Lingaloo <avinashlng1080@gmail.com>
Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
This commit is contained in:
Jason Frerich
2021-10-29 17:14:55 -05:00
committed by GitHub
parent c8d7d4c528
commit a5254992d3
23 changed files with 1642 additions and 290 deletions

View File

@@ -3,6 +3,7 @@
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import NetworkManager from '@init/network_manager';
import {queryExpandedLinks} from '@queries/servers/system';
@@ -12,16 +13,21 @@ import type {Client} from '@client/rest';
import type {ClientResponse} from '@mattermost/react-native-network-client';
export const doPing = async (serverUrl: string) => {
const client = await NetworkManager.createClient(serverUrl);
let client: Client;
try {
client = await NetworkManager.createClient(serverUrl);
} catch (error) {
return {error};
}
const certificateError = {
id: 'mobile.server_requires_client_certificate',
defaultMessage: 'Server required client certificate for authentication.',
id: t('mobile.server_requires_client_certificate'),
defaultMessage: 'Server requires client certificate for authentication.',
};
const pingError = {
id: 'mobile.server_ping_failed',
defaultMessage: 'Cannot connect to the server. Please check your server URL and internet connection.',
id: t('mobile.server_ping_failed'),
defaultMessage: 'Cannot connect to the server.',
};
let response: ClientResponse;
@@ -41,7 +47,7 @@ export const doPing = async (serverUrl: string) => {
}
} catch (error) {
NetworkManager.invalidateClient(serverUrl);
return {error};
return {error: {intl: pingError}};
}
return {error: undefined};

View File

@@ -86,7 +86,7 @@ export const getSessions = async (serverUrl: string, currentUserId: string) => {
return undefined;
};
export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password, config}: LoginArgs): Promise<LoginActionResponse> => {
export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password, config, serverDisplayName}: LoginArgs): Promise<LoginActionResponse> => {
let deviceToken;
let user: UserProfile;
@@ -117,6 +117,7 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
dbName: serverUrl,
serverUrl,
identifier: config.DiagnosticId,
displayName: serverDisplayName,
},
});
await DatabaseManager.setActiveServerDatabase(serverUrl);
@@ -180,7 +181,7 @@ export const sendPasswordResetEmail = async (serverUrl: string, email: string) =
};
};
export const ssoLogin = async (serverUrl: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
let deviceToken;
let user;
@@ -206,6 +207,7 @@ export const ssoLogin = async (serverUrl: string, serverIdentifier: string, bear
dbName: serverUrl,
serverUrl,
identifier: serverIdentifier,
displayName: serverDisplayName,
},
});
await DatabaseManager.setActiveServerDatabase(serverUrl);

View File

@@ -7,8 +7,10 @@ exports[`@components/app_version should match snapshot 1`] = `
<View
style={
Object {
"alignItems": "center",
"justifyContent": "flex-end",
"bottom": 0,
"marginBottom": 12,
"marginLeft": 20,
"position": "absolute",
}
}
>

View File

@@ -10,8 +10,10 @@ import {t} from '@i18n';
const style = StyleSheet.create({
info: {
alignItems: 'center',
justifyContent: 'flex-end',
position: 'absolute',
bottom: 0,
marginLeft: 20,
marginBottom: 12,
},
version: {
fontSize: 12,

View File

@@ -4,7 +4,7 @@
// Note: This file has been adapted from the library https://github.com/csath/react-native-reanimated-text-input
import {debounce} from 'lodash';
import React, {useState, useEffect, useRef} from 'react';
import React, {useState, useEffect, useRef, useImperativeHandle, forwardRef} from 'react';
import {View, TextInput, TouchableWithoutFeedback, Text, Platform, TextStyle, NativeSyntheticEvent, TextInputFocusEventData, TextInputProps, GestureResponderEvent, TargetedEvent} from 'react-native';
import Animated, {useCode, interpolateNode, EasingNode, Value, set, Clock} from 'react-native-reanimated';
@@ -13,12 +13,13 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {timingAnimation} from './animation_utils';
const DEFAULT_INPUT_HEIGHT = 48;
const BORDER_DEFAULT_WIDTH = 1;
const BORDER_FOCUSED_WIDTH = 2;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
container: {
height: 48,
height: DEFAULT_INPUT_HEIGHT + (2 * BORDER_DEFAULT_WIDTH),
width: '100%',
},
errorContainer: {
@@ -43,7 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
left: 16,
fontFamily: 'OpenSans',
fontSize: 16,
zIndex: 1,
zIndex: 10,
},
smallLabel: {
fontSize: 10,
@@ -51,7 +52,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
textInput: {
fontFamily: 'OpenSans',
fontSize: 16,
lineHeight: 24,
paddingTop: 12,
paddingBottom: 12,
paddingHorizontal: 16,
@@ -59,6 +59,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
borderColor: changeOpacity(theme.centerChannelColor, 0.16),
borderRadius: 4,
borderWidth: BORDER_DEFAULT_WIDTH,
backgroundColor: theme.centerChannelBg,
},
}));
@@ -85,6 +86,11 @@ const getLabelPositions = (style: TextStyle, labelStyle: TextStyle, smallLabelSt
return [unfocused, focused];
};
export type FloatingTextInputRef = {
focus: () => void;
isFocused: () => boolean;
}
type FloatingTextInputProps = TextInputProps & {
containerStyle?: TextStyle;
editable?: boolean;
@@ -100,12 +106,12 @@ type FloatingTextInputProps = TextInputProps & {
value: string;
}
const FloatingTextInput = ({
const FloatingTextInput = forwardRef<FloatingTextInputRef, FloatingTextInputProps>(({
error,
containerStyle,
isKeyboardInput = true,
editable = true,
errorIcon = 'BRKN-alert-outline',
errorIcon = 'alert-outline',
label = '',
onPress = undefined,
onFocus,
@@ -114,9 +120,9 @@ const FloatingTextInput = ({
theme,
value = '',
...props
}: FloatingTextInputProps) => {
}: FloatingTextInputProps, ref) => {
const [focusedLabel, setIsFocusLabel] = useState<boolean | undefined>();
const [focused, setIsFocused] = useState(Boolean(value));
const [focused, setIsFocused] = useState(Boolean(value) && editable);
const inputRef = useRef<TextInput>(null);
const [animation] = useState(new Value(focusedLabel ? 1 : 0));
const debouncedOnFocusTextInput = debounce(setIsFocusLabel, 500, {leading: true, trailing: false});
@@ -128,6 +134,11 @@ const FloatingTextInput = ({
to = focusedLabel ? 1 : 0;
}
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
isFocused: () => inputRef.current?.isFocused() || false,
}), [inputRef]);
useCode(
() => set(
animation,
@@ -164,6 +175,7 @@ const FloatingTextInput = ({
backgroundColor: (
focusedLabel ? theme.centerChannelBg : 'transparent'
),
paddingHorizontal: focusedLabel ? 4 : 0,
color: styles.label.color,
};
@@ -200,7 +212,10 @@ const FloatingTextInput = ({
textInputColorStyles = {borderColor: theme.errorTextColor};
}
const textInputBorder = {borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH};
const textInputBorder = {
borderWidth: focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH,
height: DEFAULT_INPUT_HEIGHT + ((focusedLabel ? BORDER_FOCUSED_WIDTH : BORDER_DEFAULT_WIDTH) * 2),
};
const combinedTextInputStyle = [styles.textInput, textInputBorder, textInputColorStyles];
const textAnimatedTextStyle = [styles.label, focusStyle, labelColorStyles];
@@ -235,7 +250,7 @@ const FloatingTextInput = ({
ref={inputRef}
underlineColorAndroid='transparent'
/>
{!focused && error && (
{error && (
<View style={styles.errorContainer}>
{showErrorIcon && errorIcon &&
<CompassIcon
@@ -249,7 +264,7 @@ const FloatingTextInput = ({
</View>
</TouchableWithoutFeedback>
);
};
});
export default FloatingTextInput;

View File

@@ -35,13 +35,14 @@ import type {LaunchProps} from '@typings/launch';
interface LoginProps extends LaunchProps {
componentId: string;
config: ClientConfig;
serverDisplayName: string;
license: ClientLicense;
theme: Theme;
}
export const MFA_EXPECTED_ERRORS = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error'];
const Login: NavigationFunctionComponent = ({config, extra, launchError, launchType, license, serverUrl, theme}: LoginProps) => {
const Login: NavigationFunctionComponent = ({config, serverDisplayName, extra, launchError, launchType, license, serverUrl, theme}: LoginProps) => {
const styles = getStyleSheet(theme);
const loginRef = useRef<TextInput>(null);
@@ -128,7 +129,7 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
});
const signIn = async () => {
const result: LoginActionResponse = await login(serverUrl!, {loginId: loginId.toLowerCase(), password, config, license});
const result: LoginActionResponse = await login(serverUrl!, {serverDisplayName, loginId: loginId.toLowerCase(), password, config, license});
if (checkLoginResponse(result)) {
if (!result.hasTeams && !result.error) {
// eslint-disable-next-line no-console
@@ -171,7 +172,7 @@ const Login: NavigationFunctionComponent = ({config, extra, launchError, launchT
const goToMfa = () => {
const screen = MFA;
const title = intl.formatMessage({id: 'mobile.routes.mfa', defaultMessage: 'Multi-factor Authentication'});
goToScreen(screen, title, {goToHome, loginId, password, config, license, serverUrl, theme});
goToScreen(screen, title, {goToHome, loginId, password, config, serverDisplayName, license, serverUrl, theme});
};
const getLoginErrorMessage = (loginError: string | ClientErrorProps | Error) => {

View File

@@ -26,6 +26,7 @@ import type {LaunchProps} from '@typings/launch';
interface LoginOptionsProps extends LaunchProps {
componentId: string;
serverUrl: string;
serverDisplayName: string;
config: ClientConfig;
license: ClientLicense;
theme: Theme;
@@ -60,7 +61,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const LoginOptions: NavigationFunctionComponent = ({config, extra, launchType, launchError, license, serverUrl, theme}: LoginOptionsProps) => {
const LoginOptions: NavigationFunctionComponent = ({config, extra, launchType, launchError, license, serverDisplayName, serverUrl, theme}: LoginOptionsProps) => {
const intl = useIntl();
const styles = getStyles(theme);
@@ -68,13 +69,13 @@ const LoginOptions: NavigationFunctionComponent = ({config, extra, launchType, l
const screen = LOGIN;
const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'});
goToScreen(screen, title, {config, extra, launchError, launchType, license, serverUrl, theme});
goToScreen(screen, title, {config, extra, launchError, launchType, license, serverDisplayName, serverUrl, theme});
});
const displaySSO = preventDoubleTap((ssoType: string) => {
const screen = SSO;
const title = intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'});
goToScreen(screen, title, {config, extra, launchError, launchType, license, theme, ssoType, serverUrl});
goToScreen(screen, title, {config, extra, launchError, launchType, license, theme, ssoType, serverDisplayName, serverUrl});
});
return (

View File

@@ -31,11 +31,12 @@ type MFAProps = {
license: Partial<ClientLicense>;
loginId: string;
password: string;
serverDisplayName: string;
serverUrl: string;
theme: Theme;
}
const MFA = ({config, goToHome, license, loginId, password, serverUrl, theme}: MFAProps) => {
const MFA = ({config, goToHome, license, loginId, password, serverDisplayName, serverUrl, theme}: MFAProps) => {
const intl = useIntl();
const [token, setToken] = useState<string>('');
const [error, setError] = useState<string>('');
@@ -78,7 +79,7 @@ const MFA = ({config, goToHome, license, loginId, password, serverUrl, theme}: M
return;
}
setIsLoading(true);
const result: LoginActionResponse = await login(serverUrl, {loginId, password, mfaToken: token, config, license});
const result: LoginActionResponse = await login(serverUrl, {loginId, password, mfaToken: token, config, license, serverDisplayName});
setIsLoading(false);
if (result?.error && result.failed) {
if (typeof result.error == 'string') {

View File

@@ -23,6 +23,7 @@ describe('*** MFA Screen ***', () => {
password: 'passwd',
license: {},
serverUrl: 'https://locahost:8065',
serverDisplayName: 'Test Server',
theme: Preferences.THEMES.denim,
};

View File

@@ -12,12 +12,13 @@ import CompassIcon from '@components/compass_icon';
import {Device, Preferences, Screens} from '@constants';
import NavigationConstants from '@constants/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {changeOpacity} from '@utils/theme';
import {changeOpacity, setNavigatorStyles} from '@utils/theme';
import type {LaunchProps} from '@typings/launch';
const {MattermostManaged} = NativeModules;
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
const appearanceControlledScreens = [Screens.SERVER, Screens.LOGIN, Screens.LOGIN_OPTIONS, Screens.FORGOT_PASSWORD, Screens.MFA];
Navigation.setDefaultOptions({
layout: {
@@ -27,6 +28,19 @@ Navigation.setDefaultOptions({
},
});
Appearance.addChangeListener(() => {
const theme = getThemeFromState();
const screens = EphemeralStore.getAllNavigationComponents();
if (screens.includes(Screens.SERVER)) {
for (const screen of screens) {
if (appearanceControlledScreens.includes(screen)) {
Navigation.updateProps(screen, {theme});
setNavigatorStyles(screen, theme);
}
}
}
});
function getThemeFromState() {
if (EphemeralStore.theme) {
return EphemeralStore.theme;

File diff suppressed because one or more lines are too long

217
app/screens/server/form.tsx Normal file
View File

@@ -0,0 +1,217 @@
// 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 {ActivityIndicator, Platform, useWindowDimensions, View} from 'react-native';
import Button from 'react-native-button';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import FormattedText from '@app/components/formatted_text';
import FloatingTextInput, {FloatingTextInputRef} from '@components/floating_text_input_label';
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';
type Props = {
buttonDisabled: boolean;
connecting: boolean;
displayName?: string;
displayNameError?: string;
handleConnect: () => void;
handleDisplayNameTextChanged: (text: string) => void;
handleUrlTextChanged: (text: string) => void;
keyboardAwareRef: MutableRefObject<KeyboardAwareScrollView | undefined>;
theme: Theme;
url?: string;
urlError?: string;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
formContainer: {
alignItems: 'center',
maxWidth: 600,
width: '100%',
paddingHorizontal: 20,
},
enterServer: {
marginBottom: 24,
},
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,
},
connectingIndicator: {
marginRight: 10,
},
}));
const ServerForm = ({
buttonDisabled,
connecting,
displayName = '',
displayNameError,
handleConnect,
handleDisplayNameTextChanged,
handleUrlTextChanged,
keyboardAwareRef,
theme,
url = '',
urlError,
}: Props) => {
const {formatMessage} = useIntl();
const isTablet = useIsTablet();
const dimensions = useWindowDimensions();
const displayNameRef = useRef<FloatingTextInputRef>(null);
const urlRef = useRef<FloatingTextInputRef>(null);
const styles = getStyleSheet(theme);
const focus = () => {
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);
});
}
};
const onBlur = useCallback(() => {
if (Platform.OS === 'ios' && isTablet && !urlRef.current?.isFocused() && !displayNameRef.current?.isFocused()) {
keyboardAwareRef.current?.scrollToPosition(0, 0);
}
}, []);
const onFocus = useCallback(() => {
focus();
}, [dimensions]);
const onUrlSubmit = useCallback(() => {
displayNameRef.current?.focus();
}, []);
useEffect(() => {
if (Platform.OS === 'ios' && isTablet) {
if (urlRef.current?.isFocused() || displayNameRef.current?.isFocused()) {
focus();
} else {
keyboardAwareRef.current?.scrollToPosition(0, 0);
}
}
}, [dimensions, isTablet]);
let styleButtonText = buttonTextStyle(theme, 'lg', 'primary', 'default');
let styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary');
let buttonID = t('mobile.components.select_server_view.connect');
let buttonText = 'Connect';
let buttonIcon;
if (connecting) {
buttonID = t('mobile.components.select_server_view.connecting');
buttonText = 'Connecting';
buttonIcon = (
<ActivityIndicator
animating={true}
size='small'
color={'white'}
style={styles.connectingIndicator}
/>
);
} else if (buttonDisabled) {
styleButtonText = buttonTextStyle(theme, 'lg', 'primary', 'disabled');
styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', 'disabled');
}
return (
<View style={styles.formContainer}>
<View style={[styles.fullWidth, urlError?.length ? styles.error : undefined]}>
<FloatingTextInput
autoCorrect={false}
blurOnSubmit={false}
containerStyle={styles.enterServer}
enablesReturnKeyAutomatically={true}
error={urlError}
keyboardType='url'
label={formatMessage({
id: 'mobile.components.select_server_view.enterServerUrl',
defaultMessage: 'Enter Server URL',
})}
onBlur={onBlur}
onChangeText={handleUrlTextChanged}
onFocus={onFocus}
onSubmitEditing={onUrlSubmit}
ref={urlRef}
returnKeyType='next'
testID='select_server.server_url.input'
theme={theme}
value={url}
/>
</View>
<View style={[styles.fullWidth, displayNameError?.length ? styles.error : undefined]}>
<FloatingTextInput
autoCorrect={false}
enablesReturnKeyAutomatically={true}
error={displayNameError}
keyboardType='url'
label={formatMessage({
id: 'mobile.components.select_server_view.displayName',
defaultMessage: 'Display Name',
})}
onBlur={onBlur}
onChangeText={handleDisplayNameTextChanged}
onFocus={onFocus}
onSubmitEditing={handleConnect}
ref={displayNameRef}
returnKeyType='done'
testID='select_server.server_display_name.input'
theme={theme}
value={displayName}
/>
</View>
<FormattedText
defaultMessage={'Choose a display name for your server'}
id={'mobile.components.select_server_view.displayHelp'}
style={styles.chooseText}
testID={'mobile.components.select_server_view.displayHelp'}
/>
<Button
containerStyle={[styles.connectButton, styleButtonBackground]}
disabled={buttonDisabled}
onPress={handleConnect}
testID='select_server.connect.button'
>
{buttonIcon}
<FormattedText
defaultMessage={buttonText}
id={buttonID}
style={styleButtonText}
/>
</Button>
</View>
);
};
export default ServerForm;

View File

@@ -0,0 +1,72 @@
// 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 '@app/components/formatted_text';
import {useIsTablet} from '@hooks/device';
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,
},
welcome: {
marginTop: 12,
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Heading', 400, 'SemiBold'),
},
connect: {
width: 270,
letterSpacing: -1,
color: theme.mentionColor,
marginVertical: 12,
...typography('Heading', 1000, 'SemiBold'),
},
connectTablet: {
width: undefined,
},
description: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 100, 'Regular'),
},
}));
const ServerHeader = ({theme}: Props) => {
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
return (
<View style={styles.textContainer}>
<FormattedText
defaultMessage={'Welcome'}
id={'mobile.components.select_server_view.msg_welcome'}
testID={'mobile.components.select_server_view.msg_welcome'}
style={styles.welcome}
/>
<FormattedText
defaultMessage={'Lets Connect to a Server'}
id={'mobile.components.select_server_view.msg_connect'}
style={[styles.connect, isTablet ? styles.connectTablet : undefined]}
testID={'mobile.components.select_server_view.msg_connect'}
/>
<FormattedText
defaultMessage={"A Server is your team's communication hub which is accessed through a unique URL"}
id={'mobile.components.select_server_view.msg_description'}
style={styles.description}
testID={'mobile.components.select_server_view.msg_description'}
/>
</View>
);
};
export default ServerHeader;

View File

@@ -4,30 +4,30 @@
import {useManagedConfig, ManagedConfig} from '@mattermost/react-native-emm';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {
ActivityIndicator, EventSubscription, Image, Keyboard, KeyboardAvoidingView,
Platform, StyleSheet, TextInput, TouchableWithoutFeedback, View,
} from 'react-native';
import Button from 'react-native-button';
import {Navigation, NavigationFunctionComponent} from 'react-native-navigation';
import {Alert, 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 {doPing} from '@actions/remote/general';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import LocalConfig from '@assets/config.json';
import ClientError from '@client/rest/error';
import AppVersion from '@components/app_version';
import ErrorText from '@components/error_text';
import FormattedText from '@components/formatted_text';
import {Screens} from '@constants';
import {t} from '@i18n';
import NetworkManager from '@init/network_manager';
import {goToScreen} from '@screens/navigation';
import {DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
import {getErrorMessage} from '@utils/client_error';
import {isMinimumServerVersion} from '@utils/helpers';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {getServerUrlAfterRedirect, isValidUrl, sanitizeUrl} from '@utils/url';
import type ClientError from '@client/rest/error';
import Background from './background';
import ServerForm from './form';
import ServerHeader from './header';
interface ServerProps extends LaunchProps {
componentId: string;
@@ -36,24 +36,75 @@ interface ServerProps extends LaunchProps {
let cancelPing: undefined | (() => void);
const Server: NavigationFunctionComponent = ({componentId, extra, launchType, launchError, theme}: ServerProps) => {
// TODO: If we have LaunchProps, ensure they get passed along to subsequent screens
// so that they are eventually accessible in the Channel screen.
const defaultServerUrlMessage = {
id: t('mobile.server_url.empty'),
defaultMessage: 'Please enter a valid server URL',
};
const Server = ({componentId, extra, launchType, launchError, theme}: ServerProps) => {
const intl = useIntl();
const managedConfig = useManagedConfig<ManagedConfig>();
const input = useRef<TextInput>(null);
const keyboardAwareRef = useRef<KeyboardAwareScrollView>();
const [connecting, setConnecting] = useState(false);
const initialError = launchError && launchType === LaunchType.Notification ? intl.formatMessage({
id: 'mobile.launchError.notification',
defaultMessage: 'Did not find a server for this notification',
}) : undefined;
const [error, setError] = useState<Partial<ClientErrorProps>|string|undefined>(initialError);
const [displayName, setDisplayName] = useState<string>('');
const [buttonDisabled, setButtonDisabled] = useState(true);
const [url, setUrl] = useState<string>('');
const [urlError, setUrlError] = useState<string | undefined>(undefined);
const styles = getStyleSheet(theme);
const {formatMessage} = intl;
useEffect(() => {
let serverUrl = managedConfig?.serverUrl || LocalConfig.DefaultServerUrl;
let autoconnect = managedConfig?.allowOtherServers === 'false' || LocalConfig.AutoSelectServerUrl;
if (launchType === LaunchType.DeepLink) {
const deepLinkServerUrl = (extra as DeepLinkWithData).data?.serverUrl;
if (managedConfig) {
autoconnect = (managedConfig.allowOtherServers === 'false' && managedConfig.serverUrl === deepLinkServerUrl);
if (managedConfig.serverUrl !== deepLinkServerUrl || launchError) {
Alert.alert('', intl.formatMessage({
id: 'mobile.server_url.deeplink.emm.denied',
defaultMessage: 'This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server',
}));
}
} else {
autoconnect = true;
serverUrl = deepLinkServerUrl;
}
}
if (serverUrl) {
// If a server Url is set by the managed or local configuration, use it.
setUrl(serverUrl);
if (autoconnect) {
// If no other servers are allowed or the local config for AutoSelectServerUrl is set, attempt to connect
handleConnect(managedConfig?.serverUrl || LocalConfig.DefaultServerUrl);
}
}
}, []);
useEffect(() => {
if (url && displayName) {
setButtonDisabled(false);
} else {
setButtonDisabled(true);
}
}, [url, displayName]);
useEffect(() => {
const listener = {
componentDidAppear: () => {
if (url) {
NetworkManager.invalidateClient(url);
}
},
};
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId);
return () => unsubscribe.remove();
}, [componentId, url]);
const displayLogin = (serverUrl: string, config: ClientConfig, license: ClientLicense) => {
const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true';
const gitlabEnabled = config.EnableSignUpWithGitLab === 'true';
@@ -77,6 +128,7 @@ const Server: NavigationFunctionComponent = ({componentId, extra, launchType, la
const passProps = {
config,
displayName,
extra,
launchError,
launchType,
@@ -98,40 +150,57 @@ const Server: NavigationFunctionComponent = ({componentId, extra, launchType, la
setUrl(serverUrl);
};
const handleConnect = preventDoubleTap((manualUrl?: string) => {
const handleConnect = preventDoubleTap(async (manualUrl?: string) => {
if (buttonDisabled) {
return;
}
if (connecting && cancelPing) {
cancelPing();
return;
}
let serverUrl = typeof manualUrl === 'string' ? manualUrl : url;
const serverUrl = typeof manualUrl === 'string' ? manualUrl : url;
if (!serverUrl || serverUrl.trim() === '') {
setError(intl.formatMessage({
id: 'mobile.server_url.empty',
defaultMessage: 'Please enter a valid server URL',
}));
setUrlError(formatMessage(defaultServerUrlMessage));
return;
}
serverUrl = sanitizeUrl(serverUrl);
if (!isValidUrl(serverUrl)) {
setError(formatMessage({
id: 'mobile.server_url.invalid_format',
defaultMessage: 'URL must start with http:// or https://',
}));
if (!isServerUrlValid(serverUrl)) {
return;
}
// TODO: Query to see if there is a server with the same display name
// Set the error here and return
// Elias to add this change in a Later PR. as soon as this one goes in
pingServer(serverUrl);
});
const handleDisplayNameTextChanged = useCallback((text: string) => {
setDisplayName(text);
}, []);
const handleUrlTextChanged = useCallback((text: string) => {
setUrlError(undefined);
setUrl(text);
}, []);
const isServerUrlValid = (serverUrl?: string) => {
const testUrl = sanitizeUrl(serverUrl ?? url);
if (!isValidUrl(testUrl)) {
setUrlError(intl.formatMessage({
id: 'mobile.server_url.invalid_format',
defaultMessage: 'URL must start with http:// or https://',
}));
return false;
}
return true;
};
const pingServer = async (pingUrl: string, retryWithHttp = true) => {
let canceled = false;
setConnecting(true);
setError(undefined);
cancelPing = () => {
// We should not need this once we have the cancelable network-client library
canceled = true;
@@ -151,16 +220,17 @@ const Server: NavigationFunctionComponent = ({componentId, extra, launchType, la
const nurl = serverUrl.replace('https:', 'http:');
pingServer(nurl, false);
} else {
setError(result.error as ClientError);
setUrlError(getErrorMessage(result.error as ClientError, intl));
setConnecting(false);
}
setButtonDisabled(true);
return;
}
const data = await fetchConfigAndLicense(serverUrl, true);
if (data.error) {
setError(data.error as ClientError);
setButtonDisabled(true);
setUrlError(getErrorMessage(data.error as ClientError, intl));
setConnecting(false);
return;
}
@@ -168,236 +238,60 @@ const Server: NavigationFunctionComponent = ({componentId, extra, launchType, la
displayLogin(serverUrl, data.config!, data.license!);
};
const blur = useCallback(() => {
input.current?.blur();
}, []);
const handleTextChanged = useCallback((text: string) => {
setUrl(text);
}, []);
useEffect(() => {
let listener: EventSubscription;
if (Platform.OS === 'android') {
listener = Keyboard.addListener('keyboardDidHide', blur);
}
return () => listener?.remove();
}, []);
useEffect(() => {
let serverUrl = managedConfig?.serverUrl || LocalConfig.DefaultServerUrl;
let autoconnect = managedConfig?.allowOtherServers === 'false' || LocalConfig.AutoSelectServerUrl;
if (launchType === LaunchType.DeepLink) {
const deepLinkServerUrl = (extra as DeepLinkWithData).data?.serverUrl;
if (managedConfig) {
autoconnect = (managedConfig.allowOtherServers === 'false' && managedConfig.serverUrl === deepLinkServerUrl);
if (managedConfig.serverUrl !== deepLinkServerUrl || launchError) {
setError(intl.formatMessage({
id: 'mobile.server_url.deeplink.emm.denied',
defaultMessage: 'This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server',
}));
}
} else {
autoconnect = true;
serverUrl = deepLinkServerUrl;
}
}
if (serverUrl) {
// If a server Url is set by the managed or local configuration, use it.
setUrl(serverUrl);
if (autoconnect) {
// If no other servers are allowed or the local config for AutoSelectServerUrl is set, attempt to connect
handleConnect(managedConfig?.serverUrl || LocalConfig.DefaultServerUrl);
}
}
}, []);
useEffect(() => {
const listener = {
componentDidAppear: () => {
if (url) {
NetworkManager.invalidateClient(url);
}
},
};
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId);
return () => unsubscribe.remove();
}, [componentId, url]);
let buttonIcon;
let buttonText;
if (connecting) {
buttonIcon = (
<ActivityIndicator
animating={true}
size='small'
color={theme.buttonBg}
style={styles.connectingIndicator}
/>
);
buttonText = (
<FormattedText
id='mobile.components.select_server_view.connecting'
defaultMessage='Connecting...'
style={styles.connectText}
/>
);
} else {
buttonText = (
<FormattedText
id='mobile.components.select_server_view.connect'
defaultMessage='Connect'
style={styles.connectText}
/>
);
}
const inputDisabled = managedConfig.allowOtherServers === 'false' || connecting;
const inputStyle = [styles.inputBox];
if (inputDisabled) {
inputStyle.push(styles.disabledInput);
}
return (
<SafeAreaView
testID='select_server.screen'
style={styles.container}
>
<KeyboardAvoidingView
behavior='padding'
<View style={styles.flex}>
<Background theme={theme}/>
<SafeAreaView
key={'server_content'}
style={styles.flex}
keyboardVerticalOffset={0}
enabled={Platform.OS === 'ios'}
testID='select_server.screen'
>
<TouchableWithoutFeedback
onPress={blur}
accessible={false}
>
<View style={styles.formContainer}>
<Image
source={require('@assets/images/logo.png')}
style={styles.logo}
/>
<KeyboardAwareScrollView
bounces={false}
contentContainerStyle={styles.scrollContainer}
enableAutomaticScroll={Platform.OS === 'android'}
enableOnAndroid={true}
enableResetScrollToCoords={true}
extraScrollHeight={20}
keyboardDismissMode='on-drag'
keyboardShouldPersistTaps='handled'
<View testID='select_server.header.text'>
<FormattedText
style={styles.header}
id='mobile.components.select_server_view.enterServerUrl'
defaultMessage='Enter Server URL'
/>
</View>
<TextInput
testID='select_server.server_url.input'
ref={input}
value={url}
editable={!inputDisabled}
onChangeText={handleTextChanged}
onSubmitEditing={handleConnect}
style={StyleSheet.flatten(inputStyle)}
autoCapitalize='none'
autoCorrect={false}
keyboardType='url'
placeholder={formatMessage({
id: 'mobile.components.select_server_view.siteUrlPlaceholder',
defaultMessage: 'https://mattermost.example.com',
})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
returnKeyType='go'
underlineColorAndroid='transparent'
disableFullscreenUI={true}
/>
<Button
testID='select_server.connect.button'
onPress={handleConnect}
containerStyle={styles.connectButton}
>
{buttonIcon}
{buttonText}
</Button>
{Boolean(error) &&
<View>
<ErrorText
testID='select_server.error.text'
error={error!}
theme={theme}
/>
</View>
}
</View>
</TouchableWithoutFeedback>
// @ts-expect-error legacy ref
ref={keyboardAwareRef}
scrollToOverflowEnabled={true}
style={styles.flex}
>
<ServerHeader theme={theme}/>
<ServerForm
buttonDisabled={buttonDisabled}
connecting={connecting}
displayName={displayName}
handleConnect={handleConnect}
handleDisplayNameTextChanged={handleDisplayNameTextChanged}
handleUrlTextChanged={handleUrlTextChanged}
keyboardAwareRef={keyboardAwareRef}
theme={theme}
url={url}
urlError={urlError}
/>
</KeyboardAwareScrollView>
<AppVersion textStyle={styles.appInfo}/>
</KeyboardAvoidingView>
</SafeAreaView>
</SafeAreaView>
</View>
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
appInfo: {
color: theme.centerChannelColor,
},
container: {
flex: 1,
backgroundColor: theme.centerChannelBg,
color: changeOpacity(theme.centerChannelColor, 0.56),
},
flex: {
flex: 1,
},
formContainer: {
flex: 1,
flexDirection: 'column',
scrollContainer: {
justifyContent: 'center',
alignItems: 'center',
paddingRight: 15,
paddingLeft: 15,
},
disabledInput: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
},
connectButton: {
borderRadius: 3,
borderColor: theme.buttonBg,
alignItems: 'center',
borderWidth: 1,
alignSelf: 'stretch',
marginTop: 10,
padding: 15,
},
connectingIndicator: {
marginRight: 5,
},
inputBox: {
fontSize: 16,
height: 45,
borderColor: theme.centerChannelColor,
borderWidth: 1,
marginTop: 5,
marginBottom: 5,
paddingLeft: 10,
alignSelf: 'stretch',
borderRadius: 3,
color: theme.centerChannelColor,
},
logo: {
height: 72,
resizeMode: 'contain',
},
header: {
color: theme.centerChannelColor,
textAlign: 'center',
marginTop: 15,
marginBottom: 15,
fontSize: 20,
},
connectText: {
textAlign: 'center',
color: theme.buttonBg,
fontSize: 17,
flex: 1,
},
}));

View File

@@ -19,10 +19,11 @@ interface SSOProps extends LaunchProps {
config: Partial<ClientConfig>;
license: Partial<ClientLicense>;
ssoType: string;
serverDisplayName: string;
theme: Partial<Theme>;
}
const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}: SSOProps) => {
const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverUrl, ssoType, theme}: SSOProps) => {
const managedConfig = useManagedConfig();
const [loginError, setLoginError] = useState<string>('');
@@ -73,7 +74,7 @@ const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}
};
const doSSOLogin = async (bearerToken: string, csrfToken: string) => {
const result: LoginActionResponse = await ssoLogin(serverUrl!, config.DiagnosticId!, bearerToken, csrfToken);
const result: LoginActionResponse = await ssoLogin(serverUrl!, serverDisplayName, config.DiagnosticId!, bearerToken, csrfToken);
if (result?.error && result.failed) {
onLoadEndError(result.error);
return;

View File

@@ -29,6 +29,7 @@ describe('SSO', () => {
ssoType: 'GITLAB',
theme: Preferences.THEMES.denim,
serverUrl: 'https://locahost:8065',
serverDisplayName: 'Test Server',
launchType: LaunchType.Normal,
};

View File

@@ -42,6 +42,8 @@ class EphemeralStore {
this.navigationModalStack = [];
}
getAllNavigationComponents = () => this.allNavigationComponentIds;
getNavigationTopComponentId = () => {
return this.navigationComponentIdStack[0];
}

423
app/utils/buttonStyles.ts Normal file
View File

@@ -0,0 +1,423 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {StyleProp, StyleSheet, TextStyle, ViewStyle} from 'react-native';
import {blendColors, changeOpacity} from '@utils/theme';
type ButtonSize = 'xs' | 's' | 'm' | 'lg'
type ButtonEmphasis = 'primary' | 'secondary' | 'tertiary' | 'link'
type ButtonType = 'default' | 'destructive' | 'inverted' | 'disabled'
type ButtonState = 'default' | 'hover' | 'active' | 'focus'
type ButtonSizes = {
[key in ButtonSize]: ViewStyle
}
type BackgroundStyles = {
[key in ButtonEmphasis]: {
[ke in ButtonType]: {
[k in ButtonState]: ViewStyle
}
}
}
/**
* Returns the appropriate Style object for <View style={} />
*
*
* @param theme
* @param size
* @param emphasis
* @param type
* @param state
* @returns
*/
export const buttonBackgroundStyle = (
theme: Theme,
size: ButtonSize = 'm',
emphasis: ButtonEmphasis = 'primary',
type: ButtonType = 'default',
state: ButtonState = 'default',
): StyleProp<ViewStyle> => {
const styles = StyleSheet.create({
main: {
flex: 0,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4,
},
fullWidth: {
width: '100%',
},
});
const backgroundStyles: BackgroundStyles = {
primary: {
default: {
default: {
backgroundColor: theme.buttonBg,
},
hover: {
backgroundColor: blendColors(theme.buttonBg, '#000000', 0.08),
},
active: {
backgroundColor: blendColors(theme.buttonBg, '#000000', 0.16),
},
focus: {
backgroundColor: theme.buttonBg,
borderColor: changeOpacity('#FFFFFF', 0.32),
borderWidth: 2,
},
},
destructive: {
default: {
backgroundColor: theme.errorTextColor,
},
hover: {
backgroundColor: blendColors(theme.errorTextColor, '#000000', 0.08),
},
active: {
backgroundColor: blendColors(theme.errorTextColor, '#000000', 0.16),
},
focus: {
backgroundColor: theme.errorTextColor,
borderColor: changeOpacity('#FFFFFF', 0.32),
borderWidth: 2,
},
},
inverted: {
default: {
backgroundColor: theme.buttonColor,
},
hover: {
backgroundColor: blendColors(theme.buttonColor, '#000000', 0.08),
},
active: {
backgroundColor: blendColors(theme.buttonColor, '#000000', 0.16),
},
focus: {
backgroundColor: theme.buttonColor,
borderColor: changeOpacity('#FFFFFF', 0.32),
borderWidth: 2,
},
},
disabled: {
default: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
hover: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
},
active: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
},
focus: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
},
},
},
secondary: {
default: {
default: {
backgroundColor: changeOpacity(theme.buttonBg, 0),
borderColor: theme.buttonBg,
borderWidth: 1,
},
hover: {
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
borderColor: theme.buttonBg,
borderWidth: 1,
},
active: {
backgroundColor: changeOpacity(theme.buttonBg, 0.16),
borderColor: theme.buttonBg,
borderWidth: 1,
},
focus: {
backgroundColor: changeOpacity(theme.buttonBg, 0),
borderColor: theme.sidebarTextActiveBorder,
borderWidth: 2,
},
},
destructive: {
default: {
backgroundColor: changeOpacity(theme.errorTextColor, 0),
borderColor: theme.errorTextColor,
borderWidth: 1,
},
hover: {
backgroundColor: changeOpacity(theme.errorTextColor, 0.08),
borderColor: theme.errorTextColor,
borderWidth: 1,
},
active: {
backgroundColor: changeOpacity(theme.errorTextColor, 0.16),
borderColor: theme.errorTextColor,
borderWidth: 1,
},
focus: {
backgroundColor: changeOpacity(theme.errorTextColor, 0),
borderColor: changeOpacity(theme.errorTextColor, 0.68), // @to-do; needs 32% white?
borderWidth: 2,
},
},
inverted: {
default: {
backgroundColor: changeOpacity(theme.buttonColor, 0),
borderColor: theme.buttonColor,
borderWidth: 1,
},
hover: {
backgroundColor: changeOpacity(theme.buttonColor, 0.08),
borderColor: theme.buttonColor,
borderWidth: 1,
},
active: {
backgroundColor: changeOpacity(theme.buttonColor, 0.16),
borderColor: theme.buttonColor,
borderWidth: 1,
},
focus: {
backgroundColor: changeOpacity(theme.buttonColor, 0),
borderColor: theme.sidebarTextActiveBorder,
borderWidth: 2,
},
},
disabled: {
default: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
borderColor: changeOpacity(theme.centerChannelColor, 0.32),
borderWidth: 1,
},
hover: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
borderColor: changeOpacity(theme.centerChannelColor, 0.32),
borderWidth: 1,
},
active: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
borderColor: changeOpacity(theme.centerChannelColor, 0.32),
borderWidth: 1,
},
focus: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0),
borderColor: changeOpacity(theme.centerChannelColor, 0.32),
borderWidth: 1,
},
},
},
tertiary: {
default: {
default: {
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
},
hover: {
backgroundColor: changeOpacity(theme.buttonBg, 0.12),
},
active: {
backgroundColor: changeOpacity(theme.buttonBg, 0.16),
},
focus: {
backgroundColor: changeOpacity(theme.buttonBg, 0.08),
borderColor: theme.sidebarTextActiveBorder,
borderWidth: 2,
},
},
destructive: {
default: {
backgroundColor: changeOpacity(theme.errorTextColor, 0.08),
},
hover: {
backgroundColor: changeOpacity(theme.errorTextColor, 0.12),
},
active: {
backgroundColor: changeOpacity(theme.errorTextColor, 0.16),
},
focus: {
backgroundColor: changeOpacity(theme.errorTextColor, 0.08),
borderColor: changeOpacity(theme.errorTextColor, 0.68), // @to-do; needs 32% white?
borderWidth: 2,
},
},
inverted: {
default: {
backgroundColor: changeOpacity('#FFFFFF', 0.12),
},
hover: {
backgroundColor: changeOpacity('#FFFFFF', 0.16),
},
active: {
backgroundColor: changeOpacity('#FFFFFF', 0.24),
},
focus: {
backgroundColor: changeOpacity('#FFFFFF', 0.08),
borderColor: theme.sidebarTextActiveBorder, // @to-do; needs 32% white?
borderWidth: 2,
},
},
disabled: {
default: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
hover: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
active: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
focus: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
},
},
},
link: {
default: {
default: {
backgroundColor: 'transparent',
},
hover: {
backgroundColor: 'transparent',
},
active: {
backgroundColor: 'transparent',
},
focus: {
backgroundColor: 'transparent',
},
},
inverted: {
default: {
backgroundColor: 'transparent',
},
hover: {
backgroundColor: 'transparent',
},
active: {
backgroundColor: 'transparent',
},
focus: {
backgroundColor: 'transparent',
},
},
disabled: {
default: {
backgroundColor: 'transparent',
},
hover: {
backgroundColor: 'transparent',
},
active: {
backgroundColor: 'transparent',
},
focus: {
backgroundColor: 'transparent',
},
},
destructive: {
default: {
backgroundColor: 'transparent',
},
hover: {
backgroundColor: 'transparent',
},
active: {
backgroundColor: 'transparent',
},
focus: {
backgroundColor: 'transparent',
},
},
},
};
const sizes: ButtonSizes = StyleSheet.create({
xs: {
height: 24,
paddingVertical: 6,
paddingHorizontal: 10,
},
s: {
height: 32,
paddingVertical: 10,
paddingHorizontal: 16,
},
m: {
height: 40,
paddingVertical: 12,
paddingHorizontal: 20,
},
lg: {
height: 48,
paddingVertical: 14,
paddingHorizontal: 24,
},
});
return StyleSheet.create([styles.main, sizes[size], backgroundStyles[emphasis][type][state]]);
};
/**
* Returns the appropriate TextStyle for the <Text style={} ...> object inside the button.
*
*
* @param theme
* @param size
* @param emphasis
* @param type
* @returns
*/
export const buttonTextStyle = (
theme: Theme,
size: ButtonSize = 'm',
emphasis: ButtonEmphasis = 'primary',
type: ButtonType = 'default',
): StyleProp<TextStyle> => {
// Color
let color: string = theme.buttonColor;
if (type === 'disabled') {
color = changeOpacity(theme.centerChannelColor, 0.32);
}
if ((type === 'destructive' && emphasis !== 'primary')) {
color = theme.errorTextColor;
}
if ((type === 'inverted' && emphasis === 'primary') ||
(type !== 'inverted' && emphasis !== 'primary')) {
color = theme.buttonBg;
}
const styles = StyleSheet.create({
main: {
fontFamily: 'OpenSans-SemiBold',
fontWeight: '600',
},
underline: {
textDecorationLine: 'underline',
},
});
const sizes = StyleSheet.create({
xs: {
fontSize: 11,
lineHeight: 10,
letterSpacing: 0.02,
},
s: {
fontSize: 12,
lineHeight: 11,
},
m: {
fontSize: 14,
lineHeight: 14,
},
lg: {
fontSize: 16,
lineHeight: 18,
},
});
return StyleSheet.create([styles.main, sizes[size], {color}]);
};

View File

@@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntlShape} from 'react-intl';
import {cleanUrlForLogging} from '@utils/url';
export class ClientError extends Error {
@@ -25,3 +27,14 @@ export class ClientError extends Error {
Object.defineProperty(this, 'message', {enumerable: true});
}
}
export const getErrorMessage = (error: Error | string, intl: IntlShape) => {
const intlError = error as ClientError;
if (intlError.intl) {
return intl.formatMessage(intlError.intl);
} else if (error instanceof Error) {
return error.message;
}
return error;
};

128
app/utils/typography.ts Normal file
View File

@@ -0,0 +1,128 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {StyleSheet, TextStyle} from 'react-native';
// type FontFamilies = 'OpenSans' | 'Metropolis';
type FontTypes = 'Heading' | 'Body';
type FontStyles = 'SemiBold' | 'Regular' | 'Light';
type FontSizes = 25 | 50 | 75 | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 1000;
const fontFamily = StyleSheet.create({
OpenSans: {
fontFamily: 'OpenSans',
},
Metropolis: {
fontFamily: 'Metropolis',
},
});
const fontStyle = StyleSheet.create({
SemiBold: {
fontWeight: '600',
},
Regular: {
fontWeight: '400',
},
Light: {
fontWeight: '300',
},
});
const fontSize = StyleSheet.create({
1000: {
fontSize: 40,
lineHeight: 48,
letterSpacing: -0.02,
},
900: {
fontSize: 36,
lineHeight: 44,
letterSpacing: -0.02,
},
800: {
fontSize: 32,
lineHeight: 40,
letterSpacing: -0.01,
},
700: {
fontSize: 28,
lineHeight: 36,
},
600: {
fontSize: 25,
lineHeight: 30,
},
500: {
fontSize: 22,
lineHeight: 28,
},
400: {
fontSize: 20,
lineHeight: 28,
},
300: {
fontSize: 18,
lineHeight: 24,
},
200: {
fontSize: 16,
lineHeight: 24,
},
100: {
fontSize: 14,
lineHeight: 20,
},
75: {
fontSize: 12,
lineHeight: 16,
},
50: {
fontSize: 11,
lineHeight: 16,
},
25: {
fontSize: 10,
lineHeight: 16,
},
});
type Typography = Pick<TextStyle, 'fontWeight' | 'fontSize' | 'fontFamily' | 'lineHeight' | 'letterSpacing'>
export const typography = (
type: FontTypes = 'Body',
size: FontSizes = 100,
style?: FontStyles,
): Typography => {
// Style defaults
if (!style) {
// eslint-disable-next-line no-param-reassign
style = type === 'Heading' ? 'SemiBold' : 'Regular';
}
const font = type === 'Heading' && size > 100 ? fontFamily.Metropolis : fontFamily.OpenSans;
const typeStyle = {
...font,
...fontSize[size],
...fontStyle[style],
};
/*
* Use the appropriate font-file (i.e. OpenSans-SemiBold)
* This switch statement can be removed when Android supports font-weight strings
*/
switch (typeStyle.fontWeight) {
case '300':
typeStyle.fontFamily = `${typeStyle.fontFamily}-${style}`;
break;
case '400':
typeStyle.fontFamily = `${typeStyle.fontFamily}`;
break;
case '600':
typeStyle.fontFamily = `${typeStyle.fontFamily}-${style}`;
break;
}
return typeStyle;
};