From e46b78eb85bf582d888942a6c00ef2d32e8ef23e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Apr 2025 18:06:28 -0500 Subject: [PATCH 1/2] Remove unused index to fix lint (#8148) --- src/logger/metrics.ts | 1 - src/screens/Search/Explore.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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..61ec367850 100644 --- a/src/screens/Search/Explore.tsx +++ b/src/screens/Search/Explore.tsx @@ -854,7 +854,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}) } }, []) From 4013855c10339266b045392469ac132e88c560c4 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Apr 2025 18:32:30 -0500 Subject: [PATCH 2/2] [Explore] Small fixes (#8145) * Hover on sugg account * Add subtle hover to all components except feed * Use skeleton states for refetch on focus * Empty results state for sugg users * Filter out pinned posts from feed previews * Add trending header if not top module * Tighten up spacing * Fetch 10 profiles * Update interests copy * Remove refetch on focus * Add PTR * use a map * Update src/screens/Search/modules/ExploreInterestsCard.tsx * fix web double border --------- Co-authored-by: Hailey --- src/components/SubtleHover.tsx | 34 ++++ src/screens/Search/Explore.tsx | 147 +++++++++++++----- .../Search/components/ModuleHeader.tsx | 10 +- .../Search/components/StarterPackCard.tsx | 140 +++++++++-------- .../Search/modules/ExploreInterestsCard.tsx | 8 +- .../modules/ExploreSuggestedAccounts.tsx | 93 +++++------ .../Search/modules/ExploreTrendingTopics.tsx | 25 ++- src/state/queries/explore-feed-previews.tsx | 66 ++++++-- .../trending/useGetSuggestedFeedsQuery.ts | 3 +- .../trending/useGetSuggestedUsersQuery.ts | 4 +- .../queries/trending/useGetTrendsQuery.ts | 1 - .../queries/useSuggestedStarterPacksQuery.ts | 3 +- 12 files changed, 344 insertions(+), 190 deletions(-) create mode 100644 src/components/SubtleHover.tsx 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/screens/Search/Explore.tsx b/src/screens/Search/Explore.tsx index 61ec367850..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 */} + @@ -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(