diff --git a/src/view/com/pager/PagerWithHeader.web.tsx b/src/view/com/pager/PagerWithHeader.web.tsx index bc276722fb..4b2aa32d45 100644 --- a/src/view/com/pager/PagerWithHeader.web.tsx +++ b/src/view/com/pager/PagerWithHeader.web.tsx @@ -6,7 +6,6 @@ import {TabBar} from './TabBar' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {ListMethods} from '../util/List' -import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' export interface PagerWithHeaderChildParams { headerHeight: number @@ -108,6 +107,7 @@ let PagerTabBar = ({ renderHeader?: () => JSX.Element onCurrentPageSelected?: (index: number) => void onSelect?: (index: number) => void + tabBarAnchor?: JSX.Element }): React.ReactNode => { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 3951dffbb4..ce62c855f2 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -197,7 +197,7 @@ function PostThreadLoaded({ // wait for loading to finish if (thread.type === 'post' && !!thread.parent) { - function onMeasure(pageY) { + function onMeasure(pageY: number) { let spinnerHeight = 0 if (isDesktop) { spinnerHeight = 40 @@ -216,8 +216,12 @@ function PostThreadLoaded({ }, ) } else { - const pageY = highlightedPostRef.current.getBoundingClientRect().top - onMeasure(pageY) + // Measure synchronously to avoid a layout jump. + const domNode = highlightedPostRef.current + if (domNode) { + const pageY = (domNode as any as Element).getBoundingClientRect().top + onMeasure(pageY) + } } needsScrollAdjustment.current = false } diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 1d16df3945..d30a9d805b 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -86,9 +86,9 @@ function ListImpl( return ( = Omit< FlatListProps, | 'onScroll' // Use ScrollContext instead. @@ -18,8 +18,9 @@ export type ListProps = Omit< headerOffset?: number refreshing?: boolean onRefresh?: () => void + desktopFixedHeight: any // TODO: Better types. } -export type ListRef = React.MutableRefObject +export type ListRef = React.MutableRefObject // TODO: Better types. function ListImpl( { @@ -27,7 +28,7 @@ function ListImpl( ListFooterComponent, contentContainerStyle, data, - desktopFixedHeight, // TODO + desktopFixedHeight, headerOffset, keyExtractor, refreshing: _unsupportedRefreshing, @@ -53,7 +54,7 @@ function ListImpl( ) } - let header: ListProps['ListHeaderComponent'] = null + let header: JSX.Element | null = null if (ListHeaderComponent != null) { if (typeof ListHeaderComponent === 'object') { header = ListHeaderComponent @@ -63,7 +64,7 @@ function ListImpl( } } - let footer: ListProps['ListHeaderComponent'] = null + let footer: JSX.Element | null = null if (ListFooterComponent != null) { if (typeof ListFooterComponent === 'object') { footer = ListFooterComponent @@ -100,11 +101,15 @@ function ListImpl( behavior: animated ? 'smooth' : 'instant', }) }, - } as any), // TODO: Types. + } as any), // TODO: Better types. [], ) - const [isVisible, setIsVisible] = React.useState(false) + // --- onContentSizeChange --- + const containerRef = useRef(null) + useResizeObserver(containerRef, onContentSizeChange) + + // --- onScroll --- const handleScroll = useNonReactiveCallback(() => { contextScrollHandlers.onScroll?.( { @@ -112,21 +117,22 @@ function ListImpl( x: window.scrollX, y: window.scrollY, }, - // TODO - }, - { - /* TODO */ - }, + } as any, // TODO: Better types. + null as any, ) }) + const [isParentTreeVisible, setIsParentTreeVisible] = React.useState(false) React.useEffect(() => { - if (!isVisible) { + if (!isParentTreeVisible) { + // Prevents hidden tabs from firing scroll events. + // Only one list is expected to be firing these at a time. return } window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) - }, [isVisible, handleScroll]) + }, [isParentTreeVisible, handleScroll]) + // --- onScrolledDownChange --- const isScrolledDown = useRef(false) function handleAboveTheFoldVisibleChange(isAboveTheFold: boolean) { const didScrollDown = !isAboveTheFold @@ -138,13 +144,21 @@ function ListImpl( } } - const containerRef = useRef(null) - useResizeObserver(containerRef, onContentSizeChange) + // --- onEndReached --- + const onTailVisibilityChange = useNonReactiveCallback( + (isTailVisible: boolean) => { + if (isTailVisible) { + onEndReached?.({ + distanceFromEnd: onEndReachedThreshold || 0, + }) + } + }, + ) return ( ( {onEndReached && ( { - if (isVisible) { - onEndReached?.({ - distanceFromEnd: onEndReachedThreshold || 0, - }) - } - }} + onVisibleChange={onTailVisibilityChange} /> )} {footer} @@ -187,17 +195,25 @@ function ListImpl( ) } -function useResizeObserver(ref, onResize) { +function useResizeObserver( + ref: React.RefObject, + onResize: undefined | ((w: number, h: number) => void), +) { const handleResize = useNonReactiveCallback(onResize ?? (() => {})) const isActive = !!onResize React.useEffect(() => { if (!isActive) { return } - const resizeObserver = new ResizeObserver(() => { - handleResize() + const resizeObserver = new ResizeObserver(entries => { + batchedUpdates(() => { + for (let entry of entries) { + const rect = entry.contentRect + handleResize(rect.width, rect.height) + } + }) }) - const node = ref.current + const node = ref.current! resizeObserver.observe(node) return () => { resizeObserver.unobserve(node)