about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-08-30 15:19:19 -0700
committerGitHub <noreply@github.com>2023-08-30 15:19:19 -0700
commita29f10aefe6fa67848ca281a0a7dffc43e292ec6 (patch)
tree7f1a534953eb515fd4a7f358885c4c1c5e2de42c /src
parent3a90b479fd42883a624b3c3e5d7ba36b510ca6fa (diff)
downloadvoidsky-a29f10aefe6fa67848ca281a0a7dffc43e292ec6.tar.zst
Moderation settings fixes (#1336)
* Default isAdultContentEnabled to false on all devices.

The original intent of setting the default based on the device was
to make the maximally-permissive choice. It turns out this was a
mistake as it created sync issues between devices; users would be
confused by the lack of congruity between them. We have to go with
false by default to ensure sync is retained.

* Update preferences model to use new sdk api

* Delete dead code

* Dont show the iOS adult content warning in content filtering settings if adult content is enabled

* Bump @atproto/api@0.6.8

* Codebase style consistency
Diffstat (limited to 'src')
-rw-r--r--src/lib/labeling/helpers.ts436
-rw-r--r--src/lib/labeling/types.ts53
-rw-r--r--src/state/models/ui/preferences.ts234
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx22
4 files changed, 67 insertions, 678 deletions
diff --git a/src/lib/labeling/helpers.ts b/src/lib/labeling/helpers.ts
deleted file mode 100644
index 447b0a99a..000000000
--- a/src/lib/labeling/helpers.ts
+++ /dev/null
@@ -1,436 +0,0 @@
-import {
-  AppBskyActorDefs,
-  AppBskyGraphDefs,
-  AppBskyEmbedRecordWithMedia,
-  AppBskyEmbedRecord,
-  AppBskyEmbedImages,
-  AppBskyEmbedExternal,
-} from '@atproto/api'
-import {
-  CONFIGURABLE_LABEL_GROUPS,
-  ILLEGAL_LABEL_GROUP,
-  ALWAYS_FILTER_LABEL_GROUP,
-  ALWAYS_WARN_LABEL_GROUP,
-  UNKNOWN_LABEL_GROUP,
-} from './const'
-import {
-  Label,
-  LabelValGroup,
-  ModerationBehaviorCode,
-  ModerationBehavior,
-  PostModeration,
-  ProfileModeration,
-  PostLabelInfo,
-  ProfileLabelInfo,
-} from './types'
-import {RootStoreModel} from 'state/index'
-
-type Embed =
-  | AppBskyEmbedRecord.View
-  | AppBskyEmbedImages.View
-  | AppBskyEmbedExternal.View
-  | AppBskyEmbedRecordWithMedia.View
-  | {$type: string; [k: string]: unknown}
-
-export function getLabelValueGroup(labelVal: string): LabelValGroup {
-  let id: keyof typeof CONFIGURABLE_LABEL_GROUPS
-  for (id in CONFIGURABLE_LABEL_GROUPS) {
-    if (ILLEGAL_LABEL_GROUP.values.includes(labelVal)) {
-      return ILLEGAL_LABEL_GROUP
-    }
-    if (ALWAYS_FILTER_LABEL_GROUP.values.includes(labelVal)) {
-      return ALWAYS_FILTER_LABEL_GROUP
-    }
-    if (ALWAYS_WARN_LABEL_GROUP.values.includes(labelVal)) {
-      return ALWAYS_WARN_LABEL_GROUP
-    }
-    if (CONFIGURABLE_LABEL_GROUPS[id].values.includes(labelVal)) {
-      return CONFIGURABLE_LABEL_GROUPS[id]
-    }
-  }
-  return UNKNOWN_LABEL_GROUP
-}
-
-export function getPostModeration(
-  store: RootStoreModel,
-  postInfo: PostLabelInfo,
-): PostModeration {
-  const accountPref = store.preferences.getLabelPreference(
-    postInfo.accountLabels,
-  )
-  const profilePref = store.preferences.getLabelPreference(
-    postInfo.profileLabels,
-  )
-  const postPref = store.preferences.getLabelPreference(postInfo.postLabels)
-
-  // avatar
-  let avatar = {
-    warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
-    blur:
-      postInfo.isBlocking ||
-      accountPref.pref === 'hide' ||
-      accountPref.pref === 'warn' ||
-      profilePref.pref === 'hide' ||
-      profilePref.pref === 'warn',
-  }
-
-  // hide no-override cases
-  if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') {
-    return hidePostNoOverride(accountPref.desc.warning)
-  }
-  if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') {
-    return hidePostNoOverride(profilePref.desc.warning)
-  }
-  if (postPref.pref === 'hide' && postPref.desc.id === 'illegal') {
-    return hidePostNoOverride(postPref.desc.warning)
-  }
-
-  // hide cases
-  if (postInfo.isBlocking) {
-    return {
-      avatar,
-      list: hide('Post from an account you blocked.'),
-      thread: hide('Post from an account you blocked.'),
-      view: warn('Post from an account you blocked.'),
-    }
-  }
-  if (postInfo.isBlockedBy) {
-    return {
-      avatar,
-      list: hide('Post from an account that has blocked you.'),
-      thread: hide('Post from an account that has blocked you.'),
-      view: warn('Post from an account that has blocked you.'),
-    }
-  }
-  if (accountPref.pref === 'hide') {
-    return {
-      avatar,
-      list: hide(accountPref.desc.warning),
-      thread: hide(accountPref.desc.warning),
-      view: warn(accountPref.desc.warning),
-    }
-  }
-  if (profilePref.pref === 'hide') {
-    return {
-      avatar,
-      list: hide(profilePref.desc.warning),
-      thread: hide(profilePref.desc.warning),
-      view: warn(profilePref.desc.warning),
-    }
-  }
-  if (postPref.pref === 'hide') {
-    return {
-      avatar,
-      list: hide(postPref.desc.warning),
-      thread: hide(postPref.desc.warning),
-      view: warn(postPref.desc.warning),
-    }
-  }
-
-  // muting
-  if (postInfo.isMuted) {
-    let msg = 'Post from an account you muted.'
-    if (postInfo.mutedByList) {
-      msg = `Muted by ${postInfo.mutedByList.name}`
-    }
-    return {
-      avatar,
-      list: isMute(hide(msg)),
-      thread: isMute(warn(msg)),
-      view: isMute(warn(msg)),
-    }
-  }
-
-  // warning cases
-  if (postPref.pref === 'warn') {
-    if (postPref.desc.isAdultImagery) {
-      return {
-        avatar,
-        list: warnImages(postPref.desc.warning),
-        thread: warnImages(postPref.desc.warning),
-        view: warnImages(postPref.desc.warning),
-      }
-    }
-    return {
-      avatar,
-      list: warnContent(postPref.desc.warning),
-      thread: warnContent(postPref.desc.warning),
-      view: warnContent(postPref.desc.warning),
-    }
-  }
-  if (accountPref.pref === 'warn') {
-    return {
-      avatar,
-      list: warnContent(accountPref.desc.warning),
-      thread: warnContent(accountPref.desc.warning),
-      view: warnContent(accountPref.desc.warning),
-    }
-  }
-
-  return {
-    avatar,
-    list: show(),
-    thread: show(),
-    view: show(),
-  }
-}
-
-export function mergePostModerations(
-  moderations: PostModeration[],
-): PostModeration {
-  const merged: PostModeration = {
-    avatar: {warn: false, blur: false},
-    list: show(),
-    thread: show(),
-    view: show(),
-  }
-  for (const mod of moderations) {
-    if (mod.list.behavior === ModerationBehaviorCode.Hide) {
-      merged.list = mod.list
-    }
-    if (mod.thread.behavior === ModerationBehaviorCode.Hide) {
-      merged.thread = mod.thread
-    }
-    if (mod.view.behavior === ModerationBehaviorCode.Hide) {
-      merged.view = mod.view
-    }
-  }
-  return merged
-}
-
-export function getProfileModeration(
-  store: RootStoreModel,
-  profileInfo: ProfileLabelInfo,
-): ProfileModeration {
-  const accountPref = store.preferences.getLabelPreference(
-    profileInfo.accountLabels,
-  )
-  const profilePref = store.preferences.getLabelPreference(
-    profileInfo.profileLabels,
-  )
-
-  // avatar
-  let avatar = {
-    warn: accountPref.pref === 'hide' || accountPref.pref === 'warn',
-    blur:
-      profileInfo.isBlocking ||
-      accountPref.pref === 'hide' ||
-      accountPref.pref === 'warn' ||
-      profilePref.pref === 'hide' ||
-      profilePref.pref === 'warn',
-  }
-
-  // hide no-override cases
-  if (accountPref.pref === 'hide' && accountPref.desc.id === 'illegal') {
-    return hideProfileNoOverride(accountPref.desc.warning)
-  }
-  if (profilePref.pref === 'hide' && profilePref.desc.id === 'illegal') {
-    return hideProfileNoOverride(profilePref.desc.warning)
-  }
-
-  // hide cases
-  if (accountPref.pref === 'hide') {
-    return {
-      avatar,
-      list: hide(accountPref.desc.warning),
-      view: hide(accountPref.desc.warning),
-    }
-  }
-  if (profilePref.pref === 'hide') {
-    return {
-      avatar,
-      list: hide(profilePref.desc.warning),
-      view: hide(profilePref.desc.warning),
-    }
-  }
-
-  // warn cases
-  if (accountPref.pref === 'warn') {
-    return {
-      avatar,
-      list:
-        profileInfo.isBlocking || profileInfo.isBlockedBy
-          ? hide('Blocked account')
-          : warn(accountPref.desc.warning),
-      view: warn(accountPref.desc.warning),
-    }
-  }
-  // we don't warn for this
-  // if (profilePref.pref === 'warn') {
-  //   return {
-  //     avatar,
-  //     list: warn(profilePref.desc.warning),
-  //     view: warn(profilePref.desc.warning),
-  //   }
-  // }
-
-  return {
-    avatar,
-    list: profileInfo.isBlocking ? hide('Blocked account') : show(),
-    view: show(),
-  }
-}
-
-export function getProfileViewBasicLabelInfo(
-  profile: AppBskyActorDefs.ProfileViewBasic,
-): ProfileLabelInfo {
-  return {
-    accountLabels: filterAccountLabels(profile.labels),
-    profileLabels: filterProfileLabels(profile.labels),
-    isMuted: profile.viewer?.muted || false,
-    isBlocking: !!profile.viewer?.blocking || false,
-    isBlockedBy: !!profile.viewer?.blockedBy || false,
-  }
-}
-
-export function getEmbedLabels(embed?: Embed): Label[] {
-  if (!embed) {
-    return []
-  }
-  if (
-    AppBskyEmbedRecord.isView(embed) &&
-    AppBskyEmbedRecord.isViewRecord(embed.record)
-  ) {
-    return embed.record.labels || []
-  }
-  return []
-}
-
-export function getEmbedMuted(embed?: Embed): boolean {
-  if (!embed) {
-    return false
-  }
-  if (
-    AppBskyEmbedRecord.isView(embed) &&
-    AppBskyEmbedRecord.isViewRecord(embed.record)
-  ) {
-    return !!embed.record.author.viewer?.muted
-  }
-  return false
-}
-
-export function getEmbedMutedByList(
-  embed?: Embed,
-): AppBskyGraphDefs.ListViewBasic | undefined {
-  if (!embed) {
-    return undefined
-  }
-  if (
-    AppBskyEmbedRecord.isView(embed) &&
-    AppBskyEmbedRecord.isViewRecord(embed.record)
-  ) {
-    return embed.record.author.viewer?.mutedByList
-  }
-  return undefined
-}
-
-export function getEmbedBlocking(embed?: Embed): boolean {
-  if (!embed) {
-    return false
-  }
-  if (
-    AppBskyEmbedRecord.isView(embed) &&
-    AppBskyEmbedRecord.isViewRecord(embed.record)
-  ) {
-    return !!embed.record.author.viewer?.blocking
-  }
-  return false
-}
-
-export function getEmbedBlockedBy(embed?: Embed): boolean {
-  if (!embed) {
-    return false
-  }
-  if (
-    AppBskyEmbedRecord.isView(embed) &&
-    AppBskyEmbedRecord.isViewRecord(embed.record)
-  ) {
-    return !!embed.record.author.viewer?.blockedBy
-  }
-  return false
-}
-
-export function filterAccountLabels(labels?: Label[]): Label[] {
-  if (!labels) {
-    return []
-  }
-  return labels.filter(
-    label => !label.uri.endsWith('/app.bsky.actor.profile/self'),
-  )
-}
-
-export function filterProfileLabels(labels?: Label[]): Label[] {
-  if (!labels) {
-    return []
-  }
-  return labels.filter(label =>
-    label.uri.endsWith('/app.bsky.actor.profile/self'),
-  )
-}
-
-// internal methods
-// =
-
-function show() {
-  return {
-    behavior: ModerationBehaviorCode.Show,
-  }
-}
-
-function hidePostNoOverride(reason: string) {
-  return {
-    avatar: {warn: true, blur: true},
-    list: hideNoOverride(reason),
-    thread: hideNoOverride(reason),
-    view: hideNoOverride(reason),
-  }
-}
-
-function hideProfileNoOverride(reason: string) {
-  return {
-    avatar: {warn: true, blur: true},
-    list: hideNoOverride(reason),
-    view: hideNoOverride(reason),
-  }
-}
-
-function hideNoOverride(reason: string) {
-  return {
-    behavior: ModerationBehaviorCode.Hide,
-    reason,
-    noOverride: true,
-  }
-}
-
-function hide(reason: string) {
-  return {
-    behavior: ModerationBehaviorCode.Hide,
-    reason,
-  }
-}
-
-function warn(reason: string) {
-  return {
-    behavior: ModerationBehaviorCode.Warn,
-    reason,
-  }
-}
-
-function warnContent(reason: string) {
-  return {
-    behavior: ModerationBehaviorCode.WarnContent,
-    reason,
-  }
-}
-
-function isMute(behavior: ModerationBehavior): ModerationBehavior {
-  behavior.isMute = true
-  return behavior
-}
-
-function warnImages(reason: string) {
-  return {
-    behavior: ModerationBehaviorCode.WarnImages,
-    reason,
-  }
-}
diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts
index 1ee058024..84d59be7f 100644
--- a/src/lib/labeling/types.ts
+++ b/src/lib/labeling/types.ts
@@ -1,4 +1,4 @@
-import {ComAtprotoLabelDefs, AppBskyGraphDefs} from '@atproto/api'
+import {ComAtprotoLabelDefs} from '@atproto/api'
 import {LabelPreferencesModel} from 'state/models/ui/preferences'
 
 export type Label = ComAtprotoLabelDefs.Label
@@ -16,54 +16,3 @@ export interface LabelValGroup {
   warning: string
   values: string[]
 }
-
-export interface PostLabelInfo {
-  postLabels: Label[]
-  accountLabels: Label[]
-  profileLabels: Label[]
-  isMuted: boolean
-  mutedByList?: AppBskyGraphDefs.ListViewBasic
-  isBlocking: boolean
-  isBlockedBy: boolean
-}
-
-export interface ProfileLabelInfo {
-  accountLabels: Label[]
-  profileLabels: Label[]
-  isMuted: boolean
-  isBlocking: boolean
-  isBlockedBy: boolean
-}
-
-export enum ModerationBehaviorCode {
-  Show,
-  Hide,
-  Warn,
-  WarnContent,
-  WarnImages,
-}
-
-export interface ModerationBehavior {
-  behavior: ModerationBehaviorCode
-  isMute?: boolean
-  noOverride?: boolean
-  reason?: string
-}
-
-export interface AvatarModeration {
-  warn: boolean
-  blur: boolean
-}
-
-export interface PostModeration {
-  avatar: AvatarModeration
-  list: ModerationBehavior
-  thread: ModerationBehavior
-  view: ModerationBehavior
-}
-
-export interface ProfileModeration {
-  avatar: AvatarModeration
-  list: ModerationBehavior
-  view: ModerationBehavior
-}
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index e9ffe28c2..3b03cdca1 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -4,21 +4,9 @@ import AwaitLock from 'await-lock'
 import isEqual from 'lodash.isequal'
 import {isObj, hasProp} from 'lib/type-guards'
 import {RootStoreModel} from '../root-store'
-import {
-  ComAtprotoLabelDefs,
-  AppBskyActorDefs,
-  ModerationOpts,
-} from '@atproto/api'
-import {LabelValGroup} from 'lib/labeling/types'
-import {getLabelValueGroup} from 'lib/labeling/helpers'
-import {
-  UNKNOWN_LABEL_GROUP,
-  ILLEGAL_LABEL_GROUP,
-  ALWAYS_FILTER_LABEL_GROUP,
-  ALWAYS_WARN_LABEL_GROUP,
-} from 'lib/labeling/const'
+import {ModerationOpts} from '@atproto/api'
 import {DEFAULT_FEEDS} from 'lib/constants'
-import {isIOS, deviceLocales} from 'platform/detection'
+import {deviceLocales} from 'platform/detection'
 import {LANGUAGES} from '../../../locale/languages'
 
 // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
@@ -32,7 +20,7 @@ const LABEL_GROUPS = [
   'spam',
   'impersonation',
 ]
-const VISIBILITY_VALUES = ['show', 'warn', 'hide']
+const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
 const DEFAULT_LANG_CODES = (deviceLocales || [])
   .concat(['en', 'ja', 'pt', 'de'])
   .slice(0, 6)
@@ -52,7 +40,7 @@ export class LabelPreferencesModel {
 }
 
 export class PreferencesModel {
-  adultContentEnabled = !isIOS
+  adultContentEnabled = false
   contentLanguages: string[] = deviceLocales || []
   postLanguage: string = deviceLocales[0] || 'en'
   postLanguageHistory: string[] = DEFAULT_LANG_CODES
@@ -189,43 +177,32 @@ export class PreferencesModel {
     await this.lock.acquireAsync()
     try {
       // fetch preferences
-      let hasSavedFeedsPref = false
-      const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
+      const prefs = await this.rootStore.agent.getPreferences()
+
       runInAction(() => {
-        for (const pref of res.data.preferences) {
+        this.adultContentEnabled = prefs.adultContentEnabled
+        for (const label in prefs.contentLabels) {
           if (
-            AppBskyActorDefs.isAdultContentPref(pref) &&
-            AppBskyActorDefs.validateAdultContentPref(pref).success
-          ) {
-            this.adultContentEnabled = pref.enabled
-          } else if (
-            AppBskyActorDefs.isContentLabelPref(pref) &&
-            AppBskyActorDefs.validateAdultContentPref(pref).success
+            LABEL_GROUPS.includes(label) &&
+            VISIBILITY_VALUES.includes(prefs.contentLabels[label])
           ) {
-            if (
-              LABEL_GROUPS.includes(pref.label) &&
-              VISIBILITY_VALUES.includes(pref.visibility)
-            ) {
-              this.contentLabels[pref.label as keyof LabelPreferencesModel] =
-                pref.visibility as LabelPreference
-            }
-          } else if (
-            AppBskyActorDefs.isSavedFeedsPref(pref) &&
-            AppBskyActorDefs.validateSavedFeedsPref(pref).success
-          ) {
-            if (!isEqual(this.savedFeeds, pref.saved)) {
-              this.savedFeeds = pref.saved
-            }
-            if (!isEqual(this.pinnedFeeds, pref.pinned)) {
-              this.pinnedFeeds = pref.pinned
-            }
-            hasSavedFeedsPref = true
+            this.contentLabels[label as keyof LabelPreferencesModel] =
+              prefs.contentLabels[label]
           }
         }
+        if (prefs.feeds.saved && !isEqual(this.savedFeeds, prefs.feeds.saved)) {
+          this.savedFeeds = prefs.feeds.saved
+        }
+        if (
+          prefs.feeds.pinned &&
+          !isEqual(this.pinnedFeeds, prefs.feeds.pinned)
+        ) {
+          this.pinnedFeeds = prefs.feeds.pinned
+        }
       })
 
       // set defaults on missing items
-      if (!hasSavedFeedsPref) {
+      if (typeof prefs.feeds.saved === 'undefined') {
         const {saved, pinned} = await DEFAULT_FEEDS(
           this.rootStore.agent.service.toString(),
           (handle: string) =>
@@ -237,14 +214,7 @@ export class PreferencesModel {
           this.savedFeeds = saved
           this.pinnedFeeds = pinned
         })
-        res.data.preferences.push({
-          $type: 'app.bsky.actor.defs#savedFeedsPref',
-          saved,
-          pinned,
-        })
-        await this.rootStore.agent.app.bsky.actor.putPreferences({
-          preferences: res.data.preferences,
-        })
+        await this.rootStore.agent.setSavedFeeds(saved, pinned)
       }
     } finally {
       this.lock.release()
@@ -254,35 +224,6 @@ export class PreferencesModel {
   }
 
   /**
-   * This function updates the preferences of a user and allows for a callback function to be executed
-   * before the update.
-   * @param cb - cb is a callback function that takes in a single parameter of type
-   * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to
-   * update the preferences of the user. The function is called with the current preferences as an
-   * argument and if the callback returns false, the preferences are not updated.
-   * @returns void
-   */
-  async update(
-    cb: (
-      prefs: AppBskyActorDefs.Preferences,
-    ) => AppBskyActorDefs.Preferences | false,
-  ) {
-    await this.lock.acquireAsync()
-    try {
-      const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
-      const newPrefs = cb(res.data.preferences)
-      if (newPrefs === false) {
-        return
-      }
-      await this.rootStore.agent.app.bsky.actor.putPreferences({
-        preferences: newPrefs,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  /**
    * This function resets the preferences to an empty array of no preferences.
    */
   async reset() {
@@ -381,84 +322,12 @@ export class PreferencesModel {
     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,
-        })
-      }
-      return prefs
-    })
+    await this.rootStore.agent.setContentLabelPref(key, 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,
-        })
-      }
-      return prefs
-    })
-  }
-
-  getLabelPreference(labels: ComAtprotoLabelDefs.Label[] | undefined): {
-    pref: LabelPreference
-    desc: LabelValGroup
-  } {
-    let res: {pref: LabelPreference; desc: LabelValGroup} = {
-      pref: 'show',
-      desc: UNKNOWN_LABEL_GROUP,
-    }
-    if (!labels?.length) {
-      return res
-    }
-    for (const label of labels) {
-      const group = getLabelValueGroup(label.val)
-      if (group.id === 'illegal') {
-        return {pref: 'hide', desc: ILLEGAL_LABEL_GROUP}
-      } else if (group.id === 'always-filter') {
-        return {pref: 'hide', desc: ALWAYS_FILTER_LABEL_GROUP}
-      } else if (group.id === 'always-warn') {
-        res.pref = 'warn'
-        res.desc = ALWAYS_WARN_LABEL_GROUP
-        continue
-      } else if (group.id === 'unknown') {
-        continue
-      }
-      let pref = this.contentLabels[group.id]
-      if (pref === 'hide') {
-        res.pref = 'hide'
-        res.desc = group
-      } else if (pref === 'warn' && res.pref === 'show') {
-        res.pref = 'warn'
-        res.desc = group
-      }
-    }
-    if (res.desc.isAdultImagery && !this.adultContentEnabled) {
-      res.pref = 'hide'
-    }
-    return res
+    await this.rootStore.agent.setAdultContentEnabled(v)
   }
 
   get moderationOpts(): ModerationOpts {
@@ -499,31 +368,20 @@ export class PreferencesModel {
     }
   }
 
-  async setSavedFeeds(saved: string[], pinned: string[]) {
+  async _optimisticUpdateSavedFeeds(
+    saved: string[],
+    pinned: string[],
+    cb: () => Promise<{saved: string[]; pinned: string[]}>,
+  ) {
     const oldSaved = this.savedFeeds
     const oldPinned = this.pinnedFeeds
     this.savedFeeds = saved
     this.pinnedFeeds = pinned
     try {
-      await this.update((prefs: AppBskyActorDefs.Preferences) => {
-        let feedsPref = prefs.find(
-          pref =>
-            AppBskyActorDefs.isSavedFeedsPref(pref) &&
-            AppBskyActorDefs.validateSavedFeedsPref(pref).success,
-        )
-        if (feedsPref) {
-          feedsPref.saved = saved
-          feedsPref.pinned = pinned
-        } else {
-          feedsPref = {
-            $type: 'app.bsky.actor.defs#savedFeedsPref',
-            saved,
-            pinned,
-          }
-        }
-        return prefs
-          .filter(pref => !AppBskyActorDefs.isSavedFeedsPref(pref))
-          .concat([feedsPref])
+      const res = await cb()
+      runInAction(() => {
+        this.savedFeeds = res.saved
+        this.pinnedFeeds = res.pinned
       })
     } catch (e) {
       runInAction(() => {
@@ -534,25 +392,41 @@ export class PreferencesModel {
     }
   }
 
+  async setSavedFeeds(saved: string[], pinned: string[]) {
+    return this._optimisticUpdateSavedFeeds(saved, pinned, () =>
+      this.rootStore.agent.setSavedFeeds(saved, pinned),
+    )
+  }
+
   async addSavedFeed(v: string) {
-    return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds)
+    return this._optimisticUpdateSavedFeeds(
+      [...this.savedFeeds, v],
+      this.pinnedFeeds,
+      () => this.rootStore.agent.addSavedFeed(v),
+    )
   }
 
   async removeSavedFeed(v: string) {
-    return this.setSavedFeeds(
+    return this._optimisticUpdateSavedFeeds(
       this.savedFeeds.filter(uri => uri !== v),
       this.pinnedFeeds.filter(uri => uri !== v),
+      () => this.rootStore.agent.removeSavedFeed(v),
     )
   }
 
   async addPinnedFeed(v: string) {
-    return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v])
+    return this._optimisticUpdateSavedFeeds(
+      this.savedFeeds,
+      [...this.pinnedFeeds, v],
+      () => this.rootStore.agent.addPinnedFeed(v),
+    )
   }
 
   async removePinnedFeed(v: string) {
-    return this.setSavedFeeds(
+    return this._optimisticUpdateSavedFeeds(
       this.savedFeeds,
       this.pinnedFeeds.filter(uri => uri !== v),
+      () => this.rootStore.agent.removePinnedFeed(v),
     )
   }
 
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index 5215c9cb4..f39351feb 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -48,15 +48,17 @@ export const Component = observer(({}: {}) => {
       <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://bsky.app"
-                text="bsky.app"
-              />
-              .
-            </Text>
+            store.preferences.adultContentEnabled ? null : (
+              <Text type="md" style={pal.textLight}>
+                Adult content can only be enabled via the Web at{' '}
+                <TextLink
+                  style={pal.link}
+                  href="https://bsky.app"
+                  text="bsky.app"
+                />
+                .
+              </Text>
+            )
           ) : (
             <ToggleButton
               type="default-light"
@@ -188,7 +190,7 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
       />
       <SelectableBtn
         current={current}
-        value="show"
+        value="ignore"
         label="Show"
         right
         onChange={onChange}