diff --git a/app/soapbox/components/account-search.tsx b/app/soapbox/components/account-search.tsx index 8b3f50b20..c519b0243 100644 --- a/app/soapbox/components/account-search.tsx +++ b/app/soapbox/components/account-search.tsx @@ -5,7 +5,6 @@ import { defineMessages, useIntl } from 'react-intl'; import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input'; import SvgIcon from './ui/icon/svg-icon'; -import { InputThemes } from './ui/input/input'; const messages = defineMessages({ placeholder: { id: 'account_search.placeholder', defaultMessage: 'Search for an account' }, @@ -16,20 +15,10 @@ interface IAccountSearch { onSelected: (accountId: string) => void, /** Override the default placeholder of the input. */ placeholder?: string, - /** Position of results relative to the input. */ - resultsPosition?: 'above' | 'below', - /** Optional class for the input */ - className?: string, - autoFocus?: boolean, - hidePortal?: boolean, - theme?: InputThemes, - showButtons?: boolean, - /** Search only among people who follow you (TruthSocial). */ - followers?: boolean, } /** Input to search for accounts. */ -const AccountSearch: React.FC = ({ onSelected, className, showButtons = true, ...rest }) => { +const AccountSearch: React.FC = ({ onSelected, ...rest }) => { const intl = useIntl(); const [value, setValue] = useState(''); @@ -71,7 +60,7 @@ const AccountSearch: React.FC = ({ onSelected, className, showBu
= ({ onSelected, className, showBu {...rest} /> - {showButtons && ( -
- +
+ - -
- )} + +
); diff --git a/app/soapbox/components/autosuggest-account-input.tsx b/app/soapbox/components/autosuggest-account-input.tsx index 352be593b..b2a205e3c 100644 --- a/app/soapbox/components/autosuggest-account-input.tsx +++ b/app/soapbox/components/autosuggest-account-input.tsx @@ -22,8 +22,6 @@ interface IAutosuggestAccountInput { menu?: Menu, onKeyDown?: React.KeyboardEventHandler, theme?: InputThemes, - /** Search only among people who follow you (TruthSocial). */ - followers?: boolean, } const AutosuggestAccountInput: React.FC = ({ @@ -31,7 +29,6 @@ const AutosuggestAccountInput: React.FC = ({ onSelected, value = '', limit = 4, - followers = false, ...rest }) => { const dispatch = useAppDispatch(); @@ -48,7 +45,7 @@ const AutosuggestAccountInput: React.FC = ({ }; const handleAccountSearch = useCallback(throttle(q => { - const params = { q, limit, followers, resolve: false }; + const params = { q, limit, resolve: false }; dispatch(accountSearch(params, controller.current.signal)) .then((accounts: { id: string }[]) => { diff --git a/app/soapbox/components/autosuggest-input.tsx b/app/soapbox/components/autosuggest-input.tsx index 87f1c41f5..35460131a 100644 --- a/app/soapbox/components/autosuggest-input.tsx +++ b/app/soapbox/components/autosuggest-input.tsx @@ -31,7 +31,6 @@ export interface IAutosuggestInput extends Pick, hidePortal?: boolean, theme?: InputThemes, @@ -43,7 +42,6 @@ export default class AutosuggestInput extends ImmutablePureComponent { @@ -260,19 +258,15 @@ export default class AutosuggestInput extends ImmutablePureComponent return (
+ {/* Spacer */} +
+ {features.chatsMedia && ( diff --git a/app/soapbox/features/chats/components/chat-list-item.tsx b/app/soapbox/features/chats/components/chat-list-item.tsx index 72584e2df..6099955e7 100644 --- a/app/soapbox/features/chats/components/chat-list-item.tsx +++ b/app/soapbox/features/chats/components/chat-list-item.tsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import RelativeTimestamp from 'soapbox/components/relative-timestamp'; -import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui'; +import { Avatar, HStack, IconButton, Stack, Text } from 'soapbox/components/ui'; import VerificationBadge from 'soapbox/components/verification-badge'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import { useChatContext } from 'soapbox/contexts/chat-context'; @@ -115,12 +115,14 @@ const ChatListItem: React.FC = ({ chat, onClick }) => { {features.chatsDelete && (
- {/* TODO: fix nested buttons here */} - + + +
)} diff --git a/app/soapbox/features/chats/components/chat-list.tsx b/app/soapbox/features/chats/components/chat-list.tsx index 9de6d5c2d..65629ce14 100644 --- a/app/soapbox/features/chats/components/chat-list.tsx +++ b/app/soapbox/features/chats/components/chat-list.tsx @@ -63,8 +63,7 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false, s
- ) - } + )} components={{ ScrollSeekPlaceholder: () => , Footer: () => hasNextPage ? : null, diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index 1f6fd6adb..983a0d6f2 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -8,7 +8,7 @@ import { Components, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { openModal } from 'soapbox/actions/modals'; import { initReport } from 'soapbox/actions/reports'; -import { Avatar, Button, Divider, HStack, Icon, Spinner, Stack, Text } from 'soapbox/components/ui'; +import { Avatar, Button, Divider, HStack, Icon, IconButton, Spinner, Stack, Text } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container'; import emojify from 'soapbox/features/emoji/emoji'; import PlaceholderChatMessage from 'soapbox/features/placeholder/components/placeholder-chat-message'; @@ -286,11 +286,14 @@ const ChatMessageList: React.FC = ({ chat }) => { })} data-testid='chat-message-menu' > - + + +
)} @@ -447,7 +450,7 @@ const ChatMessageList: React.FC = ({ chat }) => { return (
-
+
{ label={intl.formatMessage(messages.autoDeleteLabel)} hint={intl.formatMessage(messages.autoDeleteHint)} /> - handleUpdateChat(MessageExpirationValues.TWO_MINUTES)} - isSelected={chat.message_expiration === MessageExpirationValues.TWO_MINUTES} - /> handleUpdateChat(MessageExpirationValues.SEVEN)} 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/empty-results-blankslate.tsx b/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx index 15daef888..3e360347e 100644 --- a/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx +++ b/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx @@ -13,7 +13,7 @@ const EmptyResultsBlankslate = () => { return ( - + {intl.formatMessage(messages.title)} 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/chats.ts b/app/soapbox/queries/chats.ts index 68fa7fc21..9a119f5f1 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -15,14 +15,13 @@ import { useFetchRelationships } from './relationships'; import type { IAccount } from './accounts'; -export const messageExpirationOptions = [120, 604800, 1209600, 2592000, 7776000]; +export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000]; export enum MessageExpirationValues { - 'TWO_MINUTES' = messageExpirationOptions[0], - 'SEVEN' = messageExpirationOptions[1], - 'FOURTEEN' = messageExpirationOptions[2], - 'THIRTY' = messageExpirationOptions[3], - 'NINETY' = messageExpirationOptions[4] + 'SEVEN' = messageExpirationOptions[0], + 'FOURTEEN' = messageExpirationOptions[1], + 'THIRTY' = messageExpirationOptions[2], + 'NINETY' = messageExpirationOptions[3] } export interface IChat { diff --git a/app/soapbox/queries/search.ts b/app/soapbox/queries/search.ts index 2b3fd1e70..41c4b7fb3 100644 --- a/app/soapbox/queries/search.ts +++ b/app/soapbox/queries/search.ts @@ -1,27 +1,51 @@ -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 { flattenPages, 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 = flattenPages(queryInfo.data); + + return { + ...queryInfo, + data, + }; }