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 {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 {DotGrid_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 {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
631 if (!hasSession && !isDesktop) {
632 return null
633 }
634
635 return (
636 <View
637 role="navigation"
638 style={[
639 a.px_xl,
640 styles.leftNav,
641 leftNavMinimal && styles.leftNavMinimal,
642 {
643 transform: [
644 {
645 translateX:
646 -300 + (centerColumnOffset ? CENTER_COLUMN_OFFSET : 0),
647 },
648 {translateX: '-100%'},
649 ...a.scrollbar_offset.transform,
650 ],
651 },
652 ]}>
653 {hasSession ? (
654 <ProfileCard />
655 ) : !leftNavMinimal ? (
656 <View style={[a.pt_xl]}>
657 <NavSignupCard />
658 </View>
659 ) : null}
660
661 {hasSession && (
662 <>
663 <NavItem
664 href="/"
665 icon={
666 <Home
667 aria-hidden={true}
668 width={NAV_ICON_WIDTH}
669 style={pal.text}
670 />
671 }
672 iconFilled={
673 <HomeFilled
674 aria-hidden={true}
675 width={NAV_ICON_WIDTH}
676 style={pal.text}
677 />
678 }
679 label={_(msg`Home`)}
680 />
681 <NavItem
682 href="/search"
683 icon={
684 <MagnifyingGlass
685 style={pal.text}
686 aria-hidden={true}
687 width={NAV_ICON_WIDTH}
688 />
689 }
690 iconFilled={
691 <MagnifyingGlassFilled
692 style={pal.text}
693 aria-hidden={true}
694 width={NAV_ICON_WIDTH}
695 />
696 }
697 label={_(msg`Explore`)}
698 />
699 <NavItem
700 href="/notifications"
701 count={numUnreadNotifications}
702 icon={
703 <Bell
704 aria-hidden={true}
705 width={NAV_ICON_WIDTH}
706 style={pal.text}
707 />
708 }
709 iconFilled={
710 <BellFilled
711 aria-hidden={true}
712 width={NAV_ICON_WIDTH}
713 style={pal.text}
714 />
715 }
716 label={_(msg`Notifications`)}
717 />
718 <ChatNavItem />
719 <NavItem
720 href="/feeds"
721 icon={
722 <Hashtag
723 style={pal.text}
724 aria-hidden={true}
725 width={NAV_ICON_WIDTH}
726 />
727 }
728 iconFilled={
729 <HashtagFilled
730 style={pal.text}
731 aria-hidden={true}
732 width={NAV_ICON_WIDTH}
733 />
734 }
735 label={_(msg`Feeds`)}
736 />
737 <NavItem
738 href="/lists"
739 icon={
740 <List
741 style={pal.text}
742 aria-hidden={true}
743 width={NAV_ICON_WIDTH}
744 />
745 }
746 iconFilled={
747 <ListFilled
748 style={pal.text}
749 aria-hidden={true}
750 width={NAV_ICON_WIDTH}
751 />
752 }
753 label={_(msg`Lists`)}
754 />
755 <NavItem
756 href="/saved"
757 icon={
758 <Bookmark
759 style={pal.text}
760 aria-hidden={true}
761 width={NAV_ICON_WIDTH}
762 />
763 }
764 iconFilled={
765 <BookmarkFilled
766 style={pal.text}
767 aria-hidden={true}
768 width={NAV_ICON_WIDTH}
769 />
770 }
771 label={_(
772 msg({
773 message: 'Saved',
774 context: 'link to bookmarks screen',
775 }),
776 )}
777 />
778 <NavItem
779 href={currentAccount ? makeProfileLink(currentAccount) : '/'}
780 icon={
781 <UserCircle
782 aria-hidden={true}
783 width={NAV_ICON_WIDTH}
784 style={pal.text}
785 />
786 }
787 iconFilled={
788 <UserCircleFilled
789 aria-hidden={true}
790 width={NAV_ICON_WIDTH}
791 style={pal.text}
792 />
793 }
794 label={_(msg`Profile`)}
795 />
796 <NavItem
797 href="/settings"
798 icon={
799 <Settings
800 aria-hidden={true}
801 width={NAV_ICON_WIDTH}
802 style={pal.text}
803 />
804 }
805 iconFilled={
806 <SettingsFilled
807 aria-hidden={true}
808 width={NAV_ICON_WIDTH}
809 style={pal.text}
810 />
811 }
812 label={_(msg`Settings`)}
813 />
814
815 <ComposeBtn />
816 </>
817 )}
818 </View>
819 )
820}
821
822const styles = StyleSheet.create({
823 leftNav: {
824 ...a.fixed,
825 top: 0,
826 paddingTop: 10,
827 paddingBottom: 10,
828 left: '50%',
829 width: 240,
830 // @ts-expect-error web only
831 maxHeight: '100vh',
832 overflowY: 'auto',
833 },
834 leftNavMinimal: {
835 paddingTop: 0,
836 paddingBottom: 0,
837 paddingLeft: 0,
838 paddingRight: 0,
839 height: '100%',
840 width: 86,
841 alignItems: 'center',
842 ...web({overflowX: 'hidden'}),
843 },
844 backBtn: {
845 position: 'absolute',
846 top: 12,
847 right: 12,
848 width: 30,
849 height: 30,
850 },
851})