+2
-2
src/components/ProgressGuide/FollowDialog.tsx
+2
-2
src/components/ProgressGuide/FollowDialog.tsx
···
8
8
import {logEvent} from '#/lib/statsig/statsig'
9
9
import {isWeb} from '#/platform/detection'
10
10
import {useModerationOpts} from '#/state/preferences/moderation-opts'
11
-
import {useActorSearchPaginated} from '#/state/queries/actor-search'
11
+
import {useActorSearch} from '#/state/queries/actor-search'
12
12
import {usePreferencesQuery} from '#/state/queries/preferences'
13
13
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
14
14
import {useSession} from '#/state/session'
···
128
128
isFetching: isFetchingSearchResults,
129
129
error: searchResultsError,
130
130
isError: isSearchResultsError,
131
-
} = useActorSearchPaginated({
131
+
} = useActorSearch({
132
132
enabled: !!searchText,
133
133
query: searchText,
134
134
})
+1
src/lib/constants.ts
+1
src/lib/constants.ts
+2
-2
src/screens/Search/Explore.tsx
+2
-2
src/screens/Search/Explore.tsx
···
17
17
import {type MetricEvents} from '#/logger/metrics'
18
18
import {useLanguagePrefs} from '#/state/preferences/languages'
19
19
import {useModerationOpts} from '#/state/preferences/moderation-opts'
20
-
import {RQKEY_ROOT_PAGINATED as useActorSearchPaginatedQueryKeyRoot} from '#/state/queries/actor-search'
20
+
import {RQKEY_ROOT as useActorSearchQueryKeyRoot} from '#/state/queries/actor-search'
21
21
import {
22
22
type FeedPreviewItem,
23
23
useFeedPreviews,
···
308
308
queryKey: [getSuggestedUsersQueryKeyRoot],
309
309
}),
310
310
qc.resetQueries({
311
-
queryKey: [useActorSearchPaginatedQueryKeyRoot],
311
+
queryKey: [useActorSearchQueryKeyRoot],
312
312
}),
313
313
qc.resetQueries({
314
314
queryKey: createGetSuggestedFeedsQueryKey(),
+106
-24
src/screens/Search/SearchResults.tsx
+106
-24
src/screens/Search/SearchResults.tsx
···
4
4
import {msg, Trans} from '@lingui/macro'
5
5
import {useLingui} from '@lingui/react'
6
6
7
-
import {usePalette} from '#/lib/hooks/usePalette'
7
+
import {urls} from '#/lib/constants'
8
+
import {cleanError} from '#/lib/strings/errors'
8
9
import {augmentSearchQuery} from '#/lib/strings/helpers'
9
10
import {useActorSearch} from '#/state/queries/actor-search'
10
11
import {usePopularFeedsSearch} from '#/state/queries/feed'
···
21
22
import * as FeedCard from '#/components/FeedCard'
22
23
import * as Layout from '#/components/Layout'
23
24
import {InlineLinkText} from '#/components/Link'
25
+
import {ListFooter} from '#/components/Lists'
24
26
import {SearchError} from '#/components/SearchError'
25
27
import {Text} from '#/components/Typography'
26
28
···
112
114
}
113
115
114
116
function EmptyState({
115
-
message,
117
+
messageText,
116
118
error,
117
119
children,
118
120
}: {
119
-
message: string
121
+
messageText: React.ReactNode
120
122
error?: string
121
123
children?: React.ReactNode
122
124
}) {
···
126
128
<Layout.Content>
127
129
<View style={[a.p_xl]}>
128
130
<View style={[t.atoms.bg_contrast_25, a.rounded_sm, a.p_lg]}>
129
-
<Text style={[a.text_md]}>{message}</Text>
131
+
<Text style={[a.text_md]}>{messageText}</Text>
130
132
131
133
{error && (
132
134
<>
···
155
157
)
156
158
}
157
159
160
+
function NoResultsText({query}: {query: string}) {
161
+
const t = useTheme()
162
+
const {_} = useLingui()
163
+
164
+
return (
165
+
<>
166
+
<Text style={[a.text_lg, t.atoms.text_contrast_high]}>
167
+
<Trans>
168
+
No results found for "
169
+
<Text style={[a.text_lg, t.atoms.text, a.font_medium]}>{query}</Text>
170
+
".
171
+
</Trans>
172
+
</Text>
173
+
{'\n\n'}
174
+
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}>
175
+
<Trans context="english-only-resource">
176
+
Try a different search term, or{' '}
177
+
<InlineLinkText
178
+
label={_(
179
+
msg({
180
+
message: 'read about how to use search filters',
181
+
context: 'english-only-resource',
182
+
}),
183
+
)}
184
+
to={urls.website.blog.searchTipsAndTricks}
185
+
style={[a.text_md, a.leading_snug]}>
186
+
read about how to use search filters
187
+
</InlineLinkText>
188
+
.
189
+
</Trans>
190
+
</Text>
191
+
</>
192
+
)
193
+
}
194
+
158
195
type SearchResultSlice =
159
196
| {
160
197
type: 'post'
···
176
213
active: boolean
177
214
}): React.ReactNode => {
178
215
const {_} = useLingui()
179
-
const {currentAccount} = useSession()
216
+
const {currentAccount, hasSession} = useSession()
180
217
const [isPTR, setIsPTR] = useState(false)
181
-
const isLoggedin = Boolean(currentAccount?.did)
182
218
183
219
const augmentedQuery = useMemo(() => {
184
220
return augmentSearchQuery(query || '', {did: currentAccount?.did})
···
195
231
hasNextPage,
196
232
} = useSearchPostsQuery({query: augmentedQuery, sort, enabled: active})
197
233
198
-
const pal = usePalette('default')
199
234
const t = useTheme()
200
235
const onPullToRefresh = useCallback(async () => {
201
236
setIsPTR(true)
···
249
284
requestSwitchToAccount({requestedAccount: 'new'})
250
285
}
251
286
252
-
if (!isLoggedin) {
287
+
if (!hasSession) {
253
288
return (
254
289
<SearchError
255
290
title={_(msg`Search is currently unavailable when logged out`)}>
256
291
<Text style={[a.text_md, a.text_center, a.leading_snug]}>
257
292
<Trans>
258
293
<InlineLinkText
259
-
style={[pal.link]}
260
294
label={_(msg`Sign in`)}
261
295
to={'#'}
262
296
onPress={showSignIn}>
···
264
298
</InlineLinkText>
265
299
<Text style={t.atoms.text_contrast_medium}> or </Text>
266
300
<InlineLinkText
267
-
style={[pal.link]}
268
301
label={_(msg`Create an account`)}
269
302
to={'#'}
270
303
onPress={showCreateAccount}>
···
283
316
284
317
return error ? (
285
318
<EmptyState
286
-
message={_(
319
+
messageText={_(
287
320
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
288
321
)}
289
-
error={error.toString()}
322
+
error={cleanError(error)}
290
323
/>
291
324
) : (
292
325
<>
···
307
340
onRefresh={onPullToRefresh}
308
341
onEndReached={onEndReached}
309
342
desktopFixedHeight
310
-
contentContainerStyle={{paddingBottom: 100}}
343
+
ListFooterComponent={
344
+
<ListFooter
345
+
isFetchingNextPage={isFetchingNextPage}
346
+
hasNextPage={hasNextPage}
347
+
/>
348
+
}
311
349
/>
312
350
) : (
313
-
<EmptyState message={_(msg`No results found for ${query}`)} />
351
+
<EmptyState messageText={<NoResultsText query={query} />} />
314
352
)}
315
353
</>
316
354
) : (
···
329
367
active: boolean
330
368
}): React.ReactNode => {
331
369
const {_} = useLingui()
370
+
const {hasSession} = useSession()
371
+
const [isPTR, setIsPTR] = useState(false)
332
372
333
-
const {data: results, isFetched} = useActorSearch({
373
+
const {
374
+
isFetched,
375
+
data: results,
376
+
isFetching,
377
+
error,
378
+
refetch,
379
+
fetchNextPage,
380
+
isFetchingNextPage,
381
+
hasNextPage,
382
+
} = useActorSearch({
334
383
query,
335
384
enabled: active,
336
385
})
337
386
338
-
return isFetched && results ? (
387
+
const onPullToRefresh = useCallback(async () => {
388
+
setIsPTR(true)
389
+
await refetch()
390
+
setIsPTR(false)
391
+
}, [setIsPTR, refetch])
392
+
const onEndReached = useCallback(() => {
393
+
if (!hasSession) return
394
+
if (isFetching || !hasNextPage || error) return
395
+
fetchNextPage()
396
+
}, [isFetching, error, hasNextPage, fetchNextPage, hasSession])
397
+
398
+
const profiles = useMemo(() => {
399
+
return results?.pages.flatMap(page => page.actors) || []
400
+
}, [results])
401
+
402
+
if (error) {
403
+
return (
404
+
<EmptyState
405
+
messageText={_(
406
+
msg`We're sorry, but your search could not be completed. Please try again in a few minutes.`,
407
+
)}
408
+
error={error.toString()}
409
+
/>
410
+
)
411
+
}
412
+
413
+
return isFetched && profiles ? (
339
414
<>
340
-
{results.length ? (
415
+
{profiles.length ? (
341
416
<List
342
-
data={results}
417
+
data={profiles}
343
418
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
344
419
keyExtractor={item => item.did}
420
+
refreshing={isPTR}
421
+
onRefresh={onPullToRefresh}
422
+
onEndReached={onEndReached}
345
423
desktopFixedHeight
346
-
contentContainerStyle={{paddingBottom: 100}}
424
+
ListFooterComponent={
425
+
<ListFooter
426
+
hasNextPage={hasNextPage && hasSession}
427
+
isFetchingNextPage={isFetchingNextPage}
428
+
/>
429
+
}
347
430
/>
348
431
) : (
349
-
<EmptyState message={_(msg`No results found for ${query}`)} />
432
+
<EmptyState messageText={<NoResultsText query={query} />} />
350
433
)}
351
434
</>
352
435
) : (
···
363
446
active: boolean
364
447
}): React.ReactNode => {
365
448
const t = useTheme()
366
-
const {_} = useLingui()
367
449
368
450
const {data: results, isFetched} = usePopularFeedsSearch({
369
451
query,
···
378
460
renderItem={({item}) => (
379
461
<View
380
462
style={[
381
-
a.border_b,
463
+
a.border_t,
382
464
t.atoms.border_contrast_low,
383
465
a.px_lg,
384
466
a.py_lg,
···
388
470
)}
389
471
keyExtractor={item => item.uri}
390
472
desktopFixedHeight
391
-
contentContainerStyle={{paddingBottom: 100}}
473
+
ListFooterComponent={<ListFooter />}
392
474
/>
393
475
) : (
394
-
<EmptyState message={_(msg`No results found for ${query}`)} />
476
+
<EmptyState messageText={<NoResultsText query={query} />} />
395
477
)}
396
478
</>
397
479
) : (
+2
-2
src/screens/Search/util/useSuggestedUsers.ts
+2
-2
src/screens/Search/util/useSuggestedUsers.ts
···
1
1
import {useMemo} from 'react'
2
2
3
3
import {useInterestsDisplayNames} from '#/lib/interests'
4
-
import {useActorSearchPaginated} from '#/state/queries/actor-search'
4
+
import {useActorSearch} from '#/state/queries/actor-search'
5
5
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
6
6
7
7
/**
···
31
31
category,
32
32
overrideInterests,
33
33
})
34
-
const searched = useActorSearchPaginated({
34
+
const searched = useActorSearch({
35
35
enabled: !!search,
36
36
// use user's app language translation for this value
37
37
query: category ? interestsDisplayNames[category] : '',
+2
-2
src/screens/StarterPack/Wizard/StepProfiles.tsx
+2
-2
src/screens/StarterPack/Wizard/StepProfiles.tsx
···
7
7
import {isNative} from '#/platform/detection'
8
8
import {useA11y} from '#/state/a11y'
9
9
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
10
-
import {useActorSearchPaginated} from '#/state/queries/actor-search'
10
+
import {useActorSearch} from '#/state/queries/actor-search'
11
11
import {List} from '#/view/com/util/List'
12
12
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
13
13
import {atoms as a, useTheme} from '#/alf'
···
36
36
data: topPages,
37
37
fetchNextPage,
38
38
isLoading: isLoadingTopPages,
39
-
} = useActorSearchPaginated({
39
+
} = useActorSearch({
40
40
query: encodeURIComponent('*'),
41
41
})
42
42
const topFollowers = topPages?.pages
+27
-52
src/state/queries/actor-search.ts
+27
-52
src/state/queries/actor-search.ts
···
1
-
import {
2
-
type AppBskyActorDefs,
3
-
type AppBskyActorSearchActors,
4
-
} from '@atproto/api'
1
+
import {type AppBskyActorSearchActors} from '@atproto/api'
5
2
import {
6
3
type InfiniteData,
7
4
keepPreviousData,
8
5
type QueryClient,
9
6
type QueryKey,
10
7
useInfiniteQuery,
11
-
useQuery,
12
8
} from '@tanstack/react-query'
13
9
14
10
import {STALE} from '#/state/queries'
15
11
import {useAgent} from '#/state/session'
16
12
17
-
const RQKEY_ROOT = 'actor-search'
18
-
export const RQKEY = (query: string) => [RQKEY_ROOT, query]
19
-
20
-
export const RQKEY_ROOT_PAGINATED = `${RQKEY_ROOT}_paginated`
21
-
export const RQKEY_PAGINATED = (query: string, limit?: number) => [
22
-
RQKEY_ROOT_PAGINATED,
13
+
export const RQKEY_ROOT = 'actor-search'
14
+
export const RQKEY = (query: string, limit?: number) => [
15
+
RQKEY_ROOT,
23
16
query,
24
17
limit,
25
18
]
···
27
20
export function useActorSearch({
28
21
query,
29
22
enabled,
30
-
}: {
31
-
query: string
32
-
enabled?: boolean
33
-
}) {
34
-
const agent = useAgent()
35
-
return useQuery<AppBskyActorDefs.ProfileView[]>({
36
-
staleTime: STALE.MINUTES.ONE,
37
-
queryKey: RQKEY(query || ''),
38
-
async queryFn() {
39
-
const res = await agent.searchActors({
40
-
q: query,
41
-
})
42
-
return res.data.actors
43
-
},
44
-
enabled: enabled && !!query,
45
-
})
46
-
}
47
-
48
-
export function useActorSearchPaginated({
49
-
query,
50
-
enabled,
51
23
maintainData,
52
24
limit = 25,
53
25
}: {
···
65
37
string | undefined
66
38
>({
67
39
staleTime: STALE.MINUTES.FIVE,
68
-
queryKey: RQKEY_PAGINATED(query, limit),
40
+
queryKey: RQKEY(query, limit),
69
41
queryFn: async ({pageParam}) => {
70
42
const res = await agent.searchActors({
71
43
q: query,
···
78
50
initialPageParam: undefined,
79
51
getNextPageParam: lastPage => lastPage.cursor,
80
52
placeholderData: maintainData ? keepPreviousData : undefined,
53
+
select,
81
54
})
82
55
}
83
56
57
+
function select(data: InfiniteData<AppBskyActorSearchActors.OutputSchema>) {
58
+
// enforce uniqueness
59
+
const dids = new Set()
60
+
61
+
return {
62
+
...data,
63
+
pages: data.pages.map(page => ({
64
+
actors: page.actors.filter(actor => {
65
+
if (dids.has(actor.did)) {
66
+
return false
67
+
}
68
+
dids.add(actor.did)
69
+
return true
70
+
}),
71
+
})),
72
+
}
73
+
}
74
+
84
75
export function* findAllProfilesInQueryData(
85
76
queryClient: QueryClient,
86
77
did: string,
87
78
) {
88
-
const queryDatas = queryClient.getQueriesData<AppBskyActorDefs.ProfileView[]>(
89
-
{
90
-
queryKey: [RQKEY_ROOT],
91
-
},
92
-
)
93
-
for (const [_queryKey, queryData] of queryDatas) {
94
-
if (!queryData) {
95
-
continue
96
-
}
97
-
for (const actor of queryData) {
98
-
if (actor.did === did) {
99
-
yield actor
100
-
}
101
-
}
102
-
}
103
-
104
-
const queryDatasPaginated = queryClient.getQueriesData<
79
+
const queryDatas = queryClient.getQueriesData<
105
80
InfiniteData<AppBskyActorSearchActors.OutputSchema>
106
81
>({
107
-
queryKey: [RQKEY_ROOT_PAGINATED],
82
+
queryKey: [RQKEY_ROOT],
108
83
})
109
-
for (const [_queryKey, queryData] of queryDatasPaginated) {
84
+
for (const [_queryKey, queryData] of queryDatas) {
110
85
if (!queryData) {
111
86
continue
112
87
}