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