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