+1
assets/icons/arrowBottom_stroke2_corner0_rounded.svg
+1
assets/icons/arrowBottom_stroke2_corner0_rounded.svg
···
1
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z" clip-rule="evenodd"/></svg>
+4
src/components/icons/Arrow.tsx
+4
src/components/icons/Arrow.tsx
···
7
7
export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
8
8
path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z',
9
9
})
10
+
11
+
export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({
12
+
path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z',
13
+
})
+32
-2
src/state/queries/feed.ts
+32
-2
src/state/queries/feed.ts
···
190
190
191
191
type GetPopularFeedsOptions = {limit?: number}
192
192
193
-
export function createGetPopularFeedsQueryKey(...args: any[]) {
194
-
return ['getPopularFeeds', ...args]
193
+
export function createGetPopularFeedsQueryKey(
194
+
options?: GetPopularFeedsOptions,
195
+
) {
196
+
return ['getPopularFeeds', options]
195
197
}
196
198
197
199
export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
···
289
291
const agent = useAgent()
290
292
return useMutation({
291
293
mutationFn: async (query: string) => {
294
+
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
295
+
limit: 10,
296
+
query: query,
297
+
})
298
+
299
+
return res.data.feeds
300
+
},
301
+
})
302
+
}
303
+
304
+
const popularFeedsSearchQueryKeyRoot = 'popularFeedsSearch'
305
+
export const createPopularFeedsSearchQueryKey = (query: string) => [
306
+
popularFeedsSearchQueryKeyRoot,
307
+
query,
308
+
]
309
+
310
+
export function usePopularFeedsSearch({
311
+
query,
312
+
enabled,
313
+
}: {
314
+
query: string
315
+
enabled?: boolean
316
+
}) {
317
+
const agent = useAgent()
318
+
return useQuery({
319
+
enabled,
320
+
queryKey: createPopularFeedsSearchQueryKey(query),
321
+
queryFn: async () => {
292
322
const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({
293
323
limit: 10,
294
324
query: query,
+9
-4
src/state/queries/suggested-follows.ts
+9
-4
src/state/queries/suggested-follows.ts
···
23
23
import {useModerationOpts} from '../preferences/moderation-opts'
24
24
25
25
const suggestedFollowsQueryKeyRoot = 'suggested-follows'
26
-
const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
26
+
const suggestedFollowsQueryKey = (options?: SuggestedFollowsOptions) => [
27
+
suggestedFollowsQueryKeyRoot,
28
+
options,
29
+
]
27
30
28
31
const suggestedFollowsByActorQueryKeyRoot = 'suggested-follows-by-actor'
29
32
const suggestedFollowsByActorQueryKey = (did: string) => [
···
31
34
did,
32
35
]
33
36
34
-
export function useSuggestedFollowsQuery() {
37
+
type SuggestedFollowsOptions = {limit?: number}
38
+
39
+
export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
35
40
const {currentAccount} = useSession()
36
41
const agent = useAgent()
37
42
const moderationOpts = useModerationOpts()
···
46
51
>({
47
52
enabled: !!moderationOpts && !!preferences,
48
53
staleTime: STALE.HOURS.ONE,
49
-
queryKey: suggestedFollowsQueryKey,
54
+
queryKey: suggestedFollowsQueryKey(options),
50
55
queryFn: async ({pageParam}) => {
51
56
const contentLangs = getContentLanguages().join(',')
52
57
const res = await agent.app.bsky.actor.getSuggestions(
53
58
{
54
-
limit: 25,
59
+
limit: options?.limit || 25,
55
60
cursor: pageParam,
56
61
},
57
62
{
+556
src/view/screens/Search/Explore.tsx
+556
src/view/screens/Search/Explore.tsx
···
1
+
import React from 'react'
2
+
import {View} from 'react-native'
3
+
import {
4
+
AppBskyActorDefs,
5
+
AppBskyFeedDefs,
6
+
moderateProfile,
7
+
ModerationDecision,
8
+
ModerationOpts,
9
+
} from '@atproto/api'
10
+
import {msg, Trans} from '@lingui/macro'
11
+
import {useLingui} from '@lingui/react'
12
+
13
+
import {logger} from '#/logger'
14
+
import {isWeb} from '#/platform/detection'
15
+
import {useModerationOpts} from '#/state/preferences/moderation-opts'
16
+
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
17
+
import {usePreferencesQuery} from '#/state/queries/preferences'
18
+
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
19
+
import {useSession} from '#/state/session'
20
+
import {cleanError} from 'lib/strings/errors'
21
+
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
22
+
import {List} from '#/view/com/util/List'
23
+
import {UserAvatar} from '#/view/com/util/UserAvatar'
24
+
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
25
+
import {
26
+
FeedFeedLoadingPlaceholder,
27
+
ProfileCardFeedLoadingPlaceholder,
28
+
} from 'view/com/util/LoadingPlaceholder'
29
+
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
30
+
import {Button} from '#/components/Button'
31
+
import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow'
32
+
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
33
+
import {Props as SVGIconProps} from '#/components/icons/common'
34
+
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
35
+
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
36
+
import {Loader} from '#/components/Loader'
37
+
import {Text} from '#/components/Typography'
38
+
39
+
function SuggestedItemsHeader({
40
+
title,
41
+
description,
42
+
style,
43
+
icon: Icon,
44
+
}: {
45
+
title: string
46
+
description: string
47
+
icon: React.ComponentType<SVGIconProps>
48
+
} & ViewStyleProp) {
49
+
const t = useTheme()
50
+
51
+
return (
52
+
<View
53
+
style={[
54
+
isWeb
55
+
? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md]
56
+
: [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md],
57
+
a.border_b,
58
+
t.atoms.border_contrast_low,
59
+
style,
60
+
]}>
61
+
<View style={[a.flex_1, a.gap_sm]}>
62
+
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
63
+
<Icon
64
+
size="lg"
65
+
fill={t.palette.primary_500}
66
+
style={{marginLeft: -2}}
67
+
/>
68
+
<Text style={[a.text_2xl, a.font_bold, t.atoms.text]}>{title}</Text>
69
+
</View>
70
+
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
71
+
{description}
72
+
</Text>
73
+
</View>
74
+
</View>
75
+
)
76
+
}
77
+
78
+
type LoadMoreItems =
79
+
| {
80
+
type: 'profile'
81
+
key: string
82
+
avatar: string
83
+
moderation: ModerationDecision
84
+
}
85
+
| {
86
+
type: 'feed'
87
+
key: string
88
+
avatar: string
89
+
moderation: undefined
90
+
}
91
+
92
+
function LoadMore({
93
+
item,
94
+
moderationOpts,
95
+
}: {
96
+
item: ExploreScreenItems & {type: 'loadMore'}
97
+
moderationOpts?: ModerationOpts
98
+
}) {
99
+
const t = useTheme()
100
+
const {_} = useLingui()
101
+
const items = React.useMemo(() => {
102
+
return item.items
103
+
.map(_item => {
104
+
if (_item.type === 'profile') {
105
+
return {
106
+
type: 'profile',
107
+
key: _item.profile.did,
108
+
avatar: _item.profile.avatar,
109
+
moderation: moderateProfile(_item.profile, moderationOpts!),
110
+
}
111
+
} else if (_item.type === 'feed') {
112
+
return {
113
+
type: 'feed',
114
+
key: _item.feed.uri,
115
+
avatar: _item.feed.avatar,
116
+
moderation: undefined,
117
+
}
118
+
}
119
+
return undefined
120
+
})
121
+
.filter(Boolean) as LoadMoreItems[]
122
+
}, [item.items, moderationOpts])
123
+
const type = items[0].type
124
+
125
+
return (
126
+
<View style={[]}>
127
+
<Button
128
+
label={_(msg`Load more`)}
129
+
onPress={item.onLoadMore}
130
+
style={[a.relative, a.w_full]}>
131
+
{({hovered, pressed}) => (
132
+
<View
133
+
style={[
134
+
a.flex_1,
135
+
a.flex_row,
136
+
a.align_center,
137
+
a.px_lg,
138
+
a.py_md,
139
+
(hovered || pressed) && t.atoms.bg_contrast_25,
140
+
]}>
141
+
<View
142
+
style={[
143
+
a.relative,
144
+
{
145
+
height: 32,
146
+
width: 32 + 15 * 3,
147
+
},
148
+
]}>
149
+
<View
150
+
style={[
151
+
a.align_center,
152
+
a.justify_center,
153
+
a.border,
154
+
t.atoms.bg_contrast_25,
155
+
a.absolute,
156
+
{
157
+
width: 30,
158
+
height: 30,
159
+
left: 0,
160
+
backgroundColor: t.palette.primary_500,
161
+
borderColor: t.atoms.bg.backgroundColor,
162
+
borderRadius: type === 'profile' ? 999 : 4,
163
+
zIndex: 4,
164
+
},
165
+
]}>
166
+
<ArrowBottom fill={t.palette.white} />
167
+
</View>
168
+
{items.map((_item, i) => {
169
+
return (
170
+
<View
171
+
key={_item.key}
172
+
style={[
173
+
a.border,
174
+
t.atoms.bg_contrast_25,
175
+
a.absolute,
176
+
{
177
+
width: 30,
178
+
height: 30,
179
+
left: (i + 1) * 15,
180
+
borderColor: t.atoms.bg.backgroundColor,
181
+
borderRadius: _item.type === 'profile' ? 999 : 4,
182
+
zIndex: 3 - i,
183
+
},
184
+
]}>
185
+
{moderationOpts && (
186
+
<>
187
+
{_item.type === 'profile' ? (
188
+
<UserAvatar
189
+
size={28}
190
+
avatar={_item.avatar}
191
+
moderation={_item.moderation.ui('avatar')}
192
+
/>
193
+
) : _item.type === 'feed' ? (
194
+
<UserAvatar
195
+
size={28}
196
+
avatar={_item.avatar}
197
+
type="algo"
198
+
/>
199
+
) : null}
200
+
</>
201
+
)}
202
+
</View>
203
+
)
204
+
})}
205
+
</View>
206
+
207
+
<Text
208
+
style={[
209
+
a.pl_sm,
210
+
a.leading_snug,
211
+
hovered ? t.atoms.text : t.atoms.text_contrast_medium,
212
+
]}>
213
+
{type === 'profile' ? (
214
+
<Trans>Load more suggested follows</Trans>
215
+
) : (
216
+
<Trans>Load more suggested feeds</Trans>
217
+
)}
218
+
</Text>
219
+
220
+
<View style={[a.flex_1, a.align_end]}>
221
+
{item.isLoadingMore && <Loader size="lg" />}
222
+
</View>
223
+
</View>
224
+
)}
225
+
</Button>
226
+
</View>
227
+
)
228
+
}
229
+
230
+
type ExploreScreenItems =
231
+
| {
232
+
type: 'header'
233
+
key: string
234
+
title: string
235
+
description: string
236
+
style?: ViewStyleProp['style']
237
+
icon: React.ComponentType<SVGIconProps>
238
+
}
239
+
| {
240
+
type: 'profile'
241
+
key: string
242
+
profile: AppBskyActorDefs.ProfileViewBasic
243
+
}
244
+
| {
245
+
type: 'feed'
246
+
key: string
247
+
feed: AppBskyFeedDefs.GeneratorView
248
+
}
249
+
| {
250
+
type: 'loadMore'
251
+
key: string
252
+
isLoadingMore: boolean
253
+
onLoadMore: () => void
254
+
items: ExploreScreenItems[]
255
+
}
256
+
| {
257
+
type: 'profilePlaceholder'
258
+
key: string
259
+
}
260
+
| {
261
+
type: 'feedPlaceholder'
262
+
key: string
263
+
}
264
+
| {
265
+
type: 'error'
266
+
key: string
267
+
message: string
268
+
error: string
269
+
}
270
+
271
+
export function Explore() {
272
+
const {_} = useLingui()
273
+
const t = useTheme()
274
+
const {hasSession} = useSession()
275
+
const {data: preferences, error: preferencesError} = usePreferencesQuery()
276
+
const moderationOpts = useModerationOpts()
277
+
const {
278
+
data: profiles,
279
+
hasNextPage: hasNextProfilesPage,
280
+
isLoading: isLoadingProfiles,
281
+
isFetchingNextPage: isFetchingNextProfilesPage,
282
+
error: profilesError,
283
+
fetchNextPage: fetchNextProfilesPage,
284
+
} = useSuggestedFollowsQuery({limit: 3})
285
+
const {
286
+
data: feeds,
287
+
hasNextPage: hasNextFeedsPage,
288
+
isLoading: isLoadingFeeds,
289
+
isFetchingNextPage: isFetchingNextFeedsPage,
290
+
error: feedsError,
291
+
fetchNextPage: fetchNextFeedsPage,
292
+
} = useGetPopularFeedsQuery({limit: 3})
293
+
294
+
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
295
+
const onLoadMoreProfiles = React.useCallback(async () => {
296
+
if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
297
+
return
298
+
try {
299
+
await fetchNextProfilesPage()
300
+
} catch (err) {
301
+
logger.error('Failed to load more suggested follows', {message: err})
302
+
}
303
+
}, [
304
+
isFetchingNextProfilesPage,
305
+
hasNextProfilesPage,
306
+
profilesError,
307
+
fetchNextProfilesPage,
308
+
])
309
+
310
+
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
311
+
const onLoadMoreFeeds = React.useCallback(async () => {
312
+
if (isFetchingNextFeedsPage || !hasNextFeedsPage || feedsError) return
313
+
try {
314
+
await fetchNextFeedsPage()
315
+
} catch (err) {
316
+
logger.error('Failed to load more suggested follows', {message: err})
317
+
}
318
+
}, [
319
+
isFetchingNextFeedsPage,
320
+
hasNextFeedsPage,
321
+
feedsError,
322
+
fetchNextFeedsPage,
323
+
])
324
+
325
+
const items = React.useMemo<ExploreScreenItems[]>(() => {
326
+
const i: ExploreScreenItems[] = [
327
+
{
328
+
type: 'header',
329
+
key: 'suggested-follows-header',
330
+
title: _(msg`Suggested accounts`),
331
+
description: _(
332
+
msg`Follow more accounts to get connected to your interests and build your network.`,
333
+
),
334
+
icon: Person,
335
+
},
336
+
]
337
+
338
+
if (profiles) {
339
+
// Currently the responses contain duplicate items.
340
+
// Needs to be fixed on backend, but let's dedupe to be safe.
341
+
let seen = new Set()
342
+
for (const page of profiles.pages) {
343
+
for (const actor of page.actors) {
344
+
if (!seen.has(actor.did)) {
345
+
seen.add(actor.did)
346
+
i.push({
347
+
type: 'profile',
348
+
key: actor.did,
349
+
profile: actor,
350
+
})
351
+
}
352
+
}
353
+
}
354
+
355
+
i.push({
356
+
type: 'loadMore',
357
+
key: 'loadMoreProfiles',
358
+
isLoadingMore: isLoadingMoreProfiles,
359
+
onLoadMore: onLoadMoreProfiles,
360
+
items: i.filter(item => item.type === 'profile').slice(-3),
361
+
})
362
+
} else {
363
+
if (profilesError) {
364
+
i.push({
365
+
type: 'error',
366
+
key: 'profilesError',
367
+
message: _(msg`Failed to load suggested follows`),
368
+
error: cleanError(profilesError),
369
+
})
370
+
} else {
371
+
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
372
+
}
373
+
}
374
+
375
+
i.push({
376
+
type: 'header',
377
+
key: 'suggested-feeds-header',
378
+
title: _(msg`Discover new feeds`),
379
+
description: _(
380
+
msg`Custom feeds built by the community bring you new experiences and help you find the content you love.`,
381
+
),
382
+
style: [a.pt_5xl],
383
+
icon: ListSparkle,
384
+
})
385
+
386
+
if (feeds && preferences) {
387
+
// Currently the responses contain duplicate items.
388
+
// Needs to be fixed on backend, but let's dedupe to be safe.
389
+
let seen = new Set()
390
+
for (const page of feeds.pages) {
391
+
for (const feed of page.feeds) {
392
+
if (!seen.has(feed.uri)) {
393
+
seen.add(feed.uri)
394
+
i.push({
395
+
type: 'feed',
396
+
key: feed.uri,
397
+
feed,
398
+
})
399
+
}
400
+
}
401
+
}
402
+
403
+
if (feedsError) {
404
+
i.push({
405
+
type: 'error',
406
+
key: 'feedsError',
407
+
message: _(msg`Failed to load suggested feeds`),
408
+
error: cleanError(feedsError),
409
+
})
410
+
} else if (preferencesError) {
411
+
i.push({
412
+
type: 'error',
413
+
key: 'preferencesError',
414
+
message: _(msg`Failed to load feeds preferences`),
415
+
error: cleanError(preferencesError),
416
+
})
417
+
} else {
418
+
i.push({
419
+
type: 'loadMore',
420
+
key: 'loadMoreFeeds',
421
+
isLoadingMore: isLoadingMoreFeeds,
422
+
onLoadMore: onLoadMoreFeeds,
423
+
items: i.filter(item => item.type === 'feed').slice(-3),
424
+
})
425
+
}
426
+
} else {
427
+
if (feedsError) {
428
+
i.push({
429
+
type: 'error',
430
+
key: 'feedsError',
431
+
message: _(msg`Failed to load suggested feeds`),
432
+
error: cleanError(feedsError),
433
+
})
434
+
} else if (preferencesError) {
435
+
i.push({
436
+
type: 'error',
437
+
key: 'preferencesError',
438
+
message: _(msg`Failed to load feeds preferences`),
439
+
error: cleanError(preferencesError),
440
+
})
441
+
} else {
442
+
i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'})
443
+
}
444
+
}
445
+
446
+
return i
447
+
}, [
448
+
_,
449
+
profiles,
450
+
feeds,
451
+
preferences,
452
+
onLoadMoreFeeds,
453
+
onLoadMoreProfiles,
454
+
isLoadingMoreProfiles,
455
+
isLoadingMoreFeeds,
456
+
profilesError,
457
+
feedsError,
458
+
preferencesError,
459
+
])
460
+
461
+
const renderItem = React.useCallback(
462
+
({item}: {item: ExploreScreenItems}) => {
463
+
switch (item.type) {
464
+
case 'header': {
465
+
return (
466
+
<SuggestedItemsHeader
467
+
title={item.title}
468
+
description={item.description}
469
+
style={item.style}
470
+
icon={item.icon}
471
+
/>
472
+
)
473
+
}
474
+
case 'profile': {
475
+
return (
476
+
<View style={[a.border_b, t.atoms.border_contrast_low]}>
477
+
<ProfileCardWithFollowBtn profile={item.profile} noBg noBorder />
478
+
</View>
479
+
)
480
+
}
481
+
case 'feed': {
482
+
return (
483
+
<View style={[a.border_b, t.atoms.border_contrast_low]}>
484
+
<FeedSourceCard
485
+
feedUri={item.feed.uri}
486
+
showSaveBtn={hasSession}
487
+
showDescription
488
+
showLikes
489
+
pinOnSave
490
+
hideTopBorder
491
+
/>
492
+
</View>
493
+
)
494
+
}
495
+
case 'loadMore': {
496
+
return <LoadMore item={item} moderationOpts={moderationOpts} />
497
+
}
498
+
case 'profilePlaceholder': {
499
+
return <ProfileCardFeedLoadingPlaceholder />
500
+
}
501
+
case 'feedPlaceholder': {
502
+
return <FeedFeedLoadingPlaceholder />
503
+
}
504
+
case 'error': {
505
+
return (
506
+
<View
507
+
style={[
508
+
a.border_t,
509
+
a.pt_md,
510
+
a.px_md,
511
+
t.atoms.border_contrast_low,
512
+
]}>
513
+
<View
514
+
style={[
515
+
a.flex_row,
516
+
a.gap_md,
517
+
a.p_lg,
518
+
a.rounded_sm,
519
+
t.atoms.bg_contrast_25,
520
+
]}>
521
+
<CircleInfo size="md" fill={t.palette.negative_400} />
522
+
<View style={[a.flex_1, a.gap_sm]}>
523
+
<Text style={[a.font_bold, a.leading_snug]}>
524
+
{item.message}
525
+
</Text>
526
+
<Text
527
+
style={[
528
+
a.italic,
529
+
a.leading_snug,
530
+
t.atoms.text_contrast_medium,
531
+
]}>
532
+
{item.error}
533
+
</Text>
534
+
</View>
535
+
</View>
536
+
</View>
537
+
)
538
+
}
539
+
}
540
+
},
541
+
[t, hasSession, moderationOpts],
542
+
)
543
+
544
+
return (
545
+
<List
546
+
data={items}
547
+
renderItem={renderItem}
548
+
keyExtractor={item => item.key}
549
+
// @ts-ignore web only -prf
550
+
desktopFixedHeight
551
+
contentContainerStyle={{paddingBottom: 200}}
552
+
keyboardShouldPersistTaps="handled"
553
+
keyboardDismissMode="on-drag"
554
+
/>
555
+
)
556
+
}
+54
-87
src/view/screens/Search/Search.tsx
+54
-87
src/view/screens/Search/Search.tsx
···
29
29
import {makeProfileLink} from '#/lib/routes/links'
30
30
import {NavigationProp} from '#/lib/routes/types'
31
31
import {augmentSearchQuery} from '#/lib/strings/helpers'
32
-
import {s} from '#/lib/styles'
33
32
import {logger} from '#/logger'
34
33
import {isIOS, isNative, isWeb} from '#/platform/detection'
35
34
import {listenSoftReset} from '#/state/events'
36
35
import {useModerationOpts} from '#/state/preferences/moderation-opts'
37
36
import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
38
37
import {useActorSearch} from '#/state/queries/actor-search'
38
+
import {usePopularFeedsSearch} from '#/state/queries/feed'
39
39
import {useSearchPostsQuery} from '#/state/queries/search-posts'
40
-
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
41
40
import {useSession} from '#/state/session'
42
41
import {useSetDrawerOpen} from '#/state/shell'
43
42
import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from '#/state/shell'
···
56
55
import {List} from '#/view/com/util/List'
57
56
import {Text} from '#/view/com/util/text/Text'
58
57
import {CenteredView, ScrollView} from '#/view/com/util/Views'
58
+
import {Explore} from '#/view/screens/Search/Explore'
59
59
import {SearchLinkCard, SearchProfileCard} from '#/view/shell/desktop/Search'
60
-
import {ProfileCardFeedLoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
60
+
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
61
61
import {atoms as a} from '#/alf'
62
62
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
63
63
···
121
121
</CenteredView>
122
122
)
123
123
}
124
-
125
-
function useSuggestedFollows(): [
126
-
AppBskyActorDefs.ProfileViewBasic[],
127
-
() => void,
128
-
] {
129
-
const {
130
-
data: suggestions,
131
-
hasNextPage,
132
-
isFetchingNextPage,
133
-
isError,
134
-
fetchNextPage,
135
-
} = useSuggestedFollowsQuery()
136
-
137
-
const onEndReached = React.useCallback(async () => {
138
-
if (isFetchingNextPage || !hasNextPage || isError) return
139
-
try {
140
-
await fetchNextPage()
141
-
} catch (err) {
142
-
logger.error('Failed to load more suggested follows', {message: err})
143
-
}
144
-
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
145
-
146
-
const items: AppBskyActorDefs.ProfileViewBasic[] = []
147
-
if (suggestions) {
148
-
// Currently the responses contain duplicate items.
149
-
// Needs to be fixed on backend, but let's dedupe to be safe.
150
-
let seen = new Set()
151
-
for (const page of suggestions.pages) {
152
-
for (const actor of page.actors) {
153
-
if (!seen.has(actor.did)) {
154
-
seen.add(actor.did)
155
-
items.push(actor)
156
-
}
157
-
}
158
-
}
159
-
}
160
-
return [items, onEndReached]
161
-
}
162
-
163
-
let SearchScreenSuggestedFollows = (_props: {}): React.ReactNode => {
164
-
const pal = usePalette('default')
165
-
const [suggestions, onEndReached] = useSuggestedFollows()
166
-
167
-
return suggestions.length ? (
168
-
<List
169
-
data={suggestions}
170
-
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} noBg />}
171
-
keyExtractor={item => item.did}
172
-
// @ts-ignore web only -prf
173
-
desktopFixedHeight
174
-
contentContainerStyle={{paddingBottom: 200}}
175
-
keyboardShouldPersistTaps="handled"
176
-
keyboardDismissMode="on-drag"
177
-
onEndReached={onEndReached}
178
-
onEndReachedThreshold={2}
179
-
/>
180
-
) : (
181
-
<CenteredView sideBorders style={[pal.border, s.hContentRegion]}>
182
-
<ProfileCardFeedLoadingPlaceholder />
183
-
<ProfileCardFeedLoadingPlaceholder />
184
-
</CenteredView>
185
-
)
186
-
}
187
-
SearchScreenSuggestedFollows = React.memo(SearchScreenSuggestedFollows)
188
124
189
125
type SearchResultSlice =
190
126
| {
···
342
278
}
343
279
SearchScreenUserResults = React.memo(SearchScreenUserResults)
344
280
281
+
let SearchScreenFeedsResults = ({
282
+
query,
283
+
active,
284
+
}: {
285
+
query: string
286
+
active: boolean
287
+
}): React.ReactNode => {
288
+
const {_} = useLingui()
289
+
const {hasSession} = useSession()
290
+
291
+
const {data: results, isFetched} = usePopularFeedsSearch({
292
+
query,
293
+
enabled: active,
294
+
})
295
+
296
+
return isFetched && results ? (
297
+
<>
298
+
{results.length ? (
299
+
<List
300
+
data={results}
301
+
renderItem={({item}) => (
302
+
<FeedSourceCard
303
+
feedUri={item.uri}
304
+
showSaveBtn={hasSession}
305
+
showDescription
306
+
showLikes
307
+
pinOnSave
308
+
/>
309
+
)}
310
+
keyExtractor={item => item.did}
311
+
// @ts-ignore web only -prf
312
+
desktopFixedHeight
313
+
contentContainerStyle={{paddingBottom: 100}}
314
+
/>
315
+
) : (
316
+
<EmptyState message={_(msg`No results found for ${query}`)} />
317
+
)}
318
+
</>
319
+
) : (
320
+
<Loader />
321
+
)
322
+
}
323
+
SearchScreenFeedsResults = React.memo(SearchScreenFeedsResults)
324
+
345
325
let SearchScreenInner = ({query}: {query?: string}): React.ReactNode => {
346
326
const pal = usePalette('default')
347
327
const setMinimalShellMode = useSetMinimalShellMode()
···
389
369
<SearchScreenUserResults query={query} active={activeTab === 2} />
390
370
),
391
371
},
372
+
{
373
+
title: _(msg`Feeds`),
374
+
component: (
375
+
<SearchScreenFeedsResults query={query} active={activeTab === 3} />
376
+
),
377
+
},
392
378
]
393
379
}, [_, query, activeTab])
394
380
···
408
394
))}
409
395
</Pager>
410
396
) : hasSession ? (
411
-
<View>
412
-
<CenteredView sideBorders style={pal.border}>
413
-
<Text
414
-
type="title"
415
-
style={[
416
-
pal.text,
417
-
pal.border,
418
-
{
419
-
display: 'flex',
420
-
paddingVertical: 12,
421
-
paddingHorizontal: 18,
422
-
fontWeight: 'bold',
423
-
},
424
-
]}>
425
-
<Trans>Suggested Follows</Trans>
426
-
</Text>
427
-
</CenteredView>
428
-
429
-
<SearchScreenSuggestedFollows />
430
-
</View>
397
+
<Explore />
431
398
) : (
432
399
<CenteredView sideBorders style={pal.border}>
433
400
<View