diff --git a/src/lib/hooks/useHandleRef.ts b/src/lib/hooks/useHandleRef.ts
new file mode 100644
index 0000000000..167ba270b6
--- /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 1a1e7d4a24..573d38145d 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 d73b322f2e..0e25fe5e61 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 9d0817bd2f..cc3eda68d2 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 dcc330dace..16ea9d453f 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 1351a2cbc3..9dc43da8e7 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)
})()
}