forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}