From 427f3a8bd7f21f14aef32af2f7ccf1f4b2731c29 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 12 Nov 2024 11:18:53 -0800 Subject: Add email verification prompts throughout the app (#6174) --- src/lib/hooks/useEmail.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/lib/hooks/useEmail.ts (limited to 'src/lib/hooks') diff --git a/src/lib/hooks/useEmail.ts b/src/lib/hooks/useEmail.ts new file mode 100644 index 000000000..6e52846d1 --- /dev/null +++ b/src/lib/hooks/useEmail.ts @@ -0,0 +1,19 @@ +import {useServiceConfigQuery} from '#/state/queries/email-verification-required' +import {useSession} from '#/state/session' +import {BSKY_SERVICE} from '../constants' +import {getHostnameFromUrl} from '../strings/url-helpers' + +export function useEmail() { + const {currentAccount} = useSession() + + const {data: serviceConfig} = useServiceConfigQuery() + + const isSelfHost = + serviceConfig?.checkEmailConfirmed && + currentAccount && + getHostnameFromUrl(currentAccount.service) !== + getHostnameFromUrl(BSKY_SERVICE) + const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed + + return {needsEmailVerification} +} -- cgit 1.4.1 From c0fb5245f1201ac424f6fb2f789510f94fa6d2fc Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 15 Nov 2024 17:32:28 -0800 Subject: Tweak email verification dialog (#6397) --- src/components/dialogs/VerifyEmailDialog.tsx | 67 +++++++++++++++++++--------- src/lib/hooks/useEmail.ts | 16 ++++++- 2 files changed, 59 insertions(+), 24 deletions(-) (limited to 'src/lib/hooks') diff --git a/src/components/dialogs/VerifyEmailDialog.tsx b/src/components/dialogs/VerifyEmailDialog.tsx index d4412b6f8..ced9171ce 100644 --- a/src/components/dialogs/VerifyEmailDialog.tsx +++ b/src/components/dialogs/VerifyEmailDialog.tsx @@ -146,18 +146,17 @@ export function Inner({ ) : null} - - {currentStep === 'StepOne' ? ( - <> - {!reasonText ? ( - <> - - You'll receive an email at{' '} - - {currentAccount?.email} - {' '} - to verify it's you. - {' '} + {currentStep === 'StepOne' ? ( + + {reasonText ? ( + + {reasonText} + + Don't have access to{' '} + + {currentAccount?.email} + + ?{' '} - Need to change it? + Change your email address - - ) : ( - reasonText - )} - - ) : ( - uiStrings[currentStep].message - )} - + . + + + ) : ( + + + You'll receive an email at{' '} + + {currentAccount?.email} + {' '} + to verify it's you. + {' '} + { + e.preventDefault() + control.close(() => { + openModal({name: 'change-email'}) + }) + return false + }}> + Need to change it? + + + )} + + ) : ( + + {uiStrings[currentStep].message} + + )} {currentStep === 'StepTwo' ? ( diff --git a/src/lib/hooks/useEmail.ts b/src/lib/hooks/useEmail.ts index 6e52846d1..ab87f057e 100644 --- a/src/lib/hooks/useEmail.ts +++ b/src/lib/hooks/useEmail.ts @@ -1,4 +1,5 @@ import {useServiceConfigQuery} from '#/state/queries/email-verification-required' +import {useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import {BSKY_SERVICE} from '../constants' import {getHostnameFromUrl} from '../strings/url-helpers' @@ -7,13 +8,24 @@ export function useEmail() { const {currentAccount} = useSession() const {data: serviceConfig} = useServiceConfigQuery() + const {data: profile} = useProfileQuery({did: currentAccount?.did}) + + const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed + + const isNewEnough = + !!profile?.createdAt && + Date.parse(profile.createdAt) >= Date.parse('2024-11-16T02:00:00.000Z') const isSelfHost = - serviceConfig?.checkEmailConfirmed && currentAccount && getHostnameFromUrl(currentAccount.service) !== getHostnameFromUrl(BSKY_SERVICE) - const needsEmailVerification = !isSelfHost && !currentAccount?.emailConfirmed + + const needsEmailVerification = + !isSelfHost && + checkEmailConfirmed && + !!currentAccount?.emailConfirmed && + isNewEnough return {needsEmailVerification} } -- cgit 1.4.1 From 9bbea3f33a9a17285d8ec58003b598a911c192a1 Mon Sep 17 00:00:00 2001 From: Hailey Date: Sat, 16 Nov 2024 13:34:30 -0800 Subject: Email verification tweaks (date) (#6416) --- src/lib/hooks/useEmail.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/lib/hooks') diff --git a/src/lib/hooks/useEmail.ts b/src/lib/hooks/useEmail.ts index ab87f057e..a8f4c6ad2 100644 --- a/src/lib/hooks/useEmail.ts +++ b/src/lib/hooks/useEmail.ts @@ -12,9 +12,10 @@ export function useEmail() { const checkEmailConfirmed = !!serviceConfig?.checkEmailConfirmed + // Date set for 11 AM PST on the 18th of November const isNewEnough = !!profile?.createdAt && - Date.parse(profile.createdAt) >= Date.parse('2024-11-16T02:00:00.000Z') + Date.parse(profile.createdAt) >= Date.parse('2024-11-18T19:00:00.000Z') const isSelfHost = currentAccount && @@ -24,7 +25,7 @@ export function useEmail() { const needsEmailVerification = !isSelfHost && checkEmailConfirmed && - !!currentAccount?.emailConfirmed && + !currentAccount?.emailConfirmed && isNewEnough return {needsEmailVerification} -- cgit 1.4.1 From 474c4eff29b6a2454a20febf78a1edd5cf58663a Mon Sep 17 00:00:00 2001 From: dan Date: Sun, 17 Nov 2024 15:06:28 +0000 Subject: Use compiler-safe Reanimated get/set APIs (#6391) * Convert lightbox to get/set * Work around software-mansion/react-native-reanimated#6613 * Use get/set in more places * Port MainScrollProvider to get/set * Port more to get/set * Port composer to get/set * Remove unnecessary thread hops in composer * Port more things to get/set * Convert more to get/set, remove redundant runOnJS * Convert remaining cases to get/set --- src/Splash.tsx | 72 ++++++++++--------- src/components/Loader.tsx | 7 +- src/components/ProgressGuide/Toast.tsx | 48 +++++++------ src/components/anim/AnimatedCheck.tsx | 20 +++--- src/components/dms/ActionsWrapper.tsx | 16 +++-- src/components/dms/ChatEmptyPill.tsx | 6 +- src/components/dms/NewMessagesPill.tsx | 6 +- src/lib/custom-animations/GestureActionView.tsx | 84 +++++++++++----------- src/lib/custom-animations/PressableScale.tsx | 13 ++-- src/lib/hooks/useMinimalShellTransform.ts | 20 +++--- src/screens/Messages/components/MessageInput.tsx | 10 +-- src/screens/Messages/components/MessagesList.tsx | 36 +++++----- src/screens/Profile/Header/GrowableAvatar.tsx | 2 +- src/screens/Profile/Header/GrowableBanner.tsx | 15 ++-- src/state/shell/minimal-mode.tsx | 16 +++-- src/view/com/composer/Composer.tsx | 46 +++++++----- src/view/com/home/HomeHeaderLayout.web.tsx | 2 +- src/view/com/home/HomeHeaderLayoutMobile.tsx | 2 +- .../components/ImageItem/ImageItem.android.tsx | 81 ++++++++++----------- .../components/ImageItem/ImageItem.ios.tsx | 8 +-- src/view/com/lightbox/ImageViewing/index.tsx | 74 ++++++++++--------- src/view/com/pager/PagerWithHeader.tsx | 12 ++-- src/view/com/util/BottomSheetCustomBackdrop.tsx | 2 +- src/view/com/util/List.tsx | 4 +- src/view/com/util/MainScrollProvider.tsx | 51 ++++++------- src/view/shell/bottom-bar/BottomBar.tsx | 2 +- 26 files changed, 351 insertions(+), 304 deletions(-) (limited to 'src/lib/hooks') diff --git a/src/Splash.tsx b/src/Splash.tsx index 5a2b18445..a52b8837d 100644 --- a/src/Splash.tsx +++ b/src/Splash.tsx @@ -81,40 +81,40 @@ export function Splash(props: React.PropsWithChildren) { return { transform: [ { - scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), + scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), }, { scale: interpolate( - outroLogo.value, + outroLogo.get(), [0, 0.08, 1], [1, 0.8, 500], 'clamp', ), }, ], - opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), } }) const bottomLogoAnimation = useAnimatedStyle(() => { return { - opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), } }) const reducedLogoAnimation = useAnimatedStyle(() => { return { transform: [ { - scale: interpolate(intro.value, [0, 1], [0.8, 1], 'clamp'), + scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'), }, ], - opacity: interpolate(intro.value, [0, 1], [0, 1], 'clamp'), + opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'), } }) const logoWrapperAnimation = useAnimatedStyle(() => { return { opacity: interpolate( - outroAppOpacity.value, + outroAppOpacity.get(), [0, 0.1, 0.2, 1], [1, 1, 0, 0], 'clamp', @@ -126,11 +126,11 @@ export function Splash(props: React.PropsWithChildren) { return { transform: [ { - scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'), + scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'), }, ], opacity: interpolate( - outroAppOpacity.value, + outroAppOpacity.get(), [0, 0.1, 0.2, 1], [0, 0, 1, 1], 'clamp', @@ -146,29 +146,37 @@ export function Splash(props: React.PropsWithChildren) { if (isReady) { SplashScreen.hideAsync() .then(() => { - intro.value = withTiming( - 1, - {duration: 400, easing: Easing.out(Easing.cubic)}, - async () => { - // set these values to check animation at specific point - // outroLogo.value = 0.1 - // outroApp.value = 0.1 - outroLogo.value = withTiming( - 1, - {duration: 1200, easing: Easing.in(Easing.cubic)}, - () => { - runOnJS(onFinish)() - }, - ) - outroApp.value = withTiming(1, { - duration: 1200, - easing: Easing.inOut(Easing.cubic), - }) - outroAppOpacity.value = withTiming(1, { - duration: 1200, - easing: Easing.in(Easing.cubic), - }) - }, + intro.set(() => + withTiming( + 1, + {duration: 400, easing: Easing.out(Easing.cubic)}, + async () => { + // set these values to check animation at specific point + // outroLogo.set(0.1) + // outroApp.set(0.1) + outroLogo.set(() => + withTiming( + 1, + {duration: 1200, easing: Easing.in(Easing.cubic)}, + () => { + runOnJS(onFinish)() + }, + ), + ) + outroApp.set(() => + withTiming(1, { + duration: 1200, + easing: Easing.inOut(Easing.cubic), + }), + ) + outroAppOpacity.set(() => + withTiming(1, { + duration: 1200, + easing: Easing.in(Easing.cubic), + }), + ) + }, + ), ) }) .catch(() => {}) diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index e0b3be637..149554912 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -17,13 +17,12 @@ export function Loader(props: Props) { const rotation = useSharedValue(0) const animatedStyles = useAnimatedStyle(() => ({ - transform: [{rotate: rotation.value + 'deg'}], + transform: [{rotate: rotation.get() + 'deg'}], })) React.useEffect(() => { - rotation.value = withRepeat( - withTiming(360, {duration: 500, easing: Easing.linear}), - -1, + rotation.set(() => + withRepeat(withTiming(360, {duration: 500, easing: Easing.linear}), -1), ) }, [rotation]) diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx index 69e008260..b26c718f8 100644 --- a/src/components/ProgressGuide/Toast.tsx +++ b/src/components/ProgressGuide/Toast.tsx @@ -55,13 +55,15 @@ export const ProgressGuideToast = React.forwardRef< // animate the opacity then set isOpen to false when done const setIsntOpen = () => setIsOpen(false) - opacity.value = withTiming( - 0, - { - duration: 400, - easing: Easing.out(Easing.cubic), - }, - () => runOnJS(setIsntOpen)(), + opacity.set(() => + withTiming( + 0, + { + duration: 400, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(setIsntOpen)(), + ), ) }, [setIsOpen, opacity]) @@ -71,20 +73,24 @@ export const ProgressGuideToast = React.forwardRef< // animate the vertical translation, the opacity, and the checkmark const playCheckmark = () => animatedCheckRef.current?.play() - opacity.value = 0 - opacity.value = withTiming( - 1, - { - duration: 100, + opacity.set(0) + opacity.set(() => + withTiming( + 1, + { + duration: 100, + easing: Easing.out(Easing.cubic), + }, + () => runOnJS(playCheckmark)(), + ), + ) + translateY.set(0) + translateY.set(() => + withTiming(insets.top + 10, { + duration: 500, easing: Easing.out(Easing.cubic), - }, - () => runOnJS(playCheckmark)(), + }), ) - translateY.value = 0 - translateY.value = withTiming(insets.top + 10, { - duration: 500, - easing: Easing.out(Easing.cubic), - }) // start the countdown timer to autoclose timeoutRef.current = setTimeout(close, visibleDuration || 5e3) @@ -114,8 +120,8 @@ export const ProgressGuideToast = React.forwardRef< }, [winDim.width]) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{translateY: translateY.value}], - opacity: opacity.value, + transform: [{translateY: translateY.get()}], + opacity: opacity.get(), })) return ( diff --git a/src/components/anim/AnimatedCheck.tsx b/src/components/anim/AnimatedCheck.tsx index 7fdfc14cf..60407274e 100644 --- a/src/components/anim/AnimatedCheck.tsx +++ b/src/components/anim/AnimatedCheck.tsx @@ -32,21 +32,25 @@ export const AnimatedCheck = React.forwardRef< const checkAnim = useSharedValue(0) const circleAnimatedProps = useAnimatedProps(() => ({ - strokeDashoffset: 166 - circleAnim.value * 166, + strokeDashoffset: 166 - circleAnim.get() * 166, })) const checkAnimatedProps = useAnimatedProps(() => ({ - strokeDashoffset: 48 - 48 * checkAnim.value, + strokeDashoffset: 48 - 48 * checkAnim.get(), })) const play = React.useCallback( (cb?: () => void) => { - circleAnim.value = 0 - checkAnim.value = 0 + circleAnim.set(0) + checkAnim.set(0) - circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear}) - checkAnim.value = withDelay( - 500, - withTiming(1, {duration: 300, easing: Easing.linear}, cb), + circleAnim.set(() => + withTiming(1, {duration: 500, easing: Easing.linear}), + ) + checkAnim.set(() => + withDelay( + 500, + withTiming(1, {duration: 300, easing: Easing.linear}, cb), + ), ) }, [circleAnim, checkAnim], diff --git a/src/components/dms/ActionsWrapper.tsx b/src/components/dms/ActionsWrapper.tsx index b77516e7b..a087fed3f 100644 --- a/src/components/dms/ActionsWrapper.tsx +++ b/src/components/dms/ActionsWrapper.tsx @@ -34,7 +34,7 @@ export function ActionsWrapper({ const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) const open = React.useCallback(() => { @@ -46,7 +46,7 @@ export function ActionsWrapper({ const shrink = React.useCallback(() => { 'worklet' cancelAnimation(scale) - scale.value = withTiming(1, {duration: 200}) + scale.set(() => withTiming(1, {duration: 200})) }, [scale]) const doubleTapGesture = Gesture.Tap() @@ -58,11 +58,13 @@ export function ActionsWrapper({ const pressAndHoldGesture = Gesture.LongPress() .onStart(() => { 'worklet' - scale.value = withTiming(1.05, {duration: 200}, finished => { - if (!finished) return - runOnJS(open)() - shrink() - }) + scale.set(() => + withTiming(1.05, {duration: 200}, finished => { + if (!finished) return + runOnJS(open)() + shrink() + }), + ) }) .onTouchesUp(shrink) .onTouchesMove(shrink) diff --git a/src/components/dms/ChatEmptyPill.tsx b/src/components/dms/ChatEmptyPill.tsx index ffd022f56..042c3ad76 100644 --- a/src/components/dms/ChatEmptyPill.tsx +++ b/src/components/dms/ChatEmptyPill.tsx @@ -42,12 +42,12 @@ export function ChatEmptyPill() { const onPressIn = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1.075, {duration: 100}) + scale.set(() => withTiming(1.075, {duration: 100})) }, [scale]) const onPressOut = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1, {duration: 100}) + scale.set(() => withTiming(1, {duration: 100})) }, [scale]) const onPress = React.useCallback(() => { @@ -61,7 +61,7 @@ export function ChatEmptyPill() { }, [playHaptic, prompts.length]) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) return ( diff --git a/src/components/dms/NewMessagesPill.tsx b/src/components/dms/NewMessagesPill.tsx index 2f7ff8f4b..e3bc0c1f8 100644 --- a/src/components/dms/NewMessagesPill.tsx +++ b/src/components/dms/NewMessagesPill.tsx @@ -35,12 +35,12 @@ export function NewMessagesPill({ const onPressIn = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1.075, {duration: 100}) + scale.set(() => withTiming(1.075, {duration: 100})) }, [scale]) const onPressOut = React.useCallback(() => { if (isWeb) return - scale.value = withTiming(1, {duration: 100}) + scale.set(() => withTiming(1, {duration: 100})) }, [scale]) const onPress = React.useCallback(() => { @@ -49,7 +49,7 @@ export function NewMessagesPill({ }, [onPressInner, playHaptic]) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) return ( diff --git a/src/lib/custom-animations/GestureActionView.tsx b/src/lib/custom-animations/GestureActionView.tsx index 79e9db8a9..ba6952a81 100644 --- a/src/lib/custom-animations/GestureActionView.tsx +++ b/src/lib/custom-animations/GestureActionView.tsx @@ -61,7 +61,7 @@ export function GestureActionView({ const clampedTransX = useDerivedValue(() => { const min = actions.leftFirst ? -MAX_WIDTH : 0 const max = actions.rightFirst ? MAX_WIDTH : 0 - return clamp(transX.value, min, max) + return clamp(transX.get(), min, max) }) const iconScale = useSharedValue(1) @@ -75,21 +75,23 @@ export function GestureActionView({ return } - iconScale.value = withSequence( - withTiming(1.2, {duration: 175}), - withTiming(1, {duration: 100}), + iconScale.set(() => + withSequence( + withTiming(1.2, {duration: 175}), + withTiming(1, {duration: 100}), + ), ) } useAnimatedReaction( () => transX, () => { - if (transX.value === 0) { + if (transX.get() === 0) { runOnJS(setActiveAction)(null) - } else if (transX.value < 0) { + } else if (transX.get() < 0) { if ( actions.leftSecond && - transX.value <= -actions.leftSecond.threshold + transX.get() <= -actions.leftSecond.threshold ) { if (activeAction !== 'leftSecond') { runOnJS(setActiveAction)('leftSecond') @@ -97,10 +99,10 @@ export function GestureActionView({ } else if (activeAction !== 'leftFirst') { runOnJS(setActiveAction)('leftFirst') } - } else if (transX.value > 0) { + } else if (transX.get() > 0) { if ( actions.rightSecond && - transX.value > actions.rightSecond.threshold + transX.get() > actions.rightSecond.threshold ) { if (activeAction !== 'rightSecond') { runOnJS(setActiveAction)('rightSecond') @@ -119,44 +121,44 @@ export function GestureActionView({ .activeOffsetY([-200, 200]) .onStart(() => { 'worklet' - isActive.value = true + isActive.set(true) }) .onChange(e => { 'worklet' - transX.value = e.translationX + transX.set(e.translationX) if (e.translationX < 0) { // Left side if (actions.leftSecond) { if ( e.translationX <= -actions.leftSecond.threshold && - !hitSecond.value + !hitSecond.get() ) { runPopAnimation() runOnJS(haptic)() - hitSecond.value = true + hitSecond.set(true) } else if ( - hitSecond.value && + hitSecond.get() && e.translationX > -actions.leftSecond.threshold ) { runPopAnimation() - hitSecond.value = false + hitSecond.set(false) } } - if (!hitSecond.value && actions.leftFirst) { + if (!hitSecond.get() && actions.leftFirst) { if ( e.translationX <= -actions.leftFirst.threshold && - !hitFirst.value + !hitFirst.get() ) { runPopAnimation() runOnJS(haptic)() - hitFirst.value = true + hitFirst.set(true) } else if ( - hitFirst.value && + hitFirst.get() && e.translationX > -actions.leftFirst.threshold ) { - hitFirst.value = false + hitFirst.set(false) } } } else if (e.translationX > 0) { @@ -164,33 +166,33 @@ export function GestureActionView({ if (actions.rightSecond) { if ( e.translationX >= actions.rightSecond.threshold && - !hitSecond.value + !hitSecond.get() ) { runPopAnimation() runOnJS(haptic)() - hitSecond.value = true + hitSecond.set(true) } else if ( - hitSecond.value && + hitSecond.get() && e.translationX < actions.rightSecond.threshold ) { runPopAnimation() - hitSecond.value = false + hitSecond.set(false) } } - if (!hitSecond.value && actions.rightFirst) { + if (!hitSecond.get() && actions.rightFirst) { if ( e.translationX >= actions.rightFirst.threshold && - !hitFirst.value + !hitFirst.get() ) { runPopAnimation() runOnJS(haptic)() - hitFirst.value = true + hitFirst.set(true) } else if ( - hitFirst.value && + hitFirst.get() && e.translationX < actions.rightFirst.threshold ) { - hitFirst.value = false + hitFirst.set(false) } } } @@ -198,29 +200,29 @@ export function GestureActionView({ .onEnd(e => { 'worklet' if (e.translationX < 0) { - if (hitSecond.value && actions.leftSecond) { + if (hitSecond.get() && actions.leftSecond) { runOnJS(actions.leftSecond.action)() - } else if (hitFirst.value && actions.leftFirst) { + } else if (hitFirst.get() && actions.leftFirst) { runOnJS(actions.leftFirst.action)() } } else if (e.translationX > 0) { - if (hitSecond.value && actions.rightSecond) { + if (hitSecond.get() && actions.rightSecond) { runOnJS(actions.rightSecond.action)() - } else if (hitSecond.value && actions.rightFirst) { + } else if (hitSecond.get() && actions.rightFirst) { runOnJS(actions.rightFirst.action)() } } - transX.value = withTiming(0, {duration: 200}) - hitFirst.value = false - hitSecond.value = false - isActive.value = false + transX.set(() => withTiming(0, {duration: 200})) + hitFirst.set(false) + hitSecond.set(false) + isActive.set(false) }) const composedGesture = Gesture.Simultaneous(panGesture) const animatedSliderStyle = useAnimatedStyle(() => { return { - transform: [{translateX: clampedTransX.value}], + transform: [{translateX: clampedTransX.get()}], } }) @@ -274,7 +276,7 @@ export function GestureActionView({ const animatedBackgroundStyle = useAnimatedStyle(() => { return { backgroundColor: interpolateColor( - clampedTransX.value, + clampedTransX.get(), interpolation.inputRange, // @ts-expect-error - Weird type expected by reanimated, but this is okay interpolation.outputRange, @@ -283,10 +285,10 @@ export function GestureActionView({ }) const animatedIconStyle = useAnimatedStyle(() => { - const absTransX = Math.abs(clampedTransX.value) + const absTransX = Math.abs(clampedTransX.get()) return { opacity: interpolate(absTransX, [0, 75], [0.15, 1]), - transform: [{scale: iconScale.value}], + transform: [{scale: iconScale.get()}], } }) diff --git a/src/lib/custom-animations/PressableScale.tsx b/src/lib/custom-animations/PressableScale.tsx index 4737b9ea3..1e776546d 100644 --- a/src/lib/custom-animations/PressableScale.tsx +++ b/src/lib/custom-animations/PressableScale.tsx @@ -2,7 +2,6 @@ import React from 'react' import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native' import Animated, { cancelAnimation, - runOnJS, useAnimatedStyle, useReducedMotion, useSharedValue, @@ -32,27 +31,25 @@ export function PressableScale({ const scale = useSharedValue(1) const animatedStyle = useAnimatedStyle(() => ({ - transform: [{scale: scale.value}], + transform: [{scale: scale.get()}], })) return ( { - 'worklet' if (onPressIn) { - runOnJS(onPressIn)(e) + onPressIn(e) } cancelAnimation(scale) - scale.value = withTiming(targetScale, {duration: 100}) + scale.set(() => withTiming(targetScale, {duration: 100})) }} onPressOut={e => { - 'worklet' if (onPressOut) { - runOnJS(onPressOut)(e) + onPressOut(e) } cancelAnimation(scale) - scale.value = withTiming(1, {duration: 100}) + scale.set(() => withTiming(1, {duration: 100})) }} style={[!reducedMotion && animatedStyle, style]} {...rest}> diff --git a/src/lib/hooks/useMinimalShellTransform.ts b/src/lib/hooks/useMinimalShellTransform.ts index 678776755..6f16fa0f9 100644 --- a/src/lib/hooks/useMinimalShellTransform.ts +++ b/src/lib/hooks/useMinimalShellTransform.ts @@ -10,15 +10,16 @@ export function useMinimalShellHeaderTransform() { const {headerHeight} = useShellLayout() const headerTransform = useAnimatedStyle(() => { + const headerModeValue = headerMode.get() return { - pointerEvents: headerMode.value === 0 ? 'auto' : 'none', - opacity: Math.pow(1 - headerMode.value, 2), + pointerEvents: headerModeValue === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - headerModeValue, 2), transform: [ { translateY: interpolate( - headerMode.value, + headerModeValue, [0, 1], - [0, -headerHeight.value], + [0, -headerHeight.get()], ), }, ], @@ -33,15 +34,16 @@ export function useMinimalShellFooterTransform() { const {footerHeight} = useShellLayout() const footerTransform = useAnimatedStyle(() => { + const footerModeValue = footerMode.get() return { - pointerEvents: footerMode.value === 0 ? 'auto' : 'none', - opacity: Math.pow(1 - footerMode.value, 2), + pointerEvents: footerModeValue === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - footerModeValue, 2), transform: [ { translateY: interpolate( - footerMode.value, + footerModeValue, [0, 1], - [0, footerHeight.value], + [0, footerHeight.get()], ), }, ], @@ -58,7 +60,7 @@ export function useMinimalShellFabTransform() { return { transform: [ { - translateY: interpolate(footerMode.value, [0, 1], [-44, 0]), + translateY: interpolate(footerMode.get(), [0, 1], [-44, 0]), }, ], } diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx index 8edad6272..85509211b 100644 --- a/src/screens/Messages/components/MessageInput.tsx +++ b/src/screens/Messages/components/MessageInput.tsx @@ -108,22 +108,22 @@ export function MessageInput({ const measurement = measure(inputRef) if (!measurement) return - const max = windowHeight - -keyboardHeight.value - topInset - 150 + const max = windowHeight - -keyboardHeight.get() - topInset - 150 const availableSpace = max - measurement.height - maxHeight.value = max - isInputScrollable.value = availableSpace < 30 + maxHeight.set(max) + isInputScrollable.set(availableSpace < 30) }, }, [windowHeight, topInset], ) const animatedStyle = useAnimatedStyle(() => ({ - maxHeight: maxHeight.value, + maxHeight: maxHeight.get(), })) const animatedProps = useAnimatedProps(() => ({ - scrollEnabled: isInputScrollable.value, + scrollEnabled: isInputScrollable.get(), })) return ( diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 9db4f07b6..9f67929a3 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -145,7 +145,7 @@ export function MessagesList({ (_: number, height: number) => { // Because web does not have `maintainVisibleContentPosition` support, we will need to manually scroll to the // previous off whenever we add new content to the previous offset whenever we add new content to the list. - if (isWeb && isAtTop.value && hasScrolled) { + if (isWeb && isAtTop.get() && hasScrolled) { flatListRef.current?.scrollToOffset({ offset: height - prevContentHeight.current, animated: false, @@ -153,7 +153,7 @@ export function MessagesList({ } // This number _must_ be the height of the MaybeLoader component - if (height > 50 && isAtBottom.value) { + if (height > 50 && isAtBottom.get()) { // If the size of the content is changing by more than the height of the screen, then we don't // want to scroll further than the start of all the new content. Since we are storing the previous offset, // we can just scroll the user to that offset and add a little bit of padding. We'll also show the pill @@ -161,7 +161,7 @@ export function MessagesList({ if ( didBackground.current && hasScrolled && - height - prevContentHeight.current > layoutHeight.value - 50 && + height - prevContentHeight.current > layoutHeight.get() - 50 && convoState.items.length - prevItemCount.current > 1 ) { flatListRef.current?.scrollToOffset({ @@ -209,7 +209,7 @@ export function MessagesList({ ) const onStartReached = useCallback(() => { - if (hasScrolled && prevContentHeight.current > layoutHeight.value) { + if (hasScrolled && prevContentHeight.current > layoutHeight.get()) { convoState.fetchMessageHistory() } }, [convoState, hasScrolled, layoutHeight]) @@ -217,18 +217,18 @@ export function MessagesList({ const onScroll = React.useCallback( (e: ReanimatedScrollEvent) => { 'worklet' - layoutHeight.value = e.layoutMeasurement.height + layoutHeight.set(e.layoutMeasurement.height) const bottomOffset = e.contentOffset.y + e.layoutMeasurement.height // Most apps have a little bit of space the user can scroll past while still automatically scrolling ot the bottom // when a new message is added, hence the 100 pixel offset - isAtBottom.value = e.contentSize.height - 100 < bottomOffset - isAtTop.value = e.contentOffset.y <= 1 + isAtBottom.set(e.contentSize.height - 100 < bottomOffset) + isAtTop.set(e.contentOffset.y <= 1) if ( newMessagesPill.show && (e.contentOffset.y > newMessagesPill.startContentOffset + 200 || - isAtBottom.value) + isAtBottom.get()) ) { runOnJS(setNewMessagesPill)({ show: false, @@ -256,28 +256,28 @@ export function MessagesList({ // Immediate updates - like opening the emoji picker - will have a duration of zero. In those cases, we should // just update the height here instead of having the `onMove` event do it (that event will not fire!) if (e.duration === 0) { - layoutScrollWithoutAnimation.value = true - keyboardHeight.value = e.height + layoutScrollWithoutAnimation.set(true) + keyboardHeight.set(e.height) } else { - keyboardIsOpening.value = true + keyboardIsOpening.set(true) } }, onMove: e => { 'worklet' - keyboardHeight.value = e.height + keyboardHeight.set(e.height) if (e.height > bottomOffset) { scrollTo(flatListRef, 0, 1e7, false) } }, onEnd: () => { 'worklet' - keyboardIsOpening.value = false + keyboardIsOpening.set(false) }, }) const animatedListStyle = useAnimatedStyle(() => ({ marginBottom: - keyboardHeight.value > bottomOffset ? keyboardHeight.value : bottomOffset, + keyboardHeight.get() > bottomOffset ? keyboardHeight.get() : bottomOffset, })) // -- Message sending @@ -363,13 +363,13 @@ export function MessagesList({ // -- List layout changes (opening emoji keyboard, etc.) const onListLayout = React.useCallback( (e: LayoutChangeEvent) => { - layoutHeight.value = e.nativeEvent.layout.height + layoutHeight.set(e.nativeEvent.layout.height) - if (isWeb || !keyboardIsOpening.value) { + if (isWeb || !keyboardIsOpening.get()) { flatListRef.current?.scrollToEnd({ - animated: !layoutScrollWithoutAnimation.value, + animated: !layoutScrollWithoutAnimation.get(), }) - layoutScrollWithoutAnimation.value = false + layoutScrollWithoutAnimation.set(false) } }, [ diff --git a/src/screens/Profile/Header/GrowableAvatar.tsx b/src/screens/Profile/Header/GrowableAvatar.tsx index 20ac14892..dab69f955 100644 --- a/src/screens/Profile/Header/GrowableAvatar.tsx +++ b/src/screens/Profile/Header/GrowableAvatar.tsx @@ -45,7 +45,7 @@ function GrowableAvatarInner({ const animatedStyle = useAnimatedStyle(() => ({ transform: [ { - scale: interpolate(scrollY.value, [-150, 0], [1.2, 1], { + scale: interpolate(scrollY.get(), [-150, 0], [1.2, 1], { extrapolateRight: Extrapolation.CLAMP, }), }, diff --git a/src/screens/Profile/Header/GrowableBanner.tsx b/src/screens/Profile/Header/GrowableBanner.tsx index 144b7cd2d..7f5a3cd6e 100644 --- a/src/screens/Profile/Header/GrowableBanner.tsx +++ b/src/screens/Profile/Header/GrowableBanner.tsx @@ -66,7 +66,7 @@ function GrowableBannerInner({ const animatedStyle = useAnimatedStyle(() => ({ transform: [ { - scale: interpolate(scrollY.value, [-150, 0], [2, 1], { + scale: interpolate(scrollY.get(), [-150, 0], [2, 1], { extrapolateRight: Extrapolation.CLAMP, }), }, @@ -76,7 +76,7 @@ function GrowableBannerInner({ const animatedBlurViewProps = useAnimatedProps(() => { return { intensity: interpolate( - scrollY.value, + scrollY.get(), [-300, -65, -15], [50, 40, 0], Extrapolation.CLAMP, @@ -85,16 +85,17 @@ function GrowableBannerInner({ }) const animatedSpinnerStyle = useAnimatedStyle(() => { + const scrollYValue = scrollY.get() return { - display: scrollY.value < 0 ? 'flex' : 'none', + display: scrollYValue < 0 ? 'flex' : 'none', opacity: interpolate( - scrollY.value, + scrollYValue, [-60, -15], [1, 0], Extrapolation.CLAMP, ), transform: [ - {translateY: interpolate(scrollY.value, [-150, 0], [-75, 0])}, + {translateY: interpolate(scrollYValue, [-150, 0], [-75, 0])}, {rotate: '90deg'}, ], } @@ -103,7 +104,7 @@ function GrowableBannerInner({ const animatedBackButtonStyle = useAnimatedStyle(() => ({ transform: [ { - translateY: interpolate(scrollY.value, [-150, 60], [-150, 60], { + translateY: interpolate(scrollY.get(), [-150, 60], [-150, 60], { extrapolateRight: Extrapolation.CLAMP, }), }, @@ -168,7 +169,7 @@ function useShouldAnimateSpinner({ const stickyIsOverscrolled = useStickyToggle(isOverscrolled, 10) useAnimatedReaction( - () => scrollY.value < -5, + () => scrollY.get() < -5, (value, prevValue) => { if (value !== prevValue) { runOnJS(setIsOverscrolled)(value) diff --git a/src/state/shell/minimal-mode.tsx b/src/state/shell/minimal-mode.tsx index 3f1cebdf0..00547ee3e 100644 --- a/src/state/shell/minimal-mode.tsx +++ b/src/state/shell/minimal-mode.tsx @@ -44,13 +44,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) { 'worklet' // Cancel any existing animation cancelAnimation(headerMode) - headerMode.value = withSpring(v ? 1 : 0, { - overshootClamping: true, - }) + headerMode.set(() => + withSpring(v ? 1 : 0, { + overshootClamping: true, + }), + ) cancelAnimation(footerMode) - footerMode.value = withSpring(v ? 1 : 0, { - overshootClamping: true, - }) + footerMode.set(() => + withSpring(v ? 1 : 0, { + overshootClamping: true, + }), + ) }, [headerMode, footerMode], ) diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 0a94827d5..5d9f60766 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1267,12 +1267,12 @@ function useScrollTracker({ const contentHeight = useSharedValue(0) const hasScrolledToTop = useDerivedValue(() => - withTiming(contentOffset.value === 0 ? 1 : 0), + withTiming(contentOffset.get() === 0 ? 1 : 0), ) const hasScrolledToBottom = useDerivedValue(() => withTiming( - contentHeight.value - contentOffset.value - 5 <= scrollViewHeight.value + contentHeight.get() - contentOffset.get() - 5 <= scrollViewHeight.get() ? 1 : 0, ), @@ -1290,11 +1290,11 @@ function useScrollTracker({ }) => { 'worklet' if (typeof newContentHeight === 'number') - contentHeight.value = Math.floor(newContentHeight) + contentHeight.set(Math.floor(newContentHeight)) if (typeof newContentOffset === 'number') - contentOffset.value = Math.floor(newContentOffset) + contentOffset.set(Math.floor(newContentOffset)) if (typeof newScrollViewHeight === 'number') - scrollViewHeight.value = Math.floor(newScrollViewHeight) + scrollViewHeight.set(Math.floor(newScrollViewHeight)) }, [contentHeight, contentOffset, scrollViewHeight], ) @@ -1310,21 +1310,22 @@ function useScrollTracker({ }, }) - const onScrollViewContentSizeChange = useCallback( - (_width: number, height: number) => { - if (stickyBottom && height > contentHeight.value) { + const onScrollViewContentSizeChangeUIThread = useCallback( + (newContentHeight: number) => { + 'worklet' + const oldContentHeight = contentHeight.get() + let shouldScrollToBottom = false + if (stickyBottom && newContentHeight > oldContentHeight) { const isFairlyCloseToBottom = - contentHeight.value - contentOffset.value - 100 <= - scrollViewHeight.value + oldContentHeight - contentOffset.get() - 100 <= scrollViewHeight.get() if (isFairlyCloseToBottom) { - runOnUI(() => { - scrollTo(scrollViewRef, 0, contentHeight.value, true) - })() + shouldScrollToBottom = true } } - showHideBottomBorder({ - newContentHeight: height, - }) + showHideBottomBorder({newContentHeight}) + if (shouldScrollToBottom) { + scrollTo(scrollViewRef, 0, newContentHeight, true) + } }, [ showHideBottomBorder, @@ -1336,6 +1337,13 @@ function useScrollTracker({ ], ) + const onScrollViewContentSizeChange = useCallback( + (_width: number, height: number) => { + runOnUI(onScrollViewContentSizeChangeUIThread)(height) + }, + [onScrollViewContentSizeChangeUIThread], + ) + const onScrollViewLayout = useCallback( (evt: LayoutChangeEvent) => { showHideBottomBorder({ @@ -1349,7 +1357,7 @@ function useScrollTracker({ return { borderBottomWidth: StyleSheet.hairlineWidth, borderColor: interpolateColor( - hasScrolledToTop.value, + hasScrolledToTop.get(), [0, 1], [t.atoms.border_contrast_medium.borderColor, 'transparent'], ), @@ -1359,7 +1367,7 @@ function useScrollTracker({ return { borderTopWidth: StyleSheet.hairlineWidth, borderColor: interpolateColor( - hasScrolledToBottom.value, + hasScrolledToBottom.get(), [0, 1], [t.atoms.border_contrast_medium.borderColor, 'transparent'], ), @@ -1604,7 +1612,7 @@ function VideoUploadToolbar({state}: {state: VideoState}) { const animatedStyle = useAnimatedStyle(() => { return { - transform: [{rotateZ: `${rotate.value}deg`}], + transform: [{rotateZ: `${rotate.get()}deg`}], } }) diff --git a/src/view/com/home/HomeHeaderLayout.web.tsx b/src/view/com/home/HomeHeaderLayout.web.tsx index 7049306eb..bdfc2c7ff 100644 --- a/src/view/com/home/HomeHeaderLayout.web.tsx +++ b/src/view/com/home/HomeHeaderLayout.web.tsx @@ -93,7 +93,7 @@ function HomeHeaderLayoutDesktopAndTablet({ {tabBarAnchor} { - headerHeight.value = e.nativeEvent.layout.height + headerHeight.set(e.nativeEvent.layout.height) }} style={[ t.atoms.bg, diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index f5397d717..98253ad74 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -43,7 +43,7 @@ export function HomeHeaderLayoutMobile({ { - headerHeight.value = e.nativeEvent.layout.height + headerHeight.set(e.nativeEvent.layout.height) }}> diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 260787d2f..7aca8721b 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -87,11 +87,11 @@ const ImageItem = ({ // Note: DO NOT move any logic reading animated values outside this function. useAnimatedReaction( () => { - if (pinchScale.value !== 1) { + if (pinchScale.get() !== 1) { // We're currently pinching. return true } - const [, , committedScale] = readTransform(committedTransform.value) + const [, , committedScale] = readTransform(committedTransform.get()) if (committedScale !== 1) { // We started from a pinched in state. return true @@ -147,10 +147,10 @@ const ImageItem = ({ .onStart(e => { 'worklet' const screenSize = measureSafeArea() - pinchOrigin.value = { + pinchOrigin.set({ x: e.focalX - screenSize.width / 2, y: e.focalY - screenSize.height / 2, - } + }) }) .onChange(e => { 'worklet' @@ -160,7 +160,7 @@ const ImageItem = ({ } // Don't let the picture zoom in so close that it gets blurry. // Also, like in stock Android apps, don't let the user zoom out further than 1:1. - const [, , committedScale] = readTransform(committedTransform.value) + const [, , committedScale] = readTransform(committedTransform.get()) const maxCommittedScale = Math.max( MIN_SCREEN_ZOOM, (imageDimensions.width / screenSize.width) * MAX_ORIGINAL_IMAGE_ZOOM, @@ -171,20 +171,21 @@ const ImageItem = ({ Math.max(minPinchScale, e.scale), maxPinchScale, ) - pinchScale.value = nextPinchScale + pinchScale.set(nextPinchScale) // Zooming out close to the corner could push us out of bounds, which we don't want on Android. // Calculate where we'll end up so we know how much to translate back to stay in bounds. const t = createTransform() - prependPan(t, panTranslation.value) - prependPinch(t, nextPinchScale, pinchOrigin.value, pinchTranslation.value) - prependTransform(t, committedTransform.value) + prependPan(t, panTranslation.get()) + prependPinch(t, nextPinchScale, pinchOrigin.get(), pinchTranslation.get()) + prependTransform(t, committedTransform.get()) const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) if (dx !== 0 || dy !== 0) { - pinchTranslation.value = { - x: pinchTranslation.value.x + dx, - y: pinchTranslation.value.y + dy, - } + const pt = pinchTranslation.get() + pinchTranslation.set({ + x: pt.x + dx, + y: pt.y + dy, + }) } }) .onEnd(() => { @@ -193,18 +194,18 @@ const ImageItem = ({ let t = createTransform() prependPinch( t, - pinchScale.value, - pinchOrigin.value, - pinchTranslation.value, + pinchScale.get(), + pinchOrigin.get(), + pinchTranslation.get(), ) - prependTransform(t, committedTransform.value) + prependTransform(t, committedTransform.get()) applyRounding(t) - committedTransform.value = t + committedTransform.set(t) // Reset just the pinch. - pinchScale.value = 1 - pinchOrigin.value = {x: 0, y: 0} - pinchTranslation.value = {x: 0, y: 0} + pinchScale.set(1) + pinchOrigin.set({x: 0, y: 0}) + pinchTranslation.set({x: 0, y: 0}) }) const pan = Gesture.Pan() @@ -223,29 +224,29 @@ const ImageItem = ({ prependPan(t, nextPanTranslation) prependPinch( t, - pinchScale.value, - pinchOrigin.value, - pinchTranslation.value, + pinchScale.get(), + pinchOrigin.get(), + pinchTranslation.get(), ) - prependTransform(t, committedTransform.value) + prependTransform(t, committedTransform.get()) // Prevent panning from going out of bounds. const [dx, dy] = getExtraTranslationToStayInBounds(t, screenSize) nextPanTranslation.x += dx nextPanTranslation.y += dy - panTranslation.value = nextPanTranslation + panTranslation.set(nextPanTranslation) }) .onEnd(() => { 'worklet' // Commit just the pan. let t = createTransform() - prependPan(t, panTranslation.value) - prependTransform(t, committedTransform.value) + prependPan(t, panTranslation.get()) + prependTransform(t, committedTransform.get()) applyRounding(t) - committedTransform.value = t + committedTransform.set(t) // Reset just the pan. - panTranslation.value = {x: 0, y: 0} + panTranslation.set({x: 0, y: 0}) }) const singleTap = Gesture.Tap().onEnd(() => { @@ -261,11 +262,11 @@ const ImageItem = ({ if (!imageDimensions || !imageAspect) { return } - const [, , committedScale] = readTransform(committedTransform.value) + const [, , committedScale] = readTransform(committedTransform.get()) if (committedScale !== 1) { // Go back to 1:1 using the identity vector. let t = createTransform() - committedTransform.value = withClampedSpring(t) + committedTransform.set(withClampedSpring(t)) return } @@ -299,7 +300,7 @@ const ImageItem = ({ ) const finalTransform = createTransform() prependPinch(finalTransform, scale, origin, {x: dx, y: dy}) - committedTransform.value = withClampedSpring(finalTransform) + committedTransform.set(withClampedSpring(finalTransform)) }) const composedGesture = isScrollViewBeingDragged @@ -313,13 +314,13 @@ const ImageItem = ({ ) const containerStyle = useAnimatedStyle(() => { - const {scaleAndMoveTransform, isHidden} = transforms.value + const {scaleAndMoveTransform, isHidden} = transforms.get() // Apply the active adjustments on top of the committed transform before the gestures. // This is matrix multiplication, so operations are applied in the reverse order. let t = createTransform() - prependPan(t, panTranslation.value) - prependPinch(t, pinchScale.value, pinchOrigin.value, pinchTranslation.value) - prependTransform(t, committedTransform.value) + prependPan(t, panTranslation.get()) + prependPinch(t, pinchScale.get(), pinchOrigin.get(), pinchTranslation.get()) + prependTransform(t, committedTransform.get()) const [translateX, translateY, scale] = readTransform(t) const manipulationTransform = [ {translateX}, @@ -338,7 +339,7 @@ const ImageItem = ({ }) const imageCropStyle = useAnimatedStyle(() => { - const {cropFrameTransform} = transforms.value + const {cropFrameTransform} = transforms.get() return { flex: 1, overflow: 'hidden', @@ -347,7 +348,7 @@ const ImageItem = ({ }) const imageStyle = useAnimatedStyle(() => { - const {cropContentTransform} = transforms.value + const {cropContentTransform} = transforms.get() return { flex: 1, transform: cropContentTransform, @@ -359,7 +360,7 @@ const ImageItem = ({ const [hasLoaded, setHasLoaded] = useState(false) useAnimatedReaction( () => { - return transforms.value.isResting && !hasLoaded + return transforms.get().isResting && !hasLoaded }, (show, prevShow) => { if (show && !prevShow) { diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index f06a59ed6..c7be4f3e3 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -148,7 +148,7 @@ const ImageItem = ({ ) const containerStyle = useAnimatedStyle(() => { - const {scaleAndMoveTransform, isHidden} = transforms.value + const {scaleAndMoveTransform, isHidden} = transforms.get() return { flex: 1, transform: scaleAndMoveTransform, @@ -158,7 +158,7 @@ const ImageItem = ({ const imageCropStyle = useAnimatedStyle(() => { const screenSize = measureSafeArea() - const {cropFrameTransform} = transforms.value + const {cropFrameTransform} = transforms.get() return { overflow: 'hidden', transform: cropFrameTransform, @@ -171,7 +171,7 @@ const ImageItem = ({ }) const imageStyle = useAnimatedStyle(() => { - const {cropContentTransform} = transforms.value + const {cropContentTransform} = transforms.get() return { transform: cropContentTransform, width: '100%', @@ -184,7 +184,7 @@ const ImageItem = ({ const [hasLoaded, setHasLoaded] = useState(false) useAnimatedReaction( () => { - return transforms.value.isResting && !hasLoaded + return transforms.get().isResting && !hasLoaded }, (show, prevShow) => { if (show && !prevShow) { diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 68857f62d..4ba056eb0 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -109,18 +109,22 @@ export default function ImageViewRoot({ // https://github.com/software-mansion/react-native-reanimated/issues/6677 requestAnimationFrame(() => { - openProgress.value = canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1 + openProgress.set(() => + canAnimate ? withClampedSpring(1, SLOW_SPRING) : 1, + ) }) return () => { // https://github.com/software-mansion/react-native-reanimated/issues/6677 requestAnimationFrame(() => { - openProgress.value = canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0 + openProgress.set(() => + canAnimate ? withClampedSpring(0, SLOW_SPRING) : 0, + ) }) } }, [nextLightbox, openProgress]) useAnimatedReaction( - () => openProgress.value === 0, + () => openProgress.get() === 0, (isGone, wasGone) => { if (isGone && !wasGone) { runOnJS(setActiveLightbox)(null) @@ -130,7 +134,7 @@ export default function ImageViewRoot({ const onFlyAway = React.useCallback(() => { 'worklet' - openProgress.value = 0 + openProgress.set(0) runOnJS(onRequestClose)() }, [onRequestClose, openProgress]) @@ -187,7 +191,7 @@ function ImageView({ const isFlyingAway = useSharedValue(false) const containerStyle = useAnimatedStyle(() => { - if (openProgress.value < 1 || isFlyingAway.value) { + if (openProgress.get() < 1 || isFlyingAway.get()) { return {pointerEvents: 'none'} } return {pointerEvents: 'auto'} @@ -196,11 +200,12 @@ function ImageView({ const backdropStyle = useAnimatedStyle(() => { const screenSize = measure(safeAreaRef) let opacity = 1 - if (openProgress.value < 1) { - opacity = Math.sqrt(openProgress.value) + const openProgressValue = openProgress.get() + if (openProgressValue < 1) { + opacity = Math.sqrt(openProgressValue) } else if (screenSize) { const dragProgress = Math.min( - Math.abs(dismissSwipeTranslateY.value) / (screenSize.height / 2), + Math.abs(dismissSwipeTranslateY.get()) / (screenSize.height / 2), 1, ) opacity -= dragProgress @@ -212,11 +217,11 @@ function ImageView({ }) const animatedHeaderStyle = useAnimatedStyle(() => { - const show = showControls && dismissSwipeTranslateY.value === 0 + const show = showControls && dismissSwipeTranslateY.get() === 0 return { pointerEvents: show ? 'box-none' : 'none', opacity: withClampedSpring( - show && openProgress.value === 1 ? 1 : 0, + show && openProgress.get() === 1 ? 1 : 0, FAST_SPRING, ), transform: [ @@ -227,12 +232,12 @@ function ImageView({ } }) const animatedFooterStyle = useAnimatedStyle(() => { - const show = showControls && dismissSwipeTranslateY.value === 0 + const show = showControls && dismissSwipeTranslateY.get() === 0 return { flexGrow: 1, pointerEvents: show ? 'box-none' : 'none', opacity: withClampedSpring( - show && openProgress.value === 1 ? 1 : 0, + show && openProgress.get() === 1 ? 1 : 0, FAST_SPRING, ), transform: [ @@ -259,7 +264,7 @@ function ImageView({ const screenSize = measure(safeAreaRef) return ( !screenSize || - Math.abs(dismissSwipeTranslateY.value) > screenSize.height + Math.abs(dismissSwipeTranslateY.get()) > screenSize.height ) }, (isOut, wasOut) => { @@ -397,10 +402,11 @@ function LightboxImage({ const transforms = useDerivedValue(() => { 'worklet' const safeArea = measureSafeArea() + const openProgressValue = openProgress.get() const dismissTranslateY = - isActive && openProgress.value === 1 ? dismissSwipeTranslateY.value : 0 + isActive && openProgressValue === 1 ? dismissSwipeTranslateY.get() : 0 - if (openProgress.value === 0 && isFlyingAway.value) { + if (openProgressValue === 0 && isFlyingAway.get()) { return { isHidden: true, isResting: false, @@ -410,9 +416,9 @@ function LightboxImage({ } } - if (isActive && thumbRect && imageAspect && openProgress.value < 1) { + if (isActive && thumbRect && imageAspect && openProgressValue < 1) { return interpolateTransform( - openProgress.value, + openProgressValue, thumbRect, safeArea, imageAspect, @@ -434,33 +440,37 @@ function LightboxImage({ .maxPointers(1) .onUpdate(e => { 'worklet' - if (openProgress.value !== 1 || isFlyingAway.value) { + if (openProgress.get() !== 1 || isFlyingAway.get()) { return } - dismissSwipeTranslateY.value = e.translationY + dismissSwipeTranslateY.set(e.translationY) }) .onEnd(e => { 'worklet' - if (openProgress.value !== 1 || isFlyingAway.value) { + if (openProgress.get() !== 1 || isFlyingAway.get()) { return } if (Math.abs(e.velocityY) > 200) { - isFlyingAway.value = true - if (dismissSwipeTranslateY.value === 0) { + isFlyingAway.set(true) + if (dismissSwipeTranslateY.get() === 0) { // HACK: If the initial value is 0, withDecay() animation doesn't start. // This is a bug in Reanimated, but for now we'll work around it like this. - dismissSwipeTranslateY.value = 1 + dismissSwipeTranslateY.set(1) } - dismissSwipeTranslateY.value = withDecay({ - velocity: e.velocityY, - velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. - deceleration: 1, // Danger! This relies on the reaction below stopping it. - }) + dismissSwipeTranslateY.set(() => + withDecay({ + velocity: e.velocityY, + velocityFactor: Math.max(3500 / Math.abs(e.velocityY), 1), // Speed up if it's too slow. + deceleration: 1, // Danger! This relies on the reaction below stopping it. + }), + ) } else { - dismissSwipeTranslateY.value = withSpring(0, { - stiffness: 700, - damping: 50, - }) + dismissSwipeTranslateY.set(() => + withSpring(0, { + stiffness: 700, + damping: 50, + }), + ) } }) diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx index 6d601c289..92b98dc2e 100644 --- a/src/view/com/pager/PagerWithHeader.tsx +++ b/src/view/com/pager/PagerWithHeader.tsx @@ -131,11 +131,11 @@ export const PagerWithHeader = React.forwardRef( const lastForcedScrollY = useSharedValue(0) const adjustScrollForOtherPages = () => { 'worklet' - const currentScrollY = scrollY.value + const currentScrollY = scrollY.get() const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight) - if (lastForcedScrollY.value !== forcedScrollY) { - lastForcedScrollY.value = forcedScrollY - const refs = scrollRefs.value + if (lastForcedScrollY.get() !== forcedScrollY) { + lastForcedScrollY.set(forcedScrollY) + const refs = scrollRefs.get() for (let i = 0; i < refs.length; i++) { const scollRef = refs[i] if (i !== currentPage && scollRef != null) { @@ -167,7 +167,7 @@ export const PagerWithHeader = React.forwardRef( const isPossiblyInvalid = headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight if (!isPossiblyInvalid) { - scrollY.value = nextScrollY + scrollY.set(nextScrollY) runOnJS(queueThrottledOnScroll)() } }, @@ -246,7 +246,7 @@ let PagerTabBar = ({ allowHeaderOverScroll?: boolean }): React.ReactNode => { const headerTransform = useAnimatedStyle(() => { - const translateY = Math.min(scrollY.value, headerOnlyHeight) * -1 + const translateY = Math.min(scrollY.get(), headerOnlyHeight) * -1 return { transform: [ { diff --git a/src/view/com/util/BottomSheetCustomBackdrop.tsx b/src/view/com/util/BottomSheetCustomBackdrop.tsx index 25e882e87..86751861f 100644 --- a/src/view/com/util/BottomSheetCustomBackdrop.tsx +++ b/src/view/com/util/BottomSheetCustomBackdrop.tsx @@ -18,7 +18,7 @@ export function createCustomBackdrop( // animated variables const opacity = useAnimatedStyle(() => ({ opacity: interpolate( - animatedIndex.value, // current snap index + animatedIndex.get(), // current snap index [-1, 0], // input range [0, 0.5], // output range Extrapolation.CLAMP, diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 52314f954..4ee4d7d0b 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -79,8 +79,8 @@ function ListImpl( onScrollFromContext?.(e, ctx) const didScrollDown = e.contentOffset.y > SCROLLED_DOWN_LIMIT - if (isScrolledDown.value !== didScrollDown) { - isScrolledDown.value = didScrollDown + if (isScrolledDown.get() !== didScrollDown) { + isScrolledDown.set(didScrollDown) if (onScrolledDownChange != null) { runOnJS(handleScrolledDownChange)(didScrollDown) } diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 193d07d72..0d084993b 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -44,7 +44,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (v: boolean) => { 'worklet' cancelAnimation(headerMode) - headerMode.value = v ? V1.value : V0.value + headerMode.set(v ? V1.get() : V0.get()) }, [headerMode], ) @@ -52,9 +52,9 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { useEffect(() => { if (isWeb) { return listenToForcedWindowScroll(() => { - startDragOffset.value = null - startMode.value = null - didJustRestoreScroll.value = true + startDragOffset.set(null) + startMode.set(null) + didJustRestoreScroll.set(true) }) } }) @@ -63,13 +63,14 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (e: NativeScrollEvent) => { 'worklet' if (isNative) { - if (startDragOffset.value === null) { + const startDragOffsetValue = startDragOffset.get() + if (startDragOffsetValue === null) { return } - const didScrollDown = e.contentOffset.y > startDragOffset.value - startDragOffset.value = null - startMode.value = null - if (e.contentOffset.y < headerHeight.value) { + const didScrollDown = e.contentOffset.y > startDragOffsetValue + startDragOffset.set(null) + startMode.set(null) + if (e.contentOffset.y < headerHeight.get()) { // If we're close to the top, show the shell. setMode(false) } else if (didScrollDown) { @@ -77,7 +78,7 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { setMode(true) } else { // Snap to whichever state is the closest. - setMode(Math.round(headerMode.value) === 1) + setMode(Math.round(headerMode.get()) === 1) } } }, @@ -88,8 +89,8 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (e: NativeScrollEvent) => { 'worklet' if (isNative) { - startDragOffset.value = e.contentOffset.y - startMode.value = headerMode.value + startDragOffset.set(e.contentOffset.y) + startMode.set(headerMode.get()) } }, [headerMode, startDragOffset, startMode], @@ -123,10 +124,12 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { (e: NativeScrollEvent) => { 'worklet' if (isNative) { - if (startDragOffset.value === null || startMode.value === null) { + const startDragOffsetValue = startDragOffset.get() + const startModeValue = startMode.get() + if (startDragOffsetValue === null || startModeValue === null) { if ( - headerMode.value !== 0 && - e.contentOffset.y < headerHeight.value + headerMode.get() !== 0 && + e.contentOffset.y < headerHeight.get() ) { // If we're close enough to the top, always show the shell. // Even if we're not dragging. @@ -137,29 +140,29 @@ export function MainScrollProvider({children}: {children: React.ReactNode}) { // The "mode" value is always between 0 and 1. // Figure out how much to move it based on the current dragged distance. - const dy = e.contentOffset.y - startDragOffset.value + const dy = e.contentOffset.y - startDragOffsetValue const dProgress = interpolate( dy, - [-headerHeight.value, headerHeight.value], + [-headerHeight.get(), headerHeight.get()], [-1, 1], ) - const newValue = clamp(startMode.value + dProgress, 0, 1) - if (newValue !== headerMode.value) { + const newValue = clamp(startModeValue + dProgress, 0, 1) + if (newValue !== headerMode.get()) { // Manually adjust the value. This won't be (and shouldn't be) animated. // Cancel any any existing animation cancelAnimation(headerMode) - headerMode.value = newValue + headerMode.set(newValue) } } else { - if (didJustRestoreScroll.value) { - didJustRestoreScroll.value = false + if (didJustRestoreScroll.get()) { + didJustRestoreScroll.set(false) // Don't hide/show navbar based on scroll restoratoin. return } // On the web, we don't try to follow the drag because we don't know when it ends. // Instead, show/hide immediately based on whether we're scrolling up or down. - const dy = e.contentOffset.y - (startDragOffset.value ?? 0) - startDragOffset.value = e.contentOffset.y + const dy = e.contentOffset.y - (startDragOffset.get() ?? 0) + startDragOffset.set(e.contentOffset.y) if (dy < 0 || e.contentOffset.y < WEB_HIDE_SHELL_THRESHOLD) { setMode(false) diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 855ba21b2..1d1023c2b 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -134,7 +134,7 @@ export function BottomBar({navigation}: BottomTabBarProps) { footerMinimalShellTransform, ]} onLayout={e => { - footerHeight.value = e.nativeEvent.layout.height + footerHeight.set(e.nativeEvent.layout.height) }}> {hasSession ? ( <> -- cgit 1.4.1 From 7b6c18272385494145676e42cbe922bde7ceae6b Mon Sep 17 00:00:00 2001 From: dan Date: Mon, 18 Nov 2024 22:21:47 +0000 Subject: Add useHandleRef as a lighter alternative for useAnimatedRef (#6500) --- src/lib/hooks/useHandleRef.ts | 39 +++++++++++++++++++++++++++ src/screens/Profile/Header/Shell.tsx | 18 +++++-------- src/view/com/profile/ProfileSubpageHeader.tsx | 18 +++++-------- src/view/com/util/images/AutoSizedImage.tsx | 13 ++++----- src/view/com/util/images/Gallery.tsx | 13 ++++----- src/view/com/util/images/ImageLayoutGrid.tsx | 14 +++++----- src/view/com/util/post-embeds/index.tsx | 14 ++++------ 7 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 src/lib/hooks/useHandleRef.ts (limited to 'src/lib/hooks') diff --git a/src/lib/hooks/useHandleRef.ts b/src/lib/hooks/useHandleRef.ts new file mode 100644 index 000000000..167ba270b --- /dev/null +++ b/src/lib/hooks/useHandleRef.ts @@ -0,0 +1,39 @@ +import {useState} from 'react' +import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated' + +export type HandleRef = { + (node: any): void + current: null | number +} + +// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions. +// Render it like , then pass `ref.current` to `measureHandle()` and such. +export function useHandleRef(): HandleRef { + return useState(() => { + const ref = (node: any) => { + if (node) { + ref.current = + node._nativeTag ?? + node.__nativeTag ?? + node.canonical?.nativeTag ?? + null + } else { + ref.current = null + } + } + ref.current = null + return ref + })[0] as HandleRef +} + +// When using this version, you need to read ref.current on the JS thread, and pass it to UI. +export function measureHandle( + current: number | null, +): MeasuredDimensions | null { + 'worklet' + if (current !== null) { + return measure((() => current) as AnimatedRef) + } else { + return null + } +} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 1a1e7d4a2..573d38145 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -1,12 +1,6 @@ import React, {memo} from 'react' import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' -import Animated, { - measure, - MeasuredDimensions, - runOnJS, - runOnUI, - useAnimatedRef, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' @@ -14,6 +8,7 @@ import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {BACK_HITSLOP} from '#/lib/constants' +import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {NavigationProp} from '#/lib/routes/types' import {isIOS} from '#/platform/detection' @@ -49,7 +44,7 @@ let ProfileHeaderShell = ({ const {openLightbox} = useLightboxControls() const navigation = useNavigation() const {isDesktop} = useWebMediaQueries() - const aviRef = useAnimatedRef() + const aviRef = useHandleRef() const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { @@ -86,9 +81,10 @@ let ProfileHeaderShell = ({ const modui = moderation.ui('avatar') const avatar = profile.avatar if (avatar && !(modui.blur && modui.noOverride)) { + const aviHandle = aviRef.current runOnUI(() => { 'worklet' - const rect = measure(aviRef) + const rect = measureHandle(aviHandle) runOnJS(_openLightbox)(avatar, rect) })() } @@ -170,14 +166,14 @@ let ProfileHeaderShell = ({ styles.avi, profile.associated?.labeler && styles.aviLabeler, ]}> - + - + diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index d73b322f2..0e25fe5e6 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -1,18 +1,13 @@ import React from 'react' import {Pressable, StyleSheet, View} from 'react-native' -import Animated, { - measure, - MeasuredDimensions, - runOnJS, - runOnUI, - useAnimatedRef, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {BACK_HITSLOP} from '#/lib/constants' +import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {makeProfileLink} from '#/lib/routes/links' @@ -60,7 +55,7 @@ export function ProfileSubpageHeader({ const {openLightbox} = useLightboxControls() const pal = usePalette('default') const canGoBack = navigation.canGoBack() - const aviRef = useAnimatedRef() + const aviRef = useHandleRef() const onPressBack = React.useCallback(() => { if (navigation.canGoBack()) { @@ -101,9 +96,10 @@ export function ProfileSubpageHeader({ if ( avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride) ) { + const aviHandle = aviRef.current runOnUI(() => { 'worklet' - const rect = measure(aviRef) + const rect = measureHandle(aviHandle) runOnJS(_openLightbox)(avatar, rect) })() } @@ -155,7 +151,7 @@ export function ProfileSubpageHeader({ paddingBottom: 6, paddingHorizontal: isMobile ? 12 : 14, }}> - + )} - + {isLoading ? ( >, - fetchedDims: Dimensions | null, - ) => void + onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void onLongPress?: () => void onPressIn?: () => void }) { const t = useTheme() const {_} = useLingui() const largeAlt = useLargeAltBadgeEnabled() - const containerRef = useAnimatedRef() + const containerRef = useHandleRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) let aspectRatio: number | undefined @@ -109,7 +106,7 @@ export function AutoSizedImage({ const hasAlt = !!image.alt const contents = ( - + ) : null} - + ) if (cropDisabled) { diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index 9d0817bd2..cc3eda68d 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,11 +1,11 @@ import React from 'react' import {Pressable, StyleProp, View, ViewStyle} from 'react-native' -import Animated, {AnimatedRef} from 'react-native-reanimated' import {Image, ImageStyle} from 'expo-image' import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {HandleRef} from '#/lib/hooks/useHandleRef' import {Dimensions} from '#/lib/media/types' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' @@ -20,7 +20,7 @@ interface Props { index: number onPress?: ( index: number, - containerRefs: AnimatedRef>[], + containerRefs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: EventFunction @@ -28,7 +28,7 @@ interface Props { imageStyle?: StyleProp viewContext?: PostEmbedViewContext insetBorderStyle?: StyleProp - containerRefs: AnimatedRef>[] + containerRefs: HandleRef[] thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]> } @@ -52,10 +52,7 @@ export function GalleryItem({ const hideBadges = viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia return ( - + ) : null} - + ) } diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index dcc330dac..16ea9d453 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,8 +1,8 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {AnimatedRef, useAnimatedRef} from 'react-native-reanimated' import {AppBskyEmbedImages} from '@atproto/api' +import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' import {atoms as a, useBreakpoints} from '#/alf' import {Dimensions} from '../../lightbox/ImageViewing/@types' @@ -12,7 +12,7 @@ interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRefs: AnimatedRef>[], + containerRefs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: (index: number) => void @@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRefs: AnimatedRef>[], + containerRefs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => void onLongPress?: (index: number) => void @@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const gap = props.gap const count = props.images.length - const containerRef1 = useAnimatedRef() - const containerRef2 = useAnimatedRef() - const containerRef3 = useAnimatedRef() - const containerRef4 = useAnimatedRef() + const containerRef1 = useHandleRef() + const containerRef2 = useHandleRef() + const containerRef3 = useHandleRef() + const containerRef4 = useHandleRef() const thumbDimsRef = React.useRef<(Dimensions | null)[]>([]) switch (count) { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index 1351a2cbc..9dc43da8e 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -6,13 +6,7 @@ import { View, ViewStyle, } from 'react-native' -import { - AnimatedRef, - measure, - MeasuredDimensions, - runOnJS, - runOnUI, -} from 'react-native-reanimated' +import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {Image} from 'expo-image' import { AppBskyEmbedExternal, @@ -27,6 +21,7 @@ import { ModerationDecision, } from '@atproto/api' +import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef' import {usePalette} from '#/lib/hooks/usePalette' import {useLightboxControls} from '#/state/lightbox' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -163,12 +158,13 @@ export function PostEmbeds({ } const onPress = ( index: number, - refs: AnimatedRef>[], + refs: HandleRef[], fetchedDims: (Dimensions | null)[], ) => { + const handles = refs.map(r => r.current) runOnUI(() => { 'worklet' - const rects = refs.map(ref => (ref ? measure(ref) : null)) + const rects = handles.map(measureHandle) runOnJS(_openLightbox)(index, rects, fetchedDims) })() } -- cgit 1.4.1 From ac5b2cf31f2bb45f1bf8a180705249d3cce8017d Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 22 Nov 2024 23:28:45 +0000 Subject: Pass referrer on native (with an opt out) (#6648) * Pass referer on native * Add ChainLink3 * Add an opt out for sending utm * Remove noreferrer on links We do have in HTML, should be sufficient. * Narrow down the condition slightly --------- Co-authored-by: Eric Bailey --- .../icons/chainLink3_stroke2_corner0_rounded.svg | 1 + src/components/Link.tsx | 4 +- src/components/icons/ChainLink.tsx | 5 ++ src/lib/hooks/useOpenLink.ts | 24 +++++++++- src/screens/Settings/ContentAndMediaSettings.tsx | 56 +++++++++++++++++----- src/state/persisted/schema.ts | 2 + src/state/preferences/index.tsx | 5 +- src/state/preferences/opt-out-of-utm.tsx | 42 ++++++++++++++++ src/view/com/util/Link.tsx | 2 +- 9 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 assets/icons/chainLink3_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/ChainLink.tsx create mode 100644 src/state/preferences/opt-out-of-utm.tsx (limited to 'src/lib/hooks') diff --git a/assets/icons/chainLink3_stroke2_corner0_rounded.svg b/assets/icons/chainLink3_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..c1626cc61 --- /dev/null +++ b/assets/icons/chainLink3_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/Link.tsx b/src/components/Link.tsx index a5203b252..ef31ea0c5 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -223,7 +223,7 @@ export function Link({ {...web({ hrefAttrs: { target: download ? undefined : isExternal ? 'blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, + rel: isExternal ? 'noopener' : undefined, download, }, dataSet: { @@ -307,7 +307,7 @@ export function InlineLinkText({ {...web({ hrefAttrs: { target: download ? undefined : isExternal ? 'blank' : undefined, - rel: isExternal ? 'noopener noreferrer' : undefined, + rel: isExternal ? 'noopener' : undefined, download, }, dataSet: { diff --git a/src/components/icons/ChainLink.tsx b/src/components/icons/ChainLink.tsx new file mode 100644 index 000000000..ba0b417a9 --- /dev/null +++ b/src/components/icons/ChainLink.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const ChainLink3_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z', +}) diff --git a/src/lib/hooks/useOpenLink.ts b/src/lib/hooks/useOpenLink.ts index 5b75695b8..727821670 100644 --- a/src/lib/hooks/useOpenLink.ts +++ b/src/lib/hooks/useOpenLink.ts @@ -4,12 +4,14 @@ import * as WebBrowser from 'expo-web-browser' import { createBskyAppAbsoluteUrl, + isBskyAppUrl, isBskyRSSUrl, isRelativeUrl, } from '#/lib/strings/url-helpers' import {isNative} from '#/platform/detection' import {useModalControls} from '#/state/modals' import {useInAppBrowser} from '#/state/preferences/in-app-browser' +import {useOptOutOfUtm} from '#/state/preferences/opt-out-of-utm' import {useTheme} from '#/alf' import {useSheetWrapper} from '#/components/Dialog/sheet-wrapper' @@ -18,6 +20,7 @@ export function useOpenLink() { const enabled = useInAppBrowser() const t = useTheme() const sheetWrapper = useSheetWrapper() + const optOutOfUtm = useOptOutOfUtm() const openLink = useCallback( async (url: string, override?: boolean) => { @@ -26,6 +29,9 @@ export function useOpenLink() { } if (isNative && !url.startsWith('mailto:')) { + if (!optOutOfUtm && !isBskyAppUrl(url) && url.startsWith('http')) { + url = addUtmSource(url) + } if (override === undefined && enabled === undefined) { openModal({ name: 'in-app-browser-consent', @@ -47,8 +53,24 @@ export function useOpenLink() { } Linking.openURL(url) }, - [enabled, openModal, t, sheetWrapper], + [enabled, openModal, t, sheetWrapper, optOutOfUtm], ) return openLink } + +function addUtmSource(url: string): string { + let parsedUrl + try { + parsedUrl = new URL(url) + } catch (e) { + return url + } + if (!parsedUrl.searchParams.has('utm_source')) { + parsedUrl.searchParams.set('utm_source', 'bluesky') + if (!parsedUrl.searchParams.has('utm_medium')) { + parsedUrl.searchParams.set('utm_medium', 'social') + } + } + return parsedUrl.toString() +} diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index b3fb8c174..27448ba9a 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -9,9 +9,16 @@ import { useInAppBrowser, useSetInAppBrowser, } from '#/state/preferences/in-app-browser' +import { + useOptOutOfUtm, + useSetOptOutOfUtm, +} from '#/state/preferences/opt-out-of-utm' import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' import * as Toggle from '#/components/forms/Toggle' import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' +import {ChainLink3_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag' import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home' import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh' @@ -29,6 +36,8 @@ export function ContentAndMediaSettingsScreen({}: Props) { const setAutoplayDisabledPref = useSetAutoplayDisabled() const inAppBrowserPref = useInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser() + const optOutOfUtm = useOptOutOfUtm() + const setOptOutOfUtm = useSetOptOutOfUtm() return ( @@ -68,6 +77,19 @@ export function ContentAndMediaSettingsScreen({}: Props) { + setAutoplayDisabledPref(!value)}> + + + + Autoplay videos and GIFs + + + + {isNative && ( )} - setAutoplayDisabledPref(!value)}> + {isNative && } + {isNative && ( + setOptOutOfUtm(!value)}> + + + + Send Bluesky referrer + + + + + )} + {isNative && ( - - - Autoplay videos and GIFs - - + + + Helps external sites estimate traffic from Bluesky. + + - + )} diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index 804017949..85a6bf8e2 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -124,6 +124,7 @@ const schema = z.object({ subtitlesEnabled: z.boolean().optional(), /** @deprecated */ mutedThreads: z.array(z.string()), + optOutOfUtm: z.boolean().optional(), }) export type Schema = z.infer @@ -169,6 +170,7 @@ export const defaults: Schema = { kawaii: false, hasCheckedForStarterPack: false, subtitlesEnabled: true, + optOutOfUtm: false, } export function tryParse(rawData: string): Schema | undefined { diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index c7eaf2726..43a08926e 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -9,6 +9,7 @@ import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' import {Provider as LargeAltBadgeProvider} from './large-alt-badge' +import {Provider as OutOutOfUtmProvider} from './opt-out-of-utm' import {Provider as SubtitlesProvider} from './subtitles' import {Provider as UsedStarterPacksProvider} from './used-starter-packs' @@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { - {children} + + {children} + diff --git a/src/state/preferences/opt-out-of-utm.tsx b/src/state/preferences/opt-out-of-utm.tsx new file mode 100644 index 000000000..40144c8db --- /dev/null +++ b/src/state/preferences/opt-out-of-utm.tsx @@ -0,0 +1,42 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = boolean +type SetContext = (v: boolean) => void + +const stateContext = React.createContext( + Boolean(persisted.defaults.optOutOfUtm), +) +const setContext = React.createContext((_: boolean) => {}) + +export function Provider({children}: {children: React.ReactNode}) { + const [state, setState] = React.useState( + Boolean(persisted.get('optOutOfUtm')), + ) + + const setStateWrapped = React.useCallback( + (optOutOfUtm: persisted.Schema['optOutOfUtm']) => { + setState(Boolean(optOutOfUtm)) + persisted.write('optOutOfUtm', optOutOfUtm) + }, + [setState], + ) + + React.useEffect(() => { + return persisted.onUpdate('optOutOfUtm', nextOptOutOfUtm => { + setState(Boolean(nextOptOutOfUtm)) + }) + }, [setStateWrapped]) + + return ( + + + {children} + + + ) +} + +export const useOptOutOfUtm = () => React.useContext(stateContext) +export const useSetOptOutOfUtm = () => React.useContext(setContext) diff --git a/src/view/com/util/Link.tsx b/src/view/com/util/Link.tsx index 489fbc59c..f83258e45 100644 --- a/src/view/com/util/Link.tsx +++ b/src/view/com/util/Link.tsx @@ -256,7 +256,7 @@ export const TextLink = memo(function TextLink({ if (isExternal) { return { target: '_blank', - // rel: 'noopener noreferrer', + // rel: 'noopener', } } return {} -- cgit 1.4.1