Improve account switcher pending state (#3827)
* Protect against races * Reduce UI jank when switching accounts * Add pending state to selected account * Disable presses while pendingzio/stable
parent
8ba1b10ce0
commit
b86c3b486f
|
@ -16,12 +16,12 @@ export function AccountList({
|
||||||
onSelectAccount,
|
onSelectAccount,
|
||||||
onSelectOther,
|
onSelectOther,
|
||||||
otherLabel,
|
otherLabel,
|
||||||
isSwitchingAccounts,
|
pendingDid,
|
||||||
}: {
|
}: {
|
||||||
onSelectAccount: (account: SessionAccount) => void
|
onSelectAccount: (account: SessionAccount) => void
|
||||||
onSelectOther: () => void
|
onSelectOther: () => void
|
||||||
otherLabel?: string
|
otherLabel?: string
|
||||||
isSwitchingAccounts: boolean
|
pendingDid: string | null
|
||||||
}) {
|
}) {
|
||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
@ -33,6 +33,7 @@ export function AccountList({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
|
pointerEvents={pendingDid ? 'none' : 'auto'}
|
||||||
style={[
|
style={[
|
||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
|
@ -45,6 +46,7 @@ export function AccountList({
|
||||||
account={account}
|
account={account}
|
||||||
onSelect={onSelectAccount}
|
onSelect={onSelectAccount}
|
||||||
isCurrentAccount={account.did === currentAccount?.did}
|
isCurrentAccount={account.did === currentAccount?.did}
|
||||||
|
isPendingAccount={account.did === pendingDid}
|
||||||
/>
|
/>
|
||||||
<View style={[a.border_b, t.atoms.border_contrast_low]} />
|
<View style={[a.border_b, t.atoms.border_contrast_low]} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
@ -52,7 +54,7 @@ export function AccountList({
|
||||||
<Button
|
<Button
|
||||||
testID="chooseAddAccountBtn"
|
testID="chooseAddAccountBtn"
|
||||||
style={[a.flex_1]}
|
style={[a.flex_1]}
|
||||||
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
|
onPress={pendingDid ? undefined : onPressAddAccount}
|
||||||
label={_(msg`Login to account that is not listed`)}>
|
label={_(msg`Login to account that is not listed`)}>
|
||||||
{({hovered, pressed}) => (
|
{({hovered, pressed}) => (
|
||||||
<View
|
<View
|
||||||
|
@ -61,8 +63,7 @@ export function AccountList({
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
{height: 48},
|
{height: 48},
|
||||||
(hovered || pressed || isSwitchingAccounts) &&
|
(hovered || pressed) && t.atoms.bg_contrast_25,
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
]}>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
|
@ -86,10 +87,12 @@ function AccountItem({
|
||||||
account,
|
account,
|
||||||
onSelect,
|
onSelect,
|
||||||
isCurrentAccount,
|
isCurrentAccount,
|
||||||
|
isPendingAccount,
|
||||||
}: {
|
}: {
|
||||||
account: SessionAccount
|
account: SessionAccount
|
||||||
onSelect: (account: SessionAccount) => void
|
onSelect: (account: SessionAccount) => void
|
||||||
isCurrentAccount: boolean
|
isCurrentAccount: boolean
|
||||||
|
isPendingAccount: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
@ -117,7 +120,7 @@ function AccountItem({
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
{height: 48},
|
{height: 48},
|
||||||
(hovered || pressed) && t.atoms.bg_contrast_25,
|
(hovered || pressed || isPendingAccount) && t.atoms.bg_contrast_25,
|
||||||
]}>
|
]}>
|
||||||
<View style={a.p_md}>
|
<View style={a.p_md}>
|
||||||
<UserAvatar avatar={profile?.avatar} size={24} />
|
<UserAvatar avatar={profile?.avatar} size={24} />
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function SwitchAccountDialog({
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {onPressSwitchAccount, isSwitchingAccounts} = useAccountSwitcher()
|
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
|
||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
|
|
||||||
const onSelectAccount = useCallback(
|
const onSelectAccount = useCallback(
|
||||||
|
@ -54,7 +54,7 @@ export function SwitchAccountDialog({
|
||||||
onSelectAccount={onSelectAccount}
|
onSelectAccount={onSelectAccount}
|
||||||
onSelectOther={onPressAddAccount}
|
onSelectOther={onPressAddAccount}
|
||||||
otherLabel={_(msg`Add account`)}
|
otherLabel={_(msg`Add account`)}
|
||||||
isSwitchingAccounts={isSwitchingAccounts}
|
pendingDid={pendingDid}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {logEvent} from '../statsig/statsig'
|
||||||
import {LogEvents} from '../statsig/statsig'
|
import {LogEvents} from '../statsig/statsig'
|
||||||
|
|
||||||
export function useAccountSwitcher() {
|
export function useAccountSwitcher() {
|
||||||
const [isSwitchingAccounts, setIsSwitchingAccounts] = useState(false)
|
const [pendingDid, setPendingDid] = useState<string | null>(null)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const {initSession, clearCurrentAccount} = useSessionApi()
|
const {initSession, clearCurrentAccount} = useSessionApi()
|
||||||
|
@ -24,9 +24,12 @@ export function useAccountSwitcher() {
|
||||||
logContext: LogEvents['account:loggedIn']['logContext'],
|
logContext: LogEvents['account:loggedIn']['logContext'],
|
||||||
) => {
|
) => {
|
||||||
track('Settings:SwitchAccountButtonClicked')
|
track('Settings:SwitchAccountButtonClicked')
|
||||||
|
if (pendingDid) {
|
||||||
|
// The session API isn't resilient to race conditions so let's just ignore this.
|
||||||
|
return
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
setIsSwitchingAccounts(true)
|
setPendingDid(account.did)
|
||||||
if (account.accessJwt) {
|
if (account.accessJwt) {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
// We're switching accounts, which remounts the entire app.
|
// We're switching accounts, which remounts the entire app.
|
||||||
|
@ -57,11 +60,18 @@ export function useAccountSwitcher() {
|
||||||
Toast.show(_(msg`Sorry! We need you to enter your password.`))
|
Toast.show(_(msg`Sorry! We need you to enter your password.`))
|
||||||
}, 100)
|
}, 100)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSwitchingAccounts(false)
|
setPendingDid(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[_, track, clearCurrentAccount, initSession, requestSwitchToAccount],
|
[
|
||||||
|
_,
|
||||||
|
track,
|
||||||
|
clearCurrentAccount,
|
||||||
|
initSession,
|
||||||
|
requestSwitchToAccount,
|
||||||
|
pendingDid,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return {onPressSwitchAccount, isSwitchingAccounts}
|
return {onPressSwitchAccount, pendingDid}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export const ChooseAccountForm = ({
|
||||||
onSelectAccount: (account?: SessionAccount) => void
|
onSelectAccount: (account?: SessionAccount) => void
|
||||||
onPressBack: () => void
|
onPressBack: () => void
|
||||||
}) => {
|
}) => {
|
||||||
const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
|
const [pendingDid, setPendingDid] = React.useState<string | null>(null)
|
||||||
const {track, screen} = useAnalytics()
|
const {track, screen} = useAnalytics()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
@ -35,13 +35,17 @@ export const ChooseAccountForm = ({
|
||||||
|
|
||||||
const onSelect = React.useCallback(
|
const onSelect = React.useCallback(
|
||||||
async (account: SessionAccount) => {
|
async (account: SessionAccount) => {
|
||||||
|
if (pendingDid) {
|
||||||
|
// The session API isn't resilient to race conditions so let's just ignore this.
|
||||||
|
return
|
||||||
|
}
|
||||||
if (account.accessJwt) {
|
if (account.accessJwt) {
|
||||||
if (account.did === currentAccount?.did) {
|
if (account.did === currentAccount?.did) {
|
||||||
setShowLoggedOut(false)
|
setShowLoggedOut(false)
|
||||||
Toast.show(_(msg`Already signed in as @${account.handle}`))
|
Toast.show(_(msg`Already signed in as @${account.handle}`))
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
setIsSwitchingAccounts(true)
|
setPendingDid(account.did)
|
||||||
await initSession(account)
|
await initSession(account)
|
||||||
logEvent('account:loggedIn', {
|
logEvent('account:loggedIn', {
|
||||||
logContext: 'ChooseAccountForm',
|
logContext: 'ChooseAccountForm',
|
||||||
|
@ -57,14 +61,22 @@ export const ChooseAccountForm = ({
|
||||||
})
|
})
|
||||||
onSelectAccount(account)
|
onSelectAccount(account)
|
||||||
} finally {
|
} finally {
|
||||||
setIsSwitchingAccounts(false)
|
setPendingDid(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onSelectAccount(account)
|
onSelectAccount(account)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[currentAccount, track, initSession, onSelectAccount, setShowLoggedOut, _],
|
[
|
||||||
|
currentAccount,
|
||||||
|
track,
|
||||||
|
initSession,
|
||||||
|
pendingDid,
|
||||||
|
onSelectAccount,
|
||||||
|
setShowLoggedOut,
|
||||||
|
_,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -78,7 +90,7 @@ export const ChooseAccountForm = ({
|
||||||
<AccountList
|
<AccountList
|
||||||
onSelectAccount={onSelect}
|
onSelectAccount={onSelect}
|
||||||
onSelectOther={() => onSelectAccount()}
|
onSelectOther={() => onSelectAccount()}
|
||||||
isSwitchingAccounts={isSwitchingAccounts}
|
pendingDid={pendingDid}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={[a.flex_row]}>
|
<View style={[a.flex_row]}>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
|
||||||
Linking,
|
Linking,
|
||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
Pressable,
|
||||||
|
@ -63,6 +62,7 @@ import * as Toast from 'view/com/util/Toast'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {ScrollView} from 'view/com/util/Views'
|
import {ScrollView} from 'view/com/util/Views'
|
||||||
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
import {useDmServiceUrlStorage} from '#/screens/Messages/Temp/useDmServiceUrlStorage'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
|
@ -72,11 +72,11 @@ import {ExportCarDialog} from './ExportCarDialog'
|
||||||
|
|
||||||
function SettingsAccountCard({
|
function SettingsAccountCard({
|
||||||
account,
|
account,
|
||||||
isSwitchingAccounts,
|
pendingDid,
|
||||||
onPressSwitchAccount,
|
onPressSwitchAccount,
|
||||||
}: {
|
}: {
|
||||||
account: SessionAccount
|
account: SessionAccount
|
||||||
isSwitchingAccounts: boolean
|
pendingDid: string | null
|
||||||
onPressSwitchAccount: (
|
onPressSwitchAccount: (
|
||||||
account: SessionAccount,
|
account: SessionAccount,
|
||||||
logContext: 'Settings',
|
logContext: 'Settings',
|
||||||
|
@ -84,13 +84,19 @@ function SettingsAccountCard({
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {logout} = useSessionApi()
|
const {logout} = useSessionApi()
|
||||||
const {data: profile} = useProfileQuery({did: account.did})
|
const {data: profile} = useProfileQuery({did: account.did})
|
||||||
const isCurrentAccount = account.did === currentAccount?.did
|
const isCurrentAccount = account.did === currentAccount?.did
|
||||||
|
|
||||||
const contents = (
|
const contents = (
|
||||||
<View style={[pal.view, styles.linkCard]}>
|
<View
|
||||||
|
style={[
|
||||||
|
pal.view,
|
||||||
|
styles.linkCard,
|
||||||
|
account.did === pendingDid && t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
<View style={styles.avi}>
|
<View style={styles.avi}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
size={40}
|
size={40}
|
||||||
|
@ -122,7 +128,8 @@ function SettingsAccountCard({
|
||||||
}}
|
}}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Sign out`)}
|
accessibilityLabel={_(msg`Sign out`)}
|
||||||
accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}>
|
accessibilityHint={`Signs ${profile?.displayName} out of Bluesky`}
|
||||||
|
activeOpacity={0.8}>
|
||||||
<Text type="lg" style={pal.link}>
|
<Text type="lg" style={pal.link}>
|
||||||
<Trans>Sign out</Trans>
|
<Trans>Sign out</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -148,13 +155,12 @@ function SettingsAccountCard({
|
||||||
testID={`switchToAccountBtn-${account.handle}`}
|
testID={`switchToAccountBtn-${account.handle}`}
|
||||||
key={account.did}
|
key={account.did}
|
||||||
onPress={
|
onPress={
|
||||||
isSwitchingAccounts
|
pendingDid ? undefined : () => onPressSwitchAccount(account, 'Settings')
|
||||||
? undefined
|
|
||||||
: () => onPressSwitchAccount(account, 'Settings')
|
|
||||||
}
|
}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Switch to ${account.handle}`)}
|
accessibilityLabel={_(msg`Switch to ${account.handle}`)}
|
||||||
accessibilityHint={_(msg`Switches the account you are logged in to`)}>
|
accessibilityHint={_(msg`Switches the account you are logged in to`)}
|
||||||
|
activeOpacity={0.8}>
|
||||||
{contents}
|
{contents}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)
|
)
|
||||||
|
@ -181,7 +187,8 @@ export function SettingsScreen({}: Props) {
|
||||||
const closeAllActiveElements = useCloseAllActiveElements()
|
const closeAllActiveElements = useCloseAllActiveElements()
|
||||||
const exportCarControl = useDialogControl()
|
const exportCarControl = useDialogControl()
|
||||||
const birthdayControl = useDialogControl()
|
const birthdayControl = useDialogControl()
|
||||||
const {isSwitchingAccounts, onPressSwitchAccount} = useAccountSwitcher()
|
const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
|
||||||
|
const isSwitchingAccounts = !!pendingDid
|
||||||
|
|
||||||
// TODO: TEMP REMOVE WHEN CLOPS ARE RELEASED
|
// TODO: TEMP REMOVE WHEN CLOPS ARE RELEASED
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
|
@ -382,27 +389,24 @@ export function SettingsScreen({}: Props) {
|
||||||
<View style={styles.spacer20} />
|
<View style={styles.spacer20} />
|
||||||
|
|
||||||
{!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
|
{!currentAccount.emailConfirmed && <EmailConfirmationNotice />}
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
<View style={[s.flexRow, styles.heading]}>
|
<View style={[s.flexRow, styles.heading]}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
<Trans>Signed in as</Trans>
|
<Trans>Signed in as</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<View style={s.flex1} />
|
<View style={s.flex1} />
|
||||||
</View>
|
</View>
|
||||||
|
<View pointerEvents={pendingDid ? 'none' : 'auto'}>
|
||||||
{isSwitchingAccounts ? (
|
|
||||||
<View style={[pal.view, styles.linkCard]}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<SettingsAccountCard
|
<SettingsAccountCard
|
||||||
account={currentAccount!}
|
account={currentAccount}
|
||||||
onPressSwitchAccount={onPressSwitchAccount}
|
onPressSwitchAccount={onPressSwitchAccount}
|
||||||
isSwitchingAccounts={isSwitchingAccounts}
|
pendingDid={pendingDid}
|
||||||
/>
|
/>
|
||||||
)}
|
</View>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<View pointerEvents={pendingDid ? 'none' : 'auto'}>
|
||||||
{accounts
|
{accounts
|
||||||
.filter(a => a.did !== currentAccount?.did)
|
.filter(a => a.did !== currentAccount?.did)
|
||||||
.map(account => (
|
.map(account => (
|
||||||
|
@ -410,17 +414,13 @@ export function SettingsScreen({}: Props) {
|
||||||
key={account.did}
|
key={account.did}
|
||||||
account={account}
|
account={account}
|
||||||
onPressSwitchAccount={onPressSwitchAccount}
|
onPressSwitchAccount={onPressSwitchAccount}
|
||||||
isSwitchingAccounts={isSwitchingAccounts}
|
pendingDid={pendingDid}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID="switchToNewAccountBtn"
|
testID="switchToNewAccountBtn"
|
||||||
style={[
|
style={[styles.linkCard, pal.view]}
|
||||||
styles.linkCard,
|
|
||||||
pal.view,
|
|
||||||
isSwitchingAccounts && styles.dimmed,
|
|
||||||
]}
|
|
||||||
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
|
onPress={isSwitchingAccounts ? undefined : onPressAddAccount}
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={_(msg`Add account`)}
|
accessibilityLabel={_(msg`Add account`)}
|
||||||
|
@ -435,6 +435,7 @@ export function SettingsScreen({}: Props) {
|
||||||
<Trans>Add account</Trans>
|
<Trans>Add account</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.spacer20} />
|
<View style={styles.spacer20} />
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue