about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/lib/labeling/const.ts89
-rw-r--r--src/lib/labeling/types.ts18
-rw-r--r--src/state/modals/index.tsx2
-rw-r--r--src/state/models/root-store.ts2
-rw-r--r--src/state/models/ui/create-account.ts1
-rw-r--r--src/state/models/ui/preferences.ts420
-rw-r--r--src/state/models/ui/saved-feeds.ts33
-rw-r--r--src/state/queries/feed.ts106
-rw-r--r--src/state/queries/preferences/const.ts27
-rw-r--r--src/state/queries/preferences/index.ts257
-rw-r--r--src/state/queries/preferences/moderation.ts163
-rw-r--r--src/state/queries/preferences/types.ts46
-rw-r--r--src/state/queries/preferences/util.ts16
-rw-r--r--src/view/com/auth/create/CreateAccount.tsx23
-rw-r--r--src/view/com/feeds/FeedSourceCard.tsx145
-rw-r--r--src/view/com/modals/BirthDateSettings.tsx52
-rw-r--r--src/view/com/modals/ContentFilteringSettings.tsx220
-rw-r--r--src/view/com/testing/TestCtrls.e2e.tsx6
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx143
-rw-r--r--src/view/screens/PreferencesThreads.tsx154
-rw-r--r--src/view/screens/SavedFeeds.tsx317
-rw-r--r--src/view/screens/Settings.tsx9
22 files changed, 1337 insertions, 912 deletions
diff --git a/src/lib/labeling/const.ts b/src/lib/labeling/const.ts
deleted file mode 100644
index 5c2e68137..000000000
--- a/src/lib/labeling/const.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import {LabelPreferencesModel} from 'state/models/ui/preferences'
-import {LabelValGroup} from './types'
-
-export const ILLEGAL_LABEL_GROUP: LabelValGroup = {
-  id: 'illegal',
-  title: 'Illegal Content',
-  warning: 'Illegal Content',
-  values: ['csam', 'dmca-violation', 'nudity-nonconsensual'],
-}
-
-export const ALWAYS_FILTER_LABEL_GROUP: LabelValGroup = {
-  id: 'always-filter',
-  title: 'Content Warning',
-  warning: 'Content Warning',
-  values: ['!filter'],
-}
-
-export const ALWAYS_WARN_LABEL_GROUP: LabelValGroup = {
-  id: 'always-warn',
-  title: 'Content Warning',
-  warning: 'Content Warning',
-  values: ['!warn', 'account-security'],
-}
-
-export const UNKNOWN_LABEL_GROUP: LabelValGroup = {
-  id: 'unknown',
-  title: 'Unknown Label',
-  warning: 'Content Warning',
-  values: [],
-}
-
-export const CONFIGURABLE_LABEL_GROUPS: Record<
-  keyof LabelPreferencesModel,
-  LabelValGroup
-> = {
-  nsfw: {
-    id: 'nsfw',
-    title: 'Explicit Sexual Images',
-    subtitle: 'i.e. pornography',
-    warning: 'Sexually Explicit',
-    values: ['porn', 'nsfl'],
-    isAdultImagery: true,
-  },
-  nudity: {
-    id: 'nudity',
-    title: 'Other Nudity',
-    subtitle: 'Including non-sexual and artistic',
-    warning: 'Nudity',
-    values: ['nudity'],
-    isAdultImagery: true,
-  },
-  suggestive: {
-    id: 'suggestive',
-    title: 'Sexually Suggestive',
-    subtitle: 'Does not include nudity',
-    warning: 'Sexually Suggestive',
-    values: ['sexual'],
-    isAdultImagery: true,
-  },
-  gore: {
-    id: 'gore',
-    title: 'Violent / Bloody',
-    subtitle: 'Gore, self-harm, torture',
-    warning: 'Violence',
-    values: ['gore', 'self-harm', 'torture', 'nsfl', 'corpse'],
-    isAdultImagery: true,
-  },
-  hate: {
-    id: 'hate',
-    title: 'Hate Group Iconography',
-    subtitle: 'Images of terror groups, articles covering events, etc.',
-    warning: 'Hate Groups',
-    values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
-  },
-  spam: {
-    id: 'spam',
-    title: 'Spam',
-    subtitle: 'Excessive unwanted interactions',
-    warning: 'Spam',
-    values: ['spam'],
-  },
-  impersonation: {
-    id: 'impersonation',
-    title: 'Impersonation',
-    subtitle: 'Accounts falsely claiming to be people or orgs',
-    warning: 'Impersonation',
-    values: ['impersonation'],
-  },
-}
diff --git a/src/lib/labeling/types.ts b/src/lib/labeling/types.ts
deleted file mode 100644
index 84d59be7f..000000000
--- a/src/lib/labeling/types.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {ComAtprotoLabelDefs} from '@atproto/api'
-import {LabelPreferencesModel} from 'state/models/ui/preferences'
-
-export type Label = ComAtprotoLabelDefs.Label
-
-export interface LabelValGroup {
-  id:
-    | keyof LabelPreferencesModel
-    | 'illegal'
-    | 'always-filter'
-    | 'always-warn'
-    | 'unknown'
-  title: string
-  isAdultImagery?: boolean
-  subtitle?: string
-  warning: string
-  values: string[]
-}
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index f9bd1e3c9..287bbe593 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -243,7 +243,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const closeModal = React.useCallback(() => {
     let totalActiveModals = 0
     setActiveModals(activeModals => {
-      activeModals.pop()
+      activeModals = activeModals.slice(0, -1)
       totalActiveModals = activeModals.length
       return activeModals
     })
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 4085a52c3..c07cf3078 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -101,7 +101,6 @@ export class RootStoreModel {
     this.agent = agent
     applyDebugHeader(this.agent)
     this.me.clear()
-    await this.preferences.sync()
     await this.me.load()
     if (!hadSession) {
       await resetNavigation()
@@ -137,7 +136,6 @@ export class RootStoreModel {
     }
     try {
       await this.me.updateIfNeeded()
-      await this.preferences.sync()
     } catch (e: any) {
       logger.error('Failed to fetch latest state', {error: e})
     }
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index 6d76784c1..60f4fc184 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -127,7 +127,6 @@ export class CreateAccountModel {
         password: this.password,
         inviteCode: this.inviteCode.trim(),
       })
-      /* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
       track('Create Account')
     } catch (e: any) {
       onboardingDispatch({type: 'skip'}) // undo starting the onboard
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 951486592..4f43487e7 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -1,19 +1,13 @@
-import {makeAutoObservable, runInAction} from 'mobx'
+import {makeAutoObservable} from 'mobx'
 import {
   LabelPreference as APILabelPreference,
   BskyFeedViewPreference,
   BskyThreadViewPreference,
 } from '@atproto/api'
 import AwaitLock from 'await-lock'
-import isEqual from 'lodash.isequal'
 import {isObj, hasProp} from 'lib/type-guards'
 import {RootStoreModel} from '../root-store'
 import {ModerationOpts} from '@atproto/api'
-import {DEFAULT_FEEDS} from 'lib/constants'
-import {getAge} from 'lib/strings/time'
-import {FeedTuner} from 'lib/api/feed-manip'
-import {logger} from '#/logger'
-import {getContentLanguages} from '#/state/preferences/languages'
 
 // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
 export type LabelPreference = APILabelPreference | 'show'
@@ -23,24 +17,6 @@ export type FeedViewPreference = BskyFeedViewPreference & {
 export type ThreadViewPreference = BskyThreadViewPreference & {
   lab_treeViewEnabled?: boolean | undefined
 }
-const LABEL_GROUPS = [
-  'nsfw',
-  'nudity',
-  'suggestive',
-  'gore',
-  'hate',
-  'spam',
-  'impersonation',
-]
-const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
-const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random']
-
-interface LegacyPreferences {
-  hideReplies?: boolean
-  hideRepliesByLikeCount?: number
-  hideReposts?: boolean
-  hideQuotePosts?: boolean
-}
 
 export class LabelPreferencesModel {
   nsfw: LabelPreference = 'hide'
@@ -76,9 +52,6 @@ export class PreferencesModel {
     lab_treeViewEnabled: false, // experimental
   }
 
-  // used to help with transitions from device-stored to server-stored preferences
-  legacyPreferences: LegacyPreferences | undefined
-
   // used to linearize async modifications to state
   lock = new AwaitLock()
 
@@ -86,13 +59,6 @@ export class PreferencesModel {
     makeAutoObservable(this, {lock: false}, {autoBind: true})
   }
 
-  get userAge(): number | undefined {
-    if (!this.birthDate) {
-      return undefined
-    }
-    return getAge(this.birthDate)
-  }
-
   serialize() {
     return {
       contentLabels: this.contentLabels,
@@ -128,117 +94,15 @@ export class PreferencesModel {
       ) {
         this.pinnedFeeds = v.pinnedFeeds
       }
-      // grab legacy values
-      this.legacyPreferences = getLegacyPreferences(v)
-    }
-  }
-
-  /**
-   * This function fetches preferences and sets defaults for missing items.
-   */
-  async sync() {
-    await this.lock.acquireAsync()
-    try {
-      // fetch preferences
-      const prefs = await this.rootStore.agent.getPreferences()
-
-      runInAction(() => {
-        if (prefs.feedViewPrefs.home) {
-          this.homeFeed = prefs.feedViewPrefs.home
-        }
-        this.thread = prefs.threadViewPrefs
-        this.adultContentEnabled = prefs.adultContentEnabled
-        for (const label in prefs.contentLabels) {
-          if (
-            LABEL_GROUPS.includes(label) &&
-            VISIBILITY_VALUES.includes(prefs.contentLabels[label])
-          ) {
-            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
-        }
-        this.birthDate = prefs.birthDate
-      })
-
-      // sync legacy values if needed
-      await this.syncLegacyPreferences()
-
-      // set defaults on missing items
-      if (typeof prefs.feeds.saved === 'undefined') {
-        try {
-          const {saved, pinned} = await DEFAULT_FEEDS(
-            this.rootStore.agent.service.toString(),
-            (handle: string) =>
-              this.rootStore.agent
-                .resolveHandle({handle})
-                .then(({data}) => data.did),
-          )
-          runInAction(() => {
-            this.savedFeeds = saved
-            this.pinnedFeeds = pinned
-          })
-          await this.rootStore.agent.setSavedFeeds(saved, pinned)
-        } catch (error) {
-          logger.error('Failed to set default feeds', {error})
-        }
-      }
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async syncLegacyPreferences() {
-    if (this.legacyPreferences) {
-      this.homeFeed = {...this.homeFeed, ...this.legacyPreferences}
-      this.legacyPreferences = undefined
-      await this.rootStore.agent.setFeedViewPrefs('home', this.homeFeed)
-    }
-  }
-
-  /**
-   * This function resets the preferences to an empty array of no preferences.
-   */
-  async reset() {
-    await this.lock.acquireAsync()
-    try {
-      runInAction(() => {
-        this.contentLabels = new LabelPreferencesModel()
-        this.savedFeeds = []
-        this.pinnedFeeds = []
-      })
-      await this.rootStore.agent.app.bsky.actor.putPreferences({
-        preferences: [],
-      })
-    } finally {
-      this.lock.release()
     }
   }
 
   // moderation
   // =
 
-  async setContentLabelPref(
-    key: keyof LabelPreferencesModel,
-    value: LabelPreference,
-  ) {
-    this.contentLabels[key] = value
-    await this.rootStore.agent.setContentLabelPref(key, value)
-  }
-
-  async setAdultContentEnabled(v: boolean) {
-    this.adultContentEnabled = v
-    await this.rootStore.agent.setAdultContentEnabled(v)
-  }
-
+  /**
+   * @deprecated use `getModerationOpts` from '#/state/queries/preferences/moderation' instead
+   */
   get moderationOpts(): ModerationOpts {
     return {
       userDid: this.rootStore.session.currentSession?.did || '',
@@ -284,274 +148,32 @@ export class PreferencesModel {
     return this.pinnedFeeds.includes(uri)
   }
 
-  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
-    await this.lock.acquireAsync()
-    try {
-      const res = await cb()
-      runInAction(() => {
-        this.savedFeeds = res.saved
-        this.pinnedFeeds = res.pinned
-      })
-    } catch (e) {
-      runInAction(() => {
-        this.savedFeeds = oldSaved
-        this.pinnedFeeds = oldPinned
-      })
-      throw e
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async setSavedFeeds(saved: string[], pinned: string[]) {
-    return this._optimisticUpdateSavedFeeds(saved, pinned, () =>
-      this.rootStore.agent.setSavedFeeds(saved, pinned),
-    )
-  }
-
-  async addSavedFeed(v: string) {
-    return this._optimisticUpdateSavedFeeds(
-      [...this.savedFeeds.filter(uri => uri !== v), v],
-      this.pinnedFeeds,
-      () => this.rootStore.agent.addSavedFeed(v),
-    )
-  }
-
-  async removeSavedFeed(v: string) {
-    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._optimisticUpdateSavedFeeds(
-      [...this.savedFeeds.filter(uri => uri !== v), v],
-      [...this.pinnedFeeds.filter(uri => uri !== v), v],
-      () => this.rootStore.agent.addPinnedFeed(v),
-    )
-  }
-
-  async removePinnedFeed(v: string) {
-    return this._optimisticUpdateSavedFeeds(
-      this.savedFeeds,
-      this.pinnedFeeds.filter(uri => uri !== v),
-      () => this.rootStore.agent.removePinnedFeed(v),
-    )
-  }
-
-  // other
-  // =
-
-  async setBirthDate(birthDate: Date) {
-    this.birthDate = birthDate
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setPersonalDetails({birthDate})
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideReplies() {
-    this.homeFeed.hideReplies = !this.homeFeed.hideReplies
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideReplies: this.homeFeed.hideReplies,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideRepliesByUnfollowed() {
-    this.homeFeed.hideRepliesByUnfollowed =
-      !this.homeFeed.hideRepliesByUnfollowed
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideRepliesByUnfollowed: this.homeFeed.hideRepliesByUnfollowed,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async setHomeFeedHideRepliesByLikeCount(threshold: number) {
-    this.homeFeed.hideRepliesByLikeCount = threshold
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideRepliesByLikeCount: this.homeFeed.hideRepliesByLikeCount,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideReposts() {
-    this.homeFeed.hideReposts = !this.homeFeed.hideReposts
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideReposts: this.homeFeed.hideReposts,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideQuotePosts() {
-    this.homeFeed.hideQuotePosts = !this.homeFeed.hideQuotePosts
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideQuotePosts: this.homeFeed.hideQuotePosts,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedMergeFeedEnabled() {
-    this.homeFeed.lab_mergeFeedEnabled = !this.homeFeed.lab_mergeFeedEnabled
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        lab_mergeFeedEnabled: this.homeFeed.lab_mergeFeedEnabled,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async setThreadSort(v: string) {
-    if (THREAD_SORT_VALUES.includes(v)) {
-      this.thread.sort = v
-      await this.lock.acquireAsync()
-      try {
-        await this.rootStore.agent.setThreadViewPrefs({sort: v})
-      } finally {
-        this.lock.release()
-      }
-    }
-  }
-
-  async togglePrioritizedFollowedUsers() {
-    this.thread.prioritizeFollowedUsers = !this.thread.prioritizeFollowedUsers
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setThreadViewPrefs({
-        prioritizeFollowedUsers: this.thread.prioritizeFollowedUsers,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleThreadTreeViewEnabled() {
-    this.thread.lab_treeViewEnabled = !this.thread.lab_treeViewEnabled
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setThreadViewPrefs({
-        lab_treeViewEnabled: this.thread.lab_treeViewEnabled,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  getFeedTuners(
-    feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes',
-  ) {
-    if (feedType === 'custom') {
-      return [
-        FeedTuner.dedupReposts,
-        FeedTuner.preferredLangOnly(getContentLanguages()),
-      ]
-    }
-    if (feedType === 'list') {
-      return [FeedTuner.dedupReposts]
-    }
-    if (feedType === 'home' || feedType === 'following') {
-      const feedTuners = []
-
-      if (this.homeFeed.hideReposts) {
-        feedTuners.push(FeedTuner.removeReposts)
-      } else {
-        feedTuners.push(FeedTuner.dedupReposts)
-      }
+  /**
+   * @deprecated use `useAddSavedFeedMutation` from `#/state/queries/preferences` instead
+   */
+  async addSavedFeed(_v: string) {}
 
-      if (this.homeFeed.hideReplies) {
-        feedTuners.push(FeedTuner.removeReplies)
-      } else {
-        feedTuners.push(
-          FeedTuner.thresholdRepliesOnly({
-            userDid: this.rootStore.session.data?.did || '',
-            minLikes: this.homeFeed.hideRepliesByLikeCount,
-            followedOnly: !!this.homeFeed.hideRepliesByUnfollowed,
-          }),
-        )
-      }
+  /**
+   * @deprecated use `useRemoveSavedFeedMutation` from `#/state/queries/preferences` instead
+   */
+  async removeSavedFeed(_v: string) {}
 
-      if (this.homeFeed.hideQuotePosts) {
-        feedTuners.push(FeedTuner.removeQuotePosts)
-      }
+  /**
+   * @deprecated use `usePinFeedMutation` from `#/state/queries/preferences` instead
+   */
+  async addPinnedFeed(_v: string) {}
 
-      return feedTuners
-    }
-    return []
-  }
+  /**
+   * @deprecated use `useUnpinFeedMutation` from `#/state/queries/preferences` instead
+   */
+  async removePinnedFeed(_v: string) {}
 }
 
 // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
+// TODO do we need this?
 function tempfixLabelPref(pref: LabelPreference): APILabelPreference {
   if (pref === 'show') {
     return 'ignore'
   }
   return pref
 }
-
-function getLegacyPreferences(
-  v: Record<string, unknown>,
-): LegacyPreferences | undefined {
-  const legacyPreferences: LegacyPreferences = {}
-  if (
-    hasProp(v, 'homeFeedRepliesEnabled') &&
-    typeof v.homeFeedRepliesEnabled === 'boolean'
-  ) {
-    legacyPreferences.hideReplies = !v.homeFeedRepliesEnabled
-  }
-  if (
-    hasProp(v, 'homeFeedRepliesThreshold') &&
-    typeof v.homeFeedRepliesThreshold === 'number'
-  ) {
-    legacyPreferences.hideRepliesByLikeCount = v.homeFeedRepliesThreshold
-  }
-  if (
-    hasProp(v, 'homeFeedRepostsEnabled') &&
-    typeof v.homeFeedRepostsEnabled === 'boolean'
-  ) {
-    legacyPreferences.hideReposts = !v.homeFeedRepostsEnabled
-  }
-  if (
-    hasProp(v, 'homeFeedQuotePostsEnabled') &&
-    typeof v.homeFeedQuotePostsEnabled === 'boolean'
-  ) {
-    legacyPreferences.hideQuotePosts = !v.homeFeedQuotePostsEnabled
-  }
-  if (Object.keys(legacyPreferences).length) {
-    return legacyPreferences
-  }
-  return undefined
-}
diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts
index 624da4f5f..cf4cf6d71 100644
--- a/src/state/models/ui/saved-feeds.ts
+++ b/src/state/models/ui/saved-feeds.ts
@@ -3,7 +3,6 @@ import {RootStoreModel} from '../root-store'
 import {bundleAsync} from 'lib/async/bundle'
 import {cleanError} from 'lib/strings/errors'
 import {FeedSourceModel} from '../content/feed-source'
-import {track} from 'lib/analytics/analytics'
 import {logger} from '#/logger'
 
 export class SavedFeedsModel {
@@ -69,7 +68,6 @@ export class SavedFeedsModel {
   refresh = bundleAsync(async () => {
     this._xLoading(true)
     try {
-      await this.rootStore.preferences.sync()
       const uris = dedup(
         this.rootStore.preferences.pinnedFeeds.concat(
           this.rootStore.preferences.savedFeeds,
@@ -87,37 +85,6 @@ export class SavedFeedsModel {
     }
   })
 
-  async reorderPinnedFeeds(feeds: FeedSourceModel[]) {
-    this._updatePinSortOrder(feeds.map(f => f.uri))
-    await this.rootStore.preferences.setSavedFeeds(
-      this.rootStore.preferences.savedFeeds,
-      feeds.filter(feed => feed.isPinned).map(feed => feed.uri),
-    )
-  }
-
-  async movePinnedFeed(item: FeedSourceModel, direction: 'up' | 'down') {
-    const pinned = this.rootStore.preferences.pinnedFeeds.slice()
-    const index = pinned.indexOf(item.uri)
-    if (index === -1) {
-      return
-    }
-    if (direction === 'up' && index !== 0) {
-      ;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
-    } else if (direction === 'down' && index < pinned.length - 1) {
-      ;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
-    }
-    this._updatePinSortOrder(pinned.concat(this.unpinned.map(f => f.uri)))
-    await this.rootStore.preferences.setSavedFeeds(
-      this.rootStore.preferences.savedFeeds,
-      pinned,
-    )
-    track('CustomFeed:Reorder', {
-      name: item.displayName,
-      uri: item.uri,
-      index: pinned.indexOf(item.uri),
-    })
-  }
-
   // state transitions
   // =
 
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
new file mode 100644
index 000000000..0ba323314
--- /dev/null
+++ b/src/state/queries/feed.ts
@@ -0,0 +1,106 @@
+import {useQuery} from '@tanstack/react-query'
+import {AtUri, RichText, AppBskyFeedDefs, AppBskyGraphDefs} from '@atproto/api'
+
+import {sanitizeDisplayName} from '#/lib/strings/display-names'
+import {sanitizeHandle} from '#/lib/strings/handles'
+import {useSession} from '#/state/session'
+
+type FeedSourceInfo =
+  | {
+      type: 'feed'
+      uri: string
+      cid: string
+      avatar: string | undefined
+      displayName: string
+      description: RichText
+      creatorDid: string
+      creatorHandle: string
+      likeCount: number | undefined
+      likeUri: string | undefined
+    }
+  | {
+      type: 'list'
+      uri: string
+      cid: string
+      avatar: string | undefined
+      displayName: string
+      description: RichText
+      creatorDid: string
+      creatorHandle: string
+    }
+
+export const useFeedSourceInfoQueryKey = ({uri}: {uri: string}) => [
+  'getFeedSourceInfo',
+  uri,
+]
+
+const feedSourceNSIDs = {
+  feed: 'app.bsky.feed.generator',
+  list: 'app.bsky.graph.list',
+}
+
+function hydrateFeedGenerator(
+  view: AppBskyFeedDefs.GeneratorView,
+): FeedSourceInfo {
+  return {
+    type: 'feed',
+    uri: view.uri,
+    cid: view.cid,
+    avatar: view.avatar,
+    displayName: view.displayName
+      ? sanitizeDisplayName(view.displayName)
+      : `Feed by ${sanitizeHandle(view.creator.handle, '@')}`,
+    description: new RichText({
+      text: view.description || '',
+      facets: (view.descriptionFacets || [])?.slice(),
+    }),
+    creatorDid: view.creator.did,
+    creatorHandle: view.creator.handle,
+    likeCount: view.likeCount,
+    likeUri: view.viewer?.like,
+  }
+}
+
+function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
+  return {
+    type: 'list',
+    uri: view.uri,
+    cid: view.cid,
+    avatar: view.avatar,
+    description: new RichText({
+      text: view.description || '',
+      facets: (view.descriptionFacets || [])?.slice(),
+    }),
+    creatorDid: view.creator.did,
+    creatorHandle: view.creator.handle,
+    displayName: view.name
+      ? sanitizeDisplayName(view.name)
+      : `User List by ${sanitizeHandle(view.creator.handle, '@')}`,
+  }
+}
+
+export function useFeedSourceInfoQuery({uri}: {uri: string}) {
+  const {agent} = useSession()
+  const {pathname} = new AtUri(uri)
+  const type = pathname.includes(feedSourceNSIDs.feed) ? 'feed' : 'list'
+
+  return useQuery({
+    queryKey: useFeedSourceInfoQueryKey({uri}),
+    queryFn: async () => {
+      let view: FeedSourceInfo
+
+      if (type === 'feed') {
+        const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri})
+        view = hydrateFeedGenerator(res.data.view)
+      } else {
+        const res = await agent.app.bsky.graph.getList({
+          list: uri,
+          limit: 1,
+        })
+        view = hydrateList(res.data.list)
+      }
+
+      return view
+    },
+  })
+}
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
new file mode 100644
index 000000000..5db137e58
--- /dev/null
+++ b/src/state/queries/preferences/const.ts
@@ -0,0 +1,27 @@
+import {
+  UsePreferencesQueryResponse,
+  ThreadViewPreferences,
+} from '#/state/queries/preferences/types'
+
+export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
+  {
+    hideReplies: false,
+    hideRepliesByUnfollowed: false,
+    hideRepliesByLikeCount: 0,
+    hideReposts: false,
+    hideQuotePosts: false,
+    lab_mergeFeedEnabled: false, // experimental
+  }
+
+export const DEFAULT_THREAD_VIEW_PREFS: ThreadViewPreferences = {
+  sort: 'newest',
+  prioritizeFollowedUsers: true,
+  lab_treeViewEnabled: false,
+}
+
+const DEFAULT_PROD_FEED_PREFIX = (rkey: string) =>
+  `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
+export const DEFAULT_PROD_FEEDS = {
+  pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
+  saved: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
+}
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
new file mode 100644
index 000000000..d64bbd954
--- /dev/null
+++ b/src/state/queries/preferences/index.ts
@@ -0,0 +1,257 @@
+import {useQuery, useMutation, useQueryClient} from '@tanstack/react-query'
+import {LabelPreference, BskyFeedViewPreference} from '@atproto/api'
+
+import {track} from '#/lib/analytics/analytics'
+import {getAge} from '#/lib/strings/time'
+import {useSession} from '#/state/session'
+import {DEFAULT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
+import {
+  ConfigurableLabelGroup,
+  UsePreferencesQueryResponse,
+  ThreadViewPreferences,
+} from '#/state/queries/preferences/types'
+import {temp__migrateLabelPref} from '#/state/queries/preferences/util'
+import {
+  DEFAULT_HOME_FEED_PREFS,
+  DEFAULT_THREAD_VIEW_PREFS,
+} from '#/state/queries/preferences/const'
+
+export * from '#/state/queries/preferences/types'
+export * from '#/state/queries/preferences/moderation'
+export * from '#/state/queries/preferences/const'
+
+export const usePreferencesQueryKey = ['getPreferences']
+
+export function usePreferencesQuery() {
+  const {agent} = useSession()
+  return useQuery({
+    queryKey: usePreferencesQueryKey,
+    queryFn: async () => {
+      const res = await agent.getPreferences()
+      const preferences: UsePreferencesQueryResponse = {
+        ...res,
+        feeds: {
+          saved: res.feeds?.saved || [],
+          pinned: res.feeds?.pinned || [],
+          unpinned:
+            res.feeds.saved?.filter(f => {
+              return !res.feeds.pinned?.includes(f)
+            }) || [],
+        },
+        // labels are undefined until set by user
+        contentLabels: {
+          nsfw: temp__migrateLabelPref(
+            res.contentLabels?.nsfw || DEFAULT_LABEL_PREFERENCES.nsfw,
+          ),
+          nudity: temp__migrateLabelPref(
+            res.contentLabels?.nudity || DEFAULT_LABEL_PREFERENCES.nudity,
+          ),
+          suggestive: temp__migrateLabelPref(
+            res.contentLabels?.suggestive ||
+              DEFAULT_LABEL_PREFERENCES.suggestive,
+          ),
+          gore: temp__migrateLabelPref(
+            res.contentLabels?.gore || DEFAULT_LABEL_PREFERENCES.gore,
+          ),
+          hate: temp__migrateLabelPref(
+            res.contentLabels?.hate || DEFAULT_LABEL_PREFERENCES.hate,
+          ),
+          spam: temp__migrateLabelPref(
+            res.contentLabels?.spam || DEFAULT_LABEL_PREFERENCES.spam,
+          ),
+          impersonation: temp__migrateLabelPref(
+            res.contentLabels?.impersonation ||
+              DEFAULT_LABEL_PREFERENCES.impersonation,
+          ),
+        },
+        feedViewPrefs: {
+          ...DEFAULT_HOME_FEED_PREFS,
+          ...(res.feedViewPrefs.home || {}),
+        },
+        threadViewPrefs: {
+          ...DEFAULT_THREAD_VIEW_PREFS,
+          ...(res.threadViewPrefs ?? {}),
+        },
+        userAge: res.birthDate ? getAge(res.birthDate) : undefined,
+      }
+      return preferences
+    },
+  })
+}
+
+export function useClearPreferencesMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation({
+    mutationFn: async () => {
+      await agent.app.bsky.actor.putPreferences({preferences: []})
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function usePreferencesSetContentLabelMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<
+    void,
+    unknown,
+    {labelGroup: ConfigurableLabelGroup; visibility: LabelPreference}
+  >({
+    mutationFn: async ({labelGroup, visibility}) => {
+      await agent.setContentLabelPref(labelGroup, visibility)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function usePreferencesSetAdultContentMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, {enabled: boolean}>({
+    mutationFn: async ({enabled}) => {
+      await agent.setAdultContentEnabled(enabled)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function usePreferencesSetBirthDateMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, {birthDate: Date}>({
+    mutationFn: async ({birthDate}: {birthDate: Date}) => {
+      await agent.setPersonalDetails({birthDate})
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useSetFeedViewPreferencesMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
+    mutationFn: async prefs => {
+      await agent.setFeedViewPrefs('home', prefs)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useSetThreadViewPreferencesMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, Partial<ThreadViewPreferences>>({
+    mutationFn: async prefs => {
+      await agent.setThreadViewPrefs(prefs)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useSetSaveFeedsMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<
+    void,
+    unknown,
+    Pick<UsePreferencesQueryResponse['feeds'], 'saved' | 'pinned'>
+  >({
+    mutationFn: async ({saved, pinned}) => {
+      await agent.setSavedFeeds(saved, pinned)
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useSaveFeedMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, {uri: string}>({
+    mutationFn: async ({uri}) => {
+      await agent.addSavedFeed(uri)
+      track('CustomFeed:Save')
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useRemoveFeedMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, {uri: string}>({
+    mutationFn: async ({uri}) => {
+      await agent.removeSavedFeed(uri)
+      track('CustomFeed:Unsave')
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function usePinFeedMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, {uri: string}>({
+    mutationFn: async ({uri}) => {
+      await agent.addPinnedFeed(uri)
+      track('CustomFeed:Pin', {uri})
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
+
+export function useUnpinFeedMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+
+  return useMutation<void, unknown, {uri: string}>({
+    mutationFn: async ({uri}) => {
+      await agent.removePinnedFeed(uri)
+      track('CustomFeed:Unpin', {uri})
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: usePreferencesQueryKey,
+      })
+    },
+  })
+}
diff --git a/src/state/queries/preferences/moderation.ts b/src/state/queries/preferences/moderation.ts
new file mode 100644
index 000000000..a26380a36
--- /dev/null
+++ b/src/state/queries/preferences/moderation.ts
@@ -0,0 +1,163 @@
+import {
+  LabelPreference,
+  ComAtprotoLabelDefs,
+  ModerationOpts,
+} from '@atproto/api'
+
+import {
+  LabelGroup,
+  ConfigurableLabelGroup,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences/types'
+
+export type Label = ComAtprotoLabelDefs.Label
+
+export type LabelGroupConfig = {
+  id: LabelGroup
+  title: string
+  isAdultImagery?: boolean
+  subtitle?: string
+  warning: string
+  values: string[]
+}
+
+export const DEFAULT_LABEL_PREFERENCES: Record<
+  ConfigurableLabelGroup,
+  LabelPreference
+> = {
+  nsfw: 'hide',
+  nudity: 'warn',
+  suggestive: 'warn',
+  gore: 'warn',
+  hate: 'hide',
+  spam: 'hide',
+  impersonation: 'hide',
+}
+
+export const ILLEGAL_LABEL_GROUP: LabelGroupConfig = {
+  id: 'illegal',
+  title: 'Illegal Content',
+  warning: 'Illegal Content',
+  values: ['csam', 'dmca-violation', 'nudity-nonconsensual'],
+}
+
+export const ALWAYS_FILTER_LABEL_GROUP: LabelGroupConfig = {
+  id: 'always-filter',
+  title: 'Content Warning',
+  warning: 'Content Warning',
+  values: ['!filter'],
+}
+
+export const ALWAYS_WARN_LABEL_GROUP: LabelGroupConfig = {
+  id: 'always-warn',
+  title: 'Content Warning',
+  warning: 'Content Warning',
+  values: ['!warn', 'account-security'],
+}
+
+export const UNKNOWN_LABEL_GROUP: LabelGroupConfig = {
+  id: 'unknown',
+  title: 'Unknown Label',
+  warning: 'Content Warning',
+  values: [],
+}
+
+export const CONFIGURABLE_LABEL_GROUPS: Record<
+  ConfigurableLabelGroup,
+  LabelGroupConfig
+> = {
+  nsfw: {
+    id: 'nsfw',
+    title: 'Explicit Sexual Images',
+    subtitle: 'i.e. pornography',
+    warning: 'Sexually Explicit',
+    values: ['porn', 'nsfl'],
+    isAdultImagery: true,
+  },
+  nudity: {
+    id: 'nudity',
+    title: 'Other Nudity',
+    subtitle: 'Including non-sexual and artistic',
+    warning: 'Nudity',
+    values: ['nudity'],
+    isAdultImagery: true,
+  },
+  suggestive: {
+    id: 'suggestive',
+    title: 'Sexually Suggestive',
+    subtitle: 'Does not include nudity',
+    warning: 'Sexually Suggestive',
+    values: ['sexual'],
+    isAdultImagery: true,
+  },
+  gore: {
+    id: 'gore',
+    title: 'Violent / Bloody',
+    subtitle: 'Gore, self-harm, torture',
+    warning: 'Violence',
+    values: ['gore', 'self-harm', 'torture', 'nsfl', 'corpse'],
+    isAdultImagery: true,
+  },
+  hate: {
+    id: 'hate',
+    title: 'Hate Group Iconography',
+    subtitle: 'Images of terror groups, articles covering events, etc.',
+    warning: 'Hate Groups',
+    values: ['icon-kkk', 'icon-nazi', 'icon-intolerant', 'behavior-intolerant'],
+  },
+  spam: {
+    id: 'spam',
+    title: 'Spam',
+    subtitle: 'Excessive unwanted interactions',
+    warning: 'Spam',
+    values: ['spam'],
+  },
+  impersonation: {
+    id: 'impersonation',
+    title: 'Impersonation',
+    subtitle: 'Accounts falsely claiming to be people or orgs',
+    warning: 'Impersonation',
+    values: ['impersonation'],
+  },
+}
+
+export function getModerationOpts({
+  userDid,
+  preferences,
+}: {
+  userDid: string
+  preferences: UsePreferencesQueryResponse
+}): ModerationOpts {
+  return {
+    userDid: userDid,
+    adultContentEnabled: preferences.adultContentEnabled,
+    labels: {
+      porn: preferences.contentLabels.nsfw,
+      sexual: preferences.contentLabels.suggestive,
+      nudity: preferences.contentLabels.nudity,
+      nsfl: preferences.contentLabels.gore,
+      corpse: preferences.contentLabels.gore,
+      gore: preferences.contentLabels.gore,
+      torture: preferences.contentLabels.gore,
+      'self-harm': preferences.contentLabels.gore,
+      'intolerant-race': preferences.contentLabels.hate,
+      'intolerant-gender': preferences.contentLabels.hate,
+      'intolerant-sexual-orientation': preferences.contentLabels.hate,
+      'intolerant-religion': preferences.contentLabels.hate,
+      intolerant: preferences.contentLabels.hate,
+      'icon-intolerant': preferences.contentLabels.hate,
+      spam: preferences.contentLabels.spam,
+      impersonation: preferences.contentLabels.impersonation,
+      scam: 'warn',
+    },
+    labelers: [
+      {
+        labeler: {
+          did: '',
+          displayName: 'Bluesky Social',
+        },
+        labels: {},
+      },
+    ],
+  }
+}
diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts
new file mode 100644
index 000000000..9f4c30e53
--- /dev/null
+++ b/src/state/queries/preferences/types.ts
@@ -0,0 +1,46 @@
+import {
+  BskyPreferences,
+  LabelPreference,
+  BskyThreadViewPreference,
+} from '@atproto/api'
+
+export type ConfigurableLabelGroup =
+  | 'nsfw'
+  | 'nudity'
+  | 'suggestive'
+  | 'gore'
+  | 'hate'
+  | 'spam'
+  | 'impersonation'
+export type LabelGroup =
+  | ConfigurableLabelGroup
+  | 'illegal'
+  | 'always-filter'
+  | 'always-warn'
+  | 'unknown'
+
+export type UsePreferencesQueryResponse = Omit<
+  BskyPreferences,
+  'contentLabels' | 'feedViewPrefs' | 'feeds'
+> & {
+  /*
+   * Content labels previously included 'show', which has been deprecated in
+   * favor of 'ignore'. The API can return legacy data from the database, and
+   * we clean up the data in `usePreferencesQuery`.
+   */
+  contentLabels: Record<ConfigurableLabelGroup, LabelPreference>
+  feedViewPrefs: BskyPreferences['feedViewPrefs']['home']
+  /**
+   * User thread-view prefs, including newer fields that may not be typed yet.
+   */
+  threadViewPrefs: ThreadViewPreferences
+  userAge: number | undefined
+  feeds: Required<BskyPreferences['feeds']> & {
+    unpinned: string[]
+  }
+}
+
+export type ThreadViewPreferences = Omit<BskyThreadViewPreference, 'sort'> & {
+  sort: 'oldest' | 'newest' | 'most-likes' | 'random' | string
+  lab_treeViewEnabled: boolean
+}
diff --git a/src/state/queries/preferences/util.ts b/src/state/queries/preferences/util.ts
new file mode 100644
index 000000000..7b8160c28
--- /dev/null
+++ b/src/state/queries/preferences/util.ts
@@ -0,0 +1,16 @@
+import {LabelPreference} from '@atproto/api'
+
+/**
+ * Content labels previously included 'show', which has been deprecated in
+ * favor of 'ignore'. The API can return legacy data from the database, and
+ * we clean up the data in `usePreferencesQuery`.
+ *
+ * @deprecated
+ */
+export function temp__migrateLabelPref(
+  pref: LabelPreference | 'show',
+): LabelPreference {
+  // @ts-ignore
+  if (pref === 'show') return 'ignore'
+  return pref
+}
diff --git a/src/view/com/auth/create/CreateAccount.tsx b/src/view/com/auth/create/CreateAccount.tsx
index 65f9ba26d..0f3ff41af 100644
--- a/src/view/com/auth/create/CreateAccount.tsx
+++ b/src/view/com/auth/create/CreateAccount.tsx
@@ -19,6 +19,12 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useOnboardingDispatch} from '#/state/shell'
 import {useSessionApi} from '#/state/session'
+import {
+  usePreferencesSetBirthDateMutation,
+  useSetSaveFeedsMutation,
+  DEFAULT_PROD_FEEDS,
+} from '#/state/queries/preferences'
+import {IS_PROD} from '#/lib/constants'
 
 import {Step1} from './Step1'
 import {Step2} from './Step2'
@@ -36,6 +42,8 @@ export const CreateAccount = observer(function CreateAccountImpl({
   const {_} = useLingui()
   const onboardingDispatch = useOnboardingDispatch()
   const {createAccount} = useSessionApi()
+  const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
+  const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
 
   React.useEffect(() => {
     screen('CreateAccount')
@@ -70,13 +78,26 @@ export const CreateAccount = observer(function CreateAccountImpl({
           onboardingDispatch,
           createAccount,
         })
+
+        setBirthDate({birthDate: model.birthDate})
+
+        if (IS_PROD(model.serviceUrl)) {
+          setSavedFeeds(DEFAULT_PROD_FEEDS)
+        }
       } catch {
         // dont need to handle here
       } finally {
         track('Try Create Account')
       }
     }
-  }, [model, track, onboardingDispatch, createAccount])
+  }, [
+    model,
+    track,
+    onboardingDispatch,
+    createAccount,
+    setBirthDate,
+    setSavedFeeds,
+  ])
 
   return (
     <LoggedOutLayout
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 63af52619..6f9687be5 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -16,6 +16,151 @@ import * as Toast from 'view/com/util/Toast'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {logger} from '#/logger'
 import {useModalControls} from '#/state/modals'
+import {
+  usePreferencesQuery,
+  useSaveFeedMutation,
+  useRemoveFeedMutation,
+} from '#/state/queries/preferences'
+import {useFeedSourceInfoQuery} from '#/state/queries/feed'
+
+export const NewFeedSourceCard = observer(function FeedSourceCardImpl({
+  feedUri,
+  style,
+  showSaveBtn = false,
+  showDescription = false,
+  showLikes = false,
+}: {
+  feedUri: string
+  style?: StyleProp<ViewStyle>
+  showSaveBtn?: boolean
+  showDescription?: boolean
+  showLikes?: boolean
+}) {
+  const pal = usePalette('default')
+  const navigation = useNavigation<NavigationProp>()
+  const {openModal} = useModalControls()
+  const {data: preferences} = usePreferencesQuery()
+  const {data: info} = useFeedSourceInfoQuery({uri: feedUri})
+  const {isPending: isSavePending, mutateAsync: saveFeed} =
+    useSaveFeedMutation()
+  const {isPending: isRemovePending, mutateAsync: removeFeed} =
+    useRemoveFeedMutation()
+
+  const isSaved = Boolean(preferences?.feeds?.saved?.includes(feedUri))
+
+  const onToggleSaved = React.useCallback(async () => {
+    // Only feeds can be un/saved, lists are handled elsewhere
+    if (info?.type !== 'feed') return
+
+    if (isSaved) {
+      openModal({
+        name: 'confirm',
+        title: 'Remove from my feeds',
+        message: `Remove ${info?.displayName} from my feeds?`,
+        onPressConfirm: async () => {
+          try {
+            await removeFeed({uri: feedUri})
+            // await item.unsave()
+            Toast.show('Removed from my feeds')
+          } catch (e) {
+            Toast.show('There was an issue contacting your server')
+            logger.error('Failed to unsave feed', {error: e})
+          }
+        },
+      })
+    } else {
+      try {
+        await saveFeed({uri: feedUri})
+        Toast.show('Added to my feeds')
+      } catch (e) {
+        Toast.show('There was an issue contacting your server')
+        logger.error('Failed to save feed', {error: e})
+      }
+    }
+  }, [isSaved, openModal, info, feedUri, removeFeed, saveFeed])
+
+  if (!info || !preferences) return null
+
+  return (
+    <Pressable
+      testID={`feed-${info.displayName}`}
+      accessibilityRole="button"
+      style={[styles.container, pal.border, style]}
+      onPress={() => {
+        if (info.type === 'feed') {
+          navigation.push('ProfileFeed', {
+            name: info.creatorDid,
+            rkey: new AtUri(info.uri).rkey,
+          })
+        } else if (info.type === 'list') {
+          navigation.push('ProfileList', {
+            name: info.creatorDid,
+            rkey: new AtUri(info.uri).rkey,
+          })
+        }
+      }}
+      key={info.uri}>
+      <View style={[styles.headerContainer]}>
+        <View style={[s.mr10]}>
+          <UserAvatar type="algo" size={36} avatar={info.avatar} />
+        </View>
+        <View style={[styles.headerTextContainer]}>
+          <Text style={[pal.text, s.bold]} numberOfLines={3}>
+            {info.displayName}
+          </Text>
+          <Text style={[pal.textLight]} numberOfLines={3}>
+            {info.type === 'feed' ? 'Feed' : 'List'} by{' '}
+            {sanitizeHandle(info.creatorHandle, '@')}
+          </Text>
+        </View>
+
+        {showSaveBtn && info.type === 'feed' && (
+          <View>
+            <Pressable
+              disabled={isSavePending || isRemovePending}
+              accessibilityRole="button"
+              accessibilityLabel={
+                isSaved ? 'Remove from my feeds' : 'Add to my feeds'
+              }
+              accessibilityHint=""
+              onPress={onToggleSaved}
+              hitSlop={15}
+              style={styles.btn}>
+              {isSaved ? (
+                <FontAwesomeIcon
+                  icon={['far', 'trash-can']}
+                  size={19}
+                  color={pal.colors.icon}
+                />
+              ) : (
+                <FontAwesomeIcon
+                  icon="plus"
+                  size={18}
+                  color={pal.colors.link}
+                />
+              )}
+            </Pressable>
+          </View>
+        )}
+      </View>
+
+      {showDescription && info.description ? (
+        <RichText
+          style={[pal.textLight, styles.description]}
+          richText={info.description}
+          numberOfLines={3}
+        />
+      ) : null}
+
+      {showLikes && info.type === 'feed' ? (
+        <Text type="sm-medium" style={[pal.text, pal.textLight]}>
+          Liked by {info.likeCount || 0}{' '}
+          {pluralize(info.likeCount || 0, 'user')}
+        </Text>
+      ) : null}
+    </Pressable>
+  )
+})
 
 export const FeedSourceCard = observer(function FeedSourceCardImpl({
   item,
diff --git a/src/view/com/modals/BirthDateSettings.tsx b/src/view/com/modals/BirthDateSettings.tsx
index 6655b7a6b..9996c5641 100644
--- a/src/view/com/modals/BirthDateSettings.tsx
+++ b/src/view/com/modals/BirthDateSettings.tsx
@@ -9,7 +9,6 @@ import {observer} from 'mobx-react-lite'
 import {Text} from '../util/text/Text'
 import {DateInput} from '../util/forms/DateInput'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {isWeb} from 'platform/detection'
@@ -18,33 +17,36 @@ import {cleanError} from 'lib/strings/errors'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {
+  usePreferencesQuery,
+  usePreferencesSetBirthDateMutation,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
+import {logger} from '#/logger'
 
 export const snapPoints = ['50%']
 
-export const Component = observer(function Component({}: {}) {
+function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
   const pal = usePalette('default')
-  const store = useStores()
+  const {isMobile} = useWebMediaQueries()
   const {_} = useLingui()
+  const {
+    isPending,
+    isError,
+    error,
+    mutateAsync: setBirthDate,
+  } = usePreferencesSetBirthDateMutation()
+  const [date, setDate] = useState(preferences.birthDate || new Date())
   const {closeModal} = useModalControls()
-  const [date, setDate] = useState<Date>(
-    store.preferences.birthDate || new Date(),
-  )
-  const [isProcessing, setIsProcessing] = useState<boolean>(false)
-  const [error, setError] = useState<string>('')
-  const {isMobile} = useWebMediaQueries()
 
-  const onSave = async () => {
-    setError('')
-    setIsProcessing(true)
+  const onSave = React.useCallback(async () => {
     try {
-      await store.preferences.setBirthDate(date)
+      await setBirthDate({birthDate: date})
       closeModal()
     } catch (e) {
-      setError(cleanError(String(e)))
-    } finally {
-      setIsProcessing(false)
+      logger.error(`setBirthDate failed`, {error: e})
     }
-  }
+  }, [date, setBirthDate, closeModal])
 
   return (
     <View
@@ -74,12 +76,12 @@ export const Component = observer(function Component({}: {}) {
         />
       </View>
 
-      {error ? (
-        <ErrorMessage message={error} style={styles.error} />
+      {isError ? (
+        <ErrorMessage message={cleanError(error)} style={styles.error} />
       ) : undefined}
 
       <View style={[styles.btnContainer, pal.borderDark]}>
-        {isProcessing ? (
+        {isPending ? (
           <View style={styles.btn}>
             <ActivityIndicator color="#fff" />
           </View>
@@ -99,6 +101,16 @@ export const Component = observer(function Component({}: {}) {
       </View>
     </View>
   )
+}
+
+export const Component = observer(function Component({}: {}) {
+  const {data: preferences} = usePreferencesQuery()
+
+  return !preferences ? (
+    <ActivityIndicator />
+  ) : (
+    <Inner preferences={preferences} />
+  )
 })
 
 const styles = StyleSheet.create({
diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx
index ad4a0fa52..cd539406c 100644
--- a/src/view/com/modals/ContentFilteringSettings.tsx
+++ b/src/view/com/modals/ContentFilteringSettings.tsx
@@ -1,17 +1,15 @@
 import React from 'react'
+import {BskyPreferences, LabelPreference} from '@atproto/api'
 import {StyleSheet, Pressable, View} from 'react-native'
 import LinearGradient from 'react-native-linear-gradient'
 import {observer} from 'mobx-react-lite'
 import {ScrollView} from './util'
-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 {Button} from '../util/forms/Button'
 import {usePalette} from 'lib/hooks/usePalette'
-import {CONFIGURABLE_LABEL_GROUPS} from 'lib/labeling/const'
 import {isIOS} from 'platform/detection'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import * as Toast from '../util/Toast'
@@ -19,20 +17,23 @@ import {logger} from '#/logger'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useModalControls} from '#/state/modals'
+import {
+  usePreferencesQuery,
+  usePreferencesSetContentLabelMutation,
+  usePreferencesSetAdultContentMutation,
+  ConfigurableLabelGroup,
+  CONFIGURABLE_LABEL_GROUPS,
+} from '#/state/queries/preferences'
 
 export const snapPoints = ['90%']
 
 export const Component = observer(
   function ContentFilteringSettingsImpl({}: {}) {
-    const store = useStores()
     const {isMobile} = useWebMediaQueries()
     const pal = usePalette('default')
     const {_} = useLingui()
     const {closeModal} = useModalControls()
-
-    React.useEffect(() => {
-      store.preferences.sync()
-    }, [store])
+    const {data: preferences} = usePreferencesQuery()
 
     const onPressDone = React.useCallback(() => {
       closeModal()
@@ -43,29 +44,38 @@ export const Component = observer(
         <Text style={[pal.text, styles.title]}>
           <Trans>Content Filtering</Trans>
         </Text>
+
         <ScrollView style={styles.scrollContainer}>
           <AdultContentEnabledPref />
           <ContentLabelPref
-            group="nsfw"
-            disabled={!store.preferences.adultContentEnabled}
+            preferences={preferences}
+            labelGroup="nsfw"
+            disabled={!preferences?.adultContentEnabled}
           />
           <ContentLabelPref
-            group="nudity"
-            disabled={!store.preferences.adultContentEnabled}
+            preferences={preferences}
+            labelGroup="nudity"
+            disabled={!preferences?.adultContentEnabled}
           />
           <ContentLabelPref
-            group="suggestive"
-            disabled={!store.preferences.adultContentEnabled}
+            preferences={preferences}
+            labelGroup="suggestive"
+            disabled={!preferences?.adultContentEnabled}
           />
           <ContentLabelPref
-            group="gore"
-            disabled={!store.preferences.adultContentEnabled}
+            preferences={preferences}
+            labelGroup="gore"
+            disabled={!preferences?.adultContentEnabled}
+          />
+          <ContentLabelPref preferences={preferences} labelGroup="hate" />
+          <ContentLabelPref preferences={preferences} labelGroup="spam" />
+          <ContentLabelPref
+            preferences={preferences}
+            labelGroup="impersonation"
           />
-          <ContentLabelPref group="hate" />
-          <ContentLabelPref group="spam" />
-          <ContentLabelPref group="impersonation" />
           <View style={{height: isMobile ? 60 : 0}} />
         </ScrollView>
+
         <View
           style={[
             styles.btnContainer,
@@ -94,118 +104,114 @@ export const Component = observer(
   },
 )
 
-const AdultContentEnabledPref = observer(
-  function AdultContentEnabledPrefImpl() {
-    const store = useStores()
-    const pal = usePalette('default')
-    const {openModal} = useModalControls()
+function AdultContentEnabledPref() {
+  const pal = usePalette('default')
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate, variables} = usePreferencesSetAdultContentMutation()
+  const {openModal} = useModalControls()
 
-    const onSetAge = () => openModal({name: 'birth-date-settings'})
+  const onSetAge = React.useCallback(
+    () => openModal({name: 'birth-date-settings'}),
+    [openModal],
+  )
 
-    const onToggleAdultContent = 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',
-        )
-        logger.error('Failed to update preferences with server', {error: e})
-      }
+  const onToggleAdultContent = React.useCallback(async () => {
+    if (isIOS) return
+
+    try {
+      mutate({
+        enabled: !(variables?.enabled ?? preferences?.adultContentEnabled),
+      })
+    } catch (e) {
+      Toast.show('There was an issue syncing your preferences with the server')
+      logger.error('Failed to update preferences with server', {error: e})
     }
+  }, [variables, preferences, mutate])
 
-    return (
-      <View style={s.mb10}>
-        {isIOS ? (
-          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>
-          )
-        ) : typeof store.preferences.birthDate === 'undefined' ? (
-          <View style={[pal.viewLight, styles.agePrompt]}>
-            <Text type="md" style={[pal.text, {flex: 1}]}>
-              Confirm your age to enable adult content.
-            </Text>
-            <Button type="primary" label="Set Age" onPress={onSetAge} />
-          </View>
-        ) : (store.preferences.userAge || 0) >= 18 ? (
-          <ToggleButton
-            type="default-light"
-            label="Enable Adult Content"
-            isSelected={store.preferences.adultContentEnabled}
-            onPress={onToggleAdultContent}
-            style={styles.toggleBtn}
-          />
-        ) : (
-          <View style={[pal.viewLight, styles.agePrompt]}>
-            <Text type="md" style={[pal.text, {flex: 1}]}>
-              You must be 18 or older to enable adult content.
-            </Text>
-            <Button type="primary" label="Set Age" onPress={onSetAge} />
-          </View>
-        )}
-      </View>
-    )
-  },
-)
+  return (
+    <View style={s.mb10}>
+      {isIOS ? (
+        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>
+        )
+      ) : typeof preferences?.birthDate === 'undefined' ? (
+        <View style={[pal.viewLight, styles.agePrompt]}>
+          <Text type="md" style={[pal.text, {flex: 1}]}>
+            Confirm your age to enable adult content.
+          </Text>
+          <Button type="primary" label="Set Age" onPress={onSetAge} />
+        </View>
+      ) : (preferences.userAge || 0) >= 18 ? (
+        <ToggleButton
+          type="default-light"
+          label="Enable Adult Content"
+          isSelected={variables?.enabled ?? preferences?.adultContentEnabled}
+          onPress={onToggleAdultContent}
+          style={styles.toggleBtn}
+        />
+      ) : (
+        <View style={[pal.viewLight, styles.agePrompt]}>
+          <Text type="md" style={[pal.text, {flex: 1}]}>
+            You must be 18 or older to enable adult content.
+          </Text>
+          <Button type="primary" label="Set Age" onPress={onSetAge} />
+        </View>
+      )}
+    </View>
+  )
+}
 
 // TODO: Refactor this component to pass labels down to each tab
 const ContentLabelPref = observer(function ContentLabelPrefImpl({
-  group,
+  preferences,
+  labelGroup,
   disabled,
 }: {
-  group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+  preferences?: BskyPreferences
+  labelGroup: ConfigurableLabelGroup
   disabled?: boolean
 }) {
-  const store = useStores()
   const pal = usePalette('default')
+  const visibility = preferences?.contentLabels?.[labelGroup]
+  const {mutate, variables} = usePreferencesSetContentLabelMutation()
 
   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',
-        )
-        logger.error('Failed to update preferences with server', {error: e})
-      }
+    (vis: LabelPreference) => {
+      mutate({labelGroup, visibility: vis})
     },
-    [store, group],
+    [mutate, labelGroup],
   )
 
   return (
     <View style={[styles.contentLabelPref, pal.border]}>
       <View style={s.flex1}>
         <Text type="md-medium" style={[pal.text]}>
-          {CONFIGURABLE_LABEL_GROUPS[group].title}
+          {CONFIGURABLE_LABEL_GROUPS[labelGroup].title}
         </Text>
-        {typeof CONFIGURABLE_LABEL_GROUPS[group].subtitle === 'string' && (
+        {typeof CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle === 'string' && (
           <Text type="sm" style={[pal.textLight]}>
-            {CONFIGURABLE_LABEL_GROUPS[group].subtitle}
+            {CONFIGURABLE_LABEL_GROUPS[labelGroup].subtitle}
           </Text>
         )}
       </View>
-      {disabled ? (
+
+      {disabled || !visibility ? (
         <Text type="sm-bold" style={pal.textLight}>
           Hide
         </Text>
       ) : (
         <SelectGroup
-          current={store.preferences.contentLabels[group]}
+          current={variables?.visibility || visibility}
           onChange={onChange}
-          group={group}
+          labelGroup={labelGroup}
         />
       )}
     </View>
@@ -215,10 +221,10 @@ const ContentLabelPref = observer(function ContentLabelPrefImpl({
 interface SelectGroupProps {
   current: LabelPreference
   onChange: (v: LabelPreference) => void
-  group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+  labelGroup: ConfigurableLabelGroup
 }
 
-function SelectGroup({current, onChange, group}: SelectGroupProps) {
+function SelectGroup({current, onChange, labelGroup}: SelectGroupProps) {
   return (
     <View style={styles.selectableBtns}>
       <SelectableBtn
@@ -227,14 +233,14 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
         label="Hide"
         left
         onChange={onChange}
-        group={group}
+        labelGroup={labelGroup}
       />
       <SelectableBtn
         current={current}
         value="warn"
         label="Warn"
         onChange={onChange}
-        group={group}
+        labelGroup={labelGroup}
       />
       <SelectableBtn
         current={current}
@@ -242,7 +248,7 @@ function SelectGroup({current, onChange, group}: SelectGroupProps) {
         label="Show"
         right
         onChange={onChange}
-        group={group}
+        labelGroup={labelGroup}
       />
     </View>
   )
@@ -255,7 +261,7 @@ interface SelectableBtnProps {
   left?: boolean
   right?: boolean
   onChange: (v: LabelPreference) => void
-  group: keyof typeof CONFIGURABLE_LABEL_GROUPS
+  labelGroup: ConfigurableLabelGroup
 }
 
 function SelectableBtn({
@@ -265,7 +271,7 @@ function SelectableBtn({
   left,
   right,
   onChange,
-  group,
+  labelGroup,
 }: SelectableBtnProps) {
   const pal = usePalette('default')
   const palPrimary = usePalette('inverted')
@@ -281,7 +287,7 @@ function SelectableBtn({
       onPress={() => onChange(value)}
       accessibilityRole="button"
       accessibilityLabel={value}
-      accessibilityHint={`Set ${value} for ${group} content moderation policy`}>
+      accessibilityHint={`Set ${value} for ${labelGroup} content moderation policy`}>
       <Text style={current === value ? palPrimary.text : pal.text}>
         {label}
       </Text>
diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx
index 5e9d816ac..41abc25d3 100644
--- a/src/view/com/testing/TestCtrls.e2e.tsx
+++ b/src/view/com/testing/TestCtrls.e2e.tsx
@@ -1,10 +1,10 @@
 import React from 'react'
 import {Pressable, View} from 'react-native'
-import {useStores} from 'state/index'
 import {navigate} from '../../../Navigation'
 import {useModalControls} from '#/state/modals'
 import {useQueryClient} from '@tanstack/react-query'
 import {useSessionApi} from '#/state/session'
+import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences'
 
 /**
  * This utility component is only included in the test simulator
@@ -15,10 +15,10 @@ import {useSessionApi} from '#/state/session'
 const BTN = {height: 1, width: 1, backgroundColor: 'red'}
 
 export function TestCtrls() {
-  const store = useStores()
   const queryClient = useQueryClient()
   const {logout, login} = useSessionApi()
   const {openModal} = useModalControls()
+  const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const onPressSignInAlice = async () => {
     await login({
       service: 'http://localhost:3000',
@@ -79,7 +79,7 @@ export function TestCtrls() {
       />
       <Pressable
         testID="e2eToggleMergefeed"
-        onPress={() => store.preferences.toggleHomeFeedMergeFeedEnabled()}
+        onPress={() => setFeedViewPref({lab_mergeFeedEnabled: true})}
         accessibilityRole="button"
         style={BTN}
       />
diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx
index da99dc16f..7b240ded0 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesHomeFeed.tsx
@@ -4,7 +4,6 @@ import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Slider} from '@miblanchard/react-native-slider'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -16,21 +15,31 @@ import {CenteredView} from 'view/com/util/Views'
 import debounce from 'lodash.debounce'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  useSetFeedViewPreferencesMutation,
+} from '#/state/queries/preferences'
 
-function RepliesThresholdInput({enabled}: {enabled: boolean}) {
-  const store = useStores()
+function RepliesThresholdInput({
+  enabled,
+  initialValue,
+}: {
+  enabled: boolean
+  initialValue: number
+}) {
   const pal = usePalette('default')
-  const [value, setValue] = useState(
-    store.preferences.homeFeed.hideRepliesByLikeCount,
-  )
+  const [value, setValue] = useState(initialValue)
+  const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const save = React.useMemo(
     () =>
       debounce(
         threshold =>
-          store.preferences.setHomeFeedHideRepliesByLikeCount(threshold),
+          setFeedViewPref({
+            hideRepliesByLikeCount: threshold,
+          }),
         500,
       ), // debouce for 500ms
-    [store],
+    [setFeedViewPref],
   )
 
   return (
@@ -67,9 +76,15 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
   navigation,
 }: Props) {
   const pal = usePalette('default')
-  const store = useStores()
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate: setFeedViewPref, variables} =
+    useSetFeedViewPreferencesMutation()
+
+  const showReplies = !(
+    variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
+  )
 
   return (
     <CenteredView
@@ -105,17 +120,20 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             <ToggleButton
               testID="toggleRepliesBtn"
               type="default-light"
-              label={store.preferences.homeFeed.hideReplies ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideReplies}
-              onPress={store.preferences.toggleHomeFeedHideReplies}
+              label={showReplies ? 'Yes' : 'No'}
+              isSelected={showReplies}
+              onPress={() =>
+                setFeedViewPref({
+                  hideReplies: !(
+                    variables?.hideReplies ??
+                    preferences?.feedViewPrefs?.hideReplies
+                  ),
+                })
+              }
             />
           </View>
           <View
-            style={[
-              pal.viewLight,
-              styles.card,
-              store.preferences.homeFeed.hideReplies && styles.dimmed,
-            ]}>
+            style={[pal.viewLight, styles.card, !showReplies && styles.dimmed]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
               <Trans>Reply Filters</Trans>
             </Text>
@@ -128,10 +146,19 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             <ToggleButton
               type="default-light"
               label="Followed users only"
-              isSelected={store.preferences.homeFeed.hideRepliesByUnfollowed}
+              isSelected={Boolean(
+                variables?.hideRepliesByUnfollowed ??
+                  preferences?.feedViewPrefs?.hideRepliesByUnfollowed,
+              )}
               onPress={
-                !store.preferences.homeFeed.hideReplies
-                  ? store.preferences.toggleHomeFeedHideRepliesByUnfollowed
+                showReplies
+                  ? () =>
+                      setFeedViewPref({
+                        hideRepliesByUnfollowed: !(
+                          variables?.hideRepliesByUnfollowed ??
+                          preferences?.feedViewPrefs?.hideRepliesByUnfollowed
+                        ),
+                      })
                   : undefined
               }
               style={[s.mb10]}
@@ -142,9 +169,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
                 feed.
               </Trans>
             </Text>
-            <RepliesThresholdInput
-              enabled={!store.preferences.homeFeed.hideReplies}
-            />
+            {preferences && (
+              <RepliesThresholdInput
+                enabled={showReplies}
+                initialValue={preferences.feedViewPrefs.hideRepliesByLikeCount}
+              />
+            )}
           </View>
 
           <View style={[pal.viewLight, styles.card]}>
@@ -158,9 +188,26 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             </Text>
             <ToggleButton
               type="default-light"
-              label={store.preferences.homeFeed.hideReposts ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideReposts}
-              onPress={store.preferences.toggleHomeFeedHideReposts}
+              label={
+                variables?.hideReposts ??
+                preferences?.feedViewPrefs?.hideReposts
+                  ? 'No'
+                  : 'Yes'
+              }
+              isSelected={
+                !(
+                  variables?.hideReposts ??
+                  preferences?.feedViewPrefs?.hideReposts
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  hideReposts: !(
+                    variables?.hideReposts ??
+                    preferences?.feedViewPrefs?.hideReposts
+                  ),
+                })
+              }
             />
           </View>
 
@@ -176,9 +223,26 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             </Text>
             <ToggleButton
               type="default-light"
-              label={store.preferences.homeFeed.hideQuotePosts ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideQuotePosts}
-              onPress={store.preferences.toggleHomeFeedHideQuotePosts}
+              label={
+                variables?.hideQuotePosts ??
+                preferences?.feedViewPrefs?.hideQuotePosts
+                  ? 'No'
+                  : 'Yes'
+              }
+              isSelected={
+                !(
+                  variables?.hideQuotePosts ??
+                  preferences?.feedViewPrefs?.hideQuotePosts
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  hideQuotePosts: !(
+                    variables?.hideQuotePosts ??
+                    preferences?.feedViewPrefs?.hideQuotePosts
+                  ),
+                })
+              }
             />
           </View>
 
@@ -196,10 +260,25 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             <ToggleButton
               type="default-light"
               label={
-                store.preferences.homeFeed.lab_mergeFeedEnabled ? 'Yes' : 'No'
+                variables?.lab_mergeFeedEnabled ??
+                preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                  ? 'Yes'
+                  : 'No'
+              }
+              isSelected={
+                !!(
+                  variables?.lab_mergeFeedEnabled ??
+                  preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  lab_mergeFeedEnabled: !(
+                    variables?.lab_mergeFeedEnabled ??
+                    preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                  ),
+                })
               }
-              isSelected={!!store.preferences.homeFeed.lab_mergeFeedEnabled}
-              onPress={store.preferences.toggleHomeFeedMergeFeedEnabled}
             />
           </View>
         </View>
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index 8a2db13ce..2386f6445 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -1,9 +1,14 @@
 import React from 'react'
-import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {
+  ActivityIndicator,
+  ScrollView,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -14,15 +19,30 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  useSetThreadViewPreferencesMutation,
+} from '#/state/queries/preferences'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
 export const PreferencesThreads = observer(function PreferencesThreadsImpl({
   navigation,
 }: Props) {
   const pal = usePalette('default')
-  const store = useStores()
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate: setThreadViewPrefs, variables} =
+    useSetThreadViewPreferencesMutation()
+
+  const prioritizeFollowedUsers = Boolean(
+    variables?.prioritizeFollowedUsers ??
+      preferences?.threadViewPrefs?.prioritizeFollowedUsers,
+  )
+  const treeViewEnabled = Boolean(
+    variables?.lab_treeViewEnabled ??
+      preferences?.threadViewPrefs?.lab_treeViewEnabled,
+  )
 
   return (
     <CenteredView
@@ -44,71 +64,79 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
         </Text>
       </View>
 
-      <ScrollView>
-        <View style={styles.cardsContainer}>
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <Trans>Sort Replies</Trans>
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              <Trans>Sort replies to the same post by:</Trans>
-            </Text>
-            <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
-              <RadioGroup
+      {preferences ? (
+        <ScrollView>
+          <View style={styles.cardsContainer}>
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <Trans>Sort Replies</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>Sort replies to the same post by:</Trans>
+              </Text>
+              <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
+                <RadioGroup
+                  type="default-light"
+                  items={[
+                    {key: 'oldest', label: 'Oldest replies first'},
+                    {key: 'newest', label: 'Newest replies first'},
+                    {key: 'most-likes', label: 'Most-liked replies first'},
+                    {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
+                  ]}
+                  onSelect={key => setThreadViewPrefs({sort: key})}
+                  initialSelection={preferences?.threadViewPrefs?.sort}
+                />
+              </View>
+            </View>
+
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <Trans>Prioritize Your Follows</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>
+                  Show replies by people you follow before all other replies.
+                </Trans>
+              </Text>
+              <ToggleButton
                 type="default-light"
-                items={[
-                  {key: 'oldest', label: 'Oldest replies first'},
-                  {key: 'newest', label: 'Newest replies first'},
-                  {key: 'most-likes', label: 'Most-liked replies first'},
-                  {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
-                ]}
-                onSelect={store.preferences.setThreadSort}
-                initialSelection={store.preferences.thread.sort}
+                label={prioritizeFollowedUsers ? 'Yes' : 'No'}
+                isSelected={prioritizeFollowedUsers}
+                onPress={() =>
+                  setThreadViewPrefs({
+                    prioritizeFollowedUsers: !prioritizeFollowedUsers,
+                  })
+                }
               />
             </View>
-          </View>
 
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <Trans>Prioritize Your Follows</Trans>
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              <Trans>
-                Show replies by people you follow before all other replies.
-              </Trans>
-            </Text>
-            <ToggleButton
-              type="default-light"
-              label={
-                store.preferences.thread.prioritizeFollowedUsers ? 'Yes' : 'No'
-              }
-              isSelected={store.preferences.thread.prioritizeFollowedUsers}
-              onPress={store.preferences.togglePrioritizedFollowedUsers}
-            />
-          </View>
-
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
-              <Trans>Threaded Mode</Trans>
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              <Trans>
-                Set this setting to "Yes" to show replies in a threaded view.
-                This is an experimental feature.
-              </Trans>
-            </Text>
-            <ToggleButton
-              type="default-light"
-              label={
-                store.preferences.thread.lab_treeViewEnabled ? 'Yes' : 'No'
-              }
-              isSelected={!!store.preferences.thread.lab_treeViewEnabled}
-              onPress={store.preferences.toggleThreadTreeViewEnabled}
-            />
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
+                <Trans>Threaded Mode</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>
+                  Set this setting to "Yes" to show replies in a threaded view.
+                  This is an experimental feature.
+                </Trans>
+              </Text>
+              <ToggleButton
+                type="default-light"
+                label={treeViewEnabled ? 'Yes' : 'No'}
+                isSelected={treeViewEnabled}
+                onPress={() =>
+                  setThreadViewPrefs({
+                    lab_treeViewEnabled: !treeViewEnabled,
+                  })
+                }
+              />
+            </View>
           </View>
-        </View>
-      </ScrollView>
+        </ScrollView>
+      ) : (
+        <ActivityIndicator />
+      )}
 
       <View
         style={[
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 487f56643..8ca2383d2 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useMemo} from 'react'
+import React from 'react'
 import {
   StyleSheet,
   View,
@@ -8,26 +8,34 @@ import {
 } from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {track} from '#/lib/analytics/analytics'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
-import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {ScrollView, CenteredView} from 'view/com/util/Views'
 import {Text} from 'view/com/util/text/Text'
 import {s, colors} from 'lib/styles'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {FeedSourceModel} from 'state/models/content/feed-source'
+import {NewFeedSourceCard} from 'view/com/feeds/FeedSourceCard'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as Toast from 'view/com/util/Toast'
 import {Haptics} from 'lib/haptics'
 import {TextLink} from 'view/com/util/Link'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {
+  usePreferencesQuery,
+  usePinFeedMutation,
+  useUnpinFeedMutation,
+  useSetSaveFeedsMutation,
+  usePreferencesQueryKey,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
 
 const HITSLOP_TOP = {
   top: 20,
@@ -43,150 +51,178 @@ const HITSLOP_BOTTOM = {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
-export const SavedFeeds = withAuthRequired(
-  observer(function SavedFeedsImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
-    const {screen} = useAnalytics()
-    const setMinimalShellMode = useSetMinimalShellMode()
+export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) {
+  const pal = usePalette('default')
+  const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+  const {screen} = useAnalytics()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {data: preferences} = usePreferencesQuery()
 
-    const savedFeeds = useMemo(() => {
-      const model = new SavedFeedsModel(store)
-      model.refresh()
-      return model
-    }, [store])
-    useFocusEffect(
-      useCallback(() => {
-        screen('SavedFeeds')
-        setMinimalShellMode(false)
-        savedFeeds.refresh()
-      }, [screen, setMinimalShellMode, savedFeeds]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('SavedFeeds')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    return (
-      <CenteredView
-        style={[
-          s.hContentRegion,
-          pal.border,
-          isTabletOrDesktop && styles.desktopContainer,
-        ]}>
-        <ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
-        <ScrollView style={s.flex1}>
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              Pinned Feeds
-            </Text>
-          </View>
-          {savedFeeds.hasLoaded ? (
-            !savedFeeds.pinned.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  You don't have any pinned feeds.
-                </Text>
-              </View>
-            ) : (
-              savedFeeds.pinned.map(feed => (
-                <ListItem
-                  key={feed._reactKey}
-                  savedFeeds={savedFeeds}
-                  item={feed}
-                />
-              ))
-            )
+  return (
+    <CenteredView
+      style={[
+        s.hContentRegion,
+        pal.border,
+        isTabletOrDesktop && styles.desktopContainer,
+      ]}>
+      <ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
+      <ScrollView style={s.flex1}>
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            Pinned Feeds
+          </Text>
+        </View>
+        {preferences?.feeds ? (
+          !preferences.feeds.pinned.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                You don't have any pinned feeds.
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              Saved Feeds
-            </Text>
-          </View>
-          {savedFeeds.hasLoaded ? (
-            !savedFeeds.unpinned.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  You don't have any saved feeds.
-                </Text>
-              </View>
-            ) : (
-              savedFeeds.unpinned.map(feed => (
-                <ListItem
-                  key={feed._reactKey}
-                  savedFeeds={savedFeeds}
-                  item={feed}
-                />
-              ))
-            )
+            preferences?.feeds?.pinned?.map(uri => (
+              <ListItem key={uri} feedUri={uri} isPinned />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            Saved Feeds
+          </Text>
+        </View>
+        {preferences?.feeds ? (
+          !preferences.feeds.unpinned.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                You don't have any saved feeds.
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
+            preferences.feeds.unpinned.map(uri => (
+              <ListItem key={uri} feedUri={uri} isPinned={false} />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
 
-          <View style={styles.footerText}>
-            <Text type="sm" style={pal.textLight}>
-              Feeds are custom algorithms that users build with a little coding
-              expertise.{' '}
-              <TextLink
-                type="sm"
-                style={pal.link}
-                href="https://github.com/bluesky-social/feed-generator"
-                text="See this guide"
-              />{' '}
-              for more information.
-            </Text>
-          </View>
-          <View style={{height: 100}} />
-        </ScrollView>
-      </CenteredView>
-    )
-  }),
-)
+        <View style={styles.footerText}>
+          <Text type="sm" style={pal.textLight}>
+            Feeds are custom algorithms that users build with a little coding
+            expertise.{' '}
+            <TextLink
+              type="sm"
+              style={pal.link}
+              href="https://github.com/bluesky-social/feed-generator"
+              text="See this guide"
+            />{' '}
+            for more information.
+          </Text>
+        </View>
+        <View style={{height: 100}} />
+      </ScrollView>
+    </CenteredView>
+  )
+})
 
 const ListItem = observer(function ListItemImpl({
-  savedFeeds,
-  item,
+  feedUri,
+  isPinned,
 }: {
-  savedFeeds: SavedFeedsModel
-  item: FeedSourceModel
+  feedUri: string // uri
+  isPinned: boolean
 }) {
   const pal = usePalette('default')
-  const isPinned = item.isPinned
+  const queryClient = useQueryClient()
+  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
+  const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
+    useUnpinFeedMutation()
+  const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
+    useSetSaveFeedsMutation()
 
-  const onTogglePinned = useCallback(() => {
+  const onTogglePinned = React.useCallback(async () => {
     Haptics.default()
-    item.togglePin().catch(e => {
+
+    try {
+      if (isPinned) {
+        await unpinFeed({uri: feedUri})
+      } else {
+        await pinFeed({uri: feedUri})
+      }
+    } catch (e) {
       Toast.show('There was an issue contacting the server')
       logger.error('Failed to toggle pinned feed', {error: e})
-    })
-  }, [item])
-  const onPressUp = useCallback(
-    () =>
-      savedFeeds.movePinnedFeed(item, 'up').catch(e => {
-        Toast.show('There was an issue contacting the server')
-        logger.error('Failed to set pinned feed order', {error: e})
-      }),
-    [savedFeeds, item],
-  )
-  const onPressDown = useCallback(
-    () =>
-      savedFeeds.movePinnedFeed(item, 'down').catch(e => {
-        Toast.show('There was an issue contacting the server')
-        logger.error('Failed to set pinned feed order', {error: e})
-      }),
-    [savedFeeds, item],
-  )
+    }
+  }, [feedUri, isPinned, pinFeed, unpinFeed])
+
+  const onPressUp = React.useCallback(async () => {
+    if (!isPinned) return
+
+    const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
+      usePreferencesQueryKey,
+    )?.feeds
+    const pinned = feeds?.pinned ?? []
+    const index = pinned.indexOf(feedUri)
+
+    if (index === -1 || index === 0) return
+    ;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
+
+    try {
+      await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
+      track('CustomFeed:Reorder', {
+        uri: feedUri,
+        index: pinned.indexOf(feedUri),
+      })
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to set pinned feed order', {error: e})
+    }
+  }, [feedUri, isPinned, queryClient, setSavedFeeds])
+
+  const onPressDown = React.useCallback(async () => {
+    if (!isPinned) return
+
+    const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
+      usePreferencesQueryKey,
+    )?.feeds
+    const pinned = feeds?.pinned ?? []
+    const index = pinned.indexOf(feedUri)
+
+    if (index === -1 || index >= pinned.length - 1) return
+    ;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
+
+    try {
+      await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
+      track('CustomFeed:Reorder', {
+        uri: feedUri,
+        index: pinned.indexOf(feedUri),
+      })
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to set pinned feed order', {error: e})
+    }
+  }, [feedUri, isPinned, queryClient, setSavedFeeds])
 
   return (
     <Pressable
@@ -195,6 +231,7 @@ const ListItem = observer(function ListItemImpl({
       {isPinned ? (
         <View style={styles.webArrowButtonsContainer}>
           <TouchableOpacity
+            disabled={isMovePending}
             accessibilityRole="button"
             onPress={onPressUp}
             hitSlop={HITSLOP_TOP}>
@@ -205,6 +242,7 @@ const ListItem = observer(function ListItemImpl({
             />
           </TouchableOpacity>
           <TouchableOpacity
+            disabled={isMovePending}
             accessibilityRole="button"
             onPress={onPressDown}
             hitSlop={HITSLOP_BOTTOM}>
@@ -212,13 +250,14 @@ const ListItem = observer(function ListItemImpl({
           </TouchableOpacity>
         </View>
       ) : null}
-      <FeedSourceCard
-        key={item.uri}
-        item={item}
-        showSaveBtn
+      <NewFeedSourceCard
+        key={feedUri}
+        feedUri={feedUri}
         style={styles.noBorder}
+        showSaveBtn
       />
       <TouchableOpacity
+        disabled={isPinPending || isUnpinPending}
         accessibilityRole="button"
         hitSlop={10}
         onPress={onTogglePinned}>
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index e56a50d79..baad2227b 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -59,6 +59,7 @@ import {
 } from '#/state/preferences'
 import {useSession, useSessionApi, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
+import {useClearPreferencesMutation} from '#/state/queries/preferences'
 
 // TEMPORARY (APP-700)
 // remove after backend testing finishes
@@ -153,6 +154,7 @@ export const SettingsScreen = withAuthRequired(
     const {openModal} = useModalControls()
     const {isSwitchingAccounts, accounts, currentAccount} = useSession()
     const {clearCurrentAccount} = useSessionApi()
+    const {mutate: clearPreferences} = useClearPreferencesMutation()
 
     const primaryBg = useCustomPalette<ViewStyle>({
       light: {backgroundColor: colors.blue0},
@@ -219,9 +221,8 @@ export const SettingsScreen = withAuthRequired(
     }, [openModal])
 
     const onPressResetPreferences = React.useCallback(async () => {
-      await store.preferences.reset()
-      Toast.show('Preferences reset')
-    }, [store])
+      clearPreferences()
+    }, [clearPreferences])
 
     const onPressResetOnboarding = React.useCallback(async () => {
       onboardingDispatch({type: 'start'})
@@ -300,7 +301,7 @@ export const SettingsScreen = withAuthRequired(
               </View>
               <View style={[styles.infoLine]}>
                 <Text type="lg-medium" style={pal.text}>
-                  <Trans>Birthday: </Trans>
+                  <Trans>Birthday:</Trans>{' '}
                 </Text>
                 <Link onPress={() => openModal({name: 'birth-date-settings'})}>
                   <Text type="lg" style={pal.link}>