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

Configure Feed

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

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