Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Samuel Newman
Eric Bailey
and committed by
GitHub
479a4a92 1fc889b8

+259 -56
+3
src/alf/atoms.ts
··· 961 961 transitionTimingFunction: 'cubic-bezier(0.17, 0.73, 0.14, 1)', 962 962 transitionDuration: '100ms', 963 963 }), 964 + transition_delay_50ms: web({ 965 + transitionDelay: '50ms', 966 + }), 964 967 965 968 /** 966 969 * {@link Layout.SCROLLBAR_OFFSET}
+30 -16
src/components/Button.tsx
··· 3 3 AccessibilityProps, 4 4 GestureResponderEvent, 5 5 MouseEvent, 6 + NativeSyntheticEvent, 6 7 Pressable, 7 8 PressableProps, 8 9 StyleProp, 9 10 StyleSheet, 11 + TargetedEvent, 10 12 TextProps, 11 13 TextStyle, 12 14 View, ··· 76 78 | 'onHoverOut' 77 79 | 'onPressIn' 78 80 | 'onPressOut' 81 + | 'onFocus' 82 + | 'onBlur' 79 83 > & 80 84 AccessibilityProps & 81 85 VariantProps & { ··· 116 120 style, 117 121 hoverStyle: hoverStyleProp, 118 122 PressableComponent = Pressable, 123 + onPressIn: onPressInOuter, 124 + onPressOut: onPressOutOuter, 125 + onHoverIn: onHoverInOuter, 126 + onHoverOut: onHoverOutOuter, 127 + onFocus: onFocusOuter, 128 + onBlur: onBlurOuter, 119 129 ...rest 120 130 }, 121 131 ref, ··· 127 137 focused: false, 128 138 }) 129 139 130 - const onPressInOuter = rest.onPressIn 131 140 const onPressIn = React.useCallback( 132 141 (e: GestureResponderEvent) => { 133 142 setState(s => ({ ··· 138 147 }, 139 148 [setState, onPressInOuter], 140 149 ) 141 - const onPressOutOuter = rest.onPressOut 142 150 const onPressOut = React.useCallback( 143 151 (e: GestureResponderEvent) => { 144 152 setState(s => ({ ··· 149 157 }, 150 158 [setState, onPressOutOuter], 151 159 ) 152 - const onHoverInOuter = rest.onHoverIn 153 160 const onHoverIn = React.useCallback( 154 161 (e: MouseEvent) => { 155 162 setState(s => ({ ··· 160 167 }, 161 168 [setState, onHoverInOuter], 162 169 ) 163 - const onHoverOutOuter = rest.onHoverOut 164 170 const onHoverOut = React.useCallback( 165 171 (e: MouseEvent) => { 166 172 setState(s => ({ ··· 171 177 }, 172 178 [setState, onHoverOutOuter], 173 179 ) 174 - const onFocus = React.useCallback(() => { 175 - setState(s => ({ 176 - ...s, 177 - focused: true, 178 - })) 179 - }, [setState]) 180 - const onBlur = React.useCallback(() => { 181 - setState(s => ({ 182 - ...s, 183 - focused: false, 184 - })) 185 - }, [setState]) 180 + const onFocus = React.useCallback( 181 + (e: NativeSyntheticEvent<TargetedEvent>) => { 182 + setState(s => ({ 183 + ...s, 184 + focused: true, 185 + })) 186 + onFocusOuter?.(e) 187 + }, 188 + [setState, onFocusOuter], 189 + ) 190 + const onBlur = React.useCallback( 191 + (e: NativeSyntheticEvent<TargetedEvent>) => { 192 + setState(s => ({ 193 + ...s, 194 + focused: false, 195 + })) 196 + onBlurOuter?.(e) 197 + }, 198 + [setState, onBlurOuter], 199 + ) 186 200 187 201 const {baseStyles, hoverStyles} = React.useMemo(() => { 188 202 const baseStyles: ViewStyle[] = []
+2 -1
src/components/Menu/index.web.tsx
··· 202 202 ) 203 203 } 204 204 205 - export function Item({children, label, onPress, ...rest}: ItemProps) { 205 + export function Item({children, label, onPress, style, ...rest}: ItemProps) { 206 206 const t = useTheme() 207 207 const {control} = useMenuContext() 208 208 const { ··· 248 248 ? t.atoms.bg_contrast_25 249 249 : t.atoms.bg_contrast_50, 250 250 ], 251 + style, 251 252 ])} 252 253 {...web({ 253 254 onMouseEnter,
+224 -39
src/view/shell/desktop/LeftNav.tsx
··· 1 1 import React from 'react' 2 2 import {StyleSheet, View} from 'react-native' 3 + import {AppBskyActorDefs} from '@atproto/api' 3 4 import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 4 5 import {msg, Trans} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' ··· 9 10 useNavigationState, 10 11 } from '@react-navigation/native' 11 12 13 + import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 12 14 import {usePalette} from '#/lib/hooks/usePalette' 13 15 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 14 16 import {getCurrentRoute, isTab} from '#/lib/routes/helpers' 15 17 import {makeProfileLink} from '#/lib/routes/links' 16 18 import {CommonNavigatorParams} from '#/lib/routes/types' 17 19 import {useGate} from '#/lib/statsig/statsig' 18 - import {isInvalidHandle} from '#/lib/strings/handles' 20 + import {sanitizeDisplayName} from '#/lib/strings/display-names' 21 + import {isInvalidHandle, sanitizeHandle} from '#/lib/strings/handles' 19 22 import {emitSoftReset} from '#/state/events' 20 23 import {useHomeBadge} from '#/state/home-badge' 21 24 import {useFetchHandle} from '#/state/queries/handle' 22 25 import {useUnreadMessageCount} from '#/state/queries/messages/list-conversations' 23 26 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 24 - import {useProfileQuery} from '#/state/queries/profile' 25 - import {useSession} from '#/state/session' 27 + import {useProfilesQuery} from '#/state/queries/profile' 28 + import {SessionAccount, useSession, useSessionApi} from '#/state/session' 26 29 import {useComposerControls} from '#/state/shell/composer' 27 - import {Link} from '#/view/com/util/Link' 30 + import {useLoggedOutViewControls} from '#/state/shell/logged-out' 31 + import {useCloseAllActiveElements} from '#/state/util' 28 32 import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 29 33 import {PressableWithHover} from '#/view/com/util/PressableWithHover' 30 34 import {UserAvatar} from '#/view/com/util/UserAvatar' 31 35 import {NavSignupCard} from '#/view/shell/NavSignupCard' 32 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 36 + import {atoms as a, tokens, useBreakpoints, useTheme} from '#/alf' 33 37 import {Button, ButtonIcon, ButtonText} from '#/components/Button' 38 + import {DialogControlProps} from '#/components/Dialog' 39 + import {ArrowBoxLeft_Stroke2_Corner0_Rounded as LeaveIcon} from '#/components/icons/ArrowBoxLeft' 34 40 import { 35 41 Bell_Filled_Corner0_Rounded as BellFilled, 36 42 Bell_Stroke2_Corner0_Rounded as Bell, ··· 39 45 BulletList_Filled_Corner0_Rounded as ListFilled, 40 46 BulletList_Stroke2_Corner0_Rounded as List, 41 47 } from '#/components/icons/BulletList' 48 + import {DotGrid_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' 42 49 import {EditBig_Stroke2_Corner0_Rounded as EditBig} from '#/components/icons/EditBig' 43 50 import { 44 51 Hashtag_Filled_Corner0_Rounded as HashtagFilled, ··· 54 61 Message_Stroke2_Corner0_Rounded as Message, 55 62 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, 56 63 } from '#/components/icons/Message' 64 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 57 65 import { 58 66 SettingsGear2_Filled_Corner0_Rounded as SettingsFilled, 59 67 SettingsGear2_Stroke2_Corner0_Rounded as Settings, ··· 62 70 UserCircle_Filled_Corner0_Rounded as UserCircleFilled, 63 71 UserCircle_Stroke2_Corner0_Rounded as UserCircle, 64 72 } from '#/components/icons/UserCircle' 73 + import * as Menu from '#/components/Menu' 74 + import * as Prompt from '#/components/Prompt' 65 75 import {Text} from '#/components/Typography' 76 + import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army' 66 77 import {router} from '../../../routes' 67 78 68 79 const NAV_ICON_WIDTH = 28 69 80 70 81 function ProfileCard() { 71 - const {currentAccount} = useSession() 72 - const {isLoading, data: profile} = useProfileQuery({did: currentAccount!.did}) 73 - const {isDesktop} = useWebMediaQueries() 82 + const {currentAccount, accounts} = useSession() 83 + const {logoutEveryAccount} = useSessionApi() 84 + const {isLoading, data} = useProfilesQuery({ 85 + handles: accounts.map(acc => acc.did), 86 + }) 87 + const profiles = data?.profiles 88 + const signOutPromptControl = Prompt.usePromptControl() 89 + const {gtTablet} = useBreakpoints() 74 90 const {_} = useLingui() 91 + const t = useTheme() 92 + 75 93 const size = 48 76 94 77 - return !isLoading && profile ? ( 78 - <Link 79 - href={makeProfileLink({ 80 - did: currentAccount!.did, 81 - handle: currentAccount!.handle, 82 - })} 83 - style={[styles.profileCard, !isDesktop && styles.profileCardTablet]} 84 - title={_(msg`My Profile`)} 85 - asAnchor> 86 - <UserAvatar 87 - avatar={profile.avatar} 88 - size={size} 89 - type={profile?.associated?.labeler ? 'labeler' : 'user'} 90 - /> 91 - </Link> 92 - ) : ( 93 - <View style={[styles.profileCard, !isDesktop && styles.profileCardTablet]}> 94 - <LoadingPlaceholder 95 - width={size} 96 - height={size} 97 - style={{borderRadius: size}} 95 + const profile = profiles?.find(p => p.did === currentAccount!.did) 96 + const otherAccounts = accounts 97 + .filter(acc => acc.did !== currentAccount!.did) 98 + .map(account => ({ 99 + account, 100 + profile: profiles?.find(p => p.did === account.did), 101 + })) 102 + 103 + return ( 104 + <View style={[a.my_md, gtTablet && [a.w_full, a.align_start]]}> 105 + {!isLoading && profile ? ( 106 + <Menu.Root> 107 + <Menu.Trigger label={_(msg`Switch accounts`)}> 108 + {({props, state, control}) => { 109 + const active = state.hovered || state.focused || control.isOpen 110 + return ( 111 + <Button 112 + label={props.accessibilityLabel} 113 + {...props} 114 + style={[ 115 + a.w_full, 116 + a.transition_color, 117 + active ? t.atoms.bg_contrast_25 : a.transition_delay_50ms, 118 + a.rounded_full, 119 + a.justify_between, 120 + a.align_center, 121 + a.flex_row, 122 + {gap: 6}, 123 + gtTablet && [a.pl_lg, a.pr_md], 124 + ]}> 125 + <View 126 + style={[ 127 + !PlatformInfo.getIsReducedMotionEnabled() && [ 128 + a.transition_transform, 129 + {transitionDuration: '250ms'}, 130 + !active && a.transition_delay_50ms, 131 + ], 132 + a.relative, 133 + a.z_10, 134 + active && { 135 + transform: [ 136 + {scale: gtTablet ? 2 / 3 : 0.8}, 137 + {translateX: gtTablet ? -22 : 0}, 138 + ], 139 + }, 140 + ]}> 141 + <UserAvatar 142 + avatar={profile.avatar} 143 + size={size} 144 + type={profile?.associated?.labeler ? 'labeler' : 'user'} 145 + /> 146 + </View> 147 + {gtTablet && ( 148 + <> 149 + <View 150 + style={[ 151 + a.flex_1, 152 + a.transition_opacity, 153 + !active && a.transition_delay_50ms, 154 + { 155 + marginLeft: tokens.space.xl * -1, 156 + opacity: active ? 1 : 0, 157 + }, 158 + ]}> 159 + <Text 160 + style={[a.font_heavy, a.text_sm, a.leading_snug]} 161 + numberOfLines={1}> 162 + {sanitizeDisplayName( 163 + profile.displayName || profile.handle, 164 + )} 165 + </Text> 166 + <Text 167 + style={[ 168 + a.text_xs, 169 + a.leading_snug, 170 + t.atoms.text_contrast_medium, 171 + ]} 172 + numberOfLines={1}> 173 + {sanitizeHandle(profile.handle, '@')} 174 + </Text> 175 + </View> 176 + <EllipsisIcon 177 + aria-hidden={true} 178 + style={[ 179 + t.atoms.text_contrast_medium, 180 + a.transition_opacity, 181 + {opacity: active ? 1 : 0}, 182 + ]} 183 + size="sm" 184 + /> 185 + </> 186 + )} 187 + </Button> 188 + ) 189 + }} 190 + </Menu.Trigger> 191 + <SwitchMenuItems 192 + accounts={otherAccounts} 193 + signOutPromptControl={signOutPromptControl} 194 + /> 195 + </Menu.Root> 196 + ) : ( 197 + <LoadingPlaceholder 198 + width={size} 199 + height={size} 200 + style={[{borderRadius: size}, gtTablet && a.ml_lg]} 201 + /> 202 + )} 203 + <Prompt.Basic 204 + control={signOutPromptControl} 205 + title={_(msg`Sign out?`)} 206 + description={_(msg`You will be signed out of all your accounts.`)} 207 + onConfirm={() => logoutEveryAccount('Settings')} 208 + confirmButtonCta={_(msg`Sign out`)} 209 + cancelButtonCta={_(msg`Cancel`)} 210 + confirmButtonColor="negative" 98 211 /> 99 212 </View> 213 + ) 214 + } 215 + 216 + function SwitchMenuItems({ 217 + accounts, 218 + signOutPromptControl, 219 + }: { 220 + accounts: 221 + | { 222 + account: SessionAccount 223 + profile?: AppBskyActorDefs.ProfileView 224 + }[] 225 + | undefined 226 + signOutPromptControl: DialogControlProps 227 + }) { 228 + const {_} = useLingui() 229 + const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() 230 + const {setShowLoggedOut} = useLoggedOutViewControls() 231 + const closeEverything = useCloseAllActiveElements() 232 + 233 + const onAddAnotherAccount = () => { 234 + setShowLoggedOut(true) 235 + closeEverything() 236 + } 237 + return ( 238 + <Menu.Outer> 239 + {accounts && accounts.length > 0 && ( 240 + <> 241 + <Menu.Group> 242 + <Menu.LabelText> 243 + <Trans>Switch account</Trans> 244 + </Menu.LabelText> 245 + {accounts.map(other => ( 246 + <Menu.Item 247 + disabled={!!pendingDid} 248 + style={[{minWidth: 150}]} 249 + key={other.account.did} 250 + label={_( 251 + msg`Switch to ${sanitizeHandle( 252 + other.profile?.handle ?? other.account.handle, 253 + '@', 254 + )}`, 255 + )} 256 + onPress={() => 257 + onPressSwitchAccount(other.account, 'SwitchAccount') 258 + }> 259 + <View style={[{marginLeft: tokens.space._2xs * -1}]}> 260 + <UserAvatar 261 + avatar={other.profile?.avatar} 262 + size={20} 263 + type={ 264 + other.profile?.associated?.labeler ? 'labeler' : 'user' 265 + } 266 + /> 267 + </View> 268 + <Menu.ItemText> 269 + {sanitizeHandle( 270 + other.profile?.handle ?? other.account.handle, 271 + '@', 272 + )} 273 + </Menu.ItemText> 274 + </Menu.Item> 275 + ))} 276 + </Menu.Group> 277 + <Menu.Divider /> 278 + </> 279 + )} 280 + <Menu.Item 281 + label={_(msg`Add another account`)} 282 + onPress={onAddAnotherAccount}> 283 + <Menu.ItemIcon icon={PlusIcon} /> 284 + <Menu.ItemText> 285 + <Trans>Add another account</Trans> 286 + </Menu.ItemText> 287 + </Menu.Item> 288 + <Menu.Item label={_(msg`Sign out`)} onPress={signOutPromptControl.open}> 289 + <Menu.ItemIcon icon={LeaveIcon} /> 290 + <Menu.ItemText> 291 + <Trans>Sign out</Trans> 292 + </Menu.ItemText> 293 + </Menu.Item> 294 + </Menu.Outer> 100 295 ) 101 296 } 102 297 ··· 539 734 alignItems: 'center', 540 735 transform: [], 541 736 }, 542 - 543 - profileCard: { 544 - marginVertical: 10, 545 - width: 90, 546 - paddingLeft: 12, 547 - }, 548 - profileCardTablet: { 549 - width: 70, 550 - }, 551 - 552 737 backBtn: { 553 738 position: 'absolute', 554 739 top: 12,