forked from Ivasoft/mattermost-mobile
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:
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('*** MFA Screen ***', () => {
|
||||
password: 'passwd',
|
||||
license: {},
|
||||
serverUrl: 'https://locahost:8065',
|
||||
serverDisplayName: 'Test Server',
|
||||
theme: Preferences.THEMES.denim,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
550
app/screens/server/background.tsx
Normal file
550
app/screens/server/background.tsx
Normal file
File diff suppressed because one or more lines are too long
217
app/screens/server/form.tsx
Normal file
217
app/screens/server/form.tsx
Normal 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;
|
||||
72
app/screens/server/header.tsx
Normal file
72
app/screens/server/header.tsx
Normal 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={'Let’s 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;
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('SSO', () => {
|
||||
ssoType: 'GITLAB',
|
||||
theme: Preferences.THEMES.denim,
|
||||
serverUrl: 'https://locahost:8065',
|
||||
serverDisplayName: 'Test Server',
|
||||
launchType: LaunchType.Normal,
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ class EphemeralStore {
|
||||
this.navigationModalStack = [];
|
||||
}
|
||||
|
||||
getAllNavigationComponents = () => this.allNavigationComponentIds;
|
||||
|
||||
getNavigationTopComponentId = () => {
|
||||
return this.navigationComponentIdStack[0];
|
||||
}
|
||||
|
||||
423
app/utils/buttonStyles.ts
Normal file
423
app/utils/buttonStyles.ts
Normal 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}]);
|
||||
};
|
||||
|
||||
@@ -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
128
app/utils/typography.ts
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user