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

Configure Feed

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

at cope-settings-sync 135 lines 3.3 kB view raw
1import {useCallback} 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 if (showSearchFallback && q) { 94 results.unshift({ 95 key: `search-${q}`, 96 type: 'search' as const, 97 value: q, 98 }) 99 } 100 101 return results 102 }, 103 [q, showSearchFallback, moderationOpts], 104 ), 105 placeholderData: keepPreviousData, 106 }) 107 108 return { 109 query: q, 110 items: query.data || [], 111 } 112} 113 114function moderateProfileItem({ 115 query, 116 item, 117 moderationOpts, 118}: { 119 query: string 120 item: AutocompleteProfile 121 moderationOpts: ModerationOpts 122}) { 123 const modui = moderateProfile(item.profile, moderationOpts).ui('profileList') 124 const isExactMatch = query && item.profile.handle.toLowerCase() === query 125 126 if ( 127 (isExactMatch && !moduiContainsHideableOffense(modui)) || 128 !modui.filter || 129 isJustAMute(modui) 130 ) { 131 return item 132 } 133 134 return null 135}