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 (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName="HomeTab"
|
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'
|
name: 'change-email'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SwitchAccountModal {
|
||||||
|
name: 'switch-account'
|
||||||
|
}
|
||||||
|
|
||||||
export type Modal =
|
export type Modal =
|
||||||
// Account
|
// Account
|
||||||
| AddAppPasswordModal
|
| AddAppPasswordModal
|
||||||
|
@ -160,6 +164,7 @@ export type Modal =
|
||||||
| BirthDateSettingsModal
|
| BirthDateSettingsModal
|
||||||
| VerifyEmailModal
|
| VerifyEmailModal
|
||||||
| ChangeEmailModal
|
| ChangeEmailModal
|
||||||
|
| SwitchAccountModal
|
||||||
|
|
||||||
// Curation
|
// Curation
|
||||||
| ContentFilteringSettingsModal
|
| ContentFilteringSettingsModal
|
||||||
|
|
|
@ -32,6 +32,7 @@ import * as ModerationDetailsModal from './ModerationDetails'
|
||||||
import * as BirthDateSettingsModal from './BirthDateSettings'
|
import * as BirthDateSettingsModal from './BirthDateSettings'
|
||||||
import * as VerifyEmailModal from './VerifyEmail'
|
import * as VerifyEmailModal from './VerifyEmail'
|
||||||
import * as ChangeEmailModal from './ChangeEmail'
|
import * as ChangeEmailModal from './ChangeEmail'
|
||||||
|
import * as SwitchAccountModal from './SwitchAccount'
|
||||||
|
|
||||||
const DEFAULT_SNAPPOINTS = ['90%']
|
const DEFAULT_SNAPPOINTS = ['90%']
|
||||||
|
|
||||||
|
@ -144,6 +145,9 @@ export const ModalsContainer = observer(function ModalsContainer() {
|
||||||
} else if (activeModal?.name === 'change-email') {
|
} else if (activeModal?.name === 'change-email') {
|
||||||
snapPoints = ChangeEmailModal.snapPoints
|
snapPoints = ChangeEmailModal.snapPoints
|
||||||
element = <ChangeEmailModal.Component />
|
element = <ChangeEmailModal.Component />
|
||||||
|
} else if (activeModal?.name === 'switch-account') {
|
||||||
|
snapPoints = SwitchAccountModal.snapPoints
|
||||||
|
element = <SwitchAccountModal.Component />
|
||||||
} else {
|
} else {
|
||||||
return null
|
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]),
|
}, [store, screen, onPressLoadLatest]),
|
||||||
)
|
)
|
||||||
|
|
||||||
useTabFocusEffect(
|
useTabFocusEffect(
|
||||||
'Notifications',
|
'Notifications',
|
||||||
React.useCallback(
|
React.useCallback(
|
||||||
|
|
|
@ -3,8 +3,8 @@ import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
Pressable,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
|
@ -36,22 +36,21 @@ import {SelectableBtn} from 'view/com/util/forms/SelectableBtn'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useCustomPalette} from 'lib/hooks/useCustomPalette'
|
import {useCustomPalette} from 'lib/hooks/useCustomPalette'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
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 {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {HandIcon, HashtagIcon} from 'lib/icons'
|
import {HandIcon, HashtagIcon} from 'lib/icons'
|
||||||
import {formatCount} from 'view/com/util/numeric/format'
|
import {formatCount} from 'view/com/util/numeric/format'
|
||||||
import Clipboard from '@react-native-clipboard/clipboard'
|
import Clipboard from '@react-native-clipboard/clipboard'
|
||||||
import {reset as resetNavigation} from '../../Navigation'
|
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {AccountDropdownBtn} from 'view/com/util/AccountDropdownBtn'
|
||||||
|
|
||||||
// TEMPORARY (APP-700)
|
// TEMPORARY (APP-700)
|
||||||
// remove after backend testing finishes
|
// remove after backend testing finishes
|
||||||
// -prf
|
// -prf
|
||||||
import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
|
import {useDebugHeaderSetting} from 'lib/api/debug-appview-proxy-header'
|
||||||
import {STATUS_PAGE_URL} from 'lib/constants'
|
import {STATUS_PAGE_URL} from 'lib/constants'
|
||||||
import {DropdownItem, NativeDropdown} from 'view/com/util/forms/NativeDropdown'
|
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
|
||||||
export const SettingsScreen = withAuthRequired(
|
export const SettingsScreen = withAuthRequired(
|
||||||
|
@ -61,7 +60,8 @@ export const SettingsScreen = withAuthRequired(
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
const [isSwitching, setIsSwitching] = React.useState(false)
|
const [isSwitching, setIsSwitching, onPressSwitchAccount] =
|
||||||
|
useAccountSwitcher()
|
||||||
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
|
const [debugHeaderEnabled, toggleDebugHeader] = useDebugHeaderSetting(
|
||||||
store.agent,
|
store.agent,
|
||||||
)
|
)
|
||||||
|
@ -91,25 +91,6 @@ export const SettingsScreen = withAuthRequired(
|
||||||
}, [screen, store]),
|
}, [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(() => {
|
const onPressAddAccount = React.useCallback(() => {
|
||||||
track('Settings:AddAccountButtonClicked')
|
track('Settings:AddAccountButtonClicked')
|
||||||
navigation.navigate('HomeTab')
|
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(
|
const EmailConfirmationNotice = observer(
|
||||||
function EmailConfirmationNoticeImpl() {
|
function EmailConfirmationNoticeImpl() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
|
@ -75,6 +75,9 @@ export const BottomBar = observer(function BottomBarImpl({
|
||||||
const onPressProfile = React.useCallback(() => {
|
const onPressProfile = React.useCallback(() => {
|
||||||
onPressTab('MyProfile')
|
onPressTab('MyProfile')
|
||||||
}, [onPressTab])
|
}, [onPressTab])
|
||||||
|
const onLongPressProfile = React.useCallback(() => {
|
||||||
|
store.shell.openModal({name: 'switch-account'})
|
||||||
|
}, [store])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
|
@ -202,6 +205,7 @@ export const BottomBar = observer(function BottomBarImpl({
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
onPress={onPressProfile}
|
onPress={onPressProfile}
|
||||||
|
onLongPress={onLongPressProfile}
|
||||||
accessibilityRole="tab"
|
accessibilityRole="tab"
|
||||||
accessibilityLabel="Profile"
|
accessibilityLabel="Profile"
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
|
|
Loading…
Reference in New Issue