diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-01-14 12:53:13 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-14 12:53:13 +0000 |
commit | 479a4a92e10e8d12d3e164a65970bd2de4abfe8a (patch) | |
tree | d73c7c5db743d50c7e43528dc37e1f2a0ca0e24d /src/view/shell/desktop/LeftNav.tsx | |
parent | 1fc889b883ffd666b4c439738f1f5e97496d55e6 (diff) | |
download | voidsky-479a4a92e10e8d12d3e164a65970bd2de4abfe8a.tar.zst |
Account quick switch on web (#7190)
* account quick switch on web * dont show line if one account * switch account label text * add chevron hover state * swagged up hover state * add icons * tune scale anim and respect prefers-reduced-motion * fix reduced motion * fix placeholder position * move menu components out to separate component * Pipe through outer handlers to Button * Abstract lag in control.isOpen state * add profile info into empty space * fix tablet * Alternative * Revert "Alternative" This reverts commit 050ab9595ef3bbc32529ad6588e4690d37539fbe. * maybe fix flicker issue * just do 50ms when not active * delay other animations --------- Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src/view/shell/desktop/LeftNav.tsx')
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 263 |
1 files changed, 224 insertions, 39 deletions
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index d367e1b98..e9ba65ed0 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -1,5 +1,6 @@ import React from 'react' import {StyleSheet, View} from 'react-native' +import {AppBskyActorDefs} from '@atproto/api' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -9,28 +10,33 @@ import { 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 {CommonNavigatorParams} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' -import {isInvalidHandle} from '#/lib/strings/handles' +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 {useProfileQuery} from '#/state/queries/profile' -import {useSession} from '#/state/session' +import {useProfilesQuery} from '#/state/queries/profile' +import {SessionAccount, useSession, useSessionApi} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' -import {Link} from '#/view/com/util/Link' +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, useBreakpoints, useTheme} from '#/alf' +import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {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, @@ -39,6 +45,7 @@ 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, @@ -54,6 +61,7 @@ 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, @@ -62,44 +70,231 @@ import { UserCircle_Filled_Corner0_Rounded as UserCircleFilled, UserCircle_Stroke2_Corner0_Rounded as UserCircle, } from '#/components/icons/UserCircle' +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} = useSession() - const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did}) - const {isDesktop} = useWebMediaQueries() + 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 {gtTablet} = useBreakpoints() const {_} = useLingui() + const t = useTheme() + const size = 48 - return !isLoading && profile ? ( - <Link - href={makeProfileLink({ - did: currentAccount!.did, - handle: currentAccount!.handle, - })} - style={[styles.profileCard, !isDesktop && styles.profileCardTablet]} - title={_(msg`My Profile`)} - asAnchor> - <UserAvatar - avatar={profile.avatar} - size={size} - type={profile?.associated?.labeler ? 'labeler' : 'user'} - /> - </Link> - ) : ( - <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}> - <LoadingPlaceholder - width={size} - height={size} - style={{borderRadius: size}} + 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 ( + <View style={[a.my_md, gtTablet && [a.w_full, a.align_start]]}> + {!isLoading && profile ? ( + <Menu.Root> + <Menu.Trigger label={_(msg`Switch accounts`)}> + {({props, state, control}) => { + const active = state.hovered || state.focused || control.isOpen + return ( + <Button + label={props.accessibilityLabel} + {...props} + style={[ + a.w_full, + a.transition_color, + active ? t.atoms.bg_contrast_25 : a.transition_delay_50ms, + a.rounded_full, + a.justify_between, + a.align_center, + a.flex_row, + {gap: 6}, + gtTablet && [a.pl_lg, a.pr_md], + ]}> + <View + style={[ + !PlatformInfo.getIsReducedMotionEnabled() && [ + a.transition_transform, + {transitionDuration: '250ms'}, + !active && a.transition_delay_50ms, + ], + a.relative, + a.z_10, + active && { + transform: [ + {scale: gtTablet ? 2 / 3 : 0.8}, + {translateX: gtTablet ? -22 : 0}, + ], + }, + ]}> + <UserAvatar + avatar={profile.avatar} + size={size} + type={profile?.associated?.labeler ? 'labeler' : 'user'} + /> + </View> + {gtTablet && ( + <> + <View + style={[ + a.flex_1, + a.transition_opacity, + !active && a.transition_delay_50ms, + { + marginLeft: tokens.space.xl * -1, + opacity: active ? 1 : 0, + }, + ]}> + <Text + style={[a.font_heavy, a.text_sm, a.leading_snug]} + numberOfLines={1}> + {sanitizeDisplayName( + profile.displayName || profile.handle, + )} + </Text> + <Text + style={[ + a.text_xs, + a.leading_snug, + t.atoms.text_contrast_medium, + ]} + numberOfLines={1}> + {sanitizeHandle(profile.handle, '@')} + </Text> + </View> + <EllipsisIcon + aria-hidden={true} + style={[ + t.atoms.text_contrast_medium, + a.transition_opacity, + {opacity: active ? 1 : 0}, + ]} + size="sm" + /> + </> + )} + </Button> + ) + }} + </Menu.Trigger> + <SwitchMenuItems + accounts={otherAccounts} + signOutPromptControl={signOutPromptControl} + /> + </Menu.Root> + ) : ( + <LoadingPlaceholder + width={size} + height={size} + style={[{borderRadius: size}, gtTablet && a.ml_lg]} + /> + )} + <Prompt.Basic + control={signOutPromptControl} + title={_(msg`Sign out?`)} + description={_(msg`You will be signed out of all your accounts.`)} + onConfirm={() => logoutEveryAccount('Settings')} + confirmButtonCta={_(msg`Sign out`)} + cancelButtonCta={_(msg`Cancel`)} + confirmButtonColor="negative" /> </View> ) } +function SwitchMenuItems({ + accounts, + signOutPromptControl, +}: { + accounts: + | { + account: SessionAccount + profile?: AppBskyActorDefs.ProfileView + }[] + | undefined + signOutPromptControl: DialogControlProps +}) { + const {_} = useLingui() + const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeEverything = useCloseAllActiveElements() + + const onAddAnotherAccount = () => { + setShowLoggedOut(true) + closeEverything() + } + return ( + <Menu.Outer> + {accounts && accounts.length > 0 && ( + <> + <Menu.Group> + <Menu.LabelText> + <Trans>Switch account</Trans> + </Menu.LabelText> + {accounts.map(other => ( + <Menu.Item + disabled={!!pendingDid} + style={[{minWidth: 150}]} + key={other.account.did} + label={_( + msg`Switch to ${sanitizeHandle( + other.profile?.handle ?? other.account.handle, + '@', + )}`, + )} + onPress={() => + onPressSwitchAccount(other.account, 'SwitchAccount') + }> + <View style={[{marginLeft: tokens.space._2xs * -1}]}> + <UserAvatar + avatar={other.profile?.avatar} + size={20} + type={ + other.profile?.associated?.labeler ? 'labeler' : 'user' + } + /> + </View> + <Menu.ItemText> + {sanitizeHandle( + other.profile?.handle ?? other.account.handle, + '@', + )} + </Menu.ItemText> + </Menu.Item> + ))} + </Menu.Group> + <Menu.Divider /> + </> + )} + <Menu.Item + label={_(msg`Add another account`)} + onPress={onAddAnotherAccount}> + <Menu.ItemIcon icon={PlusIcon} /> + <Menu.ItemText> + <Trans>Add another account</Trans> + </Menu.ItemText> + </Menu.Item> + <Menu.Item label={_(msg`Sign out`)} onPress={signOutPromptControl.open}> + <Menu.ItemIcon icon={LeaveIcon} /> + <Menu.ItemText> + <Trans>Sign out</Trans> + </Menu.ItemText> + </Menu.Item> + </Menu.Outer> + ) +} + interface NavItemProps { count?: string hasNew?: boolean @@ -539,16 +734,6 @@ const styles = StyleSheet.create({ alignItems: 'center', transform: [], }, - - profileCard: { - marginVertical: 10, - width: 90, - paddingLeft: 12, - }, - profileCardTablet: { - width: 70, - }, - backBtn: { position: 'absolute', top: 12, |