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