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