Share menu (#7840)
* move post ctrls to #/components * restructure post controls, basic share menu * add border radius to searchable people list for android * Revert "add border radius to searchable people list for android" This reverts commit 417449086e25b82f5683b12f6405d972f48ce50e. * add copy link to native share menu * reorg files again * open native share menu on long press * Translation comments Thanks @surfdude29 * abs path * update type imports, remove forwardRef * rm react import * equal spacing of buttons, extract disco debug * add better icon * add right offset to share button for visual alignment * Add recent chats to share menu (#7853) * add recent chats to share menu * Update RecentChats.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update RecentChats.tsx * add fading edge on andriod * tweak scrollview * Add metrics and A/B alt icon to share menu (#8401) * add metrics * add a/b tested alt icon --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * More descriptive share text/icon on web (#7854) * more descriptive share text on web * revert dev mode changes * add missing import * use modified share icon everywhere * Add back conflicting changes --------- Co-authored-by: Eric Bailey <git@esb.lol> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -43,7 +43,6 @@ import {PostThreadFollowBtn} from '#/view/com/post-thread/PostThreadFollowBtn'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {Link, TextLink} from '#/view/com/util/Link'
|
||||
import {formatCount} from '#/view/com/util/numeric/format'
|
||||
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
@@ -60,6 +59,7 @@ import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {PostHider} from '#/components/moderation/PostHider'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
@@ -494,7 +494,7 @@ let PostThreadItemLoaded = ({
|
||||
marginLeft: -5,
|
||||
},
|
||||
]}>
|
||||
<PostCtrls
|
||||
<PostControls
|
||||
big
|
||||
post={post}
|
||||
record={record}
|
||||
@@ -642,7 +642,7 @@ let PostThreadItemLoaded = ({
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<PostCtrls
|
||||
<PostControls
|
||||
post={post}
|
||||
record={record}
|
||||
richText={richText}
|
||||
|
||||
+11
-11
@@ -27,21 +27,21 @@ import {
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {Link, TextLink} from '#/view/com/util/Link'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {UserInfoText} from '#/view/com/util/UserInfoText'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {ContentHider} from '../../../components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '../../../components/moderation/PostAlerts'
|
||||
import {Link, TextLink} from '../util/Link'
|
||||
import {PostCtrls} from '../util/post-ctrls/PostCtrls'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '../util/post-embeds'
|
||||
import {PostMeta} from '../util/PostMeta'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {UserInfoText} from '../util/UserInfoText'
|
||||
|
||||
export function Post({
|
||||
post,
|
||||
@@ -255,7 +255,7 @@ function PostInner({
|
||||
/>
|
||||
) : null}
|
||||
</ContentHider>
|
||||
<PostCtrls
|
||||
<PostControls
|
||||
post={post}
|
||||
record={record}
|
||||
richText={richText}
|
||||
|
||||
@@ -37,7 +37,7 @@ import {precacheProfile} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {FeedNameText} from '#/view/com/util/FeedInfoText'
|
||||
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
|
||||
import {Link, TextLink, TextLinkOnWebOnly} from '#/view/com/util/Link'
|
||||
import {PostEmbeds, PostEmbedViewContext} from '#/view/com/util/post-embeds'
|
||||
import {PostMeta} from '#/view/com/util/PostMeta'
|
||||
import {Text} from '#/view/com/util/text/Text'
|
||||
@@ -49,11 +49,12 @@ import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {PostControls} from '#/components/PostControls'
|
||||
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import {Link, TextLink, TextLinkOnWebOnly} from '../util/Link'
|
||||
|
||||
interface FeedItemProps {
|
||||
record: AppBskyFeedPost.Record
|
||||
@@ -439,7 +440,7 @@ let FeedItemInner = ({
|
||||
post={post}
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
<PostCtrls
|
||||
<PostControls
|
||||
post={post}
|
||||
record={record}
|
||||
richText={richText}
|
||||
@@ -451,6 +452,8 @@ let FeedItemInner = ({
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<DiscoverDebug feedContext={feedContext} />
|
||||
</View>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {shareText, shareUrl} from '#/lib/sharing'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {
|
||||
@@ -26,9 +27,11 @@ import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheck} from '#/components/icons/CircleCheck'
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleX} from '#/components/icons/CircleX'
|
||||
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
||||
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
||||
import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck'
|
||||
import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
|
||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
||||
@@ -236,7 +239,9 @@ let ProfileMenu = ({
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownShareBtn"
|
||||
label={_(msg`Share`)}
|
||||
label={
|
||||
isWeb ? _(msg`Copy link to profile`) : _(msg`Share via...`)
|
||||
}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
@@ -245,9 +250,13 @@ let ProfileMenu = ({
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Share</Trans>
|
||||
{isWeb ? (
|
||||
<Trans>Copy link to profile</Trans>
|
||||
) : (
|
||||
<Trans>Share via...</Trans>
|
||||
)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} />
|
||||
<Menu.ItemIcon icon={isWeb ? ChainLinkIcon : ArrowOutOfBoxIcon} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownSearchBtn"
|
||||
@@ -329,7 +338,7 @@ let ProfileMenu = ({
|
||||
<Menu.ItemText>
|
||||
<Trans>Remove verification</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CircleX} />
|
||||
<Menu.ItemIcon icon={CircleXIcon} />
|
||||
</Menu.Item>
|
||||
) : (
|
||||
<Menu.Item
|
||||
@@ -339,7 +348,7 @@ let ProfileMenu = ({
|
||||
<Menu.ItemText>
|
||||
<Trans>Verify account</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CircleCheck} />
|
||||
<Menu.ItemIcon icon={CircleCheckIcon} />
|
||||
</Menu.Item>
|
||||
))}
|
||||
{!isSelf && (
|
||||
@@ -414,7 +423,7 @@ let ProfileMenu = ({
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy at:// URI</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} />
|
||||
<Menu.ItemIcon icon={ClipboardIcon} />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="profileHeaderDropdownShareDIDBtn"
|
||||
@@ -423,7 +432,7 @@ let ProfileMenu = ({
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy DID</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} />
|
||||
<Menu.ItemIcon icon={ClipboardIcon} />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {PostDropdownMenuItems} from './PostDropdownBtnMenuItems'
|
||||
|
||||
let PostDropdownBtn = ({
|
||||
testID,
|
||||
post,
|
||||
postFeedContext,
|
||||
postReqId,
|
||||
record,
|
||||
richText,
|
||||
style,
|
||||
hitSlop,
|
||||
size,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
postReqId: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const theme = useTheme()
|
||||
const alf = useAlf()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const menuControl = useMenuControl()
|
||||
const [hasBeenOpen, setHasBeenOpen] = useState(false)
|
||||
const lazyMenuControl = useMemo(
|
||||
() => ({
|
||||
...menuControl,
|
||||
open() {
|
||||
setHasBeenOpen(true)
|
||||
// HACK. We need the state update to be flushed by the time
|
||||
// menuControl.open() fires but RN doesn't expose flushSync.
|
||||
setTimeout(menuControl.open)
|
||||
},
|
||||
}),
|
||||
[menuControl, setHasBeenOpen],
|
||||
)
|
||||
return (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root control={lazyMenuControl}>
|
||||
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||
{({props, state}) => {
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
hitSlop={hitSlop}
|
||||
testID={testID}
|
||||
style={[
|
||||
style,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && [
|
||||
alf.atoms.bg_contrast_25,
|
||||
],
|
||||
]}>
|
||||
<DotsHorizontal
|
||||
fill={defaultCtrlColor}
|
||||
style={{pointerEvents: 'none'}}
|
||||
size={size}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
{hasBeenOpen && (
|
||||
// Lazily initialized. Once mounted, they stay mounted.
|
||||
<PostDropdownMenuItems
|
||||
testID={testID}
|
||||
post={post}
|
||||
postFeedContext={postFeedContext}
|
||||
postReqId={postReqId}
|
||||
record={record}
|
||||
richText={richText}
|
||||
timestamp={timestamp}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
)}
|
||||
</Menu.Root>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
|
||||
PostDropdownBtn = memo(PostDropdownBtn)
|
||||
export {PostDropdownBtn}
|
||||
@@ -1,872 +0,0 @@
|
||||
import React, {memo} from 'react'
|
||||
import {
|
||||
Platform,
|
||||
type PressableProps,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {getCurrentRoute} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NavigationProp,
|
||||
} from '#/lib/routes/types'
|
||||
import {shareText, shareUrl} from '#/lib/sharing'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {getTranslatorLink} from '#/locale/helpers'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useLanguagePrefs} from '#/state/preferences'
|
||||
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
|
||||
import {usePinnedPostMutation} from '#/state/queries/pinned-post'
|
||||
import {
|
||||
usePostDeleteMutation,
|
||||
useThreadMuteMutationQueue,
|
||||
} from '#/state/queries/post'
|
||||
import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
|
||||
import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
|
||||
import {
|
||||
useProfileBlockMutationQueue,
|
||||
useProfileMuteMutationQueue,
|
||||
} from '#/state/queries/profile'
|
||||
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import {useBreakpoints} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {EmbedDialog} from '#/components/dialogs/Embed'
|
||||
import {
|
||||
PostInteractionSettingsDialog,
|
||||
usePrefetchPostInteractionSettings,
|
||||
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
|
||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
||||
import {
|
||||
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
||||
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
||||
} from '#/components/icons/Emoji'
|
||||
import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
|
||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
|
||||
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {
|
||||
ReportDialog,
|
||||
useReportDialogControl,
|
||||
} from '#/components/moderation/ReportDialog'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||
import * as bsky from '#/types/bsky'
|
||||
import * as Toast from '../Toast'
|
||||
|
||||
let PostDropdownMenuItems = ({
|
||||
post,
|
||||
postFeedContext,
|
||||
postReqId,
|
||||
record,
|
||||
richText,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
postReqId: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
||||
const {mutateAsync: pinPostMutate, isPending: isPinPending} =
|
||||
usePinnedPostMutation()
|
||||
const hiddenPosts = useHiddenPosts()
|
||||
const {hidePost} = useHiddenPostsApi()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const openLink = useOpenLink()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const blockPromptControl = useDialogControl()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const deletePromptControl = useDialogControl()
|
||||
const hidePromptControl = useDialogControl()
|
||||
const loggedOutWarningPromptControl = useDialogControl()
|
||||
const embedPostControl = useDialogControl()
|
||||
const sendViaChatControl = useDialogControl()
|
||||
const postInteractionSettingsDialogControl = useDialogControl()
|
||||
const quotePostDetachConfirmControl = useDialogControl()
|
||||
const hideReplyConfirmControl = useDialogControl()
|
||||
const {mutateAsync: toggleReplyVisibility} =
|
||||
useToggleReplyVisibilityMutation()
|
||||
const [devModeEnabled] = useDevMode()
|
||||
|
||||
const postUri = post.uri
|
||||
const postCid = post.cid
|
||||
const postAuthor = useProfileShadow(post.author)
|
||||
const quoteEmbed = React.useMemo(() => {
|
||||
if (!currentAccount || !post.embed) return
|
||||
return getMaybeDetachedQuoteEmbed({
|
||||
viewerDid: currentAccount.did,
|
||||
post,
|
||||
})
|
||||
}, [post, currentAccount])
|
||||
|
||||
const rootUri = record.reply?.root?.uri || postUri
|
||||
const isReply = Boolean(record.reply)
|
||||
const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
|
||||
post,
|
||||
rootUri,
|
||||
)
|
||||
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
|
||||
const isAuthor = postAuthor.did === currentAccount?.did
|
||||
const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did
|
||||
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
|
||||
threadgateRecord,
|
||||
})
|
||||
const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
|
||||
const isPinned = post.viewer?.pinned
|
||||
|
||||
const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} =
|
||||
useToggleQuoteDetachmentMutation()
|
||||
|
||||
const [queueBlock] = useProfileBlockMutationQueue(postAuthor)
|
||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(postAuthor)
|
||||
|
||||
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
|
||||
postUri: post.uri,
|
||||
rootPostUri: rootUri,
|
||||
})
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
const urip = new AtUri(postUri)
|
||||
return makeProfileLink(postAuthor, 'post', urip.rkey)
|
||||
}, [postUri, postAuthor])
|
||||
|
||||
const translatorUrl = getTranslatorLink(
|
||||
record.text,
|
||||
langPrefs.primaryLanguage,
|
||||
)
|
||||
|
||||
const onDeletePost = () => {
|
||||
deletePostMutate({uri: postUri}).then(
|
||||
() => {
|
||||
Toast.show(_(msg({message: 'Post deleted', context: 'toast'})))
|
||||
|
||||
const route = getCurrentRoute(navigation.getState())
|
||||
if (route.name === 'PostThread') {
|
||||
const params = route.params as CommonNavigatorParams['PostThread']
|
||||
if (
|
||||
currentAccount &&
|
||||
isAuthor &&
|
||||
(params.name === currentAccount.handle ||
|
||||
params.name === currentAccount.did)
|
||||
) {
|
||||
const currentHref = makeProfileLink(postAuthor, 'post', params.rkey)
|
||||
if (currentHref === href && navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
e => {
|
||||
logger.error('Failed to delete post', {message: e})
|
||||
Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const onToggleThreadMute = () => {
|
||||
try {
|
||||
if (isThreadMuted) {
|
||||
unmuteThread()
|
||||
Toast.show(_(msg`You will now receive notifications for this thread`))
|
||||
} else {
|
||||
muteThread()
|
||||
Toast.show(
|
||||
_(msg`You will no longer receive notifications for this thread`),
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to toggle thread mute', {message: e})
|
||||
Toast.show(
|
||||
_(msg`Failed to toggle thread mute, please try again`),
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onCopyPostText = () => {
|
||||
const str = richTextToString(richText, true)
|
||||
|
||||
Clipboard.setStringAsync(str)
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}
|
||||
|
||||
const onPressTranslate = async () => {
|
||||
await openLink(translatorUrl, true)
|
||||
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
logger.metric('translate', {
|
||||
sourceLanguages: post.record.langs ?? [],
|
||||
targetLanguage: langPrefs.primaryLanguage,
|
||||
textLength: post.record.text.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onHidePost = () => {
|
||||
hidePost({uri: postUri})
|
||||
}
|
||||
|
||||
const hideInPWI = !!postAuthor.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
|
||||
const showLoggedOutWarning =
|
||||
postAuthor.did !== currentAccount?.did && hideInPWI
|
||||
|
||||
const onSharePost = () => {
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
}
|
||||
|
||||
const onPressShowMore = () => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestMore',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
reqId: postReqId,
|
||||
})
|
||||
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
|
||||
}
|
||||
|
||||
const onPressShowLess = () => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestLess',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
reqId: postReqId,
|
||||
})
|
||||
if (onShowLess) {
|
||||
onShowLess({
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
} else {
|
||||
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectChatToShareTo = (conversation: string) => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation,
|
||||
embed: postUri,
|
||||
})
|
||||
}
|
||||
|
||||
const onToggleQuotePostAttachment = async () => {
|
||||
if (!quoteEmbed) return
|
||||
|
||||
const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
|
||||
const isDetach = action === 'detach'
|
||||
|
||||
try {
|
||||
await toggleQuoteDetachment({
|
||||
post,
|
||||
quoteUri: quoteEmbed.uri,
|
||||
action: quoteEmbed.isDetached ? 'reattach' : 'detach',
|
||||
})
|
||||
Toast.show(
|
||||
isDetach
|
||||
? _(msg`Quote post was successfully detached`)
|
||||
: _(msg`Quote post was re-attached`),
|
||||
)
|
||||
} catch (e: any) {
|
||||
Toast.show(
|
||||
_(msg({message: 'Updating quote attachment failed', context: 'toast'})),
|
||||
)
|
||||
logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
|
||||
}
|
||||
}
|
||||
|
||||
const canHidePostForMe = !isAuthor && !isPostHidden
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
const canHideReplyForEveryone =
|
||||
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
|
||||
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
||||
|
||||
const onToggleReplyVisibility = async () => {
|
||||
// TODO no threadgate?
|
||||
if (!canHideReplyForEveryone) return
|
||||
|
||||
const action = isReplyHiddenByThreadgate ? 'show' : 'hide'
|
||||
const isHide = action === 'hide'
|
||||
|
||||
try {
|
||||
await toggleReplyVisibility({
|
||||
postUri: rootUri,
|
||||
replyUri: postUri,
|
||||
action,
|
||||
})
|
||||
Toast.show(
|
||||
isHide
|
||||
? _(msg`Reply was successfully hidden`)
|
||||
: _(msg({message: 'Reply visibility updated', context: 'toast'})),
|
||||
)
|
||||
} catch (e: any) {
|
||||
Toast.show(
|
||||
_(msg({message: 'Updating reply visibility failed', context: 'toast'})),
|
||||
)
|
||||
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
|
||||
}
|
||||
}
|
||||
|
||||
const onPressPin = () => {
|
||||
logEvent(isPinned ? 'post:unpin' : 'post:pin', {})
|
||||
pinPostMutate({
|
||||
postUri,
|
||||
postCid,
|
||||
action: isPinned ? 'unpin' : 'pin',
|
||||
})
|
||||
}
|
||||
|
||||
const onBlockAuthor = async () => {
|
||||
try {
|
||||
await queueBlock()
|
||||
Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to block account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onMuteAuthor = async () => {
|
||||
if (postAuthor.viewer?.muted) {
|
||||
try {
|
||||
await queueUnmute()
|
||||
Toast.show(_(msg({message: 'Account unmuted', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to unmute account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await queueMute()
|
||||
Toast.show(_(msg({message: 'Account muted', context: 'toast'})))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to mute account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onShareATURI = () => {
|
||||
shareText(postUri)
|
||||
}
|
||||
|
||||
const onShareAuthorDID = () => {
|
||||
shareText(postAuthor.did)
|
||||
}
|
||||
|
||||
const onReportMisclassification = () => {
|
||||
const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl(
|
||||
href,
|
||||
)}`
|
||||
openLink(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Outer>
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="pinPostBtn"
|
||||
label={
|
||||
isPinned
|
||||
? _(msg`Unpin from profile`)
|
||||
: _(msg`Pin to your profile`)
|
||||
}
|
||||
disabled={isPinPending}
|
||||
onPress={onPressPin}>
|
||||
<Menu.ItemText>
|
||||
{isPinned
|
||||
? _(msg`Unpin from profile`)
|
||||
: _(msg`Pin to your profile`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isPinPending ? Loader : PinIcon}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Menu.Group>
|
||||
{(!hideInPWI || hasSession) && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownTranslateBtn"
|
||||
label={_(msg`Translate`)}
|
||||
onPress={onPressTranslate}>
|
||||
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Translate} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownCopyTextBtn"
|
||||
label={_(msg`Copy post text`)}
|
||||
onPress={onCopyPostText}>
|
||||
<Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={() => sendViaChatControl.open()}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Send} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShareBtn"
|
||||
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
} else {
|
||||
onSharePost()
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
{canEmbed && (
|
||||
<Menu.Item
|
||||
testID="postDropdownEmbedBtn"
|
||||
label={_(msg`Embed post`)}
|
||||
onPress={() => embedPostControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CodeBrackets} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
|
||||
{hasSession && feedFeedback.enabled && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShowMoreBtn"
|
||||
label={_(msg`Show more like this`)}
|
||||
onPress={onPressShowMore}>
|
||||
<Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSmile} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShowLessBtn"
|
||||
label={_(msg`Show less like this`)}
|
||||
onPress={onPressShowLess}>
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession &&
|
||||
IS_INTERNAL &&
|
||||
DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
<Menu.Item
|
||||
testID="postDropdownReportMisclassificationBtn"
|
||||
label={_(msg`Assign topic - help train Discover!`)}
|
||||
onPress={onReportMisclassification}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Assign topic - help train Discover!`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={AtomIcon} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteThreadBtn"
|
||||
label={
|
||||
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
|
||||
}
|
||||
onPress={onToggleThreadMute}>
|
||||
<Menu.ItemText>
|
||||
{isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isThreadMuted ? Unmute : Mute}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteWordsBtn"
|
||||
label={_(msg`Mute words & tags`)}
|
||||
onPress={() => mutedWordsDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Filter} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession &&
|
||||
(canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{canHidePostForMe && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
}
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canHideReplyForEveryone && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)
|
||||
}
|
||||
onPress={
|
||||
isReplyHiddenByThreadgate
|
||||
? onToggleReplyVisibility
|
||||
: () => hideReplyConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canDetachQuote && (
|
||||
<Menu.Item
|
||||
disabled={isDetachPending}
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)
|
||||
}
|
||||
onPress={
|
||||
quoteEmbed.isDetached
|
||||
? onToggleQuotePostAttachment
|
||||
: () => quotePostDetachConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={
|
||||
isDetachPending
|
||||
? Loader
|
||||
: quoteEmbed.isDetached
|
||||
? Eye
|
||||
: EyeSlash
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteBtn"
|
||||
label={
|
||||
postAuthor.viewer?.muted
|
||||
? _(msg`Unmute account`)
|
||||
: _(msg`Mute account`)
|
||||
}
|
||||
onPress={onMuteAuthor}>
|
||||
<Menu.ItemText>
|
||||
{postAuthor.viewer?.muted
|
||||
? _(msg`Unmute account`)
|
||||
: _(msg`Mute account`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
{!postAuthor.viewer?.blocking && (
|
||||
<Menu.Item
|
||||
testID="postDropdownBlockBtn"
|
||||
label={_(msg`Block account`)}
|
||||
onPress={() => blockPromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={PersonX} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownReportBtn"
|
||||
label={_(msg`Report post`)}
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={() => postInteractionSettingsDialogControl.open()}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
onHoverIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
native: {
|
||||
onPressIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
})
|
||||
: {})}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Group>
|
||||
|
||||
{devModeEnabled ? (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postAtUriShareBtn"
|
||||
label={_(msg`Copy post at:// URI`)}
|
||||
onPress={onShareATURI}>
|
||||
<Menu.ItemText>{_(msg`Copy post at:// URI`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postAuthorDIDShareBtn"
|
||||
label={_(msg`Copy author DID`)}
|
||||
onPress={onShareAuthorDID}>
|
||||
<Menu.ItemText>{_(msg`Copy author DID`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Share} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Menu.Outer>
|
||||
|
||||
<Prompt.Basic
|
||||
control={deletePromptControl}
|
||||
title={_(msg`Delete this post?`)}
|
||||
description={_(
|
||||
msg`If you remove this post, you won't be able to recover it.`,
|
||||
)}
|
||||
onConfirm={onDeletePost}
|
||||
confirmButtonCta={_(msg`Delete`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={hidePromptControl}
|
||||
title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)}
|
||||
description={_(
|
||||
msg`This post will be hidden from feeds and threads. This cannot be undone.`,
|
||||
)}
|
||||
onConfirm={onHidePost}
|
||||
confirmButtonCta={_(msg`Hide`)}
|
||||
/>
|
||||
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
subject={{
|
||||
...post,
|
||||
$type: 'app.bsky.feed.defs#postView',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={loggedOutWarningPromptControl}
|
||||
title={_(msg`Note about sharing`)}
|
||||
description={_(
|
||||
msg`This post is only visible to logged-in users. It won't be visible to people who aren't signed in.`,
|
||||
)}
|
||||
onConfirm={onSharePost}
|
||||
confirmButtonCta={_(msg`Share anyway`)}
|
||||
/>
|
||||
|
||||
{canEmbed && (
|
||||
<EmbedDialog
|
||||
control={embedPostControl}
|
||||
postCid={postCid}
|
||||
postUri={postUri}
|
||||
record={record}
|
||||
postAuthor={postAuthor}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SendViaChatDialog
|
||||
control={sendViaChatControl}
|
||||
onSelectChat={onSelectChatToShareTo}
|
||||
/>
|
||||
|
||||
<PostInteractionSettingsDialog
|
||||
control={postInteractionSettingsDialogControl}
|
||||
postUri={post.uri}
|
||||
rootPostUri={rootUri}
|
||||
initialThreadgateView={post.threadgate}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={quotePostDetachConfirmControl}
|
||||
title={_(msg`Detach quote post?`)}
|
||||
description={_(
|
||||
msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`,
|
||||
)}
|
||||
onConfirm={onToggleQuotePostAttachment}
|
||||
confirmButtonCta={_(msg`Yes, detach`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={hideReplyConfirmControl}
|
||||
title={_(msg`Hide this reply?`)}
|
||||
description={_(
|
||||
msg`This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others.`,
|
||||
)}
|
||||
onConfirm={onToggleReplyVisibility}
|
||||
confirmButtonCta={_(msg`Yes, hide`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={blockPromptControl}
|
||||
title={_(msg`Block Account?`)}
|
||||
description={_(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
)}
|
||||
onConfirm={onBlockAuthor}
|
||||
confirmButtonCta={_(msg`Block`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
PostDropdownMenuItems = memo(PostDropdownMenuItems)
|
||||
export {PostDropdownMenuItems}
|
||||
@@ -1,394 +0,0 @@
|
||||
import React, {memo} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type PressableStateCallbackType,
|
||||
type StyleProp,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {DISCOVER_DEBUG_DIDS, POST_CTRL_HITSLOP} from '#/lib/constants'
|
||||
import {CountWheel} from '#/lib/custom-animations/CountWheel'
|
||||
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {
|
||||
usePostLikeMutationQueue,
|
||||
usePostRepostMutationQueue,
|
||||
} from '#/state/queries/post'
|
||||
import {useRequireAuth, useSession} from '#/state/session'
|
||||
import {
|
||||
ProgressGuideAction,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
|
||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {PostDropdownBtn} from '../forms/PostDropdownBtn'
|
||||
import {formatCount} from '../numeric/format'
|
||||
import {Text} from '../text/Text'
|
||||
import * as Toast from '../Toast'
|
||||
import {RepostButton} from './RepostButton'
|
||||
|
||||
let PostCtrls = ({
|
||||
big,
|
||||
post,
|
||||
record,
|
||||
richText,
|
||||
feedContext,
|
||||
reqId,
|
||||
style,
|
||||
onPressReply,
|
||||
onPostReply,
|
||||
logContext,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
}: {
|
||||
big?: boolean
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
feedContext?: string | undefined
|
||||
reqId?: string | undefined
|
||||
style?: StyleProp<ViewStyle>
|
||||
onPressReply: () => void
|
||||
onPostReply?: (postUri: string | undefined) => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const {openComposer} = useOpenComposer()
|
||||
const {currentAccount} = useSession()
|
||||
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
|
||||
const [queueRepost, queueUnrepost] = usePostRepostMutationQueue(
|
||||
post,
|
||||
logContext,
|
||||
)
|
||||
const requireAuth = useRequireAuth()
|
||||
const loggedOutWarningPromptControl = useDialogControl()
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
const {captureAction} = useProgressGuideControls()
|
||||
const playHaptic = useHaptics()
|
||||
const gate = useGate()
|
||||
const isDiscoverDebugUser =
|
||||
IS_INTERNAL ||
|
||||
DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
|
||||
gate('debug_show_feedcontext')
|
||||
const isBlocked = Boolean(
|
||||
post.author.viewer?.blocking ||
|
||||
post.author.viewer?.blockedBy ||
|
||||
post.author.viewer?.blockingByList,
|
||||
)
|
||||
const replyDisabled = post.viewer?.replyDisabled
|
||||
|
||||
const shouldShowLoggedOutWarning = React.useMemo(() => {
|
||||
return (
|
||||
post.author.did !== currentAccount?.did &&
|
||||
!!post.author.labels?.find(label => label.val === '!no-unauthenticated')
|
||||
)
|
||||
}, [currentAccount, post])
|
||||
|
||||
const defaultCtrlColor = React.useMemo(
|
||||
() => ({
|
||||
color: t.palette.contrast_500,
|
||||
}),
|
||||
[t],
|
||||
) as StyleProp<ViewStyle>
|
||||
|
||||
const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] =
|
||||
React.useState(false)
|
||||
|
||||
const onPressToggleLike = async () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(
|
||||
_(msg`Cannot interact with a blocked user`),
|
||||
'exclamation-circle',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setHasLikeIconBeenToggled(true)
|
||||
if (!post.viewer?.like) {
|
||||
playHaptic('Light')
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionLike',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
captureAction(ProgressGuideAction.Like)
|
||||
await queueLike()
|
||||
} else {
|
||||
await queueUnlike()
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onRepost = async () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(
|
||||
_(msg`Cannot interact with a blocked user`),
|
||||
'exclamation-circle',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!post.viewer?.repost) {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionRepost',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
await queueRepost()
|
||||
} else {
|
||||
await queueUnrepost()
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onQuote = () => {
|
||||
if (isBlocked) {
|
||||
Toast.show(
|
||||
_(msg`Cannot interact with a blocked user`),
|
||||
'exclamation-circle',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionQuote',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
openComposer({
|
||||
quote: post,
|
||||
onPost: onPostReply,
|
||||
})
|
||||
}
|
||||
|
||||
const onShare = () => {
|
||||
const urip = new AtUri(post.uri)
|
||||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionShare',
|
||||
feedContext,
|
||||
reqId,
|
||||
})
|
||||
}
|
||||
|
||||
const btnStyle = React.useCallback(
|
||||
({pressed, hovered}: PressableStateCallbackType) => [
|
||||
a.gap_xs,
|
||||
a.rounded_full,
|
||||
a.flex_row,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.overflow_hidden,
|
||||
{padding: 5},
|
||||
(pressed || hovered) && t.atoms.bg_contrast_25,
|
||||
],
|
||||
[t.atoms.bg_contrast_25],
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[a.flex_row, a.justify_between, a.align_center, style]}>
|
||||
<View
|
||||
style={[
|
||||
big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
|
||||
replyDisabled ? {opacity: 0.5} : undefined,
|
||||
]}>
|
||||
<Pressable
|
||||
testID="replyBtn"
|
||||
style={btnStyle}
|
||||
onPress={() => {
|
||||
if (!replyDisabled) {
|
||||
playHaptic('Light')
|
||||
requireAuth(() => onPressReply())
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(
|
||||
msg`Reply (${plural(post.replyCount || 0, {
|
||||
one: '# reply',
|
||||
other: '# replies',
|
||||
})})`,
|
||||
)}
|
||||
accessibilityHint=""
|
||||
hitSlop={POST_CTRL_HITSLOP}>
|
||||
<Bubble
|
||||
style={[defaultCtrlColor, {pointerEvents: 'none'}]}
|
||||
width={big ? 22 : 18}
|
||||
/>
|
||||
{typeof post.replyCount !== 'undefined' && post.replyCount > 0 ? (
|
||||
<Text
|
||||
style={[
|
||||
defaultCtrlColor,
|
||||
big ? a.text_md : {fontSize: 15},
|
||||
a.user_select_none,
|
||||
]}>
|
||||
{formatCount(i18n, post.replyCount)}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||
<RepostButton
|
||||
isReposted={!!post.viewer?.repost}
|
||||
repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)}
|
||||
onRepost={onRepost}
|
||||
onQuote={onQuote}
|
||||
big={big}
|
||||
embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
|
||||
/>
|
||||
</View>
|
||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||
<Pressable
|
||||
testID="likeBtn"
|
||||
style={btnStyle}
|
||||
onPress={() => requireAuth(() => onPressToggleLike())}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
post.viewer?.like
|
||||
? _(
|
||||
msg`Unlike (${plural(post.likeCount || 0, {
|
||||
one: '# like',
|
||||
other: '# likes',
|
||||
})})`,
|
||||
)
|
||||
: _(
|
||||
msg`Like (${plural(post.likeCount || 0, {
|
||||
one: '# like',
|
||||
other: '# likes',
|
||||
})})`,
|
||||
)
|
||||
}
|
||||
accessibilityHint=""
|
||||
hitSlop={POST_CTRL_HITSLOP}>
|
||||
<AnimatedLikeIcon
|
||||
isLiked={Boolean(post.viewer?.like)}
|
||||
big={big}
|
||||
hasBeenToggled={hasLikeIconBeenToggled}
|
||||
/>
|
||||
<CountWheel
|
||||
likeCount={post.likeCount ?? 0}
|
||||
big={big}
|
||||
isLiked={Boolean(post.viewer?.like)}
|
||||
hasBeenToggled={hasLikeIconBeenToggled}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
{big && (
|
||||
<>
|
||||
<View style={a.align_center}>
|
||||
<Pressable
|
||||
testID="shareBtn"
|
||||
style={btnStyle}
|
||||
onPress={() => {
|
||||
if (shouldShowLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
} else {
|
||||
onShare()
|
||||
}
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={_(msg`Share`)}
|
||||
accessibilityHint=""
|
||||
hitSlop={POST_CTRL_HITSLOP}>
|
||||
<ArrowOutOfBox
|
||||
style={[defaultCtrlColor, {pointerEvents: 'none'}]}
|
||||
width={22}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Prompt.Basic
|
||||
control={loggedOutWarningPromptControl}
|
||||
title={_(msg`Note about sharing`)}
|
||||
description={_(
|
||||
msg`This post is only visible to logged-in users. It won't be visible to people who aren't signed in.`,
|
||||
)}
|
||||
onConfirm={onShare}
|
||||
confirmButtonCta={_(msg`Share anyway`)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||
<PostDropdownBtn
|
||||
testID="postDropdownBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
postReqId={reqId}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
</View>
|
||||
{isDiscoverDebugUser && feedContext && (
|
||||
<Pressable
|
||||
accessible={false}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onPress={e => {
|
||||
e.stopPropagation()
|
||||
Clipboard.setStringAsync(feedContext)
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
color: t.palette.contrast_400,
|
||||
fontSize: 7,
|
||||
}}>
|
||||
{feedContext}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
PostCtrls = memo(PostCtrls)
|
||||
export {PostCtrls}
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, {memo, useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {POST_CTRL_HITSLOP} from '#/lib/constants'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote'
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {formatCount} from '../numeric/format'
|
||||
|
||||
interface Props {
|
||||
isReposted: boolean
|
||||
repostCount?: number
|
||||
onRepost: () => void
|
||||
onQuote: () => void
|
||||
big?: boolean
|
||||
embeddingDisabled: boolean
|
||||
}
|
||||
|
||||
let RepostButton = ({
|
||||
isReposted,
|
||||
repostCount,
|
||||
onRepost,
|
||||
onQuote,
|
||||
big,
|
||||
embeddingDisabled,
|
||||
}: Props): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const requireAuth = useRequireAuth()
|
||||
const dialogControl = Dialog.useDialogControl()
|
||||
const playHaptic = useHaptics()
|
||||
const color = React.useMemo(
|
||||
() => ({
|
||||
color: isReposted ? t.palette.positive_600 : t.palette.contrast_500,
|
||||
}),
|
||||
[t, isReposted],
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
testID="repostBtn"
|
||||
onPress={() => {
|
||||
playHaptic('Light')
|
||||
requireAuth(() => dialogControl.open())
|
||||
}}
|
||||
onLongPress={() => {
|
||||
playHaptic('Heavy')
|
||||
requireAuth(() => onQuote())
|
||||
}}
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_xs,
|
||||
a.bg_transparent,
|
||||
{padding: 5},
|
||||
]}
|
||||
hoverStyle={t.atoms.bg_contrast_25}
|
||||
label={
|
||||
isReposted
|
||||
? _(
|
||||
msg`Undo repost (${plural(repostCount || 0, {
|
||||
one: '# repost',
|
||||
other: '# reposts',
|
||||
})})`,
|
||||
)
|
||||
: _(
|
||||
msg`Repost (${plural(repostCount || 0, {
|
||||
one: '# repost',
|
||||
other: '# reposts',
|
||||
})})`,
|
||||
)
|
||||
}
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
hitSlop={POST_CTRL_HITSLOP}>
|
||||
<Repost style={color} width={big ? 22 : 18} />
|
||||
{typeof repostCount !== 'undefined' && repostCount > 0 ? (
|
||||
<Text
|
||||
testID="repostCount"
|
||||
style={[
|
||||
color,
|
||||
big ? a.text_md : {fontSize: 15},
|
||||
isReposted && a.font_bold,
|
||||
]}>
|
||||
{formatCount(i18n, repostCount)}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</Button>
|
||||
<Dialog.Outer
|
||||
control={dialogControl}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
<RepostButtonDialogInner
|
||||
isReposted={isReposted}
|
||||
onRepost={onRepost}
|
||||
onQuote={onQuote}
|
||||
embeddingDisabled={embeddingDisabled}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
RepostButton = memo(RepostButton)
|
||||
export {RepostButton}
|
||||
|
||||
let RepostButtonDialogInner = ({
|
||||
isReposted,
|
||||
onRepost,
|
||||
onQuote,
|
||||
embeddingDisabled,
|
||||
}: {
|
||||
isReposted: boolean
|
||||
onRepost: () => void
|
||||
onQuote: () => void
|
||||
embeddingDisabled: boolean
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const playHaptic = useHaptics()
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
const onPressRepost = useCallback(() => {
|
||||
if (!isReposted) playHaptic()
|
||||
|
||||
control.close(() => {
|
||||
onRepost()
|
||||
})
|
||||
}, [control, isReposted, onRepost, playHaptic])
|
||||
|
||||
const onPressQuote = useCallback(() => {
|
||||
playHaptic()
|
||||
control.close(() => {
|
||||
onQuote()
|
||||
})
|
||||
}, [control, onQuote, playHaptic])
|
||||
|
||||
const onPressClose = useCallback(() => control.close(), [control])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Repost or quote post`)}>
|
||||
<View style={a.gap_xl}>
|
||||
<View style={a.gap_xs}>
|
||||
<Button
|
||||
style={[a.justify_start, a.px_md]}
|
||||
label={
|
||||
isReposted
|
||||
? _(msg`Remove repost`)
|
||||
: _(msg({message: `Repost`, context: 'action'}))
|
||||
}
|
||||
onPress={onPressRepost}
|
||||
size="large"
|
||||
variant="ghost"
|
||||
color="primary">
|
||||
<Repost size="lg" fill={t.palette.primary_500} />
|
||||
<Text style={[a.font_bold, a.text_xl]}>
|
||||
{isReposted ? (
|
||||
<Trans>Remove repost</Trans>
|
||||
) : (
|
||||
<Trans context="action">Repost</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={embeddingDisabled}
|
||||
testID="quoteBtn"
|
||||
style={[a.justify_start, a.px_md]}
|
||||
label={
|
||||
embeddingDisabled
|
||||
? _(msg`Quote posts disabled`)
|
||||
: _(msg`Quote post`)
|
||||
}
|
||||
onPress={onPressQuote}
|
||||
size="large"
|
||||
variant="ghost"
|
||||
color="primary">
|
||||
<Quote
|
||||
size="lg"
|
||||
fill={
|
||||
embeddingDisabled
|
||||
? t.atoms.text_contrast_low.color
|
||||
: t.palette.primary_500
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.font_bold,
|
||||
a.text_xl,
|
||||
embeddingDisabled && t.atoms.text_contrast_low,
|
||||
]}>
|
||||
{embeddingDisabled ? (
|
||||
<Trans>Quote posts disabled</Trans>
|
||||
) : (
|
||||
<Trans>Quote post</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Cancel quote post`)}
|
||||
onPress={onPressClose}
|
||||
size="large"
|
||||
variant="outline"
|
||||
color="primary">
|
||||
<ButtonText>
|
||||
<Trans>Cancel</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
RepostButtonDialogInner = memo(RepostButtonDialogInner)
|
||||
export {RepostButtonDialogInner}
|
||||
@@ -1,147 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useRequireAuth} from '#/state/session'
|
||||
import {useSession} from '#/state/session'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote'
|
||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {formatCount} from '../numeric/format'
|
||||
|
||||
interface Props {
|
||||
isReposted: boolean
|
||||
repostCount?: number
|
||||
onRepost: () => void
|
||||
onQuote: () => void
|
||||
big?: boolean
|
||||
embeddingDisabled: boolean
|
||||
}
|
||||
|
||||
export const RepostButton = ({
|
||||
isReposted,
|
||||
repostCount,
|
||||
onRepost,
|
||||
onQuote,
|
||||
big,
|
||||
embeddingDisabled,
|
||||
}: Props) => {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const {hasSession} = useSession()
|
||||
const requireAuth = useRequireAuth()
|
||||
|
||||
const color = React.useMemo(
|
||||
() => ({
|
||||
color: isReposted ? t.palette.positive_600 : t.palette.contrast_500,
|
||||
}),
|
||||
[t, isReposted],
|
||||
)
|
||||
|
||||
return hasSession ? (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Repost or quote post`)}>
|
||||
{({props, state}) => {
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
style={[
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && {
|
||||
backgroundColor: t.palette.contrast_25,
|
||||
},
|
||||
]}>
|
||||
<RepostInner
|
||||
isReposted={isReposted}
|
||||
color={color}
|
||||
repostCount={repostCount}
|
||||
big={big}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer style={{minWidth: 170}}>
|
||||
<Menu.Item
|
||||
label={isReposted ? _(msg`Undo repost`) : _(msg`Repost`)}
|
||||
testID="repostDropdownRepostBtn"
|
||||
onPress={onRepost}>
|
||||
<Menu.ItemText>
|
||||
{isReposted ? _(msg`Undo repost`) : _(msg`Repost`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Repost} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={embeddingDisabled}
|
||||
label={
|
||||
embeddingDisabled
|
||||
? _(msg`Quote posts disabled`)
|
||||
: _(msg`Quote post`)
|
||||
}
|
||||
testID="repostDropdownQuoteBtn"
|
||||
onPress={onQuote}>
|
||||
<Menu.ItemText>
|
||||
{embeddingDisabled
|
||||
? _(msg`Quote posts disabled`)
|
||||
: _(msg`Quote post`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Quote} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
</EventStopper>
|
||||
) : (
|
||||
<Button
|
||||
onPress={() => {
|
||||
requireAuth(() => {})
|
||||
}}
|
||||
label={_(msg`Repost or quote post`)}
|
||||
style={{padding: 0}}
|
||||
hoverStyle={t.atoms.bg_contrast_25}
|
||||
shape="round">
|
||||
<RepostInner
|
||||
isReposted={isReposted}
|
||||
color={color}
|
||||
repostCount={repostCount}
|
||||
big={big}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const RepostInner = ({
|
||||
isReposted,
|
||||
color,
|
||||
repostCount,
|
||||
big,
|
||||
}: {
|
||||
isReposted: boolean
|
||||
color: {color: string}
|
||||
repostCount?: number
|
||||
big?: boolean
|
||||
}) => {
|
||||
const {i18n} = useLingui()
|
||||
return (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}>
|
||||
<Repost style={color} width={big ? 22 : 18} />
|
||||
{typeof repostCount !== 'undefined' && repostCount > 0 ? (
|
||||
<Text
|
||||
testID="repostCount"
|
||||
style={[
|
||||
color,
|
||||
big ? a.text_md : {fontSize: 15},
|
||||
isReposted && [a.font_bold],
|
||||
a.user_select_none,
|
||||
]}>
|
||||
{formatCount(i18n, repostCount)}
|
||||
</Text>
|
||||
) : undefined}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user