Collections: Add default recommendations (#39202)
This commit is contained in:
parent
796f771362
commit
fa1e16ed9f
@ -100,6 +100,10 @@ interface ComboboxProps<
|
|||||||
* Icon to be displayed in the text input
|
* Icon to be displayed in the text input
|
||||||
*/
|
*/
|
||||||
icon?: TextInputProps['icon'] | null;
|
icon?: TextInputProps['icon'] | null;
|
||||||
|
/**
|
||||||
|
* Set to true to open as soon as there is focus
|
||||||
|
*/
|
||||||
|
openOnFocus?: boolean;
|
||||||
/**
|
/**
|
||||||
* Set to false to keep the menu open when an item is selected
|
* Set to false to keep the menu open when an item is selected
|
||||||
*/
|
*/
|
||||||
@ -217,8 +221,10 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
|||||||
renderGroupTitle,
|
renderGroupTitle,
|
||||||
renderItem,
|
renderItem,
|
||||||
onSelectItem,
|
onSelectItem,
|
||||||
|
onFocus,
|
||||||
onChange,
|
onChange,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
openOnFocus = false,
|
||||||
closeOnSelect = true,
|
closeOnSelect = true,
|
||||||
suppressMenu = false,
|
suppressMenu = false,
|
||||||
icon = SearchIcon,
|
icon = SearchIcon,
|
||||||
@ -288,6 +294,16 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback(
|
||||||
|
(e) => {
|
||||||
|
if (openOnFocus) {
|
||||||
|
setShouldMenuOpen(true);
|
||||||
|
}
|
||||||
|
onFocus?.(e);
|
||||||
|
},
|
||||||
|
[onFocus, openOnFocus],
|
||||||
|
);
|
||||||
|
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange(e);
|
onChange(e);
|
||||||
@ -487,6 +503,7 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
|
|||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
spellCheck='false'
|
spellCheck='false'
|
||||||
value={value}
|
value={value}
|
||||||
|
onFocus={handleFocus}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
icon={icon ?? undefined}
|
icon={icon ?? undefined}
|
||||||
|
|||||||
@ -215,6 +215,7 @@ export const CollectionAccounts: React.FC<{
|
|||||||
resetAccounts,
|
resetAccounts,
|
||||||
} = useSearchAccounts({
|
} = useSearchAccounts({
|
||||||
withRelationships: true,
|
withRelationships: true,
|
||||||
|
withDefaultFollows: searchValue === '',
|
||||||
// Don't suggest accounts that were already added
|
// Don't suggest accounts that were already added
|
||||||
filterResults: (account) =>
|
filterResults: (account) =>
|
||||||
!editorItems.find((item) => item.account_id === account.id),
|
!editorItems.find((item) => item.account_id === account.id),
|
||||||
@ -363,6 +364,7 @@ export const CollectionAccounts: React.FC<{
|
|||||||
)}
|
)}
|
||||||
{hasPendingItems && <PendingNote />}
|
{hasPendingItems && <PendingNote />}
|
||||||
<ComboboxField
|
<ComboboxField
|
||||||
|
openOnFocus
|
||||||
id={inputId}
|
id={inputId}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
id: 'collections.search_accounts_label',
|
id: 'collections.search_accounts_label',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ApiMutedAccountJSON } from '@/mastodon/api_types/accounts';
|
import type { ApiAccountJSON } from '@/mastodon/api_types/accounts';
|
||||||
import type { Account } from '@/mastodon/models/account';
|
import type { Account } from '@/mastodon/models/account';
|
||||||
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
||||||
|
|
||||||
@ -8,11 +8,11 @@ export function areCollectionsEnabled() {
|
|||||||
|
|
||||||
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
||||||
|
|
||||||
export const canAccountBeAdded = (account: ApiMutedAccountJSON | Account) =>
|
export const canAccountBeAdded = (account: ApiAccountJSON | Account) =>
|
||||||
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
||||||
|
|
||||||
export const canAccountBeAddedByFollowers = (
|
export const canAccountBeAddedByFollowers = (
|
||||||
account: ApiMutedAccountJSON | Account,
|
account: ApiAccountJSON | Account,
|
||||||
) =>
|
) =>
|
||||||
account.feature_approval.automatic.includes('followers') ||
|
account.feature_approval.automatic.includes('followers') ||
|
||||||
account.feature_approval.manual.includes('followers');
|
account.feature_approval.manual.includes('followers');
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
@ -8,16 +8,20 @@ import { apiRequest } from 'mastodon/api';
|
|||||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { useCurrentAccountId } from './useAccountId';
|
||||||
|
|
||||||
export function useSearchAccounts({
|
export function useSearchAccounts({
|
||||||
onSettled,
|
onSettled,
|
||||||
filterResults,
|
filterResults,
|
||||||
resetOnInputClear = true,
|
resetOnInputClear = true,
|
||||||
withRelationships = false,
|
withRelationships = false,
|
||||||
|
withDefaultFollows = false,
|
||||||
}: {
|
}: {
|
||||||
onSettled?: (value: string) => void;
|
onSettled?: (value: string) => void;
|
||||||
filterResults?: (account: ApiAccountJSON) => boolean;
|
filterResults?: (account: ApiAccountJSON) => boolean;
|
||||||
resetOnInputClear?: boolean;
|
resetOnInputClear?: boolean;
|
||||||
withRelationships?: boolean;
|
withRelationships?: boolean;
|
||||||
|
withDefaultFollows?: boolean;
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -29,7 +33,7 @@ export function useSearchAccounts({
|
|||||||
const searchRequestRef = useRef<AbortController | null>(null);
|
const searchRequestRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const searchAccounts = useDebouncedCallback(
|
const searchAccounts = useDebouncedCallback(
|
||||||
(value: string) => {
|
async (value: string) => {
|
||||||
if (searchRequestRef.current) {
|
if (searchRequestRef.current) {
|
||||||
searchRequestRef.current.abort();
|
searchRequestRef.current.abort();
|
||||||
}
|
}
|
||||||
@ -46,41 +50,100 @@ export function useSearchAccounts({
|
|||||||
|
|
||||||
searchRequestRef.current = new AbortController();
|
searchRequestRef.current = new AbortController();
|
||||||
|
|
||||||
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
|
try {
|
||||||
signal: searchRequestRef.current.signal,
|
const data = await apiRequest<ApiAccountJSON[]>(
|
||||||
params: {
|
'GET',
|
||||||
q: value,
|
'v1/accounts/search',
|
||||||
resolve: true,
|
{
|
||||||
},
|
signal: searchRequestRef.current.signal,
|
||||||
})
|
params: {
|
||||||
.then((data) => {
|
q: value,
|
||||||
const accounts = filterResults ? data.filter(filterResults) : data;
|
resolve: true,
|
||||||
const accountIds = accounts.map((a) => a.id);
|
},
|
||||||
dispatch(importFetchedAccounts(accounts));
|
},
|
||||||
if (withRelationships) {
|
);
|
||||||
dispatch(fetchRelationships(accountIds));
|
const accounts = filterResults ? data.filter(filterResults) : data;
|
||||||
}
|
const accountIds = accounts.map((a) => a.id);
|
||||||
setAccounts(accounts);
|
dispatch(importFetchedAccounts(accounts));
|
||||||
setLoadingState('idle');
|
if (withRelationships) {
|
||||||
onSettled?.(value);
|
dispatch(fetchRelationships(accountIds));
|
||||||
})
|
}
|
||||||
.catch(() => {
|
setAccounts(accounts);
|
||||||
setLoadingState('error');
|
setLoadingState('idle');
|
||||||
onSettled?.(value);
|
onSettled?.(value);
|
||||||
});
|
} catch {
|
||||||
|
setLoadingState('error');
|
||||||
|
onSettled?.(value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
500,
|
500,
|
||||||
{ leading: true, trailing: true },
|
{ leading: true, trailing: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const startSearch = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
void searchAccounts(value);
|
||||||
|
},
|
||||||
|
[searchAccounts],
|
||||||
|
);
|
||||||
|
|
||||||
const resetAccounts = useCallback(() => {
|
const resetAccounts = useCallback(() => {
|
||||||
setAccounts([]);
|
setAccounts([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
const currentUserId = useCurrentAccountId();
|
||||||
searchAccounts,
|
const [defaultAccounts, setDefaultAccounts] = useState<
|
||||||
resetAccounts,
|
ApiAccountJSON[] | null
|
||||||
|
>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!currentUserId ||
|
||||||
|
loadingState !== 'idle' ||
|
||||||
|
defaultAccounts !== null ||
|
||||||
|
!withDefaultFollows
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doRequest() {
|
||||||
|
setLoadingState('loading');
|
||||||
|
try {
|
||||||
|
const data = await apiRequest<ApiAccountJSON[]>(
|
||||||
|
'GET',
|
||||||
|
`v1/accounts/${currentUserId}/following`,
|
||||||
|
{ params: { limit: 40 } },
|
||||||
|
);
|
||||||
|
const accounts = filterResults ? data.filter(filterResults) : data;
|
||||||
|
const accountIds = accounts.map((a) => a.id);
|
||||||
|
dispatch(importFetchedAccounts(accounts));
|
||||||
|
if (withRelationships) {
|
||||||
|
dispatch(fetchRelationships(accountIds));
|
||||||
|
}
|
||||||
|
setDefaultAccounts(accounts);
|
||||||
|
setLoadingState('idle');
|
||||||
|
} catch {
|
||||||
|
setLoadingState('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void doRequest();
|
||||||
|
}, [
|
||||||
|
currentUserId,
|
||||||
accounts,
|
accounts,
|
||||||
|
dispatch,
|
||||||
|
filterResults,
|
||||||
|
loadingState,
|
||||||
|
withRelationships,
|
||||||
|
defaultAccounts,
|
||||||
|
withDefaultFollows,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchAccounts: startSearch,
|
||||||
|
resetAccounts,
|
||||||
|
accounts:
|
||||||
|
accounts.length === 0 && withDefaultFollows
|
||||||
|
? (defaultAccounts ?? [])
|
||||||
|
: accounts,
|
||||||
isLoading: loadingState === 'loading',
|
isLoading: loadingState === 'loading',
|
||||||
isError: loadingState === 'error',
|
isError: loadingState === 'error',
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user