Account quick switch modal (#1567)
* quick switch menu * Some small tweaks and fixes to the account switch modal * Factor out the account switcher logic to a hook * Add haptic feedback on account switcher open * Fix bad merge --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com>zio/stable
parent
3e340b336e
commit
2e5f73ff61
|
@ -246,6 +246,7 @@ function TabsNavigator() {
|
|||
),
|
||||
[],
|
||||
)
|
||||
|
||||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName="HomeTab"
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import {useCallback, useState} from 'react'
|
||||
import {useStores} from 'state/index'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {StackActions, useNavigation} from '@react-navigation/native'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {AccountData} from 'state/models/session'
|
||||
import {reset as resetNavigation} from '../../Navigation'
|
||||
import * as Toast from 'view/com/util/Toast'
|
||||
|
||||
export function useAccountSwitcher(): [
|
||||
boolean,
|
||||
(v: boolean) => void,
|
||||
(acct: AccountData) => Promise<void>,
|
||||
] {
|
||||
const {track} = useAnalytics()
|
||||
|
||||
const store = useStores()
|
||||
const [isSwitching, setIsSwitching] = useState(false)
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
|
||||
const onPressSwitchAccount = useCallback(
|
||||
async (acct: AccountData) => {
|
||||
track('Settings:SwitchAccountButtonClicked')
|
||||
setIsSwitching(true)
|
||||
const success = await store.session.resumeSession(acct)
|
||||
store.shell.closeAllActiveElements()
|
||||
if (success) {
|
||||
resetNavigation()
|
||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||
} else {
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
store.session.clear()
|
||||
}
|
||||
},
|
||||
[track, setIsSwitching, navigation, store],
|
||||
)
|
||||
|
||||
return [isSwitching, setIsSwitching, onPressSwitchAccount]
|
||||
}
|
|
@ -150,6 +150,10 @@ export interface ChangeEmailModal {
|
|||
name: 'change-email'
|
||||
}
|
||||
|
||||
export interface SwitchAccountModal {
|
||||
name: 'switch-account'
|
||||
}
|
||||
|
||||
export type Modal =
|
||||
// Account
|
||||
| AddAppPasswordModal
|
||||
|
@ -160,6 +164,7 @@ export type Modal =
|
|||
| BirthDateSettingsModal
|
||||
| VerifyEmailModal
|
||||
| ChangeEmailModal
|
||||
| SwitchAccountModal
|
||||
|
||||
// Curation
|
||||
| ContentFilteringSettingsModal
|
||||
|
|
|
@ -32,6 +32,7 @@ import * as ModerationDetailsModal from './ModerationDetails'
|
|||
import * as BirthDateSettingsModal from './BirthDateSettings'
|
||||
import * as VerifyEmailModal from './VerifyEmail'
|
||||
import * as ChangeEmailModal from './ChangeEmail'
|
||||
import * as SwitchAccountModal from './SwitchAccount'
|
||||
|
||||
const DEFAULT_SNAPPOINTS = ['90%']
|
||||
|
||||
|
@ -144,6 +145,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'change-email') {
|
||||
snapPoints = ChangeEmailModal.snapPoints
|
||||
element = <ChangeEmailModal.Component />
|
||||
} else if (activeModal?.name === 'switch-account') {
|
||||
snapPoints = SwitchAccountModal.snapPoints
|
||||
element = <SwitchAccountModal.Component />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
||||
import {UserAvatar} from '../util/UserAvatar'
|
||||
import {AccountDropdownBtn} from '../util/AccountDropdownBtn'
|
||||
import {Link} from '../util/Link'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {BottomSheetScrollView} from '@gorhom/bottom-sheet'
|
||||
import {Haptics} from 'lib/haptics'
|
||||
|
||||
export const snapPoints = ['40%', '90%']
|
||||
|
||||
export function Component({}: {}) {
|
||||
const pal = usePalette('default')
|
||||
const {track} = useAnalytics()
|
||||
|
||||
const store = useStores()
|
||||
const [isSwitching, _, onPressSwitchAccount] = useAccountSwitcher()
|
||||
|
||||
React.useEffect(() => {
|
||||
Haptics.default()
|
||||
})
|
||||
|
||||
const onPressSignout = React.useCallback(() => {
|
||||
track('Settings:SignOutButtonClicked')
|
||||
store.session.logout()
|
||||
}, [track, store])
|
||||
|
||||
return (
|
||||
<View style={[styles.container, pal.view]}>
|
||||
<Text type="title-xl" style={[styles.title, pal.text]}>
|
||||
Switch Account
|
||||
</Text>
|
||||
<BottomSheetScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={[styles.innerContainer, pal.view]}>
|
||||
{isSwitching ? (
|
||||
<View style={[pal.view, styles.linkCard]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
) : (
|
||||
<Link
|
||||
href={makeProfileLink(store.me)}
|
||||
title="Your profile"
|
||||
noFeedback>
|
||||
<View style={[pal.view, styles.linkCard]}>
|
||||
<View style={styles.avi}>
|
||||
<UserAvatar size={40} avatar={store.me.avatar} />
|
||||
</View>
|
||||
<View style={[s.flex1]}>
|
||||
<Text type="md-bold" style={pal.text} numberOfLines={1}>
|
||||
{store.me.displayName || store.me.handle}
|
||||
</Text>
|
||||
<Text type="sm" style={pal.textLight} numberOfLines={1}>
|
||||
{store.me.handle}
|
||||
</Text>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
testID="signOutBtn"
|
||||
onPress={isSwitching ? undefined : onPressSignout}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Sign out"
|
||||
accessibilityHint={`Signs ${store.me.displayName} out of Bluesky`}>
|
||||
<Text type="lg" style={pal.link}>
|
||||
Sign out
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Link>
|
||||
)}
|
||||
{store.session.switchableAccounts.map(account => (
|
||||
<TouchableOpacity
|
||||
testID={`switchToAccountBtn-${account.handle}`}
|
||||
key={account.did}
|
||||
style={[pal.view, styles.linkCard, isSwitching && styles.dimmed]}
|
||||
onPress={
|
||||
isSwitching ? undefined : () => onPressSwitchAccount(account)
|
||||
}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Switch to ${account.handle}`}
|
||||
accessibilityHint="Switches the account you are logged in to">
|
||||
<View style={styles.avi}>
|
||||
<UserAvatar size={40} avatar={account.aviUrl} />
|
||||
</View>
|
||||
<View style={[s.flex1]}>
|
||||
<Text type="md-bold" style={pal.text}>
|
||||
{account.displayName || account.handle}
|
||||
</Text>
|
||||
<Text type="sm" style={pal.textLight}>
|
||||
{account.handle}
|
||||
</Text>
|
||||
</View>
|
||||
<AccountDropdownBtn handle={account.handle} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</BottomSheetScrollView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
innerContainer: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
marginTop: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
linkCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 18,
|
||||
marginBottom: 1,
|
||||
},
|
||||
avi: {
|
||||
marginRight: 12,
|
||||
},
|
||||
dimmed: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react'
|
||||
import {Pressable} from 'react-native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {s} from 'lib/styles'
|
||||
import {useStores} from 'state/index'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {DropdownItem, NativeDropdown} from './forms/NativeDropdown'
|
||||
import * as Toast from '../../com/util/Toast'
|
||||
|
||||
export function AccountDropdownBtn({handle}: {handle: string}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const items: DropdownItem[] = [
|
||||
{
|
||||
label: 'Remove account',
|
||||
onPress: () => {
|
||||
store.session.removeAccount(handle)
|
||||
Toast.show('Account removed from quick access')
|
||||
},
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
},
|
||||
android: 'ic_delete',
|
||||
web: 'trash',
|
||||
},
|
||||
},
|
||||
]
|
||||
return (
|
||||
<Pressable accessibilityRole="button" style={s.pl10}>
|
||||
<NativeDropdown
|
||||
testID="accountSettingsDropdownBtn"
|
||||
items={items}
|
||||
accessibilityLabel="Account options"
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis-h"
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
</NativeDropdown>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
|
@ -71,6 +71,7 @@ export const NotificationsScreen = withAuthRequired(
|
|||
}
|
||||
}, [store, screen, onPressLoadLatest]),
|
||||
)
|
||||
|
||||
useTabFocusEffect(
|
||||
'Notifications',
|
||||
React.useCallback(
|
||||
|
|
|
@ -3,8 +3,8 @@ import {
|
|||
ActivityIndicator,
|
||||
Linking,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
TextStyle,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
|
@ -36,22 +36,21 @@ import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
|
|||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useCustomPalette} from 'lib/hooks/useCustomPalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {AccountData} from 'state/models/session'
|
||||
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
|
||||
import {useAnalytics} from 'lib/analytics/analytics'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {HandIcon, HashtagIcon} from 'lib/icons'
|
||||
import {formatCount} from 'view/com/util/numeric/format'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {reset as resetNavigation} from '../../Navigation'
|
||||
import {makeProfileLink} from 'lib/routes/links'
|
||||
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
|
||||
|
||||
// TEMPORARY (APP-700)
|
||||
// remove after backend testing finishes
|
||||
// -prf
|
||||
import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
|
||||
import {STATUS_PAGE_URL} from 'lib/constants'
|
||||
import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||
export const SettingsScreen = withAuthRequired(
|
||||
|
@ -61,7 +60,8 @@ export const SettingsScreen = withAuthRequired(
|
|||
const navigation = useNavigation<NavigationProp>()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
const {screen, track} = useAnalytics()
|
||||
const [isSwitching, setIsSwitching] = React.useState(false)
|
||||
const [isSwitching, setIsSwitching, onPressSwitchAccount] =
|
||||
useAccountSwitcher()
|
||||
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
|
||||
store.agent,
|
||||
)
|
||||
|
@ -91,25 +91,6 @@ export const SettingsScreen = withAuthRequired(
|
|||
}, [screen, store]),
|
||||
)
|
||||
|
||||
const onPressSwitchAccount = React.useCallback(
|
||||
async (acct: AccountData) => {
|
||||
track('Settings:SwitchAccountButtonClicked')
|
||||
setIsSwitching(true)
|
||||
if (await store.session.resumeSession(acct)) {
|
||||
setIsSwitching(false)
|
||||
resetNavigation()
|
||||
Toast.show(`Signed in as ${acct.displayName || acct.handle}`)
|
||||
return
|
||||
}
|
||||
setIsSwitching(false)
|
||||
Toast.show('Sorry! We need you to enter your password.')
|
||||
navigation.navigate('HomeTab')
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
store.session.clear()
|
||||
},
|
||||
[track, setIsSwitching, navigation, store],
|
||||
)
|
||||
|
||||
const onPressAddAccount = React.useCallback(() => {
|
||||
track('Settings:AddAccountButtonClicked')
|
||||
navigation.navigate('HomeTab')
|
||||
|
@ -646,41 +627,6 @@ export const SettingsScreen = withAuthRequired(
|
|||
}),
|
||||
)
|
||||
|
||||
function AccountDropdownBtn({handle}: {handle: string}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const items: DropdownItem[] = [
|
||||
{
|
||||
label: 'Remove account',
|
||||
onPress: () => {
|
||||
store.session.removeAccount(handle)
|
||||
Toast.show('Account removed from quick access')
|
||||
},
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
},
|
||||
android: 'ic_delete',
|
||||
web: 'trash',
|
||||
},
|
||||
},
|
||||
]
|
||||
return (
|
||||
<Pressable accessibilityRole="button" style={s.pl10}>
|
||||
<NativeDropdown
|
||||
testID="accountSettingsDropdownBtn"
|
||||
items={items}
|
||||
accessibilityLabel="Account options"
|
||||
accessibilityHint="">
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis-h"
|
||||
style={pal.textLight as FontAwesomeIconStyle}
|
||||
/>
|
||||
</NativeDropdown>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const EmailConfirmationNotice = observer(
|
||||
function EmailConfirmationNoticeImpl() {
|
||||
const pal = usePalette('default')
|
||||
|
|
|
@ -75,6 +75,9 @@ export const BottomBar = observer(function BottomBarImpl({
|
|||
const onPressProfile = React.useCallback(() => {
|
||||
onPressTab('MyProfile')
|
||||
}, [onPressTab])
|
||||
const onLongPressProfile = React.useCallback(() => {
|
||||
store.shell.openModal({name: 'switch-account'})
|
||||
}, [store])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
|
@ -202,6 +205,7 @@ export const BottomBar = observer(function BottomBarImpl({
|
|||
</View>
|
||||
}
|
||||
onPress={onPressProfile}
|
||||
onLongPress={onLongPressProfile}
|
||||
accessibilityRole="tab"
|
||||
accessibilityLabel="Profile"
|
||||
accessibilityHint=""
|
||||
|
|
Loading…
Reference in New Issue