pl-fe: merge some stuff from the deck branch
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
166
packages/pl-fe/src/columns/search.tsx
Normal file
166
packages/pl-fe/src/columns/search.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import StatusContainer from 'pl-fe/containers/status-container';
|
||||
import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account';
|
||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
|
||||
import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/queries/search/use-search';
|
||||
|
||||
import TrendsColumn from './trends';
|
||||
|
||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
interface ISearchColumn {
|
||||
type: 'accounts' | 'hashtags' | 'statuses' | 'links';
|
||||
query: string;
|
||||
accountId?: string;
|
||||
multiColumn?: boolean;
|
||||
}
|
||||
|
||||
const SearchColumn: React.FC<ISearchColumn> = ({ type, query, accountId, multiColumn }) => {
|
||||
query = query.trim();
|
||||
|
||||
const node = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const searchAccountsQuery = useSearchAccounts(type === 'accounts' && query || '');
|
||||
const searchStatusesQuery = useSearchStatuses(type === 'statuses' && query || '', {
|
||||
account_id: accountId,
|
||||
});
|
||||
const searchHashtagsQuery = useSearchHashtags(type === 'hashtags' && query || '');
|
||||
|
||||
const activeQuery = ({
|
||||
accounts: searchAccountsQuery,
|
||||
statuses: searchStatusesQuery,
|
||||
hashtags: searchHashtagsQuery,
|
||||
links: searchStatusesQuery,
|
||||
})[type]!;
|
||||
|
||||
const getCurrentIndex = (id: string): number => resultsIds?.findIndex(key => key === id);
|
||||
|
||||
const handleMoveUp = (id: string) => {
|
||||
if (!resultsIds) return;
|
||||
|
||||
const elementIndex = getCurrentIndex(id) - 1;
|
||||
selectChild(elementIndex);
|
||||
};
|
||||
|
||||
const handleMoveDown = (id: string) => {
|
||||
if (!resultsIds) return;
|
||||
|
||||
const elementIndex = getCurrentIndex(id) + 1;
|
||||
selectChild(elementIndex);
|
||||
};
|
||||
|
||||
const selectChild = (index: number) => {
|
||||
const selector = `#search-results [data-index="${index}"] .focusable`;
|
||||
const element = document.querySelector<HTMLDivElement>(selector);
|
||||
|
||||
if (element) element.focus();
|
||||
|
||||
node.current?.scrollIntoView({
|
||||
index,
|
||||
behavior: 'smooth',
|
||||
done: () => {
|
||||
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleLoadMore = () => activeQuery.fetchNextPage({ cancelRefetch: false });
|
||||
|
||||
let searchResults;
|
||||
const hasMore = activeQuery.hasNextPage;
|
||||
const isLoading = activeQuery.isFetching;
|
||||
let placeholderComponent = PlaceholderStatus;
|
||||
let resultsIds: Array<string>;
|
||||
|
||||
switch (type) {
|
||||
case 'accounts': {
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
if (!query) return <TrendsColumn type='accounts' />;
|
||||
if (searchAccountsQuery.data && searchAccountsQuery.data.length > 0) {
|
||||
resultsIds = searchAccountsQuery.data;
|
||||
searchResults = searchAccountsQuery.data.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||
} else if (!isLoading) {
|
||||
return (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.accounts'
|
||||
defaultMessage='There are no people results for "{term}"'
|
||||
values={{ term: query }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'statuses':
|
||||
case 'links': {
|
||||
if (!query) return <TrendsColumn type='statuses' />;
|
||||
if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) {
|
||||
resultsIds = searchStatusesQuery.data;
|
||||
searchResults = searchStatusesQuery.data.map(statusId => <StatusContainer key={statusId} id={statusId} onMoveUp={handleMoveUp} onMoveDown={handleMoveDown} />);
|
||||
} else if (!isLoading) {
|
||||
return (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.statuses'
|
||||
defaultMessage='There are no posts results for "{term}"'
|
||||
values={{ term: query }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'hashtags': {
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
if (!query) return <TrendsColumn type='hashtags' />;
|
||||
if (searchHashtagsQuery.data && searchHashtagsQuery.data.length > 0) {
|
||||
resultsIds = searchHashtagsQuery.data.map(hashtag => hashtag.name);
|
||||
searchResults = searchHashtagsQuery.data.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||
} else if (!isLoading) {
|
||||
return (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.statuses'
|
||||
defaultMessage='There are no posts results for "{term}"'
|
||||
values={{ term: query }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableList
|
||||
scrollKey={`search-results:${type}`}
|
||||
ref={node}
|
||||
id='search-results'
|
||||
key={type}
|
||||
isLoading={!!query && isLoading}
|
||||
showLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
listClassName={type === 'statuses' ? 'divide-y divide-solid divide-gray-200 dark:divide-gray-800' : ''}
|
||||
itemClassName={clsx({
|
||||
'pb-4': type === 'accounts' || type === 'links',
|
||||
'pb-3': type === 'hashtags',
|
||||
})}
|
||||
useWindowScroll={!multiColumn}
|
||||
>
|
||||
{searchResults || []}
|
||||
</ScrollableList>
|
||||
);
|
||||
};
|
||||
|
||||
export { SearchColumn as default };
|
||||
86
packages/pl-fe/src/columns/trends.tsx
Normal file
86
packages/pl-fe/src/columns/trends.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import TrendingLink from 'pl-fe/components/trending-link';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import StatusContainer from 'pl-fe/containers/status-container';
|
||||
import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account';
|
||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
|
||||
import useTrends from 'pl-fe/queries/trends';
|
||||
import { useSuggestedAccounts } from 'pl-fe/queries/trends/use-suggested-accounts';
|
||||
import { useTrendingLinks } from 'pl-fe/queries/trends/use-trending-links';
|
||||
import { useTrendingStatuses } from 'pl-fe/queries/trends/use-trending-statuses';
|
||||
|
||||
interface ITrendsColumn {
|
||||
type: 'accounts' | 'hashtags' | 'statuses' | 'links';
|
||||
emptyMessage?: JSX.Element;
|
||||
multiColumn?: boolean;
|
||||
}
|
||||
|
||||
const TrendsColumn: React.FC<ITrendsColumn> = ({ type, multiColumn }) => {
|
||||
const { data: accounts, isFetching: isFetchingAccounts, isLoading: isLoadingAccounts } = useSuggestedAccounts();
|
||||
const { data: trendingTags, isFetching: isFetchingTags, isLoading: isLoadingTags } = useTrends();
|
||||
const { data: trendingStatuses, isFetching: isFetchingStatuses, isLoading: isLoadingStatuses } = useTrendingStatuses();
|
||||
const { data: trendingLinks, isFetching: isFetchingLinks, isLoading: isLoadingLinks } = useTrendingLinks();
|
||||
|
||||
let placeholderComponent = PlaceholderStatus;
|
||||
|
||||
let children;
|
||||
let isFetching;
|
||||
let isLoading;
|
||||
|
||||
switch (type) {
|
||||
case 'accounts': {
|
||||
children = accounts?.map(account => <AccountContainer key={account.account_id} id={account.account_id} />);
|
||||
isFetching = isFetchingAccounts;
|
||||
isLoading = isLoadingAccounts;
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
break;
|
||||
}
|
||||
case 'hashtags': {
|
||||
children = trendingTags?.map(tag => <Hashtag key={tag.name} hashtag={tag} />);
|
||||
isFetching = isFetchingTags;
|
||||
isLoading = isLoadingTags;
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
break;
|
||||
}
|
||||
case 'statuses': {
|
||||
children = trendingStatuses?.map(statusId => <StatusContainer key={statusId} id={statusId} />);
|
||||
isFetching = isFetchingStatuses;
|
||||
isLoading = isLoadingStatuses;
|
||||
break;
|
||||
}
|
||||
case 'links': {
|
||||
children = trendingLinks?.map(link => <TrendingLink key={link.id} trendingLink={link} />);
|
||||
isFetching = isFetchingLinks;
|
||||
isLoading = isLoadingLinks;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollableList
|
||||
scrollKey={`trends:${type}`}
|
||||
// ref={node}
|
||||
id='trends'
|
||||
key={type}
|
||||
isLoading={isFetching}
|
||||
showLoading={isLoading}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
listClassName={type === 'statuses' ? 'divide-y divide-solid divide-gray-200 dark:divide-gray-800' : ''}
|
||||
itemClassName={clsx({
|
||||
'pb-4': type === 'accounts' || type === 'links',
|
||||
'pb-3': type === 'hashtags',
|
||||
})}
|
||||
useWindowScroll={!multiColumn}
|
||||
>
|
||||
{children || []}
|
||||
</ScrollableList>
|
||||
);
|
||||
};
|
||||
|
||||
export { TrendsColumn as default };
|
||||
@ -1,32 +1,18 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import SearchColumn from 'pl-fe/columns/search';
|
||||
import IconButton from 'pl-fe/components/icon-button';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import TrendingLink from 'pl-fe/components/trending-link';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
||||
import Tabs from 'pl-fe/components/ui/tabs';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
import StatusContainer from 'pl-fe/containers/status-container';
|
||||
import PlaceholderAccount from 'pl-fe/features/placeholder/components/placeholder-account';
|
||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||
import PlaceholderStatus from 'pl-fe/features/placeholder/components/placeholder-status';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useSearchAccounts, useSearchHashtags, useSearchStatuses } from 'pl-fe/queries/search/use-search';
|
||||
import useTrends from 'pl-fe/queries/trends';
|
||||
import { useSuggestedAccounts } from 'pl-fe/queries/trends/use-suggested-accounts';
|
||||
import { useTrendingLinks } from 'pl-fe/queries/trends/use-trending-links';
|
||||
import { useTrendingStatuses } from 'pl-fe/queries/trends/use-trending-statuses';
|
||||
|
||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links';
|
||||
|
||||
@ -126,12 +112,9 @@ const SearchInput: React.FC<ISearchInput> = ({ placeholder }) => {
|
||||
};
|
||||
|
||||
const SearchResults = () => {
|
||||
const node = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
|
||||
const [tabKey, setTabKey] = useState(1);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [params, setParams] = useSearchParams();
|
||||
|
||||
@ -140,30 +123,15 @@ const SearchResults = () => {
|
||||
const selectedFilter = (params.get('type') || 'accounts') as SearchFilter;
|
||||
const accountId = params.get('accountId') || undefined;
|
||||
|
||||
const searchAccountsQuery = useSearchAccounts(selectedFilter === 'accounts' && value || '');
|
||||
const searchStatusesQuery = useSearchStatuses(selectedFilter === 'statuses' && value || '', {
|
||||
account_id: accountId,
|
||||
});
|
||||
const searchHashtagsQuery = useSearchHashtags(selectedFilter === 'hashtags' && value || '');
|
||||
|
||||
const activeQuery = ({
|
||||
accounts: searchAccountsQuery,
|
||||
statuses: searchStatusesQuery,
|
||||
hashtags: searchHashtagsQuery,
|
||||
links: searchStatusesQuery,
|
||||
})[selectedFilter]!;
|
||||
|
||||
const handleLoadMore = () => activeQuery.fetchNextPage({ cancelRefetch: false });
|
||||
|
||||
const selectFilter = (newActiveFilter: SearchFilter) => {
|
||||
if (newActiveFilter === selectedFilter) activeQuery.refetch();
|
||||
else setParams(params => ({ ...Object.fromEntries(params.entries()), type: newActiveFilter }));
|
||||
if (newActiveFilter === selectedFilter) {
|
||||
queryClient.refetchQueries({
|
||||
queryKey: ['search', newActiveFilter, value, newActiveFilter === 'statuses' ? { account_id: accountId } : undefined],
|
||||
exact: true,
|
||||
});
|
||||
} else setParams(params => ({ ...Object.fromEntries(params.entries()), type: newActiveFilter }));
|
||||
};
|
||||
|
||||
const { data: suggestions } = useSuggestedAccounts();
|
||||
const { data: trendingTags } = useTrends();
|
||||
const { data: trendingStatuses } = useTrendingStatuses();
|
||||
const { data: trendingLinks } = useTrendingLinks();
|
||||
const { account } = useAccount(accountId);
|
||||
|
||||
const handleUnsetAccount = () => {
|
||||
@ -197,132 +165,9 @@ const SearchResults = () => {
|
||||
name: 'links',
|
||||
});
|
||||
|
||||
return <Tabs key={tabKey} items={items} activeItem={selectedFilter} />;
|
||||
return <Tabs items={items} activeItem={selectedFilter} />;
|
||||
};
|
||||
|
||||
const getCurrentIndex = (id: string): number => resultsIds?.findIndex(key => key === id);
|
||||
|
||||
const handleMoveUp = (id: string) => {
|
||||
if (!resultsIds) return;
|
||||
|
||||
const elementIndex = getCurrentIndex(id) - 1;
|
||||
selectChild(elementIndex);
|
||||
};
|
||||
|
||||
const handleMoveDown = (id: string) => {
|
||||
if (!resultsIds) return;
|
||||
|
||||
const elementIndex = getCurrentIndex(id) + 1;
|
||||
selectChild(elementIndex);
|
||||
};
|
||||
|
||||
const selectChild = (index: number) => {
|
||||
const selector = `#search-results [data-index="${index}"] .focusable`;
|
||||
const element = document.querySelector<HTMLDivElement>(selector);
|
||||
|
||||
if (element) element.focus();
|
||||
|
||||
node.current?.scrollIntoView({
|
||||
index,
|
||||
behavior: 'smooth',
|
||||
done: () => {
|
||||
if (!element) document.querySelector<HTMLDivElement>(selector)?.focus();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
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') {
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
|
||||
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 (submitted && !isLoading) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.accounts'
|
||||
defaultMessage='There are no people results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'statuses') {
|
||||
if (searchStatusesQuery.data && searchStatusesQuery.data.length > 0) {
|
||||
searchResults = searchStatusesQuery.data.map((statusId: string) => (
|
||||
// @ts-ignore
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
));
|
||||
resultsIds = searchStatusesQuery.data;
|
||||
} else if (!submitted && !accountId && trendingStatuses && trendingStatuses.length !== 0) {
|
||||
searchResults = trendingStatuses.map((statusId: string) => (
|
||||
// @ts-ignore
|
||||
<StatusContainer
|
||||
key={statusId}
|
||||
id={statusId}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
/>
|
||||
));
|
||||
resultsIds = trendingStatuses;
|
||||
} else if (submitted && !isLoading) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.statuses'
|
||||
defaultMessage='There are no posts results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'hashtags') {
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
|
||||
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 (submitted && !isLoading) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.hashtags'
|
||||
defaultMessage='There are no hashtags results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'links') {
|
||||
if (submitted) {
|
||||
selectFilter('accounts');
|
||||
setTabKey(key => ++key);
|
||||
} else if (trendingLinks) {
|
||||
searchResults = trendingLinks.map(trendingLink => <TrendingLink trendingLink={trendingLink} />);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{accountId ? (
|
||||
@ -338,29 +183,7 @@ const SearchResults = () => {
|
||||
</HStack>
|
||||
) : renderFilterBar()}
|
||||
|
||||
{noResultsMessage || (
|
||||
<ScrollableList
|
||||
scrollKey={`searchResults:${selectedFilter}:${value}`}
|
||||
ref={node}
|
||||
id='search-results'
|
||||
key={selectedFilter}
|
||||
isLoading={submitted && isLoading}
|
||||
showLoading={submitted && isLoading && (searchResults?.length === 0 || activeQuery.isRefetching)}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
listClassName={clsx({
|
||||
'divide-gray-200 dark:divide-gray-800 divide-solid divide-y': selectedFilter === 'statuses',
|
||||
})}
|
||||
itemClassName={clsx({
|
||||
'pb-4': selectedFilter === 'accounts' || selectedFilter === 'links',
|
||||
'pb-3': selectedFilter === 'hashtags',
|
||||
})}
|
||||
>
|
||||
{searchResults || []}
|
||||
</ScrollableList>
|
||||
)}
|
||||
<SearchColumn query={value} type={selectedFilter} accountId={accountId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user