import {useCallback} from 'react' import {moderateProfile, type ModerationOpts} from '@atproto/api' import {keepPreviousData, useQuery} from '@tanstack/react-query' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences' import {useAgent} from '#/state/session' import { type AutocompleteApi, type AutocompleteItem, type AutocompleteItemType, type AutocompleteProfile, } from '#/components/Autocomplete/types' import {useEmojiSearch} from './useEmojiSearch' const DEFAULT_MOD_OPTS = { userDid: undefined, prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, } export function useAutocomplete({ type, query: q, limit, showSearchFallback = false, }: { type: AutocompleteItemType query: string limit?: number showSearchFallback?: boolean }): AutocompleteApi { const agent = useAgent() const moderationOpts = useModerationOpts() const emojiSearch = useEmojiSearch() const query = useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: [ 'autocomplete', { type, query: q, }, ], async queryFn() { if (type === 'profile') { // TODO return recents if (!q) return [] // Going from "foo" to "foo." should not clear matches. q = q.toLowerCase().trim().replace(/\.$/, '') const res = await agent.searchActorsTypeahead({ q, limit: limit || 8, }) return (res?.data.actors || []).map(profile => ({ key: profile.did, type: 'profile' as const, value: '@' + profile.handle, profile, })) } else if (type === 'emoji') { return emojiSearch(q, limit || 8) } return [] }, select: useCallback( (items: AutocompleteItem[]) => { const seen = new Set() let results: AutocompleteItem[] = [] for (const item of items) { if (seen.has(item.key)) continue seen.add(item.key) if (item.type === 'profile') { const moderated = moderateProfileItem({ query: q, item, moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, }) if (moderated) results.push(moderated) } else { results.push(item) } } if (showSearchFallback && q) { results.unshift({ key: `search-${q}`, type: 'search' as const, value: q, }) } return results }, [q, showSearchFallback, moderationOpts], ), placeholderData: keepPreviousData, }) return { query: q, items: query.data || [], } } function moderateProfileItem({ query, item, moderationOpts, }: { query: string item: AutocompleteProfile moderationOpts: ModerationOpts }) { const modui = moderateProfile(item.profile, moderationOpts).ui('profileList') const isExactMatch = query && item.profile.handle.toLowerCase() === query if ( (isExactMatch && !moduiContainsHideableOffense(modui)) || !modui.filter || isJustAMute(modui) ) { return item } return null }