forked from Ivasoft/mattermost-mobile
MM-35065 - add addonboarding screens; first steps
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
237
app/screens/onboarding/form.tsx
Normal file
237
app/screens/onboarding/form.tsx
Normal 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;
|
||||
91
app/screens/onboarding/header.tsx
Normal file
91
app/screens/onboarding/header.tsx
Normal 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='Let’s 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;
|
||||
80
app/screens/onboarding/index.tsx
Normal file
80
app/screens/onboarding/index.tsx
Normal 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;
|
||||
5
index.ts
5
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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user