about summary refs log tree commit diff
path: root/src/view
diff options
context:
space:
mode:
authorhailey <me@haileyok.com>2025-06-12 10:46:22 -0700
committerGitHub <noreply@github.com>2025-06-12 10:46:22 -0700
commit477e5f4ecfaa0007aeed90b51274c78a730c1a9e (patch)
tree45cd5cfff9eab1bd52b5ade6c60efebe3cc5e6b6 /src/view
parenta26b20b56cd0ac80f625a5eb5136b805b9341e8d (diff)
downloadvoidsky-477e5f4ecfaa0007aeed90b51274c78a730c1a9e.tar.zst
new arch (#8295)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: Charlotte Som <charlotte@som.codes>
Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/view')
-rw-r--r--src/view/com/composer/text-input/TextInput.tsx14
-rw-r--r--src/view/com/pager/Pager.tsx220
-rw-r--r--src/view/com/pager/Pager.web.tsx47
-rw-r--r--src/view/com/pager/PagerWithHeader.tsx315
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx24
-rw-r--r--src/view/com/util/Toast.web.tsx1
-rw-r--r--src/view/com/util/images/AutoSizedImage.tsx22
-rw-r--r--src/view/com/util/images/Gallery.tsx16
-rw-r--r--src/view/com/util/images/ImageLayoutGrid.tsx20
-rw-r--r--src/view/com/util/post-embeds/index.tsx11
10 files changed, 359 insertions, 331 deletions
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 6f5e812ed..f927015af 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -1,5 +1,5 @@
 import React, {
-  ComponentProps,
+  type ComponentProps,
   forwardRef,
   useCallback,
   useMemo,
@@ -7,16 +7,16 @@ import React, {
   useState,
 } from 'react'
 import {
-  NativeSyntheticEvent,
+  type NativeSyntheticEvent,
   Text as RNText,
-  TextInput as RNTextInput,
-  TextInputSelectionChangeEventData,
+  type TextInput as RNTextInput,
+  type TextInputSelectionChangeEventData,
   View,
 } from 'react-native'
 import {AppBskyRichtextFacet, RichText} from '@atproto/api'
 import PasteInput, {
-  PastedFile,
-  PasteInputRef,
+  type PastedFile,
+  type PasteInputRef, // @ts-expect-error no types when installing from github
 } from '@mattermost/react-native-paste-input'
 
 import {POST_IMG_MAX} from '#/lib/constants'
@@ -27,7 +27,7 @@ import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip'
 import {useTheme} from '#/lib/ThemeContext'
 import {isAndroid, isNative} from '#/platform/detection'
 import {
-  LinkFacetMatch,
+  type LinkFacetMatch,
   suggestLinkCardUri,
 } from '#/view/com/composer/text-input/text-input-util'
 import {atoms as a, useAlf} from '#/alf'
diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx
index f62bffc53..8cc346903 100644
--- a/src/view/com/pager/Pager.tsx
+++ b/src/view/com/pager/Pager.tsx
@@ -1,16 +1,22 @@
-import React, {forwardRef, useCallback, useContext} from 'react'
+import {
+  useCallback,
+  useContext,
+  useImperativeHandle,
+  useRef,
+  useState,
+} from 'react'
 import {View} from 'react-native'
 import {DrawerGestureContext} from 'react-native-drawer-layout'
 import {Gesture, GestureDetector} from 'react-native-gesture-handler'
 import PagerView, {
-  PagerViewOnPageScrollEventData,
-  PagerViewOnPageSelectedEvent,
-  PagerViewOnPageSelectedEventData,
-  PageScrollStateChangedNativeEventData,
+  type PagerViewOnPageScrollEventData,
+  type PagerViewOnPageSelectedEvent,
+  type PagerViewOnPageSelectedEventData,
+  type PageScrollStateChangedNativeEventData,
 } from 'react-native-pager-view'
 import Animated, {
   runOnJS,
-  SharedValue,
+  type SharedValue,
   useEvent,
   useHandler,
   useSharedValue,
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
 
 interface Props {
+  ref?: React.Ref<PagerRef>
   initialPage?: number
   renderTabBar: RenderTabBarFn
+  // tab pressed, yet to scroll to page
+  onTabPressed?: (index: number) => void
+  // scroll settled
   onPageSelected?: (index: number) => void
   onPageScrollStateChanged?: (
     scrollState: 'idle' | 'dragging' | 'settling',
@@ -47,114 +57,112 @@ interface Props {
 
 const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
 
-export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
-  function PagerImpl(
-    {
-      children,
-      initialPage = 0,
-      renderTabBar,
-      onPageScrollStateChanged: parentOnPageScrollStateChanged,
-      onPageSelected: parentOnPageSelected,
-      testID,
-    }: React.PropsWithChildren<Props>,
-    ref,
-  ) {
-    const [selectedPage, setSelectedPage] = React.useState(initialPage)
-    const pagerView = React.useRef<PagerView>(null)
+export function Pager({
+  ref,
+  children,
+  initialPage = 0,
+  renderTabBar,
+  onPageSelected: parentOnPageSelected,
+  onTabPressed: parentOnTabPressed,
+  onPageScrollStateChanged: parentOnPageScrollStateChanged,
+  testID,
+}: React.PropsWithChildren<Props>) {
+  const [selectedPage, setSelectedPage] = useState(initialPage)
+  const pagerView = useRef<PagerView>(null)
 
-    const [isIdle, setIsIdle] = React.useState(true)
-    const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
-    useFocusEffect(
-      useCallback(() => {
-        const canSwipeDrawer = selectedPage === 0 && isIdle
-        setDrawerSwipeDisabled(!canSwipeDrawer)
-        return () => {
-          setDrawerSwipeDisabled(false)
-        }
-      }, [setDrawerSwipeDisabled, selectedPage, isIdle]),
-    )
+  const [isIdle, setIsIdle] = useState(true)
+  const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
+  useFocusEffect(
+    useCallback(() => {
+      const canSwipeDrawer = selectedPage === 0 && isIdle
+      setDrawerSwipeDisabled(!canSwipeDrawer)
+      return () => {
+        setDrawerSwipeDisabled(false)
+      }
+    }, [setDrawerSwipeDisabled, selectedPage, isIdle]),
+  )
 
-    React.useImperativeHandle(ref, () => ({
-      setPage: (index: number) => {
-        pagerView.current?.setPage(index)
-      },
-    }))
+  useImperativeHandle(ref, () => ({
+    setPage: (index: number) => {
+      pagerView.current?.setPage(index)
+    },
+  }))
 
-    const onPageSelectedJSThread = React.useCallback(
-      (nextPosition: number) => {
-        setSelectedPage(nextPosition)
-        parentOnPageSelected?.(nextPosition)
-      },
-      [setSelectedPage, parentOnPageSelected],
-    )
+  const onPageSelectedJSThread = useCallback(
+    (nextPosition: number) => {
+      setSelectedPage(nextPosition)
+      parentOnPageSelected?.(nextPosition)
+    },
+    [setSelectedPage, parentOnPageSelected],
+  )
 
-    const onTabBarSelect = React.useCallback(
-      (index: number) => {
-        pagerView.current?.setPage(index)
-      },
-      [pagerView],
-    )
+  const onTabBarSelect = useCallback(
+    (index: number) => {
+      parentOnTabPressed?.(index)
+      pagerView.current?.setPage(index)
+    },
+    [pagerView, parentOnTabPressed],
+  )
 
-    const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
-    const dragProgress = useSharedValue(selectedPage)
-    const didInit = useSharedValue(false)
-    const handlePageScroll = usePagerHandlers(
-      {
-        onPageScroll(e: PagerViewOnPageScrollEventData) {
-          'worklet'
-          if (didInit.get() === false) {
-            // On iOS, there's a spurious scroll event with 0 position
-            // even if a different page was supplied as the initial page.
-            // Ignore it and wait for the first confirmed selection instead.
-            return
-          }
-          dragProgress.set(e.offset + e.position)
-        },
-        onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
-          'worklet'
-          runOnJS(setIsIdle)(e.pageScrollState === 'idle')
-          if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
-            // This is a programmatic scroll on Android.
-            // Stay "idle" to match iOS and avoid confusing downstream code.
-            return
-          }
-          dragState.set(e.pageScrollState)
-          parentOnPageScrollStateChanged?.(e.pageScrollState)
-        },
-        onPageSelected(e: PagerViewOnPageSelectedEventData) {
-          'worklet'
-          didInit.set(true)
-          runOnJS(onPageSelectedJSThread)(e.position)
-        },
+  const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
+  const dragProgress = useSharedValue(selectedPage)
+  const didInit = useSharedValue(false)
+  const handlePageScroll = usePagerHandlers(
+    {
+      onPageScroll(e: PagerViewOnPageScrollEventData) {
+        'worklet'
+        if (didInit.get() === false) {
+          // On iOS, there's a spurious scroll event with 0 position
+          // even if a different page was supplied as the initial page.
+          // Ignore it and wait for the first confirmed selection instead.
+          return
+        }
+        dragProgress.set(e.offset + e.position)
+      },
+      onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
+        'worklet'
+        runOnJS(setIsIdle)(e.pageScrollState === 'idle')
+        if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
+          // This is a programmatic scroll on Android.
+          // Stay "idle" to match iOS and avoid confusing downstream code.
+          return
+        }
+        dragState.set(e.pageScrollState)
+        parentOnPageScrollStateChanged?.(e.pageScrollState)
+      },
+      onPageSelected(e: PagerViewOnPageSelectedEventData) {
+        'worklet'
+        didInit.set(true)
+        runOnJS(onPageSelectedJSThread)(e.position)
       },
-      [parentOnPageScrollStateChanged],
-    )
+    },
+    [parentOnPageScrollStateChanged],
+  )
 
-    const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
-    const nativeGesture =
-      Gesture.Native().requireExternalGestureToFail(drawerGesture)
+  const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
+  const nativeGesture =
+    Gesture.Native().requireExternalGestureToFail(drawerGesture)
 
-    return (
-      <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
-        {renderTabBar({
-          selectedPage,
-          onSelect: onTabBarSelect,
-          dragProgress,
-          dragState,
-        })}
-        <GestureDetector gesture={nativeGesture}>
-          <AnimatedPagerView
-            ref={pagerView}
-            style={[a.flex_1]}
-            initialPage={initialPage}
-            onPageScroll={handlePageScroll}>
-            {children}
-          </AnimatedPagerView>
-        </GestureDetector>
-      </View>
-    )
-  },
-)
+  return (
+    <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
+      {renderTabBar({
+        selectedPage,
+        onSelect: onTabBarSelect,
+        dragProgress,
+        dragState,
+      })}
+      <GestureDetector gesture={nativeGesture}>
+        <AnimatedPagerView
+          ref={pagerView}
+          style={[a.flex_1]}
+          initialPage={initialPage}
+          onPageScroll={handlePageScroll}>
+          {children}
+        </AnimatedPagerView>
+      </GestureDetector>
+    </View>
+  )
+}
 
 function usePagerHandlers(
   handlers: {
diff --git a/src/view/com/pager/Pager.web.tsx b/src/view/com/pager/Pager.web.tsx
index c620e73e3..06aac169c 100644
--- a/src/view/com/pager/Pager.web.tsx
+++ b/src/view/com/pager/Pager.web.tsx
@@ -1,8 +1,19 @@
-import React from 'react'
+import {
+  Children,
+  useCallback,
+  useImperativeHandle,
+  useRef,
+  useState,
+} from 'react'
 import {View} from 'react-native'
 import {flushSync} from 'react-dom'
 
 import {s} from '#/lib/styles'
+import {atoms as a} from '#/alf'
+
+export interface PagerRef {
+  setPage: (index: number) => void
+}
 
 export interface RenderTabBarFnProps {
   selectedPage: number
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
 export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
 
 interface Props {
+  ref?: React.Ref<PagerRef>
   initialPage?: number
   renderTabBar: RenderTabBarFn
   onPageSelected?: (index: number) => void
 }
-export const Pager = React.forwardRef(function PagerImpl(
-  {
-    children,
-    initialPage = 0,
-    renderTabBar,
-    onPageSelected,
-  }: React.PropsWithChildren<Props>,
+
+export function Pager({
   ref,
-) {
-  const [selectedPage, setSelectedPage] = React.useState(initialPage)
-  const scrollYs = React.useRef<Array<number | null>>([])
-  const anchorRef = React.useRef(null)
+  children,
+  initialPage = 0,
+  renderTabBar,
+  onPageSelected,
+}: React.PropsWithChildren<Props>) {
+  const [selectedPage, setSelectedPage] = useState(initialPage)
+  const scrollYs = useRef<Array<number | null>>([])
+  const anchorRef = useRef(null)
 
-  React.useImperativeHandle(ref, () => ({
+  useImperativeHandle(ref, () => ({
     setPage: (index: number) => {
       onTabBarSelect(index)
     },
   }))
 
-  const onTabBarSelect = React.useCallback(
+  const onTabBarSelect = useCallback(
     (index: number) => {
       const scrollY = window.scrollY
       // We want to determine if the tabbar is already "sticking" at the top (in which
@@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl(
         tabBarAnchor: <View ref={anchorRef} />,
         onSelect: e => onTabBarSelect(e),
       })}
-      {React.Children.map(children, (child, i) => (
-        <View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
+      {Children.map(children, (child, i) => (
+        <View
+          style={selectedPage === i ? a.flex_1 : a.hidden}
+          key={`page-${i}`}>
           {child}
         </View>
       ))}
     </View>
   )
-})
+}
diff --git a/src/view/com/pager/PagerWithHeader.tsx b/src/view/com/pager/PagerWithHeader.tsx
index 1746d2ca1..57aaac074 100644
--- a/src/view/com/pager/PagerWithHeader.tsx
+++ b/src/view/com/pager/PagerWithHeader.tsx
@@ -1,17 +1,16 @@
-import * as React from 'react'
+import {memo, useCallback, useEffect, useRef, useState} from 'react'
 import {
-  LayoutChangeEvent,
-  NativeScrollEvent,
-  ScrollView,
+  type LayoutChangeEvent,
+  type NativeScrollEvent,
+  type ScrollView,
   StyleSheet,
   View,
 } from 'react-native'
 import Animated, {
-  AnimatedRef,
-  runOnJS,
+  type AnimatedRef,
   runOnUI,
   scrollTo,
-  SharedValue,
+  type SharedValue,
   useAnimatedRef,
   useAnimatedStyle,
   useSharedValue,
@@ -20,9 +19,13 @@ import Animated, {
 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
 import {ScrollProvider} from '#/lib/ScrollContext'
 import {isIOS} from '#/platform/detection'
-import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
+import {
+  Pager,
+  type PagerRef,
+  type RenderTabBarFnProps,
+} from '#/view/com/pager/Pager'
 import {useTheme} from '#/alf'
-import {ListMethods} from '../util/List'
+import {type ListMethods} from '../util/List'
 import {PagerHeaderProvider} from './PagerHeaderContext'
 import {TabBar} from './TabBar'
 
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
 }
 
 export interface PagerWithHeaderProps {
+  ref?: React.Ref<PagerRef>
   testID?: string
   children:
     | (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
@@ -49,97 +53,94 @@ export interface PagerWithHeaderProps {
   onCurrentPageSelected?: (index: number) => void
   allowHeaderOverScroll?: boolean
 }
-export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
-  function PageWithHeaderImpl(
-    {
-      children,
-      testID,
+export function PagerWithHeader({
+  ref,
+  children,
+  testID,
+  items,
+  isHeaderReady,
+  renderHeader,
+  initialPage,
+  onPageSelected,
+  onCurrentPageSelected,
+  allowHeaderOverScroll,
+}: PagerWithHeaderProps) {
+  const [currentPage, setCurrentPage] = useState(0)
+  const [tabBarHeight, setTabBarHeight] = useState(0)
+  const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
+  const scrollY = useSharedValue(0)
+  const headerHeight = headerOnlyHeight + tabBarHeight
+
+  // capture the header bar sizing
+  const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
+    const height = evt.nativeEvent.layout.height
+    if (height > 0) {
+      // The rounding is necessary to prevent jumps on iOS
+      setTabBarHeight(Math.round(height * 2) / 2)
+    }
+  })
+  const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
+    if (height > 0) {
+      // The rounding is necessary to prevent jumps on iOS
+      setHeaderOnlyHeight(Math.round(height * 2) / 2)
+    }
+  })
+
+  const renderTabBar = useCallback(
+    (props: RenderTabBarFnProps) => {
+      return (
+        <PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
+          <PagerTabBar
+            headerOnlyHeight={headerOnlyHeight}
+            items={items}
+            isHeaderReady={isHeaderReady}
+            renderHeader={renderHeader}
+            currentPage={currentPage}
+            onCurrentPageSelected={onCurrentPageSelected}
+            onTabBarLayout={onTabBarLayout}
+            onHeaderOnlyLayout={onHeaderOnlyLayout}
+            onSelect={props.onSelect}
+            scrollY={scrollY}
+            testID={testID}
+            allowHeaderOverScroll={allowHeaderOverScroll}
+            dragProgress={props.dragProgress}
+            dragState={props.dragState}
+          />
+        </PagerHeaderProvider>
+      )
+    },
+    [
+      headerOnlyHeight,
       items,
       isHeaderReady,
       renderHeader,
-      initialPage,
-      onPageSelected,
+      currentPage,
       onCurrentPageSelected,
+      onTabBarLayout,
+      onHeaderOnlyLayout,
+      scrollY,
+      testID,
       allowHeaderOverScroll,
-    }: PagerWithHeaderProps,
-    ref,
-  ) {
-    const [currentPage, setCurrentPage] = React.useState(0)
-    const [tabBarHeight, setTabBarHeight] = React.useState(0)
-    const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
-    const scrollY = useSharedValue(0)
-    const headerHeight = headerOnlyHeight + tabBarHeight
-
-    // capture the header bar sizing
-    const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
-      const height = evt.nativeEvent.layout.height
-      if (height > 0) {
-        // The rounding is necessary to prevent jumps on iOS
-        setTabBarHeight(Math.round(height * 2) / 2)
-      }
-    })
-    const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
-      if (height > 0) {
-        // The rounding is necessary to prevent jumps on iOS
-        setHeaderOnlyHeight(Math.round(height * 2) / 2)
-      }
-    })
-
-    const renderTabBar = React.useCallback(
-      (props: RenderTabBarFnProps) => {
-        return (
-          <PagerHeaderProvider
-            scrollY={scrollY}
-            headerHeight={headerOnlyHeight}>
-            <PagerTabBar
-              headerOnlyHeight={headerOnlyHeight}
-              items={items}
-              isHeaderReady={isHeaderReady}
-              renderHeader={renderHeader}
-              currentPage={currentPage}
-              onCurrentPageSelected={onCurrentPageSelected}
-              onTabBarLayout={onTabBarLayout}
-              onHeaderOnlyLayout={onHeaderOnlyLayout}
-              onSelect={props.onSelect}
-              scrollY={scrollY}
-              testID={testID}
-              allowHeaderOverScroll={allowHeaderOverScroll}
-              dragProgress={props.dragProgress}
-              dragState={props.dragState}
-            />
-          </PagerHeaderProvider>
-        )
-      },
-      [
-        headerOnlyHeight,
-        items,
-        isHeaderReady,
-        renderHeader,
-        currentPage,
-        onCurrentPageSelected,
-        onTabBarLayout,
-        onHeaderOnlyLayout,
-        scrollY,
-        testID,
-        allowHeaderOverScroll,
-      ],
-    )
+    ],
+  )
 
-    const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
-    const registerRef = React.useCallback(
-      (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
-        scrollRefs.modify(refs => {
-          'worklet'
-          refs[atIndex] = scrollRef
-          return refs
-        })
-      },
-      [scrollRefs],
-    )
+  const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
+  const registerRef = useCallback(
+    (scrollRef: AnimatedRef<any> | null, atIndex: number) => {
+      scrollRefs.modify(refs => {
+        'worklet'
+        refs[atIndex] = scrollRef
+        return refs
+      })
+    },
+    [scrollRefs],
+  )
 
-    const lastForcedScrollY = useSharedValue(0)
-    const adjustScrollForOtherPages = () => {
+  const lastForcedScrollY = useSharedValue(0)
+  const adjustScrollForOtherPages = useCallback(
+    (scrollState: 'idle' | 'dragging' | 'settling') => {
       'worklet'
+      if (scrollState !== 'dragging') return
       const currentScrollY = scrollY.get()
       const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
       if (lastForcedScrollY.get() !== forcedScrollY) {
@@ -152,75 +153,69 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
           }
         }
       }
-    }
+    },
+    [currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY],
+  )
 
-    const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
-      null,
-    )
-    const queueThrottledOnScroll = useNonReactiveCallback(() => {
-      if (!throttleTimeout.current) {
-        throttleTimeout.current = setTimeout(() => {
-          throttleTimeout.current = null
-          runOnUI(adjustScrollForOtherPages)()
-        }, 80 /* Sync often enough you're unlikely to catch it unsynced */)
+  const onScrollWorklet = useCallback(
+    (e: NativeScrollEvent) => {
+      'worklet'
+      const nextScrollY = e.contentOffset.y
+      // HACK: onScroll is reporting some strange values on load (negative header height).
+      // Highly improbable that you'd be overscrolled by over 400px -
+      // in fact, I actually can't do it, so let's just ignore those. -sfn
+      const isPossiblyInvalid =
+        headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
+      if (!isPossiblyInvalid) {
+        scrollY.set(nextScrollY)
       }
-    })
+    },
+    [scrollY, headerHeight],
+  )
 
-    const onScrollWorklet = React.useCallback(
-      (e: NativeScrollEvent) => {
-        'worklet'
-        const nextScrollY = e.contentOffset.y
-        // HACK: onScroll is reporting some strange values on load (negative header height).
-        // Highly improbable that you'd be overscrolled by over 400px -
-        // in fact, I actually can't do it, so let's just ignore those. -sfn
-        const isPossiblyInvalid =
-          headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
-        if (!isPossiblyInvalid) {
-          scrollY.set(nextScrollY)
-          runOnJS(queueThrottledOnScroll)()
-        }
-      },
-      [scrollY, queueThrottledOnScroll, headerHeight],
-    )
+  const onPageSelectedInner = useCallback(
+    (index: number) => {
+      setCurrentPage(index)
+      onPageSelected?.(index)
+    },
+    [onPageSelected, setCurrentPage],
+  )
 
-    const onPageSelectedInner = React.useCallback(
-      (index: number) => {
-        setCurrentPage(index)
-        onPageSelected?.(index)
-      },
-      [onPageSelected, setCurrentPage],
-    )
+  const onTabPressed = useCallback(() => {
+    runOnUI(adjustScrollForOtherPages)('dragging')
+  }, [adjustScrollForOtherPages])
 
-    return (
-      <Pager
-        ref={ref}
-        testID={testID}
-        initialPage={initialPage}
-        onPageSelected={onPageSelectedInner}
-        renderTabBar={renderTabBar}>
-        {toArray(children)
-          .filter(Boolean)
-          .map((child, i) => {
-            const isReady =
-              isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
-            return (
-              <View key={i} collapsable={false}>
-                <PagerItem
-                  headerHeight={headerHeight}
-                  index={i}
-                  isReady={isReady}
-                  isFocused={i === currentPage}
-                  onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
-                  registerRef={registerRef}
-                  renderTab={child}
-                />
-              </View>
-            )
-          })}
-      </Pager>
-    )
-  },
-)
+  return (
+    <Pager
+      ref={ref}
+      testID={testID}
+      initialPage={initialPage}
+      onTabPressed={onTabPressed}
+      onPageSelected={onPageSelectedInner}
+      renderTabBar={renderTabBar}
+      onPageScrollStateChanged={adjustScrollForOtherPages}>
+      {toArray(children)
+        .filter(Boolean)
+        .map((child, i) => {
+          const isReady =
+            isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
+          return (
+            <View key={i} collapsable={false}>
+              <PagerItem
+                headerHeight={headerHeight}
+                index={i}
+                isReady={isReady}
+                isFocused={i === currentPage}
+                onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
+                registerRef={registerRef}
+                renderTab={child}
+              />
+            </View>
+          )
+        })}
+    </Pager>
+  )
+}
 
 let PagerTabBar = ({
   currentPage,
@@ -258,7 +253,7 @@ let PagerTabBar = ({
   dragState: SharedValue<'idle' | 'dragging' | 'settling'>
 }): React.ReactNode => {
   const t = useTheme()
-  const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
+  const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
   const headerTransform = useAnimatedStyle(() => {
     const translateY =
       Math.min(
@@ -275,7 +270,7 @@ let PagerTabBar = ({
       ],
     }
   })
-  const headerRef = React.useRef(null)
+  const headerRef = useRef(null)
   return (
     <Animated.View
       pointerEvents={isIOS ? 'auto' : 'box-none'}
@@ -327,7 +322,7 @@ let PagerTabBar = ({
     </Animated.View>
   )
 }
-PagerTabBar = React.memo(PagerTabBar)
+PagerTabBar = memo(PagerTabBar)
 
 function PagerItem({
   headerHeight,
@@ -348,7 +343,7 @@ function PagerItem({
 }) {
   const scrollElRef = useAnimatedRef()
 
-  React.useEffect(() => {
+  useEffect(() => {
     registerRef(scrollElRef, index)
     return () => {
       registerRef(null, index)
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index b0cf4d10e..02a6704bf 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -1,23 +1,28 @@
 import React from 'react'
 import {Pressable, View} from 'react-native'
-import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
-import {AppBskyGraphDefs} from '@atproto/api'
+import Animated, {
+  measure,
+  type MeasuredDimensions,
+  runOnJS,
+  runOnUI,
+  useAnimatedRef,
+} from 'react-native-reanimated'
+import {type AppBskyGraphDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
-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'
-import {NavigationProp} from '#/lib/routes/types'
+import {type NavigationProp} from '#/lib/routes/types'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {emitSoftReset} from '#/state/events'
 import {useLightboxControls} from '#/state/lightbox'
 import {TextLink} from '#/view/com/util/Link'
 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
 import {Text} from '#/view/com/util/text/Text'
-import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
+import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
 import {StarterPack} from '#/components/icons/StarterPack'
 import * as Layout from '#/components/Layout'
 
@@ -52,7 +57,7 @@ export function ProfileSubpageHeader({
   const {openLightbox} = useLightboxControls()
   const pal = usePalette('default')
   const canGoBack = navigation.canGoBack()
-  const aviRef = useHandleRef()
+  const aviRef = useAnimatedRef()
 
   const _openLightbox = React.useCallback(
     (uri: string, thumbRect: MeasuredDimensions | null) => {
@@ -81,10 +86,9 @@ export function ProfileSubpageHeader({
     if (
       avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
     ) {
-      const aviHandle = aviRef.current
       runOnUI(() => {
         'worklet'
-        const rect = measureHandle(aviHandle)
+        const rect = measure(aviRef)
         runOnJS(_openLightbox)(avatar, rect)
       })()
     }
@@ -111,7 +115,7 @@ export function ProfileSubpageHeader({
           paddingBottom: 14,
           paddingHorizontal: isMobile ? 12 : 14,
         }}>
-        <View ref={aviRef} collapsable={false}>
+        <Animated.View ref={aviRef} collapsable={false}>
           <Pressable
             testID="headerAviButton"
             onPress={onPressAvi}
@@ -125,7 +129,7 @@ export function ProfileSubpageHeader({
               <UserAvatar type={avatarType} size={58} avatar={avatar} />
             )}
           </Pressable>
-        </View>
+        </Animated.View>
         <View style={{flex: 1, gap: 4}}>
           {isLoading ? (
             <LoadingPlaceholder
diff --git a/src/view/com/util/Toast.web.tsx b/src/view/com/util/Toast.web.tsx
index 7e22fcefc..d3b7bda33 100644
--- a/src/view/com/util/Toast.web.tsx
+++ b/src/view/com/util/Toast.web.tsx
@@ -9,7 +9,6 @@ import {
   type FontAwesomeIconStyle,
   type Props as FontAwesomeProps,
 } from '@fortawesome/react-native-fontawesome'
-import type React from 'react'
 
 const DURATION = 3500
 
diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/view/com/util/images/AutoSizedImage.tsx
index 883d3814f..4e9c61d47 100644
--- a/src/view/com/util/images/AutoSizedImage.tsx
+++ b/src/view/com/util/images/AutoSizedImage.tsx
@@ -1,12 +1,15 @@
 import React, {useRef} from 'react'
-import {DimensionValue, Pressable, View} from 'react-native'
+import {type DimensionValue, Pressable, View} from 'react-native'
+import Animated, {
+  type AnimatedRef,
+  useAnimatedRef,
+} from 'react-native-reanimated'
 import {Image} from 'expo-image'
-import {AppBskyEmbedImages} from '@atproto/api'
+import {type AppBskyEmbedImages} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
-import type {Dimensions} from '#/lib/media/types'
+import {type Dimensions} from '#/lib/media/types'
 import {isNative} from '#/platform/detection'
 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
@@ -68,14 +71,17 @@ export function AutoSizedImage({
   image: AppBskyEmbedImages.ViewImage
   crop?: 'none' | 'square' | 'constrained'
   hideBadge?: boolean
-  onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void
+  onPress?: (
+    containerRef: AnimatedRef<any>,
+    fetchedDims: Dimensions | null,
+  ) => void
   onLongPress?: () => void
   onPressIn?: () => void
 }) {
   const t = useTheme()
   const {_} = useLingui()
   const largeAlt = useLargeAltBadgeEnabled()
-  const containerRef = useHandleRef()
+  const containerRef = useAnimatedRef()
   const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
 
   let aspectRatio: number | undefined
@@ -103,7 +109,7 @@ export function AutoSizedImage({
   const hasAlt = !!image.alt
 
   const contents = (
-    <View ref={containerRef} collapsable={false} style={{flex: 1}}>
+    <Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}>
       <Image
         contentFit={isContain ? 'contain' : 'cover'}
         style={[a.w_full, a.h_full]}
@@ -185,7 +191,7 @@ export function AutoSizedImage({
           )}
         </View>
       ) : null}
-    </View>
+    </Animated.View>
   )
 
   if (cropDisabled) {
diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx
index cc3eda68d..1d35c88c5 100644
--- a/src/view/com/util/images/Gallery.tsx
+++ b/src/view/com/util/images/Gallery.tsx
@@ -1,12 +1,12 @@
-import React from 'react'
-import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
-import {Image, ImageStyle} from 'expo-image'
-import {AppBskyEmbedImages} from '@atproto/api'
+import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
+import {type AnimatedRef} from 'react-native-reanimated'
+import {Image, type ImageStyle} from 'expo-image'
+import {type AppBskyEmbedImages} from '@atproto/api'
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import type React from 'react'
 
-import {HandleRef} from '#/lib/hooks/useHandleRef'
-import {Dimensions} from '#/lib/media/types'
+import {type Dimensions} from '#/lib/media/types'
 import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
 import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types'
 import {atoms as a, useTheme} from '#/alf'
@@ -20,7 +20,7 @@ interface Props {
   index: number
   onPress?: (
     index: number,
-    containerRefs: HandleRef[],
+    containerRefs: AnimatedRef<any>[],
     fetchedDims: (Dimensions | null)[],
   ) => void
   onLongPress?: EventFunction
@@ -28,7 +28,7 @@ interface Props {
   imageStyle?: StyleProp<ImageStyle>
   viewContext?: PostEmbedViewContext
   insetBorderStyle?: StyleProp<ViewStyle>
-  containerRefs: HandleRef[]
+  containerRefs: AnimatedRef<any>[]
   thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
 }
 
diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx
index 16ea9d453..b91d7a7ad 100644
--- a/src/view/com/util/images/ImageLayoutGrid.tsx
+++ b/src/view/com/util/images/ImageLayoutGrid.tsx
@@ -1,18 +1,18 @@
 import React from 'react'
-import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
-import {AppBskyEmbedImages} from '@atproto/api'
+import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
+import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
+import {type 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'
+import {type Dimensions} from '../../lightbox/ImageViewing/@types'
 import {GalleryItem} from './Gallery'
 
 interface ImageLayoutGridProps {
   images: AppBskyEmbedImages.ViewImage[]
   onPress?: (
     index: number,
-    containerRefs: HandleRef[],
+    containerRefs: AnimatedRef<any>[],
     fetchedDims: (Dimensions | null)[],
   ) => void
   onLongPress?: (index: number) => void
@@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps {
   images: AppBskyEmbedImages.ViewImage[]
   onPress?: (
     index: number,
-    containerRefs: HandleRef[],
+    containerRefs: AnimatedRef<any>[],
     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 = useHandleRef()
-  const containerRef2 = useHandleRef()
-  const containerRef3 = useHandleRef()
-  const containerRef4 = useHandleRef()
+  const containerRef1 = useAnimatedRef()
+  const containerRef2 = useAnimatedRef()
+  const containerRef3 = useAnimatedRef()
+  const containerRef4 = useAnimatedRef()
   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 431baa2b2..4cf71f948 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -7,6 +7,8 @@ import {
   type ViewStyle,
 } from 'react-native'
 import {
+  type AnimatedRef,
+  measure,
   type MeasuredDimensions,
   runOnJS,
   runOnUI,
@@ -25,7 +27,6 @@ import {
   type ModerationDecision,
 } from '@atproto/api'
 
-import {type HandleRef, measureHandle} from '#/lib/hooks/useHandleRef'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {useLightboxControls} from '#/state/lightbox'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -162,13 +163,15 @@ export function PostEmbeds({
       }
       const onPress = (
         index: number,
-        refs: HandleRef[],
+        refs: AnimatedRef<any>[],
         fetchedDims: (Dimensions | null)[],
       ) => {
-        const handles = refs.map(r => r.current)
         runOnUI(() => {
           'worklet'
-          const rects = handles.map(measureHandle)
+          const rects: (MeasuredDimensions | null)[] = []
+          for (const r of refs) {
+            rects.push(measure(r))
+          }
           runOnJS(_openLightbox)(index, rects, fetchedDims)
         })()
       }