Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at theme-changes 725 lines 28 kB view raw
1import {memo, useCallback, useMemo} from 'react' 2import {type AppBskyActorDefs} from '@atproto/api' 3import {msg} from '@lingui/core/macro' 4import {useLingui} from '@lingui/react' 5import {Trans} from '@lingui/react/macro' 6import {useNavigation} from '@react-navigation/native' 7import {useQueryClient} from '@tanstack/react-query' 8 9import {HITSLOP_20} from '#/lib/constants' 10import {useOpenLink} from '#/lib/hooks/useOpenLink' 11import {makeProfileLink} from '#/lib/routes/links' 12import {type NavigationProp} from '#/lib/routes/types' 13import {shareText, shareUrl} from '#/lib/sharing' 14import {toShareUrl, toShareUrlBsky} from '#/lib/strings/url-helpers' 15import {type Shadow} from '#/state/cache/types' 16import {useModalControls} from '#/state/modals' 17import { 18 useDeerVerificationEnabled, 19 useDeerVerificationTrusted, 20 useSetDeerVerificationTrust, 21} from '#/state/preferences/deer-verification' 22import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 23import {useShowExternalShareButtons} from '#/state/preferences/external-share-buttons' 24import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' 25import { 26 RQKEY as profileQueryKey, 27 useProfileBlockMutationQueue, 28 useProfileFollowMutationQueue, 29 useProfileMuteMutationQueue, 30} from '#/state/queries/profile' 31import {useSession} from '#/state/session' 32import {EventStopper} from '#/view/com/util/EventStopper' 33import {atoms as a, useTheme} from '#/alf' 34import {Button, ButtonIcon} from '#/components/Button' 35import {useDialogControl} from '#/components/Dialog' 36import {StarterPackDialog} from '#/components/dialogs/StarterPackDialog' 37import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as ArrowOutOfBoxIcon} from '#/components/icons/ArrowOutOfBox' 38import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 39import {CircleCheck_Stroke2_Corner0_Rounded as CircleCheckIcon} from '#/components/icons/CircleCheck' 40import {CircleX_Stroke2_Corner0_Rounded as CircleXIcon} from '#/components/icons/CircleX' 41import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' 42import {DotGrid3x1_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid' 43import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' 44import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle' 45import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live' 46import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass' 47import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' 48import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' 49import { 50 PersonCheck_Stroke2_Corner0_Rounded as PersonCheck, 51 PersonX_Stroke2_Corner0_Rounded as PersonX, 52} from '#/components/icons/Person' 53import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' 54import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 55import {SquareArrowTopRight_Stroke2_Corner0_Rounded as ExternalIcon} from '#/components/icons/SquareArrowTopRight' 56import {StarterPack} from '#/components/icons/StarterPack' 57import * as Menu from '#/components/Menu' 58import { 59 ReportDialog, 60 useReportDialogControl, 61} from '#/components/moderation/ReportDialog' 62import * as Prompt from '#/components/Prompt' 63import * as Toast from '#/components/Toast' 64import {useFullVerificationState} from '#/components/verification' 65import {VerificationCreatePrompt} from '#/components/verification/VerificationCreatePrompt' 66import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt' 67import {useAnalytics} from '#/analytics' 68import {IS_WEB} from '#/env' 69import {useActorStatus, useLiveNowConfig} from '#/features/liveNow' 70import {EditLiveDialog} from '#/features/liveNow/components/EditLiveDialog' 71import {GoLiveDialog} from '#/features/liveNow/components/GoLiveDialog' 72import {GoLiveDisabledDialog} from '#/features/liveNow/components/GoLiveDisabledDialog' 73import {Dot} from '#/features/nuxs/components/Dot' 74import {Gradient} from '#/features/nuxs/components/Gradient' 75import {useDevMode} from '#/storage/hooks/dev-mode' 76 77let ProfileMenu = ({ 78 profile, 79}: { 80 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 81}): React.ReactNode => { 82 const t = useTheme() 83 const ax = useAnalytics() 84 const {_} = useLingui() 85 const {currentAccount, hasSession} = useSession() 86 const {openModal} = useModalControls() 87 const reportDialogControl = useReportDialogControl() 88 const queryClient = useQueryClient() 89 const navigation = useNavigation<NavigationProp>() 90 const isSelf = currentAccount?.did === profile.did 91 const isFollowedBy = profile.viewer?.followedBy 92 const isFollowing = profile.viewer?.following 93 const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy 94 const isFollowingBlockedAccount = isFollowing && isBlocked 95 const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked 96 const [devModeEnabled] = useDevMode() 97 const verification = useFullVerificationState({profile}) 98 const {canGoLive} = useLiveNowConfig() 99 const status = useActorStatus(profile) 100 const statusNudge = useNux(Nux.LiveNowBetaNudge) 101 const statusNudgeActive = 102 isSelf && 103 canGoLive && 104 statusNudge.status === 'ready' && 105 !statusNudge.nux?.completed 106 const {mutate: saveNux} = useSaveNux() 107 108 const deerVerificationEnabled = useDeerVerificationEnabled() 109 const deerVerificationTrusted = useDeerVerificationTrusted().has(profile.did) 110 const setDeerVerificationTrust = useSetDeerVerificationTrust() 111 112 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 113 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) 114 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( 115 profile, 116 'ProfileMenu', 117 ) 118 119 const blockPromptControl = Prompt.usePromptControl() 120 const loggedOutWarningPromptControl = Prompt.usePromptControl() 121 const goLiveDialogControl = useDialogControl() 122 const goLiveDisabledDialogControl = useDialogControl() 123 const addToStarterPacksDialogControl = useDialogControl() 124 125 const showExternalShareButtons = useShowExternalShareButtons() 126 const openLink = useOpenLink() 127 128 const showLoggedOutWarning = useMemo(() => { 129 return ( 130 profile.did !== currentAccount?.did && 131 !!profile.labels?.find(label => label.val === '!no-unauthenticated') 132 ) 133 }, [currentAccount, profile]) 134 135 const invalidateProfileQuery = useCallback(() => { 136 queryClient.invalidateQueries({ 137 queryKey: profileQueryKey(profile.did), 138 }) 139 }, [queryClient, profile.did]) 140 141 const onPressAddToStarterPacks = useCallback(() => { 142 ax.metric('profile:addToStarterPack', {}) 143 addToStarterPacksDialogControl.open() 144 }, [addToStarterPacksDialogControl]) 145 146 const onPressShare = useCallback(() => { 147 shareUrl(toShareUrl(makeProfileLink(profile))) 148 }, [profile]) 149 150 const onPressShareBsky = useCallback(() => { 151 shareUrl(toShareUrlBsky(makeProfileLink(profile))) 152 }, [profile]) 153 154 const onPressAddRemoveLists = useCallback(() => { 155 openModal({ 156 name: 'user-add-remove-lists', 157 subject: profile.did, 158 handle: profile.handle, 159 displayName: profile.displayName || profile.handle, 160 onAdd: invalidateProfileQuery, 161 onRemove: invalidateProfileQuery, 162 }) 163 }, [profile, openModal, invalidateProfileQuery]) 164 165 const onPressMuteAccount = useCallback(async () => { 166 if (profile.viewer?.muted) { 167 try { 168 await queueUnmute() 169 Toast.show(_(msg({message: 'Account unmuted', context: 'toast'}))) 170 } catch (e: any) { 171 if (e?.name !== 'AbortError') { 172 ax.logger.error('Failed to unmute account', {message: e}) 173 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 174 type: 'error', 175 }) 176 } 177 } 178 } else { 179 try { 180 await queueMute() 181 Toast.show(_(msg({message: 'Account muted', context: 'toast'}))) 182 } catch (e: any) { 183 if (e?.name !== 'AbortError') { 184 ax.logger.error('Failed to mute account', {message: e}) 185 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 186 type: 'error', 187 }) 188 } 189 } 190 } 191 }, [ax, profile.viewer?.muted, queueUnmute, _, queueMute]) 192 193 const blockAccount = useCallback(async () => { 194 if (profile.viewer?.blocking) { 195 try { 196 await queueUnblock() 197 Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) 198 } catch (e: any) { 199 if (e?.name !== 'AbortError') { 200 ax.logger.error('Failed to unblock account', {message: e}) 201 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 202 type: 'error', 203 }) 204 } 205 } 206 } else { 207 try { 208 await queueBlock() 209 Toast.show(_(msg({message: 'Account blocked', context: 'toast'}))) 210 } catch (e: any) { 211 if (e?.name !== 'AbortError') { 212 ax.logger.error('Failed to block account', {message: e}) 213 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 214 type: 'error', 215 }) 216 } 217 } 218 } 219 }, [ax, profile.viewer?.blocking, _, queueUnblock, queueBlock]) 220 221 const onPressFollowAccount = useCallback(async () => { 222 try { 223 await queueFollow() 224 Toast.show(_(msg({message: 'Account followed', context: 'toast'}))) 225 } catch (e: any) { 226 if (e?.name !== 'AbortError') { 227 ax.logger.error('Failed to follow account', {message: e}) 228 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 229 type: 'error', 230 }) 231 } 232 } 233 }, [_, ax, queueFollow]) 234 235 const onPressUnfollowAccount = useCallback(async () => { 236 try { 237 await queueUnfollow() 238 Toast.show(_(msg({message: 'Account unfollowed', context: 'toast'}))) 239 } catch (e: any) { 240 if (e?.name !== 'AbortError') { 241 ax.logger.error('Failed to unfollow account', {message: e}) 242 Toast.show(_(msg`There was an issue! ${e.toString()}`), { 243 type: 'error', 244 }) 245 } 246 } 247 }, [_, ax, queueUnfollow]) 248 249 const onPressReportAccount = useCallback(() => { 250 reportDialogControl.open() 251 }, [reportDialogControl]) 252 253 const onPressShareATUri = useCallback(() => { 254 shareText(`at://${profile.did}`) 255 }, [profile.did]) 256 257 const onPressShareDID = useCallback(() => { 258 shareText(profile.did) 259 }, [profile.did]) 260 261 const onPressSearch = useCallback(() => { 262 navigation.navigate('ProfileSearch', {name: profile.handle}) 263 }, [navigation, profile.handle]) 264 265 const onOpenProfileInPdsls = () => { 266 openLink( 267 `https://pdsls.dev/at://${profile.did}/app.bsky.actor.profile/self`, 268 true, 269 ) 270 } 271 272 const onOpenRepoInPdsls = () => { 273 openLink(`https://pdsls.dev/at://${profile.did}`, true) 274 } 275 276 const verificationCreatePromptControl = Prompt.usePromptControl() 277 const verificationRemovePromptControl = Prompt.usePromptControl() 278 const currentAccountVerifications = 279 profile.verification?.verifications?.filter(v => { 280 return v.issuer === currentAccount?.did 281 }) ?? [] 282 283 const enableSquareButtons = useEnableSquareButtons() 284 285 return ( 286 <EventStopper onKeyDown={false}> 287 <Menu.Root> 288 <Menu.Trigger label={_(msg`More options`)}> 289 {({props}) => { 290 return ( 291 <> 292 <Button 293 {...props} 294 testID="profileHeaderDropdownBtn" 295 label={_(msg`More options`)} 296 hitSlop={HITSLOP_20} 297 variant="solid" 298 color="secondary" 299 size="small" 300 shape={enableSquareButtons ? 'square' : 'round'}> 301 {statusNudgeActive && ( 302 <Gradient 303 style={[ 304 enableSquareButtons ? a.rounded_sm : a.rounded_full, 305 ]} 306 /> 307 )} 308 <ButtonIcon icon={Ellipsis} size="sm" /> 309 </Button> 310 311 {statusNudgeActive && <Dot top={1} right={1} />} 312 </> 313 ) 314 }} 315 </Menu.Trigger> 316 317 <Menu.Outer style={{minWidth: 170}}> 318 <Menu.Group> 319 <Menu.Item 320 testID="profileHeaderDropdownShareBtn" 321 label={ 322 IS_WEB ? _(msg`Copy link to profile`) : _(msg`Share via...`) 323 } 324 onPress={() => { 325 if (showLoggedOutWarning) { 326 loggedOutWarningPromptControl.open() 327 } else { 328 onPressShare() 329 } 330 }}> 331 <Menu.ItemText> 332 {IS_WEB ? ( 333 <Trans>Copy link to profile</Trans> 334 ) : ( 335 <Trans>Share via...</Trans> 336 )} 337 </Menu.ItemText> 338 <Menu.ItemIcon 339 icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon} 340 /> 341 </Menu.Item> 342 <Menu.Item 343 testID="profileHeaderDropdownShareBtn" 344 label={ 345 IS_WEB 346 ? _(msg`Copy via bsky.app`) 347 : _(msg`Share via bsky.app...`) 348 } 349 onPress={() => { 350 if (showLoggedOutWarning) { 351 loggedOutWarningPromptControl.open() 352 } else { 353 onPressShareBsky() 354 } 355 }}> 356 <Menu.ItemText> 357 {IS_WEB ? ( 358 <Trans>Copy via bsky.app</Trans> 359 ) : ( 360 <Trans>Share via bsky.app...</Trans> 361 )} 362 </Menu.ItemText> 363 <Menu.ItemIcon 364 icon={IS_WEB ? ChainLinkIcon : ArrowOutOfBoxIcon} 365 /> 366 </Menu.Item> 367 {showExternalShareButtons && ( 368 <> 369 <Menu.Item 370 testID="profileDropdownOpenProfileInPdsls" 371 label={_(msg`Open profile in PDSls`)} 372 onPress={onOpenProfileInPdsls}> 373 <Menu.ItemText> 374 <Trans>Open profile in PDSls</Trans> 375 </Menu.ItemText> 376 <Menu.ItemIcon icon={ExternalIcon} position="right" /> 377 </Menu.Item> 378 <Menu.Item 379 testID="profileDropdownOpenRepoInPdsls" 380 label={_(msg`Open repo in PDSls`)} 381 onPress={onOpenRepoInPdsls}> 382 <Menu.ItemText> 383 <Trans>Open repo in PDSls</Trans> 384 </Menu.ItemText> 385 <Menu.ItemIcon icon={ExternalIcon} position="right" /> 386 </Menu.Item> 387 </> 388 )} 389 <Menu.Item 390 testID="profileHeaderDropdownSearchBtn" 391 label={_(msg`Search posts`)} 392 onPress={onPressSearch}> 393 <Menu.ItemText> 394 <Trans>Search posts</Trans> 395 </Menu.ItemText> 396 <Menu.ItemIcon icon={SearchIcon} /> 397 </Menu.Item> 398 </Menu.Group> 399 400 {hasSession && ( 401 <> 402 <Menu.Divider /> 403 <Menu.Group> 404 {!isSelf && ( 405 <> 406 {(isLabelerAndNotBlocked || isFollowingBlockedAccount) && ( 407 <Menu.Item 408 testID="profileHeaderDropdownFollowBtn" 409 label={ 410 isFollowing 411 ? isFollowedBy 412 ? _(msg`Divorce mutual`) 413 : _(msg`Unfollow account`) 414 : _(msg`Follow account`) 415 } 416 onPress={ 417 isFollowing 418 ? onPressUnfollowAccount 419 : onPressFollowAccount 420 }> 421 <Menu.ItemText> 422 {isFollowing ? ( 423 isFollowedBy ? ( 424 <Trans>Divorce mutual</Trans> 425 ) : ( 426 <Trans>Unfollow account</Trans> 427 ) 428 ) : ( 429 <Trans>Follow account</Trans> 430 )} 431 </Menu.ItemText> 432 <Menu.ItemIcon icon={isFollowing ? UserMinus : Plus} /> 433 </Menu.Item> 434 )} 435 </> 436 )} 437 <Menu.Item 438 testID="profileHeaderDropdownStarterPackAddRemoveBtn" 439 label={_(msg`Add to starter packs`)} 440 onPress={onPressAddToStarterPacks}> 441 <Menu.ItemText> 442 <Trans>Add to starter packs</Trans> 443 </Menu.ItemText> 444 <Menu.ItemIcon icon={StarterPack} /> 445 </Menu.Item> 446 <Menu.Item 447 testID="profileHeaderDropdownListAddRemoveBtn" 448 label={_(msg`Add to lists`)} 449 onPress={onPressAddRemoveLists}> 450 <Menu.ItemText> 451 <Trans>Add to lists</Trans> 452 </Menu.ItemText> 453 <Menu.ItemIcon icon={List} /> 454 </Menu.Item> 455 {!isSelf && 456 deerVerificationEnabled && 457 (deerVerificationTrusted ? ( 458 <Menu.Item 459 testID="profileHeaderDropdownVerificationTrustRemoveButton" 460 label={_(msg`Remove trust`)} 461 onPress={() => 462 setDeerVerificationTrust.remove(profile.did) 463 }> 464 <Menu.ItemText> 465 <Trans>Remove trust</Trans> 466 </Menu.ItemText> 467 <Menu.ItemIcon icon={CircleXIcon} /> 468 </Menu.Item> 469 ) : ( 470 <Menu.Item 471 testID="profileHeaderDropdownVerificationTrustAddButton" 472 label={_(msg`Trust verifier`)} 473 onPress={() => setDeerVerificationTrust.add(profile.did)}> 474 <Menu.ItemText> 475 <Trans>Trust verifier</Trans> 476 </Menu.ItemText> 477 <Menu.ItemIcon icon={CircleCheckIcon} /> 478 </Menu.Item> 479 ))} 480 {isSelf && canGoLive && ( 481 <Menu.Item 482 testID="profileHeaderDropdownListAddRemoveBtn" 483 label={ 484 status.isDisabled 485 ? _(msg`Go live (disabled)`) 486 : status.isActive 487 ? _(msg`Edit live status`) 488 : _(msg`Go live`) 489 } 490 onPress={() => { 491 if (status.isDisabled) { 492 goLiveDisabledDialogControl.open() 493 } else { 494 goLiveDialogControl.open() 495 } 496 saveNux({ 497 id: Nux.LiveNowBetaNudge, 498 data: undefined, 499 completed: true, 500 }) 501 }}> 502 {statusNudgeActive && <Gradient />} 503 <Menu.ItemText> 504 {status.isDisabled ? ( 505 <Trans>Go live (disabled)</Trans> 506 ) : status.isActive ? ( 507 <Trans>Edit live status</Trans> 508 ) : ( 509 <Trans>Go live</Trans> 510 )} 511 </Menu.ItemText> 512 {statusNudgeActive && ( 513 <Menu.ItemText 514 style={[ 515 a.flex_0, 516 { 517 color: t.palette.primary_500, 518 right: IS_WEB ? -8 : -4, 519 }, 520 ]}> 521 <Trans>New</Trans> 522 </Menu.ItemText> 523 )} 524 <Menu.ItemIcon 525 icon={LiveIcon} 526 fill={ 527 statusNudgeActive 528 ? () => t.palette.primary_500 529 : undefined 530 } 531 /> 532 </Menu.Item> 533 )} 534 {verification.viewer.role === 'verifier' && 535 !verification.profile.isViewer && 536 (verification.viewer.hasIssuedVerification ? ( 537 <Menu.Item 538 testID="profileHeaderDropdownVerificationRemoveButton" 539 label={_(msg`Remove verification`)} 540 onPress={() => verificationRemovePromptControl.open()}> 541 <Menu.ItemText> 542 <Trans>Remove verification</Trans> 543 </Menu.ItemText> 544 <Menu.ItemIcon icon={CircleXIcon} /> 545 </Menu.Item> 546 ) : ( 547 <Menu.Item 548 testID="profileHeaderDropdownVerificationCreateButton" 549 label={_(msg`Verify account`)} 550 onPress={() => verificationCreatePromptControl.open()}> 551 <Menu.ItemText> 552 <Trans>Verify account</Trans> 553 </Menu.ItemText> 554 <Menu.ItemIcon icon={CircleCheckIcon} /> 555 </Menu.Item> 556 ))} 557 {!isSelf && ( 558 <> 559 {!profile.viewer?.blocking && 560 !profile.viewer?.mutedByList && ( 561 <Menu.Item 562 testID="profileHeaderDropdownMuteBtn" 563 label={ 564 profile.viewer?.muted 565 ? _(msg`Unmute account`) 566 : _(msg`Mute account`) 567 } 568 onPress={onPressMuteAccount}> 569 <Menu.ItemText> 570 {profile.viewer?.muted ? ( 571 <Trans>Unmute account</Trans> 572 ) : ( 573 <Trans>Mute account</Trans> 574 )} 575 </Menu.ItemText> 576 <Menu.ItemIcon 577 icon={profile.viewer?.muted ? Unmute : Mute} 578 /> 579 </Menu.Item> 580 )} 581 {!profile.viewer?.blockingByList && ( 582 <Menu.Item 583 testID="profileHeaderDropdownBlockBtn" 584 label={ 585 profile.viewer 586 ? _(msg`Unblock account`) 587 : _(msg`Block account`) 588 } 589 onPress={() => blockPromptControl.open()}> 590 <Menu.ItemText> 591 {profile.viewer?.blocking ? ( 592 <Trans>Unblock account</Trans> 593 ) : ( 594 <Trans>Block account</Trans> 595 )} 596 </Menu.ItemText> 597 <Menu.ItemIcon 598 icon={ 599 profile.viewer?.blocking ? PersonCheck : PersonX 600 } 601 /> 602 </Menu.Item> 603 )} 604 <Menu.Item 605 testID="profileHeaderDropdownReportBtn" 606 label={_(msg`Report account`)} 607 onPress={onPressReportAccount}> 608 <Menu.ItemText> 609 <Trans>Report account</Trans> 610 </Menu.ItemText> 611 <Menu.ItemIcon icon={Flag} /> 612 </Menu.Item> 613 </> 614 )} 615 </Menu.Group> 616 </> 617 )} 618 {devModeEnabled ? ( 619 <> 620 <Menu.Divider /> 621 <Menu.Group> 622 <Menu.Item 623 testID="profileHeaderDropdownShareATURIBtn" 624 label={_(msg`Copy at:// URI`)} 625 onPress={onPressShareATUri}> 626 <Menu.ItemText> 627 <Trans>Copy at:// URI</Trans> 628 </Menu.ItemText> 629 <Menu.ItemIcon icon={ClipboardIcon} /> 630 </Menu.Item> 631 <Menu.Item 632 testID="profileHeaderDropdownShareDIDBtn" 633 label={_(msg`Copy DID`)} 634 onPress={onPressShareDID}> 635 <Menu.ItemText> 636 <Trans>Copy DID</Trans> 637 </Menu.ItemText> 638 <Menu.ItemIcon icon={ClipboardIcon} /> 639 </Menu.Item> 640 </Menu.Group> 641 </> 642 ) : null} 643 </Menu.Outer> 644 </Menu.Root> 645 646 <StarterPackDialog 647 control={addToStarterPacksDialogControl} 648 targetDid={profile.did} 649 /> 650 651 <ReportDialog 652 control={reportDialogControl} 653 subject={{ 654 ...profile, 655 $type: 'app.bsky.actor.defs#profileViewDetailed', 656 }} 657 /> 658 659 <Prompt.Basic 660 control={blockPromptControl} 661 title={ 662 profile.viewer?.blocking 663 ? _(msg`Unblock Account?`) 664 : _(msg`Block Account?`) 665 } 666 description={ 667 profile.viewer?.blocking 668 ? _( 669 msg`The account will be able to interact with you after unblocking.`, 670 ) 671 : profile.associated?.labeler 672 ? _( 673 msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`, 674 ) 675 : _( 676 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 677 ) 678 } 679 onConfirm={blockAccount} 680 confirmButtonCta={ 681 profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) 682 } 683 confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'} 684 /> 685 686 <Prompt.Basic 687 control={loggedOutWarningPromptControl} 688 title={_(msg`Note about sharing`)} 689 description={_( 690 msg`This profile is only visible to logged-in users. It won't be visible to people who aren't signed in.`, 691 )} 692 onConfirm={onPressShare} 693 confirmButtonCta={_(msg`Share anyway`)} 694 /> 695 696 <VerificationCreatePrompt 697 control={verificationCreatePromptControl} 698 profile={profile} 699 /> 700 <VerificationRemovePrompt 701 control={verificationRemovePromptControl} 702 profile={profile} 703 verifications={currentAccountVerifications} 704 /> 705 706 {status.isDisabled ? ( 707 <GoLiveDisabledDialog 708 control={goLiveDisabledDialogControl} 709 status={status} 710 /> 711 ) : status.isActive ? ( 712 <EditLiveDialog 713 control={goLiveDialogControl} 714 status={status} 715 embed={status.embed} 716 /> 717 ) : ( 718 <GoLiveDialog control={goLiveDialogControl} profile={profile} /> 719 )} 720 </EventStopper> 721 ) 722} 723 724ProfileMenu = memo(ProfileMenu) 725export {ProfileMenu}