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