From 7b5e8ae5ce674c8946b70ee7c8f01f4b8ab96598 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 8 Nov 2024 19:04:18 +0000 Subject: [Settings] Improved account switcher (#6131) * move out avatarstack to own file * improved settings switch * prefix with @ * fix types * up chevron * respect reduced motion setting * respect reduced motion in other place --- .../icons/personPlus_stroke2_corner2_rounded.svg | 1 + src/components/AvatarStack.tsx | 76 ++++++ src/components/icons/Person.tsx | 4 + src/screens/Settings/Settings.tsx | 296 +++++++++++++-------- 4 files changed, 273 insertions(+), 104 deletions(-) create mode 100644 assets/icons/personPlus_stroke2_corner2_rounded.svg create mode 100644 src/components/AvatarStack.tsx diff --git a/assets/icons/personPlus_stroke2_corner2_rounded.svg b/assets/icons/personPlus_stroke2_corner2_rounded.svg new file mode 100644 index 000000000..148c29e0f --- /dev/null +++ b/assets/icons/personPlus_stroke2_corner2_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/AvatarStack.tsx b/src/components/AvatarStack.tsx new file mode 100644 index 000000000..5f790fb67 --- /dev/null +++ b/src/components/AvatarStack.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import {View} from 'react-native' +import {moderateProfile} from '@atproto/api' + +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfilesQuery} from '#/state/queries/profile' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' + +export function AvatarStack({ + profiles, + size = 26, +}: { + profiles: string[] + size?: number +}) { + const halfSize = size / 2 + const {data, error} = useProfilesQuery({handles: profiles}) + const t = useTheme() + const moderationOpts = useModerationOpts() + + if (error) { + console.error(error) + return null + } + + const isPending = !data || !moderationOpts + + const items = isPending + ? Array.from({length: profiles.length}).map((_, i) => ({ + key: i, + profile: null, + moderation: null, + })) + : data.profiles.map(item => ({ + key: item.did, + profile: item, + moderation: moderateProfile(item, moderationOpts), + })) + + return ( + + {items.map((item, i) => ( + + {item.profile && ( + + )} + + ))} + + ) +} diff --git a/src/components/icons/Person.tsx b/src/components/icons/Person.tsx index 31d7078d9..8428fcce1 100644 --- a/src/components/icons/Person.tsx +++ b/src/components/icons/Person.tsx @@ -24,6 +24,10 @@ export const PersonPlus_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H15a3 3 0 1 1 0-6c0-.824.332-1.571.87-2.113C14.739 12.32 13.435 12 12 12Zm6 2a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', }) +export const PersonPlus_Stroke2_Corner2_Rounded = createSinglePathSVG({ + path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM12 14c-2.95 0-5.163 1.733-6.08 4.21a.47.47 0 0 0 .09.493.9.9 0 0 0 .687.297H11a1 1 0 1 1 0 2H6.697a2.9 2.9 0 0 1-2.219-1.011 2.46 2.46 0 0 1-.433-2.473C5.235 14.296 8.168 12 12 12c.787 0 1.54.097 2.252.282a1 1 0 1 1-.504 1.936A7 7 0 0 0 12 14Zm6 0a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z', +}) + export const PersonGroup_Stroke2_Corner2_Rounded = createSinglePathSVG({ path: 'M8 5a2 2 0 1 0 0 4 2 2 0 0 0 0-4ZM4 7a4 4 0 1 1 8 0 4 4 0 0 1-8 0Zm13-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-3.5 1.5a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0Zm7.301 9.7c-.836-2.6-2.88-3.503-4.575-3.111a1 1 0 0 1-.451-1.949c2.815-.651 5.81.966 6.93 4.448a2.49 2.49 0 0 1-.506 2.43A2.92 2.92 0 0 1 20 20h-2a1 1 0 1 1 0-2h2a.92.92 0 0 0 .69-.295.49.49 0 0 0 .112-.505ZM8 14c-1.865 0-3.878 1.274-4.681 4.151a.57.57 0 0 0 .132.55c.15.171.4.299.695.299h7.708a.93.93 0 0 0 .695-.299.57.57 0 0 0 .132-.55C11.878 15.274 9.865 14 8 14Zm0-2c2.87 0 5.594 1.98 6.607 5.613.53 1.9-1.09 3.387-2.753 3.387H4.146c-1.663 0-3.283-1.487-2.753-3.387C2.406 13.981 5.129 12 8 12Z', }) diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 2062fe918..adea42e5e 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -1,6 +1,7 @@ import React, {useState} from 'react' -import {LayoutAnimation, View} from 'react-native' +import {LayoutAnimation, Pressable, View} from 'react-native' import {Linking} from 'react-native' +import {useReducedMotion} from 'react-native-reanimated' import {AppBskyActorDefs, moderateProfile} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -9,13 +10,15 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {IS_INTERNAL} from '#/lib/app-info' import {HELP_DESK_URL} from '#/lib/constants' +import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' +import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {clearStorage} from '#/state/persisted' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' -import {useSession, useSessionApi} from '#/state/session' +import {SessionAccount, useSession, useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' @@ -24,42 +27,50 @@ import {UserAvatar} from '#/view/com/util/UserAvatar' import {ProfileHeaderDisplayName} from '#/screens/Profile/Header/DisplayName' import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle' import * as SettingsList from '#/screens/Settings/components/SettingsList' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, tokens, useTheme} from '#/alf' +import {AvatarStack} from '#/components/AvatarStack' import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' +import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' +import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' import { Person_Stroke2_Corner2_Rounded as PersonIcon, PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon, + PersonPlus_Stroke2_Corner2_Rounded as PersonPlusIcon, + PersonX_Stroke2_Corner0_Rounded as PersonXIcon, } from '#/components/icons/Person' import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' +import {Loader} from '#/components/Loader' +import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' type Props = NativeStackScreenProps export function SettingsScreen({}: Props) { const {_} = useLingui() + const reducedMotion = useReducedMotion() const {logoutEveryAccount} = useSessionApi() const {accounts, currentAccount} = useSession() const switchAccountControl = useDialogControl() const signOutPromptControl = Prompt.usePromptControl() const {data: profile} = useProfileQuery({did: currentAccount?.did}) - const {setShowLoggedOut} = useLoggedOutViewControls() - const closeEverything = useCloseAllActiveElements() + const {data: otherProfiles} = useProfilesQuery({ + handles: accounts + .filter(acc => acc.did !== currentAccount?.did) + .map(acc => acc.handle), + }) + const {pendingDid, onPressSwitchAccount} = useAccountSwitcher() + const [showAccounts, setShowAccounts] = useState(false) const [showDevOptions, setShowDevOptions] = useState(false) - const onAddAnotherAccount = () => { - setShowLoggedOut(true) - closeEverything() - } - return ( @@ -77,34 +88,59 @@ export function SettingsScreen({}: Props) { ]}> {profile && } - 1 - ? _(msg`Switch account`) - : _(msg`Add another account`) - } - onPress={() => - accounts.length > 1 - ? switchAccountControl.open() - : onAddAnotherAccount() - }> - - - {accounts.length > 1 ? ( - Switch account - ) : ( - Add another account + {accounts.length > 1 ? ( + <> + { + if (!reducedMotion) { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.easeInEaseOut, + ) + } + setShowAccounts(s => !s) + }}> + + + Switch account + + {showAccounts ? ( + + ) : ( + acc.did) + .filter(did => did !== currentAccount?.did) + .slice(0, 5)} + /> + )} + + {showAccounts && ( + <> + + {accounts + .filter(acc => acc.did !== currentAccount?.did) + .map(account => ( + p.did === account.did, + )} + pendingDid={pendingDid} + onPressSwitchAccount={onPressSwitchAccount} + /> + ))} + + )} - - {accounts.length > 1 && ( - acc.did) - .filter(did => did !== currentAccount?.did) - .slice(0, 5)} - /> - )} - + + ) : ( + + )} @@ -188,9 +224,11 @@ export function SettingsScreen({}: Props) { { - LayoutAnimation.configureNext( - LayoutAnimation.Presets.easeInEaseOut, - ) + if (!reducedMotion) { + LayoutAnimation.configureNext( + LayoutAnimation.Presets.easeInEaseOut, + ) + } setShowDevOptions(d => !d) }} label={_(msg`Developer options`)}> @@ -245,70 +283,6 @@ function ProfilePreview({ ) } -const AVI_SIZE = 26 -const HALF_AVI_SIZE = AVI_SIZE / 2 - -function AvatarStack({profiles}: {profiles: string[]}) { - const {data, error} = useProfilesQuery({handles: profiles}) - const t = useTheme() - const moderationOpts = useModerationOpts() - - if (error) { - console.error(error) - return null - } - - const isPending = !data || !moderationOpts - - const items = isPending - ? Array.from({length: profiles.length}).map((_, i) => ({ - key: i, - profile: null, - moderation: null, - })) - : data.profiles.map(item => ({ - key: item.did, - profile: item, - moderation: moderateProfile(item, moderationOpts), - })) - - return ( - - {items.map((item, i) => ( - - {item.profile && ( - - )} - - ))} - - ) -} - function DevOptions() { const {_} = useLingui() const onboardingDispatch = useOnboardingDispatch() @@ -373,3 +347,117 @@ function DevOptions() { ) } + +function AddAccountRow() { + const {_} = useLingui() + const {setShowLoggedOut} = useLoggedOutViewControls() + const closeEverything = useCloseAllActiveElements() + + const onAddAnotherAccount = () => { + setShowLoggedOut(true) + closeEverything() + } + + return ( + + + + Add another account + + + ) +} + +function AccountRow({ + profile, + account, + pendingDid, + onPressSwitchAccount, +}: { + profile?: AppBskyActorDefs.ProfileViewDetailed + account: SessionAccount + pendingDid: string | null + onPressSwitchAccount: ( + account: SessionAccount, + logContext: 'Settings', + ) => void +}) { + const {_} = useLingui() + const t = useTheme() + + const moderationOpts = useModerationOpts() + const removePromptControl = Prompt.usePromptControl() + const {removeAccount} = useSessionApi() + + const onSwitchAccount = () => { + if (pendingDid) return + onPressSwitchAccount(account, 'Settings') + } + + return ( + + + {moderationOpts && profile ? ( + + ) : ( + + )} + + {sanitizeHandle(account.handle, '@')} + + {pendingDid === account.did && } + + {!pendingDid && ( + + + {({props, state}) => ( + + + + )} + + + removePromptControl.open()}> + + Remove account + + + + + + )} + + { + removeAccount(account) + Toast.show(_(msg`Account removed from quick access`)) + }} + confirmButtonCta={_(msg`Remove`)} + confirmButtonColor="negative" + /> + + ) +} -- cgit 1.4.1