From aadd9439aa0270770a70a680fdded01ebd373cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 19 Sep 2024 15:15:02 +0200 Subject: [PATCH] pl-fe: move ChatList to @tanstack/virtual, some fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../pl-fe/src/components/scrollable-list.tsx | 32 +++---- .../features/chats/components/chat-list.tsx | 90 ++++++++++--------- .../components/chat-message-list.test.tsx | 11 ++- .../components/chat-page-sidebar.tsx | 9 +- .../chats/components/chat-pane/chat-pane.tsx | 10 +-- .../src/features/landing-timeline/index.tsx | 2 +- .../src/features/notifications/index.tsx | 10 +-- .../steps/suggested-accounts-step.tsx | 8 +- .../src/features/status/components/thread.tsx | 3 +- .../modals/familiar-followers-modal.tsx | 6 +- .../ui/components/modals/favourites-modal.tsx | 6 +- .../ui/components/modals/reactions-modal.tsx | 6 +- .../ui/components/modals/reblogs-modal.tsx | 7 +- .../src/features/ui/components/timeline.tsx | 9 +- 14 files changed, 108 insertions(+), 101 deletions(-) diff --git a/packages/pl-fe/src/components/scrollable-list.tsx b/packages/pl-fe/src/components/scrollable-list.tsx index b1c4f9e25..45d195924 100644 --- a/packages/pl-fe/src/components/scrollable-list.tsx +++ b/packages/pl-fe/src/components/scrollable-list.tsx @@ -8,6 +8,15 @@ import { useSettings } from 'pl-fe/hooks'; import LoadMore from './load-more'; import { Card, Spinner } from './ui'; +type IScrollableListWindowScroll = { + /** Whether to use the window to scroll the content instead of the container. */ + useWindowScroll?: true; +} | { + /** Whether to use the window to scroll the content instead of the container. */ + useWindowScroll: false; + parentRef: React.RefObject; +}; + interface IScrollableList { /** Pagination callback when the end of the list is reached. */ onLoadMore?: () => void; @@ -27,10 +36,8 @@ interface IScrollableList { emptyMessageCard?: boolean; /** Scrollable content. */ children: Iterable; - /** Callback when the list is scrolled to the top. */ - onScrollToTop?: () => void; /** Callback when the list is scrolled. */ - onScroll?: () => void; + onScroll?: (startIndex?: number, endIndex?: number) => void; /** Placeholder component to render while loading. */ placeholderComponent?: React.ComponentType | React.NamedExoticComponent; /** Number of placeholders to render while loading. */ @@ -47,15 +54,13 @@ interface IScrollableList { id?: string; /** CSS styles on the parent element. */ style?: React.CSSProperties; - /** 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; } -const ScrollableList = React.forwardRef, IScrollableList>(({ +const ScrollableList = React.forwardRef, IScrollableList & IScrollableListWindowScroll>(({ prepend = null, alwaysPrepend, children, @@ -64,7 +69,6 @@ const ScrollableList = React.forwardRef, IScrollableList>( emptyMessageCard = true, showLoading, onScroll, - onScrollToTop, onLoadMore, className, listClassName, @@ -76,8 +80,8 @@ const ScrollableList = React.forwardRef, IScrollableList>( placeholderCount = 0, initialIndex = 0, style = {}, - useWindowScroll = true, estimatedSize = 300, + ...props }, ref) => { const { autoloadMore } = useSettings(); @@ -90,15 +94,15 @@ const ScrollableList = React.forwardRef, IScrollableList>( const data = showPlaceholder ? Array(placeholderCount).fill('') : elements; - const virtualizer = useWindowScroll ? useWindowVirtualizer({ + const virtualizer = props.useWindowScroll === false ? useVirtualizer({ count: data.length + (hasMore ? 1 : 0), overscan: 3, estimateSize: () => estimatedSize, - }) : useVirtualizer({ + getScrollElement: () => props.parentRef.current || parentRef.current, + }) : useWindowVirtualizer({ count: data.length + (hasMore ? 1 : 0), overscan: 3, estimateSize: () => estimatedSize, - getScrollElement: () => parentRef.current, }); useEffect(() => { @@ -114,10 +118,8 @@ const ScrollableList = React.forwardRef, IScrollableList>( }, [showLoading, initialIndex]); useEffect(() => { - if (range?.startIndex === 0) { - onScrollToTop?.(); - } else onScroll?.(); - }, [range?.startIndex === 0]); + onScroll?.(range?.startIndex, range?.endIndex); + }, [range?.startIndex, range?.endIndex]); useEffect(() => { if (onLoadMore && range?.endIndex === data.length && !showLoading && autoloadMore && hasMore) { diff --git a/packages/pl-fe/src/features/chats/components/chat-list.tsx b/packages/pl-fe/src/features/chats/components/chat-list.tsx index 25ce691bf..d6f83feb3 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list.tsx @@ -1,9 +1,9 @@ import clsx from 'clsx'; -import React, { useRef, useState } from 'react'; -import { Virtuoso } from 'react-virtuoso'; +import React, { useState } from 'react'; import PullToRefresh from 'pl-fe/components/pull-to-refresh'; -import { Spinner, Stack } from 'pl-fe/components/ui'; +import ScrollableList from 'pl-fe/components/scrollable-list'; +import { Stack } from 'pl-fe/components/ui'; import PlaceholderChat from 'pl-fe/features/placeholder/components/placeholder-chat'; import { useChats } from 'pl-fe/queries/chats'; @@ -11,12 +11,11 @@ import ChatListItem from './chat-list-item'; interface IChatList { onClickChat: (chat: any) => void; - useWindowScroll?: boolean; + parentRef: React.RefObject; + topOffset: number; } -const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) => { - const chatListRef = useRef(null); - +const ChatList: React.FC = ({ onClickChat, parentRef, topOffset }) => { const { chatsQuery: { data: chats, isFetching, hasNextPage, fetchNextPage, refetch } } = useChats(); const [isNearBottom, setNearBottom] = useState(false); @@ -45,43 +44,48 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) }; return ( -
- - setNearTop(atTop)} - atBottomStateChange={(atBottom) => setNearBottom(atBottom)} - useWindowScroll={useWindowScroll} - data={chats} - endReached={handleLoadMore} - itemContent={(_index, chat) => ( -
- -
- )} - components={{ - ScrollSeekPlaceholder: () => , - Footer: () => hasNextPage ? : null, - EmptyPlaceholder: renderEmpty, - }} - /> -
+ <> +
+ + { + setNearTop(top === 0); + setNearBottom(bottom === chats?.length); + }} + itemClassName='px-2' + emptyMessage={renderEmpty()} + placeholderComponent={PlaceholderChat} + placeholderCount={3} + hasMore={hasNextPage} + onLoadMore={handleLoadMore} + estimatedSize={64} + useWindowScroll={false} + parentRef={parentRef} + loadMoreClassName='mx-4 mb-4' + > + {(chats || []).map(chat => ( + + ))} + + - <> -
-
- -
+
+
+
+ ); }; 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 bbd210b3f..b83fef0ff 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,6 +1,5 @@ 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'; @@ -62,11 +61,11 @@ const store = rootState .set('instance', buildInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)' })); const renderComponentWithChatContext = () => render( - - - - - , + // + + + , + // , undefined, store, ); 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 460d3a428..4c7a0551f 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 from 'react'; +import React, { useRef } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; @@ -15,6 +15,7 @@ const messages = defineMessages({ const ChatPageSidebar = () => { const intl = useIntl(); const history = useHistory(); + const listRef = useRef(null); const handleClickChat = (chat: Chat) => { history.push(`/chats/${chat.id}`); @@ -29,7 +30,7 @@ const ChatPageSidebar = () => { }; return ( - + @@ -50,8 +51,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 408b9c3f6..f9bfbce78 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 from 'react'; +import React, { useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { Stack } from 'pl-fe/components/ui'; @@ -19,6 +19,8 @@ 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(); @@ -31,11 +33,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/landing-timeline/index.tsx b/packages/pl-fe/src/features/landing-timeline/index.tsx index dc2ac1934..5e5bf9664 100644 --- a/packages/pl-fe/src/features/landing-timeline/index.tsx +++ b/packages/pl-fe/src/features/landing-timeline/index.tsx @@ -48,7 +48,7 @@ const LandingTimeline = () => { {timelineEnabled ? ( { dispatch(expandNotifications({ maxId: last && last.id })); }, 300, { leading: true }), [notifications]); - const handleScrollToTop = useCallback(debounce(() => { - dispatch(scrollTopNotifications(true)); - }, 100), []); - - const handleScroll = useCallback(debounce(() => { - dispatch(scrollTopNotifications(false)); + const handleScroll = useCallback(debounce((startIndex?: number) => { + dispatch(scrollTopNotifications(startIndex === 0)); }, 100), []); const handleMoveUp = (id: string) => { @@ -93,7 +89,6 @@ const Notifications = () => { return () => { handleLoadOlder.cancel(); - handleScrollToTop.cancel(); handleScroll.cancel(); dispatch(scrollTopNotifications(false)); }; @@ -135,7 +130,6 @@ const Notifications = () => { 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': notifications.size === 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 92f56aa88..3cc57026b 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 from 'react'; +import React, { useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { BigCard } from 'pl-fe/components/big-card'; @@ -8,6 +8,7 @@ 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 = () => { @@ -16,11 +17,12 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { } return ( -
+
{data.map((suggestion) => (
diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index c425c2041..6eea98040 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -400,13 +400,14 @@ const Thread: React.FC = ({ ref={virtualizer} placeholderComponent={() => } initialIndex={initialIndex} - useWindowScroll={useWindowScroll} itemClassName={itemClassName} listClassName={ clsx({ 'h-full': !useWindowScroll, }) } + useWindowScroll={useWindowScroll} + parentRef={node} > {children} 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 a0cbb22fa..48575c01e 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,5 +1,5 @@ import { OrderedSet as ImmutableOrderedSet } from 'immutable'; -import React from 'react'; +import React, { useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import ScrollableList from 'pl-fe/components/scrollable-list'; @@ -17,6 +17,7 @@ interface FamiliarFollowersModalProps { } const FamiliarFollowersModal: React.FC = ({ accountId, onClose }) => { + const modalRef = useRef(null); const account = useAppSelector(state => getAccount(state, accountId)); const familiarFollowerIds: ImmutableOrderedSet = useAppSelector(state => state.user_lists.familiar_followers.get(accountId)?.items || ImmutableOrderedSet()); @@ -36,8 +37,9 @@ 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 0cde9ca40..13320f881 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, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; import { fetchFavourites, expandFavourites } from 'pl-fe/actions/interactions'; @@ -14,6 +14,7 @@ interface FavouritesModalProps { } const FavouritesModal: React.FC = ({ onClose, statusId }) => { + const modalRef = useRef(null); const dispatch = useAppDispatch(); const accountIds = useAppSelector((state) => state.user_lists.favourited_by.get(statusId)?.items); @@ -50,10 +51,11 @@ const FavouritesModal: React.FC = ({ onCl listClassName='max-w-full' itemClassName='pb-3' style={{ height: 'calc(80vh - 88px)' }} - useWindowScroll={false} onLoadMore={handleLoadMore} hasMore={!!next} estimatedSize={42} + useWindowScroll={false} + parentRef={modalRef} > {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 20912f078..d73cb69a6 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,6 +1,6 @@ import clsx from 'clsx'; import { List as ImmutableList } from 'immutable'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { fetchReactions } from 'pl-fe/actions/interactions'; @@ -28,6 +28,7 @@ interface ReactionsModalProps { } const ReactionsModal: React.FC = ({ onClose, statusId, reaction: initialReaction }) => { + const modalRef = useRef(null); const dispatch = useAppDispatch(); const intl = useIntl(); const [reaction, setReaction] = useState(initialReaction); @@ -93,8 +94,9 @@ const ReactionsModal: React.FC = ({ onClos listClassName='max-w-full' itemClassName='pb-3' style={{ height: 'calc(80vh - 88px)' }} - useWindowScroll={false} estimatedSize={42} + useWindowScroll={false} + parentRef={modalRef} > {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 6031362f2..e0514763f 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, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { fetchReblogs, expandReblogs } from 'pl-fe/actions/interactions'; @@ -19,6 +19,7 @@ const ReblogsModal: React.FC = ({ onClose, s const intl = useIntl(); const accountIds = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.items); const next = useAppSelector((state) => state.user_lists.reblogged_by.get(statusId)?.next); + const modalRef = useRef(null); const fetchData = () => { dispatch(fetchReblogs(statusId)); @@ -52,10 +53,11 @@ const ReblogsModal: React.FC = ({ onClose, s listClassName='max-w-full' itemClassName='pb-3' style={{ height: 'calc(80vh - 88px)' }} - useWindowScroll={false} onLoadMore={handleLoadMore} hasMore={!!next} estimatedSize={42} + useWindowScroll={false} + parentRef={modalRef} > {accountIds.map((id) => , @@ -68,6 +70,7 @@ const ReblogsModal: React.FC = ({ onClose, s } onClose={onClickClose} + ref={modalRef} > {body} diff --git a/packages/pl-fe/src/features/ui/components/timeline.tsx b/packages/pl-fe/src/features/ui/components/timeline.tsx index c416ba2f4..6c152e3f9 100644 --- a/packages/pl-fe/src/features/ui/components/timeline.tsx +++ b/packages/pl-fe/src/features/ui/components/timeline.tsx @@ -44,12 +44,8 @@ const Timeline: React.FC = ({ dispatch(dequeueTimeline(timelineId, onLoadMore)); }, []); - const handleScrollToTop = useCallback(debounce(() => { - dispatch(scrollTopTimeline(timelineId, true)); - }, 100), [timelineId]); - - const handleScroll = useCallback(debounce(() => { - dispatch(scrollTopTimeline(timelineId, false)); + const handleScroll = useCallback(debounce((startIndex?: number) => { + dispatch(scrollTopTimeline(timelineId, startIndex === 0)); }, 100), [timelineId]); return ( @@ -65,7 +61,6 @@ const Timeline: React.FC = ({