Update presentation of trending feeds (#11206)
This commit is contained in:
@@ -1055,11 +1055,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
|
||||||
@@ -1406,11 +1401,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
|
||||||
@@ -1756,11 +1746,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
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user