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

Configure Feed

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

at 6d68a5bd212dd4eeee816828ffe4e27601cdd7f3 845 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 {sanitizeDisplayName} from '#/lib/strings/display-names' 20import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 21import {emitSoftReset} from '#/state/events' 22import {useFetchHandle} from '#/state/queries/handle' 23import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' 24import {useUnreadNotifications} from '#/state/queries/notifications/unread' 25import {useProfilesQuery} from '#/state/queries/profile' 26import {type SessionAccount, useSession, useSessionApi} from '#/state/session' 27import {useLoggedOutViewControls} from '#/state/shell/logged-out' 28import {useCloseAllActiveElements} from '#/state/util' 29import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 30import {PressableWithHover} from '#/view/com/util/PressableWithHover' 31import {UserAvatar} from '#/view/com/util/UserAvatar' 32import {NavSignupCard} from '#/view/shell/NavSignupCard' 33import {atoms as a, tokens, useLayoutBreakpoints, useTheme, web} from '#/alf' 34import {Button, ButtonIcon, ButtonText} from '#/components/Button' 35import {type DialogControlProps} from '#/components/Dialog' 36import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' 37import { 38 Bell_Filled_Corner0_Rounded as BellFilled, 39 Bell_Stroke2_Corner0_Rounded as Bell, 40} from '#/components/icons/Bell' 41import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark' 42import { 43 BulletList_Filled_Corner0_Rounded as ListFilled, 44 BulletList_Stroke2_Corner0_Rounded as List, 45} from '#/components/icons/BulletList' 46import {DotGrid_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' 47import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' 48import { 49 Hashtag_Filled_Corner0_Rounded as HashtagFilled, 50 Hashtag_Stroke2_Corner0_Rounded as Hashtag, 51} from '#/components/icons/Hashtag' 52import { 53 HomeOpen_Filled_Corner0_Rounded as HomeFilled, 54 HomeOpen_Stoke2_Corner0_Rounded as Home, 55} from '#/components/icons/HomeOpen' 56import { 57 MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled, 58 MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlass, 59} 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(), logContext: 'Fab'}) 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 621 if (!hasSession && !isDesktop) { 622 return null 623 } 624 625 return ( 626 <View 627 role="navigation" 628 style={[ 629 a.px_xl, 630 styles.leftNav, 631 !hasSession && !leftNavMinimal && styles.leftNavWide, 632 leftNavMinimal && styles.leftNavMinimal, 633 { 634 transform: [ 635 { 636 translateX: 637 -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0), 638 }, 639 {translateX: '-100%'}, 640 ...a.scrollbar_offset.transform, 641 ], 642 }, 643 ]}> 644 {hasSession ? ( 645 <ProfileCard /> 646 ) : !leftNavMinimal ? ( 647 <View style={[a.pt_xl]}> 648 <NavSignupCard /> 649 </View> 650 ) : null} 651 652 {hasSession && ( 653 <> 654 <NavItem 655 href="/" 656 icon={ 657 <Home 658 aria-hidden={true} 659 width={NAV_ICON_WIDTH} 660 style={pal.text} 661 /> 662 } 663 iconFilled={ 664 <HomeFilled 665 aria-hidden={true} 666 width={NAV_ICON_WIDTH} 667 style={pal.text} 668 /> 669 } 670 label={_(msg`Home`)} 671 /> 672 <NavItem 673 href="/search" 674 icon={ 675 <MagnifyingGlass 676 style={pal.text} 677 aria-hidden={true} 678 width={NAV_ICON_WIDTH} 679 /> 680 } 681 iconFilled={ 682 <MagnifyingGlassFilled 683 style={pal.text} 684 aria-hidden={true} 685 width={NAV_ICON_WIDTH} 686 /> 687 } 688 label={_(msg`Explore`)} 689 /> 690 <NavItem 691 href="/notifications" 692 count={numUnreadNotifications} 693 icon={ 694 <Bell 695 aria-hidden={true} 696 width={NAV_ICON_WIDTH} 697 style={pal.text} 698 /> 699 } 700 iconFilled={ 701 <BellFilled 702 aria-hidden={true} 703 width={NAV_ICON_WIDTH} 704 style={pal.text} 705 /> 706 } 707 label={_(msg`Notifications`)} 708 /> 709 <ChatNavItem /> 710 <NavItem 711 href="/feeds" 712 icon={ 713 <Hashtag 714 style={pal.text} 715 aria-hidden={true} 716 width={NAV_ICON_WIDTH} 717 /> 718 } 719 iconFilled={ 720 <HashtagFilled 721 style={pal.text} 722 aria-hidden={true} 723 width={NAV_ICON_WIDTH} 724 /> 725 } 726 label={_(msg`Feeds`)} 727 /> 728 <NavItem 729 href="/lists" 730 icon={ 731 <List 732 style={pal.text} 733 aria-hidden={true} 734 width={NAV_ICON_WIDTH} 735 /> 736 } 737 iconFilled={ 738 <ListFilled 739 style={pal.text} 740 aria-hidden={true} 741 width={NAV_ICON_WIDTH} 742 /> 743 } 744 label={_(msg`Lists`)} 745 /> 746 <NavItem 747 href="/saved" 748 icon={ 749 <Bookmark 750 style={pal.text} 751 aria-hidden={true} 752 width={NAV_ICON_WIDTH} 753 /> 754 } 755 iconFilled={ 756 <BookmarkFilled 757 style={pal.text} 758 aria-hidden={true} 759 width={NAV_ICON_WIDTH} 760 /> 761 } 762 label={_( 763 msg({ 764 message: 'Saved', 765 context: 'link to bookmarks screen', 766 }), 767 )} 768 /> 769 <NavItem 770 href={currentAccount ? makeProfileLink(currentAccount) : '/'} 771 icon={ 772 <UserCircle 773 aria-hidden={true} 774 width={NAV_ICON_WIDTH} 775 style={pal.text} 776 /> 777 } 778 iconFilled={ 779 <UserCircleFilled 780 aria-hidden={true} 781 width={NAV_ICON_WIDTH} 782 style={pal.text} 783 /> 784 } 785 label={_(msg`Profile`)} 786 /> 787 <NavItem 788 href="/settings" 789 icon={ 790 <Settings 791 aria-hidden={true} 792 width={NAV_ICON_WIDTH} 793 style={pal.text} 794 /> 795 } 796 iconFilled={ 797 <SettingsFilled 798 aria-hidden={true} 799 width={NAV_ICON_WIDTH} 800 style={pal.text} 801 /> 802 } 803 label={_(msg`Settings`)} 804 /> 805 806 <ComposeBtn /> 807 </> 808 )} 809 </View> 810 ) 811} 812 813const styles = StyleSheet.create({ 814 leftNav: { 815 ...a.fixed, 816 top: 0, 817 paddingTop: 10, 818 paddingBottom: 10, 819 left: '50%', 820 width: 240, 821 // @ts-expect-error web only 822 maxHeight: '100vh', 823 overflowY: 'auto', 824 }, 825 leftNavWide: { 826 width: 245, 827 }, 828 leftNavMinimal: { 829 paddingTop: 0, 830 paddingBottom: 0, 831 paddingLeft: 0, 832 paddingRight: 0, 833 height: '100%', 834 width: 86, 835 alignItems: 'center', 836 ...web({overflowX: 'hidden'}), 837 }, 838 backBtn: { 839 position: 'absolute', 840 top: 12, 841 right: 12, 842 width: 30, 843 height: 30, 844 }, 845})