diff --git a/package.json b/package.json
index 77eae011..a3b0bc37 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 8cd23efc..f2a352a7 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 fcd33af8..1471420f 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 91c96868..1256bd42 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({}: {}) {
Content Filtering
+
+ {isIOS ? (
+
+ Adult content can only be enabled via the Web at{' '}
+
+ .
+
+ ) : (
+
+ )}
+
)
-}
+})
// 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 (
@@ -95,7 +153,7 @@ const ContentLabelPref = observer(
) : (
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 5fbb2abe..9a2e64fe 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" "*"