diff --git a/app/hooks/device.ts b/app/hooks/device.ts index 410437b01..72266b0b3 100644 --- a/app/hooks/device.ts +++ b/app/hooks/device.ts @@ -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, containerHeight: nu return overlap; } + +export function useAvoidKeyboard(ref: RefObject, dimisher = 3) { + const height = useKeyboardHeight(); + + useEffect(() => { + let offsetY = height / dimisher; + if (offsetY < 80) { + offsetY = 0; + } + + ref.current?.scrollToPosition(0, offsetY); + }, [height, dimisher, ref]); +} diff --git a/app/screens/forgot_password/index.tsx b/app/screens/forgot_password/index.tsx index 24a64a1ff..480275ec5 100644 --- a/app/screens/forgot_password/index.tsx +++ b/app/screens/forgot_password/index.tsx @@ -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(''); const [error, setError] = useState(''); const [isPasswordLinkSent, setIsPasswordLinkSent] = useState(false); @@ -101,25 +100,13 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => { const keyboardAwareRef = useRef(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) => { { 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; diff --git a/app/screens/index.tsx b/app/screens/index.tsx index 839f53911..30e9b2068 100644 --- a/app/screens/index.tsx +++ b/app/screens/index.tsx @@ -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)); } diff --git a/app/screens/login/form.tsx b/app/screens/login/form.tsx index 025d13a48..325a58961 100644 --- a/app/screens/login/form.tsx +++ b/app/screens/login/form.tsx @@ -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; license: Partial; + keyboardAwareRef: RefObject; 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(null); const passwordRef = useRef(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); diff --git a/app/screens/login/index.tsx b/app/screens/login/index.tsx index cbda07707..f56f43644 100644 --- a/app/screens/login/index.tsx +++ b/app/screens/login/index.tsx @@ -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 = ({ { const dimensions = useWindowDimensions(); const translateX = useSharedValue(dimensions.width); - const isTablet = useIsTablet(); const keyboardAwareRef = useRef(null); const intl = useIntl(); const [token, setToken] = useState(''); @@ -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 void; handleDisplayNameTextChanged: (text: string) => void; handleUrlTextChanged: (text: string) => void; - isModal?: boolean; - keyboardAwareRef: MutableRefObject; + keyboardAwareRef: RefObject; 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(null); const urlRef = useRef(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' diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx index 1ff20c262..41ec13d35 100644 --- a/app/screens/server/index.tsx +++ b/app/screens/server/index.tsx @@ -376,7 +376,7 @@ const Server = ({