Bluesky app fork with some witchin' additions 馃挮
at main 126 lines 3.5 kB view raw
1import React from 'react' 2import { 3 type AppBskyActorDefs, 4 moderateProfile, 5 type ModerationOpts, 6} from '@atproto/api' 7import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query' 8 9import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' 10import {logger} from '#/logger' 11import {STALE} from '#/state/queries' 12import {useAgent} from '#/state/session' 13import {useModerationOpts} from '../preferences/moderation-opts' 14import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences' 15 16const DEFAULT_MOD_OPTS = { 17 userDid: undefined, 18 prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, 19} 20 21const RQKEY_ROOT = 'actor-autocomplete' 22export const RQKEY = (prefix: string) => [RQKEY_ROOT, prefix] 23 24export function useActorAutocompleteQuery( 25 prefix: string, 26 maintainData?: boolean, 27 limit?: number, 28) { 29 const moderationOpts = useModerationOpts() 30 const agent = useAgent() 31 32 prefix = prefix.toLowerCase().trim() 33 if (prefix.endsWith('.')) { 34 // Going from "foo" to "foo." should not clear matches. 35 prefix = prefix.slice(0, -1) 36 } 37 38 return useQuery<AppBskyActorDefs.ProfileViewBasic[]>({ 39 staleTime: STALE.MINUTES.ONE, 40 queryKey: RQKEY(prefix || ''), 41 async queryFn() { 42 const res = prefix 43 ? await agent.searchActorsTypeahead({ 44 q: prefix, 45 limit: limit || 8, 46 }) 47 : undefined 48 return res?.data.actors || [] 49 }, 50 select: React.useCallback( 51 (data: AppBskyActorDefs.ProfileViewBasic[]) => { 52 return computeSuggestions({ 53 q: prefix, 54 searched: data, 55 moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, 56 }) 57 }, 58 [prefix, moderationOpts], 59 ), 60 placeholderData: maintainData ? keepPreviousData : undefined, 61 }) 62} 63 64export type ActorAutocompleteFn = ReturnType<typeof useActorAutocompleteFn> 65export function useActorAutocompleteFn() { 66 const queryClient = useQueryClient() 67 const moderationOpts = useModerationOpts() 68 const agent = useAgent() 69 70 return React.useCallback( 71 async ({query, limit = 8}: {query: string; limit?: number}) => { 72 query = query.toLowerCase() 73 let res 74 if (query) { 75 try { 76 res = await queryClient.fetchQuery({ 77 staleTime: STALE.MINUTES.ONE, 78 queryKey: RQKEY(query || ''), 79 queryFn: () => 80 agent.searchActorsTypeahead({ 81 q: query, 82 limit, 83 }), 84 }) 85 } catch (e) { 86 logger.error('useActorSearch: searchActorsTypeahead failed', { 87 message: e, 88 }) 89 } 90 } 91 92 return computeSuggestions({ 93 q: query, 94 searched: res?.data.actors, 95 moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, 96 }) 97 }, 98 [queryClient, moderationOpts, agent], 99 ) 100} 101 102function computeSuggestions({ 103 q, 104 searched = [], 105 moderationOpts, 106}: { 107 q?: string 108 searched?: AppBskyActorDefs.ProfileViewBasic[] 109 moderationOpts: ModerationOpts 110}) { 111 let items: AppBskyActorDefs.ProfileViewBasic[] = [] 112 for (const item of searched) { 113 if (!items.find(item2 => item2.handle === item.handle)) { 114 items.push(item) 115 } 116 } 117 return items.filter(profile => { 118 const modui = moderateProfile(profile, moderationOpts).ui('profileList') 119 const isExactMatch = q && profile.handle.toLowerCase() === q 120 return ( 121 (isExactMatch && !moduiContainsHideableOffense(modui)) || 122 !modui.filter || 123 isJustAMute(modui) 124 ) 125 }) 126}