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

Configure Feed

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

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