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 {IS_INTERNAL} from '#/lib/app-info'
21import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
22import {useOpenLink} from '#/lib/hooks/useOpenLink'
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 {getTranslatorLink} from '#/locale/helpers'
33import {logger} from '#/logger'
34import {type Shadow} from '#/state/cache/post-shadow'
35import {useProfileShadow} from '#/state/cache/profile-shadow'
36import {useFeedFeedbackContext} from '#/state/feed-feedback'
37import {useLanguagePrefs} from '#/state/preferences'
38import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences'
39import {usePinnedPostMutation} from '#/state/queries/pinned-post'
40import {
41 usePostDeleteMutation,
42 useThreadMuteMutationQueue,
43} from '#/state/queries/post'
44import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
45import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util'
46import {
47 useProfileBlockMutationQueue,
48 useProfileMuteMutationQueue,
49} from '#/state/queries/profile'
50import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
51import {useRequireAuth, useSession} from '#/state/session'
52import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
53import * as Toast from '#/view/com/util/Toast'
54import {useDialogControl} from '#/components/Dialog'
55import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
56import {
57 PostInteractionSettingsDialog,
58 usePrefetchPostInteractionSettings,
59} from '#/components/dialogs/PostInteractionSettingsDialog'
60import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
61import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
62import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
63import {
64 EmojiSad_Stroke2_Corner0_Rounded as EmojiSad,
65 EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile,
66} from '#/components/icons/Emoji'
67import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye'
68import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
69import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
70import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
71import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
72import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person'
73import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin'
74import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2'
75import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
76import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
77import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
78import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
79import {Loader} from '#/components/Loader'
80import * as Menu from '#/components/Menu'
81import {
82 ReportDialog,
83 useReportDialogControl,
84} from '#/components/moderation/ReportDialog'
85import * as Prompt from '#/components/Prompt'
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 navigation = useNavigation<NavigationProp>()
122 const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
123 const blockPromptControl = useDialogControl()
124 const reportDialogControl = useReportDialogControl()
125 const deletePromptControl = useDialogControl()
126 const hidePromptControl = useDialogControl()
127 const postInteractionSettingsDialogControl = useDialogControl()
128 const quotePostDetachConfirmControl = useDialogControl()
129 const hideReplyConfirmControl = useDialogControl()
130 const {mutateAsync: toggleReplyVisibility} =
131 useToggleReplyVisibilityMutation()
132
133 const postUri = post.uri
134 const postCid = post.cid
135 const postAuthor = useProfileShadow(post.author)
136 const quoteEmbed = useMemo(() => {
137 if (!currentAccount || !post.embed) return
138 return getMaybeDetachedQuoteEmbed({
139 viewerDid: currentAccount.did,
140 post,
141 })
142 }, [post, currentAccount])
143
144 const rootUri = record.reply?.root?.uri || postUri
145 const isReply = Boolean(record.reply)
146 const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
147 post,
148 rootUri,
149 )
150 const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri)
151 const isAuthor = postAuthor.did === currentAccount?.did
152 const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did
153 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
154 threadgateRecord,
155 })
156 const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri)
157 const isPinned = post.viewer?.pinned
158
159 const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} =
160 useToggleQuoteDetachmentMutation()
161
162 const [queueBlock] = useProfileBlockMutationQueue(postAuthor)
163 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(postAuthor)
164
165 const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({
166 postUri: post.uri,
167 rootPostUri: rootUri,
168 })
169
170 const href = useMemo(() => {
171 const urip = new AtUri(postUri)
172 return makeProfileLink(postAuthor, 'post', urip.rkey)
173 }, [postUri, postAuthor])
174
175 const translatorUrl = getTranslatorLink(
176 record.text,
177 langPrefs.primaryLanguage,
178 )
179
180 const onDeletePost = () => {
181 deletePostMutate({uri: postUri}).then(
182 () => {
183 Toast.show(_(msg({message: 'Post deleted', context: 'toast'})))
184
185 const route = getCurrentRoute(navigation.getState())
186 if (route.name === 'PostThread') {
187 const params = route.params as CommonNavigatorParams['PostThread']
188 if (
189 currentAccount &&
190 isAuthor &&
191 (params.name === currentAccount.handle ||
192 params.name === currentAccount.did)
193 ) {
194 const currentHref = makeProfileLink(postAuthor, 'post', params.rkey)
195 if (currentHref === href && navigation.canGoBack()) {
196 navigation.goBack()
197 }
198 }
199 }
200 },
201 e => {
202 logger.error('Failed to delete post', {message: e})
203 Toast.show(_(msg`Failed to delete post, please try again`), 'xmark')
204 },
205 )
206 }
207
208 const onToggleThreadMute = () => {
209 try {
210 if (isThreadMuted) {
211 unmuteThread()
212 Toast.show(_(msg`You will now receive notifications for this thread`))
213 } else {
214 muteThread()
215 Toast.show(
216 _(msg`You will no longer receive notifications for this thread`),
217 )
218 }
219 } catch (e: any) {
220 if (e?.name !== 'AbortError') {
221 logger.error('Failed to toggle thread mute', {message: e})
222 Toast.show(
223 _(msg`Failed to toggle thread mute, please try again`),
224 'xmark',
225 )
226 }
227 }
228 }
229
230 const onCopyPostText = () => {
231 const str = richTextToString(richText, true)
232
233 Clipboard.setStringAsync(str)
234 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check')
235 }
236
237 const onPressTranslate = async () => {
238 await openLink(translatorUrl, true)
239
240 if (
241 bsky.dangerousIsType<AppBskyFeedPost.Record>(
242 post.record,
243 AppBskyFeedPost.isRecord,
244 )
245 ) {
246 logger.metric(
247 'translate',
248 {
249 sourceLanguages: post.record.langs ?? [],
250 targetLanguage: langPrefs.primaryLanguage,
251 textLength: post.record.text.length,
252 },
253 {statsig: false},
254 )
255 }
256 }
257
258 const onHidePost = () => {
259 hidePost({uri: postUri})
260 }
261
262 const hideInPWI = !!postAuthor.labels?.find(
263 label => label.val === '!no-unauthenticated',
264 )
265
266 const onPressShowMore = () => {
267 feedFeedback.sendInteraction({
268 event: 'app.bsky.feed.defs#requestMore',
269 item: postUri,
270 feedContext: postFeedContext,
271 reqId: postReqId,
272 })
273 Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
274 }
275
276 const onPressShowLess = () => {
277 feedFeedback.sendInteraction({
278 event: 'app.bsky.feed.defs#requestLess',
279 item: postUri,
280 feedContext: postFeedContext,
281 reqId: postReqId,
282 })
283 if (onShowLess) {
284 onShowLess({
285 item: postUri,
286 feedContext: postFeedContext,
287 })
288 } else {
289 Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
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 <Menu.Item
494 testID="postDropdownReportMisclassificationBtn"
495 label={_(msg`Assign topic for algo`)}
496 onPress={onReportMisclassification}>
497 <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
498 <Menu.ItemIcon icon={AtomIcon} position="right" />
499 </Menu.Item>
500 )}
501
502 {hasSession && (
503 <>
504 <Menu.Divider />
505 <Menu.Group>
506 <Menu.Item
507 testID="postDropdownMuteThreadBtn"
508 label={
509 isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
510 }
511 onPress={onToggleThreadMute}>
512 <Menu.ItemText>
513 {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
514 </Menu.ItemText>
515 <Menu.ItemIcon
516 icon={isThreadMuted ? Unmute : Mute}
517 position="right"
518 />
519 </Menu.Item>
520
521 <Menu.Item
522 testID="postDropdownMuteWordsBtn"
523 label={_(msg`Mute words & tags`)}
524 onPress={() => mutedWordsDialogControl.open()}>
525 <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
526 <Menu.ItemIcon icon={Filter} position="right" />
527 </Menu.Item>
528 </Menu.Group>
529 </>
530 )}
531
532 {hasSession &&
533 (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
534 <>
535 <Menu.Divider />
536 <Menu.Group>
537 {canHidePostForMe && (
538 <Menu.Item
539 testID="postDropdownHideBtn"
540 label={
541 isReply
542 ? _(msg`Hide reply for me`)
543 : _(msg`Hide post for me`)
544 }
545 onPress={() => hidePromptControl.open()}>
546 <Menu.ItemText>
547 {isReply
548 ? _(msg`Hide reply for me`)
549 : _(msg`Hide post for me`)}
550 </Menu.ItemText>
551 <Menu.ItemIcon icon={EyeSlash} position="right" />
552 </Menu.Item>
553 )}
554 {canHideReplyForEveryone && (
555 <Menu.Item
556 testID="postDropdownHideBtn"
557 label={
558 isReplyHiddenByThreadgate
559 ? _(msg`Show reply for everyone`)
560 : _(msg`Hide reply for everyone`)
561 }
562 onPress={
563 isReplyHiddenByThreadgate
564 ? onToggleReplyVisibility
565 : () => hideReplyConfirmControl.open()
566 }>
567 <Menu.ItemText>
568 {isReplyHiddenByThreadgate
569 ? _(msg`Show reply for everyone`)
570 : _(msg`Hide reply for everyone`)}
571 </Menu.ItemText>
572 <Menu.ItemIcon
573 icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
574 position="right"
575 />
576 </Menu.Item>
577 )}
578
579 {canDetachQuote && (
580 <Menu.Item
581 disabled={isDetachPending}
582 testID="postDropdownHideBtn"
583 label={
584 quoteEmbed.isDetached
585 ? _(msg`Re-attach quote`)
586 : _(msg`Detach quote`)
587 }
588 onPress={
589 quoteEmbed.isDetached
590 ? onToggleQuotePostAttachment
591 : () => quotePostDetachConfirmControl.open()
592 }>
593 <Menu.ItemText>
594 {quoteEmbed.isDetached
595 ? _(msg`Re-attach quote`)
596 : _(msg`Detach quote`)}
597 </Menu.ItemText>
598 <Menu.ItemIcon
599 icon={
600 isDetachPending
601 ? Loader
602 : quoteEmbed.isDetached
603 ? Eye
604 : EyeSlash
605 }
606 position="right"
607 />
608 </Menu.Item>
609 )}
610 </Menu.Group>
611 </>
612 )}
613
614 {hasSession && (
615 <>
616 <Menu.Divider />
617 <Menu.Group>
618 {!isAuthor && (
619 <>
620 <Menu.Item
621 testID="postDropdownMuteBtn"
622 label={
623 postAuthor.viewer?.muted
624 ? _(msg`Unmute account`)
625 : _(msg`Mute account`)
626 }
627 onPress={onMuteAuthor}>
628 <Menu.ItemText>
629 {postAuthor.viewer?.muted
630 ? _(msg`Unmute account`)
631 : _(msg`Mute account`)}
632 </Menu.ItemText>
633 <Menu.ItemIcon
634 icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon}
635 position="right"
636 />
637 </Menu.Item>
638
639 {!postAuthor.viewer?.blocking && (
640 <Menu.Item
641 testID="postDropdownBlockBtn"
642 label={_(msg`Block account`)}
643 onPress={() => blockPromptControl.open()}>
644 <Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText>
645 <Menu.ItemIcon icon={PersonX} position="right" />
646 </Menu.Item>
647 )}
648
649 <Menu.Item
650 testID="postDropdownReportBtn"
651 label={_(msg`Report post`)}
652 onPress={() => reportDialogControl.open()}>
653 <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
654 <Menu.ItemIcon icon={Warning} position="right" />
655 </Menu.Item>
656 </>
657 )}
658
659 {isAuthor && (
660 <>
661 <Menu.Item
662 testID="postDropdownEditPostInteractions"
663 label={_(msg`Edit interaction settings`)}
664 onPress={() => postInteractionSettingsDialogControl.open()}
665 {...(isAuthor
666 ? Platform.select({
667 web: {
668 onHoverIn: prefetchPostInteractionSettings,
669 },
670 native: {
671 onPressIn: prefetchPostInteractionSettings,
672 },
673 })
674 : {})}>
675 <Menu.ItemText>
676 {_(msg`Edit interaction settings`)}
677 </Menu.ItemText>
678 <Menu.ItemIcon icon={Gear} position="right" />
679 </Menu.Item>
680 <Menu.Item
681 testID="postDropdownDeleteBtn"
682 label={_(msg`Delete post`)}
683 onPress={() => deletePromptControl.open()}>
684 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
685 <Menu.ItemIcon icon={Trash} position="right" />
686 </Menu.Item>
687 </>
688 )}
689 </Menu.Group>
690 </>
691 )}
692 </Menu.Outer>
693
694 <Prompt.Basic
695 control={deletePromptControl}
696 title={_(msg`Delete this post?`)}
697 description={_(
698 msg`If you remove this post, you won't be able to recover it.`,
699 )}
700 onConfirm={onDeletePost}
701 confirmButtonCta={_(msg`Delete`)}
702 confirmButtonColor="negative"
703 />
704
705 <Prompt.Basic
706 control={hidePromptControl}
707 title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)}
708 description={_(
709 msg`This post will be hidden from feeds and threads. This cannot be undone.`,
710 )}
711 onConfirm={onHidePost}
712 confirmButtonCta={_(msg`Hide`)}
713 />
714
715 <ReportDialog
716 control={reportDialogControl}
717 subject={{
718 ...post,
719 $type: 'app.bsky.feed.defs#postView',
720 }}
721 />
722
723 <PostInteractionSettingsDialog
724 control={postInteractionSettingsDialogControl}
725 postUri={post.uri}
726 rootPostUri={rootUri}
727 initialThreadgateView={post.threadgate}
728 />
729
730 <Prompt.Basic
731 control={quotePostDetachConfirmControl}
732 title={_(msg`Detach quote post?`)}
733 description={_(
734 msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`,
735 )}
736 onConfirm={onToggleQuotePostAttachment}
737 confirmButtonCta={_(msg`Yes, detach`)}
738 />
739
740 <Prompt.Basic
741 control={hideReplyConfirmControl}
742 title={_(msg`Hide this reply?`)}
743 description={_(
744 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.`,
745 )}
746 onConfirm={onToggleReplyVisibility}
747 confirmButtonCta={_(msg`Yes, hide`)}
748 />
749
750 <Prompt.Basic
751 control={blockPromptControl}
752 title={_(msg`Block Account?`)}
753 description={_(
754 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
755 )}
756 onConfirm={onBlockAuthor}
757 confirmButtonCta={_(msg`Block`)}
758 confirmButtonColor="negative"
759 />
760 </>
761 )
762}
763PostMenuItems = memo(PostMenuItems)
764export {PostMenuItems}