mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at next/base 783 lines 28 kB view raw
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}