mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import {memo, useMemo} from 'react'
2import {
3 Platform,
4 type PressableProps,
5 type StyleProp,
6 type ViewStyle,
7} from 'react-native'
8import * as Clipboard from 'expo-clipboard'
9import {
10 type AppBskyFeedDefs,
11 AppBskyFeedPost,
12 type AppBskyFeedThreadgate,
13 AtUri,
14 type RichText as RichTextAPI,
15} from '@atproto/api'
16import {msg} from '@lingui/macro'
17import {useLingui} from '@lingui/react'
18import {useNavigation} from '@react-navigation/native'
19
20import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
21import {useOpenLink} from '#/lib/hooks/useOpenLink'
22import {useTranslate} from '#/lib/hooks/useTranslate'
23import {getCurrentRoute} from '#/lib/routes/helpers'
24import {makeProfileLink} from '#/lib/routes/links'
25import {
26 type CommonNavigatorParams,
27 type NavigationProp,
28} from '#/lib/routes/types'
29import {logEvent, useGate} from '#/lib/statsig/statsig'
30import {richTextToString} from '#/lib/strings/rich-text-helpers'
31import {toShareUrl} from '#/lib/strings/url-helpers'
32import {logger} from '#/logger'
33import {type Shadow} from '#/state/cache/post-shadow'
34import {useProfileShadow} from '#/state/cache/profile-shadow'
35import {useFeedFeedbackContext} from '#/state/feed-feedback'
36import {useLanguagePrefs} from '#/state/preferences'
37import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
38import {usePinnedPostMutation} from '#/state/queries/pinned-post'
39import {
40 usePostDeleteMutation,
41 useThreadMuteMutationQueue,
42} from '#/state/queries/post'
43import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
44import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
45import {
46 useProfileBlockMutationQueue,
47 useProfileMuteMutationQueue,
48} from '#/state/queries/profile'
49import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
50import {useRequireAuth, useSession} from '#/state/session'
51import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
52import * as Toast from '#/view/com/util/Toast'
53import {useDialogControl} from '#/components/Dialog'
54import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
55import {
56 PostInteractionSettingsDialog,
57 usePrefetchPostInteractionSettings,
58} from '#/components/dialogs/PostInteractionSettingsDialog'
59import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
60import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
61import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
62import {
63 EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
64 EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
65} from '#/components/icons/Emoji'
66import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
67import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
68import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
69import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
70import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
71import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
72import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
73import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
74import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
75import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
76import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
77import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
78import {Loader} from '#/components/Loader'
79import * as Menu from '#/components/Menu'
80import {
81 ReportDialog,
82 useReportDialogControl,
83} from '#/components/moderation/ReportDialog'
84import * as Prompt from '#/components/Prompt'
85import {IS_INTERNAL} from '#/env'
86import * as bsky from '#/types/bsky'
87
88let PostMenuItems = ({
89 post,
90 postFeedContext,
91 postReqId,
92 record,
93 richText,
94 threadgateRecord,
95 onShowLess,
96}: {
97 testID: string
98 post: Shadow<AppBskyFeedDefs.PostView>
99 postFeedContext: string | undefined
100 postReqId: string | undefined
101 record: AppBskyFeedPost.Record
102 richText: RichTextAPI
103 style?: StyleProp<ViewStyle>
104 hitSlop?: PressableProps['hitSlop']
105 size?: 'lg' | 'md' | 'sm'
106 timestamp: string
107 threadgateRecord?: AppBskyFeedThreadgate.Record
108 onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
109}): React.ReactNode => {
110 const {hasSession, currentAccount} = useSession()
111 const {_} = useLingui()
112 const langPrefs = useLanguagePrefs()
113 const {mutateAsync: deletePostMutate} = usePostDeleteMutation()
114 const {mutateAsync: pinPostMutate, isPending: isPinPending} =
115 usePinnedPostMutation()
116 const requireSignIn = useRequireAuth()
117 const hiddenPosts = useHiddenPosts()
118 const {hidePost} = useHiddenPostsApi()
119 const feedFeedback = useFeedFeedbackContext()
120 const openLink = useOpenLink()
121 const translate = useTranslate()
122 const navigation = useNavigation<NavigationProp>()
123 const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
124 const blockPromptControl = useDialogControl()
125 const reportDialogControl = useReportDialogControl()
126 const deletePromptControl = useDialogControl()
127 const hidePromptControl = useDialogControl()
128 const postInteractionSettingsDialogControl = useDialogControl()
129 const quotePostDetachConfirmControl = useDialogControl()
130 const hideReplyConfirmControl = useDialogControl()
131 const {mutateAsync: toggleReplyVisibility} =
132 useToggleReplyVisibilityMutation()
133
134 const postUri = post.uri
135 const postCid = post.cid
136 const postAuthor = useProfileShadow(post.author)
137 const quoteEmbed = useMemo(() => {
138 if (!currentAccount || !post.embed) return
139 return getMaybeDetachedQuoteEmbed({
140 viewerDid: currentAccount.did,
141 post,
142 })
143 }, [post, currentAccount])
144
145 const rootUri = record.reply?.root?.uri || postUri
146 const isReply = Boolean(record.reply)
147 const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
148 post,
149 rootUri,
150 )
151 const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
152 const isAuthor = postAuthor.did === currentAccount?.did
153 const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did
154 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
155 threadgateRecord,
156 })
157 const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
158 const isPinned = post.viewer?.pinned
159
160 const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} =
161 useToggleQuoteDetachmentMutation()
162
163 const [queueBlock] = useProfileBlockMutationQueue(postAuthor)
164 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(postAuthor)
165
166 const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
167 postUri: post.uri,
168 rootPostUri: rootUri,
169 })
170
171 const href = useMemo(() => {
172 const urip = new AtUri(postUri)
173 return makeProfileLink(postAuthor, 'post', urip.rkey)
174 }, [postUri, postAuthor])
175
176 const onDeletePost = () => {
177 deletePostMutate({uri: postUri}).then(
178 () => {
179 Toast.show(_(msg({message: 'Post deleted', context: 'toast'})))
180
181 const route = getCurrentRoute(navigation.getState())
182 if (route.name === 'PostThread') {
183 const params = route.params as CommonNavigatorParams['PostThread']
184 if (
185 currentAccount &&
186 isAuthor &&
187 (params.name === currentAccount.handle ||
188 params.name === currentAccount.did)
189 ) {
190 const currentHref = makeProfileLink(postAuthor, 'post', params.rkey)
191 if (currentHref === href && navigation.canGoBack()) {
192 navigation.goBack()
193 }
194 }
195 }
196 },
197 e => {
198 logger.error('Failed to delete post', {message: e})
199 Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
200 },
201 )
202 }
203
204 const onToggleThreadMute = () => {
205 try {
206 if (isThreadMuted) {
207 unmuteThread()
208 Toast.show(_(msg`You will now receive notifications for this thread`))
209 } else {
210 muteThread()
211 Toast.show(
212 _(msg`You will no longer receive notifications for this thread`),
213 )
214 }
215 } catch (e: any) {
216 if (e?.name !== 'AbortError') {
217 logger.error('Failed to toggle thread mute', {message: e})
218 Toast.show(
219 _(msg`Failed to toggle thread mute, please try again`),
220 'xmark',
221 )
222 }
223 }
224 }
225
226 const onCopyPostText = () => {
227 const str = richTextToString(richText, true)
228
229 Clipboard.setStringAsync(str)
230 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
231 }
232
233 const onPressTranslate = () => {
234 translate(record.text, langPrefs.primaryLanguage)
235
236 if (
237 bsky.dangerousIsType<AppBskyFeedPost.Record>(
238 post.record,
239 AppBskyFeedPost.isRecord,
240 )
241 ) {
242 logger.metric(
243 'translate',
244 {
245 sourceLanguages: post.record.langs ?? [],
246 targetLanguage: langPrefs.primaryLanguage,
247 textLength: post.record.text.length,
248 },
249 {statsig: false},
250 )
251 }
252 }
253
254 const onHidePost = () => {
255 hidePost({uri: postUri})
256 }
257
258 const hideInPWI = !!postAuthor.labels?.find(
259 label => label.val === '!no-unauthenticated',
260 )
261
262 const onPressShowMore = () => {
263 feedFeedback.sendInteraction({
264 event: 'app.bsky.feed.defs#requestMore',
265 item: postUri,
266 feedContext: postFeedContext,
267 reqId: postReqId,
268 })
269 Toast.show(
270 _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
271 )
272 }
273
274 const onPressShowLess = () => {
275 feedFeedback.sendInteraction({
276 event: 'app.bsky.feed.defs#requestLess',
277 item: postUri,
278 feedContext: postFeedContext,
279 reqId: postReqId,
280 })
281 if (onShowLess) {
282 onShowLess({
283 item: postUri,
284 feedContext: postFeedContext,
285 })
286 } else {
287 Toast.show(
288 _(msg({message: 'Feedback sent to feed operator', context: 'toast'})),
289 )
290 }
291 }
292
293 const onToggleQuotePostAttachment = async () => {
294 if (!quoteEmbed) return
295
296 const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
297 const isDetach = action === 'detach'
298
299 try {
300 await toggleQuoteDetachment({
301 post,
302 quoteUri: quoteEmbed.uri,
303 action: quoteEmbed.isDetached ? 'reattach' : 'detach',
304 })
305 Toast.show(
306 isDetach
307 ? _(msg`Quote post was successfully detached`)
308 : _(msg`Quote post was re-attached`),
309 )
310 } catch (e: any) {
311 Toast.show(
312 _(msg({message: 'Updating quote attachment failed', context: 'toast'})),
313 )
314 logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
315 }
316 }
317
318 const canHidePostForMe = !isAuthor && !isPostHidden
319 const canHideReplyForEveryone =
320 !isAuthor && isRootPostAuthor && !isPostHidden && isReply
321 const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
322
323 const onToggleReplyVisibility = async () => {
324 // TODO no threadgate?
325 if (!canHideReplyForEveryone) return
326
327 const action = isReplyHiddenByThreadgate ? 'show' : 'hide'
328 const isHide = action === 'hide'
329
330 try {
331 await toggleReplyVisibility({
332 postUri: rootUri,
333 replyUri: postUri,
334 action,
335 })
336 Toast.show(
337 isHide
338 ? _(msg`Reply was successfully hidden`)
339 : _(msg({message: 'Reply visibility updated', context: 'toast'})),
340 )
341 } catch (e: any) {
342 Toast.show(
343 _(msg({message: 'Updating reply visibility failed', context: 'toast'})),
344 )
345 logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
346 }
347 }
348
349 const onPressPin = () => {
350 logEvent(isPinned ? 'post:unpin' : 'post:pin', {})
351 pinPostMutate({
352 postUri,
353 postCid,
354 action: isPinned ? 'unpin' : 'pin',
355 })
356 }
357
358 const onBlockAuthor = async () => {
359 try {
360 await queueBlock()
361 Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
362 } catch (e: any) {
363 if (e?.name !== 'AbortError') {
364 logger.error('Failed to block account', {message: e})
365 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
366 }
367 }
368 }
369
370 const onMuteAuthor = async () => {
371 if (postAuthor.viewer?.muted) {
372 try {
373 await queueUnmute()
374 Toast.show(_(msg({message: 'Account unmuted', context: 'toast'})))
375 } catch (e: any) {
376 if (e?.name !== 'AbortError') {
377 logger.error('Failed to unmute account', {message: e})
378 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
379 }
380 }
381 } else {
382 try {
383 await queueMute()
384 Toast.show(_(msg({message: 'Account muted', context: 'toast'})))
385 } catch (e: any) {
386 if (e?.name !== 'AbortError') {
387 logger.error('Failed to mute account', {message: e})
388 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
389 }
390 }
391 }
392 }
393
394 const onReportMisclassification = () => {
395 const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl(
396 href,
397 )}`
398 openLink(url)
399 }
400
401 const onSignIn = () => requireSignIn(() => {})
402
403 const gate = useGate()
404 const isDiscoverDebugUser =
405 IS_INTERNAL ||
406 DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
407 gate('debug_show_feedcontext')
408
409 return (
410 <>
411 <Menu.Outer>
412 {isAuthor && (
413 <>
414 <Menu.Group>
415 <Menu.Item
416 testID="pinPostBtn"
417 label={
418 isPinned
419 ? _(msg`Unpin from profile`)
420 : _(msg`Pin to your profile`)
421 }
422 disabled={isPinPending}
423 onPress={onPressPin}>
424 <Menu.ItemText>
425 {isPinned
426 ? _(msg`Unpin from profile`)
427 : _(msg`Pin to your profile`)}
428 </Menu.ItemText>
429 <Menu.ItemIcon
430 icon={isPinPending ? Loader : PinIcon}
431 position="right"
432 />
433 </Menu.Item>
434 </Menu.Group>
435 <Menu.Divider />
436 </>
437 )}
438
439 <Menu.Group>
440 {!hideInPWI || hasSession ? (
441 <>
442 <Menu.Item
443 testID="postDropdownTranslateBtn"
444 label={_(msg`Translate`)}
445 onPress={onPressTranslate}>
446 <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
447 <Menu.ItemIcon icon={Translate} position="right" />
448 </Menu.Item>
449
450 <Menu.Item
451 testID="postDropdownCopyTextBtn"
452 label={_(msg`Copy post text`)}
453 onPress={onCopyPostText}>
454 <Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
455 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
456 </Menu.Item>
457 </>
458 ) : (
459 <Menu.Item
460 testID="postDropdownSignInBtn"
461 label={_(msg`Sign in to view post`)}
462 onPress={onSignIn}>
463 <Menu.ItemText>{_(msg`Sign in to view post`)}</Menu.ItemText>
464 <Menu.ItemIcon icon={Eye} position="right" />
465 </Menu.Item>
466 )}
467 </Menu.Group>
468
469 {hasSession && feedFeedback.enabled && (
470 <>
471 <Menu.Divider />
472 <Menu.Group>
473 <Menu.Item
474 testID="postDropdownShowMoreBtn"
475 label={_(msg`Show more like this`)}
476 onPress={onPressShowMore}>
477 <Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
478 <Menu.ItemIcon icon={EmojiSmile} position="right" />
479 </Menu.Item>
480
481 <Menu.Item
482 testID="postDropdownShowLessBtn"
483 label={_(msg`Show less like this`)}
484 onPress={onPressShowLess}>
485 <Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
486 <Menu.ItemIcon icon={EmojiSad} position="right" />
487 </Menu.Item>
488 </Menu.Group>
489 </>
490 )}
491
492 {isDiscoverDebugUser && (
493 <>
494 <Menu.Divider />
495 <Menu.Item
496 testID="postDropdownReportMisclassificationBtn"
497 label={_(msg`Assign topic for algo`)}
498 onPress={onReportMisclassification}>
499 <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
500 <Menu.ItemIcon icon={AtomIcon} position="right" />
501 </Menu.Item>
502 </>
503 )}
504
505 {hasSession && (
506 <>
507 <Menu.Divider />
508 <Menu.Group>
509 <Menu.Item
510 testID="postDropdownMuteThreadBtn"
511 label={
512 isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
513 }
514 onPress={onToggleThreadMute}>
515 <Menu.ItemText>
516 {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
517 </Menu.ItemText>
518 <Menu.ItemIcon
519 icon={isThreadMuted ? Unmute : Mute}
520 position="right"
521 />
522 </Menu.Item>
523
524 <Menu.Item
525 testID="postDropdownMuteWordsBtn"
526 label={_(msg`Mute words & tags`)}
527 onPress={() => mutedWordsDialogControl.open()}>
528 <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
529 <Menu.ItemIcon icon={Filter} position="right" />
530 </Menu.Item>
531 </Menu.Group>
532 </>
533 )}
534
535 {hasSession &&
536 (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
537 <>
538 <Menu.Divider />
539 <Menu.Group>
540 {canHidePostForMe && (
541 <Menu.Item
542 testID="postDropdownHideBtn"
543 label={
544 isReply
545 ? _(msg`Hide reply for me`)
546 : _(msg`Hide post for me`)
547 }
548 onPress={() => hidePromptControl.open()}>
549 <Menu.ItemText>
550 {isReply
551 ? _(msg`Hide reply for me`)
552 : _(msg`Hide post for me`)}
553 </Menu.ItemText>
554 <Menu.ItemIcon icon={EyeSlash} position="right" />
555 </Menu.Item>
556 )}
557 {canHideReplyForEveryone && (
558 <Menu.Item
559 testID="postDropdownHideBtn"
560 label={
561 isReplyHiddenByThreadgate
562 ? _(msg`Show reply for everyone`)
563 : _(msg`Hide reply for everyone`)
564 }
565 onPress={
566 isReplyHiddenByThreadgate
567 ? onToggleReplyVisibility
568 : () => hideReplyConfirmControl.open()
569 }>
570 <Menu.ItemText>
571 {isReplyHiddenByThreadgate
572 ? _(msg`Show reply for everyone`)
573 : _(msg`Hide reply for everyone`)}
574 </Menu.ItemText>
575 <Menu.ItemIcon
576 icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
577 position="right"
578 />
579 </Menu.Item>
580 )}
581
582 {canDetachQuote && (
583 <Menu.Item
584 disabled={isDetachPending}
585 testID="postDropdownHideBtn"
586 label={
587 quoteEmbed.isDetached
588 ? _(msg`Re-attach quote`)
589 : _(msg`Detach quote`)
590 }
591 onPress={
592 quoteEmbed.isDetached
593 ? onToggleQuotePostAttachment
594 : () => quotePostDetachConfirmControl.open()
595 }>
596 <Menu.ItemText>
597 {quoteEmbed.isDetached
598 ? _(msg`Re-attach quote`)
599 : _(msg`Detach quote`)}
600 </Menu.ItemText>
601 <Menu.ItemIcon
602 icon={
603 isDetachPending
604 ? Loader
605 : quoteEmbed.isDetached
606 ? Eye
607 : EyeSlash
608 }
609 position="right"
610 />
611 </Menu.Item>
612 )}
613 </Menu.Group>
614 </>
615 )}
616
617 {hasSession && (
618 <>
619 <Menu.Divider />
620 <Menu.Group>
621 {!isAuthor && (
622 <>
623 <Menu.Item
624 testID="postDropdownMuteBtn"
625 label={
626 postAuthor.viewer?.muted
627 ? _(msg`Unmute account`)
628 : _(msg`Mute account`)
629 }
630 onPress={onMuteAuthor}>
631 <Menu.ItemText>
632 {postAuthor.viewer?.muted
633 ? _(msg`Unmute account`)
634 : _(msg`Mute account`)}
635 </Menu.ItemText>
636 <Menu.ItemIcon
637 icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon}
638 position="right"
639 />
640 </Menu.Item>
641
642 {!postAuthor.viewer?.blocking && (
643 <Menu.Item
644 testID="postDropdownBlockBtn"
645 label={_(msg`Block account`)}
646 onPress={() => blockPromptControl.open()}>
647 <Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText>
648 <Menu.ItemIcon icon={PersonX} position="right" />
649 </Menu.Item>
650 )}
651
652 <Menu.Item
653 testID="postDropdownReportBtn"
654 label={_(msg`Report post`)}
655 onPress={() => reportDialogControl.open()}>
656 <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
657 <Menu.ItemIcon icon={Warning} position="right" />
658 </Menu.Item>
659 </>
660 )}
661
662 {isAuthor && (
663 <>
664 <Menu.Item
665 testID="postDropdownEditPostInteractions"
666 label={_(msg`Edit interaction settings`)}
667 onPress={() => postInteractionSettingsDialogControl.open()}
668 {...(isAuthor
669 ? Platform.select({
670 web: {
671 onHoverIn: prefetchPostInteractionSettings,
672 },
673 native: {
674 onPressIn: prefetchPostInteractionSettings,
675 },
676 })
677 : {})}>
678 <Menu.ItemText>
679 {_(msg`Edit interaction settings`)}
680 </Menu.ItemText>
681 <Menu.ItemIcon icon={Gear} position="right" />
682 </Menu.Item>
683 <Menu.Item
684 testID="postDropdownDeleteBtn"
685 label={_(msg`Delete post`)}
686 onPress={() => deletePromptControl.open()}>
687 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
688 <Menu.ItemIcon icon={Trash} position="right" />
689 </Menu.Item>
690 </>
691 )}
692 </Menu.Group>
693 </>
694 )}
695 </Menu.Outer>
696
697 <Prompt.Basic
698 control={deletePromptControl}
699 title={_(msg`Delete this post?`)}
700 description={_(
701 msg`If you remove this post, you won't be able to recover it.`,
702 )}
703 onConfirm={onDeletePost}
704 confirmButtonCta={_(msg`Delete`)}
705 confirmButtonColor="negative"
706 />
707
708 <Prompt.Basic
709 control={hidePromptControl}
710 title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)}
711 description={_(
712 msg`This post will be hidden from feeds and threads. This cannot be undone.`,
713 )}
714 onConfirm={onHidePost}
715 confirmButtonCta={_(msg`Hide`)}
716 />
717
718 <ReportDialog
719 control={reportDialogControl}
720 subject={{
721 ...post,
722 $type: 'app.bsky.feed.defs#postView',
723 }}
724 />
725
726 <PostInteractionSettingsDialog
727 control={postInteractionSettingsDialogControl}
728 postUri={post.uri}
729 rootPostUri={rootUri}
730 initialThreadgateView={post.threadgate}
731 />
732
733 <Prompt.Basic
734 control={quotePostDetachConfirmControl}
735 title={_(msg`Detach quote post?`)}
736 description={_(
737 msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`,
738 )}
739 onConfirm={onToggleQuotePostAttachment}
740 confirmButtonCta={_(msg`Yes, detach`)}
741 />
742
743 <Prompt.Basic
744 control={hideReplyConfirmControl}
745 title={_(msg`Hide this reply?`)}
746 description={_(
747 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.`,
748 )}
749 onConfirm={onToggleReplyVisibility}
750 confirmButtonCta={_(msg`Yes, hide`)}
751 />
752
753 <Prompt.Basic
754 control={blockPromptControl}
755 title={_(msg`Block Account?`)}
756 description={_(
757 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
758 )}
759 onConfirm={onBlockAuthor}
760 confirmButtonCta={_(msg`Block`)}
761 confirmButtonColor="negative"
762 />
763 </>
764 )
765}
766PostMenuItems = memo(PostMenuItems)
767export {PostMenuItems}