tweak ordering of explore components (#8432)

* tweak ordering of explore components

* use better feed suggestions, metrics

* tweak default limit

* tweak

* remove unnecessary if
This commit is contained in:
hailey
2025-05-30 15:29:00 -07:00
committed by GitHub
parent ce7b9dc430
commit 45a3e6c0d5
4 changed files with 188 additions and 96 deletions
+2 -2
View File
@@ -21,8 +21,7 @@ import {
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import {useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import { import {
Button, Button,
ButtonIcon, ButtonIcon,
@@ -40,6 +39,7 @@ import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from './icons/Trash'
type Props = { type Props = {
view: AppBskyFeedDefs.GeneratorView view: AppBskyFeedDefs.GeneratorView
onPress?: () => void
} }
export function Default(props: Props) { export function Default(props: Props) {
+6
View File
@@ -151,6 +151,12 @@ export type MetricEvents = {
'feed:share': { 'feed:share': {
feedUrl: string feedUrl: string
} }
'feed:suggestion:seen': {
feedUrl: string
}
'feed:suggestion:press': {
feedUrl: string
}
'discover:showMore': { 'discover:showMore': {
feedContext: string feedContext: string
} }
+179 -93
View File
@@ -10,7 +10,6 @@ import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import * as bcp47Match from 'bcp-47-match' import * as bcp47Match from 'bcp-47-match'
import {useGate} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -35,7 +34,6 @@ import {
createSuggestedStarterPacksQueryKey, createSuggestedStarterPacksQueryKey,
useSuggestedStarterPacksQuery, useSuggestedStarterPacksQuery,
} from '#/state/queries/useSuggestedStarterPacksQuery' } from '#/state/queries/useSuggestedStarterPacksQuery'
import {useProgressGuide} from '#/state/shell/progress-guide'
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed' import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
import {PostFeedItem} from '#/view/com/posts/PostFeedItem' import {PostFeedItem} from '#/view/com/posts/PostFeedItem'
import {ViewFullThread} from '#/view/com/posts/ViewFullThread' import {ViewFullThread} from '#/view/com/posts/ViewFullThread'
@@ -61,11 +59,12 @@ import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron' import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {type Props as IcoProps} from '#/components/icons/common' import {
import {type Props as SVGIconProps} from '#/components/icons/common' type Props as IcoProps,
type Props as SVGIconProps,
} from '#/components/icons/common'
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle' import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
import {StarterPack} from '#/components/icons/StarterPack' 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 {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
@@ -216,8 +215,6 @@ export function Explore({
const t = useTheme() const t = useTheme()
const {data: preferences, error: preferencesError} = usePreferencesQuery() const {data: preferences, error: preferencesError} = usePreferencesQuery()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const gate = useGate()
const guide = useProgressGuide('follow-10')
const [selectedInterest, setSelectedInterest] = useState<string | null>(null) const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
/* /*
@@ -284,9 +281,10 @@ export function Explore({
hasPressedLoadMoreFeeds, hasPressedLoadMoreFeeds,
]) ])
const {data: suggestedFeeds} = useGetSuggestedFeedsQuery({ const {data: suggestedFeeds, error: suggestedFeedsError} =
enabled: useFullExperience, useGetSuggestedFeedsQuery({
}) enabled: useFullExperience,
})
const { const {
data: feedPreviewSlices, data: feedPreviewSlices,
query: { query: {
@@ -442,13 +440,11 @@ export function Explore({
}, },
}) })
if (feeds && preferences) { if (useFullExperience) {
// Currently the responses contain duplicate items. if (suggestedFeeds && preferences) {
// Needs to be fixed on backend, but let's dedupe to be safe. let seen = new Set()
let seen = new Set() const feedItems: ExploreScreenItems[] = []
const feedItems: ExploreScreenItems[] = [] for (const feed of suggestedFeeds.feeds) {
for (const page of feeds.pages) {
for (const feed of page.feeds) {
if (!seen.has(feed.uri)) { if (!seen.has(feed.uri)) {
seen.add(feed.uri) seen.add(feed.uri)
feedItems.push({ feedItems.push({
@@ -458,78 +454,172 @@ export function Explore({
}) })
} }
} }
}
// feeds errors can occur during pagination, so feeds is truthy // feeds errors can occur during pagination, so feeds is truthy
if (feedsError) { if (suggestedFeedsError) {
i.push({ i.push({
type: 'error', type: 'error',
key: 'feedsError', key: 'feedsError',
message: _(msg`Failed to load suggested feeds`), message: _(msg`Failed to load suggested feeds`),
error: cleanError(feedsError), error: cleanError(feedsError),
}) })
} else if (preferencesError) { } else if (preferencesError) {
i.push({ i.push({
type: 'error', type: 'error',
key: 'preferencesError', key: 'preferencesError',
message: _(msg`Failed to load feeds preferences`), message: _(msg`Failed to load feeds preferences`),
error: cleanError(preferencesError), error: cleanError(preferencesError),
}) })
} else {
if (feedItems.length === 0) {
if (!hasNextFeedsPage) {
i.pop()
}
} else { } else {
// This query doesn't follow the limit very well, so the first press of the if (feedItems.length === 0) {
// load more button just unslices the array back to ~10 items i.pop()
if (!hasPressedLoadMoreFeeds) {
i.push(...feedItems.slice(0, 3))
} else { } else {
i.push(...feedItems) // This query doesn't follow the limit very well, so the first press of the
// load more button just unslices the array back to ~10 items
if (!hasPressedLoadMoreFeeds) {
i.push(...feedItems.slice(0, 6))
} else {
i.push(...feedItems)
}
for (const [index, item] of feedItems.entries()) {
if (item.type !== 'feed') {
continue
}
// don't log the ones we've already sent
if (hasPressedLoadMoreFeeds && index < 6) {
continue
}
logger.metric(
'feed:suggestion:seen',
{feedUrl: item.feed.uri},
{statsig: false},
)
}
}
if (!hasPressedLoadMoreFeeds) {
i.push({
type: 'loadMore',
key: 'loadMoreFeeds',
message: _(msg`Load more suggested feeds`),
isLoadingMore: isLoadingMoreFeeds,
onLoadMore: onLoadMoreFeeds,
})
} }
} }
if (hasNextFeedsPage) { } else {
if (feedsError) {
i.push({ i.push({
type: 'loadMore', type: 'error',
key: 'loadMoreFeeds', key: 'feedsError',
message: _(msg`Load more suggested feeds`), message: _(msg`Failed to load suggested feeds`),
isLoadingMore: isLoadingMoreFeeds, error: cleanError(feedsError),
onLoadMore: onLoadMoreFeeds,
}) })
} else if (preferencesError) {
i.push({
type: 'error',
key: 'preferencesError',
message: _(msg`Failed to load feeds preferences`),
error: cleanError(preferencesError),
})
} else {
i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'})
} }
} }
} else { } else {
if (feedsError) { if (feeds && preferences) {
i.push({ // Currently the responses contain duplicate items.
type: 'error', // Needs to be fixed on backend, but let's dedupe to be safe.
key: 'feedsError', let seen = new Set()
message: _(msg`Failed to load suggested feeds`), const feedItems: ExploreScreenItems[] = []
error: cleanError(feedsError), for (const page of feeds.pages) {
}) for (const feed of page.feeds) {
} else if (preferencesError) { if (!seen.has(feed.uri)) {
i.push({ seen.add(feed.uri)
type: 'error', feedItems.push({
key: 'preferencesError', type: 'feed',
message: _(msg`Failed to load feeds preferences`), key: feed.uri,
error: cleanError(preferencesError), feed,
}) })
}
}
}
// feeds errors can occur during pagination, so feeds is truthy
if (feedsError) {
i.push({
type: 'error',
key: 'feedsError',
message: _(msg`Failed to load suggested feeds`),
error: cleanError(feedsError),
})
} else if (preferencesError) {
i.push({
type: 'error',
key: 'preferencesError',
message: _(msg`Failed to load feeds preferences`),
error: cleanError(preferencesError),
})
} else {
if (feedItems.length === 0) {
if (!hasNextFeedsPage) {
i.pop()
}
} else {
// This query doesn't follow the limit very well, so the first press of the
// load more button just unslices the array back to ~10 items
if (!hasPressedLoadMoreFeeds) {
i.push(...feedItems.slice(0, 3))
} else {
i.push(...feedItems)
}
}
if (hasNextFeedsPage) {
i.push({
type: 'loadMore',
key: 'loadMoreFeeds',
message: _(msg`Load more suggested feeds`),
isLoadingMore: isLoadingMoreFeeds,
onLoadMore: onLoadMoreFeeds,
})
}
}
} else { } else {
i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'}) if (feedsError) {
i.push({
type: 'error',
key: 'feedsError',
message: _(msg`Failed to load suggested feeds`),
error: cleanError(feedsError),
})
} else if (preferencesError) {
i.push({
type: 'error',
key: 'preferencesError',
message: _(msg`Failed to load feeds preferences`),
error: cleanError(preferencesError),
})
} else {
i.push({type: 'feedPlaceholder', key: 'feedPlaceholder'})
}
} }
} }
return i return i
}, [ }, [
feeds,
_, _,
useFullExperience,
suggestedFeeds,
preferences,
suggestedFeedsError,
preferencesError,
feedsError, feedsError,
hasNextFeedsPage, hasNextFeedsPage,
hasPressedLoadMoreFeeds, hasPressedLoadMoreFeeds,
isLoadingMoreFeeds, isLoadingMoreFeeds,
onLoadMoreFeeds, onLoadMoreFeeds,
preferences, feeds,
preferencesError,
]) ])
const suggestedStarterPacksModule = useMemo(() => { const suggestedStarterPacksModule = useMemo(() => {
const i: ExploreScreenItems[] = [] const i: ExploreScreenItems[] = []
i.push({ i.push({
@@ -589,7 +679,6 @@ export function Explore({
] ]
}, [showInterestsNux]) }, [showInterestsNux])
const isNewUser = guide?.guide === 'follow-10' && !guide.isComplete
const items = useMemo<ExploreScreenItems[]>(() => { const items = useMemo<ExploreScreenItems[]>(() => {
const i: ExploreScreenItems[] = [] const i: ExploreScreenItems[] = []
@@ -599,25 +688,10 @@ export function Explore({
i.push(...interestsNuxModule) i.push(...interestsNuxModule)
if (useFullExperience) { if (useFullExperience) {
if (isNewUser) { i.push(trendingTopicsModule)
i.push(...suggestedFollowsModule) i.push(...suggestedFeedsModule)
i.push(...suggestedStarterPacksModule) i.push(...suggestedFollowsModule)
i.push({ i.push(...suggestedStarterPacksModule)
type: 'header',
key: 'trending-topics-header',
title: _(msg`Trending topics`),
icon: Graph,
bottomBorder: true,
})
i.push(trendingTopicsModule)
} else {
i.push(trendingTopicsModule)
i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule)
}
if (gate('explore_show_suggested_feeds')) {
i.push(...suggestedFeedsModule)
}
i.push(...feedPreviewsModule) i.push(...feedPreviewsModule)
} else { } else {
i.push(...suggestedFollowsModule) i.push(...suggestedFollowsModule)
@@ -625,16 +699,13 @@ export function Explore({
return i return i
}, [ }, [
_,
topBorder, topBorder,
isNewUser,
suggestedFollowsModule, suggestedFollowsModule,
suggestedStarterPacksModule, suggestedStarterPacksModule,
suggestedFeedsModule, suggestedFeedsModule,
trendingTopicsModule, trendingTopicsModule,
feedPreviewsModule, feedPreviewsModule,
interestsNuxModule, interestsNuxModule,
gate,
useFullExperience, useFullExperience,
]) ])
@@ -731,7 +802,17 @@ export function Explore({
a.px_lg, a.px_lg,
a.py_lg, a.py_lg,
]}> ]}>
<FeedCard.Default view={item.feed} /> <FeedCard.Default
view={item.feed}
onPress={() => {
if (!useFullExperience) {
return
}
logger.metric('feed:suggestion:press', {
feedUrl: item.feed.uri,
})
}}
/>
</View> </View>
) )
} }
@@ -918,11 +999,16 @@ export function Explore({
} }
}, },
[ [
t, t.atoms.border_contrast_low,
t.atoms.bg_contrast_25,
t.atoms.text_contrast_medium,
t.atoms.bg,
t.palette.negative_400,
focusSearchInput, focusSearchInput,
moderationOpts,
selectedInterest, selectedInterest,
moderationOpts,
interestsDisplayNames, interestsDisplayNames,
useFullExperience,
_, _,
fetchNextPageFeedPreviews, fetchNextPageFeedPreviews,
], ],
@@ -9,7 +9,7 @@ import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
export const DEFAULT_LIMIT = 5 export const DEFAULT_LIMIT = 15
export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds'] export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds']