diff --git a/assets/icons/thumbsDown_stroke2_corner0_rounded.svg b/assets/icons/thumbsDown_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..25ab381d6c
--- /dev/null
+++ b/assets/icons/thumbsDown_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/components/icons/ThumbsDown.tsx b/src/components/icons/ThumbsDown.tsx
new file mode 100644
index 0000000000..d65c1a68dd
--- /dev/null
+++ b/src/components/icons/ThumbsDown.tsx
@@ -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',
+})
diff --git a/src/components/post-ctrls/PostCtrls.tsx b/src/components/post-ctrls/PostCtrls.tsx
index 4951bc9d6c..197e55325c 100644
--- a/src/components/post-ctrls/PostCtrls.tsx
+++ b/src/components/post-ctrls/PostCtrls.tsx
@@ -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 = ({
-
+
-
+
{big && (
<>
-
- {
- if (shouldShowLoggedOutWarning) {
- loggedOutWarningPromptControl.open()
- } else {
- onShare()
- }
- }}
- accessibilityRole="button"
- accessibilityLabel={_(msg`Share`)}
- accessibilityHint=""
- hitSlop={POST_CTRL_HITSLOP}>
-
-
-
+ {
+ if (shouldShowLoggedOutWarning) {
+ loggedOutWarningPromptControl.open()
+ } else {
+ onShare()
+ }
+ }}
+ accessibilityRole="button"
+ accessibilityLabel={_(msg`Share`)}
+ accessibilityHint=""
+ hitSlop={POST_CTRL_HITSLOP}>
+
+
>
)}
-
-
-
+ {big ? (
+ <>
+
+
+ >
+ ) : (
+
+
+
+
+ )}
{isDiscoverDebugUser && feedContext && (
{
e.stopPropagation()
Clipboard.setStringAsync(feedContext)
diff --git a/src/components/post-ctrls/PostDropdownBtn.tsx b/src/components/post-ctrls/PostDropdownBtn.tsx
index 127f487aee..ccb1f61b53 100644
--- a/src/components/post-ctrls/PostDropdownBtn.tsx
+++ b/src/components/post-ctrls/PostDropdownBtn.tsx
@@ -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
hitSlop?: PressableProps['hitSlop']
- size?: 'lg' | 'md' | 'sm'
+ size?: number
timestamp: string
threadgateRecord?: AppBskyFeedThreadgate.Record
}): React.ReactNode => {
@@ -84,7 +84,7 @@ let PostDropdownBtn = ({
)
diff --git a/src/components/post-ctrls/PostDropdownBtnMenuItems.tsx b/src/components/post-ctrls/PostDropdownBtnMenuItems.tsx
index cee27106ce..c540f396c9 100644
--- a/src/components/post-ctrls/PostDropdownBtnMenuItems.tsx
+++ b/src/components/post-ctrls/PostDropdownBtnMenuItems.tsx
@@ -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()
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 (
<>
@@ -509,143 +451,36 @@ let PostDropdownMenuItems = ({
>
)}
- {hasSession &&
- (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
- <>
-
-
- {canHidePostForMe && (
- hidePromptControl.open()}>
-
- {isReply
- ? _(msg`Hide reply for me`)
- : _(msg`Hide post for me`)}
-
-
-
- )}
- {canHideReplyForEveryone && (
- hideReplyConfirmControl.open()
- }>
-
- {isReplyHiddenByThreadgate
- ? _(msg`Show reply for everyone`)
- : _(msg`Hide reply for everyone`)}
-
-
-
- )}
-
- {canDetachQuote && (
- quotePostDetachConfirmControl.open()
- }>
-
- {quoteEmbed.isDetached
- ? _(msg`Re-attach quote`)
- : _(msg`Detach quote`)}
-
-
-
- )}
-
- >
- )}
-
- {hasSession && (
+ {hasSession && isAuthor && (
<>
- {!isAuthor && (
- <>
- {!postAuthor.viewer?.blocking && (
- blockPromptControl.open()}>
- {_(msg`Block account`)}
-
-
- )}
- reportDialogControl.open()}>
- {_(msg`Report post`)}
-
-
- >
- )}
-
- {isAuthor && (
- <>
- postInteractionSettingsDialogControl.open()}
- {...(isAuthor
- ? Platform.select({
- web: {
- onHoverIn: prefetchPostInteractionSettings,
- },
- native: {
- onPressIn: prefetchPostInteractionSettings,
- },
- })
- : {})}>
-
- {_(msg`Edit interaction settings`)}
-
-
-
- deletePromptControl.open()}>
- {_(msg`Delete post`)}
-
-
- >
- )}
+ postInteractionSettingsDialogControl.open()}
+ {...(isAuthor
+ ? Platform.select({
+ web: {
+ onHoverIn: prefetchPostInteractionSettings,
+ },
+ native: {
+ onPressIn: prefetchPostInteractionSettings,
+ },
+ })
+ : {})}>
+
+ {_(msg`Edit interaction settings`)}
+
+
+
+ deletePromptControl.open()}>
+ {_(msg`Delete post`)}
+
+
>
)}
@@ -714,16 +549,6 @@ let PostDropdownMenuItems = ({
initialThreadgateView={post.threadgate}
/>
-
-
-
-
>
)
}
diff --git a/src/components/post-ctrls/PostModerationBtn.tsx b/src/components/post-ctrls/PostModerationBtn.tsx
new file mode 100644
index 0000000000..a7dea74d38
--- /dev/null
+++ b/src/components/post-ctrls/PostModerationBtn.tsx
@@ -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
+ postFeedContext: string | undefined
+ record: AppBskyFeedPost.Record
+ richText: RichTextAPI
+ style?: StyleProp
+ 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 (
+
+
+
+ {({props, state}) => {
+ return (
+
+
+
+ )
+ }}
+
+ {hasBeenOpen && (
+ // Lazily initialized. Once mounted, they stay mounted.
+
+ )}
+
+
+ )
+}
+
+PostModerationBtn = memo(PostModerationBtn)
+export {PostModerationBtn}
diff --git a/src/components/post-ctrls/PostModerationBtnMenuItems.tsx b/src/components/post-ctrls/PostModerationBtnMenuItems.tsx
new file mode 100644
index 0000000000..e656904c79
--- /dev/null
+++ b/src/components/post-ctrls/PostModerationBtnMenuItems.tsx
@@ -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
+ postFeedContext: string | undefined
+ record: AppBskyFeedPost.Record
+ richText: RichTextAPI
+ style?: StyleProp
+ 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()
+ 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 (
+ <>
+
+ {hasSession && feedFeedback.enabled && (
+ <>
+
+
+ {_(msg`Show less like this`)}
+
+
+
+ >
+ )}
+
+ {hasSession && (
+ <>
+
+
+
+
+ {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
+
+
+
+
+ mutedWordsDialogControl.open()}>
+ {_(msg`Mute words & tags`)}
+
+
+
+ >
+ )}
+
+ {hasSession &&
+ (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
+ <>
+
+
+ {canHidePostForMe && (
+ hidePromptControl.open()}>
+
+ {isReply
+ ? _(msg`Hide reply for me`)
+ : _(msg`Hide post for me`)}
+
+
+
+ )}
+ {canHideReplyForEveryone && (
+ hideReplyConfirmControl.open()
+ }>
+
+ {isReplyHiddenByThreadgate
+ ? _(msg`Show reply for everyone`)
+ : _(msg`Hide reply for everyone`)}
+
+
+
+ )}
+
+ {canDetachQuote && (
+ quotePostDetachConfirmControl.open()
+ }>
+
+ {quoteEmbed.isDetached
+ ? _(msg`Re-attach quote`)
+ : _(msg`Detach quote`)}
+
+
+
+ )}
+
+ >
+ )}
+
+ {hasSession && (
+ <>
+
+
+ {!isAuthor && (
+ <>
+ {!postAuthor.viewer?.blocking && (
+ blockPromptControl.open()}>
+ {_(msg`Block account`)}
+
+
+ )}
+ reportDialogControl.open()}>
+ {_(msg`Report post`)}
+
+
+ >
+ )}
+
+ {isAuthor && (
+ <>
+ postInteractionSettingsDialogControl.open()}
+ {...(isAuthor
+ ? Platform.select({
+ web: {
+ onHoverIn: prefetchPostInteractionSettings,
+ },
+ native: {
+ onPressIn: prefetchPostInteractionSettings,
+ },
+ })
+ : {})}>
+
+ {_(msg`Edit interaction settings`)}
+
+
+
+ deletePromptControl.open()}>
+ {_(msg`Delete post`)}
+
+
+ >
+ )}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ {canEmbed && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+PostModerationMenuItems = memo(PostModerationMenuItems)
+export {PostModerationMenuItems}