This commit is contained in:
Eric Bailey
2024-12-11 15:57:36 -06:00
parent 3a5cb7e370
commit 746abfbde6
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AtUri} from '@atproto/api' import {AppBskyActorDefs,AtUri} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro' import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -30,6 +30,7 @@ import * as Dialog from '#/components/Dialog'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {useRichText} from '#/components/hooks/useRichText' import {useRichText} from '#/components/hooks/useRichText'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import { import {
@@ -41,12 +42,9 @@ import {
Pin_Stroke2_Corner0_Rounded as Pin, Pin_Stroke2_Corner0_Rounded as Pin,
} 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 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 * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Menu from '#/components/Menu'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@@ -60,57 +58,25 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
const infoControl = Dialog.useDialogControl() const infoControl = Dialog.useDialogControl()
const playHaptic = useHaptics() const playHaptic = useHaptics()
const {data: preferences} = usePreferencesQuery()
const [likeUri, setLikeUri] = React.useState(info.likeUri || '') const [likeUri, setLikeUri] = React.useState(info.likeUri || '')
const isLiked = !!likeUri const isLiked = !!likeUri
const likeCount = const likeCount =
isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0 isLiked && likeUri ? (info.likeCount || 0) + 1 : info.likeCount || 0
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} = const {data: preferences} = usePreferencesQuery()
useAddSavedFeedsMutation()
const {mutateAsync: removeFeed, isPending: isRemovePending} =
useRemoveFeedMutation()
const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
useUpdateSavedFeedsMutation()
const isFeedStateChangePending =
isAddSavedFeedPending || isRemovePending || isUpdateFeedPending
const savedFeedConfig = preferences?.savedFeeds?.find( const savedFeedConfig = preferences?.savedFeeds?.find(
f => f.value === info.uri, f => f.value === info.uri,
) )
const isSaved = Boolean(savedFeedConfig)
const isPinned = Boolean(savedFeedConfig?.pinned) const isPinned = Boolean(savedFeedConfig?.pinned)
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
useAddSavedFeedsMutation()
const {mutateAsync: updateSavedFeeds, isPending: isUpdateFeedPending} =
useUpdateSavedFeedsMutation()
const isOuterPinPending = isAddSavedFeedPending || isUpdateFeedPending
const onToggleSaved = React.useCallback(async () => { const onTogglePinned = async () => {
try { if (isOuterPinPending) return
playHaptic()
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`))
} else {
await addSavedFeeds([
{
type: 'feed',
value: info.uri,
pinned: false,
},
])
Toast.show(_(msg`Saved to your feeds`))
}
} catch (err) {
Toast.show(
_(
msg`There was an issue updating your feeds, please check your internet connection and try again.`,
),
'xmark',
)
logger.error('Failed to update feeds', {message: err})
}
}, [_, playHaptic, info, removeFeed, addSavedFeeds, savedFeedConfig])
const onTogglePinned = React.useCallback(async () => {
try { try {
playHaptic() playHaptic()
@@ -124,9 +90,9 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
]) ])
if (pinned) { if (pinned) {
Toast.show(_(msg`Pinned to your home screen`)) Toast.show(_(msg`Pinned ${info.displayName} to Home`))
} else { } else {
Toast.show(_(msg`Un-pinned from your home screen`)) Toast.show(_(msg`Un-pinned ${info.displayName} from Home`))
} }
} else { } else {
await addSavedFeeds([ await addSavedFeeds([
@@ -136,13 +102,13 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
pinned: true, pinned: true,
}, },
]) ])
Toast.show(_(msg`Pinned to your home screen`)) Toast.show(_(msg`Pinned ${info.displayName} to Home`))
} }
} catch (e) { } catch (e) {
Toast.show(_(msg`There was an issue contacting the server`), 'xmark') Toast.show(_(msg`There was an issue contacting the server`), 'xmark')
logger.error('Failed to toggle pinned feed', {message: e}) logger.error('Failed to toggle pinned feed', {message: e})
} }
}, [playHaptic, info, _, savedFeedConfig, updateSavedFeeds, addSavedFeeds]) }
return ( return (
<> <>
@@ -166,7 +132,10 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
paddingRight: 12, paddingRight: 12,
}, },
]} ]}
onPress={() => infoControl.open()}> onPress={() => {
playHaptic()
infoControl.open()
}}>
{({hovered, pressed}) => ( {({hovered, pressed}) => (
<> <>
<View <View
@@ -247,61 +216,23 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
{hasSession && ( {hasSession && (
<Layout.Header.Slot> <Layout.Header.Slot>
<Menu.Root>
<Menu.Trigger label={_(msg`Open feed options menu`)}>
{({props}) => {
return (
<Button <Button
{...props} label={
label={_( isPinned
msg`Pin ${info.displayName} to your home screen`, ? _(`Un-pin ${info.displayName} from Home`)
)} : _(msg`Pin ${info.displayName} to Home`)
}
size="small" size="small"
variant="ghost" variant="ghost"
shape="square" shape="square"
color="secondary"> color="secondary"
onPress={onTogglePinned}>
{isPinned ? ( {isPinned ? (
<PinFilled size="lg" fill={t.palette.primary_500} /> <PinFilled size="lg" fill={t.palette.primary_500} />
) : ( ) : (
<ButtonIcon icon={Pin} size="lg" /> <ButtonIcon icon={Pin} size="lg" />
)} )}
</Button> </Button>
)
}}
</Menu.Trigger>
<Menu.Outer>
<Menu.Item
disabled={isFeedStateChangePending}
label={
isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)
}
onPress={onTogglePinned}>
<Menu.ItemText>
{isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
</Menu.ItemText>
<Menu.ItemIcon icon={isPinned ? X : Pin} position="right" />
</Menu.Item>
<Menu.Item
disabled={isFeedStateChangePending}
label={
isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)
}
onPress={onToggleSaved}>
<Menu.ItemText>
{isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)}
</Menu.ItemText>
<Menu.ItemIcon
icon={isSaved ? Trash : Plus}
position="right"
/>
</Menu.Item>
</Menu.Outer>
</Menu.Root>
</Layout.Header.Slot> </Layout.Header.Slot>
)} )}
</Layout.Header.Outer> </Layout.Header.Outer>
@@ -319,7 +250,8 @@ export function ProfileFeedHeader({info}: {info: FeedSourceFeedInfo}) {
likeCount={likeCount} likeCount={likeCount}
isPinned={isPinned} isPinned={isPinned}
onTogglePinned={onTogglePinned} onTogglePinned={onTogglePinned}
isFeedStateChangePending={isFeedStateChangePending} isOuterPinPending={isOuterPinPending}
savedFeedConfig={savedFeedConfig}
/> />
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </Dialog.Outer>
@@ -334,7 +266,8 @@ function DialogInner({
likeCount, likeCount,
isPinned, isPinned,
onTogglePinned, onTogglePinned,
isFeedStateChangePending, isOuterPinPending,
savedFeedConfig,
}: { }: {
info: FeedSourceFeedInfo info: FeedSourceFeedInfo
likeUri: string likeUri: string
@@ -342,7 +275,8 @@ function DialogInner({
likeCount: number likeCount: number
isPinned: boolean isPinned: boolean
onTogglePinned: () => void onTogglePinned: () => void
isFeedStateChangePending: boolean isOuterPinPending: boolean
savedFeedConfig: AppBskyActorDefs.SavedFeed | undefined
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -380,14 +314,55 @@ function DialogInner({
} }
}, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _]) }, [playHaptic, isLiked, likeUri, unlikeFeed, setLikeUri, likeFeed, info, _])
const onPressShare = React.useCallback(() => { const isSaved = Boolean(savedFeedConfig)
const {mutateAsync: removeFeed, isPending: isRemovePending} =
useRemoveFeedMutation()
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
useAddSavedFeedsMutation()
const isFeedChangePending =
isOuterPinPending || isRemovePending || isAddSavedFeedPending
const onToggleSaved = async () => {
if (isFeedChangePending) return
try {
playHaptic()
if (savedFeedConfig) {
await removeFeed(savedFeedConfig)
Toast.show(_(msg`Removed from your feeds`))
} else {
await addSavedFeeds([
{
type: 'feed',
value: info.uri,
pinned: false,
},
])
Toast.show(_(msg`Saved to your feeds`))
}
} catch (err) {
Toast.show(
_(
msg`There was an issue updating your feeds, please check your internet connection and try again.`,
),
'xmark',
)
logger.error('Failed to update feeds', {message: err})
}
}
const onTogglePinnedInner = () => {
if (isFeedChangePending) return
onTogglePinned()
}
const onPressShare = () => {
const url = toShareUrl(info.route.href) const url = toShareUrl(info.route.href)
shareUrl(url) shareUrl(url)
}, [info]) }
const onPressReport = React.useCallback(() => { const onPressReport = () => {
reportDialogControl.open() reportDialogControl.open()
}, [reportDialogControl]) }
return loading ? ( return loading ? (
<Loader size="xl" /> <Loader size="xl" />
@@ -438,7 +413,23 @@ function DialogInner({
<RichText value={rt} style={[a.text_md, a.leading_snug]} /> <RichText value={rt} style={[a.text_md, a.leading_snug]} />
<View style={[a.flex_row, a.gap_sm, a.align_center]}> <View style={[a.flex_row, a.gap_sm, a.align_center, {marginTop: -3}]}>
<Button
size="small"
variant="solid"
color="secondary"
shape="round"
label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)}
testID="toggleLikeBtn"
disabled={!hasSession || isLikePending || isUnlikePending}
onPress={onToggleLiked}>
{isLiked ? (
<HeartFilled size="md" fill={t.palette.like} />
) : (
<Heart size="md" fill={t.atoms.text_contrast_medium.color} />
)}
</Button>
{typeof likeCount === 'number' && ( {typeof likeCount === 'number' && (
<InlineLinkText <InlineLinkText
label={_(msg`View users who like this feed`)} label={_(msg`View users who like this feed`)}
@@ -456,37 +447,44 @@ function DialogInner({
{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]}>
<Button <Button
disabled={isLikePending || isUnlikePending} label={
label={_(msg`Report feed`)} isPinned
? _(`Remove ${info.displayName} from my saved feeds`)
: _(msg`Add ${info.displayName} to my saved feeds`)
}
size="small" size="small"
variant="solid" variant="solid"
color="secondary" color={'secondary'}
onPress={onToggleLiked} onPress={onToggleSaved}
style={[a.flex_1]}> style={[a.flex_1]}>
{isLiked ? (
<HeartFilled size="sm" fill={t.palette.like} />
) : (
<ButtonIcon icon={Heart} position="left" />
)}
<ButtonText> <ButtonText>
{isLiked ? <Trans>Unlike</Trans> : <Trans>Like</Trans>} {isSaved ? <Trans>Saved</Trans> : <Trans>Save feed</Trans>}
</ButtonText> </ButtonText>
<ButtonIcon icon={isSaved ? Check : Plus} position="right" />
</Button> </Button>
<Button <Button
disabled={isFeedStateChangePending} label={
label={_(msg`Report feed`)} isPinned
? _(`Un-pin ${info.displayName} from Home`)
: _(msg`Pin ${info.displayName} to Home`)
}
size="small" size="small"
variant="solid" variant="solid"
color={isPinned ? 'secondary' : 'primary'} color={isPinned ? 'secondary_inverted' : 'primary'}
onPress={onTogglePinned} onPress={onTogglePinnedInner}
style={[a.flex_1]}> style={[a.flex_1]}>
<ButtonText> <ButtonText>
{isPinned ? <Trans>Unpin feed</Trans> : <Trans>Pin feed</Trans>} {isPinned ? (
<Trans>Pinned</Trans>
) : isSaved ? (
<Trans>Pin</Trans>
) : (
<Trans>Pin and save</Trans>
)}
</ButtonText> </ButtonText>
<ButtonIcon icon={Pin} position="right" /> <ButtonIcon icon={isPinned ? Check : Pin} position="right" />
</Button> </Button>
</View> </View>