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