Use ALF for the account quick switch dialog (#3327)

* Use ALF for account quick switch

* clean up modal type

* add haptics to dialog opening

* move account list to it's own component and share

* make tick slightly darker
This commit is contained in:
Samuel Newman 2024-04-04 03:18:14 +01:00 committed by GitHub
parent 8cdd8394df
commit 712768dd8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 449 additions and 517 deletions

View file

@ -24,7 +24,6 @@ import * as LinkWarningModal from './LinkWarning'
import * as ListAddUserModal from './ListAddRemoveUsers'
import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel'
import * as SwitchAccountModal from './SwitchAccount'
import * as ThreadgateModal from './Threadgate'
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
import * as VerifyEmailModal from './VerifyEmail'
@ -114,9 +113,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'change-password') {
snapPoints = ChangePasswordModal.snapPoints
element = <ChangePasswordModal.Component />
} else if (activeModal?.name === 'switch-account') {
snapPoints = SwitchAccountModal.snapPoints
element = <SwitchAccountModal.Component />
} else if (activeModal?.name === 'link-warning') {
snapPoints = LinkWarningModal.snapPoints
element = <LinkWarningModal.Component {...activeModal} />

View file

@ -1,169 +0,0 @@
import React from 'react'
import {
ActivityIndicator,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native'
import {BottomSheetScrollView} from '@discord/bottom-sheet/src'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileQuery} from '#/state/queries/profile'
import {SessionAccount, useSession, useSessionApi} from '#/state/session'
import {useCloseAllActiveElements} from '#/state/util'
import {useAnalytics} from 'lib/analytics/analytics'
import {Haptics} from 'lib/haptics'
import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
import {usePalette} from 'lib/hooks/usePalette'
import {makeProfileLink} from 'lib/routes/links'
import {s} from 'lib/styles'
import {AccountDropdownBtn} from '../util/AccountDropdownBtn'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
export const snapPoints = ['40%', '90%']
function SwitchAccountCard({account}: {account: SessionAccount}) {
const pal = usePalette('default')
const {_} = useLingui()
const {track} = useAnalytics()
const {isSwitchingAccounts, currentAccount} = useSession()
const {logout} = useSessionApi()
const {data: profile} = useProfileQuery({did: account.did})
const isCurrentAccount = account.did === currentAccount?.did
const {onPressSwitchAccount} = useAccountSwitcher()
const closeAllActiveElements = useCloseAllActiveElements()
const onPressSignout = React.useCallback(() => {
track('Settings:SignOutButtonClicked')
closeAllActiveElements()
// needs to be in timeout or the modal re-opens
setTimeout(() => logout('SwitchAccount'), 0)
}, [track, logout, closeAllActiveElements])
const contents = (
<View style={[pal.view, styles.linkCard]}>
<View style={styles.avi}>
<UserAvatar
size={40}
avatar={profile?.avatar}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<View style={[s.flex1]}>
<Text type="md-bold" style={pal.text} numberOfLines={1}>
{profile?.displayName || account?.handle}
</Text>
<Text type="sm" style={pal.textLight} numberOfLines={1}>
{account?.handle}
</Text>
</View>
{isCurrentAccount ? (
<TouchableOpacity
testID="signOutBtn"
onPress={isSwitchingAccounts ? undefined : onPressSignout}
accessibilityRole="button"
accessibilityLabel={_(msg`Sign out`)}
accessibilityHint={_(
msg`Signs ${profile?.displayName} out of Bluesky`,
)}>
<Text type="lg" style={pal.link}>
<Trans>Sign out</Trans>
</Text>
</TouchableOpacity>
) : (
<AccountDropdownBtn account={account} />
)}
</View>
)
return isCurrentAccount ? (
<Link
href={makeProfileLink({
did: currentAccount.did,
handle: currentAccount.handle,
})}
title={_(msg`Your profile`)}
noFeedback>
{contents}
</Link>
) : (
<TouchableOpacity
testID={`switchToAccountBtn-${account.handle}`}
key={account.did}
style={[isSwitchingAccounts && styles.dimmed]}
onPress={
isSwitchingAccounts
? undefined
: () => onPressSwitchAccount(account, 'SwitchAccount')
}
accessibilityRole="button"
accessibilityLabel={_(msg`Switch to ${account.handle}`)}
accessibilityHint={_(msg`Switches the account you are logged in to`)}>
{contents}
</TouchableOpacity>
)
}
export function Component({}: {}) {
const pal = usePalette('default')
const {isSwitchingAccounts, currentAccount, accounts} = useSession()
React.useEffect(() => {
Haptics.default()
})
return (
<BottomSheetScrollView
style={[styles.container, pal.view]}
contentContainerStyle={[styles.innerContainer, pal.view]}>
<Text type="title-xl" style={[styles.title, pal.text]}>
<Trans>Switch Account</Trans>
</Text>
{isSwitchingAccounts || !currentAccount ? (
<View style={[pal.view, styles.linkCard]}>
<ActivityIndicator />
</View>
) : (
<SwitchAccountCard account={currentAccount} />
)}
{accounts
.filter(a => a.did !== currentAccount?.did)
.map(account => (
<SwitchAccountCard key={account.did} account={account} />
))}
</BottomSheetScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
innerContainer: {
paddingBottom: 40,
},
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,
},
})

View file

@ -1,47 +1,49 @@
import React, {ComponentProps} from 'react'
import {GestureResponderEvent, TouchableOpacity, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {StackActions} from '@react-navigation/native'
import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {Text} from 'view/com/util/text/Text'
import {useAnalytics} from 'lib/analytics/analytics'
import {clamp} from 'lib/numbers'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
import {StackActions} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {Haptics} from '#/lib/haptics'
import {useDedupe} from '#/lib/hooks/useDedupe'
import {useMinimalShellMode} from '#/lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from '#/lib/hooks/useNavigationTabState'
import {usePalette} from '#/lib/hooks/usePalette'
import {
BellIcon,
BellIconSolid,
HashtagIcon,
HomeIcon,
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
HashtagIcon,
BellIcon,
BellIconSolid,
} from 'lib/icons'
import {usePalette} from 'lib/hooks/usePalette'
import {getTabState, TabState} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {UserAvatar} from 'view/com/util/UserAvatar'
import {useLingui} from '@lingui/react'
import {msg, Trans} from '@lingui/macro'
import {useModalControls} from '#/state/modals'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
} from '#/lib/icons'
import {clamp} from '#/lib/numbers'
import {getTabState, TabState} from '#/lib/routes/helpers'
import {s} from '#/lib/styles'
import {emitSoftReset} from '#/state/events'
import {useSession} from '#/state/session'
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
import {useProfileQuery} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useShellLayout} from '#/state/shell/shell-layout'
import {useCloseAllActiveElements} from '#/state/util'
import {Button} from '#/view/com/util/forms/Button'
import {s} from 'lib/styles'
import {Text} from '#/view/com/util/text/Text'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
import {useDedupe} from 'lib/hooks/useDedupe'
import {useDialogControl} from '#/components/Dialog'
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
import {styles} from './BottomBarStyles'
type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
export function BottomBar({navigation}: BottomTabBarProps) {
const {openModal} = useModalControls()
const {hasSession, currentAccount} = useSession()
const pal = usePalette('default')
const {_} = useLingui()
@ -56,6 +58,7 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const {requestSwitchToAccount} = useLoggedOutViewControls()
const closeAllActiveElements = useCloseAllActiveElements()
const dedupe = useDedupe()
const accountSwitchControl = useDialogControl()
const showSignIn = React.useCallback(() => {
closeAllActiveElements()
@ -99,204 +102,213 @@ export function BottomBar({navigation}: BottomTabBarProps) {
const onPressProfile = React.useCallback(() => {
onPressTab('MyProfile')
}, [onPressTab])
const onLongPressProfile = React.useCallback(() => {
openModal({name: 'switch-account'})
}, [openModal])
Haptics.default()
accountSwitchControl.open()
}, [accountSwitchControl])
return (
<Animated.View
style={[
styles.bottomBar,
pal.view,
pal.border,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}
onLayout={e => {
footerHeight.value = e.nativeEvent.layout.height
}}>
{hasSession ? (
<>
<Btn
testID="bottomBarHomeBtn"
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarSearchBtn"
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarFeedsBtn"
icon={
isAtFeeds ? (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={4}
/>
) : (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={2.25}
/>
)
}
onPress={onPressFeeds}
accessibilityRole="tab"
accessibilityLabel={_(msg`Feeds`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarNotificationsBtn"
icon={
isAtNotifications ? (
<BellIconSolid
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
) : (
<BellIcon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}
onPress={onPressNotifications}
notificationCount={numUnreadNotifications}
accessible={true}
accessibilityRole="tab"
accessibilityLabel={_(msg`Notifications`)}
accessibilityHint={
numUnreadNotifications === ''
? ''
: `${numUnreadNotifications} unread`
}
/>
<Btn
testID="bottomBarProfileBtn"
icon={
<View style={styles.ctrlIconSizingWrapper}>
{isAtMyProfile ? (
<View
style={[
styles.ctrlIcon,
pal.text,
styles.profileIcon,
styles.onProfile,
{borderColor: pal.text.color},
]}>
<UserAvatar
avatar={profile?.avatar}
size={27}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
<>
<SwitchAccountDialog control={accountSwitchControl} />
<Animated.View
style={[
styles.bottomBar,
pal.view,
pal.border,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}
onLayout={e => {
footerHeight.value = e.nativeEvent.layout.height
}}>
{hasSession ? (
<>
<Btn
testID="bottomBarHomeBtn"
icon={
isAtHome ? (
<HomeIconSolid
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
) : (
<View style={[styles.ctrlIcon, pal.text, styles.profileIcon]}>
<UserAvatar
avatar={profile?.avatar}
size={28}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
)}
<HomeIcon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}
onPress={onPressHome}
accessibilityRole="tab"
accessibilityLabel={_(msg`Home`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarSearchBtn"
icon={
isAtSearch ? (
<MagnifyingGlassIcon2Solid
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
) : (
<MagnifyingGlassIcon2
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}
onPress={onPressSearch}
accessibilityRole="search"
accessibilityLabel={_(msg`Search`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarFeedsBtn"
icon={
isAtFeeds ? (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={4}
/>
) : (
<HashtagIcon
size={24}
style={[styles.ctrlIcon, pal.text, styles.feedsIcon]}
strokeWidth={2.25}
/>
)
}
onPress={onPressFeeds}
accessibilityRole="tab"
accessibilityLabel={_(msg`Feeds`)}
accessibilityHint=""
/>
<Btn
testID="bottomBarNotificationsBtn"
icon={
isAtNotifications ? (
<BellIconSolid
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
) : (
<BellIcon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}
onPress={onPressNotifications}
notificationCount={numUnreadNotifications}
accessible={true}
accessibilityRole="tab"
accessibilityLabel={_(msg`Notifications`)}
accessibilityHint={
numUnreadNotifications === ''
? ''
: `${numUnreadNotifications} unread`
}
/>
<Btn
testID="bottomBarProfileBtn"
icon={
<View style={styles.ctrlIconSizingWrapper}>
{isAtMyProfile ? (
<View
style={[
styles.ctrlIcon,
pal.text,
styles.profileIcon,
styles.onProfile,
{borderColor: pal.text.color},
]}>
<UserAvatar
avatar={profile?.avatar}
size={27}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
) : (
<View
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}>
<UserAvatar
avatar={profile?.avatar}
size={28}
// See https://github.com/bluesky-social/social-app/pull/1801:
usePlainRNImage={true}
type={profile?.associated?.labeler ? 'labeler' : 'user'}
/>
</View>
)}
</View>
}
onPress={onPressProfile}
onLongPress={onLongPressProfile}
accessibilityRole="tab"
accessibilityLabel={_(msg`Profile`)}
accessibilityHint=""
/>
</>
) : (
<>
<View
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 14,
paddingBottom: 2,
paddingLeft: 14,
paddingRight: 6,
gap: 8,
}}>
<View
style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Logo width={28} />
<View style={{paddingTop: 4}}>
<Logotype width={80} fill={pal.text.color} />
</View>
</View>
}
onPress={onPressProfile}
onLongPress={onLongPressProfile}
accessibilityRole="tab"
accessibilityLabel={_(msg`Profile`)}
accessibilityHint=""
/>
</>
) : (
<>
<View
style={{
width: '100%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 14,
paddingBottom: 2,
paddingLeft: 14,
paddingRight: 6,
gap: 8,
}}>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Logo width={28} />
<View style={{paddingTop: 4}}>
<Logotype width={80} fill={pal.text.color} />
<View
style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
<Button
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
<Trans>Sign up</Trans>
</Text>
</Button>
<Button
type="default"
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
<Trans>Sign in</Trans>
</Text>
</Button>
</View>
</View>
<View style={{flexDirection: 'row', alignItems: 'center', gap: 4}}>
<Button
onPress={showCreateAccount}
accessibilityHint={_(msg`Sign up`)}
accessibilityLabel={_(msg`Sign up`)}>
<Text type="md" style={[{color: 'white'}, s.bold]}>
<Trans>Sign up</Trans>
</Text>
</Button>
<Button
type="default"
onPress={showSignIn}
accessibilityHint={_(msg`Sign in`)}
accessibilityLabel={_(msg`Sign in`)}>
<Text type="md" style={[pal.text, s.bold]}>
<Trans>Sign in</Trans>
</Text>
</Button>
</View>
</View>
</>
)}
</Animated.View>
</>
)}
</Animated.View>
</>
)
}