fix screen position for server / login / forgot password & mfa (#8340)

* fix screen position for server / login / forgot password & mfa

* refactor to use a hook

* fix scroll to offset not to height

* feedback review
This commit is contained in:
Elias Nahum 2024-11-19 08:36:47 +08:00 committed by GitHub
parent 7d8d4a4c68
commit 37fd1609c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 48 additions and 100 deletions

View file

@ -9,6 +9,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {DeviceContext} from '@context/device';
import type {KeyboardTrackingViewRef, KeyboardWillShowEventData} from '@mattermost/keyboard-tracker';
import type {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
const utilsEmitter = new NativeEventEmitter(RNUtils);
@ -147,3 +148,16 @@ export function useKeyboardOverlap(viewRef: RefObject<View>, containerHeight: nu
return overlap;
}
export function useAvoidKeyboard(ref: RefObject<KeyboardAwareScrollView>, dimisher = 3) {
const height = useKeyboardHeight();
useEffect(() => {
let offsetY = height / dimisher;
if (offsetY < 80) {
offsetY = 0;
}
ref.current?.scrollToPosition(0, offsetY);
}, [height, dimisher, ref]);
}

View file

@ -15,7 +15,7 @@ import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import {Screens} from '@constants';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import {useAvoidKeyboard} from '@hooks/device';
import Background from '@screens/background';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {isEmail} from '@utils/helpers';
@ -93,7 +93,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
const dimensions = useWindowDimensions();
const translateX = useSharedValue(dimensions.width);
const isTablet = useIsTablet();
const [email, setEmail] = useState<string>('');
const [error, setError] = useState<string>('');
const [isPasswordLinkSent, setIsPasswordLinkSent] = useState<boolean>(false);
@ -101,25 +100,13 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
const keyboardAwareRef = useRef<KeyboardAwareScrollView>(null);
const styles = getStyleSheet(theme);
useAvoidKeyboard(keyboardAwareRef);
const changeEmail = useCallback((emailAddress: string) => {
setEmail(emailAddress);
setError('');
}, []);
const onFocus = useCallback(() => {
if (Platform.OS === 'ios') {
let offsetY = 150;
if (isTablet) {
const {width, height} = dimensions;
const isLandscape = width > height;
offsetY = (isLandscape ? 230 : 150);
}
requestAnimationFrame(() => {
keyboardAwareRef.current?.scrollToPosition(0, offsetY);
});
}
}, [dimensions]);
const onReturn = useCallback(() => {
Navigation.popTo(Screens.LOGIN);
}, []);
@ -146,7 +133,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
id: 'password_send.generic_error',
defaultMessage: 'We were unable to send you a reset password link. Please contact your System Admin for assistance.',
}));
}, [email]);
}, [email, formatMessage, serverUrl]);
const getCenterContent = () => {
if (isPasswordLinkSent) {
@ -188,7 +175,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
<KeyboardAwareScrollView
bounces={false}
contentContainerStyle={styles.innerContainer}
enableAutomaticScroll={Platform.OS === 'android'}
enableAutomaticScroll={false}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraScrollHeight={0}
@ -224,7 +211,6 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
keyboardType='email-address'
label={formatMessage({id: 'login.email', defaultMessage: 'Email'})}
onChangeText={changeEmail}
onFocus={onFocus}
onSubmitEditing={submitResetPassword}
returnKeyType='next'
spellCheck={false}
@ -270,7 +256,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId);
return () => unsubscribe.remove();
}, [dimensions]);
}, [componentId, dimensions]);
useEffect(() => {
translateX.value = 0;

View file

@ -295,6 +295,6 @@ export function registerScreens() {
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.SERVER, () => withSafeAreaInsets(withGestures(withIntl(withManagedConfig(serverScreen)), undefined)));
Navigation.registerComponent(Screens.HOME, () => withGestures(withSafeAreaInsets(withServerDatabase(withManagedConfig(homeScreen))), undefined));
}

View file

@ -3,7 +3,7 @@
import {useManagedConfig} from '@mattermost/react-native-emm';
import {Button} from '@rneui/base';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState, type RefObject} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, TextInput, TouchableOpacity, View} from 'react-native';
@ -13,6 +13,7 @@ import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
import {useAvoidKeyboard} from '@hooks/device';
import {t} from '@i18n';
import {goToScreen, loginAnimationOptions, resetToHome} from '@screens/navigation';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
@ -22,10 +23,12 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
import type {LaunchProps} from '@typings/launch';
import type {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
interface LoginProps extends LaunchProps {
config: Partial<ClientConfig>;
license: Partial<ClientLicense>;
keyboardAwareRef: RefObject<KeyboardAwareScrollView>;
serverDisplayName: string;
theme: Theme;
}
@ -77,7 +80,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const LoginForm = ({config, extra, serverDisplayName, launchError, launchType, license, serverUrl, theme}: LoginProps) => {
const LoginForm = ({config, extra, keyboardAwareRef, serverDisplayName, launchError, launchType, license, serverUrl, theme}: LoginProps) => {
const styles = getStyleSheet(theme);
const loginRef = useRef<TextInput>(null);
const passwordRef = useRef<TextInput>(null);
@ -93,6 +96,8 @@ const LoginForm = ({config, extra, serverDisplayName, launchError, launchType, l
const usernameEnabled = config.EnableSignInWithUsername === 'true';
const ldapEnabled = license.IsLicensed === 'true' && config.EnableLdap === 'true' && license.LDAP === 'true';
useAvoidKeyboard(keyboardAwareRef);
const preSignIn = preventDoubleTap(async () => {
setIsLoading(true);
@ -222,7 +227,7 @@ const LoginForm = ({config, extra, serverDisplayName, launchError, launchType, l
};
goToScreen(FORGOT_PASSWORD, '', passProps, loginAnimationOptions());
}, [theme]);
}, [config.ForgotPasswordLink, serverUrl, theme]);
const togglePasswordVisiblity = useCallback(() => {
setIsPasswordVisible((prevState) => !prevState);

View file

@ -119,7 +119,7 @@ const LoginOptions = ({
defaultMessage="You can't log in to your account yet. At least one login option must be configured. Contact your System Admin for assistance."
/>
);
}, [hasLoginForm, numberSSOs, theme]);
}, [hasLoginForm, numberSSOs, styles.subheader]);
const goToSso = preventDoubleTap((ssoType: string) => {
goToScreen(Screens.SSO, '', {config, extra, launchError, launchType, license, theme, ssoType, serverDisplayName, serverUrl}, loginAnimationOptions());
@ -160,7 +160,7 @@ const LoginOptions = ({
});
return () => navigationEvents.remove();
}, []);
}, [closeButtonId, componentId, serverUrl]);
useEffect(() => {
translateX.value = 0;
@ -178,7 +178,7 @@ const LoginOptions = ({
const unsubscribe = Navigation.events().registerComponentListener(listener, Screens.LOGIN);
return () => unsubscribe.remove();
}, [dimensions]);
}, [dimensions, translateX]);
useNavButtonPressed(closeButtonId || '', componentId, dismiss, []);
useAndroidHardwareBackHandler(componentId, pop);
@ -219,11 +219,11 @@ const LoginOptions = ({
<KeyboardAwareScrollView
bounces={true}
contentContainerStyle={[styles.innerContainer, additionalContainerStyle]}
enableAutomaticScroll={true}
enableAutomaticScroll={false}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraScrollHeight={0}
keyboardDismissMode='interactive'
extraScrollHeight={20}
keyboardDismissMode='on-drag'
keyboardShouldPersistTaps='handled'
ref={keyboardAwareRef}
scrollToOverflowEnabled={true}
@ -239,6 +239,7 @@ const LoginOptions = ({
<Form
config={config}
extra={extra}
keyboardAwareRef={keyboardAwareRef}
license={license}
launchError={launchError}
launchType={launchType}

View file

@ -15,7 +15,7 @@ import FloatingTextInput from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useIsTablet} from '@hooks/device';
import {useAvoidKeyboard} from '@hooks/device';
import {t} from '@i18n';
import Background from '@screens/background';
import {popTopScreen} from '@screens/navigation';
@ -99,7 +99,6 @@ const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
const MFA = ({componentId, config, goToHome, license, loginId, password, serverDisplayName, serverUrl, theme}: MFAProps) => {
const dimensions = useWindowDimensions();
const translateX = useSharedValue(dimensions.width);
const isTablet = useIsTablet();
const keyboardAwareRef = useRef<KeyboardAwareScrollView>(null);
const intl = useIntl();
const [token, setToken] = useState<string>('');
@ -109,20 +108,6 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
const styles = getStyleSheet(theme);
const onFocus = useCallback(() => {
if (Platform.OS === 'ios') {
let offsetY = 150;
if (isTablet) {
const {width, height} = dimensions;
const isLandscape = width > height;
offsetY = (isLandscape ? 270 : 150);
}
requestAnimationFrame(() => {
keyboardAwareRef.current?.scrollToPosition(0, offsetY);
});
}
}, [dimensions]);
const handleInput = useCallback((userToken: string) => {
setToken(userToken);
setError('');
@ -156,6 +141,8 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
};
}, []);
useAvoidKeyboard(keyboardAwareRef, 2);
useEffect(() => {
const listener = {
componentDidAppear: () => {
@ -168,7 +155,7 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId);
return () => unsubscribe.remove();
}, [dimensions]);
}, [componentId, dimensions, translateX]);
useEffect(() => {
translateX.value = 0;
@ -190,7 +177,7 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
<KeyboardAwareScrollView
bounces={false}
contentContainerStyle={styles.innerContainer}
enableAutomaticScroll={Platform.OS === 'android'}
enableAutomaticScroll={false}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraScrollHeight={0}
@ -226,7 +213,6 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
keyboardType='numeric'
label={formatMessage({id: 'login_mfa.token', defaultMessage: 'Enter MFA Token'})}
onChangeText={handleInput}
onFocus={onFocus}
onSubmitEditing={submit}
returnKeyType='go'
spellCheck={false}

View file

@ -2,14 +2,14 @@
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React, {type MutableRefObject, useCallback, useEffect, useRef} from 'react';
import React, {type RefObject, useCallback, useRef} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, useWindowDimensions, View} from 'react-native';
import {Keyboard, View} from 'react-native';
import FloatingTextInput, {type FloatingTextInputRef} from '@components/floating_text_input_label';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {useIsTablet} from '@hooks/device';
import {useAvoidKeyboard} from '@hooks/device';
import {t} from '@i18n';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -27,8 +27,7 @@ type Props = {
handleConnect: () => void;
handleDisplayNameTextChanged: (text: string) => void;
handleUrlTextChanged: (text: string) => void;
isModal?: boolean;
keyboardAwareRef: MutableRefObject<KeyboardAwareScrollView | null>;
keyboardAwareRef: RefObject<KeyboardAwareScrollView>;
theme: Theme;
url?: string;
urlError?: string;
@ -79,65 +78,27 @@ const ServerForm = ({
handleConnect,
handleDisplayNameTextChanged,
handleUrlTextChanged,
isModal,
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 = isModal ? 120 : 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);
}
}
}, []);
useAvoidKeyboard(keyboardAwareRef);
const onConnect = useCallback(() => {
Keyboard.dismiss();
handleConnect();
}, [buttonDisabled, connecting, displayName, theme, url]);
const onFocus = useCallback(() => {
focus();
}, [dimensions]);
}, [handleConnect]);
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);
@ -176,9 +137,7 @@ const ServerForm = ({
id: 'mobile.components.select_server_view.enterServerUrl',
defaultMessage: 'Enter Server URL',
})}
onBlur={onBlur}
onChangeText={handleUrlTextChanged}
onFocus={onFocus}
onSubmitEditing={onUrlSubmit}
ref={urlRef}
returnKeyType='next'
@ -198,9 +157,7 @@ const ServerForm = ({
id: 'mobile.components.select_server_view.displayName',
defaultMessage: 'Display Name',
})}
onBlur={onBlur}
onChangeText={handleDisplayNameTextChanged}
onFocus={onFocus}
onSubmitEditing={onConnect}
ref={displayNameRef}
returnKeyType='done'

View file

@ -376,7 +376,7 @@ const Server = ({
<KeyboardAwareScrollView
bounces={false}
contentContainerStyle={styles.scrollContainer}
enableAutomaticScroll={Platform.OS === 'android'}
enableAutomaticScroll={false}
enableOnAndroid={false}
enableResetScrollToCoords={true}
extraScrollHeight={20}
@ -400,7 +400,6 @@ const Server = ({
handleConnect={handleConnect}
handleDisplayNameTextChanged={handleDisplayNameTextChanged}
handleUrlTextChanged={handleUrlTextChanged}
isModal={isModal}
keyboardAwareRef={keyboardAwareRef}
theme={theme}
url={url}