diff --git a/src/components/SubtleHover.tsx b/src/components/SubtleHover.tsx
new file mode 100644
index 0000000000..bb5911baa6
--- /dev/null
+++ b/src/components/SubtleHover.tsx
@@ -0,0 +1,34 @@
+import {View} from 'react-native'
+
+import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
+
+export function SubtleHover({style, hover}: ViewStyleProp & {hover: boolean}) {
+ const t = useTheme()
+
+ let opacity: number
+ switch (t.name) {
+ case 'dark':
+ opacity = 0.4
+ break
+ case 'dim':
+ opacity = 0.45
+ break
+ case 'light':
+ opacity = 0.5
+ break
+ }
+
+ return (
+
+ )
+}
diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts
index 0cbacaf1a4..6467583692 100644
--- a/src/logger/metrics.ts
+++ b/src/logger/metrics.ts
@@ -330,7 +330,6 @@ export type MetricEvents = {
| 'suggestedFeeds'
| 'suggestedStarterPacks'
| `feed:${FeedDescriptor}`
- index: number
}
'explore:module:searchButtonPress': {
module: 'suggestedAccounts' | 'suggestedFeeds'
diff --git a/src/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx
index 79e27ec9a4..e29b85f76f 100644
--- a/src/screens/Search/Explore.tsx
+++ b/src/screens/Search/Explore.tsx
@@ -7,6 +7,7 @@ import {
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
+import {useQueryClient} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useGate} from '#/lib/statsig/statsig'
@@ -22,9 +23,19 @@ import {
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
import {Nux, useNux} from '#/state/queries/nuxs'
import {usePreferencesQuery} from '#/state/queries/preferences'
-import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
-import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
-import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
+import {
+ createGetSuggestedFeedsQueryKey,
+ useGetSuggestedFeedsQuery,
+} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
+import {
+ getSuggestedUsersQueryKeyRoot,
+ useGetSuggestedUsersQuery,
+} from '#/state/queries/trending/useGetSuggestedUsersQuery'
+import {createGetTrendsQueryKey} from '#/state/queries/trending/useGetTrendsQuery'
+import {
+ createSuggestedStarterPacksQueryKey,
+ useSuggestedStarterPacksQuery,
+} from '#/state/queries/useSuggestedStarterPacksQuery'
import {useProgressGuide} from '#/state/shell/progress-guide'
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
@@ -41,6 +52,7 @@ import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommenda
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
import {atoms as a, native, platform, useTheme, web} from '#/alf'
+import {Admonition} from '#/components/Admonition'
import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
@@ -49,9 +61,11 @@ import {type Props as IcoProps} from '#/components/icons/common'
import {type Props as SVGIconProps} from '#/components/icons/common'
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
import {StarterPack} from '#/components/icons/StarterPack'
+import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending'
import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard'
+import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import * as ModuleHeader from './components/ModuleHeader'
import {
@@ -69,33 +83,26 @@ function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
onPress={item.onLoadMore}
style={[a.relative, a.w_full]}>
{({hovered, pressed}) => (
-
-
+
+
- {item.message}
-
- {item.isLoadingMore ? (
-
- ) : (
-
- )}
-
+ {item.message}
+ {item.isLoadingMore ? (
+
+ ) : (
+
+ )}
+
+ >
)}
)
@@ -112,6 +119,7 @@ type ExploreScreenItems =
title: string
icon: React.ComponentType
iconSize?: IcoProps['size']
+ bottomBorder?: boolean
searchButton?: {
label: string
metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
@@ -147,6 +155,10 @@ type ExploreScreenItems =
profile: AppBskyActorDefs.ProfileViewBasic
recId?: number
}
+ | {
+ type: 'profileEmpty'
+ key: 'profileEmpty'
+ }
| {
type: 'feed'
key: string
@@ -203,11 +215,12 @@ export function Explore({
const gate = useGate()
const guide = useProgressGuide('follow-10')
const [selectedInterest, setSelectedInterest] = useState(null)
- // TODO always get at least 10 back
+ // TODO always get at least 10 back TODO still
const {
data: suggestedUsers,
isLoading: suggestedUsersIsLoading,
error: suggestedUsersError,
+ isRefetching: suggestedUsersIsRefetching,
} = useGetSuggestedUsersQuery({
category: selectedInterest,
})
@@ -227,6 +240,7 @@ export function Explore({
data: suggestedSPs,
isLoading: isLoadingSuggestedSPs,
error: suggestedSPsError,
+ isRefetching: isRefetchingSuggestedSPs,
} = useSuggestedStarterPacksQuery()
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
@@ -262,6 +276,27 @@ export function Explore({
},
} = useFeedPreviews(suggestedFeeds?.feeds ?? [])
+ const qc = useQueryClient()
+ const [isPTR, setIsPTR] = useState(false)
+ const onPTR = useCallback(async () => {
+ setIsPTR(true)
+ await Promise.all([
+ await qc.resetQueries({
+ queryKey: createGetTrendsQueryKey(),
+ }),
+ await qc.resetQueries({
+ queryKey: createSuggestedStarterPacksQueryKey(),
+ }),
+ await qc.resetQueries({
+ queryKey: [getSuggestedUsersQueryKeyRoot],
+ }),
+ await qc.resetQueries({
+ queryKey: createGetSuggestedFeedsQueryKey(),
+ }),
+ ])
+ setIsPTR(false)
+ }, [qc, setIsPTR])
+
const onLoadMoreFeedPreviews = useCallback(async () => {
if (
isPendingFeedPreviews ||
@@ -305,7 +340,7 @@ export function Explore({
},
})
- if (suggestedUsersIsLoading) {
+ if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
} else if (suggestedUsersError) {
i.push({
@@ -333,14 +368,18 @@ export function Explore({
}
if (profileItems.length === 0) {
- // no items! remove the header
- i.pop()
+ i.push({
+ type: 'profileEmpty',
+ key: 'profileEmpty',
+ })
} else {
i.push(...profileItems)
}
} else {
- // no items! remove the header
- i.pop()
+ i.push({
+ type: 'profileEmpty',
+ key: 'profileEmpty',
+ })
}
} else {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
@@ -352,6 +391,7 @@ export function Explore({
moderationOpts,
suggestedUsers,
suggestedUsersIsLoading,
+ suggestedUsersIsRefetching,
suggestedUsersError,
])
const suggestedFeedsModule = useMemo(() => {
@@ -466,7 +506,7 @@ export function Explore({
iconSize: 'xl',
})
- if (isLoadingSuggestedSPs) {
+ if (isLoadingSuggestedSPs || isRefetchingSuggestedSPs) {
Array.from({length: 3}).forEach((__, index) =>
i.push({
type: 'starterPackSkeleton',
@@ -486,7 +526,13 @@ export function Explore({
})
}
return i
- }, [suggestedSPs, _, isLoadingSuggestedSPs, suggestedSPsError])
+ }, [
+ suggestedSPs,
+ _,
+ isLoadingSuggestedSPs,
+ suggestedSPsError,
+ isRefetchingSuggestedSPs,
+ ])
const feedPreviewsModule = useMemo(() => {
const i: ExploreScreenItems[] = []
i.push(...feedPreviewSlices)
@@ -520,6 +566,13 @@ export function Explore({
if (isNewUser) {
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
+ i.push({
+ type: 'header',
+ key: 'trending-topics-header',
+ title: _(msg`Trending topics`),
+ icon: Graph,
+ bottomBorder: true,
+ })
i.push(trendingTopicsModule)
} else {
i.push(trendingTopicsModule)
@@ -533,6 +586,7 @@ export function Explore({
return i
}, [
+ _,
topBorder,
isNewUser,
suggestedFollowsModule,
@@ -564,7 +618,7 @@ export function Explore({
)
case 'header': {
return (
-
+
{item.title}
{item.searchButton && (
@@ -623,6 +677,15 @@ export function Explore({
/>
)
}
+ case 'profileEmpty': {
+ return (
+
+
+ No results for "{selectedInterest}".
+
+
+ )
+ }
case 'feed': {
return (
+ style={[
+ a.pt_xs,
+ t.atoms.border_contrast_low,
+ native(a.border_b),
+ ]}>
+ {/* Very non-scientific way to avoid small gap on scroll */}
+
@@ -854,7 +923,7 @@ export function Explore({
}
if (!alreadyReportedRef.current.has(module)) {
alreadyReportedRef.current.set(module, module)
- logger.metric('explore:module:seen', {module}) //, index: index ?? -1})
+ logger.metric('explore:module:seen', {module})
}
}, [])
@@ -876,6 +945,8 @@ export function Explore({
windowSize={9}
maxToRenderPerBatch={platform({ios: 5, default: 1})}
updateCellsBatchingPeriod={40}
+ refreshing={isPTR}
+ onRefresh={onPTR}
/>
)
}
diff --git a/src/screens/Search/components/ModuleHeader.tsx b/src/screens/Search/components/ModuleHeader.tsx
index c6411d1c0d..9c208d2b24 100644
--- a/src/screens/Search/components/ModuleHeader.tsx
+++ b/src/screens/Search/components/ModuleHeader.tsx
@@ -18,7 +18,12 @@ export function Container({
style,
children,
headerHeight,
-}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
+ bottomBorder,
+}: {
+ children: React.ReactNode
+ headerHeight?: number
+ bottomBorder?: boolean
+} & ViewStyleProp) {
const t = useTheme()
return (
- {/* Very non-scientific way to avoid small gap on scroll */}
-
{children}
)
diff --git a/src/screens/Search/components/StarterPackCard.tsx b/src/screens/Search/components/StarterPackCard.tsx
index 1b9f948282..fcb0ef0680 100644
--- a/src/screens/Search/components/StarterPackCard.tsx
+++ b/src/screens/Search/components/StarterPackCard.tsx
@@ -19,6 +19,7 @@ import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
+import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
@@ -48,75 +49,80 @@ export function StarterPackCard({
.map(item => item.subject)
return (
-
-
-
-
-
-
+
+ {s => (
+ <>
+
-
+
+
-
-
-
- {view.record.name}
-
-
- {view.creator?.did === currentAccount?.did
- ? _(msg`By you`)
- : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
-
-
-
-
- Open pack
-
-
-
-
+
+
+
+ {view.record.name}
+
+
+ {view.creator?.did === currentAccount?.did
+ ? _(msg`By you`)
+ : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
+
+
+
+
+ Open pack
+
+
+
+
+ >
+ )}
+
)
}
diff --git a/src/screens/Search/modules/ExploreInterestsCard.tsx b/src/screens/Search/modules/ExploreInterestsCard.tsx
index 00a15111a4..00014ffc6a 100644
--- a/src/screens/Search/modules/ExploreInterestsCard.tsx
+++ b/src/screens/Search/modules/ExploreInterestsCard.tsx
@@ -40,14 +40,14 @@ export function ExploreInterestsCard() {
<>
-
-
-
-
-
- {
- logger.metric(
- 'suggestedUser:follow',
- {
- logContext: 'Explore',
- location: 'Card',
- recId,
- position,
- },
- {statsig: true},
- )
- }}
- />
-
-
-
-
+ {s => (
+ <>
+
+
+
+
+
+
+ {
+ logger.metric(
+ 'suggestedUser:follow',
+ {
+ logContext: 'Explore',
+ location: 'Card',
+ recId,
+ position,
+ },
+ {statsig: true},
+ )
+ }}
+ />
+
+
+
+
+ >
+ )}
)
}
diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx
index 75ca193510..167f6d1938 100644
--- a/src/screens/Search/modules/ExploreTrendingTopics.tsx
+++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx
@@ -17,6 +17,7 @@ import {type Props as SVGIconProps} from '#/components/icons/common'
import {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
import {Link} from '#/components/Link'
+import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography'
const TOPIC_COUNT = 5
@@ -28,10 +29,10 @@ export function ExploreTrendingTopics() {
}
function Inner() {
- const {data: trending, error, isLoading} = useGetTrendsQuery()
+ const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
const noTopics = !isLoading && !error && !trending?.trends?.length
- return isLoading ? (
+ return isLoading || isRefetching ? (
Array.from({length: TOPIC_COUNT}).map((__, i) => (
))
@@ -92,25 +93,23 @@ export function TrendRow({
PressableComponent={Pressable}>
{({hovered, pressed}) => (
<>
-
+
+
+ style={[
+ a.text_md,
+ a.font_bold,
+ a.leading_tight,
+ {width: 20},
+ ]}>
{rank}.
{trend.displayName}
diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx
index fcf9194dbe..2aee8b6b35 100644
--- a/src/state/queries/explore-feed-previews.tsx
+++ b/src/state/queries/explore-feed-previews.tsx
@@ -34,6 +34,41 @@ const RQKEY_ROOT = 'feed-previews'
const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
const LIMIT = 8 // sliced to 6, overfetch to account for moderation
+const PINNED_POST_URIS: Record = {
+ // 📰 News
+ 'at://did:plc:kkf4naxqmweop7dv4l2iqqf5/app.bsky.feed.post/3lgh27w2ngc2b':
+ true,
+ // Gardening
+ 'at://did:plc:5rw2on4i56btlcajojaxwcat/app.bsky.feed.post/3kjorckgcwc27':
+ true,
+ // Web Development Trending
+ 'at://did:plc:m2sjv3wncvsasdapla35hzwj/app.bsky.feed.post/3lfaw445axs22':
+ true,
+ // Anime & Manga EN
+ 'at://did:plc:tazrmeme4dzahimsykusrwrk/app.bsky.feed.post/3knxx2gmkns2y':
+ true,
+ // 📽️ Film
+ 'at://did:plc:2hwwem55ce6djnk6bn62cstr/app.bsky.feed.post/3llhpzhbq7c2g':
+ true,
+ // PopSky
+ 'at://did:plc:lfdf4srj43iwdng7jn35tjsp/app.bsky.feed.post/3lbblgly65c2g':
+ true,
+ // Science
+ 'at://did:plc:hu2obebw3nhfj667522dahfg/app.bsky.feed.post/3kl33otd6ob2s':
+ true,
+ // Birds! 🦉
+ 'at://did:plc:ffkgesg3jsv2j7aagkzrtcvt/app.bsky.feed.post/3lbg4r57yk22d':
+ true,
+ // Astronomy
+ 'at://did:plc:xy2zorw2ys47poflotxthlzg/app.bsky.feed.post/3kyzye4lujs2w':
+ true,
+ // What's Cooking 🍽️
+ 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3lfqhgvxbqc2q':
+ true,
+ // BookSky 💙📚 #booksky
+ 'at://did:plc:geoqe3qls5mwezckxxsewys2/app.bsky.feed.post/3kgrm2rw5ww2e':
+ true,
+}
export type FeedPreviewItem =
| {
@@ -181,19 +216,24 @@ export function useFeedPreviews(
feedContext: item.feedContext,
reason: item.reason,
feedPostUri: item.feedPostUri,
- items: item.items.slice(0, 6).map((subItem, i) => {
- const feedPostSliceItem: FeedPostSliceItem = {
- _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
- uri: subItem.post.uri,
- post: subItem.post,
- record: subItem.record,
- moderation: moderations[i],
- parentAuthor: subItem.parentAuthor,
- isParentBlocked: subItem.isParentBlocked,
- isParentNotFound: subItem.isParentNotFound,
- }
- return feedPostSliceItem
- }),
+ items: item.items
+ .slice(0, 6)
+ .filter(subItem => {
+ return !PINNED_POST_URIS[subItem.post.uri]
+ })
+ .map((subItem, i) => {
+ const feedPostSliceItem: FeedPostSliceItem = {
+ _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
+ uri: subItem.post.uri,
+ post: subItem.post,
+ record: subItem.record,
+ moderation: moderations[i],
+ parentAuthor: subItem.parentAuthor,
+ isParentBlocked: subItem.isParentBlocked,
+ isParentNotFound: subItem.isParentNotFound,
+ }
+ return feedPostSliceItem
+ }),
}
if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2
diff --git a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
index 55b633af07..eef71f1ca3 100644
--- a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts
@@ -20,8 +20,7 @@ export function useGetSuggestedFeedsQuery() {
return useQuery({
enabled: !!preferences,
- refetchOnWindowFocus: true,
- staleTime: STALE.MINUTES.ONE,
+ staleTime: STALE.MINUTES.THREE,
queryKey: createGetSuggestedFeedsQueryKey(),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
index eb97ad6666..c8c3f00896 100644
--- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
@@ -27,14 +27,14 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
return useQuery({
enabled: !!preferences,
- refetchOnWindowFocus: true,
- staleTime: STALE.MINUTES.ONE,
+ staleTime: STALE.MINUTES.THREE,
queryKey: createGetSuggestedUsersQueryKey(props),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
{
category: props.category ?? undefined,
+ limit: 10,
},
{
headers: {
diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts
index 02386a5052..94a5b0cbaa 100644
--- a/src/state/queries/trending/useGetTrendsQuery.ts
+++ b/src/state/queries/trending/useGetTrendsQuery.ts
@@ -25,7 +25,6 @@ export function useGetTrendsQuery() {
return useQuery({
enabled: !!preferences,
- refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.THREE,
queryKey: createGetTrendsQueryKey(),
queryFn: async () => {
diff --git a/src/state/queries/useSuggestedStarterPacksQuery.ts b/src/state/queries/useSuggestedStarterPacksQuery.ts
index cda3c28ab1..3ec030ac02 100644
--- a/src/state/queries/useSuggestedStarterPacksQuery.ts
+++ b/src/state/queries/useSuggestedStarterPacksQuery.ts
@@ -20,8 +20,7 @@ export function useSuggestedStarterPacksQuery() {
return useQuery({
enabled: !!preferences,
- refetchOnWindowFocus: true,
- staleTime: STALE.MINUTES.ONE,
+ staleTime: STALE.MINUTES.THREE,
queryKey: createSuggestedStarterPacksQueryKey(),
async queryFn() {
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(