pl-fe: merge some stuff from the deck branch

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-08-25 11:54:23 +02:00
parent 444bdef71c
commit 0c901edc03
3 changed files with 264 additions and 189 deletions

View 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 };

View 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 };

View File

@ -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} />
</>
);
};