Bluesky app fork with some witchin' additions 馃挮
at readme-update 851 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 {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 {DotGrid_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 {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 631 if (!hasSession && !isDesktop) { 632 return null 633 } 634 635 return ( 636 <View 637 role="navigation" 638 style={[ 639 a.px_xl, 640 styles.leftNav, 641 leftNavMinimal && styles.leftNavMinimal, 642 { 643 transform: [ 644 { 645 translateX: 646 -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0), 647 }, 648 {translateX: '-100%'}, 649 ...a.scrollbar_offset.transform, 650 ], 651 }, 652 ]}> 653 {hasSession ? ( 654 <ProfileCard /> 655 ) : !leftNavMinimal ? ( 656 <View style={[a.pt_xl]}> 657 <NavSignupCard /> 658 </View> 659 ) : null} 660 661 {hasSession && ( 662 <> 663 <NavItem 664 href="/" 665 icon={ 666 <Home 667 aria-hidden={true} 668 width={NAV_ICON_WIDTH} 669 style={pal.text} 670 /> 671 } 672 iconFilled={ 673 <HomeFilled 674 aria-hidden={true} 675 width={NAV_ICON_WIDTH} 676 style={pal.text} 677 /> 678 } 679 label={_(msg`Home`)} 680 /> 681 <NavItem 682 href="/search" 683 icon={ 684 <MagnifyingGlass 685 style={pal.text} 686 aria-hidden={true} 687 width={NAV_ICON_WIDTH} 688 /> 689 } 690 iconFilled={ 691 <MagnifyingGlassFilled 692 style={pal.text} 693 aria-hidden={true} 694 width={NAV_ICON_WIDTH} 695 /> 696 } 697 label={_(msg`Explore`)} 698 /> 699 <NavItem 700 href="/notifications" 701 count={numUnreadNotifications} 702 icon={ 703 <Bell 704 aria-hidden={true} 705 width={NAV_ICON_WIDTH} 706 style={pal.text} 707 /> 708 } 709 iconFilled={ 710 <BellFilled 711 aria-hidden={true} 712 width={NAV_ICON_WIDTH} 713 style={pal.text} 714 /> 715 } 716 label={_(msg`Notifications`)} 717 /> 718 <ChatNavItem /> 719 <NavItem 720 href="/feeds" 721 icon={ 722 <Hashtag 723 style={pal.text} 724 aria-hidden={true} 725 width={NAV_ICON_WIDTH} 726 /> 727 } 728 iconFilled={ 729 <HashtagFilled 730 style={pal.text} 731 aria-hidden={true} 732 width={NAV_ICON_WIDTH} 733 /> 734 } 735 label={_(msg`Feeds`)} 736 /> 737 <NavItem 738 href="/lists" 739 icon={ 740 <List 741 style={pal.text} 742 aria-hidden={true} 743 width={NAV_ICON_WIDTH} 744 /> 745 } 746 iconFilled={ 747 <ListFilled 748 style={pal.text} 749 aria-hidden={true} 750 width={NAV_ICON_WIDTH} 751 /> 752 } 753 label={_(msg`Lists`)} 754 /> 755 <NavItem 756 href="/saved" 757 icon={ 758 <Bookmark 759 style={pal.text} 760 aria-hidden={true} 761 width={NAV_ICON_WIDTH} 762 /> 763 } 764 iconFilled={ 765 <BookmarkFilled 766 style={pal.text} 767 aria-hidden={true} 768 width={NAV_ICON_WIDTH} 769 /> 770 } 771 label={_( 772 msg({ 773 message: 'Saved', 774 context: 'link to bookmarks screen', 775 }), 776 )} 777 /> 778 <NavItem 779 href={currentAccount ? makeProfileLink(currentAccount) : '/'} 780 icon={ 781 <UserCircle 782 aria-hidden={true} 783 width={NAV_ICON_WIDTH} 784 style={pal.text} 785 /> 786 } 787 iconFilled={ 788 <UserCircleFilled 789 aria-hidden={true} 790 width={NAV_ICON_WIDTH} 791 style={pal.text} 792 /> 793 } 794 label={_(msg`Profile`)} 795 /> 796 <NavItem 797 href="/settings" 798 icon={ 799 <Settings 800 aria-hidden={true} 801 width={NAV_ICON_WIDTH} 802 style={pal.text} 803 /> 804 } 805 iconFilled={ 806 <SettingsFilled 807 aria-hidden={true} 808 width={NAV_ICON_WIDTH} 809 style={pal.text} 810 /> 811 } 812 label={_(msg`Settings`)} 813 /> 814 815 <ComposeBtn /> 816 </> 817 )} 818 </View> 819 ) 820} 821 822const styles = StyleSheet.create({ 823 leftNav: { 824 ...a.fixed, 825 top: 0, 826 paddingTop: 10, 827 paddingBottom: 10, 828 left: '50%', 829 width: 240, 830 // @ts-expect-error web only 831 maxHeight: '100vh', 832 overflowY: 'auto', 833 }, 834 leftNavMinimal: { 835 paddingTop: 0, 836 paddingBottom: 0, 837 paddingLeft: 0, 838 paddingRight: 0, 839 height: '100%', 840 width: 86, 841 alignItems: 'center', 842 ...web({overflowX: 'hidden'}), 843 }, 844 backBtn: { 845 position: 'absolute', 846 top: 12, 847 right: 12, 848 width: 30, 849 height: 30, 850 }, 851})