Let specified facets take priority in feed descriptions (#10740)

This commit is contained in:
Samuel Newman
2026-06-06 00:13:26 +03:00
committed by GitHub
parent fb8316fd85
commit a5338090cb
3 changed files with 50 additions and 55 deletions
-3
View File
@@ -1346,9 +1346,6 @@
} }
}, },
"src/screens/Profile/components/ProfileFeedHeader.tsx": { "src/screens/Profile/components/ProfileFeedHeader.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 1
},
"@typescript-eslint/no-misused-promises": { "@typescript-eslint/no-misused-promises": {
"count": 5 "count": 5
} }
@@ -1,9 +1,7 @@
import {useCallback, useMemo, useState} from 'react' import {useCallback, useMemo, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AtUri} from '@atproto/api' import {AtUri} from '@atproto/api'
import {msg} from '@lingui/core/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
import {useHaptics} from '#/lib/haptics' import {useHaptics} from '#/lib/haptics'
import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links' import {makeCustomFeedLink, makeProfileLink} from '#/lib/routes/links'
@@ -26,7 +24,6 @@ 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 {useRichText} from '#/components/hooks/useRichText'
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
@@ -86,7 +83,7 @@ export function ProfileFeedHeaderSkeleton() {
export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) { export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
const t = useTheme() const t = useTheme()
const {_, i18n} = useLingui() const {t: l, i18n} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const {hasSession} = useSession() const {hasSession} = useSession()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
@@ -121,7 +118,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
if (savedFeedConfig) { if (savedFeedConfig) {
await removeFeed(savedFeedConfig) await removeFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`)) Toast.show(l`Removed from your feeds`)
ax.metric('feed:unsave', {feedUrl: info.uri}) ax.metric('feed:unsave', {feedUrl: info.uri})
} else { } else {
await addSavedFeeds([ await addSavedFeeds([
@@ -131,14 +128,12 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
pinned: false, pinned: false,
}, },
]) ])
Toast.show(_(msg`Saved to your feeds`)) Toast.show(l`Saved to your feeds`)
ax.metric('feed:save', {feedUrl: info.uri}) ax.metric('feed:save', {feedUrl: info.uri})
} }
} catch (err) { } catch (err) {
Toast.show( Toast.show(
_( l`There was an issue updating your feeds, please check your internet connection and try again.`,
msg`There was an issue updating your feeds, please check your internet connection and try again.`,
),
{ {
type: 'error', type: 'error',
}, },
@@ -161,10 +156,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
]) ])
if (pinned) { if (pinned) {
Toast.show(_(msg`Pinned ${info.displayName} to Home`)) Toast.show(l`Pinned ${info.displayName} to Home`)
ax.metric('feed:pin', {feedUrl: info.uri}) ax.metric('feed:pin', {feedUrl: info.uri})
} else { } else {
Toast.show(_(msg`Unpinned ${info.displayName} from Home`)) Toast.show(l`Unpinned ${info.displayName} from Home`)
ax.metric('feed:unpin', {feedUrl: info.uri}) ax.metric('feed:unpin', {feedUrl: info.uri})
} }
} else { } else {
@@ -175,11 +170,11 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
pinned: true, pinned: true,
}, },
]) ])
Toast.show(_(msg`Pinned ${info.displayName} to Home`)) Toast.show(l`Pinned ${info.displayName} to Home`)
ax.metric('feed:pin', {feedUrl: info.uri}) ax.metric('feed:pin', {feedUrl: info.uri})
} }
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`), { Toast.show(l`There was an issue contacting the server`, {
type: 'error', type: 'error',
}) })
logger.error('Failed to toggle pinned feed', {message: e}) logger.error('Failed to toggle pinned feed', {message: e})
@@ -194,7 +189,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
<Layout.Header.BackButton /> <Layout.Header.BackButton />
<Layout.Header.Content align="left"> <Layout.Header.Content align="left">
<Button <Button
label={_(msg`Open feed info screen`)} label={l`Open feed info screen`}
style={[ style={[
a.justify_start, a.justify_start,
{ {
@@ -295,12 +290,12 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
<Layout.Header.Slot> <Layout.Header.Slot>
{isPinned ? ( {isPinned ? (
<Menu.Root> <Menu.Root>
<Menu.Trigger label={_(msg`Open feed options menu`)}> <Menu.Trigger label={l`Open feed options menu`}>
{({props}) => { {({props}) => {
return ( return (
<Button <Button
{...props} {...props}
label={_(msg`Open feed options menu`)} label={l`Open feed options menu`}
size="small" size="small"
variant="ghost" variant="ghost"
shape="square" shape="square"
@@ -314,23 +309,21 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
<Menu.Outer> <Menu.Outer>
<Menu.Item <Menu.Item
disabled={isFeedStateChangePending} disabled={isFeedStateChangePending}
label={_(msg`Unpin from home`)} label={l`Unpin from home`}
onPress={onTogglePinned}> onPress={onTogglePinned}>
<Menu.ItemText>{_(msg`Unpin from home`)}</Menu.ItemText> <Menu.ItemText>{l`Unpin from home`}</Menu.ItemText>
<Menu.ItemIcon icon={X} position="right" /> <Menu.ItemIcon icon={X} position="right" />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
disabled={isFeedStateChangePending} disabled={isFeedStateChangePending}
label={ label={
isSaved isSaved ? l`Remove from my feeds` : l`Save to my feeds`
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)
} }
onPress={onToggleSaved}> onPress={onToggleSaved}>
<Menu.ItemText> <Menu.ItemText>
{isSaved {isSaved
? _(msg`Remove from my feeds`) ? l`Remove from my feeds`
: _(msg`Save to my feeds`)} : l`Save to my feeds`}
</Menu.ItemText> </Menu.ItemText>
<Menu.ItemIcon <Menu.ItemIcon
icon={isSaved ? Trash : Plus} icon={isSaved ? Trash : Plus}
@@ -341,7 +334,7 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
</Menu.Root> </Menu.Root>
) : ( ) : (
<Button <Button
label={_(msg`Pin to Home`)} label={l`Pin to Home`}
size="small" size="small"
variant="ghost" variant="ghost"
shape="square" shape="square"
@@ -354,11 +347,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
)} )}
</Layout.Header.Outer> </Layout.Header.Outer>
</Layout.Center> </Layout.Center>
<Dialog.Outer control={infoControl}> <Dialog.Outer control={infoControl}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.ScrollableInner <Dialog.ScrollableInner
label={_(msg`Feed menu`)} label={l`Feed menu`}
style={[gtMobile ? {width: 'auto', minWidth: 450} : a.w_full]}> style={[gtMobile ? {width: 'auto', minWidth: 450} : a.w_full]}>
<DialogInner <DialogInner
info={info} info={info}
@@ -393,13 +385,12 @@ function DialogInner({
isFeedStateChangePending: boolean isFeedStateChangePending: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {t: l} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const {hasSession} = useSession() const {hasSession} = useSession()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const reportDialogControl = useReportDialogControl() const reportDialogControl = useReportDialogControl()
const [rt] = useRichText(info.description.text)
const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation() const {mutateAsync: likeFeed, isPending: isLikePending} = useLikeMutation()
const {mutateAsync: unlikeFeed, isPending: isUnlikePending} = const {mutateAsync: unlikeFeed, isPending: isUnlikePending} =
useUnlikeMutation() useUnlikeMutation()
@@ -422,9 +413,7 @@ function DialogInner({
} }
} catch (err) { } catch (err) {
Toast.show( Toast.show(
_( l`There was an issue contacting the server, please check your internet connection and try again.`,
msg`There was an issue contacting the server, please check your internet connection and try again.`,
),
{ {
type: 'error', type: 'error',
}, },
@@ -436,9 +425,9 @@ function DialogInner({
const onPressShare = useCallback(() => { const onPressShare = useCallback(() => {
playHaptic() playHaptic()
const url = toShareUrl(info.route.href) const url = toShareUrl(info.route.href)
shareUrl(url) void shareUrl(url)
ax.metric('feed:share', {feedUrl: info.uri}) ax.metric('feed:share', {feedUrl: info.uri})
}, [info, playHaptic]) }, [ax, info, playHaptic])
const onPressReport = useCallback(() => { const onPressReport = useCallback(() => {
reportDialogControl.open() reportDialogControl.open()
@@ -462,7 +451,7 @@ function DialogInner({
<Trans> <Trans>
By{' '} By{' '}
<InlineLinkText <InlineLinkText
label={_(msg`View ${info.creatorHandle}'s profile`)} label={l`View ${info.creatorHandle}'s profile`}
to={makeProfileLink({ to={makeProfileLink({
did: info.creatorDid, did: info.creatorDid,
handle: info.creatorHandle, handle: info.creatorHandle,
@@ -477,7 +466,7 @@ function DialogInner({
</View> </View>
<Button <Button
label={_(msg`Share this feed`)} label={l`Share this feed`}
size="small" size="small"
variant="ghost" variant="ghost"
color="secondary" color="secondary"
@@ -486,13 +475,11 @@ function DialogInner({
<ButtonIcon icon={Share} size="lg" /> <ButtonIcon icon={Share} size="lg" />
</Button> </Button>
</View> </View>
<RichText value={info.description} style={[a.text_md]} />
<RichText value={rt} style={[a.text_md]} />
<View style={[a.flex_row, a.gap_sm, a.align_center]}> <View style={[a.flex_row, a.gap_sm, a.align_center]}>
{typeof likeCount === 'number' && ( {typeof likeCount === 'number' && (
<InlineLinkText <InlineLinkText
label={_(msg`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')}
style={[a.underline, t.atoms.text_contrast_medium]} style={[a.underline, t.atoms.text_contrast_medium]}
onPress={() => control.close()}> onPress={() => control.close()}>
@@ -502,13 +489,12 @@ function DialogInner({
</InlineLinkText> </InlineLinkText>
)} )}
</View> </View>
{hasSession && ( {hasSession && (
<> <>
<View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}> <View style={[a.flex_row, a.gap_sm, a.align_center, a.pt_sm]}>
<Button <Button
disabled={isLikePending || isUnlikePending} disabled={isLikePending || isUnlikePending}
label={_(msg`Like this feed`)} label={l`Like this feed`}
size="small" size="small"
color="secondary" color="secondary"
onPress={onToggleLiked} onPress={onToggleLiked}
@@ -525,7 +511,7 @@ function DialogInner({
</Button> </Button>
<Button <Button
disabled={isFeedStateChangePending} disabled={isFeedStateChangePending}
label={isPinned ? _(msg`Unpin feed`) : _(msg`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}
@@ -547,7 +533,7 @@ function DialogInner({
</Text> </Text>
<Button <Button
label={_(msg`Report feed`)} label={l`Report feed`}
size="small" size="small"
variant="solid" variant="solid"
color="secondary" color="secondary"
+20 -8
View File
@@ -101,6 +101,15 @@ export function hydrateFeedGenerator(
const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}` const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}`
const route = router.matchPath(href) const route = router.matchPath(href)
const description = new RichText({
text: view.description || '',
facets: (view.descriptionFacets || [])?.slice(),
})
if (!view.descriptionFacets) {
description.detectFacetsWithoutResolution()
}
return { return {
type: 'feed', type: 'feed',
view, view,
@@ -116,10 +125,7 @@ export function hydrateFeedGenerator(
displayName: view.displayName displayName: view.displayName
? sanitizeDisplayName(view.displayName) ? sanitizeDisplayName(view.displayName)
: t`Feed by ${sanitizeHandle(view.creator.handle, '@')}`, : t`Feed by ${sanitizeHandle(view.creator.handle, '@')}`,
description: new RichText({ description,
text: view.description || '',
facets: (view.descriptionFacets || [])?.slice(),
}),
creatorDid: view.creator.did, creatorDid: view.creator.did,
creatorHandle: view.creator.handle, creatorHandle: view.creator.handle,
likeCount: view.likeCount, likeCount: view.likeCount,
@@ -136,6 +142,15 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}` const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}`
const route = router.matchPath(href) const route = router.matchPath(href)
const description = new RichText({
text: view.description || '',
facets: (view.descriptionFacets || [])?.slice(),
})
if (!view.descriptionFacets) {
description.detectFacetsWithoutResolution()
}
return { return {
type: 'list', type: 'list',
view, view,
@@ -148,10 +163,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
}, },
cid: view.cid, cid: view.cid,
avatar: view.avatar, avatar: view.avatar,
description: new RichText({ description,
text: view.description || '',
facets: (view.descriptionFacets || [])?.slice(),
}),
creatorDid: view.creator.did, creatorDid: view.creator.did,
creatorHandle: view.creator.handle, creatorHandle: view.creator.handle,
displayName: view.name displayName: view.name