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:
@@ -0,0 +1,200 @@
|
||||
import {ScrollView, View} from 'react-native'
|
||||
import {moderateProfile, type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {type NavigationProp} from '#/lib/routes/types'
|
||||
import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
|
||||
import {useSession} from '#/state/session'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
||||
import {Button} from '#/components/Button'
|
||||
import {useDialogContext} from '#/components/Dialog'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useSimpleVerificationState} from '#/components/verification'
|
||||
import {VerificationCheck} from '#/components/verification/VerificationCheck'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
|
||||
export function RecentChats({postUri}: {postUri: string}) {
|
||||
const control = useDialogContext()
|
||||
const {_} = useLingui()
|
||||
const {currentAccount} = useSession()
|
||||
const {data} = useListConvosQuery({status: 'accepted'})
|
||||
const convos = data?.pages[0]?.convos?.slice(0, 10)
|
||||
const moderationOpts = useModerationOpts()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onSelectChat = (convoId: string) => {
|
||||
control.close(() => {
|
||||
logger.metric('share:press:recentDm', {}, {statsig: true})
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: convoId,
|
||||
embed: postUri,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (!moderationOpts) return null
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[a.relative, a.flex_1, {marginHorizontal: tokens.space.md * -1}]}>
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={[a.flex_1, a.pt_2xs, {minHeight: 98}]}
|
||||
contentContainerStyle={[a.gap_sm, a.px_md]}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
fadingEdgeLength={64}
|
||||
nestedScrollEnabled>
|
||||
{convos && convos.length > 0 ? (
|
||||
convos.map(convo => {
|
||||
const otherMember = convo.members.find(
|
||||
member => member.did !== currentAccount?.did,
|
||||
)
|
||||
|
||||
if (!otherMember || otherMember.handle === 'missing.invalid')
|
||||
return null
|
||||
|
||||
return (
|
||||
<RecentChatItem
|
||||
key={convo.id}
|
||||
profile={otherMember}
|
||||
onPress={() => onSelectChat(convo.id)}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
<ConvoSkeleton />
|
||||
<ConvoSkeleton />
|
||||
<ConvoSkeleton />
|
||||
<ConvoSkeleton />
|
||||
<ConvoSkeleton />
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
{convos && convos.length === 0 && <NoConvos />}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const WIDTH = 80
|
||||
|
||||
function RecentChatItem({
|
||||
profile,
|
||||
onPress,
|
||||
moderationOpts,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
onPress: () => void
|
||||
moderationOpts: ModerationOpts
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
|
||||
const moderation = moderateProfile(profile, moderationOpts)
|
||||
const name = sanitizeDisplayName(
|
||||
profile.displayName || sanitizeHandle(profile.handle),
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
const verification = useSimpleVerificationState({profile})
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
label={_(msg`Send post to ${name}`)}
|
||||
style={[
|
||||
a.flex_col,
|
||||
{width: WIDTH},
|
||||
a.gap_sm,
|
||||
a.justify_start,
|
||||
a.align_center,
|
||||
]}>
|
||||
<UserAvatar
|
||||
avatar={profile.avatar}
|
||||
size={WIDTH - 8}
|
||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||
moderation={moderation.ui('avatar')}
|
||||
/>
|
||||
<View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}>
|
||||
<Text
|
||||
emoji
|
||||
style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]}
|
||||
numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
{verification.showBadge && (
|
||||
<View style={[a.pl_2xs]}>
|
||||
<VerificationCheck
|
||||
width={10}
|
||||
verifier={verification.role === 'verifier'}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function ConvoSkeleton() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_col,
|
||||
{width: WIDTH, height: WIDTH + 15},
|
||||
a.gap_xs,
|
||||
a.justify_start,
|
||||
a.align_center,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_50,
|
||||
{width: WIDTH - 8, height: WIDTH - 8},
|
||||
a.rounded_full,
|
||||
]}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg_contrast_50,
|
||||
{width: WIDTH - 8, height: 10},
|
||||
a.rounded_xs,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function NoConvos() {
|
||||
const t = useTheme()
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.px_2xl,
|
||||
]}>
|
||||
<View
|
||||
style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_high,
|
||||
a.text_center,
|
||||
a.font_bold,
|
||||
]}>
|
||||
<Trans>Start a conversation, and it will appear here.</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import {memo, useMemo} from 'react'
|
||||
import * as ExpoClipboard from 'expo-clipboard'
|
||||
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 {logger} from '#/logger'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useSession} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
|
||||
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
||||
import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlaneIcon} from '#/components/icons/PaperPlane'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||
import {RecentChats} from './RecentChats'
|
||||
import {type ShareMenuItemsProps} from './ShareMenuItems.types'
|
||||
|
||||
let ShareMenuItems = ({
|
||||
post,
|
||||
onShare: onShareProp,
|
||||
}: ShareMenuItemsProps): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const pwiWarningShareControl = useDialogControl()
|
||||
const pwiWarningCopyControl = 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 = () => {
|
||||
logger.metric('share:press:nativeShare', {}, {statsig: true})
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
onShareProp()
|
||||
}
|
||||
|
||||
const onCopyLink = () => {
|
||||
logger.metric('share:press:copyLink', {}, {statsig: true})
|
||||
const url = toShareUrl(href)
|
||||
ExpoClipboard.setUrlAsync(url).then(() =>
|
||||
Toast.show(_(msg`Copied to clipboard`), 'clipboard-check'),
|
||||
)
|
||||
onShareProp()
|
||||
}
|
||||
|
||||
const onSelectChatToShareTo = (conversation: string) => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation,
|
||||
embed: postUri,
|
||||
})
|
||||
}
|
||||
|
||||
const onShareATURI = () => {
|
||||
shareText(postUri)
|
||||
}
|
||||
|
||||
const onShareAuthorDID = () => {
|
||||
shareText(postAuthor.did)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Outer>
|
||||
{hasSession && (
|
||||
<Menu.Group>
|
||||
<Menu.ContainerItem>
|
||||
<RecentChats postUri={postUri} />
|
||||
</Menu.ContainerItem>
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={() => {
|
||||
logger.metric('share:press:openDmSearch', {}, {statsig: true})
|
||||
sendViaChatControl.open()
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={PaperPlaneIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
)}
|
||||
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShareBtn"
|
||||
label={_(msg`Share via...`)}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
pwiWarningShareControl.open()
|
||||
} else {
|
||||
onSharePost()
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Share via...</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ArrowOutOfBoxIcon} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownShareBtn"
|
||||
label={_(msg`Copy link to post`)}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
pwiWarningCopyControl.open()
|
||||
} else {
|
||||
onCopyLink()
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy link to post</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ChainLinkIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
|
||||
{devModeEnabled && (
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postAtUriShareBtn"
|
||||
label={_(msg`Share post at:// URI`)}
|
||||
onPress={onShareATURI}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Share post at:// URI</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postAuthorDIDShareBtn"
|
||||
label={_(msg`Share author DID`)}
|
||||
onPress={onShareAuthorDID}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Share author DID</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
)}
|
||||
</Menu.Outer>
|
||||
|
||||
<Prompt.Basic
|
||||
control={pwiWarningShareControl}
|
||||
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`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={pwiWarningCopyControl}
|
||||
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={onCopyLink}
|
||||
confirmButtonCta={_(msg`Copy anyway`)}
|
||||
/>
|
||||
|
||||
<SendViaChatDialog
|
||||
control={sendViaChatControl}
|
||||
onSelectChat={onSelectChatToShareTo}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ShareMenuItems = memo(ShareMenuItems)
|
||||
export {ShareMenuItems}
|
||||
@@ -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<AppBskyFeedDefs.PostView>
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShare: () => void
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
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 {logger} from '#/logger'
|
||||
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 CodeBracketsIcon} 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<NavigationProp>()
|
||||
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 onCopyLink = () => {
|
||||
logger.metric('share:press:copyLink', {}, {statsig: true})
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
onShareProp()
|
||||
}
|
||||
|
||||
const onSelectChatToShareTo = (conversation: string) => {
|
||||
logger.metric('share:press:dmSelected', {}, {statsig: true})
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation,
|
||||
embed: postUri,
|
||||
})
|
||||
}
|
||||
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
|
||||
const onShareATURI = () => {
|
||||
shareText(postUri)
|
||||
}
|
||||
|
||||
const onShareAuthorDID = () => {
|
||||
shareText(postAuthor.did)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Outer>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShareBtn"
|
||||
label={_(msg`Copy link to post`)}
|
||||
onPress={() => {
|
||||
if (showLoggedOutWarning) {
|
||||
loggedOutWarningPromptControl.open()
|
||||
} else {
|
||||
onCopyLink()
|
||||
}
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy link to post</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ChainLinkIcon} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
{hasSession && (
|
||||
<Menu.Item
|
||||
testID="postDropdownSendViaDMBtn"
|
||||
label={_(msg`Send via direct message`)}
|
||||
onPress={() => {
|
||||
logger.metric('share:press:openDmSearch', {}, {statsig: true})
|
||||
sendViaChatControl.open()
|
||||
}}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Send via direct message</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Send} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canEmbed && (
|
||||
<Menu.Item
|
||||
testID="postDropdownEmbedBtn"
|
||||
label={_(msg`Embed post`)}
|
||||
onPress={() => {
|
||||
logger.metric('share:press:embed', {}, {statsig: true})
|
||||
embedPostControl.open()
|
||||
}}>
|
||||
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={CodeBracketsIcon} 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>
|
||||
<Trans>Copy post at:// URI</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postAuthorDIDShareBtn"
|
||||
label={_(msg`Copy author DID`)}
|
||||
onPress={onShareAuthorDID}>
|
||||
<Menu.ItemText>
|
||||
<Trans>Copy author DID</Trans>
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
</Menu.Outer>
|
||||
|
||||
<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={onCopyLink}
|
||||
confirmButtonCta={_(msg`Share anyway`)}
|
||||
/>
|
||||
|
||||
{canEmbed && (
|
||||
<EmbedDialog
|
||||
control={embedPostControl}
|
||||
postCid={postCid}
|
||||
postUri={postUri}
|
||||
record={record}
|
||||
postAuthor={postAuthor}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SendViaChatDialog
|
||||
control={sendViaChatControl}
|
||||
onSelectChat={onSelectChatToShareTo}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
ShareMenuItems = memo(ShareMenuItems)
|
||||
export {ShareMenuItems}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
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 {logger} from '#/logger'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {native} from '#/alf'
|
||||
import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox'
|
||||
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {PostControlButton, PostControlButtonIcon} from '../PostControlButton'
|
||||
import {ShareMenuItems} from './ShareMenuItems'
|
||||
|
||||
let ShareMenuButton = ({
|
||||
testID,
|
||||
post,
|
||||
big,
|
||||
record,
|
||||
richText,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
onShare,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
big?: boolean
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShare: () => void
|
||||
}): React.ReactNode => {
|
||||
const {_} = useLingui()
|
||||
const gate = useGate()
|
||||
|
||||
const ShareIcon = gate('alt_share_icon')
|
||||
? ArrowShareRightIcon
|
||||
: ArrowOutOfBoxIcon
|
||||
|
||||
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)
|
||||
|
||||
logger.metric(
|
||||
'share:open',
|
||||
{context: big ? 'thread' : 'feed'},
|
||||
{statsig: true},
|
||||
)
|
||||
},
|
||||
}),
|
||||
[menuControl, setHasBeenOpen, big],
|
||||
)
|
||||
|
||||
const onNativeLongPress = () => {
|
||||
logger.metric('share:press:nativeShare', {}, {statsig: true})
|
||||
const urip = new AtUri(post.uri)
|
||||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
onShare()
|
||||
}
|
||||
|
||||
return (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root control={lazyMenuControl}>
|
||||
<Menu.Trigger label={_(msg`Open share menu`)}>
|
||||
{({props}) => {
|
||||
return (
|
||||
<PostControlButton
|
||||
testID="postShareBtn"
|
||||
big={big}
|
||||
label={props.accessibilityLabel}
|
||||
{...props}
|
||||
onLongPress={native(onNativeLongPress)}>
|
||||
<PostControlButtonIcon icon={ShareIcon} />
|
||||
</PostControlButton>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
{hasBeenOpen && (
|
||||
// Lazily initialized. Once mounted, they stay mounted.
|
||||
<ShareMenuItems
|
||||
testID={testID}
|
||||
post={post}
|
||||
record={record}
|
||||
richText={richText}
|
||||
timestamp={timestamp}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShare={onShare}
|
||||
/>
|
||||
)}
|
||||
</Menu.Root>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
|
||||
ShareMenuButton = memo(ShareMenuButton)
|
||||
export {ShareMenuButton}
|
||||
Reference in New Issue
Block a user