diff --git a/native/.babelrc.cjs b/native/.babelrc.cjs index 83b1d37c3..f1a4a9bf1 100644 --- a/native/.babelrc.cjs +++ b/native/.babelrc.cjs @@ -1,19 +1,24 @@ module.exports = { presets: ['module:metro-react-native-babel-preset'], plugins: [ 'transform-remove-strict-mode', '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', ['@babel/plugin-transform-private-methods', { loose: true }], '@babel/plugin-transform-numeric-separator', 'babel-plugin-transform-bigint', '@babel/plugin-transform-named-capturing-groups-regex', // react-native-reanimated must be last - 'react-native-reanimated/plugin', + [ + 'react-native-reanimated/plugin', + { + extraPresets: ['@babel/preset-flow'], + }, + ], ], env: { production: { plugins: ['transform-remove-console'], }, }, }; diff --git a/native/account/logged-out-modal.react.js b/native/account/logged-out-modal.react.js index cc28cbbc0..86f89e254 100644 --- a/native/account/logged-out-modal.react.js +++ b/native/account/logged-out-modal.react.js @@ -1,621 +1,619 @@ // @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 { Easing, useSharedValue, withTiming, useAnimatedStyle, runOnJS, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useIsLoggedInToAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { setActiveSessionRecoveryActionType } from 'lib/keyserver-conn/keyserver-conn-types.js'; import { usePersistedStateLoaded } from 'lib/selectors/app-state-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, usingRestoreFlow, } from 'lib/utils/services-utils.js'; import { splashBackgroundURI } from './background-info.js'; import FullscreenSIWEPanel from './fullscreen-siwe-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 PromptButton from './prompt-button.react.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 { 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, SignInNavigatorRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { derivedDimensionsInfoSelector } from '../selectors/dimensions-selectors.js'; import { splashStyleSelector } from '../splash.js'; import { useStyles } from '../themes/colors.js'; import { AnimatedView } from '../types/styles.js'; let initialAppLoad = true; const safeAreaEdges = ['top', 'bottom']; export type LoggedOutMode = 'loading' | 'prompt' | 'log-in' | 'siwe'; const timingConfig = { duration: 250, easing: Easing.out(Easing.ease), }; -// prettier-ignore function getPanelPaddingTop( - modeValue /*: string */, - keyboardHeightValue /*: number */, - contentHeightValue /*: number */, -) /*: number */ { + 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 === '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 */, - finishResettingToPrompt/*: () => void */, -) /*: number */ { + modeValue: string, + finishResettingToPrompt: () => void, +): number { 'worklet'; const targetPanelOpacity = modeValue === 'loading' || modeValue === 'prompt' ? 0 : 1; return withTiming( targetPanelOpacity, timingConfig, (succeeded /*?: boolean */) => { if (succeeded && targetPanelOpacity === 0) { runOnJS(finishResettingToPrompt)(); } }, ); } const unboundStyles = { animationContainer: { flex: 1, }, backButton: { position: 'absolute', top: 13, }, buttonContainer: { bottom: 0, left: 0, marginLeft: 26, marginRight: 26, paddingBottom: 20, position: 'absolute', right: 0, }, signInButtons: { flexDirection: 'row', }, firstSignInButton: { marginRight: 8, flex: 1, }, lastSignInButton: { marginLeft: 8, flex: 1, }, 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, }, 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', }, }; const isForegroundSelector = createIsForegroundSelector( LoggedOutModalRouteName, ); const backgroundSource = { uri: splashBackgroundURI }; const initialLogInState = { usernameInputText: null, passwordInputText: null, }; 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 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 = useSharedValue(dimensions.safeAreaHeight); const modeValue = useSharedValue(initialMode); const buttonOpacity = useSharedValue(persistedStateLoaded ? 1 : 0); const onPrompt = mode.curMode === 'prompt'; const prevOnPromptRef = React.useRef(onPrompt); React.useEffect(() => { if (onPrompt && !prevOnPromptRef.current) { 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.value = curContentHeight; }, [curContentHeight, contentHeight]); const combinedSetMode = React.useCallback( (newMode: LoggedOutMode) => { nextModeRef.current = newMode; setMode({ curMode: newMode, nextMode: newMode }); modeValue.value = newMode; }, [setMode, modeValue], ); const goBackToPrompt = React.useCallback(() => { nextModeRef.current = 'prompt'; setMode({ nextMode: 'prompt' }); modeValue.value = 'prompt'; Keyboard.dismiss(); }, [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, ); // We remove the password from the TextInput on iOS before dismissing it, // because otherwise iOS will prompt the user to save the password if the // iCloud password manager is enabled. We'll put the password back after the // dismissal concludes. const temporarilyHiddenPassword = React.useRef(); const curLogInPassword = logInState.passwordInputText; const resetToPrompt = React.useCallback(() => { if (nextModeRef.current === 'prompt') { return false; } if (Platform.OS === 'ios' && curLogInPassword) { temporarilyHiddenPassword.current = curLogInPassword; setLogInState({ passwordInputText: null }); } goBackToPrompt(); return true; }, [goBackToPrompt, curLogInPassword, setLogInState]); const finishResettingToPrompt = React.useCallback(() => { setMode({ curMode: nextModeRef.current }); if (temporarilyHiddenPassword.current) { setLogInState({ passwordInputText: temporarilyHiddenPassword.current }); temporarilyHiddenPassword.current = null; } }, [setMode, setLogInState]); 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 isLoggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); 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; } if (loggedIn === isLoggedInToAuthKeyserver) { return; } const actionSource = loggedIn ? recoveryFromReduxActionSources.appStartReduxLoggedInButInvalidCookie : recoveryFromReduxActionSources.appStartCookieLoggedInButInvalidRedux; dispatch({ type: setActiveSessionRecoveryActionType, payload: { activeSessionRecovery: actionSource, keyserverID: authoritativeKeyserverID, }, }); }, [rehydrateConcluded, loggedIn, isLoggedInToAuthKeyserver, dispatch]); const onPressSIWE = React.useCallback(() => { combinedSetMode('siwe'); }, [combinedSetMode]); const onPressLogIn = React.useCallback(() => { combinedSetMode('log-in'); }, [combinedSetMode]); const { navigate } = props.navigation; const onPressQRCodeSignIn = React.useCallback(() => { navigate(SignInNavigatorRouteName); }, [navigate]); const onPressNewRegister = React.useCallback(() => { navigate(RegistrationRouteName); }, [navigate]); const opacityStyle = useAnimatedStyle(() => ({ opacity: getPanelOpacity(modeValue.value, finishResettingToPrompt), })); const styles = useStyles(unboundStyles); const panel = React.useMemo(() => { if (mode.curMode === 'log-in') { return ( ); } else if (mode.curMode === 'loading') { return ( ); } return null; }, [ mode.curMode, setActiveAlert, opacityStyle, logInStateContainer, styles.loadingIndicator, ]); const buttonsViewOpacity = useAnimatedStyle(() => ({ opacity: buttonOpacity.value, })); const buttonsViewStyle = React.useMemo( () => [styles.buttonContainer, buttonsViewOpacity], [styles.buttonContainer, buttonsViewOpacity], ); const buttons = React.useMemo(() => { if (mode.curMode !== 'prompt') { return null; } const signInButtons: Array> = []; if (!usingRestoreFlow) { signInButtons.push( , ); } if (__DEV__ || usingRestoreFlow) { const buttonText = usingRestoreFlow ? 'Sign in' : 'Sign in (QR)'; signInButtons.push( , ); } if (signInButtons.length === 2) { signInButtons[0] = ( {signInButtons[0]} ); signInButtons[1] = ( {signInButtons[1]} ); } let siweSection = null; if (!usingRestoreFlow) { siweSection = ( <> or ); } return ( {siweSection} {signInButtons} ); }, [ mode.curMode, onPressNewRegister, onPressLogIn, onPressQRCodeSignIn, onPressSIWE, buttonsViewStyle, styles.firstSignInButton, styles.lastSignInButton, styles.siweOr, styles.siweOrLeftHR, styles.siweOrText, styles.siweOrRightHR, styles.signInButtons, ]); const windowWidth = dimensions.width; const backButtonStyle = React.useMemo( () => [ styles.backButton, opacityStyle, { left: windowWidth < 360 ? 28 : 40 }, ], [styles.backButton, opacityStyle, windowWidth], ); const paddingTopStyle = useAnimatedStyle(() => ({ paddingTop: getPanelPaddingTop( modeValue.value, keyboardHeightValue.value, contentHeight.value, ), })); const animatedContentStyle = React.useMemo( () => [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/chat/swipeable-message.react.js b/native/chat/swipeable-message.react.js index 03762ac84..69640f553 100644 --- a/native/chat/swipeable-message.react.js +++ b/native/chat/swipeable-message.react.js @@ -1,451 +1,437 @@ // @flow import type { IconProps } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import * as React from 'react'; import { View } from 'react-native'; import { PanGestureHandler, type PanGestureEvent, } from 'react-native-gesture-handler'; import Animated, { useAnimatedGestureHandler, useSharedValue, useAnimatedStyle, runOnJS, withSpring, interpolate, cancelAnimation, Extrapolate, type SharedValue, - // ESLint doesn't understand Flow comment syntax - // eslint-disable-next-line no-unused-vars type WithSpringConfig, } from 'react-native-reanimated'; import tinycolor from 'tinycolor2'; import { useMessageListScreenWidth } from './composed-message-width.js'; import CommIcon from '../components/comm-icon.react.js'; import { colors } from '../themes/colors.js'; import type { ViewStyle } from '../types/styles.js'; const primaryThreshold = 40; const secondaryThreshold = 120; const panGestureHandlerActiveOffsetX = [-4, 4]; const panGestureHandlerFailOffsetY = [-5, 5]; -// prettier-ignore function dividePastDistance( - value /*: number */, - distance /*: number */, - factor /*: number */, -) /*: number */ { + value: number, + distance: number, + factor: number, +): number { 'worklet'; const absValue = Math.abs(value); if (absValue < distance) { return value; } const absFactor = value >= 0 ? 1 : -1; return absFactor * (distance + (absValue - distance) / factor); } -// prettier-ignore -function makeSpringConfig(velocity /*: number */) /*: WithSpringConfig */ { +function makeSpringConfig(velocity: number): WithSpringConfig { 'worklet'; return { stiffness: 257.1370588235294, damping: 19.003038357561845, mass: 1, overshootClamping: true, restDisplacementThreshold: 0.001, restSpeedThreshold: 0.001, velocity, }; } -// prettier-ignore -function interpolateOpacityForViewerPrimarySnake( - translateX /*: number */, -) /*: number */ { +function interpolateOpacityForViewerPrimarySnake(translateX: number): number { 'worklet'; return interpolate(translateX, [-20, -5], [1, 0], Extrapolate.CLAMP); } -// prettier-ignore + function interpolateOpacityForNonViewerPrimarySnake( - translateX /*: number */, -) /*: number */ { + translateX: number, +): number { 'worklet'; return interpolate(translateX, [5, 20], [0, 1], Extrapolate.CLAMP); } -// prettier-ignore + function interpolateTranslateXForViewerSecondarySnake( - translateX /*: number */, -) /*: number */ { + translateX: number, +): number { 'worklet'; return interpolate(translateX, [-130, -120, -60, 0], [-130, -120, -5, 20]); } -// prettier-ignore + function interpolateTranslateXForNonViewerSecondarySnake( - translateX /*: number */, -) /*: number */ { + translateX: number, +): number { 'worklet'; return interpolate(translateX, [0, 80, 120, 130], [0, 30, 120, 130]); } type SwipeSnakeProps = { +isViewer: boolean, +translateX: SharedValue, +color: string, +children: React.Element>>, +opacityInterpolator?: number => number, // must be worklet +translateXInterpolator?: number => number, // must be worklet }; function SwipeSnake( props: SwipeSnakeProps, ): React.Node { const { translateX, isViewer, opacityInterpolator, translateXInterpolator } = props; const transformStyle = useAnimatedStyle(() => { const opacity = opacityInterpolator ? opacityInterpolator(translateX.value) : undefined; const translate = translateXInterpolator ? translateXInterpolator(translateX.value) : translateX.value; return { transform: [ { translateX: translate, }, ], opacity, }; }, [isViewer, translateXInterpolator, opacityInterpolator]); const animationPosition = isViewer ? styles.right0 : styles.left0; const animationContainerStyle = React.useMemo(() => { return [styles.animationContainer, animationPosition]; }, [animationPosition]); const iconPosition = isViewer ? styles.left0 : styles.right0; const swipeSnakeContainerStyle = React.useMemo(() => { return [styles.swipeSnakeContainer, transformStyle, iconPosition]; }, [transformStyle, iconPosition]); const iconAlign = isViewer ? styles.alignStart : styles.alignEnd; const screenWidth = useMessageListScreenWidth(); const { color } = props; const swipeSnakeStyle = React.useMemo(() => { return [ styles.swipeSnake, iconAlign, { width: screenWidth, backgroundColor: color, }, ]; }, [iconAlign, screenWidth, color]); const { children } = props; const iconColor = tinycolor(color).isDark() ? colors.dark.listForegroundLabel : colors.light.listForegroundLabel; const coloredIcon = React.useMemo( () => React.cloneElement(children, { color: iconColor, }), [children, iconColor], ); const swipeSnake = React.useMemo( () => ( {coloredIcon} ), [ animationContainerStyle, coloredIcon, swipeSnakeContainerStyle, swipeSnakeStyle, ], ); return swipeSnake; } type Props = { +triggerReply?: () => mixed, +triggerSidebar?: () => mixed, +isViewer: boolean, +contentStyle: ViewStyle, +threadColor: string, +children: React.Node, }; function SwipeableMessage(props: Props): React.Node { const { isViewer, triggerReply, triggerSidebar } = props; const secondaryActionExists = triggerReply && triggerSidebar; const onPassPrimaryThreshold = React.useCallback(() => { const impactStrength = secondaryActionExists ? Haptics.ImpactFeedbackStyle.Medium : Haptics.ImpactFeedbackStyle.Heavy; Haptics.impactAsync(impactStrength); }, [secondaryActionExists]); const onPassSecondaryThreshold = React.useCallback(() => { if (secondaryActionExists) { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); } }, [secondaryActionExists]); const primaryAction = React.useCallback(() => { if (triggerReply) { triggerReply(); } else if (triggerSidebar) { triggerSidebar(); } }, [triggerReply, triggerSidebar]); const secondaryAction = React.useCallback(() => { if (triggerReply && triggerSidebar) { triggerSidebar(); } }, [triggerReply, triggerSidebar]); const translateX = useSharedValue(0); const swipeEvent = useAnimatedGestureHandler( { - // prettier-ignore - onStart: ( - event /*: PanGestureEvent */, - ctx /*: { [string]: mixed } */, - ) => { + onStart: (event: PanGestureEvent, ctx: { [string]: mixed }) => { ctx.translationAtStart = translateX.value; cancelAnimation(translateX); }, - // prettier-ignore - onActive: ( - event /*: PanGestureEvent */, - ctx /*: { [string]: mixed } */, - ) => { + + onActive: (event: PanGestureEvent, ctx: { [string]: mixed }) => { const { translationAtStart } = ctx; if (typeof translationAtStart !== 'number') { throw new Error('translationAtStart should be number'); } const translationX = translationAtStart + event.translationX; const baseActiveTranslation = isViewer ? Math.min(translationX, 0) : Math.max(translationX, 0); translateX.value = dividePastDistance( baseActiveTranslation, primaryThreshold, 2, ); const absValue = Math.abs(translateX.value); const pastPrimaryThreshold = absValue >= primaryThreshold; if (pastPrimaryThreshold && !ctx.prevPastPrimaryThreshold) { runOnJS(onPassPrimaryThreshold)(); } ctx.prevPastPrimaryThreshold = pastPrimaryThreshold; const pastSecondaryThreshold = absValue >= secondaryThreshold; if (pastSecondaryThreshold && !ctx.prevPastSecondaryThreshold) { runOnJS(onPassSecondaryThreshold)(); } ctx.prevPastSecondaryThreshold = pastSecondaryThreshold; }, - // prettier-ignore - onEnd: (event /*: PanGestureEvent */) => { + + onEnd: (event: PanGestureEvent) => { const absValue = Math.abs(translateX.value); if (absValue >= secondaryThreshold && secondaryActionExists) { runOnJS(secondaryAction)(); } else if (absValue >= primaryThreshold) { runOnJS(primaryAction)(); } translateX.value = withSpring(0, makeSpringConfig(event.velocityX)); }, }, [ isViewer, onPassPrimaryThreshold, onPassSecondaryThreshold, primaryAction, secondaryAction, secondaryActionExists, ], ); const transformContentStyle = useAnimatedStyle( () => ({ transform: [{ translateX: translateX.value }], }), [], ); const { contentStyle, children } = props; const panGestureHandlerStyle = React.useMemo( () => [contentStyle, transformContentStyle], [contentStyle, transformContentStyle], ); const threadColor = `#${props.threadColor}`; const tinyThreadColor = tinycolor(threadColor); const replyIcon = React.useMemo( () => , [], ); const replySwipeSnake = React.useMemo( () => ( {replyIcon} ), [isViewer, replyIcon, threadColor, translateX], ); const sidebarIcon = React.useMemo( () => , [], ); const sidebarSwipeSnakeWithReplySwipeSnake = React.useMemo( () => ( {sidebarIcon} ), [isViewer, sidebarIcon, tinyThreadColor, translateX], ); const sidebarSwipeSnakeWithoutReplySwipeSnake = React.useMemo( () => ( {sidebarIcon} ), [isViewer, sidebarIcon, threadColor, translateX], ); const panGestureHandler = React.useMemo( () => ( {children} ), [children, isViewer, panGestureHandlerStyle, swipeEvent], ); const swipeableMessage = React.useMemo(() => { if (!triggerReply && !triggerSidebar) { return ( {children} ); } const snakes: Array = []; if (triggerReply) { snakes.push(replySwipeSnake); } if (triggerReply && triggerSidebar) { snakes.push(sidebarSwipeSnakeWithReplySwipeSnake); } else if (triggerSidebar) { snakes.push(sidebarSwipeSnakeWithoutReplySwipeSnake); } snakes.push(panGestureHandler); return snakes; }, [ children, contentStyle, panGestureHandler, replySwipeSnake, sidebarSwipeSnakeWithReplySwipeSnake, sidebarSwipeSnakeWithoutReplySwipeSnake, triggerReply, triggerSidebar, ]); return swipeableMessage; } const styles = { swipeSnakeContainer: { marginHorizontal: 20, justifyContent: 'center', position: 'absolute', top: 0, bottom: 0, }, animationContainer: { position: 'absolute', top: 0, bottom: 0, }, swipeSnake: { paddingHorizontal: 15, flex: 1, borderRadius: 25, height: 30, justifyContent: 'center', maxHeight: 50, }, left0: { left: 0, }, right0: { right: 0, }, alignStart: { alignItems: 'flex-start', }, alignEnd: { alignItems: 'flex-end', }, }; export default SwipeableMessage; diff --git a/native/tooltip/nux-tips-overlay.react.js b/native/tooltip/nux-tips-overlay.react.js index 735216db3..45c04e197 100644 --- a/native/tooltip/nux-tips-overlay.react.js +++ b/native/tooltip/nux-tips-overlay.react.js @@ -1,509 +1,503 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, TouchableWithoutFeedback, Platform, Text } from 'react-native'; import Animated, { FadeOut, withTiming, - // eslint-disable-next-line no-unused-vars type EntryAnimationsValues, - // eslint-disable-next-line no-unused-vars type ExitAnimationsValues, } from 'react-native-reanimated'; import Button from '../components/button.react.js'; import { getNUXTipParams, NUXTipsContext, type NUXTip, } from '../components/nux-tips-context.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import { OverlayContext } from '../navigation/overlay-context.js'; import type { NavigationRoute, NUXTipRouteNames, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import type { LayoutEvent } from '../types/react-native.js'; import { AnimatedView } from '../types/styles.js'; import type { WritableAnimatedStyleObj } from '../types/styles.js'; const { Value } = Animated; const animationDuration = 150; const unboundStyles = { container: { flex: 1, }, contentContainer: { flex: 1, overflow: 'hidden', }, items: { backgroundColor: 'tooltipBackground', borderRadius: 5, overflow: 'hidden', paddingHorizontal: 20, paddingTop: 20, paddingBottom: 18, }, triangleUp: { borderBottomColor: 'tooltipBackground', borderBottomWidth: 10, borderLeftColor: 'transparent', borderLeftWidth: 10, borderRightColor: 'transparent', borderRightWidth: 10, borderStyle: 'solid', borderTopColor: 'transparent', borderTopWidth: 0, bottom: Platform.OS === 'android' ? -1 : 0, height: 10, width: 10, }, triangleDown: { borderBottomColor: 'transparent', borderBottomWidth: 0, borderLeftColor: 'transparent', borderLeftWidth: 10, borderRightColor: 'transparent', borderRightWidth: 10, borderStyle: 'solid', borderTopColor: 'tooltipBackground', borderTopWidth: 10, height: 10, top: Platform.OS === 'android' ? -1 : 0, width: 10, }, tipText: { color: 'panelForegroundLabel', fontSize: 18, marginBottom: 10, }, buttonContainer: { alignSelf: 'flex-end', }, okButtonText: { fontSize: 18, color: 'panelForegroundLabel', textAlign: 'center', paddingVertical: 10, paddingHorizontal: 20, }, okButton: { backgroundColor: 'purpleButton', borderRadius: 8, }, }; export type NUXTipsOverlayParams = { +orderedTips: $ReadOnlyArray, +orderedTipsIndex: number, }; export type NUXTipsOverlayProps = { +navigation: AppNavigationProp, +route: NavigationRoute, }; const marginVertical: number = 20; const marginHorizontal: number = 10; function createNUXTipsOverlay( ButtonComponent: ?React.ComponentType>, tipText: string, ): React.ComponentType> { function NUXTipsOverlay(props: NUXTipsOverlayProps) { const nuxTipContext = React.useContext(NUXTipsContext); const { navigation, route } = props; const dimensions = useSelector(state => state.dimensions); const [coordinates, setCoordinates] = React.useState(null); React.useEffect(() => { if (!ButtonComponent) { const yInitial = (dimensions.height * 2) / 5; const xInitial = dimensions.width / 2; setCoordinates({ initialCoordinates: { height: 0, width: 0, x: xInitial, y: yInitial }, verticalBounds: { height: 0, y: yInitial }, }); return; } const currentTipKey = route.params.orderedTips[route.params.orderedTipsIndex]; const measure = nuxTipContext?.tipsProps?.[currentTipKey]; invariant(measure, 'button should be registered with nuxTipContext'); measure((x, y, width, height, pageX, pageY) => setCoordinates({ initialCoordinates: { height, width, x: pageX, y: pageY }, verticalBounds: { height, y: pageY }, }), ); }, [ dimensions, nuxTipContext?.tipsProps, route.params.orderedTips, route.params.orderedTipsIndex, ]); const overlayContext = React.useContext(OverlayContext); invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext'); const { onExitFinish } = overlayContext; const { goBackOnce } = navigation; const styles = useStyles(unboundStyles); const contentContainerStyle = React.useMemo(() => { if (!coordinates) { return {}; } const fullScreenHeight = dimensions.height; const top = coordinates.verticalBounds.y; const bottom = fullScreenHeight - coordinates.verticalBounds.y - coordinates.verticalBounds.height; return { ...styles.contentContainer, marginTop: top, marginBottom: bottom, }; }, [dimensions.height, styles.contentContainer, coordinates]); const buttonStyle = React.useMemo(() => { if (!coordinates) { return {}; } const { x, y, width, height } = coordinates.initialCoordinates; return { width: Math.ceil(width), height: Math.ceil(height), marginTop: y - coordinates.verticalBounds.y, marginLeft: x, }; }, [coordinates]); const tipHorizontalOffsetRef = React.useRef(new Value(0)); const tipHorizontalOffset = tipHorizontalOffsetRef.current; const onTipContainerLayout = React.useCallback( (event: LayoutEvent) => { if (!coordinates) { return; } const { x, width } = coordinates.initialCoordinates; const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; const actualWidth = event.nativeEvent.layout.width; if (extraLeftSpace < extraRightSpace) { const minWidth = width + 2 * extraLeftSpace; tipHorizontalOffset.setValue((minWidth - actualWidth) / 2); } else { const minWidth = width + 2 * extraRightSpace; tipHorizontalOffset.setValue((actualWidth - minWidth) / 2); } }, [coordinates, dimensions.width, tipHorizontalOffset], ); const currentTipKey = route.params.orderedTips[route.params.orderedTipsIndex]; const tipParams = getNUXTipParams(currentTipKey); const { tooltipLocation } = tipParams; const baseTipContainerStyle = React.useMemo(() => { if (!coordinates) { return {}; } const { initialCoordinates, verticalBounds } = coordinates; const { y, x, height, width } = initialCoordinates; const style: WritableAnimatedStyleObj = { position: 'absolute', alignItems: 'center', }; if (tooltipLocation === 'below') { style.top = Math.min(y + height, verticalBounds.y + verticalBounds.height) + marginVertical; } else { style.bottom = dimensions.height - Math.max(y, verticalBounds.y) + marginVertical; } const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; if (tooltipLocation === 'absolute') { style.left = marginHorizontal; style.right = marginHorizontal; } else if (extraLeftSpace < extraRightSpace) { style.left = marginHorizontal; style.minWidth = width + 2 * extraLeftSpace; style.marginRight = 2 * marginHorizontal; } else { style.right = marginHorizontal; style.minWidth = width + 2 * extraRightSpace; style.marginLeft = 2 * marginHorizontal; } return style; }, [coordinates, dimensions.height, dimensions.width, tooltipLocation]); const triangleStyle = React.useMemo(() => { if (!coordinates) { return {}; } const { x, width } = coordinates.initialCoordinates; const extraLeftSpace = x; const extraRightSpace = dimensions.width - width - x; if (extraLeftSpace < extraRightSpace) { return { alignSelf: 'flex-start', left: extraLeftSpace + (4 / 10) * width - marginHorizontal, }; } else { return { alignSelf: 'flex-end', right: extraRightSpace + (4 / 10) * width - marginHorizontal, }; } }, [coordinates, dimensions.width]); - // prettier-ignore const tipContainerEnteringAnimation = React.useCallback( - (values/*: EntryAnimationsValues*/) => { + (values: EntryAnimationsValues) => { 'worklet'; if (!coordinates) { return { animations: {}, - initialValues:{}, + initialValues: {}, }; } - if(tooltipLocation === 'absolute'){ + if (tooltipLocation === 'absolute') { return { animations: { opacity: withTiming(1, { duration: animationDuration }), - transform: [ + transform: [ { scale: withTiming(1, { duration: animationDuration }) }, ], }, initialValues: { opacity: 0, - transform: [ - { scale: 0 }, - ], + transform: [{ scale: 0 }], }, }; } const initialX = (-values.targetWidth + coordinates.initialCoordinates.width + coordinates.initialCoordinates.x) / 2; const initialY = tooltipLocation === 'below' ? -values.targetHeight / 2 : values.targetHeight / 2; return { animations: { opacity: withTiming(1, { duration: animationDuration }), transform: [ { translateX: withTiming(0, { duration: animationDuration }) }, { translateY: withTiming(0, { duration: animationDuration }) }, { scale: withTiming(1, { duration: animationDuration }) }, ], }, initialValues: { opacity: 0, transform: [ { translateX: initialX }, { translateY: initialY }, { scale: 0 }, ], }, }; }, [coordinates, tooltipLocation], ); - // prettier-ignore const tipContainerExitingAnimation = React.useCallback( - (values/*: ExitAnimationsValues*/) => { + (values: ExitAnimationsValues) => { 'worklet'; if (!coordinates) { return { animations: {}, - initialValues:{}, + initialValues: {}, }; } if (tooltipLocation === 'absolute') { return { animations: { opacity: withTiming(0, { duration: animationDuration }), transform: [ { scale: withTiming(0, { duration: animationDuration }) }, ], }, initialValues: { opacity: 1, transform: [{ scale: 1 }], }, callback: onExitFinish, }; } const toValueX = (-values.currentWidth + coordinates.initialCoordinates.width + coordinates.initialCoordinates.x) / 2; const toValueY = tooltipLocation === 'below' ? -values.currentHeight / 2 : values.currentHeight / 2; return { animations: { opacity: withTiming(0, { duration: animationDuration }), transform: [ { translateX: withTiming(toValueX, { duration: animationDuration, }), }, { translateY: withTiming(toValueY, { duration: animationDuration, }), }, { scale: withTiming(0, { duration: animationDuration }) }, ], }, initialValues: { opacity: 1, transform: [{ translateX: 0 }, { translateY: 0 }, { scale: 1 }], }, callback: onExitFinish, }; }, [coordinates, onExitFinish, tooltipLocation], ); let triangleDown = null; let triangleUp = null; if (tooltipLocation === 'above') { triangleDown = ; } else if (tooltipLocation === 'below') { triangleUp = ; } const onPressOk = React.useCallback(() => { const { orderedTips, orderedTipsIndex } = route.params; goBackOnce(); const nextOrderedTipsIndex = orderedTipsIndex + 1; if (nextOrderedTipsIndex >= orderedTips.length) { navigation.goBack(); return; } const nextTip = orderedTips[nextOrderedTipsIndex]; const { routeName } = getNUXTipParams(nextTip); navigation.navigate({ name: routeName, params: { orderedTips, orderedTipsIndex: nextOrderedTipsIndex, }, }); }, [goBackOnce, navigation, route.params]); const button = React.useMemo( () => ButtonComponent ? ( ) : undefined, [buttonStyle, contentContainerStyle, props.navigation, route], ); if (!coordinates) { return null; } return ( {button} {triangleUp} {tipText} {triangleDown} ); } function NUXTipsOverlayWrapper(props: NUXTipsOverlayProps) { const overlayContext = React.useContext(OverlayContext); invariant(overlayContext, 'NUXTipsOverlay should have OverlayContext'); const { shouldRenderScreenContent } = overlayContext; return shouldRenderScreenContent ? : null; } return React.memo>(NUXTipsOverlayWrapper); } export { createNUXTipsOverlay, animationDuration }; diff --git a/patches/react-native-reanimated+2.12.0.patch b/patches/react-native-reanimated+2.12.0.patch index 02212d38f..430175ae1 100644 --- a/patches/react-native-reanimated+2.12.0.patch +++ b/patches/react-native-reanimated+2.12.0.patch @@ -1,13 +1,33 @@ diff --git a/node_modules/react-native-reanimated/android/build.gradle b/node_modules/react-native-reanimated/android/build.gradle -index a38b2a5..32cb655 100644 +index a38b2a5..8605d09 100644 --- a/node_modules/react-native-reanimated/android/build.gradle +++ b/node_modules/react-native-reanimated/android/build.gradle @@ -658,7 +658,7 @@ if (isNewArchitectureEnabled()) { task downloadBoost(dependsOn: resolveBoost, type: Download) { def transformedVersion = BOOST_VERSION.replace("_", ".") def artifactLocalName = "boost_${BOOST_VERSION}.tar.gz" - def srcUrl = "https://boostorg.jfrog.io/artifactory/main/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz" + def srcUrl = "https://archives.boost.io/release/${BOOST_VERSION.replace("_", ".")}/source/boost_${BOOST_VERSION}.tar.gz" if (REACT_NATIVE_MINOR_VERSION < 69) { srcUrl = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${transformedVersion}-0/boost_${BOOST_VERSION}.tar.gz" } +diff --git a/node_modules/react-native-reanimated/plugin.js b/node_modules/react-native-reanimated/plugin.js +index b9ba1c3..0aa9ab7 100644 +--- a/node_modules/react-native-reanimated/plugin.js ++++ b/node_modules/react-native-reanimated/plugin.js +@@ -360,13 +360,14 @@ function makeWorklet(t, fun, state) { + + const transformed = transformSync(code, { + filename: state.file.opts.filename, +- presets: ['@babel/preset-typescript'], ++ presets: ['@babel/preset-typescript', ...(state.opts.extraPresets ?? [])], + plugins: [ + '@babel/plugin-transform-shorthand-properties', + '@babel/plugin-transform-arrow-functions', + '@babel/plugin-proposal-optional-chaining', + '@babel/plugin-proposal-nullish-coalescing-operator', + ['@babel/plugin-transform-template-literals', { loose: true }], ++ ...(state.opts.extraPlugins ?? []), + ], + ast: true, + babelrc: false,