Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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}