Skip to content

Commit

Permalink
Account quick switch modal (#1567)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
pfrazee and mozzius authored Sep 28, 2023
1 parent 3e340b3 commit 2e5f73f
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 59 deletions.
1 change: 1 addition & 0 deletions src/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ function TabsNavigator() {
),
[],
)

return (
<Tab.Navigator
initialRouteName="HomeTab"
Expand Down
41 changes: 41 additions & 0 deletions src/lib/hooks/useAccountSwitcher.ts
Original file line number Diff line number Diff line change
@@ -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]
}
5 changes: 5 additions & 0 deletions src/state/models/ui/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export interface ChangeEmailModal {
name: 'change-email'
}

export interface SwitchAccountModal {
name: 'switch-account'
}

export type Modal =
// Account
| AddAppPasswordModal
Expand All @@ -160,6 +164,7 @@ export type Modal =
| BirthDateSettingsModal
| VerifyEmailModal
| ChangeEmailModal
| SwitchAccountModal

// Curation
| ContentFilteringSettingsModal
Expand Down
4 changes: 4 additions & 0 deletions src/view/com/modals/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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%']

Expand Down Expand Up @@ -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
}
Expand Down
136 changes: 136 additions & 0 deletions src/view/com/modals/SwitchAccount.tsx
Original file line number Diff line number Diff line change
@@ -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,
},
})
46 changes: 46 additions & 0 deletions src/view/com/util/AccountDropdownBtn.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
1 change: 1 addition & 0 deletions src/view/screens/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const NotificationsScreen = withAuthRequired(
}
}, [store, screen, onPressLoadLatest]),
)

useTabFocusEffect(
'Notifications',
React.useCallback(
Expand Down
64 changes: 5 additions & 59 deletions src/view/screens/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {
ActivityIndicator,
Linking,
Platform,
Pressable,
StyleSheet,
Pressable,
TextStyle,
TouchableOpacity,
View,
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
Loading

0 comments on commit 2e5f73f

Please sign in to comment.