about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package.json4
-rw-r--r--src/state/models/root-store.ts4
-rw-r--r--src/state/models/ui/preferences.ts84
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx69
-rw-r--r--yarn.lock16
5 files changed, 158 insertions, 19 deletions
diff --git a/package.json b/package.json
index 77eae011a..a3b0bc373 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
     "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
   },
   "dependencies": {
-    "@atproto/api": "0.3.1",
+    "@atproto/api": "0.3.3",
     "@bam.tech/react-native-image-resizer": "^3.0.4",
     "@braintree/sanitize-url": "^6.0.2",
     "@expo/webpack-config": "^18.0.1",
@@ -140,7 +140,7 @@
     "zod": "^3.20.2"
   },
   "devDependencies": {
-    "@atproto/pds": "^0.1.6",
+    "@atproto/pds": "^0.1.8",
     "@babel/core": "^7.20.0",
     "@babel/preset-env": "^7.20.0",
     "@babel/runtime": "^7.20.0",
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 8cd23efcd..f2a352a79 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -37,7 +37,7 @@ export class RootStoreModel {
   log = new LogModel()
   session = new SessionModel(this)
   shell = new ShellUiModel(this)
-  preferences = new PreferencesModel()
+  preferences = new PreferencesModel(this)
   me = new MeModel(this)
   invitedUsers = new InvitedUsers(this)
   profiles = new ProfilesCache(this)
@@ -126,6 +126,7 @@ export class RootStoreModel {
     this.log.debug('RootStoreModel:handleSessionChange')
     this.agent = agent
     this.me.clear()
+    /* dont await */ this.preferences.sync()
     await this.me.load()
     if (!hadSession) {
       resetNavigation()
@@ -161,6 +162,7 @@ export class RootStoreModel {
     }
     try {
       await this.me.updateIfNeeded()
+      await this.preferences.sync()
     } catch (e: any) {
       this.log.error('Failed to fetch latest state', e)
     }
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index fcd33af8e..1471420fc 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -1,7 +1,8 @@
-import {makeAutoObservable} from 'mobx'
+import {makeAutoObservable, runInAction} from 'mobx'
 import {getLocales} from 'expo-localization'
 import {isObj, hasProp} from 'lib/type-guards'
-import {ComAtprotoLabelDefs} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {ComAtprotoLabelDefs, AppBskyActorDefs} from '@atproto/api'
 import {LabelValGroup} from 'lib/labeling/types'
 import {getLabelValueGroup} from 'lib/labeling/helpers'
 import {
@@ -15,6 +16,15 @@ import {isIOS} from 'platform/detection'
 const deviceLocales = getLocales()
 
 export type LabelPreference = 'show' | 'warn' | 'hide'
+const LABEL_GROUPS = [
+  'nsfw',
+  'nudity',
+  'suggestive',
+  'gore',
+  'hate',
+  'spam',
+  'impersonation',
+]
 
 export class LabelPreferencesModel {
   nsfw: LabelPreference = 'hide'
@@ -36,7 +46,7 @@ export class PreferencesModel {
     deviceLocales?.map?.(locale => locale.languageCode) || []
   contentLabels = new LabelPreferencesModel()
 
-  constructor() {
+  constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {}, {autoBind: true})
   }
 
@@ -65,6 +75,35 @@ export class PreferencesModel {
     }
   }
 
+  async sync() {
+    const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
+    runInAction(() => {
+      for (const pref of res.data.preferences) {
+        if (
+          AppBskyActorDefs.isAdultContentPref(pref) &&
+          AppBskyActorDefs.validateAdultContentPref(pref).success
+        ) {
+          this.adultContentEnabled = pref.enabled
+        } else if (
+          AppBskyActorDefs.isContentLabelPref(pref) &&
+          AppBskyActorDefs.validateAdultContentPref(pref).success
+        ) {
+          if (LABEL_GROUPS.includes(pref.label)) {
+            this.contentLabels[pref.label] = pref.visibility
+          }
+        }
+      }
+    })
+  }
+
+  async update(cb: (prefs: AppBskyActorDefs.Preferences) => void) {
+    const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
+    cb(res.data.preferences)
+    await this.rootStore.agent.app.bsky.actor.putPreferences({
+      preferences: res.data.preferences,
+    })
+  }
+
   hasContentLanguage(code2: string) {
     return this.contentLanguages.includes(code2)
   }
@@ -79,11 +118,48 @@ export class PreferencesModel {
     }
   }
 
-  setContentLabelPref(
+  async setContentLabelPref(
     key: keyof LabelPreferencesModel,
     value: LabelPreference,
   ) {
     this.contentLabels[key] = value
+
+    await this.update((prefs: AppBskyActorDefs.Preferences) => {
+      const existing = prefs.find(
+        pref =>
+          AppBskyActorDefs.isContentLabelPref(pref) &&
+          AppBskyActorDefs.validateAdultContentPref(pref).success &&
+          pref.label === key,
+      )
+      if (existing) {
+        existing.visibility = value
+      } else {
+        prefs.push({
+          $type: 'app.bsky.actor.defs#contentLabelPref',
+          label: key,
+          visibility: value,
+        })
+      }
+    })
+  }
+
+  async setAdultContentEnabled(v: boolean) {
+    this.adultContentEnabled = v
+    await this.update((prefs: AppBskyActorDefs.Preferences) => {
+      const existing = prefs.find(
+        pref =>
+          AppBskyActorDefs.isAdultContentPref(pref) &&
+          AppBskyActorDefs.validateAdultContentPref(pref).success,
+      )
+      if (existing) {
+        existing.enabled = v
+      } else {
+        prefs.push({
+          $type: 'app.bsky.actor.defs#adultContentPref',
+          enabled: v,
+        })
+      }
+    })
   }
 
   getLabelPreference(labels: ComAtprotoLabelDefs.Label[] | undefined): {
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 91c968684..1256bd420 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -7,15 +7,37 @@ import {useStores} from 'state/index'
 import {LabelPreference} from 'state/models/ui/preferences'
 import {s, colors, gradients} from 'lib/styles'
 import {Text} from '../util/text/Text'
+import {TextLink} from '../util/Link'
+import {ToggleButton} from '../util/forms/ToggleButton'
 import {usePalette} from 'lib/hooks/usePalette'
 import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
-import {isDesktopWeb} from 'platform/detection'
+import {isDesktopWeb, isIOS} from 'platform/detection'
+import * as Toast from '../util/Toast'
 
 export const snapPoints = ['90%']
 
-export function Component({}: {}) {
+export const Component = observer(({}: {}) => {
   const store = useStores()
   const pal = usePalette('default')
+
+  React.useEffect(() => {
+    store.preferences.sync()
+  }, [store])
+
+  const onToggleAdultContent = React.useCallback(async () => {
+    if (isIOS) {
+      return
+    }
+    try {
+      await store.preferences.setAdultContentEnabled(
+        !store.preferences.adultContentEnabled,
+      )
+    } catch (e) {
+      Toast.show('There was an issue syncing your preferences with the server')
+      store.log.error('Failed to update preferences with server', {e})
+    }
+  }, [store])
+
   const onPressDone = React.useCallback(() => {
     store.shell.closeModal()
   }, [store])
@@ -24,6 +46,27 @@ export function Component({}: {}) {
     <View testID="contentFilteringModal" style={[pal.view, styles.container]}>
       <Text style={[pal.text, styles.title]}>Content Filtering</Text>
       <ScrollView style={styles.scrollContainer}>
+        <View style={s.mb10}>
+          {isIOS ? (
+            <Text type="md" style={pal.textLight}>
+              Adult content can only be enabled via the Web at{' '}
+              <TextLink
+                style={pal.link}
+                href="https://staging.bsky.app"
+                text="staging.bsky.app"
+              />
+              .
+            </Text>
+          ) : (
+            <ToggleButton
+              type="default-light"
+              label="Enable Adult Content"
+              isSelected={store.preferences.adultContentEnabled}
+              onPress={onToggleAdultContent}
+              style={styles.toggleBtn}
+            />
+          )}
+        </View>
         <ContentLabelPref
           group="nsfw"
           disabled={!store.preferences.adultContentEnabled}
@@ -63,7 +106,7 @@ export function Component({}: {}) {
       </View>
     </View>
   )
-}
+})
 
 // TODO: Refactor this component to pass labels down to each tab
 const ContentLabelPref = observer(
@@ -76,6 +119,21 @@ const ContentLabelPref = observer(
   }) => {
     const store = useStores()
     const pal = usePalette('default')
+
+    const onChange = React.useCallback(
+      async (v: LabelPreference) => {
+        try {
+          await store.preferences.setContentLabelPref(group, v)
+        } catch (e) {
+          Toast.show(
+            'There was an issue syncing your preferences with the server',
+          )
+          store.log.error('Failed to update preferences with server', {e})
+        }
+      },
+      [store, group],
+    )
+
     return (
       <View style={[styles.contentLabelPref, pal.border]}>
         <View style={s.flex1}>
@@ -95,7 +153,7 @@ const ContentLabelPref = observer(
         ) : (
           <SelectGroup
             current={store.preferences.contentLabels[group]}
-            onChange={v => store.preferences.setContentLabelPref(group, v)}
+            onChange={onChange}
             group={group}
           />
         )}
@@ -250,4 +308,7 @@ const styles = StyleSheet.create({
     padding: 14,
     backgroundColor: colors.gray1,
   },
+  toggleBtn: {
+    paddingHorizontal: 0,
+  },
 })
diff --git a/yarn.lock b/yarn.lock
index 5fbb2abeb..9a2e64fe2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -40,10 +40,10 @@
     tlds "^1.234.0"
     typed-emitter "^2.1.0"
 
-"@atproto/api@0.3.1":
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.1.tgz#98699479f8385c7494a853144657895be392c3f3"
-  integrity sha512-/AAZntrLUPCxw7q8FMtDsSYOjsAs5aAmllmArXyye5ITvbSw4pzWfJcBiKnQdmXpdwSrVWVEX7uwIp+GYWopqg==
+"@atproto/api@0.3.3":
+  version "0.3.3"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.3.tgz#8c8d41567beb7b37217f76d2aacf2c280e9fd07e"
+  integrity sha512-BlgpYbdPO0KSBypg2KgqHM0kS2Pk82P3X0w2rJs/vrdcMl72d2WeI9kQ5PPFiz80p6C6XcLcpnzzKKtQeFvh4A==
   dependencies:
     "@atproto/common-web" "*"
     "@atproto/uri" "*"
@@ -148,10 +148,10 @@
   resolved "https://registry.yarnpkg.com/@atproto/nsid/-/nsid-0.0.1.tgz#0cdc00cefe8f0b1385f352b9f57b3ad37fff09a4"
   integrity sha512-t5M6/CzWBVYoBbIvfKDpqPj/+ZmyoK9ydZSStcTXosJ27XXwOPhz0VDUGKK2SM9G5Y7TPes8S5KTAU0UdVYFCw==
 
-"@atproto/pds@^0.1.6":
-  version "0.1.6"
-  resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.6.tgz#2858355887eac06f5e2da8701231e5cb46004c18"
-  integrity sha512-bddIWH+OrEIxJ5HYst1mBS+95bNWC08FLaa3DVtJRHRCdfYaGDndZUVpOLLgzBRklDLicJyvva2JYEgp2mdgLA==
+"@atproto/pds@^0.1.8":
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/@atproto/pds/-/pds-0.1.8.tgz#cf1a9bab2301c3fe1120c63576153ac5a20bf70d"
+  integrity sha512-I493U+/NNU9D8L8tVbM/OpD6gQ6/Mv7uE+/i4a1vfBGO6NqYJ6jKw3qeCy4jq3NVbTxcs+lSSpK27hgApx4PtA==
   dependencies:
     "@atproto/api" "*"
     "@atproto/common" "*"