An ATproto social media client -- with an independent Appview.
fork

Configure Feed

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

at main 853 lines 26 kB view raw
1import {type JSX, useCallback, useMemo, useState} from 'react' 2import {StyleSheet, type TextStyle, 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 {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 12import {getCurrentRoute, isTab} from '#/lib/routes/helpers' 13import {makeProfileLink} from '#/lib/routes/links' 14import { 15 type CommonNavigatorParams, 16 type NavigationProp, 17} from '#/lib/routes/types' 18import {useGate} from '#/lib/statsig/statsig' 19import {sanitizeDisplayName} from '#/lib/strings/display-names' 20import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 21import {emitSoftReset} from '#/state/events' 22import {useHomeBadge} from '#/state/home-badge' 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 {useColorModeTheme} from '#/alf/util/useColorModeTheme' 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 {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2' 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_heavy, 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 a.z_10, 436 { 437 width: 24, 438 height: 24, 439 }, 440 leftNavMinimal && { 441 width: 40, 442 height: 40, 443 }, 444 ]}> 445 {isCurrent ? iconFilled : icon} 446 {typeof count === 'string' && count ? ( 447 <View 448 style={[ 449 a.absolute, 450 a.inset_0, 451 {right: -20}, // more breathing room 452 ]}> 453 <Text 454 accessibilityLabel={_( 455 msg`${plural(count, { 456 one: '# unread item', 457 other: '# unread items', 458 })}`, 459 )} 460 accessibilityHint="" 461 accessible={true} 462 numberOfLines={1} 463 style={[ 464 a.absolute, 465 a.text_xs, 466 a.font_bold, 467 a.rounded_full, 468 a.text_center, 469 a.leading_tight, 470 { 471 top: '-10%', 472 left: count.length === 1 ? 12 : 8, 473 backgroundColor: t.palette.primary_500, 474 color: t.palette.black, 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 { 496 backgroundColor: t.palette.primary_500, 497 width: 8, 498 height: 8, 499 right: -2, 500 top: -4, 501 }, 502 leftNavMinimal && { 503 right: 4, 504 top: 2, 505 }, 506 ]} 507 /> 508 ) : null} 509 </View> 510 {!leftNavMinimal && ( 511 <Text style={[a.text_xl, isCurrent ? a.font_heavy : a.font_normal]}> 512 {label} 513 </Text> 514 )} 515 </PressableWithHover> 516 ) 517} 518 519function ComposeBtn() { 520 const {currentAccount} = useSession() 521 const {getState} = useNavigation() 522 const {openComposer} = useOpenComposer() 523 const {_} = useLingui() 524 const {leftNavMinimal} = useLayoutBreakpoints() 525 const [isFetchingHandle, setIsFetchingHandle] = useState(false) 526 const fetchHandle = useFetchHandle() 527 528 const getProfileHandle = async () => { 529 const routes = getState()?.routes 530 const currentRoute = routes?.[routes?.length - 1] 531 532 if (currentRoute?.name === 'Profile') { 533 let handle: string | undefined = ( 534 currentRoute.params as CommonNavigatorParams['Profile'] 535 ).name 536 537 if (handle.startsWith('did:')) { 538 try { 539 setIsFetchingHandle(true) 540 handle = await fetchHandle(handle) 541 } catch (e) { 542 handle = undefined 543 } finally { 544 setIsFetchingHandle(false) 545 } 546 } 547 548 if ( 549 !handle || 550 handle === currentAccount?.handle || 551 isInvalidHandle(handle) 552 ) 553 return undefined 554 555 return handle 556 } 557 558 return undefined 559 } 560 561 const onPressCompose = async () => 562 openComposer({mention: await getProfileHandle()}) 563 564 if (leftNavMinimal) { 565 return null 566 } 567 568 return ( 569 <View style={[a.flex_row, a.pl_md, a.pt_xl]}> 570 <Button 571 disabled={isFetchingHandle} 572 label={_(msg`Compose new post`)} 573 onPress={onPressCompose} 574 size="large" 575 variant="solid" 576 color="primary" 577 style={[a.rounded_full]}> 578 <ButtonIcon icon={EditBig} position="left" /> 579 <ButtonText> 580 <Trans context="action">New Post</Trans> 581 </ButtonText> 582 </Button> 583 </View> 584 ) 585} 586 587function ChatNavItem() { 588 const theme = useTheme() 589 const colorMode = useColorModeTheme() 590 const {_} = useLingui() 591 const numUnreadMessages = useUnreadMessageCount() 592 593 const textStyle: TextStyle = { 594 color: colorMode === 'light' ? theme.palette.black : theme.palette.white, 595 } 596 597 return ( 598 <NavItem 599 href="/messages" 600 count={numUnreadMessages.numUnread} 601 hasNew={numUnreadMessages.hasNew} 602 icon={ 603 <Message style={textStyle} aria-hidden={true} width={NAV_ICON_WIDTH} /> 604 } 605 iconFilled={ 606 <MessageFilled 607 style={textStyle} 608 aria-hidden={true} 609 width={NAV_ICON_WIDTH} 610 /> 611 } 612 label={_(msg`Chat`)} 613 /> 614 ) 615} 616 617export function DesktopLeftNav() { 618 const {hasSession, currentAccount} = useSession() 619 const theme = useTheme() 620 const colorMode = useColorModeTheme() 621 const {_} = useLingui() 622 const {isDesktop} = useWebMediaQueries() 623 const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints() 624 const numUnreadNotifications = useUnreadNotifications() 625 const hasHomeBadge = useHomeBadge() 626 const gate = useGate() 627 628 const textStyle: TextStyle = { 629 color: colorMode === 'light' ? theme.palette.black : theme.palette.white, 630 } 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 leftNavMinimal && styles.leftNavMinimal, 643 { 644 transform: [ 645 { 646 translateX: 647 -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0), 648 }, 649 {translateX: '-100%'}, 650 ...a.scrollbar_offset.transform, 651 ], 652 }, 653 ]}> 654 {hasSession ? ( 655 <ProfileCard /> 656 ) : !leftNavMinimal ? ( 657 <View style={[a.pt_xl]}> 658 <NavSignupCard /> 659 </View> 660 ) : null} 661 662 {hasSession && ( 663 <> 664 <NavItem 665 href="/" 666 hasNew={hasHomeBadge && gate('remove_show_latest_button')} 667 icon={ 668 <Home 669 aria-hidden={true} 670 width={NAV_ICON_WIDTH} 671 style={textStyle} 672 /> 673 } 674 iconFilled={ 675 <HomeFilled 676 aria-hidden={true} 677 width={NAV_ICON_WIDTH} 678 style={textStyle} 679 /> 680 } 681 label={_(msg`Home`)} 682 /> 683 <NavItem 684 href="/search" 685 icon={ 686 <MagnifyingGlass 687 style={textStyle} 688 aria-hidden={true} 689 width={NAV_ICON_WIDTH} 690 /> 691 } 692 iconFilled={ 693 <MagnifyingGlassFilled 694 style={textStyle} 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={textStyle} 709 /> 710 } 711 iconFilled={ 712 <BellFilled 713 aria-hidden={true} 714 width={NAV_ICON_WIDTH} 715 style={textStyle} 716 /> 717 } 718 label={_(msg`Notifications`)} 719 /> 720 <ChatNavItem /> 721 <NavItem 722 href="/feeds" 723 icon={ 724 <Hashtag 725 style={textStyle} 726 aria-hidden={true} 727 width={NAV_ICON_WIDTH} 728 /> 729 } 730 iconFilled={ 731 <HashtagFilled 732 style={textStyle} 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={textStyle} 744 aria-hidden={true} 745 width={NAV_ICON_WIDTH} 746 /> 747 } 748 iconFilled={ 749 <ListFilled 750 style={textStyle} 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={textStyle} 762 aria-hidden={true} 763 width={NAV_ICON_WIDTH} 764 /> 765 } 766 iconFilled={ 767 <BookmarkFilled 768 style={textStyle} 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={textStyle} 787 /> 788 } 789 iconFilled={ 790 <UserCircleFilled 791 aria-hidden={true} 792 width={NAV_ICON_WIDTH} 793 style={textStyle} 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={textStyle} 805 /> 806 } 807 iconFilled={ 808 <SettingsFilled 809 aria-hidden={true} 810 width={NAV_ICON_WIDTH} 811 style={textStyle} 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 leftNavMinimal: { 837 paddingTop: 0, 838 paddingBottom: 0, 839 paddingLeft: 0, 840 paddingRight: 0, 841 height: '100%', 842 width: 86, 843 alignItems: 'center', 844 ...web({overflowX: 'hidden'}), 845 }, 846 backBtn: { 847 position: 'absolute', 848 top: 12, 849 right: 12, 850 width: 30, 851 height: 30, 852 }, 853})