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