diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/components/AccountList.tsx | 141 | ||||
-rw-r--r-- | src/components/dialogs/BirthDateSettings.tsx | 20 | ||||
-rw-r--r-- | src/components/dialogs/SwitchAccount.tsx | 61 | ||||
-rw-r--r-- | src/screens/Login/ChooseAccountForm.tsx | 118 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 11 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 4 | ||||
-rw-r--r-- | src/view/com/modals/SwitchAccount.tsx | 169 | ||||
-rw-r--r-- | src/view/shell/bottom-bar/BottomBar.tsx | 440 |
8 files changed, 448 insertions, 516 deletions
diff --git a/src/components/AccountList.tsx b/src/components/AccountList.tsx new file mode 100644 index 000000000..169e7b84f --- /dev/null +++ b/src/components/AccountList.tsx @@ -0,0 +1,141 @@ +import React, {useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useProfileQuery} from '#/state/queries/profile' +import {type SessionAccount, useSession} from '#/state/session' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' +import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' +import {Button} from './Button' +import {Text} from './Typography' + +export function AccountList({ + onSelectAccount, + onSelectOther, + otherLabel, +}: { + onSelectAccount: (account: SessionAccount) => void + onSelectOther: () => void + otherLabel?: string +}) { + const {isSwitchingAccounts, currentAccount, accounts} = useSession() + const t = useTheme() + const {_} = useLingui() + + const onPressAddAccount = useCallback(() => { + onSelectOther() + }, [onSelectOther]) + + return ( + <View + style={[ + a.rounded_md, + a.overflow_hidden, + a.border, + t.atoms.border_contrast_low, + ]}> + {accounts.map(account => ( + <React.Fragment key={account.did}> + <AccountItem + account={account} + onSelect={onSelectAccount} + isCurrentAccount={account.did === currentAccount?.did} + /> + <View style={[a.border_b, t.atoms.border_contrast_low]} /> + </React.Fragment> + ))} + <Button + testID="chooseAddAccountBtn" + style={[a.flex_1]} + onPress={isSwitchingAccounts ? undefined : onPressAddAccount} + label={_(msg`Login to account that is not listed`)}> + {({hovered, pressed}) => ( + <View + style={[ + a.flex_1, + a.flex_row, + a.align_center, + {height: 48}, + (hovered || pressed || isSwitchingAccounts) && + t.atoms.bg_contrast_25, + ]}> + <Text + style={[ + a.align_baseline, + a.flex_1, + a.flex_row, + a.py_sm, + {paddingLeft: 48}, + ]}> + {otherLabel ?? <Trans>Other account</Trans>} + </Text> + <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> + </View> + )} + </Button> + </View> + ) +} + +function AccountItem({ + account, + onSelect, + isCurrentAccount, +}: { + account: SessionAccount + onSelect: (account: SessionAccount) => void + isCurrentAccount: boolean +}) { + const t = useTheme() + const {_} = useLingui() + const {data: profile} = useProfileQuery({did: account.did}) + + const onPress = React.useCallback(() => { + onSelect(account) + }, [account, onSelect]) + + return ( + <Button + testID={`chooseAccountBtn-${account.handle}`} + key={account.did} + style={[a.flex_1]} + onPress={onPress} + label={ + isCurrentAccount + ? _(msg`Continue as ${account.handle} (currently signed in)`) + : _(msg`Sign in as ${account.handle}`) + }> + {({hovered, pressed}) => ( + <View + style={[ + a.flex_1, + a.flex_row, + a.align_center, + {height: 48}, + (hovered || pressed) && t.atoms.bg_contrast_25, + ]}> + <View style={a.p_md}> + <UserAvatar avatar={profile?.avatar} size={24} /> + </View> + <Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}> + <Text style={[a.font_bold]}> + {profile?.displayName || account.handle}{' '} + </Text> + <Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text> + </Text> + {isCurrentAccount ? ( + <Check + size="sm" + style={[{color: t.palette.positive_600}, a.mr_md]} + /> + ) : ( + <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> + )} + </View> + )} + </Button> + ) +} diff --git a/src/components/dialogs/BirthDateSettings.tsx b/src/components/dialogs/BirthDateSettings.tsx index 4a3e96e56..d831c6002 100644 --- a/src/components/dialogs/BirthDateSettings.tsx +++ b/src/components/dialogs/BirthDateSettings.tsx @@ -1,23 +1,23 @@ import React from 'react' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' -import * as Dialog from '#/components/Dialog' -import {Text} from '../Typography' -import {DateInput} from '#/view/com/util/forms/DateInput' +import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' +import {isIOS, isWeb} from '#/platform/detection' import { usePreferencesQuery, - usePreferencesSetBirthDateMutation, UsePreferencesQueryResponse, + usePreferencesSetBirthDateMutation, } from '#/state/queries/preferences' -import {Button, ButtonIcon, ButtonText} from '../Button' -import {atoms as a, useTheme} from '#/alf' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import {cleanError} from '#/lib/strings/errors' -import {isIOS, isWeb} from '#/platform/detection' +import {DateInput} from '#/view/com/util/forms/DateInput' +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' +import {Button, ButtonIcon, ButtonText} from '../Button' +import {Text} from '../Typography' export function BirthDateSettingsDialog({ control, diff --git a/src/components/dialogs/SwitchAccount.tsx b/src/components/dialogs/SwitchAccount.tsx new file mode 100644 index 000000000..645113d4a --- /dev/null +++ b/src/components/dialogs/SwitchAccount.tsx @@ -0,0 +1,61 @@ +import React, {useCallback} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' +import {type SessionAccount, useSession} from '#/state/session' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {useCloseAllActiveElements} from '#/state/util' +import {atoms as a} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {AccountList} from '../AccountList' +import {Text} from '../Typography' + +export function SwitchAccountDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const {_} = useLingui() + const {currentAccount} = useSession() + const {onPressSwitchAccount} = useAccountSwitcher() + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeAllActiveElements = useCloseAllActiveElements() + + const onSelectAccount = useCallback( + (account: SessionAccount) => { + if (account.did === currentAccount?.did) { + control.close() + } else { + onPressSwitchAccount(account, 'SwitchAccount') + } + }, + [currentAccount, control, onPressSwitchAccount], + ) + + const onPressAddAccount = useCallback(() => { + setShowLoggedOut(true) + closeAllActiveElements() + }, [setShowLoggedOut, closeAllActiveElements]) + + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + + <Dialog.ScrollableInner label={_(msg`Switch Account`)}> + <View style={[a.gap_lg]}> + <Text style={[a.text_2xl, a.font_bold]}> + <Trans>Switch Account</Trans> + </Text> + + <AccountList + onSelectAccount={onSelectAccount} + onSelectOther={onPressAddAccount} + otherLabel={_(msg`Add account`)} + /> + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index d0d4c784d..15c06dbe8 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -5,76 +5,15 @@ import {useLingui} from '@lingui/react' import {useAnalytics} from '#/lib/analytics/analytics' import {logEvent} from '#/lib/statsig/statsig' -import {colors} from '#/lib/styles' -import {useProfileQuery} from '#/state/queries/profile' import {SessionAccount, useSession, useSessionApi} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import * as Toast from '#/view/com/util/Toast' -import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a} from '#/alf' +import {AccountList} from '#/components/AccountList' import {Button} from '#/components/Button' import * as TextField from '#/components/forms/TextField' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' -import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' -import {Text} from '#/components/Typography' import {FormContainer} from './FormContainer' -function AccountItem({ - account, - onSelect, - isCurrentAccount, -}: { - account: SessionAccount - onSelect: (account: SessionAccount) => void - isCurrentAccount: boolean -}) { - const t = useTheme() - const {_} = useLingui() - const {data: profile} = useProfileQuery({did: account.did}) - - const onPress = React.useCallback(() => { - onSelect(account) - }, [account, onSelect]) - - return ( - <Button - testID={`chooseAccountBtn-${account.handle}`} - key={account.did} - style={[a.flex_1]} - onPress={onPress} - label={ - isCurrentAccount - ? _(msg`Continue as ${account.handle} (currently signed in)`) - : _(msg`Sign in as ${account.handle}`) - }> - {({hovered, pressed}) => ( - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - {height: 48}, - (hovered || pressed) && t.atoms.bg_contrast_25, - ]}> - <View style={a.p_md}> - <UserAvatar avatar={profile?.avatar} size={24} /> - </View> - <Text style={[a.align_baseline, a.flex_1, a.flex_row, a.py_sm]}> - <Text style={[a.font_bold]}> - {profile?.displayName || account.handle}{' '} - </Text> - <Text style={[t.atoms.text_contrast_medium]}>{account.handle}</Text> - </Text> - {isCurrentAccount ? ( - <Check size="sm" style={[{color: colors.green3}, a.mr_md]} /> - ) : ( - <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> - )} - </View> - )} - </Button> - ) -} export const ChooseAccountForm = ({ onSelectAccount, onPressBack, @@ -84,8 +23,7 @@ export const ChooseAccountForm = ({ }) => { const {track, screen} = useAnalytics() const {_} = useLingui() - const t = useTheme() - const {accounts, currentAccount} = useSession() + const {currentAccount} = useSession() const {initSession} = useSessionApi() const {setShowLoggedOut} = useLoggedOutViewControls() @@ -125,52 +63,10 @@ export const ChooseAccountForm = ({ <TextField.Label> <Trans>Sign in as...</Trans> </TextField.Label> - <View - style={[ - a.rounded_md, - a.overflow_hidden, - a.border, - t.atoms.border_contrast_low, - ]}> - {accounts.map(account => ( - <React.Fragment key={account.did}> - <AccountItem - account={account} - onSelect={onSelect} - isCurrentAccount={account.did === currentAccount?.did} - /> - <View style={[a.border_b, t.atoms.border_contrast_low]} /> - </React.Fragment> - ))} - <Button - testID="chooseNewAccountBtn" - style={[a.flex_1]} - onPress={() => onSelectAccount(undefined)} - label={_(msg`Login to account that is not listed`)}> - {({hovered, pressed}) => ( - <View - style={[ - a.flex_1, - a.flex_row, - a.align_center, - {height: 48}, - (hovered || pressed) && t.atoms.bg_contrast_25, - ]}> - <Text - style={[ - a.align_baseline, - a.flex_1, - a.flex_row, - a.py_sm, - {paddingLeft: 48}, - ]}> - <Trans>Other account</Trans> - </Text> - <Chevron size="sm" style={[t.atoms.text, a.mr_md]} /> - </View> - )} - </Button> - </View> + <AccountList + onSelectAccount={onSelect} + onSelectOther={() => onSelectAccount()} + /> </View> <View style={[a.flex_row]}> <Button diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 524dcb1ba..aae4fc52f 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -1,11 +1,11 @@ import React from 'react' -import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {Image as RNImage} from 'react-native-image-crop-picker' +import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' -import {ImageModel} from '#/state/models/media/image' -import {GalleryModel} from '#/state/models/media/gallery' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {EmbedPlayerSource} from '#/lib/strings/embed-player' +import {GalleryModel} from '#/state/models/media/gallery' +import {ImageModel} from '#/state/models/media/image' import {ThreadgateSetting} from '../queries/threadgate' export interface EditProfileModal { @@ -118,10 +118,6 @@ export interface ChangePasswordModal { name: 'change-password' } -export interface SwitchAccountModal { - name: 'switch-account' -} - export interface LinkWarningModal { name: 'link-warning' text: string @@ -148,7 +144,6 @@ export type Modal = | VerifyEmailModal | ChangeEmailModal | ChangePasswordModal - | SwitchAccountModal // Curation | ContentLanguagesSettingsModal diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index af86f13a3..85ffccf12 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -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} /> diff --git a/src/view/com/modals/SwitchAccount.tsx b/src/view/com/modals/SwitchAccount.tsx deleted file mode 100644 index 03bef719e..000000000 --- a/src/view/com/modals/SwitchAccount.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx index 8a19a0b4f..f41631a96 100644 --- a/src/view/shell/bottom-bar/BottomBar.tsx +++ b/src/view/shell/bottom-bar/BottomBar.tsx @@ -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> - )} - </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} /> + <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> - </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> + <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> + <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> - </> - )} - </Animated.View> + </> + )} + </Animated.View> + </> ) } |