import React from 'react' import {StyleSheet, View} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import { useLinkProps, useNavigation, useNavigationState, } from '@react-navigation/native' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {usePalette} from '#/lib/hooks/usePalette' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {getCurrentRoute, isTab} from '#/lib/routes/helpers' import {makeProfileLink} from '#/lib/routes/links' import {type CommonNavigatorParams} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' import {emitSoftReset} from '#/state/events' import {useHomeBadge} from '#/state/home-badge' import {useFetchHandle} from '#/state/queries/handle' import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' import {useUnreadNotifications} from '#/state/queries/notifications/unread' import {useProfilesQuery} from '#/state/queries/profile' import {type SessionAccount, useSession, useSessionApi} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {PressableWithHover} from '#/view/com/util/PressableWithHover' import {UserAvatar} from '#/view/com/util/UserAvatar' import {NavSignupCard} from '#/view/shell/NavSignupCard' import {atoms as a, tokens, useLayoutBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {type DialogControlProps} from '#/components/Dialog' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' import { Bell_Filled_Corner0_Rounded as BellFilled, Bell_Stroke2_Corner0_Rounded as Bell, } from '#/components/icons/Bell' import { BulletList_Filled_Corner0_Rounded as ListFilled, BulletList_Stroke2_Corner0_Rounded as List, } from '#/components/icons/BulletList' import {DotGrid_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' import { Hashtag_Filled_Corner0_Rounded as HashtagFilled, Hashtag_Stroke2_Corner0_Rounded as Hashtag, } from '#/components/icons/Hashtag' import { HomeOpen_Filled_Corner0_Rounded as HomeFilled, HomeOpen_Stoke2_Corner0_Rounded as Home, } from '#/components/icons/HomeOpen' import {MagnifyingGlass_Filled_Stroke2_Corner0_Rounded as MagnifyingGlassFilled} from '#/components/icons/MagnifyingGlass' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as MagnifyingGlass} from '#/components/icons/MagnifyingGlass2' import { Message_Stroke2_Corner0_Rounded as Message, Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, } from '#/components/icons/Message' import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' import { SettingsGear2_Filled_Corner0_Rounded as SettingsFilled, SettingsGear2_Stroke2_Corner0_Rounded as Settings, } from '#/components/icons/SettingsGear2' import { UserCircle_Filled_Corner0_Rounded as UserCircleFilled, UserCircle_Stroke2_Corner0_Rounded as UserCircle, } from '#/components/icons/UserCircle' import {CENTER_COLUMN_OFFSET} from '#/components/Layout' import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' import {router} from '../../../routes' const NAV_ICON_WIDTH = 28 function ProfileCard() { const {currentAccount, accounts} = useSession() const {logoutEveryAccount} = useSessionApi() const {isLoading, data} = useProfilesQuery({ handles: accounts.map(acc => acc.did), }) const profiles = data?.profiles const signOutPromptControl = Prompt.usePromptControl() const {leftNavMinimal} = useLayoutBreakpoints() const {_} = useLingui() const t = useTheme() const size = 48 const profile = profiles?.find(p => p.did === currentAccount!.did) const otherAccounts = accounts .filter(acc => acc.did !== currentAccount!.did) .map(account => ({ account, profile: profiles?.find(p => p.did === account.did), })) return ( {!isLoading && profile ? ( {({props, state, control}) => { const active = state.hovered || state.focused || control.isOpen return ( ) }} ) : ( )} logoutEveryAccount('Settings')} confirmButtonCta={_(msg`Sign out`)} cancelButtonCta={_(msg`Cancel`)} confirmButtonColor="negative" /> ) } function SwitchMenuItems({ accounts, signOutPromptControl, }: { accounts: | { account: SessionAccount profile?: AppBskyActorDefs.ProfileViewDetailed }[] | undefined signOutPromptControl: DialogControlProps }) { const {_} = useLingui() const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() const closeEverything = useCloseAllActiveElements() const onAddAnotherAccount = () => { setShowLoggedOut(true) closeEverything() } return ( {accounts && accounts.length > 0 && ( <> Switch account {accounts.map(other => ( onPressSwitchAccount(other.account, 'SwitchAccount') }> {sanitizeHandle( other.profile?.handle ?? other.account.handle, '@', )} ))} )} Add another account Sign out ) } interface NavItemProps { count?: string hasNew?: boolean href: string icon: JSX.Element iconFilled: JSX.Element label: string } function NavItem({count, hasNew, href, icon, iconFilled, label}: NavItemProps) { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() const {leftNavMinimal} = useLayoutBreakpoints() const [pathName] = React.useMemo(() => router.matchPath(href), [href]) const currentRouteInfo = useNavigationState(state => { if (!state) { return {name: 'Home'} } return getCurrentRoute(state) }) let isCurrent = currentRouteInfo.name === 'Profile' ? isTab(currentRouteInfo.name, pathName) && (currentRouteInfo.params as CommonNavigatorParams['Profile']).name === currentAccount?.handle : isTab(currentRouteInfo.name, pathName) const {onPress} = useLinkProps({to: href}) const onPressWrapped = React.useCallback( (e: React.MouseEvent) => { if (e.ctrlKey || e.metaKey || e.altKey) { return } e.preventDefault() if (isCurrent) { emitSoftReset() } else { onPress() } }, [onPress, isCurrent], ) return ( {isCurrent ? iconFilled : icon} {typeof count === 'string' && count ? ( {count} ) : hasNew ? ( ) : null} {!leftNavMinimal && ( {label} )} ) } function ComposeBtn() { const {currentAccount} = useSession() const {getState} = useNavigation() const {openComposer} = useComposerControls() const {_} = useLingui() const {leftNavMinimal} = useLayoutBreakpoints() const [isFetchingHandle, setIsFetchingHandle] = React.useState(false) const fetchHandle = useFetchHandle() const getProfileHandle = async () => { const routes = getState()?.routes const currentRoute = routes?.[routes?.length - 1] if (currentRoute?.name === 'Profile') { let handle: string | undefined = ( currentRoute.params as CommonNavigatorParams['Profile'] ).name if (handle.startsWith('did:')) { try { setIsFetchingHandle(true) handle = await fetchHandle(handle) } catch (e) { handle = undefined } finally { setIsFetchingHandle(false) } } if ( !handle || handle === currentAccount?.handle || isInvalidHandle(handle) ) return undefined return handle } return undefined } const onPressCompose = async () => openComposer({mention: await getProfileHandle()}) if (leftNavMinimal) { return null } return ( ) } function ChatNavItem() { const pal = usePalette('default') const {_} = useLingui() const numUnreadMessages = useUnreadMessageCount() return ( } iconFilled={ } label={_(msg`Chat`)} /> ) } export function DesktopLeftNav() { const {hasSession, currentAccount} = useSession() const pal = usePalette('default') const {_} = useLingui() const {isDesktop} = useWebMediaQueries() const {leftNavMinimal, centerColumnOffset} = useLayoutBreakpoints() const numUnreadNotifications = useUnreadNotifications() const hasHomeBadge = useHomeBadge() const gate = useGate() if (!hasSession && !isDesktop) { return null } return ( {hasSession ? ( ) : isDesktop ? ( ) : null} {hasSession && ( <> } iconFilled={ } label={_(msg`Home`)} /> } iconFilled={ } label={_(msg`Explore`)} /> } iconFilled={ } label={_(msg`Notifications`)} /> } iconFilled={ } label={_(msg`Feeds`)} /> } iconFilled={ } label={_(msg`Lists`)} /> } iconFilled={ } label={_(msg`Profile`)} /> } iconFilled={ } label={_(msg`Settings`)} /> )} ) } const styles = StyleSheet.create({ leftNav: { position: 'fixed', top: 0, paddingTop: 10, paddingBottom: 10, left: '50%', width: 240, // @ts-expect-error web only maxHeight: '100vh', overflowY: 'auto', }, leftNavMinimal: { paddingTop: 0, paddingBottom: 0, paddingLeft: 0, paddingRight: 0, height: '100%', width: 86, alignItems: 'center', overflowX: 'hidden', }, backBtn: { position: 'absolute', top: 12, right: 12, width: 30, height: 30, }, })