diff --git a/packages/pl-fe/src/actions/search.ts b/packages/pl-fe/src/actions/search.ts deleted file mode 100644 index 4efe4cce6..000000000 --- a/packages/pl-fe/src/actions/search.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { useSettingsStore } from 'pl-fe/stores/settings'; - -import { getClient } from '../api'; - -import { fetchRelationships } from './accounts'; -import { importEntities } from './importer'; - -import type { Search } from 'pl-api'; -import type { SearchFilter } from 'pl-fe/reducers/search'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const SEARCH_CLEAR = 'SEARCH_CLEAR' as const; -const SEARCH_SHOW = 'SEARCH_SHOW' as const; -const SEARCH_RESULTS_CLEAR = 'SEARCH_RESULTS_CLEAR' as const; - -const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST' as const; -const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS' as const; -const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL' as const; - -const SEARCH_FILTER_SET = 'SEARCH_FILTER_SET' as const; - -const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST' as const; -const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS' as const; -const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL' as const; - -const SEARCH_ACCOUNT_SET = 'SEARCH_ACCOUNT_SET' as const; - -const clearSearch = () => ({ - type: SEARCH_CLEAR, -}); - -const clearSearchResults = () => ({ - type: SEARCH_RESULTS_CLEAR, -}); - -const submitSearch = (value: string, filter?: SearchFilter) => - (dispatch: AppDispatch, getState: () => RootState) => { - const type = filter || getState().search.filter || 'accounts'; - const accountId = getState().search.accountId; - - // An empty search doesn't return any results - if (value.length === 0) { - return dispatch(clearSearchResults()); - } - - dispatch(fetchSearchRequest(value)); - - const params: Record = { - resolve: true, - limit: 20, - type: type as any, - }; - - if (accountId) params.account_id = accountId; - - return getClient(getState()).search.search(value, params).then(response => { - dispatch(importEntities({ accounts: response.accounts, statuses: response.statuses })); - - dispatch(fetchSearchSuccess(response, value, type)); - dispatch(fetchRelationships(response.accounts.map((item) => item.id))); - }).catch(error => { - dispatch(fetchSearchFail(error)); - }); - }; - -const fetchSearchRequest = (value: string) => ({ - type: SEARCH_FETCH_REQUEST, - value, -}); - -const fetchSearchSuccess = (results: Search, searchTerm: string, searchType: SearchFilter) => ({ - type: SEARCH_FETCH_SUCCESS, - results, - searchTerm, - searchType, -}); - -const fetchSearchFail = (error: unknown) => ({ - type: SEARCH_FETCH_FAIL, - error, -}); - -const setFilter = (value: string, filterType: SearchFilter) => - (dispatch: AppDispatch) => { - dispatch(submitSearch(value, filterType)); - - useSettingsStore.getState().changeSetting(['search', 'filter'], filterType); - - return dispatch({ - type: SEARCH_FILTER_SET, - value: filterType, - }); - }; - -const expandSearch = (type: SearchFilter) => (dispatch: AppDispatch, getState: () => RootState) => { - if (type === 'links') return; - const value = getState().search.submittedValue; - const offset = getState().search.results[type].length; - const accountId = getState().search.accountId; - - dispatch(expandSearchRequest(type)); - - const params: Record = { - type, - offset, - }; - if (accountId) params.account_id = accountId; - - return getClient(getState()).search.search(value, params).then(response => { - dispatch(importEntities({ accounts: response.accounts, statuses: response.statuses })); - - dispatch(expandSearchSuccess(response, value, type)); - dispatch(fetchRelationships(response.accounts.map((item) => item.id))); - }).catch(error => { - dispatch(expandSearchFail(error)); - }); -}; - -const expandSearchRequest = (searchType: SearchFilter) => ({ - type: SEARCH_EXPAND_REQUEST, - searchType, -}); - -const expandSearchSuccess = (results: Search, searchTerm: string, searchType: Exclude) => ({ - type: SEARCH_EXPAND_SUCCESS, - results, - searchTerm, - searchType, -}); - -const expandSearchFail = (error: unknown) => ({ - type: SEARCH_EXPAND_FAIL, - error, -}); - -const showSearch = () => ({ - type: SEARCH_SHOW, -}); - -const setSearchAccount = (accountId: string | null) => ({ - type: SEARCH_ACCOUNT_SET, - accountId, -}); - -type SearchAction = - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | { - type: typeof SEARCH_FILTER_SET; - path: (['search', 'filter']); - value: SearchFilter; - } - | ReturnType - | ReturnType - -export { - SEARCH_CLEAR, - SEARCH_SHOW, - SEARCH_RESULTS_CLEAR, - SEARCH_FETCH_REQUEST, - SEARCH_FETCH_SUCCESS, - SEARCH_FETCH_FAIL, - SEARCH_FILTER_SET, - SEARCH_EXPAND_REQUEST, - SEARCH_EXPAND_SUCCESS, - SEARCH_EXPAND_FAIL, - SEARCH_ACCOUNT_SET, - clearSearch, - clearSearchResults, - submitSearch, - fetchSearchRequest, - fetchSearchSuccess, - fetchSearchFail, - setFilter, - expandSearch, - expandSearchRequest, - expandSearchSuccess, - expandSearchFail, - showSearch, - setSearchAccount, - type SearchAction, -}; diff --git a/packages/pl-fe/src/api/hooks/search/use-search.ts b/packages/pl-fe/src/api/hooks/search/use-search.ts new file mode 100644 index 000000000..56aab4d1d --- /dev/null +++ b/packages/pl-fe/src/api/hooks/search/use-search.ts @@ -0,0 +1,108 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import { importEntities } from 'pl-fe/actions/importer'; +import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useClient } from 'pl-fe/hooks/use-client'; + +import type { SearchParams } from 'pl-api'; +import type { PaginationParams } from 'pl-api/dist/params/common'; + +const useSearchAccounts = ( + query: string, + params?: Omit, +) => { + const client = useClient(); + const dispatch = useAppDispatch(); + + const searchQuery = useInfiniteQuery({ + queryKey: ['search', 'accounts', query, params], + queryFn: ({ pageParam, signal }) => client.search.search(query!, { + with_relationships: true, + ...params, + offset: pageParam ? data?.length : 0, + type: 'accounts', + }, { signal }).then(({ accounts }) => { + dispatch(importEntities({ accounts })); + return accounts.map(({ id }) => id); + }), + enabled: !!query?.trim(), + initialPageParam: [''], + getNextPageParam: (page) => page.length ? page : undefined, + select: (data => data.pages.flat()), + }); + + const data: Array | undefined = searchQuery.data; + + return searchQuery; +}; + +const useSearchStatuses = ( + query: string, + params?: Omit, +) => { + const client = useClient(); + const dispatch = useAppDispatch(); + + return useInfiniteQuery({ + queryKey: ['search', 'statuses', query, params], + queryFn: ({ pageParam: offset, signal }) => client.search.search(query, { + with_relationships: true, + ...params, + offset, + type: 'statuses', + }, { signal }).then(({ statuses }) => { + dispatch(importEntities({ statuses })); + return statuses.map(({ id }) => id); + }), + enabled: !!query?.trim(), + initialPageParam: 0, + getNextPageParam: (_, allPages) => allPages.flat().length, + select: (data => data.pages.flat()), + }); +}; + +const useSearchHashtags = ( + query: string, + params?: Omit, +) => { + const client = useClient(); + + return useInfiniteQuery({ + queryKey: ['search', 'hashtags', query, params], + queryFn: ({ pageParam: offset, signal }) => client.search.search(query, { + ...params, + offset, + type: 'hashtags', + }, { signal }).then(({ hashtags }) => hashtags), + enabled: !!query?.trim(), + initialPageParam: 0, + getNextPageParam: (_, allPages) => allPages.flat().length, + select: (data => data.pages.flat()), + }); +}; + +const useSearchGroups = ( + query: string, + params?: Omit, +) => { + const client = useClient(); + const dispatch = useAppDispatch(); + + return useInfiniteQuery({ + queryKey: ['search', 'groups', query, params], + queryFn: ({ pageParam: offset, signal }) => client.search.search(query, { + ...params, + offset, + type: 'groups', + }, { signal }).then(({ groups }) => { + dispatch(importEntities({ groups })); + return groups.map(({ id }) => id); + }), + enabled: !!query?.trim(), + initialPageParam: 0, + getNextPageParam: (_, allPages) => allPages.flat().length, + select: (data => data.pages.flat()), + }); +}; + +export { useSearchAccounts, useSearchStatuses, useSearchHashtags, useSearchGroups }; diff --git a/packages/pl-fe/src/components/autosuggest-account-input.tsx b/packages/pl-fe/src/components/autosuggest-account-input.tsx index 55e421ea8..aa75dae6f 100644 --- a/packages/pl-fe/src/components/autosuggest-account-input.tsx +++ b/packages/pl-fe/src/components/autosuggest-account-input.tsx @@ -20,6 +20,7 @@ interface IAutosuggestAccountInput { menu?: Menu; onKeyDown?: React.KeyboardEventHandler; theme?: InputThemes; + placeholder?: string; } const AutosuggestAccountInput: React.FC = ({ diff --git a/packages/pl-fe/src/components/search-input.tsx b/packages/pl-fe/src/components/search-input.tsx new file mode 100644 index 000000000..be7dc2ade --- /dev/null +++ b/packages/pl-fe/src/components/search-input.tsx @@ -0,0 +1,112 @@ +import clsx from 'clsx'; +import React, { useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useHistory } from 'react-router-dom'; + +import AutosuggestAccountInput from 'pl-fe/components/autosuggest-account-input'; +import SvgIcon from 'pl-fe/components/ui/svg-icon'; +import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +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; + + if (acct && routerHistory) { + routerHistory.push(`/@${acct}`); + } + }; + +const SearchInput = () => { + const [value, setValue] = useState(''); + + const dispatch = useAppDispatch(); + const history = useHistory(); + const intl = useIntl(); + + const handleChange = (event: React.ChangeEvent) => { + const { value } = event.target; + + setValue(value); + }; + + const handleClear = (event: React.MouseEvent) => { + setValue(''); + }; + + const handleSubmit = () => { + setValue(''); + history.push('/search?' + new URLSearchParams({ q: value })); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + + handleSubmit(); + } else if (event.key === 'Escape') { + document.querySelector('.ui')?.parentElement?.focus(); + } + }; + + const handleSelected = (accountId: string) => { + setValue(''); + 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; + + return ( +
+ + +
+ + +
+ + + +
+
+
+ ); +}; + +export { SearchInput as default }; diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index c440866ed..8b3c97739 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -5,7 +5,6 @@ import { useInteractionRequestsCount } from 'pl-fe/api/hooks/statuses/use-intera import Icon from 'pl-fe/components/ui/icon'; import Stack from 'pl-fe/components/ui/stack'; import { useStatContext } from 'pl-fe/contexts/stat-context'; -import Search from 'pl-fe/features/search/components/search'; import ComposeButton from 'pl-fe/features/ui/components/compose-button'; import ProfileDropdown from 'pl-fe/features/ui/components/profile-dropdown'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; @@ -18,6 +17,7 @@ import { useSettings } from 'pl-fe/hooks/use-settings'; import Account from './account'; import DropdownMenu, { Menu } from './dropdown-menu'; +import SearchInput from './search-input'; import SidebarNavigationLink from './sidebar-navigation-link'; import SiteLogo from './site-logo'; @@ -176,7 +176,7 @@ const SidebarNavigation = () => {
- +
)} diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index 55cfce312..ca5ea1c8c 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -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 = ({ 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 = ({ 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'), }); } diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index 4e20fdee6..b7f0ac521 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -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, 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, 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; diff --git a/packages/pl-fe/src/features/search/components/search-results.tsx b/packages/pl-fe/src/features/search/components/search-results.tsx index ffc394ca0..53d0ad0ac 100644 --- a/packages/pl-fe/src/features/search/components/search-results.tsx +++ b/packages/pl-fe/src/features/search/components/search-results.tsx @@ -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 | 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') { - hasMore = results.accountsHasMore; - loaded = results.accountsLoaded; placeholderComponent = PlaceholderAccount; - if (results.accounts && results.accounts.length > 0) { - searchResults = results.accounts.map(accountId => ); - } else if (!submitted && suggestions && suggestions.length !== 0) { + if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) { + searchResults = searchAccountsQuery.data.map(accountId => ); + } else if (suggestions && suggestions.length > 0) { searchResults = suggestions.map(suggestion => ); - } else if (loaded) { + } else if (submitted && !isLoading) { noResultsMessage = (
{ } 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 { 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 { /> )); resultsIds = trendingStatuses; - } else if (loaded) { + } else if (submitted && !isLoading) { noResultsMessage = (
{ } if (selectedFilter === 'hashtags') { - hasMore = results.hashtagsHasMore; - loaded = results.hashtagsLoaded; placeholderComponent = PlaceholderHashtag; - if (results.hashtags && results.hashtags.length > 0) { - searchResults = results.hashtags.map(hashtag => ); + 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 (loaded) { + } else if (submitted && !isLoading) { noResultsMessage = (
{ } if (selectedFilter === 'links') { - loaded = true; - if (submitted) { selectFilter('accounts'); setTabKey(key => ++key); - } else if (!submitted && trendingLinks) { + } else if (trendingLinks) { searchResults = trendingLinks.map(trendingLink => ); } } return ( <> - {filterByAccount ? ( + {accountId ? ( @@ -228,8 +239,8 @@ const SearchResults = () => { - (_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) => { const { value } = event.target; setValue(value); - - if (autoSubmit) { - debouncedSubmit(value); - } + debouncedSubmit(value); }; const handleClear = (event: React.MouseEvent) => { 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 (
- {autosuggest ? ( - - ) : ( - - )} +
{ return (
- +
diff --git a/packages/pl-fe/src/features/ui/components/panels/trends-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/trends-panel.tsx index 090602135..f02508730 100644 --- a/packages/pl-fe/src/features/ui/components/panels/trends-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/trends-panel.tsx @@ -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) => { } action={ - + {intl.formatMessage(messages.viewAll)} diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 9f4dc3db3..935333036 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -32,7 +32,6 @@ import plfe from './pl-fe'; import polls from './polls'; import push_notifications from './push-notifications'; import scheduled_statuses from './scheduled-statuses'; -import search from './search'; import security from './security'; import status_lists from './status-lists'; import statuses from './statuses'; @@ -72,7 +71,6 @@ const reducers = { polls, push_notifications, scheduled_statuses, - search, security, status_lists, statuses, diff --git a/packages/pl-fe/src/reducers/search.ts b/packages/pl-fe/src/reducers/search.ts deleted file mode 100644 index 8408b3205..000000000 --- a/packages/pl-fe/src/reducers/search.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Record as ImmutableRecord } from 'immutable'; - -import { - COMPOSE_MENTION, - COMPOSE_REPLY, - COMPOSE_DIRECT, - COMPOSE_QUOTE, - type ComposeAction, -} from '../actions/compose'; -import { - SEARCH_CLEAR, - SEARCH_FETCH_REQUEST, - SEARCH_FETCH_SUCCESS, - SEARCH_SHOW, - SEARCH_FILTER_SET, - SEARCH_EXPAND_REQUEST, - SEARCH_EXPAND_SUCCESS, - SEARCH_ACCOUNT_SET, - SEARCH_RESULTS_CLEAR, - type SearchAction, -} from '../actions/search'; - -import type { Search, Tag } from 'pl-api'; - -const ResultsRecord = ImmutableRecord({ - accounts: Array(), - statuses: Array(), - groups: Array(), - hashtags: Array(), // it's a list of maps - accountsHasMore: false, - statusesHasMore: false, - groupsHasMore: false, - hashtagsHasMore: false, - accountsLoaded: false, - statusesLoaded: false, - groupsLoaded: false, - hashtagsLoaded: false, -}); - -const ReducerRecord = ImmutableRecord({ - submitted: false, - submittedValue: '', - hidden: false, - results: ResultsRecord(), - filter: 'accounts' as SearchFilter, - accountId: null as string | null, -}); - -type State = ReturnType; -type SearchFilter = 'accounts' | 'statuses' | 'groups' | 'hashtags' | 'links'; - -const toIds = (items: Array<{ id: string }> = []) => items.map(item => item.id); - -const importResults = (state: State, results: Search, searchTerm: string, searchType: SearchFilter) => - state.withMutations(state => { - if (state.submittedValue === searchTerm && state.filter === searchType) { - state.set('results', ResultsRecord({ - accounts: toIds(results.accounts), - statuses: toIds(results.statuses), - groups: toIds(results.groups), - hashtags: results.hashtags, // it's a list of records - accountsHasMore: results.accounts.length !== 0, - statusesHasMore: results.statuses.length !== 0, - groupsHasMore: results.groups?.length !== 0, - hashtagsHasMore: results.hashtags.length !== 0, - accountsLoaded: true, - statusesLoaded: true, - groupsLoaded: true, - hashtagsLoaded: true, - })); - - state.set('submitted', true); - } - }); - -const paginateResults = (state: State, searchType: Exclude, results: Search, searchTerm: string) => - state.withMutations(state => { - if (state.submittedValue === searchTerm) { - state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20); - state.setIn(['results', `${searchType}Loaded`], true); - state.updateIn(['results', searchType], items => { - const data = results[searchType]; - // Hashtags are a list of maps. Others are IDs. - if (searchType === 'hashtags') { - return (items as Array).concat(data as Search['hashtags']); - } else { - return (items as Array).concat(toIds(data as Search['accounts'])); - } - }); - } - }); - -const handleSubmitted = (state: State, value: string) => - state.withMutations(state => { - state.set('results', ResultsRecord()); - state.set('submitted', true); - state.set('submittedValue', value); - }); - -const search = (state = ReducerRecord(), action: SearchAction | ComposeAction) => { - switch (action.type) { - case SEARCH_CLEAR: - return ReducerRecord(); - case SEARCH_RESULTS_CLEAR: - return state.merge({ - results: ResultsRecord(), - submitted: false, - submittedValue: '', - }); - case SEARCH_SHOW: - return state.set('hidden', false); - case COMPOSE_REPLY: - case COMPOSE_MENTION: - case COMPOSE_DIRECT: - case COMPOSE_QUOTE: - return state.set('hidden', true); - case SEARCH_FETCH_REQUEST: - return handleSubmitted(state, action.value); - case SEARCH_FETCH_SUCCESS: - return importResults(state, action.results, action.searchTerm, action.searchType); - case SEARCH_FILTER_SET: - return state.set('filter', action.value); - case SEARCH_EXPAND_REQUEST: - return state.setIn(['results', `${action.searchType}Loaded`], false); - case SEARCH_EXPAND_SUCCESS: - return paginateResults(state, action.searchType, action.results, action.searchTerm); - case SEARCH_ACCOUNT_SET: - if (!action.accountId) return state.merge({ - results: ResultsRecord(), - submitted: false, - submittedValue: '', - filter: 'accounts', - accountId: null, - }); - return ReducerRecord({ accountId: action.accountId, filter: 'statuses' }); - default: - return state; - } -}; - -export { - type SearchFilter, - search as default, -};