Update presentation of trending feeds (#11206)

This commit is contained in:
DS Boyce
2026-07-22 14:35:45 -07:00
committed by GitHub
parent c71a4f8360
commit 8ca8176eb1
6 changed files with 252 additions and 170 deletions
-15
View File
@@ -1047,11 +1047,6 @@
"count": 1 "count": 1
} }
}, },
"src/screens/Profile/components/ProfileFeedHeader.tsx": {
"typescript/no-misused-promises": {
"count": 5
}
},
"src/screens/ProfileList/FeedSection.tsx": { "src/screens/ProfileList/FeedSection.tsx": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
@@ -1398,11 +1393,6 @@
"count": 2 "count": 2
} }
}, },
"src/state/queries/feed.ts": {
"typescript/no-floating-promises": {
"count": 1
}
},
"src/state/queries/handle.ts": { "src/state/queries/handle.ts": {
"typescript/no-floating-promises": { "typescript/no-floating-promises": {
"count": 1 "count": 1
@@ -1748,11 +1738,6 @@
"count": 2 "count": 2
} }
}, },
"src/view/com/posts/PostFeed.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"src/view/com/posts/PostFeedErrorMessage.tsx": { "src/view/com/posts/PostFeedErrorMessage.tsx": {
"typescript/no-explicit-any": { "typescript/no-explicit-any": {
"count": 1 "count": 1
+3
View File
@@ -146,6 +146,9 @@ export const BSKY_FEED_OWNER_DIDS = [
'did:plc:q6gjnaw2blty4crticxkmujt', 'did:plc:q6gjnaw2blty4crticxkmujt',
] ]
export const TRENDING_DID = 'did:plc:qrz3lhbyuxbeilrc6nekdqme'
export const TRENDING_HANDLE = 'trending.bsky.app'
export const DISCOVER_FEED_URI = export const DISCOVER_FEED_URI =
'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot' 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot'
export const VIDEO_FEED_URI = export const VIDEO_FEED_URI =
+15 -14
View File
@@ -1,13 +1,12 @@
import {useCallback, useEffect, useMemo, useState} from 'react' import {useCallback, useEffect, useMemo, useState} from 'react'
import {useAnimatedRef} from 'react-native-reanimated' import {useAnimatedRef} from 'react-native-reanimated'
import {AppBskyFeedDefs} from '@atproto/api' import {AppBskyFeedDefs} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {useIsFocused} from '@react-navigation/native' import {useIsFocused} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {VIDEO_FEED_URIS} from '#/lib/constants' import {TRENDING_DID, TRENDING_HANDLE, VIDEO_FEED_URIS} from '#/lib/constants'
import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {type CommonNavigatorParams} from '#/lib/routes/types' import {type CommonNavigatorParams} from '#/lib/routes/types'
@@ -52,7 +51,7 @@ export function ProfileFeedScreen(props: Props) {
const feedParams: FeedParams | undefined = props.route.params.feedCacheKey const feedParams: FeedParams | undefined = props.route.params.feedCacheKey
? {feedCacheKey: props.route.params.feedCacheKey} ? {feedCacheKey: props.route.params.feedCacheKey}
: undefined : undefined
const {_} = useLingui() const {t: l} = useLingui()
const uri = useMemo( const uri = useMemo(
() => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey), () => makeRecordUri(handleOrDid, 'app.bsky.feed.generator', rkey),
@@ -70,7 +69,7 @@ export function ProfileFeedScreen(props: Props) {
<Layout.Screen testID="profileFeedScreenError"> <Layout.Screen testID="profileFeedScreenError">
<ErrorScreen <ErrorScreen
showHeader showHeader
title={_(msg`Could not load feed`)} title={l`Could not load feed`}
message={cleanError(error)} message={cleanError(error)}
onPressTryAgain={() => void refetch()} onPressTryAgain={() => void refetch()}
/> />
@@ -131,7 +130,7 @@ export function ProfileFeedScreenInner({
feedInfo: FeedSourceFeedInfo feedInfo: FeedSourceFeedInfo
feedParams: FeedParams | undefined feedParams: FeedParams | undefined
}) { }) {
const {_} = useLingui() const {t: l} = useLingui()
const {hasSession} = useSession() const {hasSession} = useSession()
const {openComposer} = useOpenComposer() const {openComposer} = useOpenComposer()
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
@@ -168,10 +167,10 @@ export function ProfileFeedScreenInner({
<EmptyState <EmptyState
icon={HashtagWideIcon} icon={HashtagWideIcon}
iconSize="2xl" iconSize="2xl"
message={_(msg`This feed is empty.`)} message={l`This feed is empty.`}
/> />
) )
}, [_]) }, [l])
const isVideoFeed = useMemo(() => { const isVideoFeed = useMemo(() => {
const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri) const isBskyVideoFeed = VIDEO_FEED_URIS.includes(feedInfo.uri)
@@ -181,13 +180,17 @@ export function ProfileFeedScreenInner({
return IS_NATIVE && _isVideoFeed return IS_NATIVE && _isVideoFeed
}, [feedInfo]) }, [feedInfo])
const isTrending =
feedInfo.creatorDid.toLowerCase() === TRENDING_DID ||
feedInfo.creatorHandle.toLowerCase() === TRENDING_HANDLE
return ( return (
<> <>
<ProfileFeedHeader info={feedInfo} /> <ProfileFeedHeader info={feedInfo} isTrending={isTrending} />
<FeedFeedbackProvider value={feedFeedback}> <FeedFeedbackProvider value={feedFeedback}>
<PostFeed <PostFeed
enabled enabled
description={isTrending ? feedInfo.description : undefined}
feed={feed} feed={feed}
feedParams={feedParams} feedParams={feedParams}
pollInterval={60e3} pollInterval={60e3}
@@ -199,22 +202,20 @@ export function ProfileFeedScreenInner({
isVideoFeed={isVideoFeed} isVideoFeed={isVideoFeed}
/> />
</FeedFeedbackProvider> </FeedFeedbackProvider>
{(isScrolledDown || hasNew) && ( {(isScrolledDown || hasNew) && (
<LoadLatestBtn <LoadLatestBtn
onPress={onScrollToTop} onPress={onScrollToTop}
label={_(msg`Load new posts`)} label={l`Load new posts`}
showIndicator={hasNew} showIndicator={hasNew}
/> />
)} )}
{hasSession && ( {hasSession && (
<FAB <FAB
testID="composeFAB" testID="composeFAB"
onPress={() => openComposer({logContext: 'Fab'})} onPress={() => openComposer({logContext: 'Fab'})}
icon={<EditBigIcon size="lg" fill={t.palette.white} />} icon={<EditBigIcon size="lg" fill={t.palette.white} />}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`New post`)} accessibilityLabel={l`New post`}
accessibilityHint="" accessibilityHint=""
/> />
)} )}
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {Plural, Trans, useLingui} from '@lingui/react/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {TRENDING_HANDLE} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links' import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
@@ -24,20 +25,20 @@ import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ShareIcon} from '#/components/icons/ArrowOutOfBox'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
import { import {
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled, Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilledIcon,
Heart2_Stroke2_Corner0_Rounded as Heart, Heart2_Stroke2_Corner0_Rounded as HeartIcon,
} from '#/components/icons/Heart2' } from '#/components/icons/Heart2'
import { import {
Pin_Filled_Corner0_Rounded as PinFilled, Pin_Filled_Corner0_Rounded as PinFilledIcon,
Pin_Stroke2_Corner0_Rounded as Pin, Pin_Stroke2_Corner0_Rounded as PinIcon,
} from '#/components/icons/Pin' } from '#/components/icons/Pin'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import * as Menu from '#/components/Menu' import * as Menu from '#/components/Menu'
@@ -74,14 +75,20 @@ export function ProfileFeedHeaderSkeleton() {
width: 34, width: 34,
}, },
]}> ]}>
<Pin size="lg" fill={t.atoms.text_contrast_low.color} /> <PinIcon size="lg" fill={t.atoms.text_contrast_low.color} />
</View> </View>
</Layout.Header.Slot> </Layout.Header.Slot>
</Layout.Header.Outer> </Layout.Header.Outer>
) )
} }
export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { export function ProfileFeedHeader({
info,
isTrending,
}: {
info: FeedSourceFeedInfo
isTrending: boolean
}) {
const t = useTheme() const t = useTheme()
const {t: l, i18n} = useLingui() const {t: l, i18n} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
@@ -188,105 +195,143 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
<Layout.Header.Outer> <Layout.Header.Outer>
<Layout.Header.BackButton /> <Layout.Header.BackButton />
<Layout.Header.Content align="left"> <Layout.Header.Content align="left">
<Button {isTrending ? (
label={l`Open feed info screen`} <View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
style={[ <View style={[a.flex_1]}>
a.justify_start, <Text
{
paddingVertical: IS_WEB ? 2 : 4,
paddingRight: 8,
},
]}
onPress={() => {
playHaptic()
infoControl.open()
}}>
{({hovered, pressed}) => (
<>
<View
style={[ style={[
a.absolute, a.text_md,
a.inset_0, a.font_bold,
a.rounded_sm, a.leading_snug,
a.transition_all, gtMobile && a.text_lg,
t.atoms.bg_contrast_25,
{
opacity: 0,
left: IS_WEB ? -2 : -4,
right: 0,
},
pressed && {
opacity: 1,
},
hovered && {
opacity: 1,
transform: [{scaleX: 1.01}, {scaleY: 1.1}],
},
]} ]}
/> numberOfLines={2}
emoji>
{info.displayName}
</Text>
</View>
<Button
label={l`Open feed info screen`}
size="medium"
shape="round"
color="secondary"
variant="ghost"
onPress={() => {
playHaptic()
infoControl.open()
}}>
<ButtonIcon icon={EllipsisIcon} />
</Button>
</View>
) : (
<Button
label={l`Open feed info screen`}
style={[
a.justify_start,
{
paddingVertical: IS_WEB ? 2 : 4,
paddingRight: 8,
},
]}
onPress={() => {
playHaptic()
infoControl.open()
}}>
{({hovered, pressed}) => (
<>
<View
style={[
a.absolute,
a.inset_0,
a.rounded_sm,
a.transition_all,
t.atoms.bg_contrast_25,
{
opacity: 0,
left: IS_WEB ? -2 : -4,
right: 0,
},
pressed && {
opacity: 1,
},
hovered && {
opacity: 1,
transform: [{scaleX: 1.01}, {scaleY: 1.1}],
},
]}
/>
<View <View
style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}> style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
{info.avatar && ( {info.avatar && (
<UserAvatar size={36} type="algo" avatar={info.avatar} /> <UserAvatar
)} size={36}
type="algo"
avatar={info.avatar}
/>
)}
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
<Text
style={[
a.text_md,
a.font_bold,
a.leading_snug,
gtMobile && a.text_lg,
]}
numberOfLines={2}
emoji>
{info.displayName}
</Text>
<View style={[a.flex_row, {gap: 6}]}>
<Text <Text
style={[ style={[
a.flex_shrink, a.text_md,
a.text_sm, a.font_bold,
a.leading_snug, a.leading_snug,
t.atoms.text_contrast_medium, gtMobile && a.text_lg,
]} ]}
numberOfLines={1}> numberOfLines={2}
{sanitizeHandle(info.creatorHandle, '@')} emoji>
{info.displayName}
</Text> </Text>
<View style={[a.flex_row, a.align_center, {gap: 2}]}> <View style={[a.flex_row, a.gap_2xs]}>
<HeartFilled
size="xs"
fill={
likeUri
? t.palette.pink
: t.atoms.text_contrast_low.color
}
/>
<Text <Text
style={[ style={[
a.flex_shrink,
a.text_sm, a.text_sm,
a.leading_snug, a.leading_snug,
t.atoms.text_contrast_medium, t.atoms.text_contrast_high,
]} ]}
numberOfLines={1}> numberOfLines={1}>
{formatCount(i18n, likeCount)} {sanitizeHandle(info.creatorHandle, '@')}
</Text> </Text>
{likeCount > 0 ? (
<View
style={[a.flex_row, a.align_center, {gap: 2}]}>
<HeartFilledIcon
size="xs"
fill={
likeUri
? t.palette.pink
: t.atoms.text_contrast_low.color
}
style={[{width: 14, height: 14}]}
/>
<Text
style={[
a.text_sm,
a.leading_snug,
t.atoms.text_contrast_high,
]}
numberOfLines={1}>
{formatCount(i18n, likeCount)}
</Text>
</View>
) : null}
</View> </View>
</View> </View>
</View>
<Ellipsis <EllipsisIcon
size="md" size="md"
fill={t.atoms.text_contrast_low.color} fill={t.atoms.text_contrast_high.color}
/> />
</View> </View>
</> </>
)} )}
</Button> </Button>
)}
</Layout.Header.Content> </Layout.Header.Content>
{hasSession && ( {!isTrending && hasSession ? (
<Layout.Header.Slot> <Layout.Header.Slot>
{isPinned ? ( {isPinned ? (
<Menu.Root> <Menu.Root>
@@ -300,7 +345,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
variant="ghost" variant="ghost"
shape="square" shape="square"
color="secondary"> color="secondary">
<PinFilled size="lg" fill={t.palette.primary_500} /> <PinFilledIcon
size="lg"
fill={t.palette.primary_500}
/>
</Button> </Button>
) )
}} }}
@@ -310,23 +358,23 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
<Menu.Item <Menu.Item
disabled={isFeedStateChangePending} disabled={isFeedStateChangePending}
label={l`Unpin from home`} label={l`Unpin from home`}
onPress={onTogglePinned}> onPress={() => void onTogglePinned()}>
<Menu.ItemText>{l`Unpin from home`}</Menu.ItemText> <Menu.ItemText>{l`Unpin from home`}</Menu.ItemText>
<Menu.ItemIcon icon={X} position="right" /> <Menu.ItemIcon icon={XIcon} position="right" />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
disabled={isFeedStateChangePending} disabled={isFeedStateChangePending}
label={ label={
isSaved ? l`Remove from my feeds` : l`Save to my feeds` isSaved ? l`Remove from my feeds` : l`Save to my feeds`
} }
onPress={onToggleSaved}> onPress={() => void onToggleSaved()}>
<Menu.ItemText> <Menu.ItemText>
{isSaved {isSaved
? l`Remove from my feeds` ? l`Remove from my feeds`
: l`Save to my feeds`} : l`Save to my feeds`}
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemIcon <Menu.ItemIcon
icon={isSaved ? Trash : Plus} icon={isSaved ? TrashIcon : PlusIcon}
position="right" position="right"
/> />
</Menu.Item> </Menu.Item>
@@ -339,12 +387,12 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
variant="ghost" variant="ghost"
shape="square" shape="square"
color="secondary" color="secondary"
onPress={onTogglePinned}> onPress={() => void onTogglePinned()}>
<ButtonIcon icon={Pin} size="lg" /> <ButtonIcon icon={PinIcon} size="lg" />
</Button> </Button>
)} )}
</Layout.Header.Slot> </Layout.Header.Slot>
)} ) : null}
</Layout.Header.Outer> </Layout.Header.Outer>
</Layout.Center> </Layout.Center>
<Dialog.Outer control={infoControl}> <Dialog.Outer control={infoControl}>
@@ -358,7 +406,8 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
setLikeUri={setLikeUri} setLikeUri={setLikeUri}
likeCount={likeCount} likeCount={likeCount}
isPinned={isPinned} isPinned={isPinned}
onTogglePinned={onTogglePinned} isTrending={isTrending}
onTogglePinned={() => void onTogglePinned()}
isFeedStateChangePending={isFeedStateChangePending} isFeedStateChangePending={isFeedStateChangePending}
/> />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
@@ -373,6 +422,7 @@ function DialogInner({
setLikeUri, setLikeUri,
likeCount, likeCount,
isPinned, isPinned,
isTrending,
onTogglePinned, onTogglePinned,
isFeedStateChangePending, isFeedStateChangePending,
}: { }: {
@@ -381,6 +431,7 @@ function DialogInner({
setLikeUri: (uri: string) => void setLikeUri: (uri: string) => void
likeCount: number likeCount: number
isPinned: boolean isPinned: boolean
isTrending: boolean
onTogglePinned: () => void onTogglePinned: () => void
isFeedStateChangePending: boolean isFeedStateChangePending: boolean
}) { }) {
@@ -459,7 +510,9 @@ function DialogInner({
style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]} style={[a.text_sm, a.underline, t.atoms.text_contrast_medium]}
numberOfLines={1} numberOfLines={1}
onPress={() => control.close()}> onPress={() => control.close()}>
{sanitizeHandle(info.creatorHandle, '@')} {info.creatorHandle === TRENDING_HANDLE
? l`Bluesky`
: sanitizeHandle(info.creatorHandle, '@')}
</InlineLinkText> </InlineLinkText>
</Trans> </Trans>
</Text> </Text>
@@ -472,12 +525,13 @@ function DialogInner({
color="secondary" color="secondary"
shape="round" shape="round"
onPress={onPressShare}> onPress={onPressShare}>
<ButtonIcon icon={Share} size="lg" /> <ButtonIcon icon={ShareIcon} size="lg" />
</Button> </Button>
</View> </View>
<RichText value={info.description} style={[a.text_md]} /> <RichText value={info.description} style={[a.text_md]} />
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
{typeof likeCount === 'number' && ( {typeof likeCount === 'number' && likeCount > 0 ? (
<View style={[a.flex_row, a.gap_sm, a.align_center]}>
<InlineLinkText <InlineLinkText
label={l`View users who like this feed`} label={l`View users who like this feed`}
to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')} to={makeCustomFeedLink(info.creatorDid, feedRkey, 'liked-by')}
@@ -487,41 +541,47 @@ function DialogInner({
Liked by <Plural value={likeCount} one="# user" other="# users" /> Liked by <Plural value={likeCount} one="# user" other="# users" />
</Trans> </Trans>
</InlineLinkText> </InlineLinkText>
)} </View>
</View> ) : null}
{hasSession && ( {hasSession ? (
<> <>
<View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}> {!isTrending ? (
<Button <View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}>
disabled={isLikePending || isUnlikePending} <Button
label={l`Like this feed`} disabled={isLikePending || isUnlikePending}
size="small" label={l`Like this feed`}
color="secondary" size="small"
onPress={onToggleLiked} color="secondary"
style={[a.flex_1]}> onPress={() => void onToggleLiked()}
{isLiked ? ( style={[a.flex_1]}>
<HeartFilled size="sm" fill={t.palette.pink} /> {isLiked ? (
) : ( <HeartFilledIcon size="sm" fill={t.palette.pink} />
<ButtonIcon icon={Heart} /> ) : (
)} <ButtonIcon icon={HeartIcon} />
)}
<ButtonText> <ButtonText>
{isLiked ? <Trans>Unlike</Trans> : <Trans>Like</Trans>} {isLiked ? <Trans>Unlike</Trans> : <Trans>Like</Trans>}
</ButtonText> </ButtonText>
</Button> </Button>
<Button <Button
disabled={isFeedStateChangePending} disabled={isFeedStateChangePending}
label={isPinned ? l`Unpin feed` : l`Pin feed`} label={isPinned ? l`Unpin feed` : l`Pin feed`}
size="small" size="small"
color={isPinned ? 'secondary' : 'primary'} color={isPinned ? 'secondary' : 'primary'}
onPress={onTogglePinned} onPress={onTogglePinned}
style={[a.flex_1]}> style={[a.flex_1]}>
<ButtonText> <ButtonText>
{isPinned ? <Trans>Unpin feed</Trans> : <Trans>Pin feed</Trans>} {isPinned ? (
</ButtonText> <Trans>Unpin feed</Trans>
<ButtonIcon icon={Pin} position="right" /> ) : (
</Button> <Trans>Pin feed</Trans>
</View> )}
</ButtonText>
<ButtonIcon icon={PinIcon} position="right" />
</Button>
</View>
) : null}
<View style={[a.pt_xs, a.gap_lg]}> <View style={[a.pt_xs, a.gap_lg]}>
<Divider /> <Divider />
@@ -541,7 +601,7 @@ function DialogInner({
<ButtonText> <ButtonText>
<Trans>Report feed</Trans> <Trans>Report feed</Trans>
</ButtonText> </ButtonText>
<ButtonIcon icon={CircleInfo} position="right" /> <ButtonIcon icon={CircleInfoIcon} position="right" />
</Button> </Button>
</View> </View>
@@ -556,7 +616,7 @@ function DialogInner({
)} )}
</View> </View>
</> </>
)} ) : null}
</View> </View>
) )
} }
+1 -1
View File
@@ -327,7 +327,7 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) {
count += page.feeds.length count += page.feeds.length
} }
if (count < limit && (data?.pages.length || 0) < 6) { if (count < limit && (data?.pages.length || 0) < 6) {
query.fetchNextPage() void query.fetchNextPage()
lastPageCountRef.current = data?.pages?.length || 0 lastPageCountRef.current = data?.pages?.length || 0
} }
}, [query, limit]) }, [query, limit])
+35 -2
View File
@@ -17,6 +17,7 @@ import {
AppBskyEmbedImages, AppBskyEmbedImages,
AppBskyEmbedVideo, AppBskyEmbedVideo,
type AppBskyFeedDefs, type AppBskyFeedDefs,
type RichText as RichTextType,
} from '@atproto/api' } from '@atproto/api'
import {useLingui} from '@lingui/react/macro' import {useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -50,7 +51,7 @@ import {List, type ListRef} from '#/view/com/util/List'
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types' import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
import {useBreakpoints, useLayoutBreakpoints} from '#/alf' import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf'
import { import {
AgeAssuranceDismissibleFeedBanner, AgeAssuranceDismissibleFeedBanner,
useInternalState as useAgeAssuranceBannerState, useInternalState as useAgeAssuranceBannerState,
@@ -64,6 +65,7 @@ import {FeedTrendingTopicsInterstitial} from '#/components/interstitials/FeedTre
import {TrendingInterstitial} from '#/components/interstitials/Trending' import {TrendingInterstitial} from '#/components/interstitials/Trending'
import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos' import {TrendingVideos as TrendingVideosInterstitial} from '#/components/interstitials/TrendingVideos'
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {RichText} from '#/components/RichText'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env' import {IS_IOS, IS_NATIVE, IS_WEB} from '#/env'
import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner' import {DiscoverFeedLiveEventFeedsAndTrendingBanner} from '#/features/liveEvents/components/DiscoverFeedLiveEventFeedsAndTrendingBanner'
@@ -105,6 +107,11 @@ type FeedRow =
type: 'fallbackMarker' type: 'fallbackMarker'
key: string key: string
} }
| {
type: 'description'
key: string
value: RichTextType
}
| { | {
type: 'sliceItem' type: 'sliceItem'
key: string key: string
@@ -194,6 +201,7 @@ const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
let PostFeed = ({ let PostFeed = ({
feed, feed,
description,
feedParams, feedParams,
ignoreFilterFor, ignoreFilterFor,
style, style,
@@ -216,6 +224,7 @@ let PostFeed = ({
isVideoFeed = false, isVideoFeed = false,
}: { }: {
feed: FeedDescriptor feed: FeedDescriptor
description?: RichTextType
feedParams?: FeedParams feedParams?: FeedParams
ignoreFilterFor?: string ignoreFilterFor?: string
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
@@ -232,13 +241,14 @@ let PostFeed = ({
progressViewOffset?: number progressViewOffset?: number
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
ListHeaderComponent?: () => React.ReactElement ListHeaderComponent?: () => React.ReactElement
extraData?: any extraData?: Record<string, unknown>
savedFeedConfig?: AppBskyActorDefs.SavedFeed savedFeedConfig?: AppBskyActorDefs.SavedFeed
initialNumToRender?: number initialNumToRender?: number
isVideoFeed?: boolean isVideoFeed?: boolean
lastFetchDate?: () => number lastFetchDate?: () => number
}): React.ReactNode => { }): React.ReactNode => {
const ax = useAnalytics() const ax = useAnalytics()
const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {currentAccount, hasSession} = useSession() const {currentAccount, hasSession} = useSession()
@@ -389,6 +399,7 @@ let PostFeed = ({
* Cached value of whether the current feed was selected at startup. We don't * Cached value of whether the current feed was selected at startup. We don't
* want this to update when user swipes. * want this to update when user swipes.
*/ */
// oxlint-disable-next-line react/hook-use-state
const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed) const [isCurrentFeedAtStartupSelected] = useState(selectedFeed === feed)
const blockedOrMutedAuthors = usePostAuthorShadowFilter( const blockedOrMutedAuthors = usePostAuthorShadowFilter(
@@ -680,8 +691,17 @@ let PostFeed = ({
} }
} }
if (description?.text) {
arr.unshift({
key: 'description',
type: 'description',
value: description,
})
}
return arr return arr
}, [ }, [
description,
isFetched, isFetched,
isError, isError,
isEmpty, isEmpty,
@@ -793,6 +813,18 @@ let PostFeed = ({
return <PostFeedLoadingPlaceholder /> return <PostFeedLoadingPlaceholder />
} else if (row.type === 'feedShutdownMsg') { } else if (row.type === 'feedShutdownMsg') {
return <FeedShutdownMsg feedUri={feedUriOrActorDid} /> return <FeedShutdownMsg feedUri={feedUriOrActorDid} />
} else if (row.type === 'description') {
return (
<RichText
value={row.value}
style={[
a.m_md,
a.text_md,
a.leading_snug,
t.atoms.text_contrast_high,
]}
/>
)
} else if (row.type === 'interstitialFollows') { } else if (row.type === 'interstitialFollows') {
return <SuggestedFollows feed={feed} /> return <SuggestedFollows feed={feed} />
} else if (row.type === 'interstitialProgressGuide') { } else if (row.type === 'interstitialProgressGuide') {
@@ -893,6 +925,7 @@ let PostFeed = ({
feedTab, feedTab,
feedCacheKey, feedCacheKey,
onPressShowLess, onPressShowLess,
t,
], ],
) )