From 2e5f73ff6149f9ac2834b0417c84b76763ef5ee2 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Thu, 28 Sep 2023 12:41:44 -0700 Subject: [PATCH] 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 --- src/Navigation.tsx | 1 + src/lib/hooks/useAccountSwitcher.ts | 41 +++++++ src/state/models/ui/shell.ts | 5 + src/view/com/modals/Modal.tsx | 4 + src/view/com/modals/SwitchAccount.tsx | 136 +++++++++++++++++++++++ src/view/com/util/AccountDropdownBtn.tsx | 46 ++++++++ src/view/screens/Notifications.tsx | 1 + src/view/screens/Settings.tsx | 64 +---------- src/view/shell/bottom-bar/BottomBar.tsx | 4 + 9 files changed, 243 insertions(+), 59 deletions(-) create mode 100644 src/lib/hooks/useAccountSwitcher.ts create mode 100644 src/view/com/modals/SwitchAccount.tsx create mode 100644 src/view/com/util/AccountDropdownBtn.tsx diff --git a/src/Navigation.tsx b/src/Navigation.tsx index a247c72d..97612c9e 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -246,6 +246,7 @@ function TabsNavigator() { ), [], ) + return ( void, + (acct: AccountData) => Promise, +] { + const {track} = useAnalytics() + + const store = useStores() + const [isSwitching, setIsSwitching] = useState(false) + const navigation = useNavigation() + + 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] +} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index 15d92f92..bd285c8c 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -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 diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 8590a269..cd2d2d9e 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -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 = + } else if (activeModal?.name === 'switch-account') { + snapPoints = SwitchAccountModal.snapPoints + element = } else { return null } diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx new file mode 100644 index 00000000..51d75e3e --- /dev/null +++ b/src/view/com/modals/SwitchAccount.tsx @@ -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 ( + + + Switch Account + + + {isSwitching ? ( + + + + ) : ( + + + + + + + + {store.me.displayName || store.me.handle} + + + {store.me.handle} + + + + + Sign out + + + + + )} + {store.session.switchableAccounts.map(account => ( + onPressSwitchAccount(account) + } + accessibilityRole="button" + accessibilityLabel={`Switch to ${account.handle}`} + accessibilityHint="Switches the account you are logged in to"> + + + + + + {account.displayName || account.handle} + + + {account.handle} + + + + + ))} + + + ) +} + +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, + }, +}) diff --git a/src/view/com/util/AccountDropdownBtn.tsx b/src/view/com/util/AccountDropdownBtn.tsx new file mode 100644 index 00000000..761fec21 --- /dev/null +++ b/src/view/com/util/AccountDropdownBtn.tsx @@ -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 ( + + + + + + ) +} diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 243cc959..97740135 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -71,6 +71,7 @@ export const NotificationsScreen = withAuthRequired( } }, [store, screen, onPressLoadLatest]), ) + useTabFocusEffect( 'Notifications', React.useCallback( diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 66b2b8fb..2112ec7d 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -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 export const SettingsScreen = withAuthRequired( @@ -61,7 +60,8 @@ export const SettingsScreen = withAuthRequired( const navigation = useNavigation() 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 ( - - - - - - ) -} - const EmailConfirmationNotice = observer( function EmailConfirmationNoticeImpl() { const pal = usePalette('default') diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 8ba74da2..4758c5e0 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -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 ( } onPress={onPressProfile} + onLongPress={onLongPressProfile} accessibilityRole="tab" accessibilityLabel="Profile" accessibilityHint=""