add moderation 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="M13.924 21.445a1 1 0 0 1-.896.555h-.613a2.998 2.998 0 0 1-2.958-3.462L9.852 16H5.99c-2.421 0-4.279-2.142-3.953-4.54l.68-5A3.99 3.99 0 0 1 6.668 3H21a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-3.38l-3.696 7.445ZM18 12h2V5h-2v7Zm-2-7H6.67c-.993 0-1.837.736-1.972 1.73l-.68 5A1.996 1.996 0 0 0 5.99 14h5.03a1 1 0 0 1 .987 1.153l-.573 3.692A.998.998 0 0 0 12.408 20L16 12.765V5Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 505 B |
@@ -0,0 +1,5 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const ThumbsDown_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M13.924 21.445a1 1 0 0 1-.896.555h-.613a2.998 2.998 0 0 1-2.958-3.462L9.852 16H5.99c-2.421 0-4.279-2.142-3.953-4.54l.68-5A3.99 3.99 0 0 1 6.668 3H21a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-3.38l-3.696 7.445ZM18 12h2V5h-2v7Zm-2-7H6.67c-.993 0-1.837.736-1.972 1.73l-.68 5A1.996 1.996 0 0 0 5.99 14h5.03a1 1 0 0 1 .987 1.153l-.573 3.692A.998.998 0 0 0 12.408 20L16 12.765V5Z',
|
||||
})
|
||||
@@ -18,7 +18,7 @@ import {msg, plural} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {POST_CTRL_HITSLOP} from '#/lib/constants'
|
||||
import {HITSLOP_10, POST_CTRL_HITSLOP} from '#/lib/constants'
|
||||
import {CountWheel} from '#/lib/custom-animations/CountWheel'
|
||||
import {AnimatedLikeIcon} from '#/lib/custom-animations/LikeIcon'
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
@@ -47,6 +47,7 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/componen
|
||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {PostDropdownBtn} from './PostDropdownBtn'
|
||||
import {PostModerationBtn} from './PostModerationBtn'
|
||||
import {RepostButton} from './RepostButton'
|
||||
|
||||
let PostCtrls = ({
|
||||
@@ -246,7 +247,7 @@ let PostCtrls = ({
|
||||
<View style={[a.flex_row, a.justify_between, a.align_center, style]}>
|
||||
<View
|
||||
style={[
|
||||
big ? a.align_center : [a.flex_1, a.align_start, {marginLeft: -6}],
|
||||
!big && [a.flex_1, a.align_start, {marginLeft: -6}],
|
||||
post.viewer?.replyDisabled ? {opacity: 0.5} : undefined,
|
||||
]}>
|
||||
<Pressable
|
||||
@@ -283,7 +284,7 @@ let PostCtrls = ({
|
||||
) : undefined}
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||
<View style={!big && [a.flex_1, a.align_start]}>
|
||||
<RepostButton
|
||||
isReposted={!!post.viewer?.repost}
|
||||
repostCount={(post.repostCount ?? 0) + (post.quoteCount ?? 0)}
|
||||
@@ -293,7 +294,7 @@ let PostCtrls = ({
|
||||
embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)}
|
||||
/>
|
||||
</View>
|
||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||
<View style={!big && [a.flex_1, a.align_start]}>
|
||||
<Pressable
|
||||
testID="likeBtn"
|
||||
style={btnStyle}
|
||||
@@ -331,27 +332,25 @@ let PostCtrls = ({
|
||||
</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>
|
||||
<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>
|
||||
<Prompt.Basic
|
||||
control={loggedOutWarningPromptControl}
|
||||
title={_(msg`Note about sharing`)}
|
||||
@@ -363,30 +362,73 @@ let PostCtrls = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<View style={big ? a.align_center : [a.flex_1, a.align_start]}>
|
||||
<PostDropdownBtn
|
||||
testID="postDropdownBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
</View>
|
||||
{big ? (
|
||||
<>
|
||||
<PostModerationBtn
|
||||
testID="postModerationBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
size={22}
|
||||
/>
|
||||
<PostDropdownBtn
|
||||
testID="postDropdownBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
size={22}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<View style={[a.flex_1, a.flex_row, a.justify_center, a.gap_xs]}>
|
||||
<PostModerationBtn
|
||||
testID="postModerationBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
size={18}
|
||||
/>
|
||||
<PostDropdownBtn
|
||||
testID="postDropdownBtn"
|
||||
post={post}
|
||||
postFeedContext={feedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
style={{padding: 5}}
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
size={18}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{isDiscoverDebugUser && feedContext && (
|
||||
<Pressable
|
||||
accessible={false}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
top: '95%',
|
||||
right: -12,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
height: 10,
|
||||
}}
|
||||
hitSlop={HITSLOP_10}
|
||||
onPress={e => {
|
||||
e.stopPropagation()
|
||||
Clipboard.setStringAsync(feedContext)
|
||||
|
||||
@@ -16,11 +16,11 @@ import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {Shadow} from '#/state/cache/post-shadow'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {EventStopper} from '../../view/com/util/EventStopper'
|
||||
import {PostDropdownMenuItems} from './PostDropdownBtnMenuItems'
|
||||
|
||||
let PostDropdownBtn = ({
|
||||
@@ -42,7 +42,7 @@ let PostDropdownBtn = ({
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
size?: number
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
@@ -84,7 +84,7 @@ let PostDropdownBtn = ({
|
||||
<DotsHorizontal
|
||||
fill={defaultCtrlColor}
|
||||
style={{pointerEvents: 'none'}}
|
||||
size={size}
|
||||
width={size}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
@@ -38,9 +38,6 @@ import {
|
||||
usePostDeleteMutation,
|
||||
useThreadMuteMutationQueue,
|
||||
} from '#/state/queries/post'
|
||||
import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
|
||||
import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
|
||||
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
@@ -62,17 +59,13 @@ import {
|
||||
EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
|
||||
EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
|
||||
} from '#/components/icons/Emoji'
|
||||
import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as Send} from '#/components/icons/PaperPlane'
|
||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
|
||||
import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
@@ -110,7 +103,6 @@ let PostDropdownMenuItems = ({
|
||||
const openLink = useOpenLink()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const blockPromptControl = useDialogControl()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const deletePromptControl = useDialogControl()
|
||||
const hidePromptControl = useDialogControl()
|
||||
@@ -118,7 +110,6 @@ let PostDropdownMenuItems = ({
|
||||
const embedPostControl = useDialogControl()
|
||||
const sendViaChatControl = useDialogControl()
|
||||
const postInteractionSettingsDialogControl = useDialogControl()
|
||||
const quotePostDetachConfirmControl = useDialogControl()
|
||||
const hideReplyConfirmControl = useDialogControl()
|
||||
const {mutateAsync: toggleReplyVisibility} =
|
||||
useToggleReplyVisibilityMutation()
|
||||
@@ -126,13 +117,6 @@ let PostDropdownMenuItems = ({
|
||||
const postUri = post.uri
|
||||
const postCid = post.cid
|
||||
const postAuthor = useProfileShadow(post.author)
|
||||
const quoteEmbed = React.useMemo(() => {
|
||||
if (!currentAccount || !post.embed) return
|
||||
return getMaybeDetachedQuoteEmbed({
|
||||
viewerDid: currentAccount.did,
|
||||
post,
|
||||
})
|
||||
}, [post, currentAccount])
|
||||
|
||||
const rootUri = record.reply?.root?.uri || postUri
|
||||
const isReply = Boolean(record.reply)
|
||||
@@ -149,11 +133,6 @@ let PostDropdownMenuItems = ({
|
||||
const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
|
||||
const isPinned = post.viewer?.pinned
|
||||
|
||||
const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} =
|
||||
useToggleQuoteDetachmentMutation()
|
||||
|
||||
const [queueBlock] = useProfileBlockMutationQueue(postAuthor)
|
||||
|
||||
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
|
||||
postUri: post.uri,
|
||||
rootPostUri: rootUri,
|
||||
@@ -285,34 +264,9 @@ let PostDropdownMenuItems = ({
|
||||
[navigation, postUri],
|
||||
)
|
||||
|
||||
const onToggleQuotePostAttachment = React.useCallback(async () => {
|
||||
if (!quoteEmbed) return
|
||||
|
||||
const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
|
||||
const isDetach = action === 'detach'
|
||||
|
||||
try {
|
||||
await toggleQuoteDetachment({
|
||||
post,
|
||||
quoteUri: quoteEmbed.uri,
|
||||
action: quoteEmbed.isDetached ? 'reattach' : 'detach',
|
||||
})
|
||||
Toast.show(
|
||||
isDetach
|
||||
? _(msg`Quote post was successfully detached`)
|
||||
: _(msg`Quote post was re-attached`),
|
||||
)
|
||||
} catch (e: any) {
|
||||
Toast.show(_(msg`Updating quote attachment failed`))
|
||||
logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
|
||||
}
|
||||
}, [_, quoteEmbed, post, toggleQuoteDetachment])
|
||||
|
||||
const canHidePostForMe = !isAuthor && !isPostHidden
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
const canHideReplyForEveryone =
|
||||
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
|
||||
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
||||
|
||||
const onToggleReplyVisibility = React.useCallback(async () => {
|
||||
// TODO no threadgate?
|
||||
@@ -354,18 +308,6 @@ let PostDropdownMenuItems = ({
|
||||
})
|
||||
}, [isPinned, pinPostMutate, postCid, postUri])
|
||||
|
||||
const onBlockAuthor = useCallback(async () => {
|
||||
try {
|
||||
await queueBlock()
|
||||
Toast.show(_(msg`Account blocked`))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to block account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
}, [_, queueBlock])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Outer>
|
||||
@@ -509,143 +451,36 @@ let PostDropdownMenuItems = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession &&
|
||||
(canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{canHidePostForMe && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
}
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canHideReplyForEveryone && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)
|
||||
}
|
||||
onPress={
|
||||
isReplyHiddenByThreadgate
|
||||
? onToggleReplyVisibility
|
||||
: () => hideReplyConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canDetachQuote && (
|
||||
<Menu.Item
|
||||
disabled={isDetachPending}
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)
|
||||
}
|
||||
onPress={
|
||||
quoteEmbed.isDetached
|
||||
? onToggleQuotePostAttachment
|
||||
: () => quotePostDetachConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={
|
||||
isDetachPending
|
||||
? Loader
|
||||
: quoteEmbed.isDetached
|
||||
? Eye
|
||||
: EyeSlash
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
{hasSession && isAuthor && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<>
|
||||
{!postAuthor.viewer?.blocking && (
|
||||
<Menu.Item
|
||||
testID="postDropdownBlockBtn"
|
||||
label={_(msg`Block account`)}
|
||||
onPress={() => blockPromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={PersonX} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
testID="postDropdownReportBtn"
|
||||
label={_(msg`Report post`)}
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={() => postInteractionSettingsDialogControl.open()}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
onHoverIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
native: {
|
||||
onPressIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
})
|
||||
: {})}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={() => postInteractionSettingsDialogControl.open()}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
onHoverIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
native: {
|
||||
onPressIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
})
|
||||
: {})}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
@@ -714,16 +549,6 @@ let PostDropdownMenuItems = ({
|
||||
initialThreadgateView={post.threadgate}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={quotePostDetachConfirmControl}
|
||||
title={_(msg`Detach quote post?`)}
|
||||
description={_(
|
||||
msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`,
|
||||
)}
|
||||
onConfirm={onToggleQuotePostAttachment}
|
||||
confirmButtonCta={_(msg`Yes, detach`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={hideReplyConfirmControl}
|
||||
title={_(msg`Hide this reply?`)}
|
||||
@@ -733,17 +558,6 @@ let PostDropdownMenuItems = ({
|
||||
onConfirm={onToggleReplyVisibility}
|
||||
confirmButtonCta={_(msg`Yes, hide`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={blockPromptControl}
|
||||
title={_(msg`Block Account?`)}
|
||||
description={_(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
)}
|
||||
onConfirm={onBlockAuthor}
|
||||
confirmButtonCta={_(msg`Block`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import React, {memo, useMemo, useState} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {Shadow} from '#/state/cache/post-shadow'
|
||||
import {EventStopper} from '#/view/com/util/EventStopper'
|
||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
||||
import {ThumbsDown_Stroke2_Corner0_Rounded as ThumbsDownIcon} from '#/components/icons/ThumbsDown'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {PostModerationMenuItems} from './PostModerationBtnMenuItems'
|
||||
|
||||
let PostModerationBtn = ({
|
||||
testID,
|
||||
post,
|
||||
postFeedContext,
|
||||
record,
|
||||
richText,
|
||||
style,
|
||||
hitSlop,
|
||||
size,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: number
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
const theme = useTheme()
|
||||
const alf = useAlf()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const menuControl = useMenuControl()
|
||||
const [hasBeenOpen, setHasBeenOpen] = useState(false)
|
||||
const lazyMenuControl = useMemo(
|
||||
() => ({
|
||||
...menuControl,
|
||||
open() {
|
||||
setHasBeenOpen(true)
|
||||
// HACK. We need the state update to be flushed by the time
|
||||
// menuControl.open() fires but RN doesn't expose flushSync.
|
||||
setTimeout(menuControl.open)
|
||||
},
|
||||
}),
|
||||
[menuControl, setHasBeenOpen],
|
||||
)
|
||||
return (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root control={lazyMenuControl}>
|
||||
<Menu.Trigger label={_(msg`Open post moderation menu`)}>
|
||||
{({props, state}) => {
|
||||
return (
|
||||
<Pressable
|
||||
{...props}
|
||||
hitSlop={hitSlop}
|
||||
testID={testID}
|
||||
style={[
|
||||
style,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.pressed) && [
|
||||
alf.atoms.bg_contrast_25,
|
||||
],
|
||||
]}>
|
||||
<ThumbsDownIcon
|
||||
fill={defaultCtrlColor}
|
||||
style={{pointerEvents: 'none'}}
|
||||
width={size}
|
||||
/>
|
||||
</Pressable>
|
||||
)
|
||||
}}
|
||||
</Menu.Trigger>
|
||||
{hasBeenOpen && (
|
||||
// Lazily initialized. Once mounted, they stay mounted.
|
||||
<PostModerationMenuItems
|
||||
testID={testID}
|
||||
post={post}
|
||||
postFeedContext={postFeedContext}
|
||||
record={record}
|
||||
richText={richText}
|
||||
timestamp={timestamp}
|
||||
threadgateRecord={threadgateRecord}
|
||||
/>
|
||||
)}
|
||||
</Menu.Root>
|
||||
</EventStopper>
|
||||
)
|
||||
}
|
||||
|
||||
PostModerationBtn = memo(PostModerationBtn)
|
||||
export {PostModerationBtn}
|
||||
@@ -0,0 +1,578 @@
|
||||
import React, {memo, useCallback} from 'react'
|
||||
import {
|
||||
Platform,
|
||||
type PressableProps,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {getCurrentRoute} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {Shadow} from '#/state/cache/post-shadow'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
|
||||
import {
|
||||
usePostDeleteMutation,
|
||||
useThreadMuteMutationQueue,
|
||||
} from '#/state/queries/post'
|
||||
import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
|
||||
import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
|
||||
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
|
||||
import {useSession} from '#/state/session'
|
||||
import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useBreakpoints} from '#/alf'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {EmbedDialog} from '#/components/dialogs/Embed'
|
||||
import {
|
||||
PostInteractionSettingsDialog,
|
||||
usePrefetchPostInteractionSettings,
|
||||
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
|
||||
import {EmojiSad_Stroke2_Corner0_Rounded as EmojiSad} from '#/components/icons/Emoji'
|
||||
import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
|
||||
import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||
|
||||
let PostModerationMenuItems = ({
|
||||
post,
|
||||
postFeedContext,
|
||||
record,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
postFeedContext: string | undefined
|
||||
record: AppBskyFeedPost.Record
|
||||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
|
||||
const hiddenPosts = useHiddenPosts()
|
||||
const {hidePost} = useHiddenPostsApi()
|
||||
const feedFeedback = useFeedFeedbackContext()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
|
||||
const blockPromptControl = useDialogControl()
|
||||
const reportDialogControl = useReportDialogControl()
|
||||
const deletePromptControl = useDialogControl()
|
||||
const hidePromptControl = useDialogControl()
|
||||
const embedPostControl = useDialogControl()
|
||||
const sendViaChatControl = useDialogControl()
|
||||
const postInteractionSettingsDialogControl = useDialogControl()
|
||||
const quotePostDetachConfirmControl = useDialogControl()
|
||||
const hideReplyConfirmControl = useDialogControl()
|
||||
const {mutateAsync: toggleReplyVisibility} =
|
||||
useToggleReplyVisibilityMutation()
|
||||
|
||||
const postUri = post.uri
|
||||
const postCid = post.cid
|
||||
const postAuthor = useProfileShadow(post.author)
|
||||
const quoteEmbed = React.useMemo(() => {
|
||||
if (!currentAccount || !post.embed) return
|
||||
return getMaybeDetachedQuoteEmbed({
|
||||
viewerDid: currentAccount.did,
|
||||
post,
|
||||
})
|
||||
}, [post, currentAccount])
|
||||
|
||||
const rootUri = record.reply?.root?.uri || postUri
|
||||
const isReply = Boolean(record.reply)
|
||||
const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
|
||||
post,
|
||||
rootUri,
|
||||
)
|
||||
const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
|
||||
const isAuthor = postAuthor.did === currentAccount?.did
|
||||
const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did
|
||||
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
|
||||
threadgateRecord,
|
||||
})
|
||||
const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
|
||||
|
||||
const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} =
|
||||
useToggleQuoteDetachmentMutation()
|
||||
|
||||
const [queueBlock] = useProfileBlockMutationQueue(postAuthor)
|
||||
|
||||
const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
|
||||
postUri: post.uri,
|
||||
rootPostUri: rootUri,
|
||||
})
|
||||
|
||||
const href = React.useMemo(() => {
|
||||
const urip = new AtUri(postUri)
|
||||
return makeProfileLink(postAuthor, 'post', urip.rkey)
|
||||
}, [postUri, postAuthor])
|
||||
|
||||
const onDeletePost = React.useCallback(() => {
|
||||
deletePostMutate({uri: postUri}).then(
|
||||
() => {
|
||||
Toast.show(_(msg`Post deleted`))
|
||||
|
||||
const route = getCurrentRoute(navigation.getState())
|
||||
if (route.name === 'PostThread') {
|
||||
const params = route.params as CommonNavigatorParams['PostThread']
|
||||
if (
|
||||
currentAccount &&
|
||||
isAuthor &&
|
||||
(params.name === currentAccount.handle ||
|
||||
params.name === currentAccount.did)
|
||||
) {
|
||||
const currentHref = makeProfileLink(postAuthor, 'post', params.rkey)
|
||||
if (currentHref === href && navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
e => {
|
||||
logger.error('Failed to delete post', {message: e})
|
||||
Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
|
||||
},
|
||||
)
|
||||
}, [
|
||||
navigation,
|
||||
postUri,
|
||||
deletePostMutate,
|
||||
postAuthor,
|
||||
currentAccount,
|
||||
isAuthor,
|
||||
href,
|
||||
_,
|
||||
])
|
||||
|
||||
const onToggleThreadMute = React.useCallback(() => {
|
||||
try {
|
||||
if (isThreadMuted) {
|
||||
unmuteThread()
|
||||
Toast.show(_(msg`You will now receive notifications for this thread`))
|
||||
} else {
|
||||
muteThread()
|
||||
Toast.show(
|
||||
_(msg`You will no longer receive notifications for this thread`),
|
||||
)
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to toggle thread mute', {message: e})
|
||||
Toast.show(
|
||||
_(msg`Failed to toggle thread mute, please try again`),
|
||||
'xmark',
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [isThreadMuted, unmuteThread, _, muteThread])
|
||||
|
||||
const onHidePost = React.useCallback(() => {
|
||||
hidePost({uri: postUri})
|
||||
}, [postUri, hidePost])
|
||||
|
||||
const hideInPWI = React.useMemo(() => {
|
||||
return !!postAuthor.labels?.find(
|
||||
label => label.val === '!no-unauthenticated',
|
||||
)
|
||||
}, [postAuthor])
|
||||
|
||||
const onPressShowLess = React.useCallback(() => {
|
||||
feedFeedback.sendInteraction({
|
||||
event: 'app.bsky.feed.defs#requestLess',
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
Toast.show(_(msg`Feedback sent!`))
|
||||
}, [feedFeedback, postUri, postFeedContext, _])
|
||||
|
||||
const onSelectChatToShareTo = React.useCallback(
|
||||
(conversation: string) => {
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation,
|
||||
embed: postUri,
|
||||
})
|
||||
},
|
||||
[navigation, postUri],
|
||||
)
|
||||
|
||||
const onToggleQuotePostAttachment = React.useCallback(async () => {
|
||||
if (!quoteEmbed) return
|
||||
|
||||
const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
|
||||
const isDetach = action === 'detach'
|
||||
|
||||
try {
|
||||
await toggleQuoteDetachment({
|
||||
post,
|
||||
quoteUri: quoteEmbed.uri,
|
||||
action: quoteEmbed.isDetached ? 'reattach' : 'detach',
|
||||
})
|
||||
Toast.show(
|
||||
isDetach
|
||||
? _(msg`Quote post was successfully detached`)
|
||||
: _(msg`Quote post was re-attached`),
|
||||
)
|
||||
} catch (e: any) {
|
||||
Toast.show(_(msg`Updating quote attachment failed`))
|
||||
logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
|
||||
}
|
||||
}, [_, quoteEmbed, post, toggleQuoteDetachment])
|
||||
|
||||
const canHidePostForMe = !isAuthor && !isPostHidden
|
||||
const canEmbed = isWeb && gtMobile && !hideInPWI
|
||||
const canHideReplyForEveryone =
|
||||
!isAuthor && isRootPostAuthor && !isPostHidden && isReply
|
||||
const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
|
||||
|
||||
const onToggleReplyVisibility = React.useCallback(async () => {
|
||||
// TODO no threadgate?
|
||||
if (!canHideReplyForEveryone) return
|
||||
|
||||
const action = isReplyHiddenByThreadgate ? 'show' : 'hide'
|
||||
const isHide = action === 'hide'
|
||||
|
||||
try {
|
||||
await toggleReplyVisibility({
|
||||
postUri: rootUri,
|
||||
replyUri: postUri,
|
||||
action,
|
||||
})
|
||||
Toast.show(
|
||||
isHide
|
||||
? _(msg`Reply was successfully hidden`)
|
||||
: _(msg`Reply visibility updated`),
|
||||
)
|
||||
} catch (e: any) {
|
||||
Toast.show(_(msg`Updating reply visibility failed`))
|
||||
logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
|
||||
}
|
||||
}, [
|
||||
_,
|
||||
isReplyHiddenByThreadgate,
|
||||
rootUri,
|
||||
postUri,
|
||||
canHideReplyForEveryone,
|
||||
toggleReplyVisibility,
|
||||
])
|
||||
|
||||
const onBlockAuthor = useCallback(async () => {
|
||||
try {
|
||||
await queueBlock()
|
||||
Toast.show(_(msg`Account blocked`))
|
||||
} catch (e: any) {
|
||||
if (e?.name !== 'AbortError') {
|
||||
logger.error('Failed to block account', {message: e})
|
||||
Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
|
||||
}
|
||||
}
|
||||
}, [_, queueBlock])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Outer>
|
||||
{hasSession && feedFeedback.enabled && (
|
||||
<>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownShowLessBtn"
|
||||
label={_(msg`Show less like this`)}
|
||||
onPress={onPressShowLess}>
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteThreadBtn"
|
||||
label={
|
||||
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
|
||||
}
|
||||
onPress={onToggleThreadMute}>
|
||||
<Menu.ItemText>
|
||||
{isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isThreadMuted ? Unmute : Mute}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
testID="postDropdownMuteWordsBtn"
|
||||
label={_(msg`Mute words & tags`)}
|
||||
onPress={() => mutedWordsDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Filter} position="right" />
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession &&
|
||||
(canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{canHidePostForMe && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)
|
||||
}
|
||||
onPress={() => hidePromptControl.open()}>
|
||||
<Menu.ItemText>
|
||||
{isReply
|
||||
? _(msg`Hide reply for me`)
|
||||
: _(msg`Hide post for me`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EyeSlash} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
{canHideReplyForEveryone && (
|
||||
<Menu.Item
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)
|
||||
}
|
||||
onPress={
|
||||
isReplyHiddenByThreadgate
|
||||
? onToggleReplyVisibility
|
||||
: () => hideReplyConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{isReplyHiddenByThreadgate
|
||||
? _(msg`Show reply for everyone`)
|
||||
: _(msg`Hide reply for everyone`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
|
||||
{canDetachQuote && (
|
||||
<Menu.Item
|
||||
disabled={isDetachPending}
|
||||
testID="postDropdownHideBtn"
|
||||
label={
|
||||
quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)
|
||||
}
|
||||
onPress={
|
||||
quoteEmbed.isDetached
|
||||
? onToggleQuotePostAttachment
|
||||
: () => quotePostDetachConfirmControl.open()
|
||||
}>
|
||||
<Menu.ItemText>
|
||||
{quoteEmbed.isDetached
|
||||
? _(msg`Re-attach quote`)
|
||||
: _(msg`Detach quote`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon
|
||||
icon={
|
||||
isDetachPending
|
||||
? Loader
|
||||
: quoteEmbed.isDetached
|
||||
? Eye
|
||||
: EyeSlash
|
||||
}
|
||||
position="right"
|
||||
/>
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
{hasSession && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Group>
|
||||
{!isAuthor && (
|
||||
<>
|
||||
{!postAuthor.viewer?.blocking && (
|
||||
<Menu.Item
|
||||
testID="postDropdownBlockBtn"
|
||||
label={_(msg`Block account`)}
|
||||
onPress={() => blockPromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={PersonX} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
testID="postDropdownReportBtn"
|
||||
label={_(msg`Report post`)}
|
||||
onPress={() => reportDialogControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Warning} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<>
|
||||
<Menu.Item
|
||||
testID="postDropdownEditPostInteractions"
|
||||
label={_(msg`Edit interaction settings`)}
|
||||
onPress={() => postInteractionSettingsDialogControl.open()}
|
||||
{...(isAuthor
|
||||
? Platform.select({
|
||||
web: {
|
||||
onHoverIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
native: {
|
||||
onPressIn: prefetchPostInteractionSettings,
|
||||
},
|
||||
})
|
||||
: {})}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Edit interaction settings`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Gear} position="right" />
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
testID="postDropdownDeleteBtn"
|
||||
label={_(msg`Delete post`)}
|
||||
onPress={() => deletePromptControl.open()}>
|
||||
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={Trash} position="right" />
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
</Menu.Outer>
|
||||
|
||||
<Prompt.Basic
|
||||
control={deletePromptControl}
|
||||
title={_(msg`Delete this post?`)}
|
||||
description={_(
|
||||
msg`If you remove this post, you won't be able to recover it.`,
|
||||
)}
|
||||
onConfirm={onDeletePost}
|
||||
confirmButtonCta={_(msg`Delete`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={hidePromptControl}
|
||||
title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)}
|
||||
description={_(
|
||||
msg`This post will be hidden from feeds and threads. This cannot be undone.`,
|
||||
)}
|
||||
onConfirm={onHidePost}
|
||||
confirmButtonCta={_(msg`Hide`)}
|
||||
/>
|
||||
|
||||
<ReportDialog
|
||||
control={reportDialogControl}
|
||||
params={{
|
||||
type: 'post',
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
}}
|
||||
/>
|
||||
|
||||
{canEmbed && (
|
||||
<EmbedDialog
|
||||
control={embedPostControl}
|
||||
postCid={postCid}
|
||||
postUri={postUri}
|
||||
record={record}
|
||||
postAuthor={postAuthor}
|
||||
timestamp={timestamp}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SendViaChatDialog
|
||||
control={sendViaChatControl}
|
||||
onSelectChat={onSelectChatToShareTo}
|
||||
/>
|
||||
|
||||
<PostInteractionSettingsDialog
|
||||
control={postInteractionSettingsDialogControl}
|
||||
postUri={post.uri}
|
||||
rootPostUri={rootUri}
|
||||
initialThreadgateView={post.threadgate}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={quotePostDetachConfirmControl}
|
||||
title={_(msg`Detach quote post?`)}
|
||||
description={_(
|
||||
msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`,
|
||||
)}
|
||||
onConfirm={onToggleQuotePostAttachment}
|
||||
confirmButtonCta={_(msg`Yes, detach`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={hideReplyConfirmControl}
|
||||
title={_(msg`Hide this reply?`)}
|
||||
description={_(
|
||||
msg`This reply will be sorted into a hidden section at the bottom of your thread and will mute notifications for subsequent replies - both for yourself and others.`,
|
||||
)}
|
||||
onConfirm={onToggleReplyVisibility}
|
||||
confirmButtonCta={_(msg`Yes, hide`)}
|
||||
/>
|
||||
|
||||
<Prompt.Basic
|
||||
control={blockPromptControl}
|
||||
title={_(msg`Block Account?`)}
|
||||
description={_(
|
||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||
)}
|
||||
onConfirm={onBlockAuthor}
|
||||
confirmButtonCta={_(msg`Block`)}
|
||||
confirmButtonColor="negative"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
PostModerationMenuItems = memo(PostModerationMenuItems)
|
||||
export {PostModerationMenuItems}
|
||||
Reference in New Issue
Block a user