From b7e8c3b739ab1ff17f1faeb9d4e305bce76e99da Mon Sep 17 00:00:00 2001 From: Pablo Velez Vidal Date: Tue, 25 Oct 2022 08:19:44 +0200 Subject: [PATCH] MM-35065 - add addonboarding screens; first steps --- app/constants/screens.ts | 2 + app/init/launch.ts | 9 +- app/screens/index.tsx | 2 + app/screens/navigation.ts | 48 ++++++ app/screens/onboarding/form.tsx | 237 ++++++++++++++++++++++++++++++ app/screens/onboarding/header.tsx | 91 ++++++++++++ app/screens/onboarding/index.tsx | 80 ++++++++++ index.ts | 5 + package.json | 2 +- 9 files changed, 473 insertions(+), 3 deletions(-) create mode 100644 app/screens/onboarding/form.tsx create mode 100644 app/screens/onboarding/header.tsx create mode 100644 app/screens/onboarding/index.tsx diff --git a/app/constants/screens.ts b/app/constants/screens.ts index ddef79467d..a5aab658c6 100644 --- a/app/constants/screens.ts +++ b/app/constants/screens.ts @@ -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, diff --git a/app/init/launch.ts b/app/init/launch.ts index 04aa737a03..6a372a61ac 100644 --- a/app/init/launch.ts +++ b/app/init/launch.ts @@ -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); }; diff --git a/app/screens/index.tsx b/app/screens/index.tsx index baa707d88d..68b7f3798f 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -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(onboardingScreen)), undefined)); Navigation.registerComponent(Screens.SERVER, () => withGestures(withIntl(withManagedConfig(serverScreen)), undefined)); Navigation.registerComponent(Screens.HOME, () => withGestures(withSafeAreaInsets(withServerDatabase(withManagedConfig(homeScreen))), undefined)); } diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 7eb8fda449..b985754207 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -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(); diff --git a/app/screens/onboarding/form.tsx b/app/screens/onboarding/form.tsx new file mode 100644 index 0000000000..9285a8c651 --- /dev/null +++ b/app/screens/onboarding/form.tsx @@ -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; + 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(null); + const urlRef = useRef(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 = ( + + ); + } + + const connectButtonTestId = buttonDisabled ? 'server_form.connect.button.disabled' : 'server_form.connect.button'; + + return ( + + + + + + + + {!displayNameError && + + } + + + ); +}; + +export default ServerForm; diff --git a/app/screens/onboarding/header.tsx b/app/screens/onboarding/header.tsx new file mode 100644 index 0000000000..d7fd475cb9 --- /dev/null +++ b/app/screens/onboarding/header.tsx @@ -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 = ( + + ); + } else { + title = ( + + ); + } + + return ( + + {!additionalServer && + + } + {title} + + + ); +}; + +export default ServerHeader; diff --git a/app/screens/onboarding/index.tsx b/app/screens/onboarding/index.tsx new file mode 100644 index 0000000000..2b13f582d6 --- /dev/null +++ b/app/screens/onboarding/index.tsx @@ -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(null); + const styles = getStyleSheet(theme); + + const transform = useAnimatedStyle(() => { + const duration = Platform.OS === 'android' ? 250 : 350; + return { + transform: [{translateX: withTiming(translateX.value, {duration})}], + }; + }, []); + + return ( + + + + + {'Hola mundo'} + + + + + ); +}; + +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; diff --git a/index.ts b/index.ts index ccceb3ec9e..9f8d9199c1 100644 --- a/index.ts +++ b/index.ts @@ -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(); }); diff --git a/package.json b/package.json index be35b8c9a3..b5636dd4ea 100644 --- a/package.json +++ b/package.json @@ -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",