diff --git a/native/account/legacy-register-panel.react.js b/native/account/legacy-register-panel.react.js --- a/native/account/legacy-register-panel.react.js +++ b/native/account/legacy-register-panel.react.js @@ -10,7 +10,6 @@ Keyboard, Linking, } from 'react-native'; -import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { @@ -47,6 +46,7 @@ import { useSelector } from '../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../selectors/account-selectors.js'; import type { KeyPressEvent } from '../types/react-native.js'; +import type { ViewStyle } from '../types/styles.js'; import { appOutOfDateAlertDetails, usernameReservedAlertDetails, @@ -64,7 +64,7 @@ export type LegacyRegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, - +opacityValue: Animated.Node, + +opacityStyle: ViewStyle, +legacyRegisterState: StateContainer, }; type Props = { @@ -128,7 +128,7 @@ ); return ( - + void, - +opacityValue: Animated.Node, + +opacityStyle: ViewStyle, +logInState: StateContainer, }; type Props = { @@ -118,7 +118,7 @@ render(): React.Node { return ( - + void */, +) /*: number */ { + 'worklet'; + const targetPanelOpacity = + modeValue === 'loading' || modeValue === 'prompt' ? 0 : 1; + return withTiming( + targetPanelOpacity, + timingConfig, + (succeeded /*?: boolean */) => { + if (succeeded && targetPanelOpacity === 0) { + runOnJS(proceedToNextMode)(); + } + }, ); } @@ -304,138 +319,21 @@ const nextModeRef = React.useRef(initialMode); const dimensions = useSelector(derivedDimensionsInfoSelector); - const contentHeight = useValue(dimensions.safeAreaHeight); - const modeValue = useValue(modeNumbers[initialMode]); - const buttonOpacity = useValue(persistedStateLoaded ? 1 : 0); - - const [activeAlert, setActiveAlert] = React.useState(false); - - const navContext = React.useContext(NavContext); - const isForeground = isForegroundSelector(navContext); - - const keyboardHeightInput = React.useMemo( - () => ({ - ignoreKeyboardDismissal: activeAlert, - disabled: !isForeground, - }), - [activeAlert, isForeground], - ); - const keyboardHeightValue = useKeyboardHeight(keyboardHeightInput); - - const prevModeValue = useValue(modeNumbers[initialMode]); - const panelPaddingTop = React.useMemo(() => { - const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54; - const promptButtonsSize = Platform.OS === 'ios' ? 40 : 61; - const logInContainerSize = 140; - const registerPanelSize = Platform.OS === 'ios' ? 181 : 180; - const siwePanelSize = 250; - - const containerSize = add( - headerHeight, - cond(not(isPastPrompt(modeValue)), promptButtonsSize, 0), - cond(eq(modeValue, modeNumbers['log-in']), logInContainerSize, 0), - cond(eq(modeValue, modeNumbers['register']), registerPanelSize, 0), - cond(eq(modeValue, modeNumbers['siwe']), siwePanelSize, 0), - ); - const potentialPanelPaddingTop = divide( - max(sub(contentHeight, keyboardHeightValue, containerSize), 0), - 2, - ); - - const panelPaddingTopValue = new Value(-1); - const targetPanelPaddingTop = new Value(-1); - const clock = new Clock(); - const keyboardTimeoutClock = new Clock(); - return block([ - cond(lessThan(panelPaddingTopValue, 0), [ - set(panelPaddingTopValue, potentialPanelPaddingTop), - set(targetPanelPaddingTop, potentialPanelPaddingTop), - ]), - cond( - lessThan(keyboardHeightValue, 0), - [ - runTiming(keyboardTimeoutClock, 0, 1, true, { duration: 500 }), - cond( - not(clockRunning(keyboardTimeoutClock)), - set(keyboardHeightValue, 0), - ), - ], - stopClock(keyboardTimeoutClock), - ), - cond( - and(greaterOrEq(keyboardHeightValue, 0), neq(prevModeValue, modeValue)), - [ - stopClock(clock), - cond( - neq(isPastPrompt(prevModeValue), isPastPrompt(modeValue)), - set(targetPanelPaddingTop, potentialPanelPaddingTop), - ), - set(prevModeValue, modeValue), - ], - ), - ratchetAlongWithKeyboardHeight(keyboardHeightValue, [ - stopClock(clock), - set(targetPanelPaddingTop, potentialPanelPaddingTop), - ]), - cond( - neq(panelPaddingTopValue, targetPanelPaddingTop), - set( - panelPaddingTopValue, - runTiming(clock, panelPaddingTopValue, targetPanelPaddingTop), - ), - ), - panelPaddingTopValue, - ]); - }, [modeValue, contentHeight, keyboardHeightValue, prevModeValue]); + const contentHeight = useSharedValue(dimensions.safeAreaHeight); + const modeValue = useSharedValue(initialMode); + const buttonOpacity = useSharedValue(persistedStateLoaded ? 1 : 0); const proceedToNextMode = React.useCallback(() => { setMode({ curMode: nextModeRef.current }); }, [setMode]); - const panelOpacity = React.useMemo(() => { - const targetPanelOpacity = isPastPrompt(modeValue); - - const panelOpacityValue = new Value(-1); - const prevPanelOpacity = new Value(-1); - const prevTargetPanelOpacity = new Value(-1); - const clock = new Clock(); - return block([ - cond(lessThan(panelOpacityValue, 0), [ - set(panelOpacityValue, targetPanelOpacity), - set(prevPanelOpacity, targetPanelOpacity), - set(prevTargetPanelOpacity, targetPanelOpacity), - ]), - cond(greaterOrEq(keyboardHeightValue, 0), [ - cond(neq(targetPanelOpacity, prevTargetPanelOpacity), [ - stopClock(clock), - set(prevTargetPanelOpacity, targetPanelOpacity), - ]), - cond( - neq(panelOpacityValue, targetPanelOpacity), - set( - panelOpacityValue, - runTiming(clock, panelOpacityValue, targetPanelOpacity), - ), - ), - ]), - cond( - and(eq(panelOpacityValue, 0), neq(prevPanelOpacity, 0)), - call([], proceedToNextMode), - ), - set(prevPanelOpacity, panelOpacityValue), - panelOpacityValue, - ]); - }, [modeValue, keyboardHeightValue, proceedToNextMode]); const onPrompt = mode.curMode === 'prompt'; const prevOnPromptRef = React.useRef(onPrompt); React.useEffect(() => { if (onPrompt && !prevOnPromptRef.current) { - buttonOpacity.setValue(0); - Animated.timing(buttonOpacity, { - easing: EasingNode.out(EasingNode.ease), - duration: 250, - toValue: 1.0, - }).start(); + buttonOpacity.value = withTiming(1, { + easing: Easing.out(Easing.ease), + }); } prevOnPromptRef.current = onPrompt; }, [onPrompt, buttonOpacity]); @@ -447,14 +345,14 @@ return; } prevContentHeightRef.current = curContentHeight; - contentHeight.setValue(curContentHeight); + contentHeight.value = curContentHeight; }, [curContentHeight, contentHeight]); const combinedSetMode = React.useCallback( (newMode: LoggedOutMode) => { nextModeRef.current = newMode; setMode({ curMode: newMode, nextMode: newMode }); - modeValue.setValue(modeNumbers[newMode]); + modeValue.value = newMode; }, [setMode, modeValue], ); @@ -462,10 +360,9 @@ const goBackToPrompt = React.useCallback(() => { nextModeRef.current = 'prompt'; setMode({ nextMode: 'prompt' }); - keyboardHeightValue.setValue(0); - modeValue.setValue(modeNumbers['prompt']); + modeValue.value = 'prompt'; Keyboard.dismiss(); - }, [setMode, keyboardHeightValue, modeValue]); + }, [setMode, modeValue]); const loadingCompleteRef = React.useRef(persistedStateLoaded); React.useEffect(() => { @@ -475,6 +372,22 @@ } }, [persistedStateLoaded, combinedSetMode]); + const [activeAlert, setActiveAlert] = React.useState(false); + + const navContext = React.useContext(NavContext); + const isForeground = isForegroundSelector(navContext); + + const ratchetingKeyboardHeightInput = React.useMemo( + () => ({ + ignoreKeyboardDismissal: activeAlert, + disabled: !isForeground, + }), + [activeAlert, isForeground], + ); + const keyboardHeightValue = useRatchetingKeyboardHeight( + ratchetingKeyboardHeightInput, + ); + const resetToPrompt = React.useCallback(() => { if (nextModeRef.current !== 'prompt') { goBackToPrompt(); @@ -532,20 +445,8 @@ }, [combinedSetMode]); const onPressLogIn = React.useCallback(() => { - if (Platform.OS !== 'ios') { - // For some strange reason, iOS's password management logic doesn't - // realize that the username and password fields in LogInPanel are related - // if the username field gets focused on mount. To avoid this issue we - // need the username and password fields to both appear on-screen before - // we focus the username field. However, when we set keyboardHeightValue - // to -1 here, we are telling our Reanimated logic to wait until the - // keyboard appears before showing LogInPanel. Since we need LogInPanel to - // appear before the username field is focused, we need to avoid this - // behavior on iOS. - keyboardHeightValue.setValue(-1); - } combinedSetMode('log-in'); - }, [keyboardHeightValue, combinedSetMode]); + }, [combinedSetMode]); const { navigate } = props.navigation; const onPressQRCodeSignIn = React.useCallback(() => { @@ -553,21 +454,24 @@ }, [navigate]); const onPressRegister = React.useCallback(() => { - keyboardHeightValue.setValue(-1); combinedSetMode('register'); - }, [keyboardHeightValue, combinedSetMode]); + }, [combinedSetMode]); const onPressNewRegister = React.useCallback(() => { navigate(RegistrationRouteName); }, [navigate]); + const opacityStyle = useAnimatedStyle(() => ({ + opacity: getPanelOpacity(modeValue.value, proceedToNextMode), + })); + const styles = useStyles(unboundStyles); const panel = React.useMemo(() => { if (mode.curMode === 'log-in') { return ( ); @@ -575,7 +479,7 @@ return ( ); @@ -592,7 +496,7 @@ }, [ mode.curMode, setActiveAlert, - panelOpacity, + opacityStyle, logInStateContainer, legacyRegisterStateContainer, styles.loadingIndicator, @@ -615,7 +519,7 @@ [styles.buttonText, styles.siweButtonText], ); const buttonsViewStyle = React.useMemo( - () => [styles.buttonContainer, { opacity: buttonOpacity }], + () => [styles.buttonContainer, { opacity: buttonOpacity.value }], [styles.buttonContainer, buttonOpacity], ); const buttons = React.useMemo(() => { @@ -672,7 +576,7 @@ } return ( - + {signInButtons} {registerButtons} - + ); }, [ mode.curMode, @@ -718,28 +622,36 @@ const backButtonStyle = React.useMemo( () => [ styles.backButton, - { opacity: panelOpacity, left: windowWidth < 360 ? 28 : 40 }, + opacityStyle, + { left: windowWidth < 360 ? 28 : 40 }, ], - [styles.backButton, panelOpacity, windowWidth], + [styles.backButton, opacityStyle, windowWidth], ); + const paddingTopStyle = useAnimatedStyle(() => ({ + paddingTop: getPanelPaddingTop( + modeValue.value, + keyboardHeightValue.value, + contentHeight.value, + ), + })); const animatedContentStyle = React.useMemo( - () => [styles.animationContainer, { paddingTop: panelPaddingTop }], - [styles.animationContainer, panelPaddingTop], + () => [styles.animationContainer, paddingTopStyle], + [styles.animationContainer, paddingTopStyle], ); const animatedContent = React.useMemo( () => ( - + Comm - + - + {panel} - + ), [ animatedContentStyle, diff --git a/native/account/panel-components.react.js b/native/account/panel-components.react.js --- a/native/account/panel-components.react.js +++ b/native/account/panel-components.react.js @@ -9,13 +9,12 @@ StyleSheet, ScrollView, } from 'react-native'; -import Animated from 'react-native-reanimated'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import Button from '../components/button.react.js'; import { useSelector } from '../redux/redux-utils.js'; -import type { ViewStyle } from '../types/styles.js'; +import { type ViewStyle, AnimatedView } from '../types/styles.js'; type ButtonProps = { +text: string, @@ -59,7 +58,7 @@ } type PanelProps = { - +opacityValue: Animated.Node, + +opacityStyle: ViewStyle, +children: React.Node, +style?: ViewStyle, }; @@ -68,17 +67,17 @@ const containerStyle = React.useMemo( () => [ styles.container, + props.opacityStyle, { - opacity: props.opacityValue, marginTop: dimensions.height < 641 ? 15 : 40, }, props.style, ], - [props.opacityValue, props.style, dimensions.height], + [props.opacityStyle, props.style, dimensions.height], ); return ( - {props.children} + {props.children} ); } diff --git a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js --- a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js +++ b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js @@ -545,6 +545,19 @@ declare type CancelAnimation = (animation: number) => void; + declare type AnimatedKeyboardInfo = {| + +height: SharedValue, + +state: SharedValue<0 | 1 | 2 | 3 | 4>, + |}; + declare type UseAnimatedKeyboard = (config?: {| + +isStatusBarTranslucentAndroid?: boolean, + |}) => AnimatedKeyboardInfo; + + declare type UseAnimatedReaction = ( + () => T, + (currentValue: T, previousValue: T) => mixed, + ) => void; + declare export var Node: typeof NodeImpl; declare export var Value: typeof ValueImpl; declare export var Clock: typeof ClockImpl; @@ -599,6 +612,8 @@ declare export var withTiming: WithTiming; declare export var runOnJS: RunOnJS; declare export var cancelAnimation: CancelAnimation; + declare export var useAnimatedKeyboard: UseAnimatedKeyboard; + declare export var useAnimatedReaction: UseAnimatedReaction; declare export default { +Node: typeof NodeImpl, diff --git a/native/keyboard/animated-keyboard.js b/native/keyboard/animated-keyboard.js --- a/native/keyboard/animated-keyboard.js +++ b/native/keyboard/animated-keyboard.js @@ -3,7 +3,11 @@ import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { Platform } from 'react-native'; -import Reanimated from 'react-native-reanimated'; +import { + useSharedValue, + type SharedValue, + useAnimatedReaction, +} from 'react-native-reanimated'; import { addKeyboardShowListener, @@ -14,18 +18,18 @@ import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; import type { KeyboardEvent } from '../types/react-native.js'; -const { useValue, Value } = Reanimated; - type UseKeyboardHeightParams = { +ignoreKeyboardDismissal?: ?boolean, +disabled?: ?boolean, }; -function useKeyboardHeight(params?: ?UseKeyboardHeightParams): Value { +function useKeyboardHeight( + params?: ?UseKeyboardHeightParams, +): SharedValue { const ignoreKeyboardDismissal = params?.ignoreKeyboardDismissal; const disabled = params?.disabled; - const keyboardHeightValue = useValue(0); + const keyboardHeightValue = useSharedValue(0); const dimensions = useSelector(derivedDimensionsInfoSelector); const keyboardShow = React.useCallback( @@ -44,13 +48,13 @@ 0, ), }); - keyboardHeightValue.setValue(keyboardHeight); + keyboardHeightValue.value = keyboardHeight; }, [dimensions.bottomInset, keyboardHeightValue], ); const keyboardHide = React.useCallback(() => { if (!ignoreKeyboardDismissal) { - keyboardHeightValue.setValue(0); + keyboardHeightValue.value = 0; } }, [ignoreKeyboardDismissal, keyboardHeightValue]); @@ -69,4 +73,20 @@ return keyboardHeightValue; } -export { useKeyboardHeight }; +function useRatchetingKeyboardHeight( + params?: UseKeyboardHeightParams, +): SharedValue { + const keyboardHeightValue = useKeyboardHeight(params); + const ratchetedKeyboardHeight = useSharedValue(0); + useAnimatedReaction( + () => keyboardHeightValue.value, + (currentValue, previousValue) => { + if (currentValue > previousValue || currentValue === 0) { + ratchetedKeyboardHeight.value = currentValue; + } + }, + ); + return ratchetedKeyboardHeight; +} + +export { useKeyboardHeight, useRatchetingKeyboardHeight }; diff --git a/native/utils/animation-utils.js b/native/utils/animation-utils.js --- a/native/utils/animation-utils.js +++ b/native/utils/animation-utils.js @@ -4,7 +4,6 @@ import { State as GestureState } from 'react-native-gesture-handler'; import Animated, { EasingNode, - type NodeParam, type SpringConfig, type TimingConfig, useSharedValue, @@ -18,8 +17,6 @@ block, cond, not, - and, - or, greaterThan, lessThan, eq, @@ -28,7 +25,6 @@ sub, divide, set, - max, startClock, stopClock, clockRunning, @@ -152,35 +148,6 @@ ]); } -// You provide a node that performs a "ratchet", -// and this function will call it as keyboard height increases -function ratchetAlongWithKeyboardHeight( - keyboardHeight: Node, - ratchetFunction: NodeParam, -): Node { - const prevKeyboardHeightValue = new Value(-1); - // In certain situations, iOS will send multiple keyboardShows in rapid - // succession with increasing height values. Only the final value has any - // semblance of reality. I've encountered this when using the native - // password management integration - const whenToUpdate = greaterThan( - keyboardHeight, - max(prevKeyboardHeightValue, 0), - ); - const whenToReset = and( - eq(keyboardHeight, 0), - greaterThan(prevKeyboardHeightValue, 0), - ); - return block([ - cond( - lessThan(prevKeyboardHeightValue, 0), - set(prevKeyboardHeightValue, keyboardHeight), - ), - cond(or(whenToUpdate, whenToReset), ratchetFunction), - set(prevKeyboardHeightValue, keyboardHeight), - ]); -} - function useSharedValueForBoolean(booleanValue: boolean): SharedValue { const sharedValue = useSharedValue(booleanValue); React.useEffect(() => { @@ -246,7 +213,6 @@ gestureJustEnded, runTiming, runSpring, - ratchetAlongWithKeyboardHeight, useSharedValueForBoolean, animateTowards, };