From 8da53b3006889faaa65c61a32a553356a796b537 Mon Sep 17 00:00:00 2001 From: mkljczk Date: Thu, 13 Mar 2025 17:32:07 +0100 Subject: [PATCH] pl-fe: Migrate back to react-virtuoso Signed-off-by: mkljczk --- packages/pl-fe/package.json | 1 + .../pl-fe/src/components/scrollable-list.tsx | 300 ++++++++++-------- packages/pl-fe/src/components/status-list.tsx | 24 +- .../src/features/admin/announcements.tsx | 1 + packages/pl-fe/src/features/admin/domains.tsx | 1 + .../src/features/admin/moderation-log.tsx | 1 + packages/pl-fe/src/features/admin/relays.tsx | 1 + packages/pl-fe/src/features/admin/rules.tsx | 1 + .../features/admin/tabs/awaiting-approval.tsx | 1 + .../pl-fe/src/features/admin/tabs/reports.tsx | 1 + .../pl-fe/src/features/admin/user-index.tsx | 1 + packages/pl-fe/src/features/aliases/index.tsx | 5 +- packages/pl-fe/src/features/blocks/index.tsx | 1 + .../features/chats/components/chat-list.tsx | 83 +++-- .../components/chat-message-list.test.tsx | 10 +- .../chats/components/chat-message-list.tsx | 98 ++++-- .../components/chat-page-sidebar.tsx | 9 +- .../chats/components/chat-pane/chat-pane.tsx | 8 +- .../components/chat-search/chat-search.tsx | 4 +- .../chats/components/chat-search/results.tsx | 32 +- .../tabs/manage-pending-participants.tsx | 1 + .../components/conversations-list.tsx | 15 +- .../src/features/domain-blocks/index.tsx | 1 + .../src/features/draft-statuses/index.tsx | 1 + .../src/features/event/event-discussion.tsx | 12 + packages/pl-fe/src/features/filters/index.tsx | 6 +- .../features/follow-recommendations/index.tsx | 6 +- .../components/outgoing-follow-requests.tsx | 1 + .../src/features/follow-requests/index.tsx | 1 + .../src/features/followed-tags/index.tsx | 1 + .../pl-fe/src/features/followers/index.tsx | 1 + .../pl-fe/src/features/following/index.tsx | 1 + .../features/group/group-blocked-members.tsx | 6 +- .../src/features/group/group-members.tsx | 1 + .../group/group-membership-requests.tsx | 1 + packages/pl-fe/src/features/groups/index.tsx | 1 + .../features/interaction-requests/index.tsx | 1 + .../src/features/landing-timeline/index.tsx | 2 +- .../src/features/notifications/index.tsx | 22 +- .../steps/suggested-accounts-step.tsx | 8 +- packages/pl-fe/src/features/quotes/index.tsx | 35 +- .../src/features/scheduled-statuses/index.tsx | 1 + .../search/components/search-results.tsx | 16 +- .../src/features/status/components/thread.tsx | 44 ++- .../ui/components/modals/birthdays-modal.tsx | 2 +- .../ui/components/modals/dislikes-modal.tsx | 8 +- .../modals/event-participants-modal.tsx | 2 +- .../modals/familiar-followers-modal.tsx | 7 +- .../ui/components/modals/favourites-modal.tsx | 8 +- .../ui/components/modals/mentions-modal.tsx | 7 +- .../ui/components/modals/reactions-modal.tsx | 7 +- .../ui/components/modals/reblogs-modal.tsx | 8 +- packages/pl-fe/yarn.lock | 5 + 53 files changed, 512 insertions(+), 310 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 4544352e0..4922abe5e 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -127,6 +127,7 @@ "react-sparklines": "^1.7.0", "react-sticky-box": "^2.0.5", "react-swipeable-views": "^0.14.0", + "react-virtuoso": "^4.12.5", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.1", diff --git a/packages/pl-fe/src/components/scrollable-list.tsx b/packages/pl-fe/src/components/scrollable-list.tsx index e8560a02b..4c07b47d9 100644 --- a/packages/pl-fe/src/components/scrollable-list.tsx +++ b/packages/pl-fe/src/components/scrollable-list.tsx @@ -1,16 +1,43 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { useVirtualizer, useWindowVirtualizer, type Virtualizer } from '@tanstack/react-virtual'; -import clsx from 'clsx'; -import React, { useEffect, useMemo, useRef } from 'react'; +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'; import LoadMore from 'pl-fe/components/load-more'; import Card from 'pl-fe/components/ui/card'; import Spinner from 'pl-fe/components/ui/spinner'; import { useSettings } from 'pl-fe/hooks/use-settings'; -const measureElement = (element: Element) => element.scrollHeight; +/** Custom Viruoso component context. */ +type Context = { + itemClassName?: string; + listClassName?: string; +} -interface IScrollableListBase { +/** 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; /** Pagination callback when the end of the list is reached. */ onLoadMore?: () => void; /** Whether the data is currently being fetched. */ @@ -29,42 +56,32 @@ interface IScrollableListBase { emptyMessageCard?: boolean; /** Scrollable content. */ children: Iterable; + /** Callback when the list is scrolled to the top. */ + onScrollToTop?: () => void; /** Callback when the list is scrolled. */ - onScroll?: (startIndex?: number, endIndex?: number) => void; + onScroll?: () => void; /** Placeholder component to render while loading. */ placeholderComponent?: React.ComponentType | React.NamedExoticComponent; /** Number of placeholders to render while loading. */ placeholderCount?: number; + /** Extra class names on the Virtuoso element. */ + className?: string; /** Extra class names on the list element. */ listClassName?: string; /** Class names on each item container. */ itemClassName?: string; /** Extra class names on the LoadMore element */ loadMoreClassName?: string; - /** `id` attribute on the parent element. */ + /** `id` attribute on the Virtuoso element. */ id?: string; - /** Initial item index to scroll to. */ - initialIndex?: number; - /** Estimated size for items. */ - estimatedSize?: number; - /** Align the items to the bottom of the list. */ - alignToBottom?: boolean; -} - -interface IScrollableListWithContainer extends IScrollableListBase { - /** Extra class names on the container element. */ - className?: string; - /** CSS styles on the container element. */ + /** CSS styles on the Virtuoso element. */ style?: React.CSSProperties; + /** Whether to use the window to scroll the content instead of Virtuoso's container. */ + useWindowScroll?: boolean; } -interface IScrollableListWithoutContainer extends IScrollableListBase { - parentRef: React.RefObject; -} - -type IScrollableList = IScrollableListWithContainer | IScrollableListWithoutContainer; - -const ScrollableList = React.forwardRef, IScrollableList>(({ +const ScrollableList = React.forwardRef(({ + scrollKey, prepend = null, alwaysPrepend, children, @@ -73,7 +90,9 @@ const ScrollableList = React.forwardRef, IScrollableList>( emptyMessageCard = true, showLoading, onScroll, + onScrollToTop, onLoadMore, + className, listClassName, itemClassName, loadMoreClassName, @@ -81,75 +100,59 @@ const ScrollableList = React.forwardRef, IScrollableList>( hasMore, placeholderComponent: Placeholder, placeholderCount = 0, - initialIndex, - estimatedSize = 300, - alignToBottom, - ...props + initialTopMostItemIndex = 0, + style = {}, + useWindowScroll = true, + ...params }, ref) => { - const listRef = useRef(null); - + 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); + /** 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; - const virtualizer = 'parentRef' in props ? useVirtualizer({ - count: data.length + (hasMore ? 1 : 0), - overscan: 1, - estimateSize: () => estimatedSize, - getScrollElement: () => props.parentRef.current, - measureElement, - }) : useWindowVirtualizer({ - count: data.length + (hasMore ? 1 : 0), - overscan: 1, - estimateSize: () => estimatedSize, - scrollMargin: listRef.current ? listRef.current.getBoundingClientRect().top + window.scrollY : 0, - measureElement, - }); + // 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(); + } - useEffect(() => { - if (typeof ref === 'function') ref(virtualizer); else if (ref !== null) ref.current = virtualizer; - }, [virtualizer]); - - const range = virtualizer.calculateRange(); - - useEffect(() => { - if (showLoading) return; - - if (typeof initialIndex === 'number') { - const targetIndex = (initialIndex === 0) ? initialIndex : initialIndex + 1; - virtualizer.scrollToIndex(targetIndex); - setTimeout(() => { - virtualizer.scrollToIndex(targetIndex); - }, 1); - } - }, [showLoading, initialIndex]); - - useEffect(() => { - onScroll?.(range?.startIndex, range?.endIndex); - }, [range?.startIndex, range?.endIndex]); - - useEffect(() => { - if (onLoadMore && range?.endIndex === data.length && !showLoading && autoloadMore && hasMore) { - onLoadMore(); - } - }, [range?.endIndex]); - - const loadMore = useMemo(() => { - if (autoloadMore || !hasMore || !onLoadMore) { - return null; + 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 { - const button = ; - - if (loadMoreClassName) return
{button}
; - - return button; + topOffset.current = 0; } - }, [autoloadMore, hasMore, isLoading]); + }, 150, { trailing: true }), []); + + useEffect(() => { + document.addEventListener('scroll', handleScroll); + sessionStorage.removeItem(scrollDataKey); + + return () => { + if (scrollKey) { + const data: SavedScrollPosition = { index: topIndex.current, offset: topOffset.current }; + sessionStorage.setItem(scrollDataKey, JSON.stringify(data)); + } + document.removeEventListener('scroll', handleScroll); + }; + }, []); /* Render an empty state instead of the scrollable list. */ const renderEmpty = (): JSX.Element => ( @@ -170,65 +173,100 @@ const ScrollableList = React.forwardRef, IScrollableList>(
); - const renderItem = (index: number): JSX.Element => { - const PlaceholderComponent = Placeholder || Spinner; - if (alignToBottom && hasMore ? index === 0 : index === data.length) return (isLoading) ? : loadMore ||
; - if (showPlaceholder) return ; - return data[alignToBottom && hasMore ? index - 1 : index]; + + /** Render a single item. */ + const renderItem = (_i: number, element: JSX.Element): JSX.Element => { + if (showPlaceholder) { + return ; + } else { + return element; + } }; - const virtualItems = virtualizer.getVirtualItems(); + const handleEndReached = () => { + if (autoloadMore && hasMore && onLoadMore) { + onLoadMore(); + } + }; - const body = ( -
- {(!showLoading || showPlaceholder) && data.length ? ( - <> - {prepend} - {virtualItems.map((item) => ( -
- {renderItem(item.index)} -
- ))} - - ) : renderEmpty()} -
- ); + const loadMore = () => { + if (autoloadMore || !hasMore || !onLoadMore) { + return null; + } else { + const button = ; - if ('parentRef' in props) return body; + 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]); return ( -
- {body} -
+ useWindowScroll={useWindowScroll} + data={data} + totalCount={data.length} + startReached={onScrollToTop} + endReached={handleEndReached} + isScrolling={isScrolling => isScrolling && onScroll && onScroll()} + itemContent={renderItem} + initialTopMostItemIndex={initialIndex} + rangeChanged={handleRangeChange} + className={className} + style={style} + context={{ + listClassName, + itemClassName, + }} + components={{ + Header: () => <>{prepend}, + ScrollSeekPlaceholder: Placeholder as any, + EmptyPlaceholder: () => renderEmpty(), + List, + Item, + Footer: loadMore, + }} + /> ); }); export { type IScrollableList, - type IScrollableListWithContainer, - type IScrollableListWithoutContainer, ScrollableList as default, }; diff --git a/packages/pl-fe/src/components/status-list.tsx b/packages/pl-fe/src/components/status-list.tsx index 32acc34d7..1015e464c 100644 --- a/packages/pl-fe/src/components/status-list.tsx +++ b/packages/pl-fe/src/components/status-list.tsx @@ -1,17 +1,19 @@ import clsx from 'clsx'; import debounce from 'lodash/debounce'; -import React, { useCallback, useMemo } from 'react'; +import React, { useRef, useCallback, useMemo } from 'react'; import { FormattedMessage } from 'react-intl'; import LoadGap from 'pl-fe/components/load-gap'; -import ScrollableList, { type IScrollableListWithContainer } from 'pl-fe/components/scrollable-list'; +import ScrollableList, { type IScrollableList } from 'pl-fe/components/scrollable-list'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import StatusContainer from 'pl-fe/containers/status-container'; import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status'; import PendingStatus from 'pl-fe/features/ui/components/pending-status'; -interface IStatusList extends Omit { +import type { VirtuosoHandle } from 'react-virtuoso'; + +interface IStatusList extends Omit { /** Unique key to preserve the scroll position when navigating back. */ scrollKey: string; /** List of status IDs to display. */ @@ -29,7 +31,7 @@ interface IStatusList extends Omit = ({ className, ...other }) => { + const node = useRef(null); + const getFeaturedStatusCount = () => featuredStatusIds?.length || 0; const getCurrentStatusIndex = (id: string, featured: boolean): number => { @@ -84,6 +88,14 @@ 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) => { @@ -200,10 +212,10 @@ const StatusList: React.FC = ({ onLoadMore={handleLoadOlder} placeholderComponent={() => } placeholderCount={20} - className={className} + ref={node} 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 8bb9f8822..91fa614d2 100644 --- a/packages/pl-fe/src/features/admin/announcements.tsx +++ b/packages/pl-fe/src/features/admin/announcements.tsx @@ -117,6 +117,7 @@ const Announcements: React.FC = () => { { {domains && ( { return ( { {relays && ( { { return ( { return ( { 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 aefb762a6..c9d6f2cc0 100644 --- a/packages/pl-fe/src/features/blocks/index.tsx +++ b/packages/pl-fe/src/features/blocks/index.tsx @@ -34,6 +34,7 @@ const Blocks: React.FC = () => { return ( void; - parentRef: React.RefObject; - topOffset: number; + useWindowScroll?: boolean; } -const ChatList: React.FC = ({ onClickChat, parentRef, topOffset }) => { +const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) => { const { chatsQuery: { data: chats, isFetching, hasNextPage, fetchNextPage, refetch } } = useChats(); const [isNearBottom, setNearBottom] = useState(false); @@ -44,47 +44,42 @@ const ChatList: React.FC = ({ onClickChat, parentRef, topOffset }) => }; return ( - <> -
- - { - setNearTop(top === 0); - setNearBottom(bottom === chats?.length); - }} - itemClassName='px-2' - emptyMessage={renderEmpty()} - placeholderComponent={PlaceholderChat} - placeholderCount={3} - hasMore={hasNextPage} - onLoadMore={handleLoadMore} - estimatedSize={64} - parentRef={parentRef} - loadMoreClassName='mx-4 mb-4' - > - {(chats || []).map(chat => ( - - ))} - - +
+ + setNearTop(atTop)} + atBottomStateChange={(atBottom) => setNearBottom(atBottom)} + useWindowScroll={useWindowScroll} + data={chats} + endReached={handleLoadMore} + itemContent={(_index, chat) => ( +
+ +
+ )} + components={{ + ScrollSeekPlaceholder: () => , + Footer: () => hasNextPage ? : null, + EmptyPlaceholder: renderEmpty, + }} + /> +
-
-
-
- + <> +
+
+ +
); }; diff --git a/packages/pl-fe/src/features/chats/components/chat-message-list.test.tsx b/packages/pl-fe/src/features/chats/components/chat-message-list.test.tsx index 9e944afd5..caf0df94d 100644 --- a/packages/pl-fe/src/features/chats/components/chat-message-list.test.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-message-list.test.tsx @@ -1,5 +1,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; +import { VirtuosoMockContext } from 'react-virtuoso'; import { __stub } from 'pl-fe/api'; import { ChatContext } from 'pl-fe/contexts/chat-context'; @@ -63,10 +64,11 @@ const store = { }; const renderComponentWithChatContext = () => render( - - - , - undefined, + + + + + , store, ); diff --git a/packages/pl-fe/src/features/chats/components/chat-message-list.tsx b/packages/pl-fe/src/features/chats/components/chat-message-list.tsx index 095292c53..ae1f93a2a 100644 --- a/packages/pl-fe/src/features/chats/components/chat-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-message-list.tsx @@ -1,10 +1,11 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { useIntl, defineMessages } from 'react-intl'; +import { Components, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; -import ScrollableList from 'pl-fe/components/scrollable-list'; import Avatar from 'pl-fe/components/ui/avatar'; import Button from 'pl-fe/components/ui/button'; import Divider from 'pl-fe/components/ui/divider'; +import Spinner from 'pl-fe/components/ui/spinner'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; import { Entities } from 'pl-fe/entity-store/entities'; @@ -39,6 +40,28 @@ const timeChange = (prev: Pick, curr: Pick { + const { context, ...rest } = props; + return
; +}); + +const Scroller: Components['Scroller'] = React.forwardRef((props, ref) => { + const { style, context, ...rest } = props; + + return ( +
+ ); +}); + interface IChatMessageList { /** Chat the messages are being rendered from. */ chat: Chat; @@ -48,7 +71,8 @@ interface IChatMessageList { const ChatMessageList: React.FC = ({ chat }) => { const intl = useIntl(); - const parentRef = useRef(null); + const node = useRef(null); + const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX - 20); const { markChatAsRead } = useChatActions(chat.id); const { @@ -66,6 +90,17 @@ const ChatMessageList: React.FC = ({ chat }) => { const isBlocked = !!useAppSelector((state) => (state.entities[Entities.RELATIONSHIPS]?.store[chat.account.id] as Relationship)?.blocked_by); + const lastChatMessage = chatMessages ? chatMessages[chatMessages.length - 1] : null; + + useEffect(() => { + if (!chatMessages) { + return; + } + + const nextFirstItemIndex = START_INDEX - chatMessages.length; + setFirstItemIndex(nextFirstItemIndex); + }, [lastChatMessage]); + const buildCachedMessages = (): Array => { if (!chatMessages) { return []; @@ -108,11 +143,23 @@ const ChatMessageList: React.FC = ({ chat }) => { }; const cachedChatMessages = buildCachedMessages(); - const handleStartReached = () => { - if (hasNextPage && !isLoading && !isFetching && !isFetchingNextPage) { + const initialScrollPositionProps = useMemo(() => { + if (process.env.NODE_ENV === 'test') { + return {}; + } + + return { + initialTopMostItemIndex: cachedChatMessages.length - 1, + firstItemIndex: Math.max(0, firstItemIndex), + }; + }, [cachedChatMessages.length, firstItemIndex]); + + const handleStartReached = useCallback(() => { + if (hasNextPage && !isFetching) { fetchNextPage(); } - }; + return false; + }, [firstItemIndex, hasNextPage, isFetching]); const renderDivider = (key: React.Key, text: string) => ; @@ -190,27 +237,34 @@ const ChatMessageList: React.FC = ({ chat }) => { } return ( -
-
- +
+ - {cachedChatMessages.map((chatMessage, index) => { + {...initialScrollPositionProps} + data={cachedChatMessages} + startReached={handleStartReached} + followOutput='auto' + itemContent={(index, chatMessage) => { if (chatMessage.type === 'divider') { - return renderDivider(index, chatMessage.text); + return renderDivider(index, (chatMessage as any).text); } else { return ; } - })} - + }} + components={{ + List, + Scroller, + Header: () => { + if (hasNextPage || isFetchingNextPage) { + return ; + } + + return null; + }, + }} + />
); diff --git a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx index 8cc549908..11770b503 100644 --- a/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-page/components/chat-page-sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; @@ -18,7 +18,6 @@ const messages = defineMessages({ const ChatPageSidebar = () => { const intl = useIntl(); const history = useHistory(); - const listRef = useRef(null); const handleClickChat = (chat: Chat) => { history.push(`/chats/${chat.id}`); @@ -33,7 +32,7 @@ const ChatPageSidebar = () => { }; return ( - + @@ -54,8 +53,8 @@ const ChatPageSidebar = () => { - - + + ); diff --git a/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx b/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx index ac1c41d1e..84459826d 100644 --- a/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import Stack from 'pl-fe/components/ui/stack'; @@ -19,8 +19,6 @@ import Blankslate from './blankslate'; import type { Chat } from 'pl-api'; const ChatPane = () => { - const ref = useRef(null); - const { unreadChatsCount } = useStatContext(); const { screen, changeScreen, isOpen, toggleChatPane } = useChatContext(); @@ -33,9 +31,9 @@ const ChatPane = () => { const renderBody = () => { if (Number(chats?.length) > 0 || isLoading) { return ( - + {(Number(chats?.length) > 0 || isLoading) ? ( - + ) : ( )} diff --git a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx index 69971a98b..e5dfe36b7 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx @@ -89,7 +89,7 @@ const ChatSearch: React.FC = ({ isMainPage = false }) => { }; return ( - +
= ({ isMainPage = false }) => { />
- + {renderBody()} diff --git a/packages/pl-fe/src/features/chats/components/chat-search/results.tsx b/packages/pl-fe/src/features/chats/components/chat-search/results.tsx index fd91367c4..f7f33a336 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/results.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/results.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import React, { useCallback, useState } from 'react'; +import { Virtuoso } from 'react-virtuoso'; -import ScrollableList from 'pl-fe/components/scrollable-list'; import Avatar from 'pl-fe/components/ui/avatar'; import HStack from 'pl-fe/components/ui/hstack'; import Stack from 'pl-fe/components/ui/stack'; @@ -54,23 +54,19 @@ const Results = ({ accountSearchResult, onSelect, parentRef }: IResults) => { ), []); - //
return ( - <> - { - setNearTop(startIndex === 0); - setNearBottom(endIndex === accounts?.length); - }} - isLoading={isFetching} - hasMore={hasNextPage} - onLoadMore={handleLoadMore} - parentRef={parentRef} - > - {(accounts || []).map((chat) => renderAccount(chat))} - +
+ ( +
+ {renderAccount(chat)} +
+ )} + endReached={handleLoadMore} + atTopStateChange={(atTop) => setNearTop(atTop)} + atBottomStateChange={(atBottom) => setNearBottom(atBottom)} + />
{ 'opacity-100': !isNearBottom, })} /> - +
); }; diff --git a/packages/pl-fe/src/features/compose-event/tabs/manage-pending-participants.tsx b/packages/pl-fe/src/features/compose-event/tabs/manage-pending-participants.tsx index 909fbf90f..23e92f21c 100644 --- a/packages/pl-fe/src/features/compose-event/tabs/manage-pending-participants.tsx +++ b/packages/pl-fe/src/features/compose-event/tabs/manage-pending-participants.tsx @@ -60,6 +60,7 @@ const ManagePendingParticipants: React.FC = ({ statu return accounts ? ( } hasMore={hasNextPage} isLoading={typeof isLoading === 'boolean' ? isLoading : true} diff --git a/packages/pl-fe/src/features/conversations/components/conversations-list.tsx b/packages/pl-fe/src/features/conversations/components/conversations-list.tsx index ac920c852..9a2dca048 100644 --- a/packages/pl-fe/src/features/conversations/components/conversations-list.tsx +++ b/packages/pl-fe/src/features/conversations/components/conversations-list.tsx @@ -1,5 +1,5 @@ import debounce from 'lodash/debounce'; -import React from 'react'; +import React, { useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { expandConversations } from 'pl-fe/actions/conversations'; @@ -9,8 +9,11 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import Conversation from './conversation'; +import type { VirtuosoHandle } from 'react-virtuoso'; + const ConversationsList: React.FC = () => { const dispatch = useAppDispatch(); + const ref = useRef(null); const conversations = useAppSelector((state) => state.conversations.items); const isLoading = useAppSelector((state) => state.conversations.isLoading); @@ -33,6 +36,14 @@ 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(() => { @@ -41,6 +52,8 @@ const ConversationsList: React.FC = () => { return ( { 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 d7566d41c..f9384175c 100644 --- a/packages/pl-fe/src/features/draft-statuses/index.tsx +++ b/packages/pl-fe/src/features/draft-statuses/index.tsx @@ -28,6 +28,7 @@ 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 3b1915cb8..810148c41 100644 --- a/packages/pl-fe/src/features/event/event-discussion.tsx +++ b/packages/pl-fe/src/features/event/event-discussion.tsx @@ -18,6 +18,7 @@ import ThreadStatus from '../status/components/thread-status'; import { ComposeForm } from '../ui/util/async-components'; import type { MediaAttachment } from 'pl-api'; +import type { VirtuosoHandle } from 'react-virtuoso'; type RouteParams = { statusId: string }; @@ -42,6 +43,7 @@ const EventDiscussion: React.FC = ({ params: { statusId: statu const [isLoaded, setIsLoaded] = useState(!!status); const node = useRef(null); + const scroller = useRef(null); const fetchData = () => dispatch(fetchStatusWithContext(statusId, intl)); @@ -72,6 +74,14 @@ const EventDiscussion: React.FC = ({ params: { statusId: statu 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) => ( @@ -142,8 +152,10 @@ const EventDiscussion: React.FC = ({ params: { statusId: statu
}
} + 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 37ba00068..df7bdd9aa 100644 --- a/packages/pl-fe/src/features/filters/index.tsx +++ b/packages/pl-fe/src/features/filters/index.tsx @@ -69,7 +69,11 @@ 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 6bac66783..d4e429a93 100644 --- a/packages/pl-fe/src/features/follow-recommendations/index.tsx +++ b/packages/pl-fe/src/features/follow-recommendations/index.tsx @@ -30,7 +30,11 @@ const FollowRecommendations: React.FC = () => { return ( - + {suggestions.map((suggestion) => ( { const body = accountIds ? ( fetchNextPage({ cancelRefetch: false })} diff --git a/packages/pl-fe/src/features/follow-requests/index.tsx b/packages/pl-fe/src/features/follow-requests/index.tsx index 5c14e2a92..e984fbb0a 100644 --- a/packages/pl-fe/src/features/follow-requests/index.tsx +++ b/packages/pl-fe/src/features/follow-requests/index.tsx @@ -20,6 +20,7 @@ const FollowRequests: React.FC = () => { const body = accountIds ? ( fetchNextPage({ cancelRefetch: false })} diff --git a/packages/pl-fe/src/features/followed-tags/index.tsx b/packages/pl-fe/src/features/followed-tags/index.tsx index 30fe7d987..1c0b79830 100644 --- a/packages/pl-fe/src/features/followed-tags/index.tsx +++ b/packages/pl-fe/src/features/followed-tags/index.tsx @@ -21,6 +21,7 @@ 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 b97b9225d..17518d1c7 100644 --- a/packages/pl-fe/src/features/following/index.tsx +++ b/packages/pl-fe/src/features/following/index.tsx @@ -55,6 +55,7 @@ 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 b795bcdc8..6f5b689a9 100644 --- a/packages/pl-fe/src/features/group/group-blocked-members.tsx +++ b/packages/pl-fe/src/features/group/group-blocked-members.tsx @@ -83,7 +83,11 @@ 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 4a0dcd7dd..81eb2fc9b 100644 --- a/packages/pl-fe/src/features/group/group-members.tsx +++ b/packages/pl-fe/src/features/group/group-members.tsx @@ -36,6 +36,7 @@ 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 b8698dd99..d29a36575 100644 --- a/packages/pl-fe/src/features/groups/index.tsx +++ b/packages/pl-fe/src/features/groups/index.tsx @@ -68,6 +68,7 @@ const Groups: React.FC = () => { )} { refetch()}> { {timelineEnabled ? ( { // const isUnread = useAppSelector(state => state.notifications.unread > 0); const hasMore = useAppSelector(state => state.notifications.hasMore); + const node = useRef(null); const scrollableContentRef = useRef | null>(null); // const handleLoadGap = (maxId) => { @@ -80,8 +82,12 @@ const Notifications = () => { dispatch(expandNotifications({ maxId: minId })); }, 300, { leading: true }), [displayedNotifications]); - const handleScroll = useCallback(debounce((startIndex?: number) => { - dispatch(scrollTopNotifications(startIndex === 0)); + const handleScrollToTop = useCallback(debounce(() => { + dispatch(scrollTopNotifications(true)); + }, 100), []); + + const handleScroll = useCallback(debounce(() => { + dispatch(scrollTopNotifications(false)); }, 100), []); const handleMoveUp = (id: string) => { @@ -99,6 +105,14 @@ 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(() => { @@ -114,6 +128,7 @@ const Notifications = () => { return () => { handleLoadOlder.cancel?.(); + handleScrollToTop.cancel(); handleScroll.cancel?.(); dispatch(scrollTopNotifications(false)); }; @@ -153,6 +168,8 @@ const Notifications = () => { const scrollContainer = ( { placeholderComponent={PlaceholderNotification} placeholderCount={20} onLoadMore={handleLoadOlder} + onScrollToTop={handleScrollToTop} onScroll={handleScroll} listClassName={clsx('divide-y divide-solid divide-gray-200 black:divide-gray-800 dark:divide-primary-800', { 'animate-pulse': displayedNotifications.length === 0, diff --git a/packages/pl-fe/src/features/onboarding/steps/suggested-accounts-step.tsx b/packages/pl-fe/src/features/onboarding/steps/suggested-accounts-step.tsx index 3ccc075c0..bb7e9d068 100644 --- a/packages/pl-fe/src/features/onboarding/steps/suggested-accounts-step.tsx +++ b/packages/pl-fe/src/features/onboarding/steps/suggested-accounts-step.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import { BigCard } from 'pl-fe/components/big-card'; @@ -10,7 +10,6 @@ import AccountContainer from 'pl-fe/containers/account-container'; import { useOnboardingSuggestions } from 'pl-fe/queries/suggestions'; const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { - const parentRef = useRef(null); const { data, isFetching } = useOnboardingSuggestions(); const renderSuggestions = () => { @@ -19,11 +18,12 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { } return ( -
+
{data.map((suggestion) => (
diff --git a/packages/pl-fe/src/features/quotes/index.tsx b/packages/pl-fe/src/features/quotes/index.tsx index 73f95d29f..f3bfbd98f 100644 --- a/packages/pl-fe/src/features/quotes/index.tsx +++ b/packages/pl-fe/src/features/quotes/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { useParams } from 'react-router-dom'; +import PullToRefresh from 'pl-fe/components/pull-to-refresh'; import StatusList from 'pl-fe/components/status-list'; import Column from 'pl-fe/components/ui/column'; import { useIsMobile } from 'pl-fe/hooks/use-is-mobile'; @@ -18,23 +19,33 @@ const Quotes: React.FC = () => { const theme = useTheme(); const isMobile = useIsMobile(); - const { data: statusIds = [], isLoading, hasNextPage, fetchNextPage } = useStatusQuotes(statusId); + const { data: statusIds = [], isLoading, hasNextPage, fetchNextPage, refetch } = useStatusQuotes(statusId); + + const handleRefresh = async () => { + await refetch(); + }; + + const handleLoadMore = () => { + fetchNextPage({ cancelRefetch: false }); + }; const emptyMessage = ; return ( - fetchNextPage({ cancelRefetch: false })} - 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 d670b71ee..5cb4eaa1f 100644 --- a/packages/pl-fe/src/features/scheduled-statuses/index.tsx +++ b/packages/pl-fe/src/features/scheduled-statuses/index.tsx @@ -35,6 +35,7 @@ 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 097591290..4fae1ed0b 100644 --- a/packages/pl-fe/src/features/search/components/search-results.tsx +++ b/packages/pl-fe/src/features/search/components/search-results.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useSearchParams } from 'react-router-dom-v5-compat'; @@ -23,6 +23,8 @@ import { useSuggestedAccounts } from 'pl-fe/queries/trends/use-suggested-account import { useTrendingLinks } from 'pl-fe/queries/trends/use-trending-links'; import { useTrendingStatuses } from 'pl-fe/queries/trends/use-trending-statuses'; +import type { VirtuosoHandle } from 'react-virtuoso'; + type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links'; const messages = defineMessages({ @@ -33,6 +35,8 @@ const messages = defineMessages({ }); const SearchResults = () => { + const node = useRef(null); + const intl = useIntl(); const features = useFeatures(); @@ -126,6 +130,14 @@ const SearchResults = () => { const element = document.querySelector(selector); if (element) element.focus(); + + node.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + if (!element) document.querySelector(selector)?.focus(); + }, + }); }; let searchResults: Array | undefined; @@ -237,6 +249,8 @@ const SearchResults = () => { {noResultsMessage || ( createSelector([ (_: RootState, statusId: string | undefined) => statusId, @@ -126,7 +126,7 @@ const Thread: React.FC = ({ const node = useRef(null); const statusRef = useRef(null); - const virtualizer = useRef>(null); + const scroller = useRef(null); const handleHotkeyReact = () => { if (statusRef.current) { @@ -247,10 +247,13 @@ const Thread: React.FC = ({ if (element) element.focus(); - if (!element) { - virtualizer.current?.scrollToIndex(index, { behavior: 'smooth' }); - setTimeout(() => node.current?.querySelector(selector)?.focus(), 0); - } + scroller.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + if (!element) node.current?.querySelector(selector)?.focus(); + }, + }); }; const renderTombstone = (id: string) => ( @@ -299,7 +302,20 @@ const Thread: React.FC = ({ // Scroll focused status into view when thread updates. useEffect(() => { - virtualizer.current?.scrollToIndex(ancestorsIds.length); + scroller.current?.scrollToIndex({ + index: ancestorsIds.length, + offset: -146, + }); + + // TODO: Actually fix this + setTimeout(() => { + scroller.current?.scrollToIndex({ + index: ancestorsIds.length, + offset: -146, + }); + + setTimeout(() => statusRef.current?.querySelector('.detailed-actualStatus')?.focus(), 0); + }, 0); }, [status.id, ancestorsIds.length]); const handleOpenCompareHistoryModal = useCallback((status: Pick) => { @@ -362,10 +378,10 @@ const Thread: React.FC = ({
); - const renderedAncestors = useMemo(() => [isModal ?
: null, ...renderChildren(ancestorsIds)], [ancestorsIds]); + const renderedAncestors = useMemo(() => [...(isModal ? [
] : []), ...renderChildren(ancestorsIds)], [ancestorsIds]); const renderedDescendants = useMemo(() => renderChildren(descendantsIds), [descendantsIds]); - const children: (JSX.Element | null)[] = [...renderedAncestors, focusedStatus, ...renderedDescendants]; + const children: (JSX.Element)[] = [...renderedAncestors, focusedStatus, ...renderedDescendants]; return ( = ({ } > } - initialIndex={initialIndex} + initialTopMostItemIndex={initialIndex} itemClassName={itemClassName} listClassName={ clsx({ 'h-full': isModal, }) } - {...(isModal ? { parentRef: node } : undefined)} + useWindowScroll={!isModal} + customScrollParent={node.current || undefined} > {children} 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 ef0c1c2f1..261311a48 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 @@ -30,7 +30,7 @@ const BirthdaysModal = ({ onClose }: BaseModalProps) => { emptyMessage={emptyMessage} listClassName='max-w-full' itemClassName='pb-3' - estimatedSize={42} + useWindowScroll={false} > {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 cf58f788d..9067cff5d 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 @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import ScrollableList from 'pl-fe/components/scrollable-list'; @@ -14,8 +14,6 @@ interface DislikesModalProps { } const DislikesModal: React.FC = ({ onClose, statusId }) => { - const modalRef = useRef(null); - const { data: accountIds, isLoading, hasNextPage, fetchNextPage } = useStatusDislikes(statusId); const onClickClose = () => { @@ -38,8 +36,7 @@ const DislikesModal: React.FC = ({ onClose, hasMore={hasNextPage} isLoading={typeof isLoading === 'boolean' ? isLoading : true} onLoadMore={() => fetchNextPage({ cancelRefetch: false })} - estimatedSize={42} - parentRef={modalRef} + useWindowScroll={false} > {accountIds.map(id => , @@ -52,7 +49,6 @@ const DislikesModal: React.FC = ({ onClose, } onClose={onClickClose} - ref={modalRef} > {body} 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 5cf2657b4..fe65c1c1f 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 @@ -32,10 +32,10 @@ const EventParticipantsModal: React.FC fetchNextPage({ cancelRefetch: false })} + useWindowScroll={false} > {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 136e15f8a..e5f7446fd 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 @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import ScrollableList from 'pl-fe/components/scrollable-list'; @@ -19,7 +19,6 @@ interface FamiliarFollowersModalProps { } const FamiliarFollowersModal: React.FC = ({ accountId, onClose }) => { - const modalRef = useRef(null); const account = useAppSelector(state => getAccount(state, accountId)); const { data: familiarFollowerIds } = useFamiliarFollowers(accountId); @@ -45,8 +44,7 @@ const FamiliarFollowersModal: React.FC {familiarFollowerIds.map(id => , @@ -65,7 +63,6 @@ const FamiliarFollowersModal: React.FC } onClose={onClickClose} - ref={modalRef} > {body} 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 0cbffdc6a..bc1b1f13d 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 @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import ScrollableList from 'pl-fe/components/scrollable-list'; @@ -14,8 +14,6 @@ interface FavouritesModalProps { } const FavouritesModal: React.FC = ({ onClose, statusId }) => { - const modalRef = useRef(null); - const { data: accountIds, isLoading, hasNextPage, fetchNextPage } = useStatusFavourites(statusId); const onClickClose = () => { @@ -38,8 +36,7 @@ const FavouritesModal: React.FC = ({ onCl hasMore={hasNextPage} isLoading={typeof isLoading === 'boolean' ? isLoading : true} onLoadMore={() => fetchNextPage({ cancelRefetch: false })} - estimatedSize={42} - parentRef={modalRef} + useWindowScroll={false} > {accountIds.map(id => , @@ -52,7 +49,6 @@ const FavouritesModal: React.FC = ({ onCl } onClose={onClickClose} - ref={modalRef} > {body} 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 39347a0f5..66c5fca5c 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 @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { fetchStatusWithContext } from 'pl-fe/actions/statuses'; @@ -17,7 +17,6 @@ interface MentionsModalProps { } const MentionsModal: React.FC = ({ onClose, statusId }) => { - const modalRef = useRef(null); const dispatch = useAppDispatch(); const intl = useIntl(); const getStatus = useCallback(makeGetStatus(), []); @@ -46,8 +45,7 @@ const MentionsModal: React.FC = ({ onClose, {accountIds.map(id => , @@ -60,7 +58,6 @@ const MentionsModal: React.FC = ({ onClose, } onClose={onClickClose} - ref={modalRef} > {body} 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 72e574e2c..6c0eae005 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 @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React, { useMemo, useRef, useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import ScrollableList from 'pl-fe/components/scrollable-list'; @@ -29,7 +29,6 @@ interface ReactionsModalProps { } const ReactionsModal: React.FC = ({ onClose, statusId, reaction: initialReaction }) => { - const modalRef = useRef(null); const intl = useIntl(); const [reaction, setReaction] = useState(initialReaction); @@ -91,8 +90,7 @@ const ReactionsModal: React.FC = ({ onClos itemClassName='pb-3' style={{ height: 'calc(80vh - 88px)' }} isLoading={typeof isLoading === 'boolean' ? isLoading : true} - estimatedSize={42} - parentRef={modalRef} + useWindowScroll={false} > {accounts.map((account) => , @@ -105,7 +103,6 @@ const ReactionsModal: React.FC = ({ onClos } onClose={onClickClose} - ref={modalRef} > {body} 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 45d968930..71b24c063 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 @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React from 'react'; import { FormattedMessage } from 'react-intl'; import ScrollableList from 'pl-fe/components/scrollable-list'; @@ -14,8 +14,6 @@ interface ReblogsModalProps { } const ReblogsModal: React.FC = ({ onClose, statusId }) => { - const modalRef = useRef(null); - const { data: accountIds, isLoading, hasNextPage, fetchNextPage } = useStatusReblogs(statusId); const onClickClose = () => { @@ -38,8 +36,7 @@ const ReblogsModal: React.FC = ({ onClose, s hasMore={hasNextPage} isLoading={typeof isLoading === 'boolean' ? isLoading : true} onLoadMore={() => fetchNextPage({ cancelRefetch: false })} - estimatedSize={42} - parentRef={modalRef} + useWindowScroll={false} > {accountIds.map((id) => , @@ -52,7 +49,6 @@ const ReblogsModal: React.FC = ({ onClose, s } onClose={onClickClose} - ref={modalRef} > {body} diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index 3ac89aab7..80237571d 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -8212,6 +8212,11 @@ react-swipeable-views@^0.14.0: react-swipeable-views-utils "^0.14.0" warning "^4.0.1" +react-virtuoso@^4.12.5: + version "4.12.5" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.12.5.tgz#cf92efc2527e56d6df1d4d63c6e4dd3fac5a4030" + integrity sha512-YeCbRRsC9CLf0buD0Rct7WsDbzf+yBU1wGbo05/XjbcN2nJuhgh040m3y3+6HVogTZxEqVm45ac9Fpae4/MxRQ== + react@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"