diff --git a/packages/pl-fe/src/columns/search.tsx b/packages/pl-fe/src/columns/search.tsx new file mode 100644 index 000000000..5266bf125 --- /dev/null +++ b/packages/pl-fe/src/columns/search.tsx @@ -0,0 +1,166 @@ +import clsx from 'clsx'; +import React, { useRef } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Hashtag from 'pl-fe/components/hashtag'; +import ScrollableList from 'pl-fe/components/scrollable-list'; +import AccountContainer from 'pl-fe/containers/account-container'; +import StatusContainer from 'pl-fe/containers/status-container'; +import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account'; +import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag'; +import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status'; +import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/queries/search/use-search'; + +import TrendsColumn from './trends'; + +import type { VirtuosoHandle } from 'react-virtuoso'; + +interface ISearchColumn { + type: 'accounts' | 'hashtags' | 'statuses' | 'links'; + query: string; + accountId?: string; + multiColumn?: boolean; +} + +const SearchColumn: React.FC = ({ type, query, accountId, multiColumn }) => { + query = query.trim(); + + const node = useRef(null); + + const searchAccountsQuery = useSearchAccounts(type === 'accounts' && query || ''); + const searchStatusesQuery = useSearchStatuses(type === 'statuses' && query || '', { + account_id: accountId, + }); + const searchHashtagsQuery = useSearchHashtags(type === 'hashtags' && query || ''); + + const activeQuery = ({ + accounts: searchAccountsQuery, + statuses: searchStatusesQuery, + hashtags: searchHashtagsQuery, + links: searchStatusesQuery, + })[type]!; + + const getCurrentIndex = (id: string): number => resultsIds?.findIndex(key => key === id); + + const handleMoveUp = (id: string) => { + if (!resultsIds) return; + + const elementIndex = getCurrentIndex(id) - 1; + selectChild(elementIndex); + }; + + const handleMoveDown = (id: string) => { + if (!resultsIds) return; + + const elementIndex = getCurrentIndex(id) + 1; + selectChild(elementIndex); + }; + + const selectChild = (index: number) => { + const selector = `#search-results [data-index="${index}"] .focusable`; + const element = document.querySelector(selector); + + if (element) element.focus(); + + node.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + if (!element) document.querySelector(selector)?.focus(); + }, + }); + }; + + const handleLoadMore = () => activeQuery.fetchNextPage({ cancelRefetch: false }); + + let searchResults; + const hasMore = activeQuery.hasNextPage; + const isLoading = activeQuery.isFetching; + let placeholderComponent = PlaceholderStatus; + let resultsIds: Array; + + switch (type) { + case 'accounts': { + placeholderComponent = PlaceholderAccount; + if (!query) return ; + if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) { + resultsIds = searchAccountsQuery.data; + searchResults = searchAccountsQuery.data.map(accountId => ); + } else if (!isLoading) { + return ( +
+ +
+ ); + } + break; + } + case 'statuses': + case 'links': { + if (!query) return ; + if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) { + resultsIds = searchStatusesQuery.data; + searchResults = searchStatusesQuery.data.map(statusId => ); + } else if (!isLoading) { + return ( +
+ +
+ ); + } + break; + } + case 'hashtags': { + placeholderComponent = PlaceholderHashtag; + if (!query) return ; + if (searchHashtagsQuery.data && searchHashtagsQuery.data.length > 0) { + resultsIds = searchHashtagsQuery.data.map(hashtag => hashtag.name); + searchResults = searchHashtagsQuery.data.map(hashtag => ); + } else if (!isLoading) { + return ( +
+ +
+ ); + } + break; + } + } + + return ( + + {searchResults || []} + + ); +}; + +export { SearchColumn as default }; diff --git a/packages/pl-fe/src/columns/trends.tsx b/packages/pl-fe/src/columns/trends.tsx new file mode 100644 index 000000000..dc442a0b3 --- /dev/null +++ b/packages/pl-fe/src/columns/trends.tsx @@ -0,0 +1,86 @@ +import clsx from 'clsx'; +import React from 'react'; + +import Hashtag from 'pl-fe/components/hashtag'; +import ScrollableList from 'pl-fe/components/scrollable-list'; +import TrendingLink from 'pl-fe/components/trending-link'; +import AccountContainer from 'pl-fe/containers/account-container'; +import StatusContainer from 'pl-fe/containers/status-container'; +import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account'; +import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag'; +import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status'; +import useTrends from 'pl-fe/queries/trends'; +import { useSuggestedAccounts } from 'pl-fe/queries/trends/use-suggested-accounts'; +import { useTrendingLinks } from 'pl-fe/queries/trends/use-trending-links'; +import { useTrendingStatuses } from 'pl-fe/queries/trends/use-trending-statuses'; + +interface ITrendsColumn { + type: 'accounts' | 'hashtags' | 'statuses' | 'links'; + emptyMessage?: JSX.Element; + multiColumn?: boolean; +} + +const TrendsColumn: React.FC = ({ type, multiColumn }) => { + const { data: accounts, isFetching: isFetchingAccounts, isLoading: isLoadingAccounts } = useSuggestedAccounts(); + const { data: trendingTags, isFetching: isFetchingTags, isLoading: isLoadingTags } = useTrends(); + const { data: trendingStatuses, isFetching: isFetchingStatuses, isLoading: isLoadingStatuses } = useTrendingStatuses(); + const { data: trendingLinks, isFetching: isFetchingLinks, isLoading: isLoadingLinks } = useTrendingLinks(); + + let placeholderComponent = PlaceholderStatus; + + let children; + let isFetching; + let isLoading; + + switch (type) { + case 'accounts': { + children = accounts?.map(account => ); + isFetching = isFetchingAccounts; + isLoading = isLoadingAccounts; + placeholderComponent = PlaceholderAccount; + break; + } + case 'hashtags': { + children = trendingTags?.map(tag => ); + isFetching = isFetchingTags; + isLoading = isLoadingTags; + placeholderComponent = PlaceholderHashtag; + break; + } + case 'statuses': { + children = trendingStatuses?.map(statusId => ); + isFetching = isFetchingStatuses; + isLoading = isLoadingStatuses; + break; + } + case 'links': { + children = trendingLinks?.map(link => ); + isFetching = isFetchingLinks; + isLoading = isLoadingLinks; + break; + } + } + + return ( + + {children || []} + + ); +}; + +export { TrendsColumn as default }; diff --git a/packages/pl-fe/src/pages/search/search.tsx b/packages/pl-fe/src/pages/search/search.tsx index 1da557711..8a7703e7c 100644 --- a/packages/pl-fe/src/pages/search/search.tsx +++ b/packages/pl-fe/src/pages/search/search.tsx @@ -1,32 +1,18 @@ -import clsx from 'clsx'; -import React, { useRef, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { useSearchParams } from 'react-router-dom-v5-compat'; import { useAccount } from 'pl-fe/api/hooks/accounts/use-account'; -import Hashtag from 'pl-fe/components/hashtag'; +import SearchColumn from 'pl-fe/columns/search'; import IconButton from 'pl-fe/components/icon-button'; -import ScrollableList from 'pl-fe/components/scrollable-list'; -import TrendingLink from 'pl-fe/components/trending-link'; import Column from 'pl-fe/components/ui/column'; import HStack from 'pl-fe/components/ui/hstack'; import Input from 'pl-fe/components/ui/input'; import SvgIcon from 'pl-fe/components/ui/svg-icon'; import Tabs from 'pl-fe/components/ui/tabs'; import Text from 'pl-fe/components/ui/text'; -import AccountContainer from 'pl-fe/containers/account-container'; -import StatusContainer from 'pl-fe/containers/status-container'; -import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account'; -import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag'; -import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status'; import { useFeatures } from 'pl-fe/hooks/use-features'; -import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/queries/search/use-search'; -import useTrends from 'pl-fe/queries/trends'; -import { useSuggestedAccounts } from 'pl-fe/queries/trends/use-suggested-accounts'; -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'; @@ -126,12 +112,9 @@ const SearchInput: React.FC = ({ placeholder }) => { }; const SearchResults = () => { - const node = useRef(null); - const intl = useIntl(); const features = useFeatures(); - - const [tabKey, setTabKey] = useState(1); + const queryClient = useQueryClient(); const [params, setParams] = useSearchParams(); @@ -140,30 +123,15 @@ const SearchResults = () => { const selectedFilter = (params.get('type') || 'accounts') as SearchFilter; const accountId = params.get('accountId') || undefined; - const searchAccountsQuery = useSearchAccounts(selectedFilter === 'accounts' && value || ''); - const searchStatusesQuery = useSearchStatuses(selectedFilter === 'statuses' && value || '', { - account_id: accountId, - }); - const searchHashtagsQuery = useSearchHashtags(selectedFilter === 'hashtags' && value || ''); - - const activeQuery = ({ - accounts: searchAccountsQuery, - statuses: searchStatusesQuery, - hashtags: searchHashtagsQuery, - links: searchStatusesQuery, - })[selectedFilter]!; - - const handleLoadMore = () => activeQuery.fetchNextPage({ cancelRefetch: false }); - const selectFilter = (newActiveFilter: SearchFilter) => { - if (newActiveFilter === selectedFilter) activeQuery.refetch(); - else setParams(params => ({ ...Object.fromEntries(params.entries()), type: newActiveFilter })); + if (newActiveFilter === selectedFilter) { + queryClient.refetchQueries({ + queryKey: ['search', newActiveFilter, value, newActiveFilter === 'statuses' ? { account_id: accountId } : undefined], + exact: true, + }); + } else setParams(params => ({ ...Object.fromEntries(params.entries()), type: newActiveFilter })); }; - const { data: suggestions } = useSuggestedAccounts(); - const { data: trendingTags } = useTrends(); - const { data: trendingStatuses } = useTrendingStatuses(); - const { data: trendingLinks } = useTrendingLinks(); const { account } = useAccount(accountId); const handleUnsetAccount = () => { @@ -197,132 +165,9 @@ const SearchResults = () => { name: 'links', }); - return ; + return ; }; - const getCurrentIndex = (id: string): number => resultsIds?.findIndex(key => key === id); - - const handleMoveUp = (id: string) => { - if (!resultsIds) return; - - const elementIndex = getCurrentIndex(id) - 1; - selectChild(elementIndex); - }; - - const handleMoveDown = (id: string) => { - if (!resultsIds) return; - - const elementIndex = getCurrentIndex(id) + 1; - selectChild(elementIndex); - }; - - const selectChild = (index: number) => { - const selector = `#search-results [data-index="${index}"] .focusable`; - 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; - const hasMore = activeQuery.hasNextPage; - const isLoading = activeQuery.isFetching; - let noResultsMessage: JSX.Element | undefined; - let placeholderComponent = PlaceholderStatus as React.ComponentType; - let resultsIds: Array; - - if (selectedFilter === 'accounts') { - placeholderComponent = PlaceholderAccount; - - if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) { - searchResults = searchAccountsQuery.data.map(accountId => ); - } else if (suggestions && suggestions.length > 0) { - searchResults = suggestions.map(suggestion => ); - } else if (submitted && !isLoading) { - noResultsMessage = ( -
- -
- ); - } - } - - if (selectedFilter === 'statuses') { - if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) { - searchResults = searchStatusesQuery.data.map((statusId: string) => ( - // @ts-ignore - - )); - resultsIds = searchStatusesQuery.data; - } else if (!submitted && !accountId && trendingStatuses && trendingStatuses.length !== 0) { - searchResults = trendingStatuses.map((statusId: string) => ( - // @ts-ignore - - )); - resultsIds = trendingStatuses; - } else if (submitted && !isLoading) { - noResultsMessage = ( -
- -
- ); - } - } - - if (selectedFilter === 'hashtags') { - placeholderComponent = PlaceholderHashtag; - - if (searchHashtagsQuery.data && searchHashtagsQuery.data.length > 0) { - searchResults = searchHashtagsQuery.data.map(hashtag => ); - } else if (!submitted && suggestions && suggestions.length !== 0) { - searchResults = trendingTags?.map(hashtag => ); - } else if (submitted && !isLoading) { - noResultsMessage = ( -
- -
- ); - } - } - - if (selectedFilter === 'links') { - if (submitted) { - selectFilter('accounts'); - setTabKey(key => ++key); - } else if (trendingLinks) { - searchResults = trendingLinks.map(trendingLink => ); - } - } - return ( <> {accountId ? ( @@ -338,29 +183,7 @@ const SearchResults = () => { ) : renderFilterBar()} - {noResultsMessage || ( - - {searchResults || []} - - )} + ); };