diff --git a/assets/icons/personPlus_stroke2_corner2_rounded.svg b/assets/icons/personPlus_stroke2_corner2_rounded.svg
new file mode 100644
index 0000000000..148c29e0f0
--- /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 0000000000..5f790fb671
--- /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 31d7078d9c..8428fcce1b 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 2062fe9180..adea42e5ed 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"
+ />
+
+ )
+}