diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index 1485ca7fc8..a7cf6cb3f4 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -961,6 +961,9 @@ export const atoms = { transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', transitionDuration: '100ms', }), + transition_delay_50ms: web({ + transitionDelay: '50ms', + }), /** * {@link Layout.SCROLLBAR_OFFSET} diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 871c17ed51..123e6ee422 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -3,10 +3,12 @@ import { AccessibilityProps, GestureResponderEvent, MouseEvent, + NativeSyntheticEvent, Pressable, PressableProps, StyleProp, StyleSheet, + TargetedEvent, TextProps, TextStyle, View, @@ -76,6 +78,8 @@ export type ButtonProps = Pick< | 'onHoverOut' | 'onPressIn' | 'onPressOut' + | 'onFocus' + | 'onBlur' > & AccessibilityProps & VariantProps & { @@ -116,6 +120,12 @@ export const Button = React.forwardRef( style, hoverStyle: hoverStyleProp, PressableComponent = Pressable, + onPressIn: onPressInOuter, + onPressOut: onPressOutOuter, + onHoverIn: onHoverInOuter, + onHoverOut: onHoverOutOuter, + onFocus: onFocusOuter, + onBlur: onBlurOuter, ...rest }, ref, @@ -127,7 +137,6 @@ export const Button = React.forwardRef( focused: false, }) - const onPressInOuter = rest.onPressIn const onPressIn = React.useCallback( (e: GestureResponderEvent) => { setState(s => ({ @@ -138,7 +147,6 @@ export const Button = React.forwardRef( }, [setState, onPressInOuter], ) - const onPressOutOuter = rest.onPressOut const onPressOut = React.useCallback( (e: GestureResponderEvent) => { setState(s => ({ @@ -149,7 +157,6 @@ export const Button = React.forwardRef( }, [setState, onPressOutOuter], ) - const onHoverInOuter = rest.onHoverIn const onHoverIn = React.useCallback( (e: MouseEvent) => { setState(s => ({ @@ -160,7 +167,6 @@ export const Button = React.forwardRef( }, [setState, onHoverInOuter], ) - const onHoverOutOuter = rest.onHoverOut const onHoverOut = React.useCallback( (e: MouseEvent) => { setState(s => ({ @@ -171,18 +177,26 @@ export const Button = React.forwardRef( }, [setState, onHoverOutOuter], ) - const onFocus = React.useCallback(() => { - setState(s => ({ - ...s, - focused: true, - })) - }, [setState]) - const onBlur = React.useCallback(() => { - setState(s => ({ - ...s, - focused: false, - })) - }, [setState]) + const onFocus = React.useCallback( + (e: NativeSyntheticEvent) => { + setState(s => ({ + ...s, + focused: true, + })) + onFocusOuter?.(e) + }, + [setState, onFocusOuter], + ) + const onBlur = React.useCallback( + (e: NativeSyntheticEvent) => { + setState(s => ({ + ...s, + focused: false, + })) + onBlurOuter?.(e) + }, + [setState, onBlurOuter], + ) const {baseStyles, hoverStyles} = React.useMemo(() => { const baseStyles: ViewStyle[] = [] diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index 6bbb8c21c0..d1863e478e 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -202,7 +202,7 @@ export function Outer({ ) } -export function Item({children, label, onPress, ...rest}: ItemProps) { +export function Item({children, label, onPress, style, ...rest}: ItemProps) { const t = useTheme() const {control} = useMenuContext() const { @@ -248,6 +248,7 @@ export function Item({children, label, onPress, ...rest}: ItemProps) { ? t.atoms.bg_contrast_25 : t.atoms.bg_contrast_50, ], + style, ])} {...web({ onMouseEnter, diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index d367e1b98e..e9ba65ed02 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 ? ( - - - - ) : ( - - 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.ProfileView + }[] + | 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 @@ -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,