mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at tooltip 26 kB view raw
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 {IS_INTERNAL} from '#/lib/app-info' 21import {DISCOVER_DEBUG_DIDS} from '#/lib/constants' 22import {useOpenLink} from '#/lib/hooks/useOpenLink' 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 {getTranslatorLink} from '#/locale/helpers' 33import {logger} from '#/logger' 34import {type Shadow} from '#/state/cache/post-shadow' 35import {useProfileShadow} from '#/state/cache/profile-shadow' 36import {useFeedFeedbackContext} from '#/state/feed-feedback' 37import {useLanguagePrefs} from '#/state/preferences' 38import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' 39import {usePinnedPostMutation} from '#/state/queries/pinned-post' 40import { 41 usePostDeleteMutation, 42 useThreadMuteMutationQueue, 43} from '#/state/queries/post' 44import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate' 45import {getMaybeDetachedQuoteEmbed} from '#/state/queries/postgate/util' 46import { 47 useProfileBlockMutationQueue, 48 useProfileMuteMutationQueue, 49} from '#/state/queries/profile' 50import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate' 51import {useRequireAuth, useSession} from '#/state/session' 52import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' 53import * as Toast from '#/view/com/util/Toast' 54import {useDialogControl} from '#/components/Dialog' 55import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 56import { 57 PostInteractionSettingsDialog, 58 usePrefetchPostInteractionSettings, 59} from '#/components/dialogs/PostInteractionSettingsDialog' 60import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' 61import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' 62import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' 63import { 64 EmojiSad_Stroke2_Corner0_Rounded as EmojiSad, 65 EmojiSmile_Stroke2_Corner0_Rounded as EmojiSmile, 66} from '#/components/icons/Emoji' 67import {Eye_Stroke2_Corner0_Rounded as Eye} from '#/components/icons/Eye' 68import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' 69import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' 70import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute' 71import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' 72import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Person' 73import {Pin_Stroke2_Corner0_Rounded as PinIcon} from '#/components/icons/Pin' 74import {SettingsGear2_Stroke2_Corner0_Rounded as Gear} from '#/components/icons/SettingsGear2' 75import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker' 76import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 77import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' 78import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' 79import {Loader} from '#/components/Loader' 80import * as Menu from '#/components/Menu' 81import { 82 ReportDialog, 83 useReportDialogControl, 84} from '#/components/moderation/ReportDialog' 85import * as Prompt from '#/components/Prompt' 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 navigation = useNavigation<NavigationProp>() 122 const {mutedWordsDialogControl} = useGlobalDialogsControlContext() 123 const blockPromptControl = useDialogControl() 124 const reportDialogControl = useReportDialogControl() 125 const deletePromptControl = useDialogControl() 126 const hidePromptControl = useDialogControl() 127 const postInteractionSettingsDialogControl = useDialogControl() 128 const quotePostDetachConfirmControl = useDialogControl() 129 const hideReplyConfirmControl = useDialogControl() 130 const {mutateAsync: toggleReplyVisibility} = 131 useToggleReplyVisibilityMutation() 132 133 const postUri = post.uri 134 const postCid = post.cid 135 const postAuthor = useProfileShadow(post.author) 136 const quoteEmbed = useMemo(() => { 137 if (!currentAccount || !post.embed) return 138 return getMaybeDetachedQuoteEmbed({ 139 viewerDid: currentAccount.did, 140 post, 141 }) 142 }, [post, currentAccount]) 143 144 const rootUri = record.reply?.root?.uri || postUri 145 const isReply = Boolean(record.reply) 146 const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue( 147 post, 148 rootUri, 149 ) 150 const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) 151 const isAuthor = postAuthor.did === currentAccount?.did 152 const isRootPostAuthor = new AtUri(rootUri).host === currentAccount?.did 153 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({ 154 threadgateRecord, 155 }) 156 const isReplyHiddenByThreadgate = threadgateHiddenReplies.has(postUri) 157 const isPinned = post.viewer?.pinned 158 159 const {mutateAsync: toggleQuoteDetachment, isPending: isDetachPending} = 160 useToggleQuoteDetachmentMutation() 161 162 const [queueBlock] = useProfileBlockMutationQueue(postAuthor) 163 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(postAuthor) 164 165 const prefetchPostInteractionSettings = usePrefetchPostInteractionSettings({ 166 postUri: post.uri, 167 rootPostUri: rootUri, 168 }) 169 170 const href = useMemo(() => { 171 const urip = new AtUri(postUri) 172 return makeProfileLink(postAuthor, 'post', urip.rkey) 173 }, [postUri, postAuthor]) 174 175 const translatorUrl = getTranslatorLink( 176 record.text, 177 langPrefs.primaryLanguage, 178 ) 179 180 const onDeletePost = () => { 181 deletePostMutate({uri: postUri}).then( 182 () => { 183 Toast.show(_(msg({message: 'Post deleted', context: 'toast'}))) 184 185 const route = getCurrentRoute(navigation.getState()) 186 if (route.name === 'PostThread') { 187 const params = route.params as CommonNavigatorParams['PostThread'] 188 if ( 189 currentAccount && 190 isAuthor && 191 (params.name === currentAccount.handle || 192 params.name === currentAccount.did) 193 ) { 194 const currentHref = makeProfileLink(postAuthor, 'post', params.rkey) 195 if (currentHref === href && navigation.canGoBack()) { 196 navigation.goBack() 197 } 198 } 199 } 200 }, 201 e => { 202 logger.error('Failed to delete post', {message: e}) 203 Toast.show(_(msg`Failed to delete post, please try again`), 'xmark') 204 }, 205 ) 206 } 207 208 const onToggleThreadMute = () => { 209 try { 210 if (isThreadMuted) { 211 unmuteThread() 212 Toast.show(_(msg`You will now receive notifications for this thread`)) 213 } else { 214 muteThread() 215 Toast.show( 216 _(msg`You will no longer receive notifications for this thread`), 217 ) 218 } 219 } catch (e: any) { 220 if (e?.name !== 'AbortError') { 221 logger.error('Failed to toggle thread mute', {message: e}) 222 Toast.show( 223 _(msg`Failed to toggle thread mute, please try again`), 224 'xmark', 225 ) 226 } 227 } 228 } 229 230 const onCopyPostText = () => { 231 const str = richTextToString(richText, true) 232 233 Clipboard.setStringAsync(str) 234 Toast.show(_(msg`Copied to clipboard`), 'clipboard-check') 235 } 236 237 const onPressTranslate = async () => { 238 await openLink(translatorUrl, true) 239 240 if ( 241 bsky.dangerousIsType<AppBskyFeedPost.Record>( 242 post.record, 243 AppBskyFeedPost.isRecord, 244 ) 245 ) { 246 logger.metric( 247 'translate', 248 { 249 sourceLanguages: post.record.langs ?? [], 250 targetLanguage: langPrefs.primaryLanguage, 251 textLength: post.record.text.length, 252 }, 253 {statsig: false}, 254 ) 255 } 256 } 257 258 const onHidePost = () => { 259 hidePost({uri: postUri}) 260 } 261 262 const hideInPWI = !!postAuthor.labels?.find( 263 label => label.val === '!no-unauthenticated', 264 ) 265 266 const onPressShowMore = () => { 267 feedFeedback.sendInteraction({ 268 event: 'app.bsky.feed.defs#requestMore', 269 item: postUri, 270 feedContext: postFeedContext, 271 reqId: postReqId, 272 }) 273 Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) 274 } 275 276 const onPressShowLess = () => { 277 feedFeedback.sendInteraction({ 278 event: 'app.bsky.feed.defs#requestLess', 279 item: postUri, 280 feedContext: postFeedContext, 281 reqId: postReqId, 282 }) 283 if (onShowLess) { 284 onShowLess({ 285 item: postUri, 286 feedContext: postFeedContext, 287 }) 288 } else { 289 Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'}))) 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 <Menu.Item 494 testID="postDropdownReportMisclassificationBtn" 495 label={_(msg`Assign topic for algo`)} 496 onPress={onReportMisclassification}> 497 <Menu.ItemText>{_(msg`Assign topic for algo`)}</Menu.ItemText> 498 <Menu.ItemIcon icon={AtomIcon} position="right" /> 499 </Menu.Item> 500 )} 501 502 {hasSession && ( 503 <> 504 <Menu.Divider /> 505 <Menu.Group> 506 <Menu.Item 507 testID="postDropdownMuteThreadBtn" 508 label={ 509 isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`) 510 } 511 onPress={onToggleThreadMute}> 512 <Menu.ItemText> 513 {isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)} 514 </Menu.ItemText> 515 <Menu.ItemIcon 516 icon={isThreadMuted ? Unmute : Mute} 517 position="right" 518 /> 519 </Menu.Item> 520 521 <Menu.Item 522 testID="postDropdownMuteWordsBtn" 523 label={_(msg`Mute words & tags`)} 524 onPress={() => mutedWordsDialogControl.open()}> 525 <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText> 526 <Menu.ItemIcon icon={Filter} position="right" /> 527 </Menu.Item> 528 </Menu.Group> 529 </> 530 )} 531 532 {hasSession && 533 (canHideReplyForEveryone || canDetachQuote || canHidePostForMe) && ( 534 <> 535 <Menu.Divider /> 536 <Menu.Group> 537 {canHidePostForMe && ( 538 <Menu.Item 539 testID="postDropdownHideBtn" 540 label={ 541 isReply 542 ? _(msg`Hide reply for me`) 543 : _(msg`Hide post for me`) 544 } 545 onPress={() => hidePromptControl.open()}> 546 <Menu.ItemText> 547 {isReply 548 ? _(msg`Hide reply for me`) 549 : _(msg`Hide post for me`)} 550 </Menu.ItemText> 551 <Menu.ItemIcon icon={EyeSlash} position="right" /> 552 </Menu.Item> 553 )} 554 {canHideReplyForEveryone && ( 555 <Menu.Item 556 testID="postDropdownHideBtn" 557 label={ 558 isReplyHiddenByThreadgate 559 ? _(msg`Show reply for everyone`) 560 : _(msg`Hide reply for everyone`) 561 } 562 onPress={ 563 isReplyHiddenByThreadgate 564 ? onToggleReplyVisibility 565 : () => hideReplyConfirmControl.open() 566 }> 567 <Menu.ItemText> 568 {isReplyHiddenByThreadgate 569 ? _(msg`Show reply for everyone`) 570 : _(msg`Hide reply for everyone`)} 571 </Menu.ItemText> 572 <Menu.ItemIcon 573 icon={isReplyHiddenByThreadgate ? Eye : EyeSlash} 574 position="right" 575 /> 576 </Menu.Item> 577 )} 578 579 {canDetachQuote && ( 580 <Menu.Item 581 disabled={isDetachPending} 582 testID="postDropdownHideBtn" 583 label={ 584 quoteEmbed.isDetached 585 ? _(msg`Re-attach quote`) 586 : _(msg`Detach quote`) 587 } 588 onPress={ 589 quoteEmbed.isDetached 590 ? onToggleQuotePostAttachment 591 : () => quotePostDetachConfirmControl.open() 592 }> 593 <Menu.ItemText> 594 {quoteEmbed.isDetached 595 ? _(msg`Re-attach quote`) 596 : _(msg`Detach quote`)} 597 </Menu.ItemText> 598 <Menu.ItemIcon 599 icon={ 600 isDetachPending 601 ? Loader 602 : quoteEmbed.isDetached 603 ? Eye 604 : EyeSlash 605 } 606 position="right" 607 /> 608 </Menu.Item> 609 )} 610 </Menu.Group> 611 </> 612 )} 613 614 {hasSession && ( 615 <> 616 <Menu.Divider /> 617 <Menu.Group> 618 {!isAuthor && ( 619 <> 620 <Menu.Item 621 testID="postDropdownMuteBtn" 622 label={ 623 postAuthor.viewer?.muted 624 ? _(msg`Unmute account`) 625 : _(msg`Mute account`) 626 } 627 onPress={onMuteAuthor}> 628 <Menu.ItemText> 629 {postAuthor.viewer?.muted 630 ? _(msg`Unmute account`) 631 : _(msg`Mute account`)} 632 </Menu.ItemText> 633 <Menu.ItemIcon 634 icon={postAuthor.viewer?.muted ? UnmuteIcon : MuteIcon} 635 position="right" 636 /> 637 </Menu.Item> 638 639 {!postAuthor.viewer?.blocking && ( 640 <Menu.Item 641 testID="postDropdownBlockBtn" 642 label={_(msg`Block account`)} 643 onPress={() => blockPromptControl.open()}> 644 <Menu.ItemText>{_(msg`Block account`)}</Menu.ItemText> 645 <Menu.ItemIcon icon={PersonX} position="right" /> 646 </Menu.Item> 647 )} 648 649 <Menu.Item 650 testID="postDropdownReportBtn" 651 label={_(msg`Report post`)} 652 onPress={() => reportDialogControl.open()}> 653 <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText> 654 <Menu.ItemIcon icon={Warning} position="right" /> 655 </Menu.Item> 656 </> 657 )} 658 659 {isAuthor && ( 660 <> 661 <Menu.Item 662 testID="postDropdownEditPostInteractions" 663 label={_(msg`Edit interaction settings`)} 664 onPress={() => postInteractionSettingsDialogControl.open()} 665 {...(isAuthor 666 ? Platform.select({ 667 web: { 668 onHoverIn: prefetchPostInteractionSettings, 669 }, 670 native: { 671 onPressIn: prefetchPostInteractionSettings, 672 }, 673 }) 674 : {})}> 675 <Menu.ItemText> 676 {_(msg`Edit interaction settings`)} 677 </Menu.ItemText> 678 <Menu.ItemIcon icon={Gear} position="right" /> 679 </Menu.Item> 680 <Menu.Item 681 testID="postDropdownDeleteBtn" 682 label={_(msg`Delete post`)} 683 onPress={() => deletePromptControl.open()}> 684 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> 685 <Menu.ItemIcon icon={Trash} position="right" /> 686 </Menu.Item> 687 </> 688 )} 689 </Menu.Group> 690 </> 691 )} 692 </Menu.Outer> 693 694 <Prompt.Basic 695 control={deletePromptControl} 696 title={_(msg`Delete this post?`)} 697 description={_( 698 msg`If you remove this post, you won't be able to recover it.`, 699 )} 700 onConfirm={onDeletePost} 701 confirmButtonCta={_(msg`Delete`)} 702 confirmButtonColor="negative" 703 /> 704 705 <Prompt.Basic 706 control={hidePromptControl} 707 title={isReply ? _(msg`Hide this reply?`) : _(msg`Hide this post?`)} 708 description={_( 709 msg`This post will be hidden from feeds and threads. This cannot be undone.`, 710 )} 711 onConfirm={onHidePost} 712 confirmButtonCta={_(msg`Hide`)} 713 /> 714 715 <ReportDialog 716 control={reportDialogControl} 717 subject={{ 718 ...post, 719 $type: 'app.bsky.feed.defs#postView', 720 }} 721 /> 722 723 <PostInteractionSettingsDialog 724 control={postInteractionSettingsDialogControl} 725 postUri={post.uri} 726 rootPostUri={rootUri} 727 initialThreadgateView={post.threadgate} 728 /> 729 730 <Prompt.Basic 731 control={quotePostDetachConfirmControl} 732 title={_(msg`Detach quote post?`)} 733 description={_( 734 msg`This will remove your post from this quote post for all users, and replace it with a placeholder.`, 735 )} 736 onConfirm={onToggleQuotePostAttachment} 737 confirmButtonCta={_(msg`Yes, detach`)} 738 /> 739 740 <Prompt.Basic 741 control={hideReplyConfirmControl} 742 title={_(msg`Hide this reply?`)} 743 description={_( 744 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.`, 745 )} 746 onConfirm={onToggleReplyVisibility} 747 confirmButtonCta={_(msg`Yes, hide`)} 748 /> 749 750 <Prompt.Basic 751 control={blockPromptControl} 752 title={_(msg`Block Account?`)} 753 description={_( 754 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 755 )} 756 onConfirm={onBlockAuthor} 757 confirmButtonCta={_(msg`Block`)} 758 confirmButtonColor="negative" 759 /> 760 </> 761 ) 762} 763PostMenuItems = memo(PostMenuItems) 764export {PostMenuItems}