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" "*"