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