pl-fe: migrate search to react-query
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
7609a7e2a7
commit
42f7226594
13 changed files with 309 additions and 509 deletions
|
@ -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<string, any> = {
|
|
||||||
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<string, any> = {
|
|
||||||
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<SearchFilter, 'links'>) => ({
|
|
||||||
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<typeof clearSearch>
|
|
||||||
| ReturnType<typeof clearSearchResults>
|
|
||||||
| ReturnType<typeof fetchSearchRequest>
|
|
||||||
| ReturnType<typeof fetchSearchSuccess>
|
|
||||||
| ReturnType<typeof fetchSearchFail>
|
|
||||||
| ReturnType<typeof expandSearchRequest>
|
|
||||||
| ReturnType<typeof expandSearchSuccess>
|
|
||||||
| ReturnType<typeof expandSearchFail>
|
|
||||||
| {
|
|
||||||
type: typeof SEARCH_FILTER_SET;
|
|
||||||
path: (['search', 'filter']);
|
|
||||||
value: SearchFilter;
|
|
||||||
}
|
|
||||||
| ReturnType<typeof showSearch>
|
|
||||||
| ReturnType<typeof setSearchAccount>
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
108
packages/pl-fe/src/api/hooks/search/use-search.ts
Normal file
108
packages/pl-fe/src/api/hooks/search/use-search.ts
Normal file
|
@ -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<SearchParams, keyof PaginationParams | 'type' | 'offset'>,
|
||||||
|
) => {
|
||||||
|
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<string> | undefined = searchQuery.data;
|
||||||
|
|
||||||
|
return searchQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSearchStatuses = (
|
||||||
|
query: string,
|
||||||
|
params?: Omit<SearchParams, keyof PaginationParams | 'type' | 'offset'>,
|
||||||
|
) => {
|
||||||
|
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<SearchParams, keyof PaginationParams | 'type' | 'offset'>,
|
||||||
|
) => {
|
||||||
|
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<SearchParams, keyof PaginationParams | 'type' | 'offset'>,
|
||||||
|
) => {
|
||||||
|
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 };
|
|
@ -20,6 +20,7 @@ interface IAutosuggestAccountInput {
|
||||||
menu?: Menu;
|
menu?: Menu;
|
||||||
onKeyDown?: React.KeyboardEventHandler;
|
onKeyDown?: React.KeyboardEventHandler;
|
||||||
theme?: InputThemes;
|
theme?: InputThemes;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
||||||
|
|
112
packages/pl-fe/src/components/search-input.tsx
Normal file
112
packages/pl-fe/src/components/search-input.tsx
Normal file
|
@ -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<HTMLInputElement>) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
|
||||||
|
setValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
setValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
setValue('');
|
||||||
|
history.push('/search?' + new URLSearchParams({ q: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className='w-full'>
|
||||||
|
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||||
|
|
||||||
|
<div className='relative'>
|
||||||
|
<AutosuggestAccountInput
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onSelected={handleSelected}
|
||||||
|
menu={makeMenu()}
|
||||||
|
autoSelect={false}
|
||||||
|
theme='search'
|
||||||
|
className='pr-10 rtl:pl-10 rtl:pr-3'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto'
|
||||||
|
onClick={handleClear}
|
||||||
|
>
|
||||||
|
<SvgIcon
|
||||||
|
src={require('@tabler/icons/outline/search.svg')}
|
||||||
|
className={clsx('size-4 text-gray-600', { hidden: hasValue })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SvgIcon
|
||||||
|
src={require('@tabler/icons/outline/x.svg')}
|
||||||
|
className={clsx('size-4 text-gray-600', { hidden: !hasValue })}
|
||||||
|
aria-label={intl.formatMessage(messages.placeholder)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { SearchInput as default };
|
|
@ -5,7 +5,6 @@ import { useInteractionRequestsCount } from 'pl-fe/api/hooks/statuses/use-intera
|
||||||
import Icon from 'pl-fe/components/ui/icon';
|
import Icon from 'pl-fe/components/ui/icon';
|
||||||
import Stack from 'pl-fe/components/ui/stack';
|
import Stack from 'pl-fe/components/ui/stack';
|
||||||
import { useStatContext } from 'pl-fe/contexts/stat-context';
|
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 ComposeButton from 'pl-fe/features/ui/components/compose-button';
|
||||||
import ProfileDropdown from 'pl-fe/features/ui/components/profile-dropdown';
|
import ProfileDropdown from 'pl-fe/features/ui/components/profile-dropdown';
|
||||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
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 Account from './account';
|
||||||
import DropdownMenu, { Menu } from './dropdown-menu';
|
import DropdownMenu, { Menu } from './dropdown-menu';
|
||||||
|
import SearchInput from './search-input';
|
||||||
import SidebarNavigationLink from './sidebar-navigation-link';
|
import SidebarNavigationLink from './sidebar-navigation-link';
|
||||||
import SiteLogo from './site-logo';
|
import SiteLogo from './site-logo';
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ const SidebarNavigation = () => {
|
||||||
</ProfileDropdown>
|
</ProfileDropdown>
|
||||||
</div>
|
</div>
|
||||||
<div className='block w-full max-w-xs'>
|
<div className='block w-full max-w-xs'>
|
||||||
<Search openInRoute autosuggest />
|
<SearchInput />
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { biteAccount, blockAccount, pinAccount, removeFromFollowers, unblockAcco
|
||||||
import { mentionCompose, directCompose } from 'pl-fe/actions/compose';
|
import { mentionCompose, directCompose } from 'pl-fe/actions/compose';
|
||||||
import { blockDomain, unblockDomain } from 'pl-fe/actions/domain-blocks';
|
import { blockDomain, unblockDomain } from 'pl-fe/actions/domain-blocks';
|
||||||
import { initReport, ReportableEntities } from 'pl-fe/actions/reports';
|
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 { useFollow } from 'pl-fe/api/hooks/accounts/use-follow';
|
||||||
import Badge from 'pl-fe/components/badge';
|
import Badge from 'pl-fe/components/badge';
|
||||||
import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu';
|
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 onAvatarClick = () => {
|
||||||
const avatar = v.parse(mediaAttachmentSchema, {
|
const avatar = v.parse(mediaAttachmentSchema, {
|
||||||
id: '',
|
id: '',
|
||||||
|
@ -334,7 +328,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
if (features.searchFromAccount) {
|
if (features.searchFromAccount) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(account.id === ownAccount.id ? messages.searchSelf : messages.search, { name: account.username }),
|
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'),
|
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 EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||||
import { ComposeEditor } from 'pl-fe/features/ui/util/async-components';
|
import { ComposeEditor } from 'pl-fe/features/ui/util/async-components';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
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 { useCompose } from 'pl-fe/hooks/use-compose';
|
||||||
import { useDraggedFiles } from 'pl-fe/hooks/use-dragged-files';
|
import { useDraggedFiles } from 'pl-fe/hooks/use-dragged-files';
|
||||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
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 { configuration } = useInstance();
|
||||||
|
|
||||||
const compose = useCompose(id);
|
const compose = useCompose(id);
|
||||||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
|
||||||
const maxTootChars = configuration.statuses.max_characters;
|
const maxTootChars = configuration.statuses.max_characters;
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
|
@ -111,7 +109,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
const isEmpty = !(fulltext.trim() || anyMedia);
|
const isEmpty = !(fulltext.trim() || anyMedia);
|
||||||
const condensed = shouldCondense && !isDraggedOver && !composeFocused && isEmpty && !isUploading;
|
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 canSubmit = !!editorRef.current && !isSubmitting && !isUploading && !isChangingUpload && !isEmpty && length(fulltext) <= maxTootChars;
|
||||||
|
|
||||||
const getClickableArea = () => clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
const getClickableArea = () => clickableAreaRef ? clickableAreaRef.current : formRef.current;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
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 { 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 { useTrendingLinks } from 'pl-fe/api/hooks/trends/use-trending-links';
|
||||||
import { useTrendingStatuses } from 'pl-fe/api/hooks/trends/use-trending-statuses';
|
import { useTrendingStatuses } from 'pl-fe/api/hooks/trends/use-trending-statuses';
|
||||||
import Hashtag from 'pl-fe/components/hashtag';
|
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 PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account';
|
||||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||||
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
|
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 { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||||
import useTrends from 'pl-fe/queries/trends';
|
import useTrends from 'pl-fe/queries/trends';
|
||||||
|
|
||||||
import type { SearchFilter } from 'pl-fe/reducers/search';
|
type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
||||||
|
@ -34,27 +34,47 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const SearchResults = () => {
|
const SearchResults = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
const [tabKey, setTabKey] = useState(1);
|
const [tabKey, setTabKey] = useState(1);
|
||||||
|
|
||||||
const value = useAppSelector((state) => state.search.submittedValue);
|
const [params, setParams] = useSearchParams();
|
||||||
const results = useAppSelector((state) => state.search.results);
|
|
||||||
|
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 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: trendingTags } = useTrends();
|
||||||
const { data: trendingStatuses } = useTrendingStatuses();
|
const { data: trendingStatuses } = useTrendingStatuses();
|
||||||
const { trendingLinks } = useTrendingLinks();
|
const { trendingLinks } = useTrendingLinks();
|
||||||
const { account } = useAccount(filterByAccount);
|
const { account } = useAccount(accountId);
|
||||||
|
|
||||||
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
const handleUnsetAccount = () => {
|
||||||
|
params.delete('accountId');
|
||||||
const handleUnsetAccount = () => dispatch(setSearchAccount(null));
|
setParams(params => Object.fromEntries(params.entries()));
|
||||||
|
};
|
||||||
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(value, newActiveFilter));
|
|
||||||
|
|
||||||
const renderFilterBar = () => {
|
const renderFilterBar = () => {
|
||||||
const items = [];
|
const items = [];
|
||||||
|
@ -108,23 +128,21 @@ const SearchResults = () => {
|
||||||
if (element) element.focus();
|
if (element) element.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
let searchResults;
|
let searchResults: Array<JSX.Element> | undefined;
|
||||||
let hasMore = false;
|
const hasMore = activeQuery.hasNextPage;
|
||||||
let loaded;
|
const isLoading = activeQuery.isFetching;
|
||||||
let noResultsMessage;
|
let noResultsMessage: JSX.Element | undefined;
|
||||||
let placeholderComponent = PlaceholderStatus as React.ComponentType;
|
let placeholderComponent = PlaceholderStatus as React.ComponentType;
|
||||||
let resultsIds: Array<string>;
|
let resultsIds: Array<string>;
|
||||||
|
|
||||||
if (selectedFilter === 'accounts') {
|
if (selectedFilter === 'accounts') {
|
||||||
hasMore = results.accountsHasMore;
|
|
||||||
loaded = results.accountsLoaded;
|
|
||||||
placeholderComponent = PlaceholderAccount;
|
placeholderComponent = PlaceholderAccount;
|
||||||
|
|
||||||
if (results.accounts && results.accounts.length > 0) {
|
if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) {
|
||||||
searchResults = results.accounts.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
searchResults = searchAccountsQuery.data.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||||
} else if (!submitted && suggestions && suggestions.length !== 0) {
|
} else if (suggestions && suggestions.length > 0) {
|
||||||
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account_id} id={suggestion.account_id} />);
|
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account_id} id={suggestion.account_id} />);
|
||||||
} else if (loaded) {
|
} else if (submitted && !isLoading) {
|
||||||
noResultsMessage = (
|
noResultsMessage = (
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -138,11 +156,8 @@ const SearchResults = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFilter === 'statuses') {
|
if (selectedFilter === 'statuses') {
|
||||||
hasMore = results.statusesHasMore;
|
if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) {
|
||||||
loaded = results.statusesLoaded;
|
searchResults = searchStatusesQuery.data.map((statusId: string) => (
|
||||||
|
|
||||||
if (results.statuses && results.statuses.length > 0) {
|
|
||||||
searchResults = results.statuses.map((statusId: string) => (
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={statusId}
|
key={statusId}
|
||||||
|
@ -151,8 +166,8 @@ const SearchResults = () => {
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
resultsIds = results.statuses;
|
resultsIds = searchStatusesQuery.data;
|
||||||
} else if (!submitted && !filterByAccount && trendingStatuses && trendingStatuses.length !== 0) {
|
} else if (!submitted && !accountId && trendingStatuses && trendingStatuses.length !== 0) {
|
||||||
searchResults = trendingStatuses.map((statusId: string) => (
|
searchResults = trendingStatuses.map((statusId: string) => (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
|
@ -163,7 +178,7 @@ const SearchResults = () => {
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
resultsIds = trendingStatuses;
|
resultsIds = trendingStatuses;
|
||||||
} else if (loaded) {
|
} else if (submitted && !isLoading) {
|
||||||
noResultsMessage = (
|
noResultsMessage = (
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -177,15 +192,13 @@ const SearchResults = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFilter === 'hashtags') {
|
if (selectedFilter === 'hashtags') {
|
||||||
hasMore = results.hashtagsHasMore;
|
|
||||||
loaded = results.hashtagsLoaded;
|
|
||||||
placeholderComponent = PlaceholderHashtag;
|
placeholderComponent = PlaceholderHashtag;
|
||||||
|
|
||||||
if (results.hashtags && results.hashtags.length > 0) {
|
if (searchHashtagsQuery.data && searchHashtagsQuery.data.length > 0) {
|
||||||
searchResults = results.hashtags.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
searchResults = searchHashtagsQuery.data.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||||
} else if (!submitted && suggestions && suggestions.length !== 0) {
|
} else if (!submitted && suggestions && suggestions.length !== 0) {
|
||||||
searchResults = trendingTags?.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
searchResults = trendingTags?.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||||
} else if (loaded) {
|
} else if (submitted && !isLoading) {
|
||||||
noResultsMessage = (
|
noResultsMessage = (
|
||||||
<div className='empty-column-indicator'>
|
<div className='empty-column-indicator'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -199,19 +212,17 @@ const SearchResults = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedFilter === 'links') {
|
if (selectedFilter === 'links') {
|
||||||
loaded = true;
|
|
||||||
|
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
selectFilter('accounts');
|
selectFilter('accounts');
|
||||||
setTabKey(key => ++key);
|
setTabKey(key => ++key);
|
||||||
} else if (!submitted && trendingLinks) {
|
} else if (trendingLinks) {
|
||||||
searchResults = trendingLinks.map(trendingLink => <TrendingLink trendingLink={trendingLink} />);
|
searchResults = trendingLinks.map(trendingLink => <TrendingLink trendingLink={trendingLink} />);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{filterByAccount ? (
|
{accountId ? (
|
||||||
<HStack className='border-b border-solid border-gray-200 p-2 pb-4 dark:border-gray-800' space={2}>
|
<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} />
|
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/outline/x.svg')} onClick={handleUnsetAccount} />
|
||||||
<Text truncate>
|
<Text truncate>
|
||||||
|
@ -228,8 +239,8 @@ const SearchResults = () => {
|
||||||
<ScrollableList
|
<ScrollableList
|
||||||
id='search-results'
|
id='search-results'
|
||||||
key={selectedFilter}
|
key={selectedFilter}
|
||||||
isLoading={submitted && !loaded}
|
isLoading={submitted && isLoading}
|
||||||
showLoading={submitted && !loaded && (!searchResults || searchResults?.length === 0)}
|
showLoading={submitted && isLoading && (searchResults?.length === 0 || activeQuery.isRefetching)}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
placeholderComponent={placeholderComponent}
|
placeholderComponent={placeholderComponent}
|
||||||
|
|
|
@ -1,91 +1,43 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import debounce from 'lodash/debounce';
|
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 { 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 Input from 'pl-fe/components/ui/input';
|
||||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
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({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||||
action: { id: 'search.action', defaultMessage: 'Search for “{query}”' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirectToAccount = (accountId: string, routerHistory: any) =>
|
const Search = () => {
|
||||||
(_dispatch: AppDispatch, getState: () => RootState) => {
|
const [params, setParams] = useSearchParams();
|
||||||
const acct = selectAccount(getState(), accountId)!.acct;
|
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 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) => {
|
const debouncedSubmit = useCallback(debounce((value: string) => {
|
||||||
dispatch(submitSearch(value));
|
setQuery(value);
|
||||||
}, 900), []);
|
}, 900), []);
|
||||||
|
|
||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = event.target;
|
const { value } = event.target;
|
||||||
|
|
||||||
setValue(value);
|
setValue(value);
|
||||||
|
debouncedSubmit(value);
|
||||||
if (autoSubmit) {
|
|
||||||
debouncedSubmit(value);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
const handleClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (value.length > 0 || submitted) {
|
if (value.length > 0) {
|
||||||
dispatch(clearSearchResults());
|
setValue('');
|
||||||
}
|
setQuery('');
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (openInRoute) {
|
|
||||||
dispatch(setSearchAccount(null));
|
|
||||||
dispatch(submitSearch(value));
|
|
||||||
|
|
||||||
history.push('/search');
|
|
||||||
} else {
|
|
||||||
dispatch(submitSearch(value));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -93,67 +45,32 @@ const Search = (props: ISearch) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
handleSubmit();
|
setQuery(value);
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
document.querySelector('.ui')?.parentElement?.focus();
|
document.querySelector('.ui')?.parentElement?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => {
|
const hasValue = value.length > 0;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('w-full', {
|
className='sticky top-[76px] z-10 w-full bg-white/90 backdrop-blur black:bg-black/80 dark:bg-primary-900/90'
|
||||||
'sticky top-[76px] z-10 bg-white/90 backdrop-blur black:bg-black/80 dark:bg-primary-900/90': !openInRoute,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||||
|
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
{autosuggest ? (
|
<Input
|
||||||
<AutosuggestAccountInput {...componentProps} />
|
type='text'
|
||||||
) : (
|
id='search'
|
||||||
<Input {...componentProps} />
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
)}
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
autoFocus
|
||||||
|
theme='search'
|
||||||
|
className='pr-10 rtl:pl-10 rtl:pr-3'
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
role='button'
|
role='button'
|
||||||
|
|
|
@ -15,7 +15,7 @@ const SearchPage = () => {
|
||||||
return (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)}>
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
<Search autoFocus autoSubmit />
|
<Search />
|
||||||
<SearchResults />
|
<SearchResults />
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -2,12 +2,10 @@ import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { setFilter } from 'pl-fe/actions/search';
|
|
||||||
import Hashtag from 'pl-fe/components/hashtag';
|
import Hashtag from 'pl-fe/components/hashtag';
|
||||||
import Text from 'pl-fe/components/ui/text';
|
import Text from 'pl-fe/components/ui/text';
|
||||||
import Widget from 'pl-fe/components/ui/widget';
|
import Widget from 'pl-fe/components/ui/widget';
|
||||||
import PlaceholderSidebarTrends from 'pl-fe/features/placeholder/components/placeholder-sidebar-trends';
|
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';
|
import useTrends from 'pl-fe/queries/trends';
|
||||||
|
|
||||||
interface ITrendsPanel {
|
interface ITrendsPanel {
|
||||||
|
@ -22,15 +20,10 @@ const messages = defineMessages({
|
||||||
});
|
});
|
||||||
|
|
||||||
const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const { data: trends, isFetching } = useTrends();
|
const { data: trends, isFetching } = useTrends();
|
||||||
|
|
||||||
const setHashtagsFilter = () => {
|
|
||||||
dispatch(setFilter('', 'hashtags'));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isFetching && !trends?.length) {
|
if (!isFetching && !trends?.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -39,7 +32,7 @@ const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
||||||
<Widget
|
<Widget
|
||||||
title={<FormattedMessage id='trends.title' defaultMessage='Trends' />}
|
title={<FormattedMessage id='trends.title' defaultMessage='Trends' />}
|
||||||
action={
|
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'>
|
<Text tag='span' theme='primary' size='sm' className='hover:underline'>
|
||||||
{intl.formatMessage(messages.viewAll)}
|
{intl.formatMessage(messages.viewAll)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -32,7 +32,6 @@ import plfe from './pl-fe';
|
||||||
import polls from './polls';
|
import polls from './polls';
|
||||||
import push_notifications from './push-notifications';
|
import push_notifications from './push-notifications';
|
||||||
import scheduled_statuses from './scheduled-statuses';
|
import scheduled_statuses from './scheduled-statuses';
|
||||||
import search from './search';
|
|
||||||
import security from './security';
|
import security from './security';
|
||||||
import status_lists from './status-lists';
|
import status_lists from './status-lists';
|
||||||
import statuses from './statuses';
|
import statuses from './statuses';
|
||||||
|
@ -72,7 +71,6 @@ const reducers = {
|
||||||
polls,
|
polls,
|
||||||
push_notifications,
|
push_notifications,
|
||||||
scheduled_statuses,
|
scheduled_statuses,
|
||||||
search,
|
|
||||||
security,
|
security,
|
||||||
status_lists,
|
status_lists,
|
||||||
statuses,
|
statuses,
|
||||||
|
|
|
@ -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<string>(),
|
|
||||||
statuses: Array<string>(),
|
|
||||||
groups: Array<string>(),
|
|
||||||
hashtags: Array<Tag>(), // 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<typeof ReducerRecord>;
|
|
||||||
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<SearchFilter, 'links'>, 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<Tag>).concat(data as Search['hashtags']);
|
|
||||||
} else {
|
|
||||||
return (items as Array<string>).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,
|
|
||||||
};
|
|
Loading…
Reference in a new issue