about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-11-15 20:35:58 -0600
committerGitHub <noreply@github.com>2023-11-15 18:35:58 -0800
commit0de8d40981fecdeaec92307bafe121ccb2091b45 (patch)
tree73415ce38b154dd2ba8882b687067e6837f79bf2
parente749f2f3a52f5c1e137ce8262701b9c9df96324f (diff)
downloadvoidsky-0de8d40981fecdeaec92307bafe121ccb2091b45.tar.zst
Misc cleanup (#1925)
* Remove unused prefs

* Cleanup

* Remove my-follows cache

* Replace moderationOpts in ProfileCard comp

* Replace moderationOpts in FeedSlice

* Remove preferences model
-rw-r--r--src/state/models/cache/my-follows.ts137
-rw-r--r--src/state/models/discovery/foafs.ts132
-rw-r--r--src/state/models/discovery/suggested-actors.ts151
-rw-r--r--src/state/models/discovery/user-autocomplete.ts143
-rw-r--r--src/state/models/feeds/post.ts181
-rw-r--r--src/state/models/me.ts4
-rw-r--r--src/state/models/root-store.ts6
-rw-r--r--src/state/models/ui/preferences.ts129
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx7
-rw-r--r--src/view/com/posts/Feed.tsx16
-rw-r--r--src/view/com/posts/FeedSlice.tsx12
-rw-r--r--src/view/com/profile/ProfileCard.tsx26
12 files changed, 37 insertions, 907 deletions
diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts
deleted file mode 100644
index e1e8af509..000000000
--- a/src/state/models/cache/my-follows.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  AppBskyActorDefs,
-  AppBskyGraphGetFollows as GetFollows,
-  moderateProfile,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-
-const MAX_SYNC_PAGES = 10
-const SYNC_TTL = 60e3 * 10 // 10 minutes
-
-type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
-
-export enum FollowState {
-  Following,
-  NotFollowing,
-  Unknown,
-}
-
-export interface FollowInfo {
-  did: string
-  followRecordUri: string | undefined
-  handle: string
-  displayName: string | undefined
-  avatar: string | undefined
-}
-
-/**
- * This model is used to maintain a synced local cache of the user's
- * follows. It should be periodically refreshed and updated any time
- * the user makes a change to their follows.
- */
-export class MyFollowsCache {
-  // data
-  byDid: Record<string, FollowInfo> = {}
-  lastSync = 0
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  // public api
-  // =
-
-  clear() {
-    this.byDid = {}
-  }
-
-  /**
-   * Syncs a subset of the user's follows
-   * for performance reasons, caps out at 1000 follows
-   */
-  syncIfNeeded = bundleAsync(async () => {
-    if (this.lastSync > Date.now() - SYNC_TTL) {
-      return
-    }
-
-    let cursor
-    for (let i = 0; i < MAX_SYNC_PAGES; i++) {
-      const res: GetFollows.Response = await this.rootStore.agent.getFollows({
-        actor: this.rootStore.me.did,
-        cursor,
-        limit: 100,
-      })
-      res.data.follows = res.data.follows.filter(
-        profile =>
-          !moderateProfile(profile, this.rootStore.preferences.moderationOpts)
-            .account.filter,
-      )
-      this.hydrateMany(res.data.follows)
-      if (!res.data.cursor) {
-        break
-      }
-      cursor = res.data.cursor
-    }
-
-    this.lastSync = Date.now()
-  })
-
-  getFollowState(did: string): FollowState {
-    if (typeof this.byDid[did] === 'undefined') {
-      return FollowState.Unknown
-    }
-    if (typeof this.byDid[did].followRecordUri === 'string') {
-      return FollowState.Following
-    }
-    return FollowState.NotFollowing
-  }
-
-  async fetchFollowState(did: string): Promise<FollowState> {
-    // TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf
-    const res = await this.rootStore.agent.getProfile({actor: did})
-    this.hydrate(did, res.data)
-    return this.getFollowState(did)
-  }
-
-  getFollowUri(did: string): string {
-    const v = this.byDid[did]
-    if (v && typeof v.followRecordUri === 'string') {
-      return v.followRecordUri
-    }
-    throw new Error('Not a followed user')
-  }
-
-  addFollow(did: string, info: FollowInfo) {
-    this.byDid[did] = info
-  }
-
-  removeFollow(did: string) {
-    if (this.byDid[did]) {
-      this.byDid[did].followRecordUri = undefined
-    }
-  }
-
-  hydrate(did: string, profile: Profile) {
-    this.byDid[did] = {
-      did,
-      followRecordUri: profile.viewer?.following,
-      handle: profile.handle,
-      displayName: profile.displayName,
-      avatar: profile.avatar,
-    }
-  }
-
-  hydrateMany(profiles: Profile[]) {
-    for (const profile of profiles) {
-      this.hydrate(profile.did, profile)
-    }
-  }
-}
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts
deleted file mode 100644
index 4a647dcfe..000000000
--- a/src/state/models/discovery/foafs.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import {AppBskyActorDefs} from '@atproto/api'
-import {makeAutoObservable, runInAction} from 'mobx'
-import sampleSize from 'lodash.samplesize'
-import {bundleAsync} from 'lib/async/bundle'
-import {RootStoreModel} from '../root-store'
-
-export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & {
-  followers: AppBskyActorDefs.ProfileView[]
-}
-
-export type ProfileViewFollows = AppBskyActorDefs.ProfileView & {
-  follows: AppBskyActorDefs.ProfileViewBasic[]
-}
-
-export class FoafsModel {
-  isLoading = false
-  hasData = false
-  sources: string[] = []
-  foafs: Map<string, ProfileViewFollows> = new Map() // FOAF stands for Friend of a Friend
-  popular: RefWithInfoAndFollowers[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this)
-  }
-
-  get hasContent() {
-    if (this.popular.length > 0) {
-      return true
-    }
-    for (const foaf of this.foafs.values()) {
-      if (foaf.follows.length) {
-        return true
-      }
-    }
-    return false
-  }
-
-  fetch = bundleAsync(async () => {
-    try {
-      this.isLoading = true
-
-      // fetch some of the user's follows
-      await this.rootStore.me.follows.syncIfNeeded()
-
-      // grab 10 of the users followed by the user
-      runInAction(() => {
-        this.sources = sampleSize(
-          Object.keys(this.rootStore.me.follows.byDid),
-          10,
-        )
-      })
-      if (this.sources.length === 0) {
-        return
-      }
-      runInAction(() => {
-        this.foafs.clear()
-        this.popular.length = 0
-      })
-
-      // fetch their profiles
-      const profiles = await this.rootStore.agent.getProfiles({
-        actors: this.sources,
-      })
-
-      // fetch their follows
-      const results = await Promise.allSettled(
-        this.sources.map(source =>
-          this.rootStore.agent.getFollows({actor: source}),
-        ),
-      )
-
-      // store the follows and construct a "most followed" set
-      const popular: RefWithInfoAndFollowers[] = []
-      for (let i = 0; i < results.length; i++) {
-        const res = results[i]
-        if (res.status === 'fulfilled') {
-          this.rootStore.me.follows.hydrateMany(res.value.data.follows)
-        }
-        const profile = profiles.data.profiles[i]
-        const source = this.sources[i]
-        if (res.status === 'fulfilled' && profile) {
-          // filter out inappropriate suggestions
-          res.value.data.follows = res.value.data.follows.filter(follow => {
-            const viewer = follow.viewer
-            if (viewer) {
-              if (
-                viewer.following ||
-                viewer.muted ||
-                viewer.mutedByList ||
-                viewer.blockedBy ||
-                viewer.blocking
-              ) {
-                return false
-              }
-            }
-            if (follow.did === this.rootStore.me.did) {
-              return false
-            }
-            return true
-          })
-
-          runInAction(() => {
-            this.foafs.set(source, {
-              ...profile,
-              follows: res.value.data.follows,
-            })
-          })
-          for (const follow of res.value.data.follows) {
-            let item = popular.find(p => p.did === follow.did)
-            if (!item) {
-              item = {...follow, followers: []}
-              popular.push(item)
-            }
-            item.followers.push(profile)
-          }
-        }
-      }
-
-      popular.sort((a, b) => b.followers.length - a.followers.length)
-      runInAction(() => {
-        this.popular = popular.filter(p => p.followers.length > 1).slice(0, 20)
-      })
-      this.hasData = true
-    } catch (e) {
-      console.error('Failed to fetch FOAFs', e)
-    } finally {
-      runInAction(() => {
-        this.isLoading = false
-      })
-    }
-  })
-}
diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts
deleted file mode 100644
index 450786c2f..000000000
--- a/src/state/models/discovery/suggested-actors.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {cleanError} from 'lib/strings/errors'
-import {bundleAsync} from 'lib/async/bundle'
-import {logger} from '#/logger'
-
-const PAGE_SIZE = 30
-
-export type SuggestedActor =
-  | AppBskyActorDefs.ProfileViewBasic
-  | AppBskyActorDefs.ProfileView
-
-export class SuggestedActorsModel {
-  // state
-  pageSize = PAGE_SIZE
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  loadMoreCursor: string | undefined = undefined
-  error = ''
-  hasMore = false
-  lastInsertedAtIndex = -1
-
-  // data
-  suggestions: SuggestedActor[] = []
-
-  constructor(public rootStore: RootStoreModel, opts?: {pageSize?: number}) {
-    if (opts?.pageSize) {
-      this.pageSize = opts.pageSize
-    }
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasContent() {
-    return this.suggestions.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (replace) {
-      this.hasMore = true
-      this.loadMoreCursor = undefined
-    }
-    if (!this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.actor.getSuggestions({
-        limit: 25,
-        cursor: this.loadMoreCursor,
-      })
-      let {actors, cursor} = res.data
-      actors = actors.filter(
-        actor =>
-          !moderateProfile(actor, this.rootStore.preferences.moderationOpts)
-            .account.filter,
-      )
-      this.rootStore.me.follows.hydrateMany(actors)
-
-      runInAction(() => {
-        if (replace) {
-          this.suggestions = []
-        }
-        this.loadMoreCursor = cursor
-        this.hasMore = !!cursor
-        this.suggestions = this.suggestions.concat(
-          actors.filter(actor => {
-            const viewer = actor.viewer
-            if (viewer) {
-              if (
-                viewer.following ||
-                viewer.muted ||
-                viewer.mutedByList ||
-                viewer.blockedBy ||
-                viewer.blocking
-              ) {
-                return false
-              }
-            }
-            if (actor.did === this.rootStore.me.did) {
-              return false
-            }
-            return true
-          }),
-        )
-      })
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  async insertSuggestionsByActor(actor: string, indexToInsertAt: number) {
-    // fetch suggestions
-    const res =
-      await this.rootStore.agent.app.bsky.graph.getSuggestedFollowsByActor({
-        actor: actor,
-      })
-    const {suggestions: moreSuggestions} = res.data
-    this.rootStore.me.follows.hydrateMany(moreSuggestions)
-    // dedupe
-    const toInsert = moreSuggestions.filter(
-      s => !this.suggestions.find(s2 => s2.did === s.did),
-    )
-    //  insert
-    this.suggestions.splice(indexToInsertAt + 1, 0, ...toInsert)
-    // update index
-    this.lastInsertedAtIndex = indexToInsertAt
-  }
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      logger.error('Failed to fetch suggested actors', {error: err})
-    }
-  }
-}
diff --git a/src/state/models/discovery/user-autocomplete.ts b/src/state/models/discovery/user-autocomplete.ts
deleted file mode 100644
index f28869e83..000000000
--- a/src/state/models/discovery/user-autocomplete.ts
+++ /dev/null
@@ -1,143 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyActorDefs} from '@atproto/api'
-import AwaitLock from 'await-lock'
-import {RootStoreModel} from '../root-store'
-import {isInvalidHandle} from 'lib/strings/handles'
-
-type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic
-
-export class UserAutocompleteModel {
-  // state
-  isLoading = false
-  isActive = false
-  prefix = ''
-  lock = new AwaitLock()
-
-  // data
-  knownHandles: Set<string> = new Set()
-  _suggestions: ProfileViewBasic[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        knownHandles: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get follows(): ProfileViewBasic[] {
-    return Object.values(this.rootStore.me.follows.byDid).map(item => ({
-      did: item.did,
-      handle: item.handle,
-      displayName: item.displayName,
-      avatar: item.avatar,
-    }))
-  }
-
-  get suggestions(): ProfileViewBasic[] {
-    if (!this.isActive) {
-      return []
-    }
-    return this._suggestions
-  }
-
-  // public api
-  // =
-
-  async setup() {
-    this.isLoading = true
-    await this.rootStore.me.follows.syncIfNeeded()
-    runInAction(() => {
-      for (const did in this.rootStore.me.follows.byDid) {
-        const info = this.rootStore.me.follows.byDid[did]
-        if (!isInvalidHandle(info.handle)) {
-          this.knownHandles.add(info.handle)
-        }
-      }
-      this.isLoading = false
-    })
-  }
-
-  setActive(v: boolean) {
-    this.isActive = v
-  }
-
-  async setPrefix(prefix: string) {
-    const origPrefix = prefix.trim().toLocaleLowerCase()
-    this.prefix = origPrefix
-    await this.lock.acquireAsync()
-    try {
-      if (this.prefix) {
-        if (this.prefix !== origPrefix) {
-          return // another prefix was set before we got our chance
-        }
-
-        // reset to follow results
-        this._computeSuggestions([])
-
-        // ask backend
-        const res = await this.rootStore.agent.searchActorsTypeahead({
-          term: this.prefix,
-          limit: 8,
-        })
-        this._computeSuggestions(res.data.actors)
-
-        // update known handles
-        runInAction(() => {
-          for (const u of res.data.actors) {
-            this.knownHandles.add(u.handle)
-          }
-        })
-      } else {
-        runInAction(() => {
-          this._computeSuggestions([])
-        })
-      }
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  // internal
-  // =
-
-  _computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) {
-    if (this.prefix) {
-      const items: ProfileViewBasic[] = []
-      for (const item of this.follows) {
-        if (prefixMatch(this.prefix, item)) {
-          items.push(item)
-        }
-        if (items.length >= 8) {
-          break
-        }
-      }
-      for (const item of searchRes) {
-        if (!items.find(item2 => item2.handle === item.handle)) {
-          items.push({
-            did: item.did,
-            handle: item.handle,
-            displayName: item.displayName,
-            avatar: item.avatar,
-          })
-        }
-      }
-      this._suggestions = items
-    } else {
-      this._suggestions = this.follows
-    }
-  }
-}
-
-function prefixMatch(prefix: string, info: ProfileViewBasic): boolean {
-  if (info.handle.includes(prefix)) {
-    return true
-  }
-  if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
-    return true
-  }
-  return false
-}
diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts
deleted file mode 100644
index 4fa1213b5..000000000
--- a/src/state/models/feeds/post.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  AppBskyFeedPost as FeedPost,
-  AppBskyFeedDefs,
-  RichText,
-  moderatePost,
-  PostModeration,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {updateDataOptimistically} from 'lib/async/revertible'
-import {track} from 'lib/analytics/analytics'
-import {hackAddDeletedEmbed} from 'lib/api/hack-add-deleted-embed'
-import {logger} from '#/logger'
-
-type FeedViewPost = AppBskyFeedDefs.FeedViewPost
-type ReasonRepost = AppBskyFeedDefs.ReasonRepost
-type PostView = AppBskyFeedDefs.PostView
-
-export class PostsFeedItemModel {
-  // ui state
-  _reactKey: string = ''
-
-  // data
-  post: PostView
-  postRecord?: FeedPost.Record
-  reply?: FeedViewPost['reply']
-  reason?: FeedViewPost['reason']
-  richText?: RichText
-
-  constructor(
-    public rootStore: RootStoreModel,
-    _reactKey: string,
-    v: FeedViewPost,
-  ) {
-    this._reactKey = _reactKey
-    this.post = v.post
-    if (FeedPost.isRecord(this.post.record)) {
-      const valid = FeedPost.validateRecord(this.post.record)
-      if (valid.success) {
-        hackAddDeletedEmbed(this.post)
-        this.postRecord = this.post.record
-        this.richText = new RichText(this.postRecord, {cleanNewlines: true})
-      } else {
-        this.postRecord = undefined
-        this.richText = undefined
-        logger.warn('Received an invalid app.bsky.feed.post record', {
-          error: valid.error,
-        })
-      }
-    } else {
-      this.postRecord = undefined
-      this.richText = undefined
-      logger.warn(
-        'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type',
-        {record: this.post.record},
-      )
-    }
-    this.reply = v.reply
-    this.reason = v.reason
-    makeAutoObservable(this, {rootStore: false})
-  }
-
-  get uri() {
-    return this.post.uri
-  }
-
-  get parentUri() {
-    return this.postRecord?.reply?.parent.uri
-  }
-
-  get rootUri(): string {
-    if (typeof this.postRecord?.reply?.root.uri === 'string') {
-      return this.postRecord?.reply?.root.uri
-    }
-    return this.post.uri
-  }
-
-  get moderation(): PostModeration {
-    return moderatePost(this.post, this.rootStore.preferences.moderationOpts)
-  }
-
-  copy(v: FeedViewPost) {
-    this.post = v.post
-    this.reply = v.reply
-    this.reason = v.reason
-  }
-
-  copyMetrics(v: FeedViewPost) {
-    this.post.replyCount = v.post.replyCount
-    this.post.repostCount = v.post.repostCount
-    this.post.likeCount = v.post.likeCount
-    this.post.viewer = v.post.viewer
-  }
-
-  get reasonRepost(): ReasonRepost | undefined {
-    if (this.reason?.$type === 'app.bsky.feed.defs#reasonRepost') {
-      return this.reason as ReasonRepost
-    }
-  }
-
-  async toggleLike() {
-    this.post.viewer = this.post.viewer || {}
-    try {
-      if (this.post.viewer.like) {
-        // unlike
-        const url = this.post.viewer.like
-        await updateDataOptimistically(
-          this.post,
-          () => {
-            this.post.likeCount = (this.post.likeCount || 0) - 1
-            this.post.viewer!.like = undefined
-          },
-          () => this.rootStore.agent.deleteLike(url),
-        )
-        track('Post:Unlike')
-      } else {
-        // like
-        await updateDataOptimistically(
-          this.post,
-          () => {
-            this.post.likeCount = (this.post.likeCount || 0) + 1
-            this.post.viewer!.like = 'pending'
-          },
-          () => this.rootStore.agent.like(this.post.uri, this.post.cid),
-          res => {
-            this.post.viewer!.like = res.uri
-          },
-        )
-        track('Post:Like')
-      }
-    } catch (error) {
-      logger.error('Failed to toggle like', {error})
-    }
-  }
-
-  async toggleRepost() {
-    this.post.viewer = this.post.viewer || {}
-    try {
-      if (this.post.viewer?.repost) {
-        // unrepost
-        const url = this.post.viewer.repost
-        await updateDataOptimistically(
-          this.post,
-          () => {
-            this.post.repostCount = (this.post.repostCount || 0) - 1
-            this.post.viewer!.repost = undefined
-          },
-          () => this.rootStore.agent.deleteRepost(url),
-        )
-        track('Post:Unrepost')
-      } else {
-        // repost
-        await updateDataOptimistically(
-          this.post,
-          () => {
-            this.post.repostCount = (this.post.repostCount || 0) + 1
-            this.post.viewer!.repost = 'pending'
-          },
-          () => this.rootStore.agent.repost(this.post.uri, this.post.cid),
-          res => {
-            this.post.viewer!.repost = res.uri
-          },
-        )
-        track('Post:Repost')
-      }
-    } catch (error) {
-      logger.error('Failed to toggle repost', {error})
-    }
-  }
-
-  async delete() {
-    try {
-      await this.rootStore.agent.deletePost(this.post.uri)
-      this.rootStore.emitPostDeleted(this.post.uri)
-    } catch (error) {
-      logger.error('Failed to delete post', {error})
-    } finally {
-      track('Post:Delete')
-    }
-  }
-}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 427b0e35e..586be4f42 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -4,7 +4,6 @@ import {
   ComAtprotoServerListAppPasswords,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
-import {MyFollowsCache} from './cache/my-follows'
 import {isObj, hasProp} from 'lib/type-guards'
 import {logger} from '#/logger'
 
@@ -18,7 +17,6 @@ export class MeModel {
   avatar: string = ''
   followsCount: number | undefined
   followersCount: number | undefined
-  follows: MyFollowsCache
   invites: ComAtprotoServerDefs.InviteCode[] = []
   appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = []
   lastProfileStateUpdate = Date.now()
@@ -33,11 +31,9 @@ export class MeModel {
       {rootStore: false, serialize: false, hydrate: false},
       {autoBind: true},
     )
-    this.follows = new MyFollowsCache(this.rootStore)
   }
 
   clear() {
-    this.follows.clear()
     this.rootStore.profiles.cache.clear()
     this.rootStore.posts.cache.clear()
     this.did = ''
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 288e8b8e1..6d7c2c12e 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -15,7 +15,6 @@ import {ProfilesCache} from './cache/profiles-view'
 import {PostsCache} from './cache/posts'
 import {LinkMetasCache} from './cache/link-metas'
 import {MeModel} from './me'
-import {PreferencesModel} from './ui/preferences'
 import {resetToTab} from '../../Navigation'
 import {ImageSizesCache} from './cache/image-sizes'
 import {reset as resetNavigation} from '../../Navigation'
@@ -39,7 +38,6 @@ export class RootStoreModel {
   appInfo?: AppInfo
   session = new SessionModel(this)
   shell = new ShellUiModel(this)
-  preferences = new PreferencesModel(this)
   me = new MeModel(this)
   handleResolutions = new HandleResolutionsCache()
   profiles = new ProfilesCache(this)
@@ -64,7 +62,6 @@ export class RootStoreModel {
     return {
       appInfo: this.appInfo,
       me: this.me.serialize(),
-      preferences: this.preferences.serialize(),
     }
   }
 
@@ -79,9 +76,6 @@ export class RootStoreModel {
       if (hasProp(v, 'me')) {
         this.me.hydrate(v.me)
       }
-      if (hasProp(v, 'preferences')) {
-        this.preferences.hydrate(v.preferences)
-      }
     }
   }
 
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
deleted file mode 100644
index 3a7fcf6c3..000000000
--- a/src/state/models/ui/preferences.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  LabelPreference as APILabelPreference,
-  BskyThreadViewPreference,
-} from '@atproto/api'
-import {isObj, hasProp} from 'lib/type-guards'
-import {RootStoreModel} from '../root-store'
-import {ModerationOpts} from '@atproto/api'
-
-// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
-export type LabelPreference = APILabelPreference | 'show'
-export type ThreadViewPreference = BskyThreadViewPreference & {
-  lab_treeViewEnabled?: boolean | undefined
-}
-
-export class LabelPreferencesModel {
-  nsfw: LabelPreference = 'hide'
-  nudity: LabelPreference = 'warn'
-  suggestive: LabelPreference = 'warn'
-  gore: LabelPreference = 'warn'
-  hate: LabelPreference = 'hide'
-  spam: LabelPreference = 'hide'
-  impersonation: LabelPreference = 'warn'
-
-  constructor() {
-    makeAutoObservable(this, {}, {autoBind: true})
-  }
-}
-
-export class PreferencesModel {
-  contentLabels = new LabelPreferencesModel()
-  savedFeeds: string[] = []
-  pinnedFeeds: string[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this, {}, {autoBind: true})
-  }
-
-  serialize() {
-    return {
-      contentLabels: this.contentLabels,
-      savedFeeds: this.savedFeeds,
-      pinnedFeeds: this.pinnedFeeds,
-    }
-  }
-
-  /**
-   * The function hydrates an object with properties related to content languages, labels, saved feeds,
-   * and pinned feeds that it gets from the parameter `v` (probably local storage)
-   * @param {unknown} v - the data object to hydrate from
-   */
-  hydrate(v: unknown) {
-    if (isObj(v)) {
-      // check if content labels in preferences exist, then hydrate
-      if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
-        Object.assign(this.contentLabels, v.contentLabels)
-      }
-      // check if saved feeds in preferences, then hydrate
-      if (
-        hasProp(v, 'savedFeeds') &&
-        Array.isArray(v.savedFeeds) &&
-        typeof v.savedFeeds.every(item => typeof item === 'string')
-      ) {
-        this.savedFeeds = v.savedFeeds
-      }
-      // check if pinned feeds in preferences exist, then hydrate
-      if (
-        hasProp(v, 'pinnedFeeds') &&
-        Array.isArray(v.pinnedFeeds) &&
-        typeof v.pinnedFeeds.every(item => typeof item === 'string')
-      ) {
-        this.pinnedFeeds = v.pinnedFeeds
-      }
-    }
-  }
-
-  // moderation
-  // =
-
-  /**
-   * @deprecated use `getModerationOpts` from '#/state/queries/preferences/moderation' instead
-   */
-  get moderationOpts(): ModerationOpts {
-    return {
-      userDid: this.rootStore.session.currentSession?.did || '',
-      adultContentEnabled: false,
-      labels: {
-        // TEMP translate old settings until this UI can be migrated -prf
-        porn: tempfixLabelPref(this.contentLabels.nsfw),
-        sexual: tempfixLabelPref(this.contentLabels.suggestive),
-        nudity: tempfixLabelPref(this.contentLabels.nudity),
-        nsfl: tempfixLabelPref(this.contentLabels.gore),
-        corpse: tempfixLabelPref(this.contentLabels.gore),
-        gore: tempfixLabelPref(this.contentLabels.gore),
-        torture: tempfixLabelPref(this.contentLabels.gore),
-        'self-harm': tempfixLabelPref(this.contentLabels.gore),
-        'intolerant-race': tempfixLabelPref(this.contentLabels.hate),
-        'intolerant-gender': tempfixLabelPref(this.contentLabels.hate),
-        'intolerant-sexual-orientation': tempfixLabelPref(
-          this.contentLabels.hate,
-        ),
-        'intolerant-religion': tempfixLabelPref(this.contentLabels.hate),
-        intolerant: tempfixLabelPref(this.contentLabels.hate),
-        'icon-intolerant': tempfixLabelPref(this.contentLabels.hate),
-        spam: tempfixLabelPref(this.contentLabels.spam),
-        impersonation: tempfixLabelPref(this.contentLabels.impersonation),
-        scam: 'warn',
-      },
-      labelers: [
-        {
-          labeler: {
-            did: '',
-            displayName: 'Bluesky Social',
-          },
-          labels: {},
-        },
-      ],
-    }
-  }
-}
-
-// 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
-}
diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 144cc6cd4..192923886 100644
--- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
@@ -1,9 +1,8 @@
 import React from 'react'
 import {View, StyleSheet, ActivityIndicator} from 'react-native'
-import {ProfileModeration} from '@atproto/api'
+import {ProfileModeration, AppBskyActorDefs} from '@atproto/api'
 import {Button} from '#/view/com/util/forms/Button'
 import {usePalette} from 'lib/hooks/usePalette'
-import {SuggestedActor} from 'state/models/discovery/suggested-actors'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {s} from 'lib/styles'
@@ -21,7 +20,7 @@ import {
 import {logger} from '#/logger'
 
 type Props = {
-  profile: SuggestedActor
+  profile: AppBskyActorDefs.ProfileViewBasic
   dataUpdatedAt: number
   moderation: ProfileModeration
   onFollowStateChange: (props: {
@@ -67,7 +66,7 @@ export function ProfileCard({
   onFollowStateChange,
   moderation,
 }: {
-  profile: Shadow<SuggestedActor>
+  profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
   moderation: ProfileModeration
   onFollowStateChange: (props: {
     did: string
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 5c9d1ad2c..74bff1ab0 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -24,6 +24,7 @@ import {
   FeedParams,
   usePostFeedQuery,
 } from '#/state/queries/post-feed'
+import {useModerationOpts} from '#/state/queries/preferences'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@@ -71,6 +72,7 @@ export function Feed({
   const [isPTRing, setIsPTRing] = React.useState(false)
   const checkForNewRef = React.useRef<(() => void) | null>(null)
 
+  const moderationOpts = useModerationOpts()
   const opts = React.useMemo(() => ({enabled}), [enabled])
   const {
     data,
@@ -115,7 +117,7 @@ export function Feed({
 
   const feedItems = React.useMemo(() => {
     let arr: any[] = []
-    if (isFetched) {
+    if (isFetched && moderationOpts) {
       if (isError && isEmpty) {
         arr = arr.concat([ERROR_ITEM])
       }
@@ -133,7 +135,7 @@ export function Feed({
       arr.push(LOADING_ITEM)
     }
     return arr
-  }, [isFetched, isError, isEmpty, data])
+  }, [isFetched, isError, isEmpty, data, moderationOpts])
 
   // events
   // =
@@ -195,7 +197,14 @@ export function Feed({
       } else if (item === LOADING_ITEM) {
         return <PostFeedLoadingPlaceholder />
       }
-      return <FeedSlice slice={item} dataUpdatedAt={dataUpdatedAt} />
+      return (
+        <FeedSlice
+          slice={item}
+          dataUpdatedAt={dataUpdatedAt}
+          // we check for this before creating the feedItems array
+          moderationOpts={moderationOpts!}
+        />
+      )
     },
     [
       feed,
@@ -204,6 +213,7 @@ export function Feed({
       onPressTryAgain,
       onPressRetryLoadMore,
       renderEmptyState,
+      moderationOpts,
     ],
   )
 
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index c33c6028d..fad9f9b4e 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -2,30 +2,28 @@ import React from 'react'
 import {StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FeedPostSlice} from '#/state/queries/post-feed'
-import {AtUri, moderatePost} from '@atproto/api'
+import {AtUri, moderatePost, ModerationOpts} from '@atproto/api'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
 import Svg, {Circle, Line} from 'react-native-svg'
 import {FeedItem} from './FeedItem'
 import {usePalette} from 'lib/hooks/usePalette'
 import {makeProfileLink} from 'lib/routes/links'
-import {useStores} from '#/state'
 
 export const FeedSlice = observer(function FeedSliceImpl({
   slice,
   dataUpdatedAt,
   ignoreFilterFor,
+  moderationOpts,
 }: {
   slice: FeedPostSlice
   dataUpdatedAt: number
   ignoreFilterFor?: string
+  moderationOpts: ModerationOpts
 }) {
-  const store = useStores()
   const moderations = React.useMemo(() => {
-    return slice.items.map(item =>
-      moderatePost(item.post, store.preferences.moderationOpts),
-    )
-  }, [slice, store.preferences.moderationOpts])
+    return slice.items.map(item => moderatePost(item.post, moderationOpts))
+  }, [slice, moderationOpts])
 
   // apply moderation filter
   for (let i = 0; i < slice.items.length; i++) {
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index eeee17d4b..2f359018f 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -11,7 +11,6 @@ import {Text} from '../util/text/Text'
 import {UserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {FollowButton} from './FollowButton'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
@@ -158,19 +157,26 @@ const FollowersList = observer(function FollowersListImpl({
 }: {
   followers?: AppBskyActorDefs.ProfileView[] | undefined
 }) {
-  const store = useStores()
   const pal = usePalette('default')
-  if (!followers?.length) {
+  const moderationOpts = useModerationOpts()
+
+  const followersWithMods = React.useMemo(() => {
+    if (!followers || !moderationOpts) {
+      return []
+    }
+
+    return followers
+      .map(f => ({
+        f,
+        mod: moderateProfile(f, moderationOpts),
+      }))
+      .filter(({mod}) => !mod.account.filter)
+  }, [followers, moderationOpts])
+
+  if (!followersWithMods?.length) {
     return null
   }
 
-  const followersWithMods = followers
-    .map(f => ({
-      f,
-      mod: moderateProfile(f, store.preferences.moderationOpts),
-    }))
-    .filter(({mod}) => !mod.account.filter)
-
   return (
     <View style={styles.followedBy}>
       <Text