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

Configure Feed

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

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