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} 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 {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
23import {useFetchHandle} from '#/state/queries/handle'
24import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations'
25import {useUnreadNotifications} from '#/state/queries/notifications/unread'
26import {useProfilesQuery} from '#/state/queries/profile'
27import {type SessionAccount, useSession, useSessionApi} from '#/state/session'
28import {useLoggedOutViewControls} from '#/state/shell/logged-out'
29import {useCloseAllActiveElements} from '#/state/util'
30import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
31import {PressableWithHover} from '#/view/com/util/PressableWithHover'
32import {UserAvatar} from '#/view/com/util/UserAvatar'
33import {NavSignupCard} from '#/view/shell/NavSignupCard'
34import {atoms as a, tokens, useLayoutBreakpoints, useTheme, web} from '#/alf'
35import {Button, ButtonIcon, ButtonText} from '#/components/Button'
36import {type DialogControlProps} from '#/components/Dialog'
37import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft'
38import {
39 Bell_Filled_Corner0_Rounded as BellFilled,
40 Bell_Stroke2_Corner0_Rounded as Bell,
41} from '#/components/icons/Bell'
42import {Bookmark, BookmarkFilled} from '#/components/icons/Bookmark'
43import {
44 BulletList_Filled_Corner0_Rounded as ListFilled,
45 BulletList_Stroke2_Corner0_Rounded as List,
46} from '#/components/icons/BulletList'
47import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid'
48import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig'
49import {
50 Hashtag_Filled_Corner0_Rounded as HashtagFilled,
51 Hashtag_Stroke2_Corner0_Rounded as Hashtag,
52} from '#/components/icons/Hashtag'
53import {
54 HomeOpen_Filled_Corner0_Rounded as HomeFilled,
55 HomeOpen_Stoke2_Corner0_Rounded as Home,
56} from '#/components/icons/HomeOpen'
57import {
58 MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled,
59 MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlass,
60} from '#/components/icons/MagnifyingGlass'
61import {
62 Message_Stroke2_Corner0_Rounded as Message,
63 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
64} from '#/components/icons/Message'
65import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
66import {
67 SettingsGear2_Filled_Corner0_Rounded as SettingsFilled,
68 SettingsGear2_Stroke2_Corner0_Rounded as Settings,
69} from '#/components/icons/SettingsGear2'
70import {
71 UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
72 UserCircle_Stroke2_Corner0_Rounded as UserCircle,
73} from '#/components/icons/UserCircle'
74import {CENTER_COLUMN_OFFSET} from '#/components/Layout'
75import * as Menu from '#/components/Menu'
76import * as Prompt from '#/components/Prompt'
77import {Text} from '#/components/Typography'
78import {useAgeAssurance} from '#/ageAssurance'
79import {useActorStatus} from '#/features/liveNow'
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={() => void 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 {leftNavMinimal} = useLayoutBreakpoints()
536 const [isFetchingHandle, setIsFetchingHandle] = useState(false)
537 const fetchHandle = useFetchHandle()
538
539 const enableSquareButtons = useEnableSquareButtons()
540
541 const getProfileHandle = async () => {
542 const routes = getState()?.routes
543 const currentRoute = routes?.[routes?.length - 1]
544
545 if (currentRoute?.name === 'Profile') {
546 let handle: string | undefined = (
547 currentRoute.params as CommonNavigatorParams['Profile']
548 ).name
549
550 if (handle.startsWith('did:')) {
551 try {
552 setIsFetchingHandle(true)
553 handle = await fetchHandle(handle)
554 } catch (e) {
555 handle = undefined
556 } finally {
557 setIsFetchingHandle(false)
558 }
559 }
560
561 if (
562 !handle ||
563 handle === currentAccount?.handle ||
564 isInvalidHandle(handle)
565 )
566 return undefined
567
568 return handle
569 }
570
571 return undefined
572 }
573
574 const onPressCompose = async () =>
575 openComposer({mention: await getProfileHandle(), logContext: 'Fab'})
576
577 if (leftNavMinimal) {
578 return null
579 }
580
581 return (
582 <View style={[a.flex_row, a.pl_md, a.pt_xl]}>
583 <Button
584 disabled={isFetchingHandle}
585 label={_(msg`Compose new post`)}
586 onPress={() => void onPressCompose()}
587 size="large"
588 variant="solid"
589 color="primary"
590 style={enableSquareButtons ? [a.rounded_sm] : [a.rounded_full]}>
591 <ButtonIcon icon={EditBig} position="left" />
592 <ButtonText>
593 <Trans context="action">New post</Trans>
594 </ButtonText>
595 </Button>
596 </View>
597 )
598}
599
600function ChatNavItem() {
601 const pal = usePalette('default')
602 const {_} = useLingui()
603 const numUnreadMessages = useUnreadMessageCount()
604 const aa = useAgeAssurance()
605
606 return (
607 <NavItem
608 href="/messages"
609 count={aa.flags.chatDisabled ? undefined : numUnreadMessages.numUnread}
610 hasNew={aa.flags.chatDisabled ? false : numUnreadMessages.hasNew}
611 icon={
612 <Message style={pal.text} aria-hidden={true} width={NAV_ICON_WIDTH} />
613 }
614 iconFilled={
615 <MessageFilled
616 style={pal.text}
617 aria-hidden={true}
618 width={NAV_ICON_WIDTH}
619 />
620 }
621 label={_(msg`Chat`)}
622 />
623 )
624}
625
626export function DesktopLeftNav() {
627 const {hasSession, currentAccount} = useSession()
628 const pal = usePalette('default')
629 const {_} = useLingui()
630 const {isDesktop} = useWebMediaQueries()
631 const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints()
632 const numUnreadNotifications = useUnreadNotifications()
633
634 if (!hasSession && !isDesktop) {
635 return null
636 }
637
638 return (
639 <View
640 role="navigation"
641 style={[
642 a.px_xl,
643 styles.leftNav,
644 !hasSession && !leftNavMinimal && styles.leftNavWide,
645 leftNavMinimal && styles.leftNavMinimal,
646 {
647 transform: [
648 {
649 translateX:
650 -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0),
651 },
652 {translateX: '-100%'},
653 ...a.scrollbar_offset.transform,
654 ],
655 },
656 ]}>
657 {hasSession ? (
658 <ProfileCard />
659 ) : !leftNavMinimal ? (
660 <View style={[a.pt_xl]}>
661 <NavSignupCard />
662 </View>
663 ) : null}
664
665 {hasSession && (
666 <>
667 <NavItem
668 href="/"
669 icon={
670 <Home
671 aria-hidden={true}
672 width={NAV_ICON_WIDTH}
673 style={pal.text}
674 />
675 }
676 iconFilled={
677 <HomeFilled
678 aria-hidden={true}
679 width={NAV_ICON_WIDTH}
680 style={pal.text}
681 />
682 }
683 label={_(msg`Home`)}
684 />
685 <NavItem
686 href="/search"
687 icon={
688 <MagnifyingGlass
689 style={pal.text}
690 aria-hidden={true}
691 width={NAV_ICON_WIDTH}
692 />
693 }
694 iconFilled={
695 <MagnifyingGlassFilled
696 style={pal.text}
697 aria-hidden={true}
698 width={NAV_ICON_WIDTH}
699 />
700 }
701 label={_(msg`Explore`)}
702 />
703 <NavItem
704 href="/notifications"
705 count={numUnreadNotifications}
706 icon={
707 <Bell
708 aria-hidden={true}
709 width={NAV_ICON_WIDTH}
710 style={pal.text}
711 />
712 }
713 iconFilled={
714 <BellFilled
715 aria-hidden={true}
716 width={NAV_ICON_WIDTH}
717 style={pal.text}
718 />
719 }
720 label={_(msg`Notifications`)}
721 />
722 <ChatNavItem />
723 <NavItem
724 href="/feeds"
725 icon={
726 <Hashtag
727 style={pal.text}
728 aria-hidden={true}
729 width={NAV_ICON_WIDTH}
730 />
731 }
732 iconFilled={
733 <HashtagFilled
734 style={pal.text}
735 aria-hidden={true}
736 width={NAV_ICON_WIDTH}
737 />
738 }
739 label={_(msg`Feeds`)}
740 />
741 <NavItem
742 href="/lists"
743 icon={
744 <List
745 style={pal.text}
746 aria-hidden={true}
747 width={NAV_ICON_WIDTH}
748 />
749 }
750 iconFilled={
751 <ListFilled
752 style={pal.text}
753 aria-hidden={true}
754 width={NAV_ICON_WIDTH}
755 />
756 }
757 label={_(msg`Lists`)}
758 />
759 <NavItem
760 href="/saved"
761 icon={
762 <Bookmark
763 style={pal.text}
764 aria-hidden={true}
765 width={NAV_ICON_WIDTH}
766 />
767 }
768 iconFilled={
769 <BookmarkFilled
770 style={pal.text}
771 aria-hidden={true}
772 width={NAV_ICON_WIDTH}
773 />
774 }
775 label={_(
776 msg({
777 message: 'Saved',
778 context: 'link to bookmarks screen',
779 }),
780 )}
781 />
782 <NavItem
783 href={currentAccount ? makeProfileLink(currentAccount) : '/'}
784 icon={
785 <UserCircle
786 aria-hidden={true}
787 width={NAV_ICON_WIDTH}
788 style={pal.text}
789 />
790 }
791 iconFilled={
792 <UserCircleFilled
793 aria-hidden={true}
794 width={NAV_ICON_WIDTH}
795 style={pal.text}
796 />
797 }
798 label={_(msg`Profile`)}
799 />
800 <NavItem
801 href="/settings"
802 icon={
803 <Settings
804 aria-hidden={true}
805 width={NAV_ICON_WIDTH}
806 style={pal.text}
807 />
808 }
809 iconFilled={
810 <SettingsFilled
811 aria-hidden={true}
812 width={NAV_ICON_WIDTH}
813 style={pal.text}
814 />
815 }
816 label={_(msg`Settings`)}
817 />
818
819 <ComposeBtn />
820 </>
821 )}
822 </View>
823 )
824}
825
826const styles = StyleSheet.create({
827 leftNav: {
828 ...a.fixed,
829 top: 0,
830 paddingTop: 10,
831 paddingBottom: 10,
832 left: '50%',
833 width: 240,
834 // @ts-expect-error web only
835 maxHeight: '100vh',
836 overflowY: 'auto',
837 },
838 leftNavWide: {
839 width: 245,
840 },
841 leftNavMinimal: {
842 paddingTop: 0,
843 paddingBottom: 0,
844 paddingLeft: 0,
845 paddingRight: 0,
846 height: '100%',
847 width: 86,
848 alignItems: 'center',
849 ...web({overflowX: 'hidden'}),
850 },
851 backBtn: {
852 position: 'absolute',
853 top: 12,
854 right: 12,
855 width: 30,
856 height: 30,
857 },
858})