Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 856 lines 26 kB view raw
1import {type JSX, useCallback, useMemo, useState} from 'react' 2import {StyleSheet, View} from 'react-native' 3import {type AppBskyActorDefs} from '@atproto/api' 4import {msg, plural} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {Trans} from '@lingui/react/macro' 7import {useNavigation, useNavigationState} from '@react-navigation/native' 8 9import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 10import {useOpenComposer} from '#/lib/hooks/useOpenComposer' 11import {usePalette} from '#/lib/hooks/usePalette' 12import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 13import {getCurrentRoute, isTab} from '#/lib/routes/helpers' 14import {makeProfileLink} from '#/lib/routes/links' 15import { 16 type CommonNavigatorParams, 17 type NavigationProp, 18} from '#/lib/routes/types' 19import {sanitizeDisplayName} from '#/lib/strings/display-names' 20import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 21import {emitSoftReset} from '#/state/events' 22import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 23import {useFetchHandle} from '#/state/queries/handle' 24import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' 25import {useUnreadNotifications} from '#/state/queries/notifications/unread' 26import {useProfilesQuery} from '#/state/queries/profile' 27import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 28import {useLoggedOutViewControls} from '#/state/shell/logged-out' 29import {useCloseAllActiveElements} from '#/state/util' 30import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 31import {PressableWithHover} from '#/view/com/util/PressableWithHover' 32import {UserAvatar} from '#/view/com/util/UserAvatar' 33import {NavSignupCard} from '#/view/shell/NavSignupCard' 34import {atoms as a, tokens, useLayoutBreakpoints, useTheme, web} from '#/alf' 35import {Button, ButtonIcon, ButtonText} from '#/components/Button' 36import {type DialogControlProps} from '#/components/Dialog' 37import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' 38import { 39 Bell_Filled_Corner0_Rounded as BellFilled, 40 Bell_Stroke2_Corner0_Rounded as Bell, 41} from '#/components/icons/Bell' 42import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark' 43import { 44 BulletList_Filled_Corner0_Rounded as ListFilled, 45 BulletList_Stroke2_Corner0_Rounded as List, 46} from '#/components/icons/BulletList' 47import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' 48import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' 49import { 50 Hashtag_Filled_Corner0_Rounded as HashtagFilled, 51 Hashtag_Stroke2_Corner0_Rounded as Hashtag, 52} from '#/components/icons/Hashtag' 53import { 54 HomeOpen_Filled_Corner0_Rounded as HomeFilled, 55 HomeOpen_Stoke2_Corner0_Rounded as Home, 56} from '#/components/icons/HomeOpen' 57import { 58 MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled, 59 MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlass, 60} from '#/components/icons/MagnifyingGlass' 61import { 62 Message_Stroke2_Corner0_Rounded as Message, 63 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, 64} from '#/components/icons/Message' 65import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 66import { 67 SettingsGear2_Filled_Corner0_Rounded as SettingsFilled, 68 SettingsGear2_Stroke2_Corner0_Rounded as Settings, 69} from '#/components/icons/SettingsGear2' 70import { 71 UserCircle_Filled_Corner0_Rounded as UserCircleFilled, 72 UserCircle_Stroke2_Corner0_Rounded as UserCircle, 73} from '#/components/icons/UserCircle' 74import {CENTER_COLUMN_OFFSET} from '#/components/Layout' 75import * as Menu from '#/components/Menu' 76import * as Prompt from '#/components/Prompt' 77import {Text} from '#/components/Typography' 78import {useActorStatus} from '#/features/liveNow' 79import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' 80import {router} from '../../../routes' 81 82const NAV_ICON_WIDTH = 28 83 84function ProfileCard() { 85 const {currentAccount, accounts} = useSession() 86 const {logoutEveryAccount} = useSessionApi() 87 const {isLoading, data} = useProfilesQuery({ 88 handles: accounts.map(acc => acc.did), 89 }) 90 const profiles = data?.profiles 91 const signOutPromptControl = Prompt.usePromptControl() 92 const {leftNavMinimal} = useLayoutBreakpoints() 93 const {_} = useLingui() 94 const t = useTheme() 95 96 const size = 48 97 98 const profile = profiles?.find(p => p.did === currentAccount!.did) 99 const otherAccounts = accounts 100 .filter(acc => acc.did !== currentAccount!.did) 101 .map(account => ({ 102 account, 103 profile: profiles?.find(p => p.did === account.did), 104 })) 105 106 const {isActive: live} = useActorStatus(profile) 107 108 const enableSquareButtons = useEnableSquareButtons() 109 110 return ( 111 <View style={[a.my_md, !leftNavMinimal && [a.w_full, a.align_start]]}> 112 {!isLoading && profile ? ( 113 <Menu.Root> 114 <Menu.Trigger label={_(msg`Switch accounts`)}> 115 {({props, state, control}) => { 116 const active = state.hovered || state.focused || control.isOpen 117 return ( 118 <Button 119 label={props.accessibilityLabel} 120 {...props} 121 style={[ 122 a.w_full, 123 a.transition_color, 124 active ? t.atoms.bg_contrast_25 : a.transition_delay_50ms, 125 enableSquareButtons ? a.rounded_sm : a.rounded_full, 126 a.justify_between, 127 a.align_center, 128 a.flex_row, 129 {gap: 6}, 130 !leftNavMinimal && [a.pl_lg, a.pr_md], 131 ]}> 132 <View 133 style={[ 134 !PlatformInfo.getIsReducedMotionEnabled() && [ 135 a.transition_transform, 136 {transitionDuration: '250ms'}, 137 !active && a.transition_delay_50ms, 138 ], 139 a.relative, 140 a.z_10, 141 active && { 142 transform: [ 143 {scale: !leftNavMinimal ? 2 / 3 : 0.8}, 144 {translateX: !leftNavMinimal ? -22 : 0}, 145 ], 146 }, 147 ]}> 148 <UserAvatar 149 avatar={profile.avatar} 150 size={size} 151 type={profile?.associated?.labeler ? 'labeler' : 'user'} 152 live={live} 153 /> 154 </View> 155 {!leftNavMinimal && ( 156 <> 157 <View 158 style={[ 159 a.flex_1, 160 a.transition_opacity, 161 !active && a.transition_delay_50ms, 162 { 163 marginLeft: tokens.space.xl * -1, 164 opacity: active ? 1 : 0, 165 }, 166 ]}> 167 <Text 168 style={[a.font_bold, a.text_sm, a.leading_snug]} 169 numberOfLines={1}> 170 {sanitizeDisplayName( 171 profile.displayName || profile.handle, 172 )} 173 </Text> 174 <Text 175 style={[ 176 a.text_xs, 177 a.leading_snug, 178 t.atoms.text_contrast_medium, 179 ]} 180 numberOfLines={1}> 181 {sanitizeHandle(profile.handle, '@')} 182 </Text> 183 </View> 184 <EllipsisIcon 185 aria-hidden={true} 186 style={[ 187 t.atoms.text_contrast_medium, 188 a.transition_opacity, 189 {opacity: active ? 1 : 0}, 190 ]} 191 size="sm" 192 /> 193 </> 194 )} 195 </Button> 196 ) 197 }} 198 </Menu.Trigger> 199 <SwitchMenuItems 200 accounts={otherAccounts} 201 signOutPromptControl={signOutPromptControl} 202 /> 203 </Menu.Root> 204 ) : ( 205 <LoadingPlaceholder 206 width={size} 207 height={size} 208 style={[{borderRadius: size}, !leftNavMinimal && a.ml_lg]} 209 /> 210 )} 211 <Prompt.Basic 212 control={signOutPromptControl} 213 title={_(msg`Sign out?`)} 214 description={_(msg`You will be signed out of all your accounts.`)} 215 onConfirm={() => logoutEveryAccount('Settings')} 216 confirmButtonCta={_(msg`Sign out`)} 217 cancelButtonCta={_(msg`Cancel`)} 218 confirmButtonColor="negative" 219 /> 220 </View> 221 ) 222} 223 224function SwitchMenuItems({ 225 accounts, 226 signOutPromptControl, 227}: { 228 accounts: 229 | { 230 account: SessionAccount 231 profile?: AppBskyActorDefs.ProfileViewDetailed 232 }[] 233 | undefined 234 signOutPromptControl: DialogControlProps 235}) { 236 const {_} = useLingui() 237 const {setShowLoggedOut} = useLoggedOutViewControls() 238 const closeEverything = useCloseAllActiveElements() 239 240 const onAddAnotherAccount = () => { 241 setShowLoggedOut(true) 242 closeEverything() 243 } 244 245 return ( 246 <Menu.Outer> 247 {accounts && accounts.length > 0 && ( 248 <> 249 <Menu.Group> 250 <Menu.LabelText> 251 <Trans>Switch account</Trans> 252 </Menu.LabelText> 253 {accounts.map(other => ( 254 <SwitchMenuItem 255 key={other.account.did} 256 account={other.account} 257 profile={other.profile} 258 /> 259 ))} 260 </Menu.Group> 261 <Menu.Divider /> 262 </> 263 )} 264 <SwitcherMenuProfileLink /> 265 <Menu.Item 266 label={_(msg`Add another account`)} 267 onPress={onAddAnotherAccount}> 268 <Menu.ItemIcon icon={PlusIcon} /> 269 <Menu.ItemText> 270 <Trans>Add another account</Trans> 271 </Menu.ItemText> 272 </Menu.Item> 273 <Menu.Item label={_(msg`Sign out`)} onPress={signOutPromptControl.open}> 274 <Menu.ItemIcon icon={LeaveIcon} /> 275 <Menu.ItemText> 276 <Trans>Sign out</Trans> 277 </Menu.ItemText> 278 </Menu.Item> 279 </Menu.Outer> 280 ) 281} 282 283function SwitcherMenuProfileLink() { 284 const {_} = useLingui() 285 const {currentAccount} = useSession() 286 const navigation = useNavigation() 287 const context = Menu.useMenuContext() 288 const profileLink = currentAccount ? makeProfileLink(currentAccount) : '/' 289 const [pathName] = useMemo(() => router.matchPath(profileLink), [profileLink]) 290 const currentRouteInfo = useNavigationState(state => { 291 if (!state) { 292 return {name: 'Home'} 293 } 294 return getCurrentRoute(state) 295 }) 296 let isCurrent = 297 currentRouteInfo.name === 'Profile' 298 ? isTab(currentRouteInfo.name, pathName) && 299 (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 300 currentAccount?.handle 301 : isTab(currentRouteInfo.name, pathName) 302 const onProfilePress = useCallback( 303 (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { 304 if (e.ctrlKey || e.metaKey || e.altKey) { 305 return 306 } 307 e.preventDefault() 308 context.control.close() 309 if (isCurrent) { 310 emitSoftReset() 311 } else { 312 const [screen, params] = router.matchPath(profileLink) 313 // @ts-expect-error TODO: type matchPath well enough that it can be plugged into navigation.navigate directly 314 navigation.navigate(screen, params, {pop: true}) 315 } 316 }, 317 [navigation, profileLink, isCurrent, context], 318 ) 319 return ( 320 <Menu.Item 321 label={_(msg`Go to profile`)} 322 // @ts-expect-error The function signature differs on web -inb 323 onPress={onProfilePress} 324 href={profileLink}> 325 <Menu.ItemIcon icon={UserCircle} /> 326 <Menu.ItemText> 327 <Trans>Go to profile</Trans> 328 </Menu.ItemText> 329 </Menu.Item> 330 ) 331} 332 333function SwitchMenuItem({ 334 account, 335 profile, 336}: { 337 account: SessionAccount 338 profile: AppBskyActorDefs.ProfileViewDetailed | undefined 339}) { 340 const {_} = useLingui() 341 const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() 342 const {isActive: live} = useActorStatus(profile) 343 344 return ( 345 <Menu.Item 346 disabled={!!pendingDid} 347 style={[a.gap_sm, {minWidth: 150}]} 348 key={account.did} 349 label={_( 350 msg`Switch to ${sanitizeHandle( 351 profile?.handle ?? account.handle, 352 '@', 353 )}`, 354 )} 355 onPress={() => onPressSwitchAccount(account, 'SwitchAccount')}> 356 <View> 357 <UserAvatar 358 avatar={profile?.avatar} 359 size={20} 360 type={profile?.associated?.labeler ? 'labeler' : 'user'} 361 live={live} 362 hideLiveBadge 363 /> 364 </View> 365 <Menu.ItemText> 366 {sanitizeHandle(profile?.handle ?? account.handle, '@')} 367 </Menu.ItemText> 368 </Menu.Item> 369 ) 370} 371 372interface NavItemProps { 373 count?: string 374 hasNew?: boolean 375 href: string 376 icon: JSX.Element 377 iconFilled: JSX.Element 378 label: string 379} 380function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { 381 const t = useTheme() 382 const {_} = useLingui() 383 const {currentAccount} = useSession() 384 const {leftNavMinimal} = useLayoutBreakpoints() 385 const [pathName] = useMemo(() => router.matchPath(href), [href]) 386 387 const enableSquareButtons = useEnableSquareButtons() 388 389 const currentRouteInfo = useNavigationState(state => { 390 if (!state) { 391 return {name: 'Home'} 392 } 393 return getCurrentRoute(state) 394 }) 395 let isCurrent = 396 currentRouteInfo.name === 'Profile' 397 ? isTab(currentRouteInfo.name, pathName) && 398 ((currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 399 currentAccount?.handle || 400 (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === 401 currentAccount?.did) 402 : isTab(currentRouteInfo.name, pathName) 403 const navigation = useNavigation<NavigationProp>() 404 const onPressWrapped = useCallback( 405 (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { 406 if (e.ctrlKey || e.metaKey || e.altKey) { 407 return 408 } 409 e.preventDefault() 410 if (isCurrent) { 411 emitSoftReset() 412 } else { 413 const [screen, params] = router.matchPath(href) 414 // @ts-expect-error TODO: type matchPath well enough that it can be plugged into navigation.navigate directly 415 navigation.navigate(screen, params, {pop: true}) 416 } 417 }, 418 [navigation, href, isCurrent], 419 ) 420 421 return ( 422 <PressableWithHover 423 style={[ 424 a.flex_row, 425 a.align_center, 426 a.p_md, 427 a.rounded_sm, 428 a.gap_sm, 429 a.outline_inset_1, 430 a.transition_color, 431 ]} 432 hoverStyle={t.atoms.bg_contrast_25} 433 // @ts-expect-error the function signature differs on web -prf 434 onPress={onPressWrapped} 435 href={href} 436 dataSet={{noUnderline: 1}} 437 role="link" 438 accessibilityLabel={label} 439 accessibilityHint=""> 440 <View 441 style={[ 442 a.align_center, 443 a.justify_center, 444 { 445 width: 24, 446 height: 24, 447 }, 448 leftNavMinimal && { 449 width: 40, 450 height: 40, 451 }, 452 ]}> 453 {isCurrent ? iconFilled : icon} 454 {typeof count === 'string' && count ? ( 455 <View 456 style={[ 457 a.absolute, 458 a.inset_0, 459 {right: -20}, // more breathing room 460 ]}> 461 <Text 462 accessibilityLabel={_( 463 msg`${plural(count, { 464 one: '# unread item', 465 other: '# unread items', 466 })}`, 467 )} 468 accessibilityHint="" 469 accessible={true} 470 numberOfLines={1} 471 style={[ 472 a.absolute, 473 a.text_xs, 474 a.font_semi_bold, 475 enableSquareButtons ? a.rounded_sm : a.rounded_full, 476 a.text_center, 477 a.leading_tight, 478 a.z_20, 479 { 480 top: '-10%', 481 left: count.length === 1 ? 12 : 8, 482 backgroundColor: t.palette.primary_500, 483 color: t.palette.white, 484 lineHeight: a.text_sm.fontSize, 485 paddingHorizontal: 4, 486 paddingVertical: 1, 487 minWidth: 16, 488 }, 489 leftNavMinimal && [ 490 { 491 top: '10%', 492 left: count.length === 1 ? 20 : 16, 493 }, 494 ], 495 ]}> 496 {count} 497 </Text> 498 </View> 499 ) : hasNew ? ( 500 <View 501 style={[ 502 a.absolute, 503 enableSquareButtons ? a.rounded_sm : a.rounded_full, 504 a.z_20, 505 { 506 backgroundColor: t.palette.primary_500, 507 width: 8, 508 height: 8, 509 right: -2, 510 top: -4, 511 }, 512 leftNavMinimal && { 513 right: 4, 514 top: 2, 515 }, 516 ]} 517 /> 518 ) : null} 519 </View> 520 {!leftNavMinimal && ( 521 <Text style={[a.text_xl, isCurrent ? a.font_bold : a.font_normal]}> 522 {label} 523 </Text> 524 )} 525 </PressableWithHover> 526 ) 527} 528 529function ComposeBtn() { 530 const {currentAccount} = useSession() 531 const {getState} = useNavigation() 532 const {openComposer} = useOpenComposer() 533 const {_} = useLingui() 534 const {leftNavMinimal} = useLayoutBreakpoints() 535 const [isFetchingHandle, setIsFetchingHandle] = useState(false) 536 const fetchHandle = useFetchHandle() 537 538 const enableSquareButtons = useEnableSquareButtons() 539 540 const getProfileHandle = async () => { 541 const routes = getState()?.routes 542 const currentRoute = routes?.[routes?.length - 1] 543 544 if (currentRoute?.name === 'Profile') { 545 let handle: string | undefined = ( 546 currentRoute.params as CommonNavigatorParams['Profile'] 547 ).name 548 549 if (handle.startsWith('did:')) { 550 try { 551 setIsFetchingHandle(true) 552 handle = await fetchHandle(handle) 553 } catch (e) { 554 handle = undefined 555 } finally { 556 setIsFetchingHandle(false) 557 } 558 } 559 560 if ( 561 !handle || 562 handle === currentAccount?.handle || 563 isInvalidHandle(handle) 564 ) 565 return undefined 566 567 return handle 568 } 569 570 return undefined 571 } 572 573 const onPressCompose = async () => 574 openComposer({mention: await getProfileHandle(), logContext: 'Fab'}) 575 576 if (leftNavMinimal) { 577 return null 578 } 579 580 return ( 581 <View style={[a.flex_row, a.pl_md, a.pt_xl]}> 582 <Button 583 disabled={isFetchingHandle} 584 label={_(msg`Compose new post`)} 585 onPress={onPressCompose} 586 size="large" 587 variant="solid" 588 color="primary" 589 style={enableSquareButtons ? [a.rounded_sm] : [a.rounded_full]}> 590 <ButtonIcon icon={EditBig} position="left" /> 591 <ButtonText> 592 <Trans context="action">New Post</Trans> 593 </ButtonText> 594 </Button> 595 </View> 596 ) 597} 598 599function ChatNavItem() { 600 const pal = usePalette('default') 601 const {_} = useLingui() 602 const numUnreadMessages = useUnreadMessageCount() 603 604 return ( 605 <NavItem 606 href="/messages" 607 count={numUnreadMessages.numUnread} 608 hasNew={numUnreadMessages.hasNew} 609 icon={ 610 <Message style={pal.text} aria-hidden={true} width={NAV_ICON_WIDTH} /> 611 } 612 iconFilled={ 613 <MessageFilled 614 style={pal.text} 615 aria-hidden={true} 616 width={NAV_ICON_WIDTH} 617 /> 618 } 619 label={_(msg`Chat`)} 620 /> 621 ) 622} 623 624export function DesktopLeftNav() { 625 const {hasSession, currentAccount} = useSession() 626 const pal = usePalette('default') 627 const {_} = useLingui() 628 const {isDesktop} = useWebMediaQueries() 629 const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints() 630 const numUnreadNotifications = useUnreadNotifications() 631 632 if (!hasSession && !isDesktop) { 633 return null 634 } 635 636 return ( 637 <View 638 role="navigation" 639 style={[ 640 a.px_xl, 641 styles.leftNav, 642 !hasSession && !leftNavMinimal && styles.leftNavWide, 643 leftNavMinimal && styles.leftNavMinimal, 644 { 645 transform: [ 646 { 647 translateX: 648 -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0), 649 }, 650 {translateX: '-100%'}, 651 ...a.scrollbar_offset.transform, 652 ], 653 }, 654 ]}> 655 {hasSession ? ( 656 <ProfileCard /> 657 ) : !leftNavMinimal ? ( 658 <View style={[a.pt_xl]}> 659 <NavSignupCard /> 660 </View> 661 ) : null} 662 663 {hasSession && ( 664 <> 665 <NavItem 666 href="/" 667 icon={ 668 <Home 669 aria-hidden={true} 670 width={NAV_ICON_WIDTH} 671 style={pal.text} 672 /> 673 } 674 iconFilled={ 675 <HomeFilled 676 aria-hidden={true} 677 width={NAV_ICON_WIDTH} 678 style={pal.text} 679 /> 680 } 681 label={_(msg`Home`)} 682 /> 683 <NavItem 684 href="/search" 685 icon={ 686 <MagnifyingGlass 687 style={pal.text} 688 aria-hidden={true} 689 width={NAV_ICON_WIDTH} 690 /> 691 } 692 iconFilled={ 693 <MagnifyingGlassFilled 694 style={pal.text} 695 aria-hidden={true} 696 width={NAV_ICON_WIDTH} 697 /> 698 } 699 label={_(msg`Explore`)} 700 /> 701 <NavItem 702 href="/notifications" 703 count={numUnreadNotifications} 704 icon={ 705 <Bell 706 aria-hidden={true} 707 width={NAV_ICON_WIDTH} 708 style={pal.text} 709 /> 710 } 711 iconFilled={ 712 <BellFilled 713 aria-hidden={true} 714 width={NAV_ICON_WIDTH} 715 style={pal.text} 716 /> 717 } 718 label={_(msg`Notifications`)} 719 /> 720 <ChatNavItem /> 721 <NavItem 722 href="/feeds" 723 icon={ 724 <Hashtag 725 style={pal.text} 726 aria-hidden={true} 727 width={NAV_ICON_WIDTH} 728 /> 729 } 730 iconFilled={ 731 <HashtagFilled 732 style={pal.text} 733 aria-hidden={true} 734 width={NAV_ICON_WIDTH} 735 /> 736 } 737 label={_(msg`Feeds`)} 738 /> 739 <NavItem 740 href="/lists" 741 icon={ 742 <List 743 style={pal.text} 744 aria-hidden={true} 745 width={NAV_ICON_WIDTH} 746 /> 747 } 748 iconFilled={ 749 <ListFilled 750 style={pal.text} 751 aria-hidden={true} 752 width={NAV_ICON_WIDTH} 753 /> 754 } 755 label={_(msg`Lists`)} 756 /> 757 <NavItem 758 href="/saved" 759 icon={ 760 <Bookmark 761 style={pal.text} 762 aria-hidden={true} 763 width={NAV_ICON_WIDTH} 764 /> 765 } 766 iconFilled={ 767 <BookmarkFilled 768 style={pal.text} 769 aria-hidden={true} 770 width={NAV_ICON_WIDTH} 771 /> 772 } 773 label={_( 774 msg({ 775 message: 'Saved', 776 context: 'link to bookmarks screen', 777 }), 778 )} 779 /> 780 <NavItem 781 href={currentAccount ? makeProfileLink(currentAccount) : '/'} 782 icon={ 783 <UserCircle 784 aria-hidden={true} 785 width={NAV_ICON_WIDTH} 786 style={pal.text} 787 /> 788 } 789 iconFilled={ 790 <UserCircleFilled 791 aria-hidden={true} 792 width={NAV_ICON_WIDTH} 793 style={pal.text} 794 /> 795 } 796 label={_(msg`Profile`)} 797 /> 798 <NavItem 799 href="/settings" 800 icon={ 801 <Settings 802 aria-hidden={true} 803 width={NAV_ICON_WIDTH} 804 style={pal.text} 805 /> 806 } 807 iconFilled={ 808 <SettingsFilled 809 aria-hidden={true} 810 width={NAV_ICON_WIDTH} 811 style={pal.text} 812 /> 813 } 814 label={_(msg`Settings`)} 815 /> 816 817 <ComposeBtn /> 818 </> 819 )} 820 </View> 821 ) 822} 823 824const styles = StyleSheet.create({ 825 leftNav: { 826 ...a.fixed, 827 top: 0, 828 paddingTop: 10, 829 paddingBottom: 10, 830 left: '50%', 831 width: 240, 832 // @ts-expect-error web only 833 maxHeight: '100vh', 834 overflowY: 'auto', 835 }, 836 leftNavWide: { 837 width: 245, 838 }, 839 leftNavMinimal: { 840 paddingTop: 0, 841 paddingBottom: 0, 842 paddingLeft: 0, 843 paddingRight: 0, 844 height: '100%', 845 width: 86, 846 alignItems: 'center', 847 ...web({overflowX: 'hidden'}), 848 }, 849 backBtn: { 850 position: 'absolute', 851 top: 12, 852 right: 12, 853 width: 30, 854 height: 30, 855 }, 856})