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(_(msg({message: 'Feedback sent!', context: 'toast'})))
270 }
271
272 const onPressShowLess = () => {
273 feedFeedback.sendInteraction({
274 event: 'app.bsky.feed.defs#requestLess',
275 item: postUri,
276 feedContext: postFeedContext,
277 reqId: postReqId,
278 })
279 if (onShowLess) {
280 onShowLess({
281 item: postUri,
282 feedContext: postFeedContext,
283 })
284 } else {
285 Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
286 }
287 }
288
289 const onToggleQuotePostAttachment = async () => {
290 if (!quoteEmbed) return
291
292 const action = quoteEmbed.isDetached ? 'reattach' : 'detach'
293 const isDetach = action === 'detach'
294
295 try {
296 await toggleQuoteDetachment({
297 post,
298 quoteUri: quoteEmbed.uri,
299 action: quoteEmbed.isDetached ? 'reattach' : 'detach',
300 })
301 Toast.show(
302 isDetach
303 ? _(msg`Quote post was successfully detached`)
304 : _(msg`Quote post was re-attached`),
305 )
306 } catch (e: any) {
307 Toast.show(
308 _(msg({message: 'Updating quote attachment failed', context: 'toast'})),
309 )
310 logger.error(`Failed to ${action} quote`, {safeMessage: e.message})
311 }
312 }
313
314 const canHidePostForMe = !isAuthor && !isPostHidden
315 const canHideReplyForEveryone =
316 !isAuthor && isRootPostAuthor && !isPostHidden && isReply
317 const canDetachQuote = quoteEmbed && quoteEmbed.isOwnedByViewer
318
319 const onToggleReplyVisibility = async () => {
320 // TODO no threadgate?
321 if (!canHideReplyForEveryone) return
322
323 const action = isReplyHiddenByThreadgate ? 'show' : 'hide'
324 const isHide = action === 'hide'
325
326 try {
327 await toggleReplyVisibility({
328 postUri: rootUri,
329 replyUri: postUri,
330 action,
331 })
332 Toast.show(
333 isHide
334 ? _(msg`Reply was successfully hidden`)
335 : _(msg({message: 'Reply visibility updated', context: 'toast'})),
336 )
337 } catch (e: any) {
338 Toast.show(
339 _(msg({message: 'Updating reply visibility failed', context: 'toast'})),
340 )
341 logger.error(`Failed to ${action} reply`, {safeMessage: e.message})
342 }
343 }
344
345 const onPressPin = () => {
346 logEvent(isPinned ? 'post:unpin' : 'post:pin', {})
347 pinPostMutate({
348 postUri,
349 postCid,
350 action: isPinned ? 'unpin' : 'pin',
351 })
352 }
353
354 const onBlockAuthor = async () => {
355 try {
356 await queueBlock()
357 Toast.show(_(msg({message: 'Account blocked', context: 'toast'})))
358 } catch (e: any) {
359 if (e?.name !== 'AbortError') {
360 logger.error('Failed to block account', {message: e})
361 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
362 }
363 }
364 }
365
366 const onMuteAuthor = async () => {
367 if (postAuthor.viewer?.muted) {
368 try {
369 await queueUnmute()
370 Toast.show(_(msg({message: 'Account unmuted', context: 'toast'})))
371 } catch (e: any) {
372 if (e?.name !== 'AbortError') {
373 logger.error('Failed to unmute account', {message: e})
374 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
375 }
376 }
377 } else {
378 try {
379 await queueMute()
380 Toast.show(_(msg({message: 'Account muted', context: 'toast'})))
381 } catch (e: any) {
382 if (e?.name !== 'AbortError') {
383 logger.error('Failed to mute account', {message: e})
384 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
385 }
386 }
387 }
388 }
389
390 const onReportMisclassification = () => {
391 const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl(
392 href,
393 )}`
394 openLink(url)
395 }
396
397 const onSignIn = () => requireSignIn(() => {})
398
399 const gate = useGate()
400 const isDiscoverDebugUser =
401 IS_INTERNAL ||
402 DISCOVER_DEBUG_DIDS[currentAccount?.did || ''] ||
403 gate('debug_show_feedcontext')
404
405 return (
406 <>
407 <Menu.Outer>
408 {isAuthor && (
409 <>
410 <Menu.Group>
411 <Menu.Item
412 testID="pinPostBtn"
413 label={
414 isPinned
415 ? _(msg`Unpin from profile`)
416 : _(msg`Pin to your profile`)
417 }
418 disabled={isPinPending}
419 onPress={onPressPin}>
420 <Menu.ItemText>
421 {isPinned
422 ? _(msg`Unpin from profile`)
423 : _(msg`Pin to your profile`)}
424 </Menu.ItemText>
425 <Menu.ItemIcon
426 icon={isPinPending ? Loader : PinIcon}
427 position="right"
428 />
429 </Menu.Item>
430 </Menu.Group>
431 <Menu.Divider />
432 </>
433 )}
434
435 <Menu.Group>
436 {!hideInPWI || hasSession ? (
437 <>
438 <Menu.Item
439 testID="postDropdownTranslateBtn"
440 label={_(msg`Translate`)}
441 onPress={onPressTranslate}>
442 <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
443 <Menu.ItemIcon icon={Translate} position="right" />
444 </Menu.Item>
445
446 <Menu.Item
447 testID="postDropdownCopyTextBtn"
448 label={_(msg`Copy post text`)}
449 onPress={onCopyPostText}>
450 <Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
451 <Menu.ItemIcon icon={ClipboardIcon} position="right" />
452 </Menu.Item>
453 </>
454 ) : (
455 <Menu.Item
456 testID="postDropdownSignInBtn"
457 label={_(msg`Sign in to view post`)}
458 onPress={onSignIn}>
459 <Menu.ItemText>{_(msg`Sign in to view post`)}</Menu.ItemText>
460 <Menu.ItemIcon icon={Eye} position="right" />
461 </Menu.Item>
462 )}
463 </Menu.Group>
464
465 {hasSession && feedFeedback.enabled && (
466 <>
467 <Menu.Divider />
468 <Menu.Group>
469 <Menu.Item
470 testID="postDropdownShowMoreBtn"
471 label={_(msg`Show more like this`)}
472 onPress={onPressShowMore}>
473 <Menu.ItemText>{_(msg`Show more like this`)}</Menu.ItemText>
474 <Menu.ItemIcon icon={EmojiSmile} position="right" />
475 </Menu.Item>
476
477 <Menu.Item
478 testID="postDropdownShowLessBtn"
479 label={_(msg`Show less like this`)}
480 onPress={onPressShowLess}>
481 <Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
482 <Menu.ItemIcon icon={EmojiSad} position="right" />
483 </Menu.Item>
484 </Menu.Group>
485 </>
486 )}
487
488 {isDiscoverDebugUser && (
489 <Menu.Item
490 testID="postDropdownReportMisclassificationBtn"
491 label={_(msg`Assign topic for algo`)}
492 onPress={onReportMisclassification}>
493 <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText>
494 <Menu.ItemIcon icon={AtomIcon} position="right" />
495 </Menu.Item>
496 )}
497
498 {hasSession && (
499 <>
500 <Menu.Divider />
501 <Menu.Group>
502 <Menu.Item
503 testID="postDropdownMuteThreadBtn"
504 label={
505 isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
506 }
507 onPress={onToggleThreadMute}>
508 <Menu.ItemText>
509 {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)}
510 </Menu.ItemText>
511 <Menu.ItemIcon
512 icon={isThreadMuted ? Unmute : Mute}
513 position="right"
514 />
515 </Menu.Item>
516
517 <Menu.Item
518 testID="postDropdownMuteWordsBtn"
519 label={_(msg`Mute words & tags`)}
520 onPress={() => mutedWordsDialogControl.open()}>
521 <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
522 <Menu.ItemIcon icon={Filter} position="right" />
523 </Menu.Item>
524 </Menu.Group>
525 </>
526 )}
527
528 {hasSession &&
529 (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && (
530 <>
531 <Menu.Divider />
532 <Menu.Group>
533 {canHidePostForMe && (
534 <Menu.Item
535 testID="postDropdownHideBtn"
536 label={
537 isReply
538 ? _(msg`Hide reply for me`)
539 : _(msg`Hide post for me`)
540 }
541 onPress={() => hidePromptControl.open()}>
542 <Menu.ItemText>
543 {isReply
544 ? _(msg`Hide reply for me`)
545 : _(msg`Hide post for me`)}
546 </Menu.ItemText>
547 <Menu.ItemIcon icon={EyeSlash} position="right" />
548 </Menu.Item>
549 )}
550 {canHideReplyForEveryone && (
551 <Menu.Item
552 testID="postDropdownHideBtn"
553 label={
554 isReplyHiddenByThreadgate
555 ? _(msg`Show reply for everyone`)
556 : _(msg`Hide reply for everyone`)
557 }
558 onPress={
559 isReplyHiddenByThreadgate
560 ? onToggleReplyVisibility
561 : () => hideReplyConfirmControl.open()
562 }>
563 <Menu.ItemText>
564 {isReplyHiddenByThreadgate
565 ? _(msg`Show reply for everyone`)
566 : _(msg`Hide reply for everyone`)}
567 </Menu.ItemText>
568 <Menu.ItemIcon
569 icon={isReplyHiddenByThreadgate ? Eye : EyeSlash}
570 position="right"
571 />
572 </Menu.Item>
573 )}
574
575 {canDetachQuote && (
576 <Menu.Item
577 disabled={isDetachPending}
578 testID="postDropdownHideBtn"
579 label={
580 quoteEmbed.isDetached
581 ? _(msg`Re-attach quote`)
582 : _(msg`Detach quote`)
583 }
584 onPress={
585 quoteEmbed.isDetached
586 ? onToggleQuotePostAttachment
587 : () => quotePostDetachConfirmControl.open()
588 }>
589 <Menu.ItemText>
590 {quoteEmbed.isDetached
591 ? _(msg`Re-attach quote`)
592 : _(msg`Detach quote`)}
593 </Menu.ItemText>
594 <Menu.ItemIcon
595 icon={
596 isDetachPending
597 ? Loader
598 : quoteEmbed.isDetached
599 ? Eye
600 : EyeSlash
601 }
602 position="right"
603 />
604 </Menu.Item>
605 )}
606 </Menu.Group>
607 </>
608 )}
609
610 {hasSession && (
611 <>
612 <Menu.Divider />
613 <Menu.Group>
614 {!isAuthor && (
615 <>
616 <Menu.Item
617 testID="postDropdownMuteBtn"
618 label={
619 postAuthor.viewer?.muted
620 ? _(msg`Unmute account`)
621 : _(msg`Mute account`)
622 }
623 onPress={onMuteAuthor}>
624 <Menu.ItemText>
625 {postAuthor.viewer?.muted
626 ? _(msg`Unmute account`)
627 : _(msg`Mute account`)}
628 </Menu.ItemText>
629 <Menu.ItemIcon
630 icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon}
631 position="right"
632 />
633 </Menu.Item>
634
635 {!postAuthor.viewer?.blocking && (
636 <Menu.Item
637 testID="postDropdownBlockBtn"
638 label={_(msg`Block account`)}
639 onPress={() => blockPromptControl.open()}>
640 <Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText>
641 <Menu.ItemIcon icon={PersonX} position="right" />
642 </Menu.Item>
643 )}
644
645 <Menu.Item
646 testID="postDropdownReportBtn"
647 label={_(msg`Report post`)}
648 onPress={() => reportDialogControl.open()}>
649 <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
650 <Menu.ItemIcon icon={Warning} position="right" />
651 </Menu.Item>
652 </>
653 )}
654
655 {isAuthor && (
656 <>
657 <Menu.Item
658 testID="postDropdownEditPostInteractions"
659 label={_(msg`Edit interaction settings`)}
660 onPress={() => postInteractionSettingsDialogControl.open()}
661 {...(isAuthor
662 ? Platform.select({
663 web: {
664 onHoverIn: prefetchPostInteractionSettings,
665 },
666 native: {
667 onPressIn: prefetchPostInteractionSettings,
668 },
669 })
670 : {})}>
671 <Menu.ItemText>
672 {_(msg`Edit interaction settings`)}
673 </Menu.ItemText>
674 <Menu.ItemIcon icon={Gear} position="right" />
675 </Menu.Item>
676 <Menu.Item
677 testID="postDropdownDeleteBtn"
678 label={_(msg`Delete post`)}
679 onPress={() => deletePromptControl.open()}>
680 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
681 <Menu.ItemIcon icon={Trash} position="right" />
682 </Menu.Item>
683 </>
684 )}
685 </Menu.Group>
686 </>
687 )}
688 </Menu.Outer>
689
690 <Prompt.Basic
691 control={deletePromptControl}
692 title={_(msg`Delete this post?`)}
693 description={_(
694 msg`If you remove this post, you won't be able to recover it.`,
695 )}
696 onConfirm={onDeletePost}
697 confirmButtonCta={_(msg`Delete`)}
698 confirmButtonColor="negative"
699 />
700
701 <Prompt.Basic
702 control={hidePromptControl}
703 title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)}
704 description={_(
705 msg`This post will be hidden from feeds and threads. This cannot be undone.`,
706 )}
707 onConfirm={onHidePost}
708 confirmButtonCta={_(msg`Hide`)}
709 />
710
711 <ReportDialog
712 control={reportDialogControl}
713 subject={{
714 ...post,
715 $type: 'app.bsky.feed.defs#postView',
716 }}
717 />
718
719 <PostInteractionSettingsDialog
720 control={postInteractionSettingsDialogControl}
721 postUri={post.uri}
722 rootPostUri={rootUri}
723 initialThreadgateView={post.threadgate}
724 />
725
726 <Prompt.Basic
727 control={quotePostDetachConfirmControl}
728 title={_(msg`Detach quote post?`)}
729 description={_(
730 msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`,
731 )}
732 onConfirm={onToggleQuotePostAttachment}
733 confirmButtonCta={_(msg`Yes, detach`)}
734 />
735
736 <Prompt.Basic
737 control={hideReplyConfirmControl}
738 title={_(msg`Hide this reply?`)}
739 description={_(
740 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.`,
741 )}
742 onConfirm={onToggleReplyVisibility}
743 confirmButtonCta={_(msg`Yes, hide`)}
744 />
745
746 <Prompt.Basic
747 control={blockPromptControl}
748 title={_(msg`Block Account?`)}
749 description={_(
750 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
751 )}
752 onConfirm={onBlockAuthor}
753 confirmButtonCta={_(msg`Block`)}
754 confirmButtonColor="negative"
755 />
756 </>
757 )
758}
759PostMenuItems = memo(PostMenuItems)
760export {PostMenuItems}