diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 2dd24cbe10..b98e1a23c3 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -76,6 +76,7 @@ "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.10", "@tanstack/react-query": "^5.0.0", + "@tanstack/react-virtual": "^3.10.8", "@types/escape-html": "^1.0.1", "@types/http-link-header": "^1.0.3", "@types/leaflet": "^1.8.0", diff --git a/packages/pl-fe/src/components/scrollable-list.tsx b/packages/pl-fe/src/components/scrollable-list.tsx index aa821c12c7..7970c91c57 100644 --- a/packages/pl-fe/src/components/scrollable-list.tsx +++ b/packages/pl-fe/src/components/scrollable-list.tsx @@ -1,43 +1,14 @@ -import debounce from 'lodash/debounce'; -import React, { useEffect, useRef, useMemo, useCallback } from 'react'; -import { useHistory } from 'react-router-dom'; -import { Virtuoso, Components, VirtuosoProps, VirtuosoHandle, ListRange, IndexLocationWithAlign } from 'react-virtuoso'; +/* eslint-disable react-hooks/rules-of-hooks */ +import { useVirtualizer, useWindowVirtualizer, type Virtualizer } from '@tanstack/react-virtual'; +import clsx from 'clsx'; +import React, { useEffect, useMemo } from 'react'; import { useSettings } from 'pl-fe/hooks'; import LoadMore from './load-more'; import { Card, Spinner } from './ui'; -/** Custom Viruoso component context. */ -type Context = { - itemClassName?: string; - listClassName?: string; -} - -/** Scroll position saved in sessionStorage. */ -type SavedScrollPosition = { - index: number; - offset: number; -} - -/** Custom Virtuoso Item component representing a single scrollable item. */ -// NOTE: It's crucial to space lists with **padding** instead of margin! -// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className -// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around -const Item: Components['Item'] = ({ context, ...rest }) => ( -
-); - -/** Custom Virtuoso List component for the outer container. */ -// Ensure the className winds up here -const List: Components['List'] = React.forwardRef((props, ref) => { - const { context, ...rest } = props; - return
; -}); - -interface IScrollableList extends VirtuosoProps { - /** Unique key to preserve the scroll position when navigating back. */ - scrollKey?: string; +interface IScrollableList { /** Pagination callback when the end of the list is reached. */ onLoadMore?: () => void; /** Whether the data is currently being fetched. */ @@ -64,12 +35,7 @@ interface IScrollableList extends VirtuosoProps { placeholderComponent?: React.ComponentType | React.NamedExoticComponent; /** Number of placeholders to render while loading. */ placeholderCount?: number; - /** - * Pull to refresh callback. - * @deprecated Put a PTR around the component instead. - */ - onRefresh?: () => Promise; - /** Extra class names on the Virtuoso element. */ + /** Extra class names on the parent element. */ className?: string; /** Extra class names on the list element. */ listClassName?: string; @@ -77,17 +43,19 @@ interface IScrollableList extends VirtuosoProps { itemClassName?: string; /** Extra class names on the LoadMore element */ loadMoreClassName?: string; - /** `id` attribute on the Virtuoso element. */ + /** `id` attribute on the parent element. */ id?: string; - /** CSS styles on the Virtuoso element. */ + /** CSS styles on the parent element. */ style?: React.CSSProperties; - /** Whether to use the window to scroll the content instead of Virtuoso's container. */ + /** Whether to use the window to scroll the content instead of the container. */ useWindowScroll?: boolean; + /** Initial item index to scroll to. */ + initialIndex?: number; + /** Estimated size for items */ + estimatedSize?: number; } -/** Legacy ScrollableList with Virtuoso for backwards-compatibility. */ -const ScrollableList = React.forwardRef(({ - scrollKey, +const ScrollableList = React.forwardRef, IScrollableList>(({ prepend = null, alwaysPrepend, children, @@ -95,7 +63,6 @@ const ScrollableList = React.forwardRef(({ emptyMessage, emptyMessageCard = true, showLoading, - onRefresh, onScroll, onScrollToTop, onLoadMore, @@ -107,58 +74,70 @@ const ScrollableList = React.forwardRef(({ hasMore, placeholderComponent: Placeholder, placeholderCount = 0, - initialTopMostItemIndex = 0, + initialIndex = 0, style = {}, useWindowScroll = true, + estimatedSize = 300, }, ref) => { - const history = useHistory(); const { autoloadMore } = useSettings(); - // Preserve scroll position - const scrollDataKey = `plfe:scrollData:${scrollKey}`; - const scrollData: SavedScrollPosition | null = useMemo(() => JSON.parse(sessionStorage.getItem(scrollDataKey)!), [scrollDataKey]); - const topIndex = useRef(scrollData ? scrollData.index : 0); - const topOffset = useRef(scrollData ? scrollData.offset : 0); + const parentRef = React.useRef(null); /** Normalized children. */ const elements = Array.from(children || []); const showPlaceholder = showLoading && Placeholder && placeholderCount > 0; - // NOTE: We are doing some trickery to load a feed of placeholders - // Virtuoso's `EmptyPlaceholder` unfortunately doesn't work for our use-case const data = showPlaceholder ? Array(placeholderCount).fill('') : elements; - // Add a placeholder at the bottom for loading - // (Don't use Virtuoso's `Footer` component because it doesn't preserve its height) - if (hasMore && (autoloadMore || isLoading) && Placeholder) { - data.push(); - } else if (hasMore && (autoloadMore || isLoading)) { - data.push(); - } - - const handleScroll = useCallback(debounce(() => { - // HACK: Virtuoso has no better way to get this... - const node = document.querySelector(`[data-virtuoso-scroller] [data-item-index="${topIndex.current}"]`); - if (node) { - topOffset.current = node.getBoundingClientRect().top * -1; - } else { - topOffset.current = 0; - } - }, 150, { trailing: true }), []); + const virtualizer = useWindowScroll ? useWindowVirtualizer({ + count: data.length + (hasMore ? 1 : 0), + overscan: 3, + // scrollMargin: parentRef.current?.offsetTop ?? 0, + estimateSize: () => estimatedSize, + }) : useVirtualizer({ + count: data.length + (hasMore ? 1 : 0), + overscan: 3, + // scrollMargin: parentRef.current?.offsetTop ?? 0, + estimateSize: () => estimatedSize, + getScrollElement: () => parentRef.current, + }); useEffect(() => { - document.addEventListener('scroll', handleScroll); - sessionStorage.removeItem(scrollDataKey); + if (typeof ref === 'function') ref(virtualizer); else if (ref !== null) ref.current = virtualizer; + }, [virtualizer]); - return () => { - if (scrollKey) { - const data: SavedScrollPosition = { index: topIndex.current, offset: topOffset.current }; - sessionStorage.setItem(scrollDataKey, JSON.stringify(data)); - } - document.removeEventListener('scroll', handleScroll); - }; - }, []); + const range = virtualizer.calculateRange(); + + useEffect(() => { + if (showLoading) return; + + if (typeof initialIndex === 'number') virtualizer.scrollToIndex(initialIndex); + }, [showLoading, initialIndex]); + + useEffect(() => { + if (range?.startIndex === 0) { + onScrollToTop?.(); + } else onScroll?.(); + }, [range?.startIndex === 0]); + + useEffect(() => { + if (onLoadMore && range?.endIndex === data.length && !showLoading && autoloadMore && hasMore) { + onLoadMore(); + } + }, [range?.endIndex]); + + const loadMore = useMemo(() => { + if (autoloadMore || !hasMore || !onLoadMore) { + return null; + } else { + const button = ; + + if (loadMoreClassName) return
{button}
; + + return button; + } + }, [autoloadMore, hasMore, isLoading]); /* Render an empty state instead of the scrollable list. */ const renderEmpty = (): JSX.Element => ( @@ -179,94 +158,52 @@ const ScrollableList = React.forwardRef(({
); - /** Render a single item. */ - const renderItem = (_i: number, element: JSX.Element): JSX.Element => { - if (showPlaceholder) { - return ; - } else { - return element; - } + const renderItem = (index: number): JSX.Element => { + const PlaceholderComponent = Placeholder || Spinner; + if (index === data.length) return (isLoading) ? : loadMore ||
; + if (showPlaceholder) return ; + return data[index]; }; - const handleEndReached = () => { - if (autoloadMore && hasMore && onLoadMore) { - onLoadMore(); - } - }; - - const loadMore = () => { - if (autoloadMore || !hasMore || !onLoadMore) { - return null; - } else { - const button = ; - - if (loadMoreClassName) return
{button}
; - - return button; - } - }; - - const handleRangeChange = (range: ListRange) => { - // HACK: using the first index can be buggy. - // Track the second item instead, unless the endIndex comes before it (eg one 1 item in view). - topIndex.current = Math.min(range.startIndex + 1, range.endIndex); - handleScroll(); - }; - - /** Figure out the initial index to scroll to. */ - const initialIndex = useMemo(() => { - if (showLoading) return 0; - - if (initialTopMostItemIndex) { - if (typeof initialTopMostItemIndex === 'number') { - return { - align: 'start', - index: initialTopMostItemIndex, - offset: 60, - }; - } - return initialTopMostItemIndex; - } - - if (scrollData && history.action === 'POP') { - return { - align: 'start', - index: scrollData.index, - offset: scrollData.offset, - }; - } - - return 0; - }, [showLoading, initialTopMostItemIndex]); + const virtualItems = virtualizer.getVirtualItems(); return ( - isScrolling && onScroll && onScroll()} - itemContent={renderItem} - initialTopMostItemIndex={initialIndex} - rangeChanged={handleRangeChange} - className={className} + className={clsx(className, 'w-full')} style={style} - context={{ - listClassName, - itemClassName, - }} - components={{ - Header: () => <>{prepend}, - ScrollSeekPlaceholder: Placeholder as any, - EmptyPlaceholder: () => renderEmpty(), - List, - Item, - Footer: loadMore, - }} - /> + > +
+ {!showLoading && data.length ? ( + <> + {prepend} + {virtualItems.map((item) => ( +
+ {renderItem(item.index)} +
+ ))} + + ) : renderEmpty()} +
+
); }); diff --git a/packages/pl-fe/src/components/status-list.tsx b/packages/pl-fe/src/components/status-list.tsx index d21374fabb..83f52ddfdb 100644 --- a/packages/pl-fe/src/components/status-list.tsx +++ b/packages/pl-fe/src/components/status-list.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import debounce from 'lodash/debounce'; -import React, { useRef, useCallback } from 'react'; +import React, { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; import LoadGap from 'pl-fe/components/load-gap'; @@ -15,7 +15,6 @@ import { Stack, Text } from './ui'; import type { OrderedSet as ImmutableOrderedSet } from 'immutable'; import type { IScrollableList } from 'pl-fe/components/scrollable-list'; -import type { VirtuosoHandle } from 'react-virtuoso'; interface IStatusList extends Omit { /** Unique key to preserve the scroll position when navigating back. */ @@ -62,7 +61,6 @@ const StatusList: React.FC = ({ ...other }) => { const plFeConfig = usePlFeConfig(); - const node = useRef(null); const getFeaturedStatusCount = () => featuredStatusIds?.size || 0; @@ -96,14 +94,6 @@ const StatusList: React.FC = ({ const element = document.querySelector(selector); if (element) element.focus(); - - node.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - if (!element) document.querySelector(selector)?.focus(); - }, - }); }; const renderLoadGap = (index: number) => { @@ -179,7 +169,6 @@ const StatusList: React.FC = ({ return statusIds.toList().reduce((acc, statusId, index) => { if (statusId === null) { const gap = renderLoadGap(index); - // one does not simply push a null item to Virtuoso: https://github.com/petyosi/react-virtuoso/issues/206#issuecomment-747363793 if (gap) { acc.push(gap); } @@ -234,10 +223,10 @@ const StatusList: React.FC = ({ onLoadMore={handleLoadOlder} placeholderComponent={() => } placeholderCount={20} - ref={node} + className={className} listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { 'divide-none': divideType !== 'border', - }, className)} + })} itemClassName={clsx({ 'pb-3': divideType !== 'border', })} diff --git a/packages/pl-fe/src/features/admin/announcements.tsx b/packages/pl-fe/src/features/admin/announcements.tsx index 5ee6f30b6b..fe1473bd71 100644 --- a/packages/pl-fe/src/features/admin/announcements.tsx +++ b/packages/pl-fe/src/features/admin/announcements.tsx @@ -109,7 +109,6 @@ const Announcements: React.FC = () => { { {domains && ( { { {relays && ( { { diff --git a/packages/pl-fe/src/features/admin/tabs/reports.tsx b/packages/pl-fe/src/features/admin/tabs/reports.tsx index dae9a1d41b..04401f5747 100644 --- a/packages/pl-fe/src/features/admin/tabs/reports.tsx +++ b/packages/pl-fe/src/features/admin/tabs/reports.tsx @@ -33,7 +33,6 @@ const Reports: React.FC = () => { diff --git a/packages/pl-fe/src/features/admin/user-index.tsx b/packages/pl-fe/src/features/admin/user-index.tsx index f92c9f02db..e3b161fbbf 100644 --- a/packages/pl-fe/src/features/admin/user-index.tsx +++ b/packages/pl-fe/src/features/admin/user-index.tsx @@ -49,7 +49,6 @@ const UserIndex: React.FC = () => { placeholder={intl.formatMessage(messages.searchPlaceholder)} /> {
- + {aliases.map((alias, i) => (
diff --git a/packages/pl-fe/src/features/blocks/index.tsx b/packages/pl-fe/src/features/blocks/index.tsx index d1d171b749..bf61043d1f 100644 --- a/packages/pl-fe/src/features/blocks/index.tsx +++ b/packages/pl-fe/src/features/blocks/index.tsx @@ -33,7 +33,6 @@ const Blocks: React.FC = () => { return ( { const dispatch = useAppDispatch(); - const ref = useRef(null); const conversations = useAppSelector((state) => state.conversations.items); const isLoading = useAppSelector((state) => state.conversations.isLoading); @@ -35,14 +32,6 @@ const ConversationsList: React.FC = () => { const element = document.querySelector(selector); if (element) element.focus(); - - ref.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - if (!element) document.querySelector(selector)?.focus(); - }, - }); }; const handleLoadOlder = debounce(() => { @@ -54,8 +43,6 @@ const ConversationsList: React.FC = () => { hasMore={hasMore} onLoadMore={handleLoadOlder} id='direct-list' - scrollKey='direct' - ref={ref} isLoading={isLoading} showLoading={isLoading && conversations.size === 0} emptyMessage={} diff --git a/packages/pl-fe/src/features/domain-blocks/index.tsx b/packages/pl-fe/src/features/domain-blocks/index.tsx index 26a36253b6..e1ea885ee6 100644 --- a/packages/pl-fe/src/features/domain-blocks/index.tsx +++ b/packages/pl-fe/src/features/domain-blocks/index.tsx @@ -41,7 +41,6 @@ const DomainBlocks: React.FC = () => { return ( handleLoadMore(dispatch)} hasMore={hasMore} emptyMessage={emptyMessage} diff --git a/packages/pl-fe/src/features/draft-statuses/index.tsx b/packages/pl-fe/src/features/draft-statuses/index.tsx index d368184a92..94f58da125 100644 --- a/packages/pl-fe/src/features/draft-statuses/index.tsx +++ b/packages/pl-fe/src/features/draft-statuses/index.tsx @@ -27,7 +27,6 @@ const DraftStatuses = () => { return ( diff --git a/packages/pl-fe/src/features/event/event-discussion.tsx b/packages/pl-fe/src/features/event/event-discussion.tsx index 039fb9327a..ac25a89bf2 100644 --- a/packages/pl-fe/src/features/event/event-discussion.tsx +++ b/packages/pl-fe/src/features/event/event-discussion.tsx @@ -18,7 +18,6 @@ import { getDescendantsIds } from '../status/components/thread'; import ThreadStatus from '../status/components/thread-status'; import type { MediaAttachment } from 'pl-api'; -import type { VirtuosoHandle } from 'react-virtuoso'; type RouteParams = { statusId: string }; @@ -52,7 +51,6 @@ const EventDiscussion: React.FC = (props) => { const [isLoaded, setIsLoaded] = useState(!!status); const node = useRef(null); - const scroller = useRef(null); const fetchData = () => { const { params } = props; @@ -87,14 +85,6 @@ const EventDiscussion: React.FC = (props) => { const element = document.querySelector(selector); if (element) element.focus(); - - scroller.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - if (!element) document.querySelector(selector)?.focus(); - }, - }); }; const renderTombstone = (id: string) => ( @@ -166,9 +156,7 @@ const EventDiscussion: React.FC = (props) => {
} - initialTopMostItemIndex={0} emptyMessage={} > {children} diff --git a/packages/pl-fe/src/features/filters/index.tsx b/packages/pl-fe/src/features/filters/index.tsx index 3212271a31..23c53ea303 100644 --- a/packages/pl-fe/src/features/filters/index.tsx +++ b/packages/pl-fe/src/features/filters/index.tsx @@ -63,11 +63,7 @@ const Filters = () => { - + {filters.map((filter) => (
diff --git a/packages/pl-fe/src/features/follow-recommendations/index.tsx b/packages/pl-fe/src/features/follow-recommendations/index.tsx index 6c449c621e..8e20fc18c4 100644 --- a/packages/pl-fe/src/features/follow-recommendations/index.tsx +++ b/packages/pl-fe/src/features/follow-recommendations/index.tsx @@ -28,11 +28,7 @@ const FollowRecommendations: React.FC = () => { return ( - + {suggestions.map((suggestion) => ( { return ( handleLoadMore(dispatch)} hasMore={hasMore} emptyMessage={emptyMessage} diff --git a/packages/pl-fe/src/features/followed-tags/index.tsx b/packages/pl-fe/src/features/followed-tags/index.tsx index 7d653988ff..12f4eb72c6 100644 --- a/packages/pl-fe/src/features/followed-tags/index.tsx +++ b/packages/pl-fe/src/features/followed-tags/index.tsx @@ -34,7 +34,6 @@ const FollowedTags = () => { return ( = ({ params }) => { return ( } diff --git a/packages/pl-fe/src/features/following/index.tsx b/packages/pl-fe/src/features/following/index.tsx index b75dc0b1b3..0a0b35a797 100644 --- a/packages/pl-fe/src/features/following/index.tsx +++ b/packages/pl-fe/src/features/following/index.tsx @@ -53,7 +53,6 @@ const Following: React.FC = ({ params }) => { return ( } diff --git a/packages/pl-fe/src/features/group/group-blocked-members.tsx b/packages/pl-fe/src/features/group/group-blocked-members.tsx index 534852a2c4..e4e5fcf20a 100644 --- a/packages/pl-fe/src/features/group/group-blocked-members.tsx +++ b/packages/pl-fe/src/features/group/group-blocked-members.tsx @@ -83,11 +83,7 @@ const GroupBlockedMembers: React.FC = ({ params }) => { return ( - + {accountIds.map((accountId) => , )} diff --git a/packages/pl-fe/src/features/group/group-members.tsx b/packages/pl-fe/src/features/group/group-members.tsx index e4e70a9eae..45e515e764 100644 --- a/packages/pl-fe/src/features/group/group-members.tsx +++ b/packages/pl-fe/src/features/group/group-members.tsx @@ -34,7 +34,6 @@ const GroupMembers: React.FC = (props) => { return ( <> = ({ params }) return ( } > {accounts.map((account) => ( diff --git a/packages/pl-fe/src/features/groups/index.tsx b/packages/pl-fe/src/features/groups/index.tsx index 30456e093e..8a76361d44 100644 --- a/packages/pl-fe/src/features/groups/index.tsx +++ b/packages/pl-fe/src/features/groups/index.tsx @@ -66,7 +66,6 @@ const Groups: React.FC = () => { )} { const hasMore = useAppSelector(state => state.notifications.hasMore); const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0); - const node = useRef(null); const column = useRef(null); const scrollableContentRef = useRef | null>(null); @@ -81,14 +79,6 @@ const Notifications = () => { const element = document.querySelector(selector); if (element) element.focus(); - - node.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - if (!element) document.querySelector(selector)?.focus(); - }, - }); }; const handleDequeueNotifications = useCallback(() => { @@ -138,8 +128,6 @@ const Notifications = () => { const scrollContainer = ( void }) => {
diff --git a/packages/pl-fe/src/features/quotes/index.tsx b/packages/pl-fe/src/features/quotes/index.tsx index 591c5cddd8..c3987bacd0 100644 --- a/packages/pl-fe/src/features/quotes/index.tsx +++ b/packages/pl-fe/src/features/quotes/index.tsx @@ -46,7 +46,6 @@ const Quotes: React.FC = () => { hasMore={hasMore} isLoading={typeof isLoading === 'boolean' ? isLoading : true} onLoadMore={() => handleLoadMore(statusId, dispatch)} - onRefresh={handleRefresh} emptyMessage={emptyMessage} divideType={(theme === 'black' || isMobile) ? 'border' : 'space'} /> diff --git a/packages/pl-fe/src/features/scheduled-statuses/index.tsx b/packages/pl-fe/src/features/scheduled-statuses/index.tsx index 5d941099db..fcaa86f964 100644 --- a/packages/pl-fe/src/features/scheduled-statuses/index.tsx +++ b/packages/pl-fe/src/features/scheduled-statuses/index.tsx @@ -34,7 +34,6 @@ const ScheduledStatuses = () => { return ( handleLoadMore(dispatch)} diff --git a/packages/pl-fe/src/features/search/components/search-results.tsx b/packages/pl-fe/src/features/search/components/search-results.tsx index 34891951f7..8866526d75 100644 --- a/packages/pl-fe/src/features/search/components/search-results.tsx +++ b/packages/pl-fe/src/features/search/components/search-results.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { List as ImmutableList, type OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { expandSearch, setFilter, setSearchAccount } from 'pl-fe/actions/search'; @@ -19,7 +19,6 @@ import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder import { useAppDispatch, useAppSelector, useFeatures } from 'pl-fe/hooks'; import type { SearchFilter } from 'pl-fe/reducers/search'; -import type { VirtuosoHandle } from 'react-virtuoso'; const messages = defineMessages({ accounts: { id: 'search_results.accounts', defaultMessage: 'People' }, @@ -29,8 +28,6 @@ const messages = defineMessages({ }); const SearchResults = () => { - const node = useRef(null); - const intl = useIntl(); const dispatch = useAppDispatch(); const features = useFeatures(); @@ -104,14 +101,6 @@ const SearchResults = () => { const element = document.querySelector(selector); if (element) element.focus(); - - node.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - if (!element) document.querySelector(selector)?.focus(); - }, - }); }; useEffect(() => { @@ -237,9 +226,7 @@ const SearchResults = () => { {noResultsMessage || ( statusId, @@ -113,12 +113,12 @@ const Thread: React.FC = ({ }; }); - let initialTopMostItemIndex = ancestorsIds.size; - if (!useWindowScroll && initialTopMostItemIndex !== 0) initialTopMostItemIndex = ancestorsIds.size + 1; + let initialIndex = ancestorsIds.size; + if (!useWindowScroll && initialIndex !== 0) initialIndex = ancestorsIds.size + 1; const node = useRef(null); const statusRef = useRef(null); - const scroller = useRef(null); + const virtualizer = useRef>(null); const handleHotkeyReact = () => { if (statusRef.current) { @@ -241,13 +241,10 @@ const Thread: React.FC = ({ if (element) element.focus(); - scroller.current?.scrollIntoView({ - index, - behavior: 'smooth', - done: () => { - if (!element) node.current?.querySelector(selector)?.focus(); - }, - }); + if (!element) { + virtualizer.current?.scrollToIndex(index, { behavior: 'smooth' }); + setTimeout(() => node.current?.querySelector(selector)?.focus(), 0); + } }; const renderTombstone = (id: string) => ( @@ -296,20 +293,7 @@ const Thread: React.FC = ({ // Scroll focused status into view when thread updates. useEffect(() => { - scroller.current?.scrollToIndex({ - index: ancestorsIds.size, - offset: -146, - }); - - // TODO: Actually fix this - setTimeout(() => { - scroller.current?.scrollToIndex({ - index: ancestorsIds.size, - offset: -146, - }); - - setTimeout(() => statusRef.current?.querySelector('.detailed-actualStatus')?.focus(), 0); - }, 0); + virtualizer.current?.scrollToIndex(ancestorsIds.size); }, [status.id, ancestorsIds.size]); const handleOpenCompareHistoryModal = (status: Pick) => { @@ -413,9 +397,9 @@ const Thread: React.FC = ({ > } - initialTopMostItemIndex={initialTopMostItemIndex} + initialIndex={initialIndex} useWindowScroll={useWindowScroll} itemClassName={itemClassName} listClassName={ diff --git a/packages/pl-fe/src/features/ui/components/modals/birthdays-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/birthdays-modal.tsx index 608b670691..dc4f460916 100644 --- a/packages/pl-fe/src/features/ui/components/modals/birthdays-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/birthdays-modal.tsx @@ -24,10 +24,10 @@ const BirthdaysModal = ({ onClose }: BaseModalProps) => { body = ( {accountIds.map(id => , diff --git a/packages/pl-fe/src/features/ui/components/modals/dislikes-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/dislikes-modal.tsx index 74960f8ea2..92adacc98f 100644 --- a/packages/pl-fe/src/features/ui/components/modals/dislikes-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/dislikes-modal.tsx @@ -39,10 +39,10 @@ const DislikesModal: React.FC = ({ onClose, body = ( {accountIds.map(id => , diff --git a/packages/pl-fe/src/features/ui/components/modals/event-participants-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/event-participants-modal.tsx index 7d424af536..f62051486a 100644 --- a/packages/pl-fe/src/features/ui/components/modals/event-participants-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/event-participants-modal.tsx @@ -39,10 +39,10 @@ const EventParticipantsModal: React.FC {accountIds.map(id => , diff --git a/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx index ec05ebf0ef..a0cbb22fa7 100644 --- a/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/familiar-followers-modal.tsx @@ -33,11 +33,11 @@ const FamiliarFollowersModal: React.FC {familiarFollowerIds.map(id => , diff --git a/packages/pl-fe/src/features/ui/components/modals/favourites-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/favourites-modal.tsx index 52bff20338..0cde9ca40e 100644 --- a/packages/pl-fe/src/features/ui/components/modals/favourites-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/favourites-modal.tsx @@ -46,7 +46,6 @@ const FavouritesModal: React.FC = ({ onCl body = ( = ({ onCl useWindowScroll={false} onLoadMore={handleLoadMore} hasMore={!!next} + estimatedSize={42} > {accountIds.map(id => , diff --git a/packages/pl-fe/src/features/ui/components/modals/mentions-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/mentions-modal.tsx index 23c29fe025..019809239b 100644 --- a/packages/pl-fe/src/features/ui/components/modals/mentions-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/mentions-modal.tsx @@ -42,9 +42,9 @@ const MentionsModal: React.FC = ({ onClose, } else { body = ( {accountIds.map(id => , diff --git a/packages/pl-fe/src/features/ui/components/modals/reactions-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/reactions-modal.tsx index 680b468f1e..20912f078a 100644 --- a/packages/pl-fe/src/features/ui/components/modals/reactions-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/reactions-modal.tsx @@ -86,7 +86,6 @@ const ReactionsModal: React.FC = ({ onClos body = (<> {reactions.size > 0 && renderFilterBar()} 0, @@ -95,6 +94,7 @@ const ReactionsModal: React.FC = ({ onClos itemClassName='pb-3' style={{ height: 'calc(80vh - 88px)' }} useWindowScroll={false} + estimatedSize={42} > {accounts.map((account) => , diff --git a/packages/pl-fe/src/features/ui/components/modals/reblogs-modal.tsx b/packages/pl-fe/src/features/ui/components/modals/reblogs-modal.tsx index 04af7da1ef..6031362f20 100644 --- a/packages/pl-fe/src/features/ui/components/modals/reblogs-modal.tsx +++ b/packages/pl-fe/src/features/ui/components/modals/reblogs-modal.tsx @@ -48,7 +48,6 @@ const ReblogsModal: React.FC = ({ onClose, s body = ( = ({ onClose, s useWindowScroll={false} onLoadMore={handleLoadMore} hasMore={!!next} + estimatedSize={42} > {accountIds.map((id) => , diff --git a/packages/pl-fe/src/features/ui/components/timeline.tsx b/packages/pl-fe/src/features/ui/components/timeline.tsx index 6d8855bda4..c416ba2f4b 100644 --- a/packages/pl-fe/src/features/ui/components/timeline.tsx +++ b/packages/pl-fe/src/features/ui/components/timeline.tsx @@ -15,6 +15,8 @@ const messages = defineMessages({ }); interface ITimeline extends Omit { + /** Unique key to preserve the scroll position when navigating back. */ + scrollKey: string; /** ID of the timeline in Redux. */ timelineId: string; /** Settings path to use instead of the timelineId. */ diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index d729598038..0037711fd2 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -2346,6 +2346,18 @@ dependencies: "@tanstack/query-core" "5.0.0" +"@tanstack/react-virtual@^3.10.8": + version "3.10.8" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.10.8.tgz#bf4b06f157ed298644a96ab7efc1a2b01ab36e3c" + integrity sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA== + dependencies: + "@tanstack/virtual-core" "3.10.8" + +"@tanstack/virtual-core@3.10.8": + version "3.10.8" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.10.8.tgz#975446a667755222f62884c19e5c3c66d959b8b4" + integrity sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA== + "@testing-library/dom@^9.0.0": version "9.0.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.0.1.tgz#fb9e3837fe2a662965df1536988f0863f01dbf51"