pl-fe: migrate search to react-query
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@@ -9,7 +9,6 @@ import { biteAccount, blockAccount, pinAccount, removeFromFollowers, unblockAcco
|
||||
import { mentionCompose, directCompose } from 'pl-fe/actions/compose';
|
||||
import { blockDomain, unblockDomain } from 'pl-fe/actions/domain-blocks';
|
||||
import { initReport, ReportableEntities } from 'pl-fe/actions/reports';
|
||||
import { setSearchAccount } from 'pl-fe/actions/search';
|
||||
import { useFollow } from 'pl-fe/api/hooks/accounts/use-follow';
|
||||
import Badge from 'pl-fe/components/badge';
|
||||
import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu';
|
||||
@@ -240,11 +239,6 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onSearch = () => {
|
||||
dispatch(setSearchAccount(account.id));
|
||||
history.push('/search');
|
||||
};
|
||||
|
||||
const onAvatarClick = () => {
|
||||
const avatar = v.parse(mediaAttachmentSchema, {
|
||||
id: '',
|
||||
@@ -334,7 +328,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||
if (features.searchFromAccount) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(account.id === ownAccount.id ? messages.searchSelf : messages.search, { name: account.username }),
|
||||
action: onSearch,
|
||||
to: '/search?' + new URLSearchParams({ type: 'statuses', accountId: account.id }).toString(),
|
||||
icon: require('@tabler/icons/outline/search.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import Stack from 'pl-fe/components/ui/stack';
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { ComposeEditor } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useCompose } from 'pl-fe/hooks/use-compose';
|
||||
import { useDraggedFiles } from 'pl-fe/hooks/use-dragged-files';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
@@ -79,7 +78,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
const { configuration } = useInstance();
|
||||
|
||||
const compose = useCompose(id);
|
||||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
||||
const maxTootChars = configuration.statuses.max_characters;
|
||||
const features = useFeatures();
|
||||
|
||||
@@ -111,7 +109,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
|
||||
const isEmpty = !(fulltext.trim() || anyMedia);
|
||||
const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty && !isUploading;
|
||||
const shouldAutoFocus = autoFocus && !showSearch;
|
||||
const shouldAutoFocus = autoFocus;
|
||||
const canSubmit = !!editorRef.current && !isSubmitting && !isUploading && !isChangingUpload && !isEmpty && length(fulltext) <= maxTootChars;
|
||||
|
||||
const getClickableArea = () => clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { expandSearch, setFilter, setSearchAccount } from 'pl-fe/actions/search';
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/api/hooks/search/use-search';
|
||||
import { useTrendingLinks } from 'pl-fe/api/hooks/trends/use-trending-links';
|
||||
import { useTrendingStatuses } from 'pl-fe/api/hooks/trends/use-trending-statuses';
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
@@ -18,12 +19,11 @@ 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 { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import useTrends from 'pl-fe/queries/trends';
|
||||
|
||||
import type { SearchFilter } from 'pl-fe/reducers/search';
|
||||
type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links';
|
||||
|
||||
const messages = defineMessages({
|
||||
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
||||
@@ -34,27 +34,47 @@ const messages = defineMessages({
|
||||
|
||||
const SearchResults = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const [tabKey, setTabKey] = useState(1);
|
||||
|
||||
const value = useAppSelector((state) => state.search.submittedValue);
|
||||
const results = useAppSelector((state) => state.search.results);
|
||||
const [params, setParams] = useSearchParams();
|
||||
|
||||
const value = params.get('q') || '';
|
||||
const submitted = !!value.trim();
|
||||
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 }));
|
||||
};
|
||||
|
||||
const suggestions = useAppSelector((state) => state.suggestions.items);
|
||||
const submitted = useAppSelector((state) => state.search.submitted);
|
||||
const selectedFilter = useAppSelector((state) => state.search.filter);
|
||||
const filterByAccount = useAppSelector((state) => state.search.accountId || undefined);
|
||||
const { data: trendingTags } = useTrends();
|
||||
const { data: trendingStatuses } = useTrendingStatuses();
|
||||
const { trendingLinks } = useTrendingLinks();
|
||||
const { account } = useAccount(filterByAccount);
|
||||
const { account } = useAccount(accountId);
|
||||
|
||||
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
||||
|
||||
const handleUnsetAccount = () => dispatch(setSearchAccount(null));
|
||||
|
||||
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(value, newActiveFilter));
|
||||
const handleUnsetAccount = () => {
|
||||
params.delete('accountId');
|
||||
setParams(params => Object.fromEntries(params.entries()));
|
||||
};
|
||||
|
||||
const renderFilterBar = () => {
|
||||
const items = [];
|
||||
@@ -108,23 +128,21 @@ const SearchResults = () => {
|
||||
if (element) element.focus();
|
||||
};
|
||||
|
||||
let searchResults;
|
||||
let hasMore = false;
|
||||
let loaded;
|
||||
let noResultsMessage;
|
||||
let searchResults: Array<JSX.Element> | undefined;
|
||||
const hasMore = activeQuery.hasNextPage;
|
||||
const isLoading = activeQuery.isFetching;
|
||||
let noResultsMessage: JSX.Element | undefined;
|
||||
let placeholderComponent = PlaceholderStatus as React.ComponentType;
|
||||
let resultsIds: Array<string>;
|
||||
|
||||
if (selectedFilter === 'accounts') {
|
||||
hasMore = results.accountsHasMore;
|
||||
loaded = results.accountsLoaded;
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
|
||||
if (results.accounts && results.accounts.length > 0) {
|
||||
searchResults = results.accounts.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||
} else if (!submitted && suggestions && suggestions.length !== 0) {
|
||||
if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) {
|
||||
searchResults = searchAccountsQuery.data.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||
} else if (suggestions && suggestions.length > 0) {
|
||||
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account_id} id={suggestion.account_id} />);
|
||||
} else if (loaded) {
|
||||
} else if (submitted && !isLoading) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
@@ -138,11 +156,8 @@ const SearchResults = () => {
|
||||
}
|
||||
|
||||
if (selectedFilter === 'statuses') {
|
||||
hasMore = results.statusesHasMore;
|
||||
loaded = results.statusesLoaded;
|
||||
|
||||
if (results.statuses && results.statuses.length > 0) {
|
||||
searchResults = results.statuses.map((statusId: string) => (
|
||||
if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) {
|
||||
searchResults = searchStatusesQuery.data.map((statusId: string) => (
|
||||
// @ts-ignore
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
@@ -151,8 +166,8 @@ const SearchResults = () => {
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
));
|
||||
resultsIds = results.statuses;
|
||||
} else if (!submitted && !filterByAccount && trendingStatuses && trendingStatuses.length !== 0) {
|
||||
resultsIds = searchStatusesQuery.data;
|
||||
} else if (!submitted && !accountId && trendingStatuses && trendingStatuses.length !== 0) {
|
||||
searchResults = trendingStatuses.map((statusId: string) => (
|
||||
// @ts-ignore
|
||||
<StatusContainer
|
||||
@@ -163,7 +178,7 @@ const SearchResults = () => {
|
||||
/>
|
||||
));
|
||||
resultsIds = trendingStatuses;
|
||||
} else if (loaded) {
|
||||
} else if (submitted && !isLoading) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
@@ -177,15 +192,13 @@ const SearchResults = () => {
|
||||
}
|
||||
|
||||
if (selectedFilter === 'hashtags') {
|
||||
hasMore = results.hashtagsHasMore;
|
||||
loaded = results.hashtagsLoaded;
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
|
||||
if (results.hashtags && results.hashtags.length > 0) {
|
||||
searchResults = results.hashtags.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||
if (searchHashtagsQuery.data && searchHashtagsQuery.data.length > 0) {
|
||||
searchResults = searchHashtagsQuery.data.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||
} else if (!submitted && suggestions && suggestions.length !== 0) {
|
||||
searchResults = trendingTags?.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||
} else if (loaded) {
|
||||
} else if (submitted && !isLoading) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
@@ -199,19 +212,17 @@ const SearchResults = () => {
|
||||
}
|
||||
|
||||
if (selectedFilter === 'links') {
|
||||
loaded = true;
|
||||
|
||||
if (submitted) {
|
||||
selectFilter('accounts');
|
||||
setTabKey(key => ++key);
|
||||
} else if (!submitted && trendingLinks) {
|
||||
} else if (trendingLinks) {
|
||||
searchResults = trendingLinks.map(trendingLink => <TrendingLink trendingLink={trendingLink} />);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{filterByAccount ? (
|
||||
{accountId ? (
|
||||
<HStack className='border-b border-solid border-gray-200 p-2 pb-4 dark:border-gray-800' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/outline/x.svg')} onClick={handleUnsetAccount} />
|
||||
<Text truncate>
|
||||
@@ -228,8 +239,8 @@ const SearchResults = () => {
|
||||
<ScrollableList
|
||||
id='search-results'
|
||||
key={selectedFilter}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && (!searchResults || searchResults?.length === 0)}
|
||||
isLoading={submitted && isLoading}
|
||||
showLoading={submitted && isLoading && (searchResults?.length === 0 || activeQuery.isRefetching)}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
|
||||
@@ -1,91 +1,43 @@
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import {
|
||||
clearSearch,
|
||||
clearSearchResults,
|
||||
setSearchAccount,
|
||||
showSearch,
|
||||
submitSearch,
|
||||
} from 'pl-fe/actions/search';
|
||||
import AutosuggestAccountInput from 'pl-fe/components/autosuggest-account-input';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { selectAccount } from 'pl-fe/selectors';
|
||||
import { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
action: { id: 'search.action', defaultMessage: 'Search for “{query}”' },
|
||||
});
|
||||
|
||||
const redirectToAccount = (accountId: string, routerHistory: any) =>
|
||||
(_dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const acct = selectAccount(getState(), accountId)!.acct;
|
||||
const Search = () => {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const [value, setValue] = useState(params.get('q') || '');
|
||||
|
||||
if (acct && routerHistory) {
|
||||
routerHistory.push(`/@${acct}`);
|
||||
}
|
||||
};
|
||||
|
||||
interface ISearch {
|
||||
autoFocus?: boolean;
|
||||
autoSubmit?: boolean;
|
||||
autosuggest?: boolean;
|
||||
openInRoute?: boolean;
|
||||
}
|
||||
|
||||
const Search = (props: ISearch) => {
|
||||
const submittedValue = useAppSelector((state) => state.search.submittedValue);
|
||||
const [value, setValue] = useState(submittedValue);
|
||||
const {
|
||||
autoFocus = false,
|
||||
autoSubmit = false,
|
||||
autosuggest = false,
|
||||
openInRoute = false,
|
||||
} = props;
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const submitted = useAppSelector((state) => state.search.submitted);
|
||||
const setQuery = (value: string) => {
|
||||
setParams(params => ({ ...Object.fromEntries(params.entries()), q: value }));
|
||||
};
|
||||
|
||||
const debouncedSubmit = useCallback(debounce((value: string) => {
|
||||
dispatch(submitSearch(value));
|
||||
setQuery(value);
|
||||
}, 900), []);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
|
||||
setValue(value);
|
||||
|
||||
if (autoSubmit) {
|
||||
debouncedSubmit(value);
|
||||
}
|
||||
debouncedSubmit(value);
|
||||
};
|
||||
|
||||
const handleClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (value.length > 0 || submitted) {
|
||||
dispatch(clearSearchResults());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (openInRoute) {
|
||||
dispatch(setSearchAccount(null));
|
||||
dispatch(submitSearch(value));
|
||||
|
||||
history.push('/search');
|
||||
} else {
|
||||
dispatch(submitSearch(value));
|
||||
if (value.length > 0) {
|
||||
setValue('');
|
||||
setQuery('');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,67 +45,32 @@ const Search = (props: ISearch) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
handleSubmit();
|
||||
setQuery(value);
|
||||
} else if (event.key === 'Escape') {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
dispatch(showSearch());
|
||||
};
|
||||
|
||||
const handleSelected = (accountId: string) => {
|
||||
dispatch(clearSearch());
|
||||
dispatch(redirectToAccount(accountId, history));
|
||||
};
|
||||
|
||||
const makeMenu = () => [
|
||||
{
|
||||
text: intl.formatMessage(messages.action, { query: value }),
|
||||
icon: require('@tabler/icons/outline/search.svg'),
|
||||
action: handleSubmit,
|
||||
},
|
||||
];
|
||||
|
||||
const hasValue = value.length > 0 || submitted;
|
||||
const componentProps: any = {
|
||||
type: 'text',
|
||||
id: 'search',
|
||||
placeholder: intl.formatMessage(messages.placeholder),
|
||||
value,
|
||||
onChange: handleChange,
|
||||
onKeyDown: handleKeyDown,
|
||||
onFocus: handleFocus,
|
||||
autoFocus: autoFocus,
|
||||
theme: 'search',
|
||||
className: 'pr-10 rtl:pl-10 rtl:pr-3',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (value !== submittedValue) setValue(submittedValue);
|
||||
}, [submittedValue]);
|
||||
|
||||
if (autosuggest) {
|
||||
componentProps.onSelected = handleSelected;
|
||||
componentProps.menu = makeMenu();
|
||||
componentProps.autoSelect = false;
|
||||
}
|
||||
const hasValue = value.length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('w-full', {
|
||||
'sticky top-[76px] z-10 bg-white/90 backdrop-blur black:bg-black/80 dark:bg-primary-900/90': !openInRoute,
|
||||
})}
|
||||
className='sticky top-[76px] z-10 w-full bg-white/90 backdrop-blur black:bg-black/80 dark:bg-primary-900/90'
|
||||
>
|
||||
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||
|
||||
<div className='relative'>
|
||||
{autosuggest ? (
|
||||
<AutosuggestAccountInput {...componentProps} />
|
||||
) : (
|
||||
<Input {...componentProps} />
|
||||
)}
|
||||
<Input
|
||||
type='text'
|
||||
id='search'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
theme='search'
|
||||
className='pr-10 rtl:pl-10 rtl:pr-3'
|
||||
/>
|
||||
|
||||
<div
|
||||
role='button'
|
||||
|
||||
@@ -15,7 +15,7 @@ const SearchPage = () => {
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<div className='space-y-4'>
|
||||
<Search autoFocus autoSubmit />
|
||||
<Search />
|
||||
<SearchResults />
|
||||
</div>
|
||||
</Column>
|
||||
|
||||
@@ -2,12 +2,10 @@ import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { setFilter } from 'pl-fe/actions/search';
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Widget from 'pl-fe/components/ui/widget';
|
||||
import PlaceholderSidebarTrends from 'pl-fe/features/placeholder/components/placeholder-sidebar-trends';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import useTrends from 'pl-fe/queries/trends';
|
||||
|
||||
interface ITrendsPanel {
|
||||
@@ -22,15 +20,10 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { data: trends, isFetching } = useTrends();
|
||||
|
||||
const setHashtagsFilter = () => {
|
||||
dispatch(setFilter('', 'hashtags'));
|
||||
};
|
||||
|
||||
if (!isFetching && !trends?.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -39,7 +32,7 @@ const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
||||
<Widget
|
||||
title={<FormattedMessage id='trends.title' defaultMessage='Trends' />}
|
||||
action={
|
||||
<Link className='text-right' to='/search' onClick={setHashtagsFilter}>
|
||||
<Link className='text-right' to='/search?type=hashtags'>
|
||||
<Text tag='span' theme='primary' size='sm' className='hover:underline'>
|
||||
{intl.formatMessage(messages.viewAll)}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user