forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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})