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