From 5a30509fa6d2f732080750d2341852ffaf0e415e Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 8 Dec 2022 14:37:04 -0500 Subject: [PATCH] Add support for pagination in Chat Search --- .../chat-page/components/chat-page-new.tsx | 40 ++----- .../chats/components/chat-pane/chat-pane.tsx | 9 +- .../chats/components/chat-search-input.tsx | 1 + .../components/chat-search/chat-search.tsx | 107 +++++++----------- .../chats/components/chat-search/results.tsx | 103 +++++++++++------ .../headers/chat-search-header.tsx | 46 ++++++++ app/soapbox/queries/search.ts | 45 ++++++-- 7 files changed, 210 insertions(+), 141 deletions(-) create mode 100644 app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx diff --git a/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx b/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx index 5b441026e..c362a4159 100644 --- a/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import AccountSearch from 'soapbox/components/account-search'; -import { CardTitle, HStack, IconButton, Stack, Text } from 'soapbox/components/ui'; -import { ChatKeys, useChats } from 'soapbox/queries/chats'; -import { queryClient } from 'soapbox/queries/client'; +import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui'; + +import ChatSearch from '../../chat-search/chat-search'; interface IChatPageNew { } @@ -13,17 +11,10 @@ interface IChatPageNew { /** New message form to create a chat. */ const ChatPageNew: React.FC = () => { const history = useHistory(); - const { getOrCreateChatByAccountId } = useChats(); - - const handleAccountSelected = async (accountId: string) => { - const { data } = await getOrCreateChatByAccountId(accountId); - history.push(`/chats/${data.id}`); - queryClient.invalidateQueries(ChatKeys.chatSearch()); - }; return ( - - + + = () => { - - - - - - - - + + ); }; diff --git a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx index ad0dd8e9e..59953b54c 100644 --- a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx +++ b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx @@ -13,6 +13,7 @@ import ChatSearch from '../chat-search/chat-search'; import EmptyResultsBlankslate from '../chat-search/empty-results-blankslate'; import ChatPaneHeader from '../chat-widget/chat-pane-header'; import ChatWindow from '../chat-widget/chat-window'; +import ChatSearchHeader from '../chat-widget/headers/chat-search-header'; import { Pane } from '../ui'; import Blankslate from './blankslate'; @@ -86,7 +87,13 @@ const ChatPane = () => { } if (screen === ChatWidgetScreens.SEARCH) { - return ; + return ( + + + + {isOpen ? : null} + + ); } return ( diff --git a/app/soapbox/features/chats/components/chat-search-input.tsx b/app/soapbox/features/chats/components/chat-search-input.tsx index 2474ac6cb..bc8644aab 100644 --- a/app/soapbox/features/chats/components/chat-search-input.tsx +++ b/app/soapbox/features/chats/components/chat-search-input.tsx @@ -29,6 +29,7 @@ const ChatSearchInput: React.FC = ({ value, onChange, onClear className='rounded-full' value={value} onChange={onChange} + outerClassName='mt-0' theme='search' append={ + } + /> + - - {intl.formatMessage(messages.title)} - - - } - isOpen={isOpen} - isToggleable={false} - onToggle={toggleChatPane} - /> - - {isOpen ? ( - -
- setValue(event.target.value)} - theme='search' - append={ - - } - /> -
- - - {renderBody()} - -
- ) : null} - + + {renderBody()} + +
); }; diff --git a/app/soapbox/features/chats/components/chat-search/results.tsx b/app/soapbox/features/chats/components/chat-search/results.tsx index 26127a20b..48909f67e 100644 --- a/app/soapbox/features/chats/components/chat-search/results.tsx +++ b/app/soapbox/features/chats/components/chat-search/results.tsx @@ -1,43 +1,80 @@ -import React from 'react'; +import classNames from 'clsx'; +import React, { useCallback, useState } from 'react'; +import { Virtuoso } from 'react-virtuoso'; import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification-badge'; +import useAccountSearch from 'soapbox/queries/search'; interface IResults { - accounts: { - display_name: string - acct: string - id: string - avatar: string - verified: boolean - }[] + accountSearchResult: ReturnType onSelect(id: string): void } -const Results = ({ accounts, onSelect }: IResults) => ( - <> - {(accounts || []).map((account: any) => ( - - ))} - -); + const [isNearBottom, setNearBottom] = useState(false); + const [isNearTop, setNearTop] = useState(true); -export default Results; \ No newline at end of file + const handleLoadMore = () => { + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }; + + const renderAccount = useCallback((_index, account) => ( + + ), []); + + return ( +
+ ( +
+ {renderAccount(index, chat)} +
+ )} + endReached={handleLoadMore} + atTopStateChange={(atTop) => setNearTop(atTop)} + atBottomStateChange={(atBottom) => setNearBottom(atBottom)} + /> + + <> +
+
+ +
+ ); +}; + +export default Results; diff --git a/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx b/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx new file mode 100644 index 000000000..d0fd40921 --- /dev/null +++ b/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { defineMessages, useIntl } from 'react-intl'; + +import { HStack, Icon, Text } from 'soapbox/components/ui'; +import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context'; + +import ChatPaneHeader from '../chat-pane-header'; + +const messages = defineMessages({ + title: { id: 'chat_search.title', defaultMessage: 'Messages' }, +}); + +const ChatSearchHeader = () => { + const intl = useIntl(); + + const { changeScreen, isOpen, toggleChatPane } = useChatContext(); + + return ( + + + + + {intl.formatMessage(messages.title)} + + + } + isOpen={isOpen} + isToggleable={false} + onToggle={toggleChatPane} + /> + ); +}; + +export default ChatSearchHeader; \ No newline at end of file diff --git a/app/soapbox/queries/search.ts b/app/soapbox/queries/search.ts index 2b3fd1e70..40b78a015 100644 --- a/app/soapbox/queries/search.ts +++ b/app/soapbox/queries/search.ts @@ -1,27 +1,54 @@ -import { useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { getNextLink } from 'soapbox/api'; import { useApi } from 'soapbox/hooks'; +import { Account } from 'soapbox/types/entities'; +import { PaginatedResult } from 'soapbox/utils/queries'; export default function useAccountSearch(q: string) { const api = useApi(); - const getAccountSearch = async(q: string) => { - if (typeof q === 'undefined') { - return null; - } + const getAccountSearch = async(q: string, pageParam: { link?: string }): Promise> => { + const nextPageLink = pageParam?.link; + const uri = nextPageLink || '/api/v1/accounts/search'; - const { data } = await api.get('/api/v1/accounts/search', { + const response = await api.get(uri, { params: { q, + limit: 10, followers: true, }, }); + const { data } = response; - return data; + const link = getNextLink(response); + const hasMore = !!link; + + return { + result: data, + link, + hasMore, + }; }; - return useQuery(['search', 'accounts', q], () => getAccountSearch(q), { + const queryInfo = useInfiniteQuery(['search', 'accounts', q], ({ pageParam }) => getAccountSearch(q, pageParam), { keepPreviousData: true, - placeholderData: [], + getNextPageParam: (config) => { + if (config.hasMore) { + return { link: config.link }; + } + + return undefined; + }, }); + + const data = queryInfo.data?.pages.reduce( + (prev: Account[], curr) => [...prev, ...curr.result], + [], + ); + + return { + ...queryInfo, + data, + }; }