diff --git a/assets/icons/chainLink_stroke2_corner0_rounded.svg b/assets/icons/chainLink_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..c1626cc618
--- /dev/null
+++ b/assets/icons/chainLink_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/icons/ChainLink.tsx b/src/components/icons/ChainLink.tsx
new file mode 100644
index 0000000000..be19b556a4
--- /dev/null
+++ b/src/components/icons/ChainLink.tsx
@@ -0,0 +1,5 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const ChainLink_Stroke2_Corner0_Rounded = createSinglePathSVG({
+ path: 'M18.535 5.465a5.003 5.003 0 0 0-7.076 0l-.005.005-.752.742a1 1 0 1 1-1.404-1.424l.749-.74a7.003 7.003 0 0 1 9.904 9.905l-.002.003-.737.746a1 1 0 1 1-1.424-1.404l.747-.757a5.003 5.003 0 0 0 0-7.076ZM6.202 9.288a1 1 0 0 1 .01 1.414l-.747.757a5.003 5.003 0 1 0 7.076 7.076l.005-.005.752-.742a1 1 0 1 1 1.404 1.424l-.746.737-.003.002a7.003 7.003 0 0 1-9.904-9.904l.74-.75a1 1 0 0 1 1.413-.009Zm8.505.005a1 1 0 0 1 0 1.414l-4 4a1 1 0 0 1-1.414-1.414l4-4a1 1 0 0 1 1.414 0Z',
+})
diff --git a/src/components/post-ctrls/PostCtrlButton.tsx b/src/components/post-ctrls/PostCtrlButton.tsx
new file mode 100644
index 0000000000..17566b93bb
--- /dev/null
+++ b/src/components/post-ctrls/PostCtrlButton.tsx
@@ -0,0 +1,125 @@
+import React, {useContext, useMemo} from 'react'
+import {type GestureResponderEvent, type View} from 'react-native'
+
+import {POST_CTRL_HITSLOP} from '#/lib/constants'
+import {useHaptics} from '#/lib/haptics'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, type ButtonProps} from '#/components/Button'
+import {type Props as SVGIconProps} from '#/components/icons/common'
+import {Text, type TextProps} from '#/components/Typography'
+
+const PostCtrlContext = React.createContext<{
+ big?: boolean
+ active?: boolean
+ color?: {color: string}
+}>({})
+
+// Base button style, which the the other ones extend
+export const PostCtrlButton = React.forwardRef<
+ View,
+ ButtonProps & {
+ active?: boolean
+ big?: boolean
+ color?: string
+ activeColor?: string
+ }
+>(
+ (
+ {onPress, onLongPress, children, big, active, activeColor, ...props},
+ ref,
+ ) => {
+ const t = useTheme()
+ const playHaptic = useHaptics()
+
+ const ctx = React.useMemo(
+ () => ({
+ big,
+ active,
+ color: {
+ color: activeColor && active ? activeColor : t.palette.contrast_500,
+ },
+ }),
+ [big, active, activeColor, t.palette.contrast_500],
+ )
+
+ const style = useMemo(
+ () => [
+ a.flex_row,
+ a.align_center,
+ a.gap_xs,
+ a.bg_transparent,
+ {padding: 5},
+ ],
+ [],
+ )
+
+ const handlePress = useMemo(() => {
+ if (!onPress) return
+ return (evt: GestureResponderEvent) => {
+ playHaptic('Light')
+ onPress(evt)
+ }
+ }, [onPress, playHaptic])
+
+ const handleLongPress = useMemo(() => {
+ if (!onLongPress) return
+ return (evt: GestureResponderEvent) => {
+ playHaptic('Heavy')
+ onLongPress(evt)
+ }
+ }, [onLongPress, playHaptic])
+
+ return (
+
+ )
+ },
+)
+PostCtrlButton.displayName = 'PostCtrlButton'
+
+export function PostCtrlButtonIcon({
+ icon: Comp,
+}: {
+ icon: React.ComponentType
+}) {
+ const {big, color} = useContext(PostCtrlContext)
+
+ return
+}
+
+export function PostCtrlButtonText({style, ...props}: TextProps) {
+ const {big, active, color} = useContext(PostCtrlContext)
+
+ return (
+
+ )
+}
diff --git a/src/components/post-ctrls/PostCtrls.tsx b/src/components/post-ctrls/PostCtrls.tsx
index a74a1c9c4f..d7acb46488 100644
--- a/src/components/post-ctrls/PostCtrls.tsx
+++ b/src/components/post-ctrls/PostCtrls.tsx
@@ -1,32 +1,22 @@
import React, {memo} from 'react'
-import {
- Pressable,
- type PressableStateCallbackType,
- type StyleProp,
- View,
- type ViewStyle,
-} from 'react-native'
+import {Pressable, 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 {DISCOVER_DEBUG_DIDS} 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 {
@@ -41,13 +31,16 @@ import {
import {formatCount} from '#/view/com/util/numeric/format'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useTheme} from '#/alf'
-import {useDialogControl} from '#/components/Dialog'
-import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
+import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
-import * as Prompt from '#/components/Prompt'
-import {PostDropdownBtn} from './PostDropdownBtn'
+import {
+ PostCtrlButton,
+ PostCtrlButtonIcon,
+ PostCtrlButtonText,
+} from './PostCtrlButton'
+import {PostMenuButton} from './PostMenuButton'
import {RepostButton} from './RepostButton'
+import {ShareMenuButton} from './ShareMenuButton'
let PostCtrls = ({
big,
@@ -77,6 +70,7 @@ let PostCtrls = ({
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
}): React.ReactNode => {
const t = useTheme()
+ const {gtMobile} = useBreakpoints()
const {_, i18n} = useLingui()
const {openComposer} = useOpenComposer()
const {currentAccount} = useSession()
@@ -86,7 +80,6 @@ let PostCtrls = ({
logContext,
)
const requireAuth = useRequireAuth()
- const loggedOutWarningPromptControl = useDialogControl()
const {sendInteraction} = useFeedFeedbackContext()
const {captureAction} = useProgressGuideControls()
const playHaptic = useHaptics()
@@ -102,20 +95,6 @@ let PostCtrls = ({
)
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
-
const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] =
React.useState(false)
@@ -200,10 +179,6 @@ let PostCtrls = ({
}
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',
@@ -212,20 +187,6 @@ let PostCtrls = ({
})
}
- 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 (
- {
- if (!replyDisabled) {
- playHaptic('Light')
- requireAuth(() => onPressReply())
- }
- }}
- accessibilityRole="button"
- accessibilityLabel={_(
+ onPress={
+ !replyDisabled ? () => requireAuth(() => onPressReply()) : undefined
+ }
+ label={_(
msg`Reply (${plural(post.replyCount || 0, {
one: '# reply',
other: '# replies',
})})`,
)}
- accessibilityHint=""
- hitSlop={POST_CTRL_HITSLOP}>
-
- {typeof post.replyCount !== 'undefined' && post.replyCount > 0 ? (
-
+ big={big}>
+
+ {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
+
{formatCount(i18n, post.replyCount)}
-
- ) : undefined}
-
+
+ )}
+
- requireAuth(() => onPressToggleLike())}
- accessibilityRole="button"
- accessibilityLabel={
+ label={
post.viewer?.like
? _(
msg`Unlike (${plural(post.likeCount || 0, {
@@ -297,9 +243,7 @@ let PostCtrls = ({
other: '# likes',
})})`,
)
- }
- accessibilityHint=""
- hitSlop={POST_CTRL_HITSLOP}>
+ }>
-
+
+
+
+
- {big && (
- <>
-
- {
- if (shouldShowLoggedOutWarning) {
- loggedOutWarningPromptControl.open()
- } else {
- onShare()
- }
- }}
- accessibilityRole="button"
- accessibilityLabel={_(msg`Share`)}
- accessibilityHint=""
- hitSlop={POST_CTRL_HITSLOP}>
-
-
-
-
- >
- )}
-
postFeedContext: string | undefined
postReqId: string | undefined
+ big?: boolean
record: AppBskyFeedPost.Record
richText: RichTextAPI
- style?: StyleProp
- 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(
@@ -73,31 +60,21 @@ let PostDropdownBtn = ({
- {({props, state}) => {
+ {({props}) => {
return (
-
-
-
+
+
+
)
}}
{hasBeenOpen && (
// Lazily initialized. Once mounted, they stay mounted.
- void
}): React.ReactNode => {
const {hasSession, currentAccount} = useSession()
- const {gtMobile} = useBreakpoints()
const {_} = useLingui()
const langPrefs = useLanguagePrefs()
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
@@ -134,9 +126,6 @@ let PostDropdownMenuItems = ({
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()
@@ -273,14 +262,6 @@ let PostDropdownMenuItems = ({
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',
@@ -308,13 +289,6 @@ let PostDropdownMenuItems = ({
}
}
- const onSelectChatToShareTo = (conversation: string) => {
- navigation.navigate('MessagesConversation', {
- conversation,
- embed: postUri,
- })
- }
-
const onToggleQuotePostAttachment = async () => {
if (!quoteEmbed) return
@@ -341,7 +315,6 @@ let PostDropdownMenuItems = ({
}
const canHidePostForMe = !isAuthor && !isPostHidden
- const canEmbed = isWeb && gtMobile && !hideInPWI
const canHideReplyForEveryone =
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
@@ -482,44 +455,6 @@ let PostDropdownMenuItems = ({
>
)}
-
- {hasSession && (
- sendViaChatControl.open()}>
-
- Send via direct message
-
-
-
- )}
-
- {
- if (showLoggedOutWarning) {
- loggedOutWarningPromptControl.open()
- } else {
- onSharePost()
- }
- }}>
-
- {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
-
-
-
-
- {canEmbed && (
- embedPostControl.open()}>
- {_(msg`Embed post`)}
-
-
- )}
{hasSession && feedFeedback.enabled && (
@@ -802,32 +737,6 @@ let PostDropdownMenuItems = ({
}}
/>
-
-
- {canEmbed && (
-
- )}
-
-
-
)
}
-PostDropdownMenuItems = memo(PostDropdownMenuItems)
-export {PostDropdownMenuItems}
+PostMenuItems = memo(PostMenuItems)
+export {PostMenuItems}
diff --git a/src/components/post-ctrls/RepostButton.tsx b/src/components/post-ctrls/RepostButton.tsx
index b0566ddae1..1970c7c770 100644
--- a/src/components/post-ctrls/RepostButton.tsx
+++ b/src/components/post-ctrls/RepostButton.tsx
@@ -1,9 +1,8 @@
-import React, {memo, useCallback} from 'react'
+import {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 {formatCount} from '#/view/com/util/numeric/format'
@@ -13,6 +12,11 @@ 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 {
+ PostCtrlButton,
+ PostCtrlButtonIcon,
+ PostCtrlButtonText,
+} from './PostCtrlButton'
interface Props {
isReposted: boolean
@@ -35,33 +39,16 @@ let RepostButton = ({
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 (
<>
-
+
+ )}
+
diff --git a/src/components/post-ctrls/RepostButton.web.tsx b/src/components/post-ctrls/RepostButton.web.tsx
index 3140e3e4b8..ace4c360ee 100644
--- a/src/components/post-ctrls/RepostButton.web.tsx
+++ b/src/components/post-ctrls/RepostButton.web.tsx
@@ -1,5 +1,3 @@
-import React from 'react'
-import {Pressable, View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -7,12 +5,15 @@ import {useRequireAuth} from '#/state/session'
import {useSession} from '#/state/session'
import {EventStopper} from '#/view/com/util/EventStopper'
import {formatCount} from '#/view/com/util/numeric/format'
-import {atoms as a, useTheme} from '#/alf'
-import {Button} from '#/components/Button'
+import {useTheme} from '#/alf'
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 {
+ PostCtrlButton,
+ PostCtrlButtonIcon,
+ PostCtrlButtonText,
+} from './PostCtrlButton'
interface Props {
isReposted: boolean
@@ -32,38 +33,30 @@ export const RepostButton = ({
embeddingDisabled,
}: Props) => {
const t = useTheme()
- const {_} = useLingui()
+ const {_, i18n} = 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 ? (
- {({props, state}) => {
+ {({props}) => {
return (
-
-
-
+
+
+ {typeof repostCount !== 'undefined' && repostCount > 0 && (
+
+ {formatCount(i18n, repostCount)}
+
+ )}
+
)
}}
@@ -97,51 +90,18 @@ export const RepostButton = ({
) : (
-
- )
-}
-
-const RepostInner = ({
- isReposted,
- color,
- repostCount,
- big,
-}: {
- isReposted: boolean
- color: {color: string}
- repostCount?: number
- big?: boolean
-}) => {
- const {i18n} = useLingui()
- return (
-
-
- {typeof repostCount !== 'undefined' && repostCount > 0 ? (
-
+ big={big}>
+
+ {typeof repostCount !== 'undefined' && repostCount > 0 && (
+
{formatCount(i18n, repostCount)}
-
- ) : undefined}
-
+
+ )}
+
)
}
diff --git a/src/components/post-ctrls/ShareMenuButton.tsx b/src/components/post-ctrls/ShareMenuButton.tsx
new file mode 100644
index 0000000000..123ac1a4db
--- /dev/null
+++ b/src/components/post-ctrls/ShareMenuButton.tsx
@@ -0,0 +1,89 @@
+import {memo, useMemo, useState} from 'react'
+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 {type Shadow} from '#/state/cache/post-shadow'
+import {EventStopper} from '#/view/com/util/EventStopper'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
+import {useMenuControl} from '#/components/Menu'
+import * as Menu from '#/components/Menu'
+import {PostCtrlButton, PostCtrlButtonIcon} from './PostCtrlButton'
+import {ShareMenuItems} from './ShareMenuItems'
+
+let ShareMenuButton = ({
+ testID,
+ post,
+ big,
+ record,
+ richText,
+ timestamp,
+ threadgateRecord,
+ onShare,
+}: {
+ testID: string
+ post: Shadow
+ big?: boolean
+ record: AppBskyFeedPost.Record
+ richText: RichTextAPI
+ timestamp: string
+ threadgateRecord?: AppBskyFeedThreadgate.Record
+ onShare: () => void
+}): React.ReactNode => {
+ const {_} = useLingui()
+
+ 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 (
+
+
+
+ {({props}) => {
+ return (
+
+
+
+ )
+ }}
+
+ {hasBeenOpen && (
+ // Lazily initialized. Once mounted, they stay mounted.
+
+ )}
+
+
+ )
+}
+
+ShareMenuButton = memo(ShareMenuButton)
+export {ShareMenuButton}
diff --git a/src/components/post-ctrls/ShareMenuItems.tsx b/src/components/post-ctrls/ShareMenuItems.tsx
new file mode 100644
index 0000000000..82c40f058b
--- /dev/null
+++ b/src/components/post-ctrls/ShareMenuItems.tsx
@@ -0,0 +1,150 @@
+import {memo, useMemo} from 'react'
+import {AtUri} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+
+import {makeProfileLink} from '#/lib/routes/links'
+import {type NavigationProp} from '#/lib/routes/types'
+import {shareText, shareUrl} from '#/lib/sharing'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useSession} from '#/state/session'
+import {useDialogControl} from '#/components/Dialog'
+import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
+import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
+import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
+import * as Menu from '#/components/Menu'
+import * as Prompt from '#/components/Prompt'
+import {useDevMode} from '#/storage/hooks/dev-mode'
+import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBoxIcon} from '../icons/ArrowOutOfBox'
+import {type ShareMenuItemsProps} from './ShareMenuItems.types'
+
+let ShareMenuItems = ({
+ post,
+ onShare: onShareProp,
+}: ShareMenuItemsProps): React.ReactNode => {
+ const {hasSession, currentAccount} = useSession()
+ const {_} = useLingui()
+ const navigation = useNavigation()
+ const loggedOutWarningPromptControl = useDialogControl()
+ const sendViaChatControl = useDialogControl()
+ const [devModeEnabled] = useDevMode()
+
+ const postUri = post.uri
+ const postAuthor = useProfileShadow(post.author)
+
+ const href = useMemo(() => {
+ const urip = new AtUri(postUri)
+ return makeProfileLink(postAuthor, 'post', urip.rkey)
+ }, [postUri, postAuthor])
+
+ const hideInPWI = useMemo(() => {
+ return !!postAuthor.labels?.find(
+ label => label.val === '!no-unauthenticated',
+ )
+ }, [postAuthor])
+
+ const showLoggedOutWarning =
+ postAuthor.did !== currentAccount?.did && hideInPWI
+
+ const onSharePost = () => {
+ const url = toShareUrl(href)
+ shareUrl(url)
+ onShareProp()
+ }
+
+ const onSelectChatToShareTo = (conversation: string) => {
+ navigation.navigate('MessagesConversation', {
+ conversation,
+ embed: postUri,
+ })
+ }
+
+ const onShareATURI = () => {
+ shareText(postUri)
+ }
+
+ const onShareAuthorDID = () => {
+ shareText(postAuthor.did)
+ }
+
+ return (
+ <>
+
+
+ {
+ if (showLoggedOutWarning) {
+ loggedOutWarningPromptControl.open()
+ } else {
+ onSharePost()
+ }
+ }}>
+
+ Share post
+
+
+
+
+ {hasSession && (
+ sendViaChatControl.open()}>
+
+ Send via direct message
+
+
+
+ )}
+
+
+ {devModeEnabled && (
+ <>
+
+
+
+
+ Share post at:// URI
+
+
+
+
+
+ Share author DID
+
+
+
+
+ >
+ )}
+
+
+
+
+
+ >
+ )
+}
+ShareMenuItems = memo(ShareMenuItems)
+export {ShareMenuItems}
diff --git a/src/components/post-ctrls/ShareMenuItems.types.tsx b/src/components/post-ctrls/ShareMenuItems.types.tsx
new file mode 100644
index 0000000000..5bc2a8fb6e
--- /dev/null
+++ b/src/components/post-ctrls/ShareMenuItems.types.tsx
@@ -0,0 +1,22 @@
+import {type PressableProps, type StyleProp, type ViewStyle} from 'react-native'
+import {
+ type AppBskyFeedDefs,
+ type AppBskyFeedPost,
+ type AppBskyFeedThreadgate,
+ type RichText as RichTextAPI,
+} from '@atproto/api'
+
+import {type Shadow} from '#/state/cache/post-shadow'
+
+export interface ShareMenuItemsProps {
+ testID: string
+ post: Shadow
+ record: AppBskyFeedPost.Record
+ richText: RichTextAPI
+ style?: StyleProp
+ hitSlop?: PressableProps['hitSlop']
+ size?: 'lg' | 'md' | 'sm'
+ timestamp: string
+ threadgateRecord?: AppBskyFeedThreadgate.Record
+ onShare: () => void
+}
diff --git a/src/components/post-ctrls/ShareMenuItems.web.tsx b/src/components/post-ctrls/ShareMenuItems.web.tsx
new file mode 100644
index 0000000000..5a8a2e7db7
--- /dev/null
+++ b/src/components/post-ctrls/ShareMenuItems.web.tsx
@@ -0,0 +1,183 @@
+import {memo, useMemo} from 'react'
+import {AtUri} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useNavigation} from '@react-navigation/native'
+import type React from 'react'
+
+import {makeProfileLink} from '#/lib/routes/links'
+import {type NavigationProp} from '#/lib/routes/types'
+import {shareText, shareUrl} from '#/lib/sharing'
+import {toShareUrl} from '#/lib/strings/url-helpers'
+import {isWeb} from '#/platform/detection'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useSession} from '#/state/session'
+import {useBreakpoints} from '#/alf'
+import {useDialogControl} from '#/components/Dialog'
+import {EmbedDialog} from '#/components/dialogs/Embed'
+import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
+import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
+import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
+import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
+import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
+import * as Menu from '#/components/Menu'
+import * as Prompt from '#/components/Prompt'
+import {useDevMode} from '#/storage/hooks/dev-mode'
+import {type ShareMenuItemsProps} from './ShareMenuItems.types'
+
+let ShareMenuItems = ({
+ post,
+ record,
+ timestamp,
+ onShare: onShareProp,
+}: ShareMenuItemsProps): React.ReactNode => {
+ const {hasSession, currentAccount} = useSession()
+ const {gtMobile} = useBreakpoints()
+ const {_} = useLingui()
+ const navigation = useNavigation()
+ const loggedOutWarningPromptControl = useDialogControl()
+ const embedPostControl = useDialogControl()
+ const sendViaChatControl = useDialogControl()
+ const [devModeEnabled] = useDevMode()
+
+ const postUri = post.uri
+ const postCid = post.cid
+ const postAuthor = useProfileShadow(post.author)
+
+ const href = useMemo(() => {
+ const urip = new AtUri(postUri)
+ return makeProfileLink(postAuthor, 'post', urip.rkey)
+ }, [postUri, postAuthor])
+
+ const hideInPWI = useMemo(() => {
+ return !!postAuthor.labels?.find(
+ label => label.val === '!no-unauthenticated',
+ )
+ }, [postAuthor])
+
+ const showLoggedOutWarning =
+ postAuthor.did !== currentAccount?.did && hideInPWI
+
+ const onSharePost = () => {
+ const url = toShareUrl(href)
+ shareUrl(url)
+ onShareProp()
+ }
+
+ const onSelectChatToShareTo = (conversation: string) => {
+ navigation.navigate('MessagesConversation', {
+ conversation,
+ embed: postUri,
+ })
+ }
+
+ const canEmbed = isWeb && gtMobile && !hideInPWI
+
+ const onShareATURI = () => {
+ shareText(postUri)
+ }
+
+ const onShareAuthorDID = () => {
+ shareText(postAuthor.did)
+ }
+
+ return (
+ <>
+
+
+ {
+ if (showLoggedOutWarning) {
+ loggedOutWarningPromptControl.open()
+ } else {
+ onSharePost()
+ }
+ }}>
+
+ Copy link to post
+
+
+
+
+ {hasSession && (
+ sendViaChatControl.open()}>
+
+ Send via direct message
+
+
+
+ )}
+
+ {canEmbed && (
+ embedPostControl.open()}>
+ {_(msg`Embed post`)}
+
+
+ )}
+
+
+ {devModeEnabled && (
+ <>
+
+
+
+
+ Copy post at:// URI
+
+
+
+
+
+ Copy author DID
+
+
+
+
+ >
+ )}
+
+
+
+
+ {canEmbed && (
+
+ )}
+
+
+ >
+ )
+}
+ShareMenuItems = memo(ShareMenuItems)
+export {ShareMenuItems}