diff options
author | hailey <me@haileyok.com> | 2025-06-12 10:46:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-06-12 10:46:22 -0700 |
commit | 477e5f4ecfaa0007aeed90b51274c78a730c1a9e (patch) | |
tree | 45cd5cfff9eab1bd52b5ade6c60efebe3cc5e6b6 /src/view/com | |
parent | a26b20b56cd0ac80f625a5eb5136b805b9341e8d (diff) | |
download | voidsky-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/com')
-rw-r--r-- | src/view/com/composer/text-input/TextInput.tsx | 14 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 220 | ||||
-rw-r--r-- | src/view/com/pager/Pager.web.tsx | 47 | ||||
-rw-r--r-- | src/view/com/pager/PagerWithHeader.tsx | 315 | ||||
-rw-r--r-- | src/view/com/profile/ProfileSubpageHeader.tsx | 24 | ||||
-rw-r--r-- | src/view/com/util/Toast.web.tsx | 1 | ||||
-rw-r--r-- | src/view/com/util/images/AutoSizedImage.tsx | 22 | ||||
-rw-r--r-- | src/view/com/util/images/Gallery.tsx | 16 | ||||
-rw-r--r-- | src/view/com/util/images/ImageLayoutGrid.tsx | 20 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 11 |
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) })() } |