Merge remote-tracking branch 'origin/main' into explore/tabs

This commit is contained in:
Hailey
2025-04-07 16:33:21 -07:00
13 changed files with 345 additions and 192 deletions
+34
View File
@@ -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 (
<View
style={[
a.absolute,
a.inset_0,
a.pointer_events_none,
a.transition_opacity,
t.atoms.bg_contrast_25,
style,
{opacity: hover ? opacity : 0},
]}
/>
)
}
-1
View File
@@ -330,7 +330,6 @@ export type MetricEvents = {
| 'suggestedFeeds' | 'suggestedFeeds'
| 'suggestedStarterPacks' | 'suggestedStarterPacks'
| `feed:${FeedDescriptor}` | `feed:${FeedDescriptor}`
index: number
} }
'explore:module:searchButtonPress': { 'explore:module:searchButtonPress': {
module: 'suggestedAccounts' | 'suggestedFeeds' module: 'suggestedAccounts' | 'suggestedFeeds'
+110 -39
View File
@@ -7,6 +7,7 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {useGate} from '#/lib/statsig/statsig' import {useGate} from '#/lib/statsig/statsig'
@@ -22,9 +23,19 @@ import {
import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {useGetPopularFeedsQuery} from '#/state/queries/feed'
import {Nux, useNux} from '#/state/queries/nuxs' import {Nux, useNux} from '#/state/queries/nuxs'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery' import {
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery' createGetSuggestedFeedsQueryKey,
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery' 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 {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'
@@ -41,6 +52,7 @@ import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommenda
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics' import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos' import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
import {atoms as a, native, platform, useTheme, web} from '#/alf' import {atoms as a, native, platform, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Button} from '#/components/Button' 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'
@@ -49,9 +61,11 @@ import {type Props as IcoProps} from '#/components/icons/common'
import {type Props as SVGIconProps} 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 {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'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import * as ModuleHeader from './components/ModuleHeader' import * as ModuleHeader from './components/ModuleHeader'
import { import {
@@ -69,33 +83,26 @@ function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
onPress={item.onLoadMore} onPress={item.onLoadMore}
style={[a.relative, a.w_full]}> style={[a.relative, a.w_full]}>
{({hovered, pressed}) => ( {({hovered, pressed}) => (
<View <>
style={[ <SubtleHover hover={hovered || pressed} />
a.flex_1, <View
a.flex_row,
a.align_center,
a.justify_center,
a.px_lg,
a.py_md,
a.gap_sm,
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<Text
style={[ style={[
a.leading_snug, a.flex_1,
hovered ? t.atoms.text : t.atoms.text_contrast_medium, a.flex_row,
a.align_center,
a.justify_center,
a.px_lg,
a.py_md,
a.gap_sm,
]}> ]}>
{item.message} <Text style={[a.leading_snug]}>{item.message}</Text>
</Text> {item.isLoadingMore ? (
{item.isLoadingMore ? ( <Loader size="sm" />
<Loader size="sm" /> ) : (
) : ( <ChevronDownIcon size="sm" style={t.atoms.text_contrast_medium} />
<ChevronDownIcon )}
size="sm" </View>
style={hovered ? t.atoms.text : t.atoms.text_contrast_medium} </>
/>
)}
</View>
)} )}
</Button> </Button>
) )
@@ -112,6 +119,7 @@ type ExploreScreenItems =
title: string title: string
icon: React.ComponentType<SVGIconProps> icon: React.ComponentType<SVGIconProps>
iconSize?: IcoProps['size'] iconSize?: IcoProps['size']
bottomBorder?: boolean
searchButton?: { searchButton?: {
label: string label: string
metricsTag: MetricEvents['explore:module:searchButtonPress']['module'] metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
@@ -147,6 +155,10 @@ type ExploreScreenItems =
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
recId?: number recId?: number
} }
| {
type: 'profileEmpty'
key: 'profileEmpty'
}
| { | {
type: 'feed' type: 'feed'
key: string key: string
@@ -203,11 +215,12 @@ export function Explore({
const gate = useGate() const gate = useGate()
const guide = useProgressGuide('follow-10') const guide = useProgressGuide('follow-10')
const [selectedInterest, setSelectedInterest] = useState<string | null>(null) const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
// TODO always get at least 10 back // TODO always get at least 10 back TODO still
const { const {
data: suggestedUsers, data: suggestedUsers,
isLoading: suggestedUsersIsLoading, isLoading: suggestedUsersIsLoading,
error: suggestedUsersError, error: suggestedUsersError,
isRefetching: suggestedUsersIsRefetching,
} = useGetSuggestedUsersQuery({ } = useGetSuggestedUsersQuery({
category: selectedInterest, category: selectedInterest,
}) })
@@ -227,6 +240,7 @@ export function Explore({
data: suggestedSPs, data: suggestedSPs,
isLoading: isLoadingSuggestedSPs, isLoading: isLoadingSuggestedSPs,
error: suggestedSPsError, error: suggestedSPsError,
isRefetching: isRefetchingSuggestedSPs,
} = useSuggestedStarterPacksQuery() } = useSuggestedStarterPacksQuery()
const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds const isLoadingMoreFeeds = isFetchingNextFeedsPage && !isLoadingFeeds
@@ -262,6 +276,27 @@ export function Explore({
}, },
} = useFeedPreviews(suggestedFeeds?.feeds ?? []) } = 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 () => { const onLoadMoreFeedPreviews = useCallback(async () => {
if ( if (
isPendingFeedPreviews || isPendingFeedPreviews ||
@@ -305,7 +340,7 @@ export function Explore({
}, },
}) })
if (suggestedUsersIsLoading) { if (suggestedUsersIsLoading || suggestedUsersIsRefetching) {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
} else if (suggestedUsersError) { } else if (suggestedUsersError) {
i.push({ i.push({
@@ -333,14 +368,18 @@ export function Explore({
} }
if (profileItems.length === 0) { if (profileItems.length === 0) {
// no items! remove the header i.push({
i.pop() type: 'profileEmpty',
key: 'profileEmpty',
})
} else { } else {
i.push(...profileItems) i.push(...profileItems)
} }
} else { } else {
// no items! remove the header i.push({
i.pop() type: 'profileEmpty',
key: 'profileEmpty',
})
} }
} else { } else {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
@@ -352,6 +391,7 @@ export function Explore({
moderationOpts, moderationOpts,
suggestedUsers, suggestedUsers,
suggestedUsersIsLoading, suggestedUsersIsLoading,
suggestedUsersIsRefetching,
suggestedUsersError, suggestedUsersError,
]) ])
const suggestedFeedsModule = useMemo(() => { const suggestedFeedsModule = useMemo(() => {
@@ -466,7 +506,7 @@ export function Explore({
iconSize: 'xl', iconSize: 'xl',
}) })
if (isLoadingSuggestedSPs) { if (isLoadingSuggestedSPs || isRefetchingSuggestedSPs) {
Array.from({length: 3}).forEach((__, index) => Array.from({length: 3}).forEach((__, index) =>
i.push({ i.push({
type: 'starterPackSkeleton', type: 'starterPackSkeleton',
@@ -486,7 +526,13 @@ export function Explore({
}) })
} }
return i return i
}, [suggestedSPs, _, isLoadingSuggestedSPs, suggestedSPsError]) }, [
suggestedSPs,
_,
isLoadingSuggestedSPs,
suggestedSPsError,
isRefetchingSuggestedSPs,
])
const feedPreviewsModule = useMemo(() => { const feedPreviewsModule = useMemo(() => {
const i: ExploreScreenItems[] = [] const i: ExploreScreenItems[] = []
i.push(...feedPreviewSlices) i.push(...feedPreviewSlices)
@@ -520,6 +566,13 @@ export function Explore({
if (isNewUser) { if (isNewUser) {
i.push(...suggestedFollowsModule) i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule) i.push(...suggestedStarterPacksModule)
i.push({
type: 'header',
key: 'trending-topics-header',
title: _(msg`Trending topics`),
icon: Graph,
bottomBorder: true,
})
i.push(trendingTopicsModule) i.push(trendingTopicsModule)
} else { } else {
i.push(trendingTopicsModule) i.push(trendingTopicsModule)
@@ -533,6 +586,7 @@ export function Explore({
return i return i
}, [ }, [
_,
topBorder, topBorder,
isNewUser, isNewUser,
suggestedFollowsModule, suggestedFollowsModule,
@@ -564,7 +618,7 @@ export function Explore({
) )
case 'header': { case 'header': {
return ( return (
<ModuleHeader.Container> <ModuleHeader.Container bottomBorder={item.bottomBorder}>
<ModuleHeader.Icon icon={item.icon} size={item.iconSize} /> <ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
<ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText> <ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
{item.searchButton && ( {item.searchButton && (
@@ -623,6 +677,15 @@ export function Explore({
/> />
) )
} }
case 'profileEmpty': {
return (
<View style={[a.px_lg, a.pb_lg]}>
<Admonition>
<Trans>No results for "{selectedInterest}".</Trans>
</Admonition>
</View>
)
}
case 'feed': { case 'feed': {
return ( return (
<View <View
@@ -738,7 +801,13 @@ export function Explore({
return ( return (
<ModuleHeader.Container <ModuleHeader.Container
headerHeight={headerHeight} headerHeight={headerHeight}
style={[a.pt_xs, a.border_b, t.atoms.border_contrast_low]}> style={[
a.pt_xs,
t.atoms.border_contrast_low,
native(a.border_b),
]}>
{/* Very non-scientific way to avoid small gap on scroll */}
<View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
<ModuleHeader.FeedLink feed={item.feed}> <ModuleHeader.FeedLink feed={item.feed}>
<ModuleHeader.FeedAvatar feed={item.feed} /> <ModuleHeader.FeedAvatar feed={item.feed} />
<View style={[a.flex_1, a.gap_xs]}> <View style={[a.flex_1, a.gap_xs]}>
@@ -854,7 +923,7 @@ export function Explore({
} }
if (!alreadyReportedRef.current.has(module)) { if (!alreadyReportedRef.current.has(module)) {
alreadyReportedRef.current.set(module, 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} windowSize={9}
maxToRenderPerBatch={platform({ios: 5, default: 1})} maxToRenderPerBatch={platform({ios: 5, default: 1})}
updateCellsBatchingPeriod={40} updateCellsBatchingPeriod={40}
refreshing={isPTR}
onRefresh={onPTR}
/> />
) )
} }
@@ -18,7 +18,12 @@ export function Container({
style, style,
children, children,
headerHeight, headerHeight,
}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) { bottomBorder,
}: {
children: React.ReactNode
headerHeight?: number
bottomBorder?: boolean
} & ViewStyleProp) {
const t = useTheme() const t = useTheme()
return ( return (
<View <View
@@ -31,10 +36,9 @@ export function Container({
a.gap_sm, a.gap_sm,
t.atoms.bg, t.atoms.bg,
headerHeight && web({position: 'sticky', top: headerHeight}), headerHeight && web({position: 'sticky', top: headerHeight}),
bottomBorder && [a.border_b, t.atoms.border_contrast_low],
style, style,
]}> ]}>
{/* Very non-scientific way to avoid small gap on scroll */}
<View style={[a.absolute, a.inset_0, t.atoms.bg, {top: -2}]} />
{children} {children}
</View> </View>
) )
@@ -19,6 +19,7 @@ import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard' import {useStarterPackLink} from '#/components/StarterPack/StarterPackCard'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
@@ -48,75 +49,80 @@ export function StarterPackCard({
.map(item => item.subject) .map(item => item.subject)
return ( return (
<View <Link
style={[ to={link.to}
a.w_full, label={link.label}
a.p_lg, onHoverIn={link.precache}
a.gap_md, onPress={link.precache}>
a.border, {s => (
a.rounded_sm, <>
a.overflow_hidden, <SubtleHover hover={s.hovered || s.pressed} />
t.atoms.border_contrast_low,
]}>
<View aria-hidden style={[a.absolute, a.inset_0, a.z_40]}>
<Link
to={link.to}
label={link.label}
style={[a.absolute, a.inset_0]}
onHoverIn={link.precache}
onPress={link.precache}>
<View />
</Link>
</View>
<AvatarStack <View
profiles={profiles ?? []} style={[
numPending={profileCount} a.w_full,
total={view.list?.listItemCount} a.p_lg,
/> a.gap_md,
a.border,
a.rounded_sm,
a.overflow_hidden,
t.atoms.border_contrast_low,
]}>
<AvatarStack
profiles={profiles ?? []}
numPending={profileCount}
total={view.list?.listItemCount}
/>
<View <View
style={[ style={[
a.w_full, a.w_full,
a.flex_row, a.flex_row,
a.align_start, a.align_start,
a.gap_lg, a.gap_lg,
web({ web({
position: 'static', position: 'static',
zIndex: 'unset', zIndex: 'unset',
}), }),
]}> ]}>
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<Text <Text
emoji emoji
style={[a.text_md, a.font_bold, a.leading_snug]} style={[a.text_md, a.font_bold, a.leading_snug]}
numberOfLines={1}> numberOfLines={1}>
{view.record.name} {view.record.name}
</Text> </Text>
<Text <Text
emoji emoji
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]} style={[
numberOfLines={1}> a.text_sm,
{view.creator?.did === currentAccount?.did a.leading_snug,
? _(msg`By you`) t.atoms.text_contrast_medium,
: _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)} ]}
</Text> numberOfLines={1}>
</View> {view.creator?.did === currentAccount?.did
<Link ? _(msg`By you`)
to={link.to} : _(msg`By ${sanitizeHandle(view.creator.handle, '@')}`)}
label={link.label} </Text>
onHoverIn={link.precache} </View>
onPress={link.precache} <Link
variant="solid" to={link.to}
color="secondary" label={link.label}
size="small" onHoverIn={link.precache}
style={[a.z_50]}> onPress={link.precache}
<ButtonText> variant="solid"
<Trans>Open pack</Trans> color="secondary"
</ButtonText> size="small"
</Link> style={[a.z_50]}>
</View> <ButtonText>
</View> <Trans>Open pack</Trans>
</ButtonText>
</Link>
</View>
</View>
</>
)}
</Link>
) )
} }
@@ -40,14 +40,14 @@ export function ExploreInterestsCard() {
<> <>
<Prompt.Basic <Prompt.Basic
control={trendingPrompt} control={trendingPrompt}
title={_(msg`Your interests`)} title={_(msg`Dismiss interests`)}
description={_( description={_(
msg`You can adjust your interests at any time from your "Content and media" settings.`, msg`You can adjust your interests at any time from "Content and media" settings.`,
)} )}
confirmButtonCta={_( confirmButtonCta={_(
msg({ msg({
message: `Copy that!`, message: `OK`,
comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`, comment: `Confirm button text.`,
}), }),
)} )}
onConfirm={onConfirmClose} onConfirm={onConfirmClose}
@@ -17,6 +17,7 @@ import {atoms as a} from '#/alf'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog' import {boostInterests, Tabs} from '#/components/ProgressGuide/FollowDialog'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -133,10 +134,7 @@ let Tab = ({
a.py_sm, a.py_sm,
a.border, a.border,
active || hovered || pressed || focused active || hovered || pressed || focused
? [ ? [t.atoms.bg_contrast_25, t.atoms.border_contrast_medium]
t.atoms.bg_contrast_25,
{borderColor: t.atoms.bg_contrast_25.backgroundColor},
]
: [t.atoms.bg, t.atoms.border_contrast_low], : [t.atoms.bg, t.atoms.border_contrast_low],
]}> ]}>
<Text <Text
@@ -186,47 +184,52 @@ let SuggestedProfileCard = ({
{statsig: true}, {statsig: true},
) )
}}> }}>
<View {s => (
style={[ <>
a.w_full, <SubtleHover hover={s.hovered || s.pressed} />
a.py_lg, <View
a.px_lg, style={[
a.border_t, a.flex_1,
t.atoms.border_contrast_low, a.w_full,
a.flex_1, a.py_lg,
]}> a.px_lg,
<ProfileCard.Outer> a.border_t,
<ProfileCard.Header> t.atoms.border_contrast_low,
<ProfileCard.Avatar ]}>
profile={profile} <ProfileCard.Outer>
moderationOpts={moderationOpts} <ProfileCard.Header>
/> <ProfileCard.Avatar
<ProfileCard.NameAndHandle profile={profile}
profile={profile} moderationOpts={moderationOpts}
moderationOpts={moderationOpts} />
/> <ProfileCard.NameAndHandle
<ProfileCard.FollowButton profile={profile}
profile={profile} moderationOpts={moderationOpts}
moderationOpts={moderationOpts} />
withIcon={false} <ProfileCard.FollowButton
logContext="ExploreSuggestedAccounts" profile={profile}
onFollow={() => { moderationOpts={moderationOpts}
logger.metric( withIcon={false}
'suggestedUser:follow', logContext="ExploreSuggestedAccounts"
{ onFollow={() => {
logContext: 'Explore', logger.metric(
location: 'Card', 'suggestedUser:follow',
recId, {
position, logContext: 'Explore',
}, location: 'Card',
{statsig: true}, recId,
) position,
}} },
/> {statsig: true},
</ProfileCard.Header> )
<ProfileCard.Description profile={profile} numberOfLines={2} /> }}
</ProfileCard.Outer> />
</View> </ProfileCard.Header>
<ProfileCard.Description profile={profile} numberOfLines={2} />
</ProfileCard.Outer>
</View>
</>
)}
</ProfileCard.Link> </ProfileCard.Link>
) )
} }
@@ -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 {Flame_Stroke2_Corner1_Rounded as FlameIcon} from '#/components/icons/Flame'
import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending' import {Trending3_Stroke2_Corner1_Rounded as TrendingIcon} from '#/components/icons/Trending'
import {Link} from '#/components/Link' import {Link} from '#/components/Link'
import {SubtleHover} from '#/components/SubtleHover'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
const TOPIC_COUNT = 5 const TOPIC_COUNT = 5
@@ -28,10 +29,10 @@ export function ExploreTrendingTopics() {
} }
function Inner() { function Inner() {
const {data: trending, error, isLoading} = useGetTrendsQuery() const {data: trending, error, isLoading, isRefetching} = useGetTrendsQuery()
const noTopics = !isLoading && !error && !trending?.trends?.length const noTopics = !isLoading && !error && !trending?.trends?.length
return isLoading ? ( return isLoading || isRefetching ? (
Array.from({length: TOPIC_COUNT}).map((__, i) => ( Array.from({length: TOPIC_COUNT}).map((__, i) => (
<TrendingTopicRowSkeleton key={i} withPosts={i === 0} /> <TrendingTopicRowSkeleton key={i} withPosts={i === 0} />
)) ))
@@ -92,25 +93,23 @@ export function TrendRow({
PressableComponent={Pressable}> PressableComponent={Pressable}>
{({hovered, pressed}) => ( {({hovered, pressed}) => (
<> <>
<View <SubtleHover hover={hovered || pressed} />
style={[ <View style={[gutters, a.w_full, a.py_lg, a.flex_row, a.gap_2xs]}>
gutters,
a.w_full,
a.py_lg,
a.flex_row,
a.gap_2xs,
(hovered || pressed) && t.atoms.bg_contrast_25,
]}>
<View style={[a.flex_1, a.gap_xs]}> <View style={[a.flex_1, a.gap_xs]}>
<View style={[a.flex_row]}> <View style={[a.flex_row]}>
<Text <Text
style={[a.text_md, a.font_bold, a.leading_snug, {width: 20}]}> style={[
a.text_md,
a.font_bold,
a.leading_tight,
{width: 20},
]}>
<Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'> <Trans comment='The trending topic rank, i.e. "1. March Madness", "2. The Bachelor"'>
{rank}. {rank}.
</Trans> </Trans>
</Text> </Text>
<Text <Text
style={[a.text_md, a.font_bold, a.leading_snug]} style={[a.text_md, a.font_bold, a.leading_tight]}
numberOfLines={1}> numberOfLines={1}>
{trend.displayName} {trend.displayName}
</Text> </Text>
+53 -13
View File
@@ -34,6 +34,41 @@ const RQKEY_ROOT = 'feed-previews'
const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds] const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds]
const LIMIT = 8 // sliced to 6, overfetch to account for moderation const LIMIT = 8 // sliced to 6, overfetch to account for moderation
const PINNED_POST_URIS: Record<string, boolean> = {
// 📰 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 = export type FeedPreviewItem =
| { | {
@@ -181,19 +216,24 @@ export function useFeedPreviews(
feedContext: item.feedContext, feedContext: item.feedContext,
reason: item.reason, reason: item.reason,
feedPostUri: item.feedPostUri, feedPostUri: item.feedPostUri,
items: item.items.slice(0, 6).map((subItem, i) => { items: item.items
const feedPostSliceItem: FeedPostSliceItem = { .slice(0, 6)
_reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`, .filter(subItem => {
uri: subItem.post.uri, return !PINNED_POST_URIS[subItem.post.uri]
post: subItem.post, })
record: subItem.record, .map((subItem, i) => {
moderation: moderations[i], const feedPostSliceItem: FeedPostSliceItem = {
parentAuthor: subItem.parentAuthor, _reactKey: `${item._reactKey}-${i}-${subItem.post.uri}`,
isParentBlocked: subItem.isParentBlocked, uri: subItem.post.uri,
isParentNotFound: subItem.isParentNotFound, post: subItem.post,
} record: subItem.record,
return feedPostSliceItem moderation: moderations[i],
}), parentAuthor: subItem.parentAuthor,
isParentBlocked: subItem.isParentBlocked,
isParentNotFound: subItem.isParentNotFound,
}
return feedPostSliceItem
}),
} }
if (slice.isIncompleteThread && slice.items.length >= 3) { if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2 const beforeLast = slice.items.length - 2
@@ -20,8 +20,7 @@ export function useGetSuggestedFeedsQuery() {
return useQuery({ return useQuery({
enabled: !!preferences, enabled: !!preferences,
refetchOnWindowFocus: true, staleTime: STALE.MINUTES.THREE,
staleTime: STALE.MINUTES.ONE,
queryKey: createGetSuggestedFeedsQueryKey(), queryKey: createGetSuggestedFeedsQueryKey(),
queryFn: async () => { queryFn: async () => {
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
@@ -27,14 +27,14 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
return useQuery({ return useQuery({
enabled: !!preferences, enabled: !!preferences,
refetchOnWindowFocus: true, staleTime: STALE.MINUTES.THREE,
staleTime: STALE.MINUTES.ONE,
queryKey: createGetSuggestedUsersQueryKey(props), queryKey: createGetSuggestedUsersQueryKey(props),
queryFn: async () => { queryFn: async () => {
const contentLangs = getContentLanguages().join(',') const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers( const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
{ {
category: props.category ?? undefined, category: props.category ?? undefined,
limit: 10,
}, },
{ {
headers: { headers: {
@@ -25,7 +25,6 @@ export function useGetTrendsQuery() {
return useQuery({ return useQuery({
enabled: !!preferences, enabled: !!preferences,
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.THREE, staleTime: STALE.MINUTES.THREE,
queryKey: createGetTrendsQueryKey(), queryKey: createGetTrendsQueryKey(),
queryFn: async () => { queryFn: async () => {
@@ -20,8 +20,7 @@ export function useSuggestedStarterPacksQuery() {
return useQuery({ return useQuery({
enabled: !!preferences, enabled: !!preferences,
refetchOnWindowFocus: true, staleTime: STALE.MINUTES.THREE,
staleTime: STALE.MINUTES.ONE,
queryKey: createSuggestedStarterPacksQueryKey(), queryKey: createSuggestedStarterPacksQueryKey(),
async queryFn() { async queryFn() {
const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks( const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks(