restructure post controls, basic share menu
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
@@ -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',
|
||||||
|
})
|
||||||
@@ -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 (
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
onPress={handlePress}
|
||||||
|
onLongPress={handleLongPress}
|
||||||
|
style={style}
|
||||||
|
hoverStyle={t.atoms.bg_contrast_25}
|
||||||
|
shape="round"
|
||||||
|
variant="ghost"
|
||||||
|
color="secondary"
|
||||||
|
hitSlop={POST_CTRL_HITSLOP}
|
||||||
|
{...props}>
|
||||||
|
{typeof children === 'function' ? (
|
||||||
|
args => (
|
||||||
|
<PostCtrlContext.Provider value={ctx}>
|
||||||
|
{children(args)}
|
||||||
|
</PostCtrlContext.Provider>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<PostCtrlContext.Provider value={ctx}>
|
||||||
|
{children}
|
||||||
|
</PostCtrlContext.Provider>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
PostCtrlButton.displayName = 'PostCtrlButton'
|
||||||
|
|
||||||
|
export function PostCtrlButtonIcon({
|
||||||
|
icon: Comp,
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType<SVGIconProps>
|
||||||
|
}) {
|
||||||
|
const {big, color} = useContext(PostCtrlContext)
|
||||||
|
|
||||||
|
return <Comp style={[color, a.pointer_events_none]} width={big ? 22 : 18} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostCtrlButtonText({style, ...props}: TextProps) {
|
||||||
|
const {big, active, color} = useContext(PostCtrlContext)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
color,
|
||||||
|
big ? a.text_md : {fontSize: 15},
|
||||||
|
active && a.font_bold,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,32 +1,22 @@
|
|||||||
import React, {memo} from 'react'
|
import React, {memo} from 'react'
|
||||||
import {
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
Pressable,
|
|
||||||
type PressableStateCallbackType,
|
|
||||||
type StyleProp,
|
|
||||||
View,
|
|
||||||
type ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import * as Clipboard from 'expo-clipboard'
|
import * as Clipboard from 'expo-clipboard'
|
||||||
import {
|
import {
|
||||||
type AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
type AppBskyFeedPost,
|
type AppBskyFeedPost,
|
||||||
type AppBskyFeedThreadgate,
|
type AppBskyFeedThreadgate,
|
||||||
AtUri,
|
|
||||||
type RichText as RichTextAPI,
|
type RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, plural} from '@lingui/macro'
|
import {msg, plural} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {IS_INTERNAL} from '#/lib/app-info'
|
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 {CountWheel} from '#/lib/custom-animations/CountWheel'
|
||||||
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
|
||||||
import {shareUrl} from '#/lib/sharing'
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
|
||||||
import {type Shadow} from '#/state/cache/types'
|
import {type Shadow} from '#/state/cache/types'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
import {
|
import {
|
||||||
@@ -41,13 +31,16 @@ import {
|
|||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
import {formatCount} from '#/view/com/util/numeric/format'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, 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 {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import {
|
||||||
import {PostDropdownBtn} from './PostDropdownBtn'
|
PostCtrlButton,
|
||||||
|
PostCtrlButtonIcon,
|
||||||
|
PostCtrlButtonText,
|
||||||
|
} from './PostCtrlButton'
|
||||||
|
import {PostMenuButton} from './PostMenuButton'
|
||||||
import {RepostButton} from './RepostButton'
|
import {RepostButton} from './RepostButton'
|
||||||
|
import {ShareMenuButton} from './ShareMenuButton'
|
||||||
|
|
||||||
let PostCtrls = ({
|
let PostCtrls = ({
|
||||||
big,
|
big,
|
||||||
@@ -77,6 +70,7 @@ let PostCtrls = ({
|
|||||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {openComposer} = useOpenComposer()
|
const {openComposer} = useOpenComposer()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
@@ -86,7 +80,6 @@ let PostCtrls = ({
|
|||||||
logContext,
|
logContext,
|
||||||
)
|
)
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
const loggedOutWarningPromptControl = useDialogControl()
|
|
||||||
const {sendInteraction} = useFeedFeedbackContext()
|
const {sendInteraction} = useFeedFeedbackContext()
|
||||||
const {captureAction} = useProgressGuideControls()
|
const {captureAction} = useProgressGuideControls()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
@@ -102,20 +95,6 @@ let PostCtrls = ({
|
|||||||
)
|
)
|
||||||
const replyDisabled = post.viewer?.replyDisabled
|
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] =
|
const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] =
|
||||||
React.useState(false)
|
React.useState(false)
|
||||||
|
|
||||||
@@ -200,10 +179,6 @@ let PostCtrls = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onShare = () => {
|
const onShare = () => {
|
||||||
const urip = new AtUri(post.uri)
|
|
||||||
const href = makeProfileLink(post.author, 'post', urip.rkey)
|
|
||||||
const url = toShareUrl(href)
|
|
||||||
shareUrl(url)
|
|
||||||
sendInteraction({
|
sendInteraction({
|
||||||
item: post.uri,
|
item: post.uri,
|
||||||
event: 'app.bsky.feed.defs#interactionShare',
|
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 (
|
return (
|
||||||
<View style={[a.flex_row, a.justify_between, a.align_center, style]}>
|
<View style={[a.flex_row, a.justify_between, a.align_center, style]}>
|
||||||
<View
|
<View
|
||||||
@@ -233,39 +194,25 @@ let PostCtrls = ({
|
|||||||
big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
|
big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
|
||||||
replyDisabled ? {opacity: 0.5} : undefined,
|
replyDisabled ? {opacity: 0.5} : undefined,
|
||||||
]}>
|
]}>
|
||||||
<Pressable
|
<PostCtrlButton
|
||||||
testID="replyBtn"
|
testID="replyBtn"
|
||||||
style={btnStyle}
|
onPress={
|
||||||
onPress={() => {
|
!replyDisabled ? () => requireAuth(() => onPressReply()) : undefined
|
||||||
if (!replyDisabled) {
|
}
|
||||||
playHaptic('Light')
|
label={_(
|
||||||
requireAuth(() => onPressReply())
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(
|
|
||||||
msg`Reply (${plural(post.replyCount || 0, {
|
msg`Reply (${plural(post.replyCount || 0, {
|
||||||
one: '# reply',
|
one: '# reply',
|
||||||
other: '# replies',
|
other: '# replies',
|
||||||
})})`,
|
})})`,
|
||||||
)}
|
)}
|
||||||
accessibilityHint=""
|
big={big}>
|
||||||
hitSlop={POST_CTRL_HITSLOP}>
|
<PostCtrlButtonIcon icon={Bubble} />
|
||||||
<Bubble
|
{typeof post.replyCount !== 'undefined' && post.replyCount > 0 && (
|
||||||
style={[defaultCtrlColor, {pointerEvents: 'none'}]}
|
<PostCtrlButtonText>
|
||||||
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)}
|
{formatCount(i18n, post.replyCount)}
|
||||||
</Text>
|
</PostCtrlButtonText>
|
||||||
) : undefined}
|
)}
|
||||||
</Pressable>
|
</PostCtrlButton>
|
||||||
</View>
|
</View>
|
||||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||||
<RepostButton
|
<RepostButton
|
||||||
@@ -278,12 +225,11 @@ let PostCtrls = ({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||||
<Pressable
|
<PostCtrlButton
|
||||||
testID="likeBtn"
|
testID="likeBtn"
|
||||||
style={btnStyle}
|
big={big}
|
||||||
onPress={() => requireAuth(() => onPressToggleLike())}
|
onPress={() => requireAuth(() => onPressToggleLike())}
|
||||||
accessibilityRole="button"
|
label={
|
||||||
accessibilityLabel={
|
|
||||||
post.viewer?.like
|
post.viewer?.like
|
||||||
? _(
|
? _(
|
||||||
msg`Unlike (${plural(post.likeCount || 0, {
|
msg`Unlike (${plural(post.likeCount || 0, {
|
||||||
@@ -297,9 +243,7 @@ let PostCtrls = ({
|
|||||||
other: '# likes',
|
other: '# likes',
|
||||||
})})`,
|
})})`,
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
accessibilityHint=""
|
|
||||||
hitSlop={POST_CTRL_HITSLOP}>
|
|
||||||
<AnimatedLikeIcon
|
<AnimatedLikeIcon
|
||||||
isLiked={Boolean(post.viewer?.like)}
|
isLiked={Boolean(post.viewer?.like)}
|
||||||
big={big}
|
big={big}
|
||||||
@@ -311,52 +255,32 @@ let PostCtrls = ({
|
|||||||
isLiked={Boolean(post.viewer?.like)}
|
isLiked={Boolean(post.viewer?.like)}
|
||||||
hasBeenToggled={hasLikeIconBeenToggled}
|
hasBeenToggled={hasLikeIconBeenToggled}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</PostCtrlButton>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={
|
||||||
|
big ? a.align_center : [gtMobile ? a.mr_sm : a.mr_xs, a.align_start]
|
||||||
|
}>
|
||||||
|
<ShareMenuButton
|
||||||
|
testID="postShareBtn"
|
||||||
|
post={post}
|
||||||
|
big={big}
|
||||||
|
record={record}
|
||||||
|
richText={richText}
|
||||||
|
timestamp={post.indexedAt}
|
||||||
|
threadgateRecord={threadgateRecord}
|
||||||
|
onShare={onShare}
|
||||||
|
/>
|
||||||
</View>
|
</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]}>
|
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||||
<PostDropdownBtn
|
<PostMenuButton
|
||||||
testID="postDropdownBtn"
|
testID="postDropdownBtn"
|
||||||
post={post}
|
post={post}
|
||||||
postFeedContext={feedContext}
|
postFeedContext={feedContext}
|
||||||
postReqId={reqId}
|
postReqId={reqId}
|
||||||
|
big={big}
|
||||||
record={record}
|
record={record}
|
||||||
richText={richText}
|
richText={richText}
|
||||||
style={{padding: 5}}
|
|
||||||
hitSlop={POST_CTRL_HITSLOP}
|
|
||||||
timestamp={post.indexedAt}
|
timestamp={post.indexedAt}
|
||||||
threadgateRecord={threadgateRecord}
|
threadgateRecord={threadgateRecord}
|
||||||
onShowLess={onShowLess}
|
onShowLess={onShowLess}
|
||||||
|
|||||||
+18
-41
@@ -1,10 +1,4 @@
|
|||||||
import {memo, useMemo, useState} from 'react'
|
import {memo, useMemo, useState} from 'react'
|
||||||
import {
|
|
||||||
Pressable,
|
|
||||||
type PressableProps,
|
|
||||||
type StyleProp,
|
|
||||||
type ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {
|
import {
|
||||||
type AppBskyFeedDefs,
|
type AppBskyFeedDefs,
|
||||||
type AppBskyFeedPost,
|
type AppBskyFeedPost,
|
||||||
@@ -15,25 +9,22 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
|
|
||||||
import {useTheme} from '#/lib/ThemeContext'
|
|
||||||
import {type Shadow} from '#/state/cache/post-shadow'
|
import {type Shadow} from '#/state/cache/post-shadow'
|
||||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||||
import {useMenuControl} from '#/components/Menu'
|
import {useMenuControl} from '#/components/Menu'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {EventStopper} from '../../view/com/util/EventStopper'
|
import {PostCtrlButton, PostCtrlButtonIcon} from './PostCtrlButton'
|
||||||
import {PostDropdownMenuItems} from './PostDropdownBtnMenuItems'
|
import {PostMenuItems} from './PostMenuItems'
|
||||||
|
|
||||||
let PostDropdownBtn = ({
|
let PostMenuButton = ({
|
||||||
testID,
|
testID,
|
||||||
post,
|
post,
|
||||||
postFeedContext,
|
postFeedContext,
|
||||||
postReqId,
|
postReqId,
|
||||||
|
big,
|
||||||
record,
|
record,
|
||||||
richText,
|
richText,
|
||||||
style,
|
|
||||||
hitSlop,
|
|
||||||
size,
|
|
||||||
timestamp,
|
timestamp,
|
||||||
threadgateRecord,
|
threadgateRecord,
|
||||||
onShowLess,
|
onShowLess,
|
||||||
@@ -42,19 +33,15 @@ let PostDropdownBtn = ({
|
|||||||
post: Shadow<AppBskyFeedDefs.PostView>
|
post: Shadow<AppBskyFeedDefs.PostView>
|
||||||
postFeedContext: string | undefined
|
postFeedContext: string | undefined
|
||||||
postReqId: string | undefined
|
postReqId: string | undefined
|
||||||
|
big?: boolean
|
||||||
record: AppBskyFeedPost.Record
|
record: AppBskyFeedPost.Record
|
||||||
richText: RichTextAPI
|
richText: RichTextAPI
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
hitSlop?: PressableProps['hitSlop']
|
|
||||||
size?: 'lg' | 'md' | 'sm'
|
|
||||||
timestamp: string
|
timestamp: string
|
||||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const theme = useTheme()
|
|
||||||
const alf = useAlf()
|
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
|
||||||
const menuControl = useMenuControl()
|
const menuControl = useMenuControl()
|
||||||
const [hasBeenOpen, setHasBeenOpen] = useState(false)
|
const [hasBeenOpen, setHasBeenOpen] = useState(false)
|
||||||
const lazyMenuControl = useMemo(
|
const lazyMenuControl = useMemo(
|
||||||
@@ -73,31 +60,21 @@ let PostDropdownBtn = ({
|
|||||||
<EventStopper onKeyDown={false}>
|
<EventStopper onKeyDown={false}>
|
||||||
<Menu.Root control={lazyMenuControl}>
|
<Menu.Root control={lazyMenuControl}>
|
||||||
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||||
{({props, state}) => {
|
{({props}) => {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<PostCtrlButton
|
||||||
{...props}
|
testID="postDropdownBtn"
|
||||||
hitSlop={hitSlop}
|
big={big}
|
||||||
testID={testID}
|
label={props.accessibilityLabel}
|
||||||
style={[
|
{...props}>
|
||||||
style,
|
<PostCtrlButtonIcon icon={DotsHorizontal} />
|
||||||
a.rounded_full,
|
</PostCtrlButton>
|
||||||
(state.hovered || state.pressed) && [
|
|
||||||
alf.atoms.bg_contrast_25,
|
|
||||||
],
|
|
||||||
]}>
|
|
||||||
<DotsHorizontal
|
|
||||||
fill={defaultCtrlColor}
|
|
||||||
style={{pointerEvents: 'none'}}
|
|
||||||
size={size}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Menu.Trigger>
|
</Menu.Trigger>
|
||||||
{hasBeenOpen && (
|
{hasBeenOpen && (
|
||||||
// Lazily initialized. Once mounted, they stay mounted.
|
// Lazily initialized. Once mounted, they stay mounted.
|
||||||
<PostDropdownMenuItems
|
<PostMenuItems
|
||||||
testID={testID}
|
testID={testID}
|
||||||
post={post}
|
post={post}
|
||||||
postFeedContext={postFeedContext}
|
postFeedContext={postFeedContext}
|
||||||
@@ -114,5 +91,5 @@ let PostDropdownBtn = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PostDropdownBtn = memo(PostDropdownBtn)
|
PostMenuButton = memo(PostMenuButton)
|
||||||
export {PostDropdownBtn}
|
export {PostMenuButton}
|
||||||
+5
-96
@@ -13,7 +13,7 @@ import {
|
|||||||
AtUri,
|
AtUri,
|
||||||
type RichText as RichTextAPI,
|
type RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
@@ -26,13 +26,12 @@ import {
|
|||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NavigationProp,
|
type NavigationProp,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
import {shareText, shareUrl} from '#/lib/sharing'
|
import {shareText} from '#/lib/sharing'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {getTranslatorLink} from '#/locale/helpers'
|
import {getTranslatorLink} from '#/locale/helpers'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isWeb} from '#/platform/detection'
|
|
||||||
import {type Shadow} from '#/state/cache/post-shadow'
|
import {type Shadow} from '#/state/cache/post-shadow'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
@@ -53,20 +52,16 @@ import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {useBreakpoints} from '#/alf'
|
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||||
import {EmbedDialog} from '#/components/dialogs/Embed'
|
|
||||||
import {
|
import {
|
||||||
PostInteractionSettingsDialog,
|
PostInteractionSettingsDialog,
|
||||||
usePrefetchPostInteractionSettings,
|
usePrefetchPostInteractionSettings,
|
||||||
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||||
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
|
|
||||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
|
import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
|
||||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
|
||||||
import {
|
import {
|
||||||
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
||||||
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
||||||
@@ -76,7 +71,6 @@ import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/E
|
|||||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
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 MuteIcon} from '#/components/icons/Mute'
|
||||||
import {Mute_Stroke2_Corner0_Rounded as Mute} 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 {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
|
||||||
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
|
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
|
||||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||||
@@ -94,13 +88,12 @@ import * as Prompt from '#/components/Prompt'
|
|||||||
import {useDevMode} from '#/storage/hooks/dev-mode'
|
import {useDevMode} from '#/storage/hooks/dev-mode'
|
||||||
import * as bsky from '#/types/bsky'
|
import * as bsky from '#/types/bsky'
|
||||||
|
|
||||||
let PostDropdownMenuItems = ({
|
let PostMenuItems = ({
|
||||||
post,
|
post,
|
||||||
postFeedContext,
|
postFeedContext,
|
||||||
postReqId,
|
postReqId,
|
||||||
record,
|
record,
|
||||||
richText,
|
richText,
|
||||||
timestamp,
|
|
||||||
threadgateRecord,
|
threadgateRecord,
|
||||||
onShowLess,
|
onShowLess,
|
||||||
}: {
|
}: {
|
||||||
@@ -118,7 +111,6 @@ let PostDropdownMenuItems = ({
|
|||||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
||||||
@@ -134,9 +126,6 @@ let PostDropdownMenuItems = ({
|
|||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const deletePromptControl = useDialogControl()
|
const deletePromptControl = useDialogControl()
|
||||||
const hidePromptControl = useDialogControl()
|
const hidePromptControl = useDialogControl()
|
||||||
const loggedOutWarningPromptControl = useDialogControl()
|
|
||||||
const embedPostControl = useDialogControl()
|
|
||||||
const sendViaChatControl = useDialogControl()
|
|
||||||
const postInteractionSettingsDialogControl = useDialogControl()
|
const postInteractionSettingsDialogControl = useDialogControl()
|
||||||
const quotePostDetachConfirmControl = useDialogControl()
|
const quotePostDetachConfirmControl = useDialogControl()
|
||||||
const hideReplyConfirmControl = useDialogControl()
|
const hideReplyConfirmControl = useDialogControl()
|
||||||
@@ -273,14 +262,6 @@ let PostDropdownMenuItems = ({
|
|||||||
label => label.val === '!no-unauthenticated',
|
label => label.val === '!no-unauthenticated',
|
||||||
)
|
)
|
||||||
|
|
||||||
const showLoggedOutWarning =
|
|
||||||
postAuthor.did !== currentAccount?.did && hideInPWI
|
|
||||||
|
|
||||||
const onSharePost = () => {
|
|
||||||
const url = toShareUrl(href)
|
|
||||||
shareUrl(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onPressShowMore = () => {
|
const onPressShowMore = () => {
|
||||||
feedFeedback.sendInteraction({
|
feedFeedback.sendInteraction({
|
||||||
event: 'app.bsky.feed.defs#requestMore',
|
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 () => {
|
const onToggleQuotePostAttachment = async () => {
|
||||||
if (!quoteEmbed) return
|
if (!quoteEmbed) return
|
||||||
|
|
||||||
@@ -341,7 +315,6 @@ let PostDropdownMenuItems = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canHidePostForMe = !isAuthor && !isPostHidden
|
const canHidePostForMe = !isAuthor && !isPostHidden
|
||||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
|
||||||
const canHideReplyForEveryone =
|
const canHideReplyForEveryone =
|
||||||
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
|
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
|
||||||
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
||||||
@@ -482,44 +455,6 @@ let PostDropdownMenuItems = ({
|
|||||||
</Menu.Item>
|
</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>
|
</Menu.Group>
|
||||||
|
|
||||||
{hasSession && feedFeedback.enabled && (
|
{hasSession && feedFeedback.enabled && (
|
||||||
@@ -802,32 +737,6 @@ let PostDropdownMenuItems = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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
|
<PostInteractionSettingsDialog
|
||||||
control={postInteractionSettingsDialogControl}
|
control={postInteractionSettingsDialogControl}
|
||||||
postUri={post.uri}
|
postUri={post.uri}
|
||||||
@@ -868,5 +777,5 @@ let PostDropdownMenuItems = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PostDropdownMenuItems = memo(PostDropdownMenuItems)
|
PostMenuItems = memo(PostMenuItems)
|
||||||
export {PostDropdownMenuItems}
|
export {PostMenuItems}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, {memo, useCallback} from 'react'
|
import {memo, useCallback} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg, plural, Trans} from '@lingui/macro'
|
import {msg, plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {POST_CTRL_HITSLOP} from '#/lib/constants'
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {useRequireAuth} from '#/state/session'
|
import {useRequireAuth} from '#/state/session'
|
||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
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 {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote'
|
||||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {
|
||||||
|
PostCtrlButton,
|
||||||
|
PostCtrlButtonIcon,
|
||||||
|
PostCtrlButtonText,
|
||||||
|
} from './PostCtrlButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isReposted: boolean
|
isReposted: boolean
|
||||||
@@ -35,33 +39,16 @@ let RepostButton = ({
|
|||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
const dialogControl = Dialog.useDialogControl()
|
const dialogControl = Dialog.useDialogControl()
|
||||||
const playHaptic = useHaptics()
|
|
||||||
const color = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
color: isReposted ? t.palette.positive_600 : t.palette.contrast_500,
|
|
||||||
}),
|
|
||||||
[t, isReposted],
|
|
||||||
)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<PostCtrlButton
|
||||||
testID="repostBtn"
|
testID="repostBtn"
|
||||||
onPress={() => {
|
active={isReposted}
|
||||||
playHaptic('Light')
|
activeColor={t.palette.positive_600}
|
||||||
requireAuth(() => dialogControl.open())
|
big={big}
|
||||||
}}
|
onPress={() => requireAuth(() => dialogControl.open())}
|
||||||
onLongPress={() => {
|
onLongPress={() => requireAuth(() => onQuote())}
|
||||||
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={
|
label={
|
||||||
isReposted
|
isReposted
|
||||||
? _(
|
? _(
|
||||||
@@ -76,24 +63,14 @@ let RepostButton = ({
|
|||||||
other: '# reposts',
|
other: '# reposts',
|
||||||
})})`,
|
})})`,
|
||||||
)
|
)
|
||||||
}
|
}>
|
||||||
shape="round"
|
<PostCtrlButtonIcon icon={Repost} />
|
||||||
variant="ghost"
|
{typeof repostCount !== 'undefined' && repostCount > 0 && (
|
||||||
color="secondary"
|
<PostCtrlButtonText testID="repostCount">
|
||||||
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)}
|
{formatCount(i18n, repostCount)}
|
||||||
</Text>
|
</PostCtrlButtonText>
|
||||||
) : undefined}
|
)}
|
||||||
</Button>
|
</PostCtrlButton>
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={dialogControl}
|
control={dialogControl}
|
||||||
nativeOptions={{preventExpansion: true}}>
|
nativeOptions={{preventExpansion: true}}>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react'
|
|
||||||
import {Pressable, View} from 'react-native'
|
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -7,12 +5,15 @@ import {useRequireAuth} from '#/state/session'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||||
import {formatCount} from '#/view/com/util/numeric/format'
|
import {formatCount} from '#/view/com/util/numeric/format'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
|
||||||
import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote'
|
import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote'
|
||||||
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import {Text} from '#/components/Typography'
|
import {
|
||||||
|
PostCtrlButton,
|
||||||
|
PostCtrlButtonIcon,
|
||||||
|
PostCtrlButtonText,
|
||||||
|
} from './PostCtrlButton'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isReposted: boolean
|
isReposted: boolean
|
||||||
@@ -32,38 +33,30 @@ export const RepostButton = ({
|
|||||||
embeddingDisabled,
|
embeddingDisabled,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
|
|
||||||
const color = React.useMemo(
|
|
||||||
() => ({
|
|
||||||
color: isReposted ? t.palette.positive_600 : t.palette.contrast_500,
|
|
||||||
}),
|
|
||||||
[t, isReposted],
|
|
||||||
)
|
|
||||||
|
|
||||||
return hasSession ? (
|
return hasSession ? (
|
||||||
<EventStopper onKeyDown={false}>
|
<EventStopper onKeyDown={false}>
|
||||||
<Menu.Root>
|
<Menu.Root>
|
||||||
<Menu.Trigger label={_(msg`Repost or quote post`)}>
|
<Menu.Trigger label={_(msg`Repost or quote post`)}>
|
||||||
{({props, state}) => {
|
{({props}) => {
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<PostCtrlButton
|
||||||
{...props}
|
testID="repostBtn"
|
||||||
style={[
|
active={isReposted}
|
||||||
a.rounded_full,
|
activeColor={t.palette.positive_600}
|
||||||
(state.hovered || state.pressed) && {
|
label={props.accessibilityLabel}
|
||||||
backgroundColor: t.palette.contrast_25,
|
big={big}
|
||||||
},
|
{...props}>
|
||||||
]}>
|
<PostCtrlButtonIcon icon={Repost} />
|
||||||
<RepostInner
|
{typeof repostCount !== 'undefined' && repostCount > 0 && (
|
||||||
isReposted={isReposted}
|
<PostCtrlButtonText testID="repostCount">
|
||||||
color={color}
|
{formatCount(i18n, repostCount)}
|
||||||
repostCount={repostCount}
|
</PostCtrlButtonText>
|
||||||
big={big}
|
)}
|
||||||
/>
|
</PostCtrlButton>
|
||||||
</Pressable>
|
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Menu.Trigger>
|
</Menu.Trigger>
|
||||||
@@ -97,51 +90,18 @@ export const RepostButton = ({
|
|||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
</EventStopper>
|
</EventStopper>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<PostCtrlButton
|
||||||
onPress={() => {
|
onPress={() => requireAuth(() => {})}
|
||||||
requireAuth(() => {})
|
active={isReposted}
|
||||||
}}
|
activeColor={t.palette.positive_600}
|
||||||
label={_(msg`Repost or quote post`)}
|
label={_(msg`Repost or quote post`)}
|
||||||
style={{padding: 0}}
|
big={big}>
|
||||||
hoverStyle={t.atoms.bg_contrast_25}
|
<PostCtrlButtonIcon icon={Repost} />
|
||||||
shape="round">
|
{typeof repostCount !== 'undefined' && repostCount > 0 && (
|
||||||
<RepostInner
|
<PostCtrlButtonText testID="repostCount">
|
||||||
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)}
|
{formatCount(i18n, repostCount)}
|
||||||
</Text>
|
</PostCtrlButtonText>
|
||||||
) : undefined}
|
)}
|
||||||
</View>
|
</PostCtrlButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<AppBskyFeedDefs.PostView>
|
||||||
|
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 (
|
||||||
|
<EventStopper onKeyDown={false}>
|
||||||
|
<Menu.Root control={lazyMenuControl}>
|
||||||
|
<Menu.Trigger label={_(msg`Open share menu`)}>
|
||||||
|
{({props}) => {
|
||||||
|
return (
|
||||||
|
<PostCtrlButton
|
||||||
|
testID="postShareBtn"
|
||||||
|
big={big}
|
||||||
|
label={props.accessibilityLabel}
|
||||||
|
{...props}>
|
||||||
|
<PostCtrlButtonIcon icon={ArrowOutOfBoxIcon} />
|
||||||
|
</PostCtrlButton>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</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}
|
||||||
@@ -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<NavigationProp>()
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Menu.Outer>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="postDropdownShareBtn"
|
||||||
|
label={_(msg`Copy link to post`)}
|
||||||
|
onPress={() => {
|
||||||
|
if (showLoggedOutWarning) {
|
||||||
|
loggedOutWarningPromptControl.open()
|
||||||
|
} else {
|
||||||
|
onSharePost()
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Share post</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={ArrowOutOfBoxIcon} 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.Group>
|
||||||
|
|
||||||
|
{devModeEnabled && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="postAtUriShareBtn"
|
||||||
|
label={_(msg`Copy 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`Copy 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={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`)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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,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<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 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 (
|
||||||
|
<>
|
||||||
|
<Menu.Outer>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="postDropdownShareBtn"
|
||||||
|
label={_(msg`Copy link to post`)}
|
||||||
|
onPress={() => {
|
||||||
|
if (showLoggedOutWarning) {
|
||||||
|
loggedOutWarningPromptControl.open()
|
||||||
|
} else {
|
||||||
|
onSharePost()
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<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={() => 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={() => embedPostControl.open()}>
|
||||||
|
<Menu.ItemText>{_(msg`Embed post`)}</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={CodeBrackets} 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={onSharePost}
|
||||||
|
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}
|
||||||
Reference in New Issue
Block a user