diff --git a/src/features/searchHistory/index.ts b/src/features/searchHistory/index.ts new file mode 100644 index 0000000000..d1193941c6 --- /dev/null +++ b/src/features/searchHistory/index.ts @@ -0,0 +1,93 @@ +import {useCallback} from 'react' + +import {useProfilesQuery} from '#/state/queries/profile' +import {useSession} from '#/state/session' +import { + type SearchFilters, + serializeHistoryEntry, +} from '#/screens/Search/searchParams' +import {account, useStorage} from '#/storage' +import type * as bsky from '#/types/bsky' + +const MAX_TERMS = 6 +const MAX_PROFILES = 10 + +/** + * Per-account recent search history (device storage). Terms are stored as + * serialized history entries (plain string, or JSON when filters are + * attached); profiles are stored as DIDs and hydrated via useProfilesQuery, + * which keeps avatars/names fresh (stale-while-revalidate). + */ +export function useSearchHistory() { + const {currentAccount} = useSession() + const [termHistory = [], setTermHistory] = useStorage(account, [ + currentAccount?.did ?? 'pwi', + 'searchTermHistory', + ] as const) + const [accountHistory = [], setAccountHistory] = useStorage(account, [ + currentAccount?.did ?? 'pwi', + 'searchAccountHistory', + ]) + + const {data: accountHistoryProfiles} = useProfilesQuery({ + handles: accountHistory, + maintainData: true, + }) + + const profiles = + accountHistoryProfiles?.profiles.filter(p => + accountHistory.includes(p.did), + ) ?? [] + + const updateSearchHistory = useCallback( + (q: string, searchFilters: SearchFilters = {}) => { + if (!q) return + /* + * Store the query plus any advanced-search filters. Term-only searches + * serialize to a plain string (back-compatible with existing history); + * filtered searches serialize to JSON. Dedupe on the serialized form. + */ + const item = serializeHistoryEntry(q, searchFilters) + const newSearchHistory = [ + item, + ...termHistory.filter(search => search !== item), + ].slice(0, MAX_TERMS) + setTermHistory(newSearchHistory) + }, + [termHistory, setTermHistory], + ) + + const updateProfileHistory = useCallback( + (item: bsky.profile.AnyProfileView) => { + const newAccountHistory = [ + item.did, + ...accountHistory.filter(p => p !== item.did), + ].slice(0, MAX_PROFILES) + setAccountHistory(newAccountHistory) + }, + [accountHistory, setAccountHistory], + ) + + const deleteSearchHistoryItem = useCallback( + (item: string) => { + setTermHistory(termHistory.filter(search => search !== item)) + }, + [termHistory, setTermHistory], + ) + + const deleteProfileHistoryItem = useCallback( + (item: bsky.profile.AnyProfileView) => { + setAccountHistory(accountHistory.filter(p => p !== item.did)) + }, + [accountHistory, setAccountHistory], + ) + + return { + termHistory, + profiles, + updateSearchHistory, + updateProfileHistory, + deleteSearchHistoryItem, + deleteProfileHistoryItem, + } +} diff --git a/src/screens/Search/Shell.tsx b/src/screens/Search/Shell.tsx index 951d415a87..acd02bea2b 100644 --- a/src/screens/Search/Shell.tsx +++ b/src/screens/Search/Shell.tsx @@ -28,10 +28,7 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {MagnifyingGlassIcon} from '#/lib/icons' import {type NavigationProp, type SearchParams} from '#/lib/routes/types' import {listenSoftReset} from '#/state/events' -import { - unstableCacheProfileView, - useProfilesQuery, -} from '#/state/queries/profile' +import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' import { countActiveFilters, @@ -41,7 +38,6 @@ import { parseHistoryEntry, readSearchFilters, type SearchFilters, - serializeHistoryEntry, withoutFilterParams, } from '#/screens/Search/searchParams' import { @@ -62,7 +58,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE, IS_WEB} from '#/env' -import {account, useStorage} from '#/storage' +import {useSearchHistory} from '#/features/searchHistory' import type * as bsky from '#/types/bsky' import {AdvancedSearchDialog} from './components/AdvancedSearchDialog' import {AutocompleteResults} from './components/AutocompleteResults' @@ -116,7 +112,6 @@ export function SearchScreenShell({ const route = useRoute() const textInput = useRef(null) const {t: l} = useLingui() - const {currentAccount} = useSession() const queryClient = useQueryClient() // Get tab parameter from route params @@ -144,61 +139,14 @@ export function SearchScreenShell({ const [showAutocomplete, setShowAutocomplete] = useState(false) - const [termHistory = [], setTermHistory] = useStorage(account, [ - currentAccount?.did ?? 'pwi', - 'searchTermHistory', - ] as const) - const [accountHistory = [], setAccountHistory] = useStorage(account, [ - currentAccount?.did ?? 'pwi', - 'searchAccountHistory', - ]) - - const {data: accountHistoryProfiles} = useProfilesQuery({ - handles: accountHistory, - maintainData: true, - }) - - const updateSearchHistory = useCallback( - (q: string, searchFilters: SearchFilters = {}) => { - if (!q) return - /* - * Store the query plus any advanced-search filters. Term-only searches - * serialize to a plain string (back-compatible with existing history); - * filtered searches serialize to JSON. Dedupe on the serialized form. - */ - const item = serializeHistoryEntry(q, searchFilters) - const newSearchHistory = [ - item, - ...termHistory.filter(search => search !== item), - ].slice(0, 6) - setTermHistory(newSearchHistory) - }, - [termHistory, setTermHistory], - ) - - const updateProfileHistory = useCallback( - (item: bsky.profile.AnyProfileView) => { - const newAccountHistory = [ - item.did, - ...accountHistory.filter(p => p !== item.did), - ].slice(0, 10) - setAccountHistory(newAccountHistory) - }, - [accountHistory, setAccountHistory], - ) - - const deleteSearchHistoryItem = useCallback( - (item: string) => { - setTermHistory(termHistory.filter(search => search !== item)) - }, - [termHistory, setTermHistory], - ) - const deleteProfileHistoryItem = useCallback( - (item: bsky.profile.AnyProfileView) => { - setAccountHistory(accountHistory.filter(p => p !== item.did)) - }, - [accountHistory, setAccountHistory], - ) + const { + termHistory, + profiles: historyProfiles, + updateSearchHistory, + updateProfileHistory, + deleteSearchHistoryItem, + deleteProfileHistoryItem, + } = useSearchHistory() const {query, filters, setFilters, hasFilters} = useQueryManager({ initialQuery: queryParam, @@ -683,11 +631,7 @@ export function SearchScreenShell({ ) : ( - accountHistory.includes(p.did), - ) ?? [] - } + selectedProfiles={historyProfiles} onItemClick={handleHistoryItemClick} onProfileClick={handleProfileClick} onRemoveItemClick={deleteSearchHistoryItem}