MM-35065 - add addonboarding screens; first steps

This commit is contained in:
Pablo Velez Vidal
2022-10-25 08:19:44 +02:00
parent 1d4806d7b0
commit b7e8c3b739
9 changed files with 473 additions and 3 deletions

View File

@@ -33,6 +33,7 @@ export const LATEX = 'Latex';
export const LOGIN = 'Login';
export const MENTIONS = 'Mentions';
export const MFA = 'MFA';
export const ONBOARDING = 'Onboarding';
export const PERMALINK = 'Permalink';
export const PINNED_MESSAGES = 'PinnedMessages';
export const POST_OPTIONS = 'PostOptions';
@@ -94,6 +95,7 @@ export default {
LOGIN,
MENTIONS,
MFA,
ONBOARDING,
PERMALINK,
PINNED_MESSAGES,
POST_OPTIONS,

View File

@@ -12,7 +12,7 @@ import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from
import {getThemeForCurrentTeam} from '@queries/servers/preference';
import {getCurrentUserId} from '@queries/servers/system';
import {queryMyTeams} from '@queries/servers/team';
import {goToScreen, resetToHome, resetToSelectServer, resetToTeams} from '@screens/navigation';
import {goToScreen, resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {logInfo} from '@utils/log';
import {convertToNotificationData} from '@utils/notification';
@@ -58,6 +58,7 @@ const launchAppFromNotification = async (notification: NotificationWithData) =>
launchApp(props);
};
// pablo - here I need to validate the logic for launching the onboarding screen
const launchApp = async (props: LaunchProps, resetNavigation = true) => {
let serverUrl: string | undefined;
switch (props?.launchType) {
@@ -126,7 +127,7 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
}
}
return launchToServer(props, resetNavigation);
return launchToOnboarding(props);
};
const launchToHome = async (props: LaunchProps) => {
@@ -180,6 +181,10 @@ const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
return goToScreen(Screens.SERVER, title, {...props});
};
const launchToOnboarding = (props: LaunchProps) => {
return resetToOnboarding(props);
};
export const relaunchApp = (props: LaunchProps, resetNavigation = false) => {
return launchApp(props, resetNavigation);
};

View File

@@ -227,6 +227,8 @@ Navigation.setLazyComponentRegistrator((screenName) => {
export function registerScreens() {
const homeScreen = require('@screens/home').default;
const serverScreen = require('@screens/server').default;
const onboardingScreen = require('@screens/onboarding').default;
Navigation.registerComponent(Screens.ONBOARDING, () => withGestures(withIntl(withManagedConfig<ManagedConfig>(onboardingScreen)), undefined));
Navigation.registerComponent(Screens.SERVER, () => withGestures(withIntl(withManagedConfig<ManagedConfig>(serverScreen)), undefined));
Navigation.registerComponent(Screens.HOME, () => withGestures(withSafeAreaInsets(withServerDatabase(withManagedConfig<ManagedConfig>(homeScreen))), undefined));
}

View File

@@ -281,6 +281,54 @@ export function resetToSelectServer(passProps: LaunchProps) {
});
}
export function resetToOnboarding(passProps: LaunchProps) {
const theme = getDefaultThemeByAppearance();
const isDark = tinyColor(theme.sidebarBg).isDark();
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
NavigationStore.clearNavigationComponents();
const children = [{
component: {
id: Screens.ONBOARDING,
name: Screens.ONBOARDING,
passProps: {
...passProps,
theme,
},
options: {
layout: {
backgroundColor: theme.centerChannelBg,
componentBackgroundColor: theme.centerChannelBg,
},
statusBar: {
visible: true,
backgroundColor: theme.sidebarBg,
},
topBar: {
backButton: {
color: theme.sidebarHeaderTextColor,
title: '',
},
background: {
color: theme.sidebarBg,
},
visible: false,
height: 0,
},
},
},
}];
return Navigation.setRoot({
root: {
stack: {
children,
},
},
});
}
export function resetToTeams() {
const theme = getThemeFromState();
const isDark = tinyColor(theme.sidebarBg).isDark();

View File

@@ -0,0 +1,237 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {MutableRefObject, useCallback, useEffect, useRef} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, useWindowDimensions, View} from 'react-native';
import Button from 'react-native-button';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import FloatingTextInput, {FloatingTextInputRef} from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {useIsTablet} from '@hooks/device';
import {t} from '@i18n';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
autoFocus?: boolean;
buttonDisabled: boolean;
connecting: boolean;
displayName?: string;
displayNameError?: string;
handleConnect: () => void;
handleDisplayNameTextChanged: (text: string) => void;
handleUrlTextChanged: (text: string) => void;
keyboardAwareRef: MutableRefObject<KeyboardAwareScrollView | null>;
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,
},
loadingContainerStyle: {
marginRight: 10,
padding: 0,
top: -2,
},
}));
const ServerForm = ({
autoFocus = false,
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') {
const reset = !displayNameRef.current?.isFocused() && !urlRef.current?.isFocused();
if (reset) {
keyboardAwareRef.current?.scrollToPosition(0, 0);
}
}
}, []);
const onConnect = useCallback(() => {
Keyboard.dismiss();
handleConnect();
}, [buttonDisabled, connecting, displayName, theme, url]);
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]);
const buttonType = buttonDisabled ? 'disabled' : 'default';
const styleButtonText = buttonTextStyle(theme, 'lg', 'primary', buttonType);
const styleButtonBackground = buttonBackgroundStyle(theme, 'lg', 'primary', buttonType);
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 = (
<Loading
containerStyle={styles.loadingContainerStyle}
color={theme.buttonColor}
/>
);
}
const connectButtonTestId = buttonDisabled ? 'server_form.connect.button.disabled' : 'server_form.connect.button';
return (
<View style={styles.formContainer}>
<View style={[styles.fullWidth, urlError?.length ? styles.error : undefined]}>
<FloatingTextInput
autoCorrect={false}
autoCapitalize={'none'}
autoFocus={autoFocus}
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'
spellCheck={false}
testID='server_form.server_url.input'
theme={theme}
value={url}
/>
</View>
<View style={[styles.fullWidth, displayNameError?.length ? styles.error : undefined]}>
<FloatingTextInput
autoCorrect={false}
autoCapitalize={'none'}
enablesReturnKeyAutomatically={true}
error={displayNameError}
label={formatMessage({
id: 'mobile.components.select_server_view.displayName',
defaultMessage: 'Display Name',
})}
onBlur={onBlur}
onChangeText={handleDisplayNameTextChanged}
onFocus={onFocus}
onSubmitEditing={onConnect}
ref={displayNameRef}
returnKeyType='done'
spellCheck={false}
testID='server_form.server_display_name.input'
theme={theme}
value={displayName}
/>
</View>
{!displayNameError &&
<FormattedText
defaultMessage={'Choose a display name for your server'}
id={'mobile.components.select_server_view.displayHelp'}
style={styles.chooseText}
testID={'server_form.display_help'}
/>
}
<Button
containerStyle={[styles.connectButton, styleButtonBackground]}
disabled={buttonDisabled}
onPress={onConnect}
testID={connectButtonTestId}
>
{buttonIcon}
<FormattedText
defaultMessage={buttonText}
id={buttonID}
style={styleButtonText}
/>
</Button>
</View>
);
};
export default ServerForm;

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import FormattedText from '@components/formatted_text';
import {useIsTablet} from '@hooks/device';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
type Props = {
additionalServer: boolean;
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.centerChannelColor,
marginVertical: 12,
...typography('Heading', 1000, 'SemiBold'),
},
connectTablet: {
width: undefined,
},
description: {
color: changeOpacity(theme.centerChannelColor, 0.64),
...typography('Body', 200, 'Regular'),
},
}));
const ServerHeader = ({additionalServer, theme}: Props) => {
const isTablet = useIsTablet();
const styles = getStyleSheet(theme);
let title;
if (additionalServer) {
title = (
<FormattedText
defaultMessage='Add a server'
id='servers.create_button'
style={[styles.connect, isTablet ? styles.connectTablet : undefined]}
testID='server_header.title.add_server'
/>
);
} else {
title = (
<FormattedText
defaultMessage='Lets Connect to a Server'
id='mobile.components.select_server_view.msg_connect'
style={[styles.connect, isTablet ? styles.connectTablet : undefined]}
testID='server_header.title.connect_to_server'
/>
);
}
return (
<View style={styles.textContainer}>
{!additionalServer &&
<FormattedText
defaultMessage='Welcome'
id='mobile.components.select_server_view.msg_welcome'
testID='server_header.welcome'
style={styles.welcome}
/>
}
{title}
<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='server_header.description'
/>
</View>
);
};
export default ServerHeader;

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react';
import {Platform, Text, View} from 'react-native';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import {SafeAreaView} from 'react-native-safe-area-context';
import AppVersion from '@components/app_version';
import Background from '@screens/background';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {LaunchProps} from '@typings/launch';
interface OnboardingProps extends LaunchProps {
theme: Theme;
}
const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
const Onboarding = ({
theme,
}: OnboardingProps) => {
const translateX = useSharedValue(0);
const keyboardAwareRef = useRef<KeyboardAwareScrollView>(null);
const styles = getStyleSheet(theme);
const transform = useAnimatedStyle(() => {
const duration = Platform.OS === 'android' ? 250 : 350;
return {
transform: [{translateX: withTiming(translateX.value, {duration})}],
};
}, []);
return (
<View
style={styles.flex}
testID='server.screen'
>
<Background theme={theme}/>
<AnimatedSafeArea
key={'server_content'}
style={[styles.flex, transform]}
>
<KeyboardAwareScrollView
bounces={false}
contentContainerStyle={styles.scrollContainer}
enableAutomaticScroll={Platform.OS === 'android'}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraScrollHeight={20}
keyboardDismissMode='on-drag'
keyboardShouldPersistTaps='handled'
ref={keyboardAwareRef}
scrollToOverflowEnabled={true}
style={styles.flex}
>
<Text>{'Hola mundo'}</Text>
</KeyboardAwareScrollView>
<AppVersion textStyle={styles.appInfo}/>
</AnimatedSafeArea>
</View>
);
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
appInfo: {
color: changeOpacity(theme.centerChannelColor, 0.56),
},
flex: {
flex: 1,
},
scrollContainer: {
alignItems: 'center',
height: '100%',
justifyContent: 'center',
},
}));
export default Onboarding;

View File

@@ -67,6 +67,7 @@ if (global.HermesInternal) {
let alreadyInitialized = false;
Navigation.events().registerAppLaunchedListener(async () => {
// See caution in the library doc https://wix.github.io/react-native-navigation/docs/app-launch#android
console.log('*** already init', alreadyInitialized);
if (!alreadyInitialized) {
alreadyInitialized = true;
GlobalEventHandler.init();
@@ -82,8 +83,12 @@ Navigation.events().registerAppLaunchedListener(async () => {
await WebsocketManager.init(serverCredentials);
PushNotifications.init();
SessionManager.init();
console.log('*** already init 2', alreadyInitialized);
}
console.log('*** already init 3', alreadyInitialized);
initialLaunch();
});

View File

@@ -190,7 +190,7 @@
"mmjstool": "mmjstool",
"pod-install": "cd ios && bundle exec pod install",
"postinstall": "patch-package && ./scripts/postinstall.sh",
"preinstall": "npx solidarity",
"preinstall": "",
"prestorybook": "rnstl",
"start": "react-native start",
"storybook": "start-storybook -p 7007",