diff --git a/native/account/legacy-register-panel.react.js b/native/account/legacy-register-panel.react.js index fec6d14ba..9c1cdef75 100644 --- a/native/account/legacy-register-panel.react.js +++ b/native/account/legacy-register-panel.react.js @@ -1,521 +1,521 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View, StyleSheet, Platform, Keyboard, Linking, } from 'react-native'; -import Animated from 'react-native-reanimated'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validUsernameRegex } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import type { LegacyRegisterInfo, LegacyLogInExtraInfo, LegacyRegisterResult, LegacyLogInStartingPayload, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { TextInput } from './modal-components.react.js'; import { setNativeCredentials } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; 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, usernameTakenAlertDetails, unknownErrorAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import { type StateContainer } from '../utils/state-container.js'; type WritableLegacyRegisterState = { usernameInputText: string, passwordInputText: string, confirmPasswordInputText: string, }; export type LegacyRegisterState = $ReadOnly; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, - +opacityValue: Animated.Node, + +opacityStyle: ViewStyle, +legacyRegisterState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +legacyLogInExtraInfo: () => Promise, +dispatch: Dispatch, +dispatchActionPromise: DispatchActionPromise, +legacyRegister: ( registerInfo: LegacyRegisterInfo, ) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +confirmPasswordFocused: boolean, }; class LegacyRegisterPanel extends React.PureComponent { state: State = { confirmPasswordFocused: false, }; usernameInput: ?TextInput; passwordInput: ?TextInput; confirmPasswordInput: ?TextInput; passwordBeingAutoFilled = false; render(): React.Node { let confirmPasswordTextInputExtraProps; if ( Platform.OS !== 'ios' || this.state.confirmPasswordFocused || this.props.legacyRegisterState.state.confirmPasswordInputText.length > 0 ) { confirmPasswordTextInputExtraProps = { secureTextEntry: true, textContentType: 'password', }; } let onPasswordKeyPress; if (Platform.OS === 'ios') { onPasswordKeyPress = this.onPasswordKeyPress; } const privatePolicyNotice = ( By signing up, you agree to our{' '} Terms {' & '} Privacy Policy . ); return ( - + {privatePolicyNotice} ); } usernameInputRef = (usernameInput: ?TextInput) => { this.usernameInput = usernameInput; }; passwordInputRef = (passwordInput: ?TextInput) => { this.passwordInput = passwordInput; }; confirmPasswordInputRef = (confirmPasswordInput: ?TextInput) => { this.confirmPasswordInput = confirmPasswordInput; }; focusUsernameInput = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; focusPasswordInput = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; focusConfirmPasswordInput = () => { invariant(this.confirmPasswordInput, 'ref should be set'); this.confirmPasswordInput.focus(); }; onTermsOfUsePressed = () => { void Linking.openURL('https://comm.app/terms'); }; onPrivacyPolicyPressed = () => { void Linking.openURL('https://comm.app/privacy'); }; onChangeUsernameInputText = (text: string) => { this.props.legacyRegisterState.setState({ usernameInputText: text }); }; onChangePasswordInputText = (text: string) => { const stateUpdate: Partial = {}; stateUpdate.passwordInputText = text; if (this.passwordBeingAutoFilled) { this.passwordBeingAutoFilled = false; stateUpdate.confirmPasswordInputText = text; } this.props.legacyRegisterState.setState(stateUpdate); }; onPasswordKeyPress = (event: KeyPressEvent) => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.props.legacyRegisterState.state.confirmPasswordInputText.length === 0 ) { this.passwordBeingAutoFilled = true; } }; onChangeConfirmPasswordInputText = (text: string) => { this.props.legacyRegisterState.setState({ confirmPasswordInputText: text }); }; onConfirmPasswordFocus = () => { this.setState({ confirmPasswordFocused: true }); }; onSubmit = async () => { this.props.setActiveAlert(true); if (this.props.legacyRegisterState.state.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.legacyRegisterState.state.passwordInputText !== this.props.legacyRegisterState.state.confirmPasswordInputText ) { Alert.alert( 'Passwords don’t match', 'Password fields must contain the same password', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); } else if ( this.props.legacyRegisterState.state.usernameInputText.search( validUsernameRegex, ) === -1 ) { Alert.alert( 'Invalid username', 'Usernames must be at least six characters long, start with either a ' + 'letter or a number, and may contain only letters, numbers, or the ' + 'characters “-” and “_”', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else { Keyboard.dismiss(); const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyKeyserverRegisterActionTypes, this.legacyRegisterAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); } }; onPasswordAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.passwordInput, 'ref should exist'); this.passwordInput.focus(); }, ); }; onUsernameAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { usernameInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; async legacyRegisterAction( extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyRegister({ ...extraInfo, username: this.props.legacyRegisterState.state.usernameInputText, password: this.props.legacyRegisterState.state.passwordInputText, }); this.props.setActiveAlert(false); this.props.dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.props.legacyRegisterState.state.passwordInputText, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( usernameReservedAlertDetails.title, usernameReservedAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'username_taken') { Alert.alert( usernameTakenAlertDetails.title, usernameTakenAlertDetails.message, [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } onUnknownErrorAlertAcknowledged = () => { this.props.setActiveAlert(false); this.props.legacyRegisterState.setState( { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }, () => { invariant(this.usernameInput, 'ref should exist'); this.usernameInput.focus(); }, ); }; onAppOutOfDateAlertAcknowledged = () => { this.props.setActiveAlert(false); }; } const styles = StyleSheet.create({ container: { zIndex: 2, }, footer: { alignItems: 'stretch', flexDirection: 'row', flexShrink: 1, justifyContent: 'space-between', paddingLeft: 24, }, hyperlinkText: { color: '#036AFF', fontWeight: 'bold', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, notice: { alignSelf: 'center', display: 'flex', flexShrink: 1, maxWidth: 190, paddingBottom: 18, paddingRight: 8, paddingTop: 12, }, noticeText: { color: '#444', fontSize: 13, lineHeight: 20, textAlign: 'center', }, row: { marginHorizontal: 24, }, }); const registerLoadingStatusSelector = createLoadingStatusSelector( legacyKeyserverRegisterActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLegacyRegisterPanel: React.ComponentType = React.memo(function ConnectedLegacyRegisterPanel( props: BaseProps, ) { const registerLoadingStatus = useSelector(registerLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( registerLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const legacyLogInExtraInfo = useSelector( nativeLegacyLogInExtraInfoSelector, ); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLegacyRegisterPanel; diff --git a/native/account/log-in-panel.react.js b/native/account/log-in-panel.react.js index 26e9ba7c3..7e1057340 100644 --- a/native/account/log-in-panel.react.js +++ b/native/account/log-in-panel.react.js @@ -1,484 +1,484 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet, Keyboard, Platform } from 'react-native'; -import Animated from 'react-native-reanimated'; import { legacyLogInActionTypes, useLegacyLogIn, getOlmSessionInitializationDataActionTypes, } from 'lib/actions/user-actions.js'; import { usePasswordLogIn } from 'lib/hooks/login-hooks.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { validEmailRegex, oldValidUsernameRegex, } from 'lib/shared/account-utils.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import { type LegacyLogInInfo, type LegacyLogInExtraInfo, type LegacyLogInResult, type LegacyLogInStartingPayload, logInActionSources, } from 'lib/types/account-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { TextInput } from './modal-components.react.js'; import { fetchNativeCredentials, setNativeCredentials, } from './native-credentials.js'; import { PanelButton, Panel } from './panel-components.react.js'; import PasswordInput from './password-input.react.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; 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, unknownErrorAlertDetails, userNotFoundAlertDetails, } from '../utils/alert-messages.js'; import Alert from '../utils/alert.js'; import type { StateContainer } from '../utils/state-container.js'; export type LogInState = { +usernameInputText: ?string, +passwordInputText: ?string, }; type BaseProps = { +setActiveAlert: (activeAlert: boolean) => void, - +opacityValue: Animated.Node, + +opacityStyle: ViewStyle, +logInState: StateContainer, }; type Props = { ...BaseProps, +loadingStatus: LoadingStatus, +legacyLogInExtraInfo: () => Promise, +dispatchActionPromise: DispatchActionPromise, +legacyLogIn: (logInInfo: LegacyLogInInfo) => Promise, +identityPasswordLogIn: (username: string, password: string) => Promise, +getInitialNotificationsEncryptedMessage: () => Promise, }; type State = { +logInPending: boolean, }; class LogInPanel extends React.PureComponent { usernameInput: ?TextInput; passwordInput: ?PasswordInput; state: State = { logInPending: false }; componentDidMount() { void this.attemptToFetchCredentials(); } get usernameInputText(): string { return this.props.logInState.state.usernameInputText || ''; } get passwordInputText(): string { return this.props.logInState.state.passwordInputText || ''; } async attemptToFetchCredentials() { if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } const credentials = await fetchNativeCredentials(); if (!credentials) { return; } if ( this.props.logInState.state.usernameInputText !== null && this.props.logInState.state.usernameInputText !== undefined ) { return; } this.props.logInState.setState({ usernameInputText: credentials.username, passwordInputText: credentials.password, }); } render(): React.Node { return ( - + ); } getLoadingStatus(): LoadingStatus { if (this.props.loadingStatus === 'loading') { return 'loading'; } if (this.state.logInPending) { return 'loading'; } return 'inactive'; } usernameInputRef: (usernameInput: ?TextInput) => void = usernameInput => { this.usernameInput = usernameInput; if (Platform.OS === 'ios' && usernameInput) { setTimeout(() => usernameInput.focus()); } }; focusUsernameInput: () => void = () => { invariant(this.usernameInput, 'ref should be set'); this.usernameInput.focus(); }; passwordInputRef: (passwordInput: ?PasswordInput) => void = passwordInput => { this.passwordInput = passwordInput; }; focusPasswordInput: () => void = () => { invariant(this.passwordInput, 'ref should be set'); this.passwordInput.focus(); }; onChangeUsernameInputText: (text: string) => void = text => { this.props.logInState.setState({ usernameInputText: text.trim() }); }; onUsernameKeyPress: (event: KeyPressEvent) => void = event => { const { key } = event.nativeEvent; if ( key.length > 1 && key !== 'Backspace' && key !== 'Enter' && this.passwordInputText.length === 0 ) { this.focusPasswordInput(); } }; onChangePasswordInputText: (text: string) => void = text => { this.props.logInState.setState({ passwordInputText: text }); }; onSubmit: () => Promise = async () => { this.props.setActiveAlert(true); if (this.usernameInputText.search(validEmailRegex) > -1) { Alert.alert( 'Can’t log in with email', 'You need to log in with your username now', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.usernameInputText.search(oldValidUsernameRegex) === -1) { Alert.alert( 'Invalid username', 'Alphanumeric usernames only', [{ text: 'OK', onPress: this.onUsernameAlertAcknowledged }], { cancelable: false }, ); return; } else if (this.passwordInputText === '') { Alert.alert( 'Empty password', 'Password cannot be empty', [{ text: 'OK', onPress: this.onPasswordAlertAcknowledged }], { cancelable: false }, ); return; } Keyboard.dismiss(); if (usingCommServicesAccessToken) { await this.identityPasswordLogIn(); return; } const extraInfo = await this.props.legacyLogInExtraInfo(); const initialNotificationsEncryptedMessage = await this.props.getInitialNotificationsEncryptedMessage(); void this.props.dispatchActionPromise( legacyLogInActionTypes, this.legacyLogInAction({ ...extraInfo, initialNotificationsEncryptedMessage, }), undefined, ({ calendarQuery: extraInfo.calendarQuery }: LegacyLogInStartingPayload), ); }; async legacyLogInAction( extraInfo: LegacyLogInExtraInfo, ): Promise { try { const result = await this.props.legacyLogIn({ ...extraInfo, username: this.usernameInputText, password: this.passwordInputText, authActionSource: logInActionSources.logInFromNativeForm, }); this.props.setActiveAlert(false); await setNativeCredentials({ username: result.currentUserInfo.username, password: this.passwordInputText, }); return result; } catch (e) { if (e.message === 'invalid_credentials') { Alert.alert( userNotFoundAlertDetails.title, userNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if (e.message === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } } async identityPasswordLogIn(): Promise { if (this.state.logInPending) { return; } this.setState({ logInPending: true }); try { await this.props.identityPasswordLogIn( this.usernameInputText, this.passwordInputText, ); this.props.setActiveAlert(false); await setNativeCredentials({ username: this.usernameInputText, password: this.passwordInputText, }); } catch (e) { const messageForException = getMessageForException(e); if ( messageForException === 'user not found' || messageForException === 'login failed' ) { Alert.alert( userNotFoundAlertDetails.title, userNotFoundAlertDetails.message, [{ text: 'OK', onPress: this.onUnsuccessfulLoginAlertAckowledged }], { cancelable: false }, ); } else if ( messageForException === 'Unsupported version' || messageForException === 'client_version_unsupported' ) { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: this.onAppOutOfDateAlertAcknowledged }], { cancelable: false }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }], { cancelable: false }, ); } throw e; } finally { this.setState({ logInPending: false }); } } onUnsuccessfulLoginAlertAckowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onUsernameAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', }, this.focusUsernameInput, ); }; onPasswordAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { passwordInputText: '', }, this.focusPasswordInput, ); }; onUnknownErrorAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); this.props.logInState.setState( { usernameInputText: '', passwordInputText: '', }, this.focusUsernameInput, ); }; onAppOutOfDateAlertAcknowledged: () => void = () => { this.props.setActiveAlert(false); }; } export type InnerLogInPanel = LogInPanel; const styles = StyleSheet.create({ footer: { flexDirection: 'row', justifyContent: 'flex-end', }, icon: { bottom: 10, left: 4, position: 'absolute', }, input: { paddingLeft: 35, }, row: { marginHorizontal: 24, }, }); const logInLoadingStatusSelector = createLoadingStatusSelector( legacyLogInActionTypes, ); const olmSessionInitializationDataLoadingStatusSelector = createLoadingStatusSelector(getOlmSessionInitializationDataActionTypes); const ConnectedLogInPanel: React.ComponentType = React.memo(function ConnectedLogInPanel(props: BaseProps) { const logInLoadingStatus = useSelector(logInLoadingStatusSelector); const olmSessionInitializationDataLoadingStatus = useSelector( olmSessionInitializationDataLoadingStatusSelector, ); const loadingStatus = combineLoadingStatuses( logInLoadingStatus, olmSessionInitializationDataLoadingStatus, ); const legacyLogInExtraInfo = useSelector( nativeLegacyLogInExtraInfoSelector, ); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyLogIn = useLegacyLogIn(); const callIdentityPasswordLogIn = usePasswordLogIn(); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(authoritativeKeyserverID); return ( ); }); export default ConnectedLogInPanel; diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js index 89ab7bfd6..67709d3b0 100644 --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -1,793 +1,705 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { View, Text, TouchableOpacity, Image, Keyboard, Platform, BackHandler, ActivityIndicator, } from 'react-native'; -import Animated, { EasingNode } from 'react-native-reanimated'; +import { + Easing, + useSharedValue, + withTiming, + useAnimatedStyle, + runOnJS, +} from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { setActiveSessionRecoveryActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { cookieSelector } from 'lib/selectors/keyserver-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { recoveryFromReduxActionSources } from 'lib/types/account-types.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { splashBackgroundURI } from './background-info.js'; import FullscreenSIWEPanel from './fullscreen-siwe-panel.react.js'; import LegacyRegisterPanel from './legacy-register-panel.react.js'; import type { LegacyRegisterState } from './legacy-register-panel.react.js'; import LogInPanel from './log-in-panel.react.js'; import type { LogInState } from './log-in-panel.react.js'; import LoggedOutStaffInfo from './logged-out-staff-info.react.js'; import { enableNewRegistrationMode } from './registration/registration-types.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import KeyboardAvoidingView from '../components/keyboard-avoiding-view.react.js'; import ConnectedStatusBar from '../connected-status-bar.react.js'; -import { useKeyboardHeight } from '../keyboard/animated-keyboard.js'; +import { useRatchetingKeyboardHeight } from '../keyboard/animated-keyboard.js'; import { createIsForegroundSelector } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import type { RootNavigationProp } from '../navigation/root-navigator.react.js'; import { type NavigationRoute, LoggedOutModalRouteName, RegistrationRouteName, QRCodeSignInNavigatorRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { usePersistedStateLoaded } from '../selectors/app-state-selectors.js'; import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; import { splashStyleSelector } from '../splash.js'; import { useStyles } from '../themes/colors.js'; -import { - runTiming, - ratchetAlongWithKeyboardHeight, -} from '../utils/animation-utils.js'; +import { AnimatedView } from '../types/styles.js'; import EthereumLogo from '../vectors/ethereum-logo.react.js'; let initialAppLoad = true; const safeAreaEdges = ['top', 'bottom']; -const { - Value, - Node, - Clock, - block, - set, - call, - cond, - not, - and, - eq, - neq, - lessThan, - greaterOrEq, - add, - sub, - divide, - max, - stopClock, - clockRunning, - useValue, -} = Animated; - export type LoggedOutMode = | 'loading' | 'prompt' | 'log-in' | 'register' | 'siwe'; -const modeNumbers: { [LoggedOutMode]: number } = { - 'loading': 0, - 'prompt': 1, - 'log-in': 2, - 'register': 3, - 'siwe': 4, + +const timingConfig = { + duration: 250, + easing: Easing.out(Easing.ease), }; -function isPastPrompt(modeValue: Node) { - return and( - neq(modeValue, modeNumbers['loading']), - neq(modeValue, modeNumbers['prompt']), + +// prettier-ignore +function getPanelPaddingTop( + modeValue /*: string */, + keyboardHeightValue /*: number */, + contentHeightValue /*: number */, +) /*: number */ { + 'worklet'; + const headerHeight = Platform.OS === 'ios' ? 62.33 : 58.54; + let containerSize = headerHeight; + if (modeValue === 'loading' || modeValue === 'prompt') { + containerSize += Platform.OS === 'ios' ? 40 : 61; + } else if (modeValue === 'log-in') { + containerSize += 140; + } else if (modeValue === 'register') { + containerSize += Platform.OS === 'ios' ? 181 : 180; + } else if (modeValue === 'siwe') { + containerSize += 250; + } + + const freeSpace = contentHeightValue - keyboardHeightValue - containerSize; + const targetPanelPaddingTop = Math.max(freeSpace, 0) / 2; + return withTiming(targetPanelPaddingTop, timingConfig); +} + +// prettier-ignore +function getPanelOpacity( + modeValue /*: string */, + proceedToNextMode /*: () => void */, +) /*: number */ { + 'worklet'; + const targetPanelOpacity = + modeValue === 'loading' || modeValue === 'prompt' ? 0 : 1; + return withTiming( + targetPanelOpacity, + timingConfig, + (succeeded /*?: boolean */) => { + if (succeeded && targetPanelOpacity === 0) { + runOnJS(proceedToNextMode)(); + } + }, ); } const unboundStyles = { animationContainer: { flex: 1, }, backButton: { position: 'absolute', top: 13, }, button: { borderRadius: 4, marginBottom: 4, marginTop: 4, marginLeft: 4, marginRight: 4, paddingBottom: 14, paddingLeft: 18, paddingRight: 18, paddingTop: 14, flex: 1, }, buttonContainer: { bottom: 0, left: 0, marginLeft: 26, marginRight: 26, paddingBottom: 20, position: 'absolute', right: 0, }, buttonText: { fontFamily: 'OpenSans-Semibold', fontSize: 17, textAlign: 'center', }, classicAuthButton: { backgroundColor: 'purpleButton', }, classicAuthButtonText: { color: 'whiteText', }, registerButtons: { flexDirection: 'row', }, signInButtons: { flexDirection: 'row', }, container: { backgroundColor: 'transparent', flex: 1, }, header: { color: 'white', fontFamily: Platform.OS === 'ios' ? 'IBMPlexSans' : 'IBMPlexSans-Medium', fontSize: 56, fontWeight: '500', lineHeight: 66, textAlign: 'center', }, loadingIndicator: { paddingTop: 15, }, modalBackground: { bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, }, siweButton: { backgroundColor: 'siweButton', flex: 1, flexDirection: 'row', justifyContent: 'center', }, siweButtonText: { color: 'siweButtonText', }, siweOr: { flex: 1, flexDirection: 'row', marginBottom: 18, marginTop: 14, }, siweOrLeftHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginRight: 18, marginTop: 10, }, siweOrRightHR: { borderColor: 'logInSpacer', borderTopWidth: 1, flex: 1, marginLeft: 18, marginTop: 10, }, siweOrText: { color: 'whiteText', fontSize: 17, textAlign: 'center', }, siweIcon: { paddingRight: 10, }, }; const isForegroundSelector = createIsForegroundSelector( LoggedOutModalRouteName, ); const backgroundSource = { uri: splashBackgroundURI }; const initialLogInState = { usernameInputText: null, passwordInputText: null, }; const initialLegacyRegisterState = { usernameInputText: '', passwordInputText: '', confirmPasswordInputText: '', }; type Mode = { +curMode: LoggedOutMode, +nextMode: LoggedOutMode, }; type Props = { +navigation: RootNavigationProp<'LoggedOutModal'>, +route: NavigationRoute<'LoggedOutModal'>, }; function LoggedOutModal(props: Props) { const mountedRef = React.useRef(false); React.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const [logInState, baseSetLogInState] = React.useState(initialLogInState); const setLogInState = React.useCallback( (newLogInState: Partial) => { if (!mountedRef.current) { return; } baseSetLogInState(prevLogInState => ({ ...prevLogInState, ...newLogInState, })); }, [], ); const logInStateContainer = React.useMemo( () => ({ state: logInState, setState: setLogInState, }), [logInState, setLogInState], ); const [legacyRegisterState, baseSetLegacyRegisterState] = React.useState(initialLegacyRegisterState); const setLegacyRegisterState = React.useCallback( (newLegacyRegisterState: Partial) => { if (!mountedRef.current) { return; } baseSetLegacyRegisterState(prevLegacyRegisterState => ({ ...prevLegacyRegisterState, ...newLegacyRegisterState, })); }, [], ); const legacyRegisterStateContainer = React.useMemo( () => ({ state: legacyRegisterState, setState: setLegacyRegisterState, }), [legacyRegisterState, setLegacyRegisterState], ); const persistedStateLoaded = usePersistedStateLoaded(); const initialMode = persistedStateLoaded ? 'prompt' : 'loading'; const [mode, baseSetMode] = React.useState(() => ({ curMode: initialMode, nextMode: initialMode, })); const setMode = React.useCallback((newMode: Partial) => { if (!mountedRef.current) { return; } baseSetMode(prevMode => ({ ...prevMode, ...newMode, })); }, []); 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]); const curContentHeight = dimensions.safeAreaHeight; const prevContentHeightRef = React.useRef(curContentHeight); React.useEffect(() => { if (curContentHeight === prevContentHeightRef.current) { 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], ); 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(() => { if (!loadingCompleteRef.current && persistedStateLoaded) { combinedSetMode('prompt'); loadingCompleteRef.current = true; } }, [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(); return true; } return false; }, [goBackToPrompt]); React.useEffect(() => { if (!isForeground) { return undefined; } BackHandler.addEventListener('hardwareBackPress', resetToPrompt); return () => { BackHandler.removeEventListener('hardwareBackPress', resetToPrompt); }; }, [isForeground, resetToPrompt]); const rehydrateConcluded = useSelector( state => !!(state._persist && state._persist.rehydrated && navContext), ); const cookie = useSelector(cookieSelector(authoritativeKeyserverID)); const loggedIn = useSelector(isLoggedIn); const dispatch = useDispatch(); React.useEffect(() => { // This gets triggered when an app is killed and restarted // Not when it is returned from being backgrounded if (!initialAppLoad || !rehydrateConcluded) { return; } initialAppLoad = false; if (usingCommServicesAccessToken || __DEV__) { return; } const hasUserCookie = cookie && cookie.startsWith('user='); if (loggedIn === !!hasUserCookie) { return; } const actionSource = loggedIn ? recoveryFromReduxActionSources.appStartReduxLoggedInButInvalidCookie : recoveryFromReduxActionSources.appStartCookieLoggedInButInvalidRedux; dispatch({ type: setActiveSessionRecoveryActionType, payload: { activeSessionRecovery: actionSource, keyserverID: authoritativeKeyserverID, }, }); }, [rehydrateConcluded, loggedIn, cookie, dispatch]); const onPressSIWE = React.useCallback(() => { combinedSetMode('siwe'); }, [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(() => { navigate(QRCodeSignInNavigatorRouteName); }, [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 ( ); } else if (mode.curMode === 'register') { return ( ); } else if (mode.curMode === 'loading') { return ( ); } return null; }, [ mode.curMode, setActiveAlert, - panelOpacity, + opacityStyle, logInStateContainer, legacyRegisterStateContainer, styles.loadingIndicator, ]); const classicAuthButtonStyle = React.useMemo( () => [styles.button, styles.classicAuthButton], [styles.button, styles.classicAuthButton], ); const classicAuthButtonTextStyle = React.useMemo( () => [styles.buttonText, styles.classicAuthButtonText], [styles.buttonText, styles.classicAuthButtonText], ); const siweAuthButtonStyle = React.useMemo( () => [styles.button, styles.siweButton], [styles.button, styles.siweButton], ); const siweAuthButtonTextStyle = React.useMemo( () => [styles.buttonText, styles.siweButtonText], [styles.buttonText, styles.siweButtonText], ); const buttonsViewStyle = React.useMemo( - () => [styles.buttonContainer, { opacity: buttonOpacity }], + () => [styles.buttonContainer, { opacity: buttonOpacity.value }], [styles.buttonContainer, buttonOpacity], ); const buttons = React.useMemo(() => { if (mode.curMode !== 'prompt') { return null; } const registerButtons = []; registerButtons.push( Register , ); if (enableNewRegistrationMode) { registerButtons.push( Register (new) , ); } const signInButtons = []; signInButtons.push( Sign in , ); if (__DEV__) { signInButtons.push( Sign in (QR) , ); } return ( - + Sign in with Ethereum or {signInButtons} {registerButtons} - + ); }, [ mode.curMode, onPressRegister, onPressNewRegister, onPressLogIn, onPressQRCodeSignIn, onPressSIWE, classicAuthButtonStyle, classicAuthButtonTextStyle, siweAuthButtonStyle, siweAuthButtonTextStyle, buttonsViewStyle, styles.siweIcon, styles.siweOr, styles.siweOrLeftHR, styles.siweOrText, styles.siweOrRightHR, styles.signInButtons, styles.registerButtons, ]); const windowWidth = dimensions.width; 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, styles.header, backButtonStyle, resetToPrompt, panel, ], ); const curModeIsSIWE = mode.curMode === 'siwe'; const nextModeIsPrompt = mode.nextMode === 'prompt'; const siwePanel = React.useMemo(() => { if (!curModeIsSIWE) { return null; } return ( ); }, [curModeIsSIWE, goBackToPrompt, nextModeIsPrompt]); const splashStyle = useSelector(splashStyleSelector); const backgroundStyle = React.useMemo( () => [styles.modalBackground, splashStyle], [styles.modalBackground, splashStyle], ); return React.useMemo( () => ( <> {animatedContent} {buttons} {siwePanel} ), [backgroundStyle, styles.container, animatedContent, buttons, siwePanel], ); } const MemoizedLoggedOutModal: React.ComponentType = React.memo(LoggedOutModal); export default MemoizedLoggedOutModal; diff --git a/native/account/panel-components.react.js b/native/account/panel-components.react.js index a9072a904..07f282053 100644 --- a/native/account/panel-components.react.js +++ b/native/account/panel-components.react.js @@ -1,128 +1,127 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import * as React from 'react'; import { View, ActivityIndicator, Text, 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, +loadingStatus: LoadingStatus, +onSubmit: () => mixed, +disabled?: boolean, }; function PanelButton(props: ButtonProps): React.Node { let buttonIcon; if (props.loadingStatus === 'loading') { buttonIcon = ( ); } else { buttonIcon = ( ); } return ( ); } type PanelProps = { - +opacityValue: Animated.Node, + +opacityStyle: ViewStyle, +children: React.Node, +style?: ViewStyle, }; function Panel(props: PanelProps): React.Node { const dimensions = useSelector(state => state.dimensions); 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} ); } const styles = StyleSheet.create({ container: { backgroundColor: '#FFFFFFAA', borderRadius: 6, marginLeft: 20, marginRight: 20, paddingTop: 6, }, innerSubmitButton: { alignItems: 'flex-end', flexDirection: 'row', paddingVertical: 6, }, loadingIndicatorContainer: { paddingBottom: 2, width: 14, }, submitButton: { borderBottomRightRadius: 6, justifyContent: 'center', paddingLeft: 10, paddingRight: 18, }, submitButtonHorizontalContainer: { alignSelf: 'flex-end', }, submitButtonVerticalContainer: { flexGrow: 1, justifyContent: 'center', }, submitContentIconContainer: { paddingBottom: 5, width: 14, }, submitContentText: { color: '#555', fontFamily: 'OpenSans-Semibold', fontSize: 18, paddingRight: 7, }, }); export { PanelButton, Panel }; 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 index 15aad3879..e5c992c48 100644 --- a/native/flow-typed/npm/react-native-reanimated_v2.x.x.js +++ b/native/flow-typed/npm/react-native-reanimated_v2.x.x.js @@ -1,661 +1,676 @@ // flow-typed signature: 3742390ed7eeeb6c96844c62149ea639 // flow-typed version: <>/react-native-reanimated_v2.2.0/flow_v0.137.0 /** * This is an autogenerated libdef stub for: * * 'react-native-reanimated' * * Fill this stub out by replacing all the `any` types. * * Once filled out, we encourage you to share your work with the * community by sending a pull request to: * https://github.com/flowtype/flow-typed */ declare module 'react-native-reanimated' { // This was taken from the flow typed library definitions of bottom-tabs_v6 declare type StyleObj = | null | void | number | false | '' | $ReadOnlyArray | { [name: string]: any, ... }; declare type ViewStyleProp = StyleObj; declare type TextStyleProp = StyleObj; declare type StyleProps = {| ...ViewStyleProp, ...TextStyleProp, +originX?: number, +originY?: number, +[key: string]: any, |}; declare class NodeImpl { } declare class ValueImpl extends NodeImpl { constructor(val: number): this; setValue(num: number): void; } declare class ClockImpl extends NodeImpl { } declare class ViewImpl extends React$Component<{ ... }> { } declare class TextImpl extends React$Component<{ ... }> { } declare class ImageImpl extends React$Component<{ ... }> { } declare class CodeImpl extends React$Component<{ +exec: NodeImpl, ... }> { } declare type NodeOrNum = NodeImpl | number; declare export type NodeParam = NodeOrNum | $ReadOnlyArray; declare type NodeOrArrayOfNodes = NodeImpl | $ReadOnlyArray; declare export type Block = ( nodes: $ReadOnlyArray, ) => NodeImpl; declare export type Set = (node: ValueImpl, val: NodeParam) => NodeImpl; declare type ToNumber = (val: mixed) => number; declare export type Call = >( nodes: N, callback: (vals: $TupleMap) => mixed, ) => NodeImpl; declare export type Cond = ( cond: NodeParam, branch1: ?NodeParam, branch2?: ?NodeParam, ) => NodeImpl; declare export type Not = NodeImpl => NodeImpl; declare export type And = (...$ReadOnlyArray) => NodeImpl; declare export type Or = (...$ReadOnlyArray) => NodeImpl; declare export type Eq = (NodeParam, NodeParam) => NodeImpl; declare export type Neq = (NodeParam, NodeParam) => NodeImpl; declare export type LessThan = (NodeParam, NodeParam) => NodeImpl; declare export type GreaterThan = (NodeParam, NodeParam) => NodeImpl; declare export type LessOrEq = (NodeParam, NodeParam) => NodeImpl; declare export type GreaterOrEq = (NodeParam, NodeParam) => NodeImpl; declare export type Add = (...$ReadOnlyArray) => NodeImpl; declare export type Sub = (...$ReadOnlyArray) => NodeImpl; declare export type Multiply = (...$ReadOnlyArray) => NodeImpl; declare export type Divide = (...$ReadOnlyArray) => NodeImpl; declare export type Pow = (...$ReadOnlyArray) => NodeImpl; declare export type Max = (NodeParam, NodeParam) => NodeImpl; declare export type Min = (NodeParam, NodeParam) => NodeImpl; declare export type Abs = (NodeParam) => NodeImpl; declare export type Ceil = (NodeParam) => NodeImpl; declare export type Floor = (NodeParam) => NodeImpl; declare export type Round = (NodeParam) => NodeImpl; declare export type StartClock = ClockImpl => NodeImpl; declare export type StopClock = ClockImpl => NodeImpl; declare export type ClockRunning = ClockImpl => NodeImpl; declare export type Debug = (string, NodeParam) => NodeImpl; declare type AnimationCallback = ( finished?: boolean, current?: AnimatableValue ) => mixed; declare type Animatable = number | string | Array; declare type AnimatableValueObject = { +[key: string]: Animatable }; declare export type AnimatableValue = Animatable | AnimatableValueObject; declare type ExtrapolateType = { ... }; declare type ExtrapolateModule = { +CLAMP: ExtrapolateType, ... }; declare export type InterpolationConfig = { +inputRange: $ReadOnlyArray, +outputRange: $ReadOnlyArray, +extrapolate?: ?ExtrapolateType, ... }; declare export type InterpolateNode = ( node: NodeParam, interpolationConfig: InterpolationConfig, ) => NodeImpl; declare export type InterpolateColorsConfig = { +inputRange: $ReadOnlyArray, +outputColorRange: $ReadOnlyArray, }; declare export type InterpolateColors = ( animationValue: NodeParam, interpolationConfig: InterpolateColorsConfig ) => NodeImpl; declare export type Interpolate = ( input: number, inputRange: $ReadOnlyArray, outputRange: $ReadOnlyArray, extrapolate?: ?ExtrapolateType, ) => number; declare export type InterpolateColorConfig = $Shape<{ +gamma: number, +useCorrectedHSVInterpolation: boolean, }>; declare export type InterpolateColor = ( input: number, inputRange: $ReadOnlyArray, outputRange: $ReadOnlyArray, colorSpace?: 'RGB' | 'HSV', interpolateColorConfig?: InterpolateColorConfig, ) => T; declare type EasingType = { ... }; declare type EasingNodeModule = { +ease: EasingType, +quad: EasingType, +in: EasingType => EasingType, +out: EasingType => EasingType, +inOut: EasingType => EasingType, ... }; declare export var EasingNode: EasingNodeModule; declare type EasingFn = (t: number) => number; declare type EasingFnFactory = { +factory: () => EasingFn }; declare type EasingModule = { +ease: EasingFn, +quad: EasingFn, +in: EasingFn => EasingFn, +out: EasingFn => EasingFn, +inOut: EasingFn => EasingFn, ... }; declare export var Easing: EasingModule; declare export type TimingState = { +finished: ValueImpl, +position: ValueImpl, +frameTime: ValueImpl, +time: ValueImpl, ... }; declare export type TimingConfig = { +duration: number, +toValue: NodeOrNum, +easing?: ?EasingType, ... }; declare type Animator = { +start: () => void, ... }; declare type Timing = {| ( value: ValueImpl, config: TimingConfig, ): Animator, ( clock: ClockImpl, state: TimingState, config: TimingConfig, ): NodeImpl, |}; declare export type SpringConfig = { +overshootClamping: boolean, +damping: number, +mass: number, +toValue: NodeOrNum, ... }; declare type SpringUtilsModule = { +makeDefaultConfig: () => SpringConfig, +makeConfigFromBouncinessAndSpeed: ({ ...SpringConfig, +bounciness: ?number, +speed: ?number, ... }) => SpringConfig, ... }; declare export type SpringState = { +finished: ValueImpl, +position: ValueImpl, +velocity: ValueImpl, +time: ValueImpl, ... }; declare type Spring = {| ( value: ValueImpl, config: SpringConfig, ): Animator, ( clock: ClockImpl, state: SpringState, config: SpringConfig, ): NodeImpl, |}; declare export type DecayConfig = { +deceleration: number, ... }; declare export type DecayState = { +finished: ValueImpl, +position: ValueImpl, +velocity: ValueImpl, +time: ValueImpl, ... }; declare type Decay = {| ( value: ValueImpl, config: DecayConfig, ): Animator, ( clock: ClockImpl, state: DecayState, config: DecayConfig, ): NodeImpl, |}; declare type LayoutAnimation = {| +initialValues: StyleProps, +animations: StyleProps, +callback?: (finished: boolean) => void, |}; declare type AnimationFunction = (a?: any, b?: any, c?: any) => any; declare type EntryAnimationsValues = {| +targetOriginX: number, +targetOriginY: number, +targetWidth: number, +targetHeight: number, +targetGlobalOriginX: number, +targetGlobalOriginY: number, |}; declare type ExitAnimationsValues = {| +currentOriginX: number, +currentOriginY: number, +currentWidth: number, +currentHeight: number, +currentGlobalOriginX: number, +currentGlobalOriginY: number, |}; declare export type EntryExitAnimationFunction = ( targetValues: EntryAnimationsValues | ExitAnimationsValues, ) => LayoutAnimation; declare type AnimationConfigFunction = ( targetValues: T, ) => LayoutAnimation; declare type LayoutAnimationsValues = {| +currentOriginX: number, +currentOriginY: number, +currentWidth: number, +currentHeight: number, +currentGlobalOriginX: number, +currentGlobalOriginY: number, +targetOriginX: number, +targetOriginY: number, +targetWidth: number, +targetHeight: number, +targetGlobalOriginX: number, +argetGlobalOriginY: number, +windowWidth: number, +windowHeight: number, |}; declare type LayoutAnimationFunction = ( targetValues: LayoutAnimationsValues, ) => LayoutAnimation; declare type BaseLayoutAnimationConfig = {| +duration?: number, +easing?: EasingFn, +type?: AnimationFunction, +damping?: number, +mass?: number, +stiffness?: number, +overshootClamping?: number, +restDisplacementThreshold?: number, +restSpeedThreshold?: number, |}; declare type BaseBuilderAnimationConfig = {| ...BaseLayoutAnimationConfig, rotate?: number | string, |}; declare type LayoutAnimationAndConfig = [ AnimationFunction, BaseBuilderAnimationConfig, ]; declare export class BaseAnimationBuilder { static duration(durationMs: number): BaseAnimationBuilder; duration(durationMs: number): BaseAnimationBuilder; static delay(delayMs: number): BaseAnimationBuilder; delay(delayMs: number): BaseAnimationBuilder; static withCallback( callback: (finished: boolean) => void, ): BaseAnimationBuilder; withCallback(callback: (finished: boolean) => void): BaseAnimationBuilder; static getDuration(): number; getDuration(): number; static randomDelay(): BaseAnimationBuilder; randomDelay(): BaseAnimationBuilder; getDelay(): number; getDelayFunction(): AnimationFunction; static build(): EntryExitAnimationFunction | LayoutAnimationFunction; } declare export type ReanimatedAnimationBuilder = | Class | BaseAnimationBuilder; declare export class ComplexAnimationBuilder extends BaseAnimationBuilder { static easing(easingFunction: EasingFn): ComplexAnimationBuilder; easing(easingFunction: EasingFn): ComplexAnimationBuilder; static rotate(degree: string): ComplexAnimationBuilder; rotate(degree: string): ComplexAnimationBuilder; static springify(): ComplexAnimationBuilder; springify(): ComplexAnimationBuilder; static damping(damping: number): ComplexAnimationBuilder; damping(damping: number): ComplexAnimationBuilder; static mass(mass: number): ComplexAnimationBuilder; mass(mass: number): ComplexAnimationBuilder; static stiffness(stiffness: number): ComplexAnimationBuilder; stiffness(stiffness: number): ComplexAnimationBuilder; static overshootClamping( overshootClamping: number, ): ComplexAnimationBuilder; overshootClamping(overshootClamping: number): ComplexAnimationBuilder; static restDisplacementThreshold( restDisplacementThreshold: number, ): ComplexAnimationBuilder; restDisplacementThreshold( restDisplacementThreshold: number, ): ComplexAnimationBuilder; static restSpeedThreshold( restSpeedThreshold: number, ): ComplexAnimationBuilder; restSpeedThreshold(restSpeedThreshold: number): ComplexAnimationBuilder; static withInitialValues(values: StyleProps): BaseAnimationBuilder; withInitialValues(values: StyleProps): BaseAnimationBuilder; getAnimationAndConfig(): LayoutAnimationAndConfig; } declare export class SlideInDown extends ComplexAnimationBuilder { static createInstance(): SlideInDown; build(): AnimationConfigFunction; } declare export class SlideOutDown extends ComplexAnimationBuilder { static createInstance(): SlideOutDown; build(): AnimationConfigFunction; } declare export class FadeInDown extends ComplexAnimationBuilder { static createInstance(): FadeInDown; build(): AnimationConfigFunction; } declare export class FadeOutDown extends ComplexAnimationBuilder { static createInstance(): FadeOutDown; build(): AnimationConfigFunction; } declare type $SyntheticEvent = { +nativeEvent: $ReadOnly<$Exact>, ... }; declare type GestureStateUndetermined = 0; declare type GestureStateFailed = 1; declare type GestureStateBegan = 2; declare type GestureStateCancelled = 3; declare type GestureStateActive = 4; declare type GestureStateEnd = 5; declare type GestureState = | GestureStateUndetermined | GestureStateFailed | GestureStateBegan | GestureStateCancelled | GestureStateActive | GestureStateEnd; declare export type $Event = { handlerTag: number, numberOfPointers: number, state: GestureState, oldState: GestureState, ...$Exact, ... }; declare export type EventResult = $Event> = $SyntheticEvent => void; declare type ToValue = (val: mixed) => ValueImpl; declare type Event = = $Event>( defs: $ReadOnlyArray<{ +nativeEvent: $Shape<$ObjMap>, ... }>, ) => EventResult; declare type UseValue = (initialVal: number) => ValueImpl; declare type AnimatedGestureHandlerEventCallback> = ( event: $Shape, context: {| [name: string]: mixed |}, ) => mixed; declare type UseAnimatedGestureHandler = = $Event>( callbacks: $Shape<{| +onStart: AnimatedGestureHandlerEventCallback, +onActive: AnimatedGestureHandlerEventCallback, +onEnd: AnimatedGestureHandlerEventCallback, +onFail: AnimatedGestureHandlerEventCallback, +onCancel: AnimatedGestureHandlerEventCallback, +onFinish: AnimatedGestureHandlerEventCallback, |}>, dependencies?: $ReadOnlyArray, ) => $SyntheticEvent => mixed; declare export type SharedValue = { value: T, ... }; declare type UseSharedValue = (val: T) => SharedValue; declare type UseDerivedValue = ( updater: () => T, dependencies?: $ReadOnlyArray, ) => SharedValue; declare type UseAnimatedStyle = ( styleSelector: () => T, dependencies?: $ReadOnlyArray, ) => T; declare export type WithSpringConfig = $Shape<{| +stiffness: number, +damping: number, +mass: number, +overshootClamping: boolean, +restDisplacementThreshold: number, +restSpeedThreshold: number, +velocity: number, |}>; // Doesn't actually return a number, but sharedValue.value has a differently // typed getter vs. setter, and Flow doesn't support that declare type WithSpring = ( toValue: number | string, springConfig?: WithSpringConfig, ) => number; declare type WithTimingConfig = $Shape<{ +duration: number, +easing: EasingFn | EasingFnFactory, }>; declare type WithTiming = ( toValue: T, timingConfig?: WithTimingConfig, callback?: AnimationCallback, ) => T; declare type RunOnJS = (func: F) => F; 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; declare export var View: typeof ViewImpl; declare export var Text: typeof TextImpl; declare export var Image: typeof ImageImpl; declare export var Code: typeof CodeImpl; declare export var block: Block; declare export var set: Set; declare export var call: Call; declare export var cond: Cond; declare export var not: Not; declare export var and: And; declare export var or: Or; declare export var eq: Eq; declare export var neq: Neq; declare export var lessThan: LessThan; declare export var greaterThan: GreaterThan; declare export var lessOrEq: LessOrEq; declare export var greaterOrEq: GreaterOrEq; declare export var add: Add; declare export var sub: Sub; declare export var multiply: Multiply; declare export var divide: Divide; declare export var pow: Pow; declare export var max: Max; declare export var min: Min; declare export var abs: Abs; declare export var ceil: Ceil; declare export var floor: Floor; declare export var round: Round; declare export var startClock: StartClock; declare export var stopClock: StopClock; declare export var clockRunning: ClockRunning; declare export var debug: Debug; declare export var interpolateNode: InterpolateNode; declare export var interpolateColors: InterpolateColors; declare export var interpolate: Interpolate; declare export var interpolateColor: InterpolateColor; declare export var Extrapolate: ExtrapolateModule; declare export var timing: Timing; declare export var SpringUtils: SpringUtilsModule; declare export var spring: Spring; declare export var decay: Decay; declare export var event: Event; declare export var useValue: UseValue; declare export var useAnimatedGestureHandler: UseAnimatedGestureHandler; declare export var useSharedValue: UseSharedValue; declare export var useDerivedValue: UseDerivedValue; declare export var useAnimatedStyle: UseAnimatedStyle; declare export var withSpring: WithSpring; 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, +Value: typeof ValueImpl, +Clock: typeof ClockImpl, +View: typeof ViewImpl, +Text: typeof TextImpl, +Image: typeof ImageImpl, +Code: typeof CodeImpl, +block: Block, +set: Set, +call: Call, +cond: Cond, +not: Not, +and: And, +or: Or, +eq: Eq, +neq: Neq, +lessThan: LessThan, +greaterThan: GreaterThan, +lessOrEq: LessOrEq, +greaterOrEq: GreaterOrEq, +add: Add, +sub: Sub, +multiply: Multiply, +divide: Divide, +pow: Pow, +max: Max, +min: Min, +abs: Abs, +ceil: Ceil, +floor: Floor, +round: Round, +startClock: StartClock, +stopClock: StopClock, +clockRunning: ClockRunning, +debug: Debug, +interpolateNode: InterpolateNode, +interpolateColors: InterpolateColors, +interpolate: Interpolate, +interpolateColor: InterpolateColor, +Extrapolate: ExtrapolateModule, +timing: Timing, +spring: Spring, +decay: Decay, +SpringUtils: SpringUtilsModule, +event: Event, +useValue: UseValue, +useAnimatedGestureHandler: UseAnimatedGestureHandler, +useSharedValue: UseSharedValue, +useDerivedValue: UseDerivedValue, +useAnimatedStyle: UseAnimatedStyle, +withSpring: WithSpring, +withTiming: WithTiming, +runOnJS: RunOnJS, +cancelAnimation: CancelAnimation, ... }; } diff --git a/native/keyboard/animated-keyboard.js b/native/keyboard/animated-keyboard.js index 47734d6f8..2ef9cf7fd 100644 --- a/native/keyboard/animated-keyboard.js +++ b/native/keyboard/animated-keyboard.js @@ -1,72 +1,92 @@ // @flow 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, addKeyboardDismissListener, removeKeyboardListener, } from './keyboard.js'; import { useSelector } from '../redux/redux-utils.js'; 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( (event: KeyboardEvent) => { if ( event.startCoordinates && _isEqual(event.startCoordinates)(event.endCoordinates) ) { return; } const keyboardHeight: number = Platform.select({ // Android doesn't include the bottomInset in this height measurement android: event.endCoordinates.height, default: Math.max( event.endCoordinates.height - dimensions.bottomInset, 0, ), }); - keyboardHeightValue.setValue(keyboardHeight); + keyboardHeightValue.value = keyboardHeight; }, [dimensions.bottomInset, keyboardHeightValue], ); const keyboardHide = React.useCallback(() => { if (!ignoreKeyboardDismissal) { - keyboardHeightValue.setValue(0); + keyboardHeightValue.value = 0; } }, [ignoreKeyboardDismissal, keyboardHeightValue]); React.useEffect(() => { if (disabled) { return undefined; } const keyboardShowListener = addKeyboardShowListener(keyboardShow); const keyboardHideListener = addKeyboardDismissListener(keyboardHide); return () => { removeKeyboardListener(keyboardShowListener); removeKeyboardListener(keyboardHideListener); }; }, [disabled, keyboardShow, keyboardHide]); 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 index c00a6bc88..121cf9f8e 100644 --- a/native/utils/animation-utils.js +++ b/native/utils/animation-utils.js @@ -1,252 +1,218 @@ // @flow import * as React from 'react'; import { State as GestureState } from 'react-native-gesture-handler'; import Animated, { EasingNode, - type NodeParam, type SpringConfig, type TimingConfig, useSharedValue, type SharedValue, } from 'react-native-reanimated'; const { Clock, Node, Value, block, cond, not, - and, - or, greaterThan, lessThan, eq, neq, add, sub, divide, set, - max, startClock, stopClock, clockRunning, timing, spring, SpringUtils, } = Animated; function clamp( value: Node, minValue: Node | number, maxValue: Node | number, ): Node { return cond( greaterThan(value, maxValue), maxValue, cond(greaterThan(minValue, value), minValue, value), ); } function delta(value: Node): Node { const prevValue = new Value(0); const deltaValue = new Value(0); return block([ set(deltaValue, cond(eq(prevValue, 0), 0, sub(value, prevValue))), set(prevValue, value), deltaValue, ]); } function gestureJustStarted(state: Node): Node { const prevValue = new Value(-1); return cond(eq(prevValue, state), 0, [ set(prevValue, state), eq(state, GestureState.ACTIVE), ]); } function gestureJustEnded(state: Node): Node { const prevValue = new Value(-1); return cond(eq(prevValue, state), 0, [ set(prevValue, state), eq(state, GestureState.END), ]); } const defaultTimingConfig = { duration: 250, easing: EasingNode.out(EasingNode.ease), }; function runTiming( clock: Clock, initialValue: Node | number, finalValue: Node | number, startStopClock: boolean = true, config?: Partial, ): Node { const state = { finished: new Value(0), position: new Value(0), frameTime: new Value(0), time: new Value(0), }; const timingConfig = { ...defaultTimingConfig, ...config, toValue: new Value(0), }; return block([ cond(not(clockRunning(clock)), [ set(state.finished, 0), set(state.frameTime, 0), set(state.time, 0), set(state.position, initialValue), set(timingConfig.toValue, finalValue), startStopClock ? startClock(clock) : undefined, ]), timing(clock, state, timingConfig), cond(state.finished, startStopClock ? stopClock(clock) : undefined), state.position, ]); } const defaultSpringConfig = SpringUtils.makeDefaultConfig(); type SpringAnimationInitialState = Partial<{ +velocity: Value | number, }>; function runSpring( clock: Clock, initialValue: Node | number, finalValue: Node | number, startStopClock: boolean = true, config?: Partial, initialState?: SpringAnimationInitialState, ): Node { const state = { finished: new Value(0), position: new Value(0), velocity: new Value(0), time: new Value(0), }; const springConfig = { ...defaultSpringConfig, ...config, toValue: new Value(0), }; return block([ cond(not(clockRunning(clock)), [ set(state.finished, 0), set(state.velocity, initialState?.velocity ?? 0), set(state.time, 0), set(state.position, initialValue), set(springConfig.toValue, finalValue), startStopClock ? startClock(clock) : undefined, ]), spring(clock, state, springConfig), cond(state.finished, startStopClock ? stopClock(clock) : undefined), state.position, ]); } -// 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(() => { sharedValue.value = booleanValue; }, [sharedValue, booleanValue]); return sharedValue; } // Target can be either 0 or 1. Caller handles interpolating function animateTowards( target: Node, fullAnimationLength: number, // in ms ): Node { const curValue = new Value(-1); const prevTarget = new Value(-1); const clock = new Clock(); const prevClockValue = new Value(0); const curDeltaClockValue = new Value(0); const deltaClockValue = [ set( curDeltaClockValue, cond(eq(prevClockValue, 0), 0, sub(clock, prevClockValue)), ), set(prevClockValue, clock), curDeltaClockValue, ]; const progressPerFrame = divide(deltaClockValue, fullAnimationLength); return block([ [ cond(eq(curValue, -1), set(curValue, target)), cond(eq(prevTarget, -1), set(prevTarget, target)), ], cond(neq(target, prevTarget), [stopClock(clock), set(prevTarget, target)]), cond(neq(curValue, target), [ cond(not(clockRunning(clock)), [ set(prevClockValue, 0), startClock(clock), ]), set( curValue, cond( eq(target, 1), add(curValue, progressPerFrame), sub(curValue, progressPerFrame), ), ), ]), [ cond(greaterThan(curValue, 1), set(curValue, 1)), cond(lessThan(curValue, 0), set(curValue, 0)), ], cond(eq(curValue, target), [stopClock(clock)]), curValue, ]); } export { clamp, delta, gestureJustStarted, gestureJustEnded, runTiming, runSpring, - ratchetAlongWithKeyboardHeight, useSharedValueForBoolean, animateTowards, };