about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/RadioGroup.tsx76
-rw-r--r--src/screens/Messages/Settings.tsx70
-rw-r--r--src/screens/Messages/Settings/index.tsx24
-rw-r--r--src/state/queries/messages/actor-declaration.ts64
-rw-r--r--src/view/com/util/forms/RadioButton.tsx5
-rw-r--r--src/view/com/util/forms/RadioGroup.tsx5
6 files changed, 216 insertions, 28 deletions
diff --git a/src/components/RadioGroup.tsx b/src/components/RadioGroup.tsx
new file mode 100644
index 000000000..010f65bc3
--- /dev/null
+++ b/src/components/RadioGroup.tsx
@@ -0,0 +1,76 @@
+import React from 'react'
+import {View, ViewProps} from 'react-native'
+
+import {atoms as a, useTheme} from '#/alf'
+import {Button} from './Button'
+import {Text} from './Typography'
+
+export function RadioGroup<T extends string | number>({
+  value,
+  onSelect,
+  items,
+  ...props
+}: ViewProps & {
+  value: T
+  onSelect: (value: T) => void
+  items: Array<{label: string; value: T}>
+}) {
+  return (
+    <View {...props}>
+      {items.map(item => (
+        <Button
+          label={item.label}
+          key={item.value}
+          variant="ghost"
+          color="secondary"
+          size="small"
+          onPress={() => onSelect(item.value)}
+          style={[a.justify_between, a.px_sm]}>
+          <Text style={a.text_md}>{item.label}</Text>
+          <RadioIcon selected={value === item.value} />
+        </Button>
+      ))}
+    </View>
+  )
+}
+
+function RadioIcon({selected}: {selected: boolean}) {
+  const t = useTheme()
+  return (
+    <View
+      style={[
+        {
+          width: 30,
+          height: 30,
+          borderWidth: 2,
+          borderColor: selected
+            ? t.palette.primary_500
+            : t.palette.contrast_200,
+        },
+        selected
+          ? {
+              backgroundColor:
+                t.name === 'light'
+                  ? t.palette.primary_100
+                  : t.palette.primary_900,
+            }
+          : t.atoms.bg,
+        a.align_center,
+        a.justify_center,
+        a.rounded_full,
+      ]}>
+      {selected && (
+        <View
+          style={[
+            {
+              width: 18,
+              height: 18,
+              backgroundColor: t.palette.primary_500,
+            },
+            a.rounded_full,
+          ]}
+        />
+      )}
+    </View>
+  )
+}
diff --git a/src/screens/Messages/Settings.tsx b/src/screens/Messages/Settings.tsx
new file mode 100644
index 000000000..9faab4130
--- /dev/null
+++ b/src/screens/Messages/Settings.tsx
@@ -0,0 +1,70 @@
+import React, {useCallback} from 'react'
+import {View} from 'react-native'
+import {AppBskyActorDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {UseQueryResult} from '@tanstack/react-query'
+
+import {CommonNavigatorParams} from '#/lib/routes/types'
+import {useGate} from '#/lib/statsig/statsig'
+import {useUpdateActorDeclaration} from '#/state/queries/messages/actor-declaration'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+import * as Toast from '#/view/com/util/Toast'
+import {ViewHeader} from '#/view/com/util/ViewHeader'
+import {CenteredView} from '#/view/com/util/Views'
+import {atoms as a} from '#/alf'
+import {RadioGroup} from '#/components/RadioGroup'
+import {Text} from '#/components/Typography'
+import {ClipClopGate} from './gate'
+
+type AllowIncoming = 'all' | 'none' | 'following'
+
+type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'>
+export function MessagesSettingsScreen({}: Props) {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const {data: profile} = useProfileQuery({
+    did: currentAccount!.did,
+  }) as UseQueryResult<AppBskyActorDefs.ProfileViewDetailed, Error>
+
+  const {mutate: updateDeclaration} = useUpdateActorDeclaration({
+    onError: () => {
+      Toast.show(_(msg`Failed to update settings`))
+    },
+  })
+
+  const onSelectItem = useCallback(
+    (key: string) => {
+      updateDeclaration(key as AllowIncoming)
+    },
+    [updateDeclaration],
+  )
+
+  const gate = useGate()
+  if (!gate('dms')) return <ClipClopGate />
+
+  return (
+    <CenteredView sideBorders>
+      <ViewHeader title={_(msg`Settings`)} showOnDesktop showBorder />
+      <View style={[a.px_md, a.py_lg, a.gap_md]}>
+        <Text style={[a.text_xl, a.font_bold, a.px_sm]}>
+          <Trans>Allow messages from</Trans>
+        </Text>
+        <RadioGroup<AllowIncoming>
+          value={
+            (profile?.associated?.chat?.allowIncoming as AllowIncoming) ??
+            'following'
+          }
+          items={[
+            {label: _(msg`Everyone`), value: 'all'},
+            {label: _(msg`Follows only`), value: 'following'},
+            {label: _(msg`No one`), value: 'none'},
+          ]}
+          onSelect={onSelectItem}
+        />
+      </View>
+    </CenteredView>
+  )
+}
diff --git a/src/screens/Messages/Settings/index.tsx b/src/screens/Messages/Settings/index.tsx
deleted file mode 100644
index bd093c792..000000000
--- a/src/screens/Messages/Settings/index.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {msg} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
-
-import {CommonNavigatorParams} from '#/lib/routes/types'
-import {useGate} from '#/lib/statsig/statsig'
-import {ViewHeader} from '#/view/com/util/ViewHeader'
-import {ClipClopGate} from '../gate'
-
-type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'>
-export function MessagesSettingsScreen({}: Props) {
-  const {_} = useLingui()
-
-  const gate = useGate()
-  if (!gate('dms')) return <ClipClopGate />
-
-  return (
-    <View>
-      <ViewHeader title={_(msg`Settings`)} showOnDesktop />
-    </View>
-  )
-}
diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts
new file mode 100644
index 000000000..c8cc4acbd
--- /dev/null
+++ b/src/state/queries/messages/actor-declaration.ts
@@ -0,0 +1,64 @@
+import {AppBskyActorDefs} from '@atproto/api'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {useAgent, useSession} from '#/state/session'
+import {RQKEY as PROFILE_RKEY} from '../profile'
+
+export function useUpdateActorDeclaration({
+  onSuccess,
+  onError,
+}: {
+  onSuccess?: () => void
+  onError?: (error: Error) => void
+}) {
+  const queryClient = useQueryClient()
+  const {currentAccount} = useSession()
+  const {getAgent} = useAgent()
+
+  return useMutation({
+    mutationFn: async (allowIncoming: 'all' | 'none' | 'following') => {
+      if (!currentAccount) throw new Error('Not logged in')
+      // TODO(sam): remove validate: false once PDSes have the new lexicon
+      const result = await getAgent().api.com.atproto.repo.putRecord({
+        collection: 'chat.bsky.actor.declaration',
+        rkey: 'self',
+        repo: currentAccount.did,
+        validate: false,
+        record: {
+          $type: 'chat.bsky.actor.declaration',
+          allowIncoming,
+        },
+      })
+      return result
+    },
+    onMutate: allowIncoming => {
+      if (!currentAccount) return
+      queryClient.setQueryData(
+        PROFILE_RKEY(currentAccount?.did),
+        (old?: AppBskyActorDefs.ProfileViewDetailed) => {
+          if (!old) return old
+          return {
+            ...old,
+            associated: {
+              ...old.associated,
+              chat: {
+                allowIncoming,
+              },
+            },
+          } satisfies AppBskyActorDefs.ProfileViewDetailed
+        },
+      )
+    },
+    onSuccess,
+    onError: error => {
+      logger.error(error)
+      if (currentAccount) {
+        queryClient.invalidateQueries({
+          queryKey: PROFILE_RKEY(currentAccount.did),
+        })
+      }
+      onError?.(error)
+    },
+  })
+}
diff --git a/src/view/com/util/forms/RadioButton.tsx b/src/view/com/util/forms/RadioButton.tsx
index 9d1cb4749..6cecd318e 100644
--- a/src/view/com/util/forms/RadioButton.tsx
+++ b/src/view/com/util/forms/RadioButton.tsx
@@ -1,9 +1,10 @@
 import React from 'react'
 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
+
+import {choose} from 'lib/functions'
+import {useTheme} from 'lib/ThemeContext'
 import {Text} from '../text/Text'
 import {Button, ButtonType} from './Button'
-import {useTheme} from 'lib/ThemeContext'
-import {choose} from 'lib/functions'
 
 export function RadioButton({
   testID,
diff --git a/src/view/com/util/forms/RadioGroup.tsx b/src/view/com/util/forms/RadioGroup.tsx
index 14599e649..493c36a9d 100644
--- a/src/view/com/util/forms/RadioGroup.tsx
+++ b/src/view/com/util/forms/RadioGroup.tsx
@@ -1,8 +1,9 @@
 import React, {useState} from 'react'
 import {View} from 'react-native'
-import {RadioButton} from './RadioButton'
-import {ButtonType} from './Button'
+
 import {s} from 'lib/styles'
+import {ButtonType} from './Button'
+import {RadioButton} from './RadioButton'
 
 export interface RadioGroupItem {
   label: string | JSX.Element