Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 145 lines 3.5 kB view raw
1import {useCallback, useMemo} from 'react' 2import {moderateProfile, type ModerationOpts} from '@atproto/api' 3import {keepPreviousData, useQuery} from '@tanstack/react-query' 4 5import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' 6import {useModerationOpts} from '#/state/preferences/moderation-opts' 7import {STALE} from '#/state/queries' 8import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences' 9import {useAgent} from '#/state/session' 10import { 11 type AutocompleteApi, 12 type AutocompleteItem, 13 type AutocompleteItemType, 14 type AutocompleteProfile, 15} from '#/components/Autocomplete/types' 16import {useEmojiSearch} from './useEmojiSearch' 17 18const DEFAULT_MOD_OPTS = { 19 userDid: undefined, 20 prefs: DEFAULT_LOGGED_OUT_PREFERENCES.moderationPrefs, 21} 22 23export function useAutocomplete({ 24 type, 25 query: q, 26 limit, 27 showSearchFallback = false, 28}: { 29 type: AutocompleteItemType 30 query: string 31 limit?: number 32 showSearchFallback?: boolean 33}): AutocompleteApi { 34 const agent = useAgent() 35 const moderationOpts = useModerationOpts() 36 const emojiSearch = useEmojiSearch() 37 38 const query = useQuery({ 39 staleTime: STALE.MINUTES.ONE, 40 queryKey: [ 41 'autocomplete', 42 { 43 type, 44 query: q, 45 }, 46 ], 47 async queryFn() { 48 if (type === 'profile') { 49 // TODO return recents 50 if (!q) return [] 51 52 // Going from "foo" to "foo." should not clear matches. 53 q = q.toLowerCase().trim().replace(/\.$/, '') 54 55 const res = await agent.searchActorsTypeahead({ 56 q, 57 limit: limit || 8, 58 }) 59 60 return (res?.data.actors || []).map(profile => ({ 61 key: profile.did, 62 type: 'profile' as const, 63 value: '@' + profile.handle, 64 profile, 65 })) 66 } else if (type === 'emoji') { 67 return emojiSearch(q, limit || 8) 68 } 69 70 return [] 71 }, 72 select: useCallback( 73 (items: AutocompleteItem[]) => { 74 const seen = new Set<string>() 75 let results: AutocompleteItem[] = [] 76 77 for (const item of items) { 78 if (seen.has(item.key)) continue 79 seen.add(item.key) 80 81 if (item.type === 'profile') { 82 const moderated = moderateProfileItem({ 83 query: q, 84 item, 85 moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, 86 }) 87 if (moderated) results.push(moderated) 88 } else { 89 results.push(item) 90 } 91 } 92 93 return results 94 }, 95 [q, moderationOpts], 96 ), 97 placeholderData: keepPreviousData, 98 }) 99 100 const items = useMemo(() => { 101 if (!query.data) { 102 return [] 103 } 104 105 const results = [...query.data] 106 107 if (showSearchFallback && q) { 108 results.unshift({ 109 key: `search-${q}`, 110 type: 'search' as const, 111 value: q, 112 }) 113 } 114 115 return results 116 }, [query.data, showSearchFallback, q]) 117 118 return { 119 query: q, 120 items, 121 } 122} 123 124function moderateProfileItem({ 125 query, 126 item, 127 moderationOpts, 128}: { 129 query: string 130 item: AutocompleteProfile 131 moderationOpts: ModerationOpts 132}) { 133 const modui = moderateProfile(item.profile, moderationOpts).ui('profileList') 134 const isExactMatch = query && item.profile.handle.toLowerCase() === query 135 136 if ( 137 (isExactMatch && !moduiContainsHideableOffense(modui)) || 138 !modui.filter || 139 isJustAMute(modui) 140 ) { 141 return item 142 } 143 144 return null 145}