about summary refs log tree commit diff
path: root/src/screens/Settings/Settings.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/screens/Settings/Settings.tsx')
-rw-r--r--src/screens/Settings/Settings.tsx282
1 files changed, 282 insertions, 0 deletions
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
new file mode 100644
index 000000000..789ffb56f
--- /dev/null
+++ b/src/screens/Settings/Settings.tsx
@@ -0,0 +1,282 @@
+import React from 'react'
+import {View} from 'react-native'
+import {Linking} from 'react-native'
+import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+
+import {HELP_DESK_URL} from '#/lib/constants'
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile'
+import {useSession, useSessionApi} from '#/state/session'
+import {useLoggedOutViewControls} from '#/state/shell/logged-out'
+import {useCloseAllActiveElements} from '#/state/util'
+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 {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 {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
+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,
+} 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 * as Prompt from '#/components/Prompt'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
+export function SettingsScreen({}: Props) {
+  const {_} = useLingui()
+  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 onAddAnotherAccount = () => {
+    setShowLoggedOut(true)
+    closeEverything()
+  }
+
+  return (
+    <Layout.Screen>
+      <Layout.Header title={_(msg`Settings`)} />
+      <Layout.Content>
+        <SettingsList.Container>
+          <View
+            style={[
+              a.px_xl,
+              a.pb_md,
+              a.w_full,
+              a.gap_2xs,
+              a.align_center,
+              {minHeight: 160},
+            ]}>
+            {profile && <ProfilePreview profile={profile} />}
+          </View>
+          <SettingsList.PressableItem
+            label={
+              accounts.length > 1
+                ? _(msg`Switch account`)
+                : _(msg`Add another account`)
+            }
+            onPress={() =>
+              accounts.length > 1
+                ? switchAccountControl.open()
+                : onAddAnotherAccount()
+            }>
+            <SettingsList.ItemIcon icon={PersonGroupIcon} />
+            <SettingsList.ItemText>
+              {accounts.length > 1 ? (
+                <Trans>Switch account</Trans>
+              ) : (
+                <Trans>Add another account</Trans>
+              )}
+            </SettingsList.ItemText>
+            {accounts.length > 1 && (
+              <AvatarStack
+                profiles={accounts
+                  .map(acc => acc.did)
+                  .filter(did => did !== currentAccount?.did)
+                  .slice(0, 5)}
+              />
+            )}
+          </SettingsList.PressableItem>
+          <SettingsList.Divider />
+          <SettingsList.LinkItem to="/settings/account" label={_(msg`Account`)}>
+            <SettingsList.ItemIcon icon={PersonIcon} />
+            <SettingsList.ItemText>
+              <Trans>Account</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.LinkItem
+            to="/settings/privacy-and-security"
+            label={_(msg`Privacy and security`)}>
+            <SettingsList.ItemIcon icon={LockIcon} />
+            <SettingsList.ItemText>
+              <Trans>Privacy and security</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.LinkItem to="/moderation" label={_(msg`Moderation`)}>
+            <SettingsList.ItemIcon icon={HandIcon} />
+            <SettingsList.ItemText>
+              <Trans>Moderation</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.LinkItem
+            to="/settings/content-and-media"
+            label={_(msg`Content and media`)}>
+            <SettingsList.ItemIcon icon={WindowIcon} />
+            <SettingsList.ItemText>
+              <Trans>Content and media</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.LinkItem
+            to="/settings/appearance"
+            label={_(msg`Appearance`)}>
+            <SettingsList.ItemIcon icon={PaintRollerIcon} />
+            <SettingsList.ItemText>
+              <Trans>Appearance</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.LinkItem
+            to="/settings/accessibility"
+            label={_(msg`Accessibility`)}>
+            <SettingsList.ItemIcon icon={AccessibilityIcon} />
+            <SettingsList.ItemText>
+              <Trans>Accessibility</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.LinkItem
+            to="/settings/language"
+            label={_(msg`Languages`)}>
+            <SettingsList.ItemIcon icon={EarthIcon} />
+            <SettingsList.ItemText>
+              <Trans>Languages</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.PressableItem
+            onPress={() => Linking.openURL(HELP_DESK_URL)}
+            label={_(msg`Help`)}
+            accessibilityHint={_(msg`Open helpdesk in browser`)}>
+            <SettingsList.ItemIcon icon={CircleQuestionIcon} />
+            <SettingsList.ItemText>
+              <Trans>Help</Trans>
+            </SettingsList.ItemText>
+            <SettingsList.Chevron />
+          </SettingsList.PressableItem>
+          <SettingsList.LinkItem to="/settings/about" label={_(msg`About`)}>
+            <SettingsList.ItemIcon icon={BubbleInfoIcon} />
+            <SettingsList.ItemText>
+              <Trans>About</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.LinkItem>
+          <SettingsList.Divider />
+          <SettingsList.PressableItem
+            destructive
+            onPress={() => signOutPromptControl.open()}
+            label={_(msg`Sign out`)}>
+            <SettingsList.ItemText>
+              <Trans>Sign out</Trans>
+            </SettingsList.ItemText>
+          </SettingsList.PressableItem>
+        </SettingsList.Container>
+      </Layout.Content>
+
+      <Prompt.Basic
+        control={signOutPromptControl}
+        title={_(msg`Sign out?`)}
+        description={_(msg`You will be signed out of all your accounts.`)}
+        onConfirm={() => logoutEveryAccount('Settings')}
+        confirmButtonCta={_(msg`Sign out`)}
+        cancelButtonCta={_(msg`Cancel`)}
+        confirmButtonColor="negative"
+      />
+
+      <SwitchAccountDialog control={switchAccountControl} />
+    </Layout.Screen>
+  )
+}
+
+function ProfilePreview({
+  profile,
+}: {
+  profile: AppBskyActorDefs.ProfileViewDetailed
+}) {
+  const shadow = useProfileShadow(profile)
+  const moderationOpts = useModerationOpts()
+
+  if (!moderationOpts) return null
+
+  const moderation = moderateProfile(profile, moderationOpts)
+
+  return (
+    <>
+      <UserAvatar
+        size={80}
+        avatar={shadow.avatar}
+        moderation={moderation.ui('avatar')}
+      />
+      <ProfileHeaderDisplayName profile={shadow} moderation={moderation} />
+      <ProfileHeaderHandle profile={shadow} />
+    </>
+  )
+}
+
+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 (
+    <View
+      style={[
+        a.flex_row,
+        a.align_center,
+        a.relative,
+        {width: AVI_SIZE + (items.length - 1) * HALF_AVI_SIZE},
+      ]}>
+      {items.map((item, i) => (
+        <View
+          key={item.key}
+          style={[
+            t.atoms.bg_contrast_25,
+            a.relative,
+            {
+              width: AVI_SIZE,
+              height: AVI_SIZE,
+              left: i * -HALF_AVI_SIZE,
+              borderWidth: 1,
+              borderColor: t.atoms.bg.backgroundColor,
+              borderRadius: 999,
+              zIndex: 3 - i,
+            },
+          ]}>
+          {item.profile && (
+            <UserAvatar
+              size={AVI_SIZE - 2}
+              avatar={item.profile.avatar}
+              moderation={item.moderation.ui('avatar')}
+            />
+          )}
+        </View>
+      ))}
+    </View>
+  )
+}