From 2045c615a8f8a39ee9f54638a234f3d45f028399 Mon Sep 17 00:00:00 2001
From: Paul Frazee <pfrazee@gmail.com>
Date: Mon, 3 Apr 2023 15:21:17 -0500
Subject: [PATCH] Reorganize state models for clarity (#378)

---
 src/lib/link-meta/bsky.ts                     |  4 +--
 src/lib/notifee.ts                            |  4 +--
 src/state/models/{ => cache}/profiles-view.ts |  4 +--
 .../post-thread.ts}                           | 20 ++++++------
 src/state/models/{ => content}/post.ts        |  4 +--
 .../{profile-view.ts => content/profile.ts}   |  8 ++---
 .../suggested-posts.ts}                       | 10 +++---
 .../user-autocomplete.ts}                     |  4 +--
 .../notifications.ts}                         | 32 +++++++++----------
 .../models/{feed-view.ts => feeds/posts.ts}   | 26 ++++++++-------
 .../models/{likes-view.ts => lists/likes.ts}  |  6 ++--
 .../reposted-by.ts}                           |  6 ++--
 .../user-followers.ts}                        |  4 +--
 .../user-follows.ts}                          |  4 +--
 src/state/models/me.ts                        | 12 +++----
 src/state/models/root-store.ts                | 10 +++---
 src/state/models/ui/profile.ts                | 16 +++++-----
 src/state/models/ui/shell.ts                  |  6 ++--
 src/view/com/composer/Composer.tsx            |  6 ++--
 .../com/composer/text-input/TextInput.tsx     |  4 +--
 .../com/composer/text-input/TextInput.web.tsx |  4 +--
 .../text-input/mobile/Autocomplete.tsx        |  4 +--
 .../composer/text-input/web/Autocomplete.tsx  |  4 +--
 src/view/com/discover/SuggestedPosts.tsx      |  6 ++--
 src/view/com/modals/EditProfile.tsx           |  4 +--
 src/view/com/notifications/Feed.tsx           |  4 +--
 src/view/com/notifications/FeedItem.tsx       |  8 ++---
 src/view/com/post-thread/PostLikedBy.tsx      |  7 ++--
 src/view/com/post-thread/PostRepostedBy.tsx   |  7 ++--
 src/view/com/post-thread/PostThread.tsx       | 18 +++++------
 src/view/com/post-thread/PostThreadItem.tsx   |  4 +--
 src/view/com/post/Post.tsx                    |  8 ++---
 src/view/com/post/PostText.tsx                |  2 +-
 src/view/com/posts/Feed.tsx                   |  4 +--
 src/view/com/posts/FeedItem.tsx               |  4 +--
 src/view/com/posts/FeedSlice.tsx              |  6 ++--
 src/view/com/profile/ProfileFollowers.tsx     |  6 ++--
 src/view/com/profile/ProfileFollows.tsx       |  4 +--
 src/view/com/profile/ProfileHeader.tsx        | 12 ++-----
 src/view/screens/Home.tsx                     |  6 ++--
 src/view/screens/PostThread.tsx               |  6 ++--
 src/view/screens/Profile.tsx                  |  4 +--
 src/view/screens/Search.tsx                   |  6 ++--
 src/view/shell/desktop/Search.tsx             |  6 ++--
 44 files changed, 163 insertions(+), 171 deletions(-)
 rename src/state/models/{ => cache}/profiles-view.ts (93%)
 rename src/state/models/{post-thread-view.ts => content/post-thread.ts} (94%)
 rename src/state/models/{ => content}/post.ts (95%)
 rename src/state/models/{profile-view.ts => content/profile.ts} (97%)
 rename src/state/models/{suggested-posts-view.ts => discovery/suggested-posts.ts} (87%)
 rename src/state/models/{user-autocomplete-view.ts => discovery/user-autocomplete.ts} (96%)
 rename src/state/models/{notifications-view.ts => feeds/notifications.ts} (94%)
 rename src/state/models/{feed-view.ts => feeds/posts.ts} (96%)
 rename src/state/models/{likes-view.ts => lists/likes.ts} (95%)
 rename src/state/models/{reposted-by-view.ts => lists/reposted-by.ts} (95%)
 rename src/state/models/{user-followers-view.ts => lists/user-followers.ts} (96%)
 rename src/state/models/{user-follows-view.ts => lists/user-follows.ts} (96%)

diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts
index 67ce3ad4..f4a96a22 100644
--- a/src/lib/link-meta/bsky.ts
+++ b/src/lib/link-meta/bsky.ts
@@ -2,7 +2,7 @@ import {LikelyType, LinkMeta} from './link-meta'
 // import {match as matchRoute} from 'view/routes'
 import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers'
 import {RootStoreModel} from 'state/index'
-import {PostThreadViewModel} from 'state/models/post-thread-view'
+import {PostThreadModel} from 'state/models/content/post-thread'
 import {ComposerOptsQuote} from 'state/models/ui/shell'
 
 // TODO
@@ -108,7 +108,7 @@ export async function getPostAsQuote(
   const [_0, user, _1, rkey] = url.split('/').filter(Boolean)
   const threadUri = makeRecordUri(user, 'app.bsky.feed.post', rkey)
 
-  const threadView = new PostThreadViewModel(store, {
+  const threadView = new PostThreadModel(store, {
     uri: threadUri,
     depth: 0,
   })
diff --git a/src/lib/notifee.ts b/src/lib/notifee.ts
index 4b53ed72..d2e29c0a 100644
--- a/src/lib/notifee.ts
+++ b/src/lib/notifee.ts
@@ -1,7 +1,7 @@
 import notifee, {EventType} from '@notifee/react-native'
 import {AppBskyEmbedImages} from '@atproto/api'
 import {RootStoreModel} from 'state/models/root-store'
-import {NotificationsViewItemModel} from 'state/models/notifications-view'
+import {NotificationsFeedItemModel} from 'state/models/feeds/notifications'
 import {enforceLen} from 'lib/strings/helpers'
 import {resetToTab} from '../Navigation'
 
@@ -40,7 +40,7 @@ export function displayNotification(
 }
 
 export function displayNotificationFromModel(
-  notif: NotificationsViewItemModel,
+  notif: NotificationsFeedItemModel,
 ) {
   let author = notif.author.displayName || notif.author.handle
   let title: string
diff --git a/src/state/models/profiles-view.ts b/src/state/models/cache/profiles-view.ts
similarity index 93%
rename from src/state/models/profiles-view.ts
rename to src/state/models/cache/profiles-view.ts
index 30e6d044..b4bd70db 100644
--- a/src/state/models/profiles-view.ts
+++ b/src/state/models/cache/profiles-view.ts
@@ -1,10 +1,10 @@
 import {makeAutoObservable} from 'mobx'
 import {LRUMap} from 'lru_map'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
 
 type CacheValue = Promise<GetProfile.Response> | GetProfile.Response
-export class ProfilesViewModel {
+export class ProfilesCache {
   cache: LRUMap<string, CacheValue> = new LRUMap(100)
 
   constructor(public rootStore: RootStoreModel) {
diff --git a/src/state/models/post-thread-view.ts b/src/state/models/content/post-thread.ts
similarity index 94%
rename from src/state/models/post-thread-view.ts
rename to src/state/models/content/post-thread.ts
index c5395b9c..031b8243 100644
--- a/src/state/models/post-thread-view.ts
+++ b/src/state/models/content/post-thread.ts
@@ -5,8 +5,8 @@ import {
   AppBskyFeedDefs,
   RichText,
 } from '@atproto/api'
-import {AtUri} from '../../third-party/uri'
-import {RootStoreModel} from './root-store'
+import {AtUri} from '../../../third-party/uri'
+import {RootStoreModel} from '../root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
 
@@ -17,7 +17,7 @@ function* reactKeyGenerator(): Generator<string> {
   }
 }
 
-export class PostThreadViewPostModel {
+export class PostThreadItemModel {
   // ui state
   _reactKey: string = ''
   _depth = 0
@@ -29,8 +29,8 @@ export class PostThreadViewPostModel {
   // data
   post: AppBskyFeedDefs.PostView
   postRecord?: FeedPost.Record
-  parent?: PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost
-  replies?: (PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost)[]
+  parent?: PostThreadItemModel | AppBskyFeedDefs.NotFoundPost
+  replies?: (PostThreadItemModel | AppBskyFeedDefs.NotFoundPost)[]
   richText?: RichText
 
   get uri() {
@@ -79,7 +79,7 @@ export class PostThreadViewPostModel {
     // parents
     if (includeParent && v.parent) {
       if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
-        const parentModel = new PostThreadViewPostModel(
+        const parentModel = new PostThreadItemModel(
           this.rootStore,
           keyGen.next().value,
           v.parent,
@@ -106,7 +106,7 @@ export class PostThreadViewPostModel {
       const replies = []
       for (const item of v.replies) {
         if (AppBskyFeedDefs.isThreadViewPost(item)) {
-          const itemModel = new PostThreadViewPostModel(
+          const itemModel = new PostThreadItemModel(
             this.rootStore,
             keyGen.next().value,
             item,
@@ -182,7 +182,7 @@ export class PostThreadViewPostModel {
   }
 }
 
-export class PostThreadViewModel {
+export class PostThreadModel {
   // state
   isLoading = false
   isRefreshing = false
@@ -193,7 +193,7 @@ export class PostThreadViewModel {
   params: GetPostThread.QueryParams
 
   // data
-  thread?: PostThreadViewPostModel
+  thread?: PostThreadItemModel
 
   constructor(
     public rootStore: RootStoreModel,
@@ -321,7 +321,7 @@ export class PostThreadViewModel {
   _replaceAll(res: GetPostThread.Response) {
     sortThread(res.data.thread)
     const keyGen = reactKeyGenerator()
-    const thread = new PostThreadViewPostModel(
+    const thread = new PostThreadItemModel(
       this.rootStore,
       keyGen.next().value,
       res.data.thread as AppBskyFeedDefs.ThreadViewPost,
diff --git a/src/state/models/post.ts b/src/state/models/content/post.ts
similarity index 95%
rename from src/state/models/post.ts
rename to src/state/models/content/post.ts
index c7f2896b..bf22ccf1 100644
--- a/src/state/models/post.ts
+++ b/src/state/models/content/post.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable} from 'mobx'
 import {AppBskyFeedPost as Post} from '@atproto/api'
-import {AtUri} from '../../third-party/uri'
-import {RootStoreModel} from './root-store'
+import {AtUri} from '../../../third-party/uri'
+import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
 
 type RemoveIndex<T> = {
diff --git a/src/state/models/profile-view.ts b/src/state/models/content/profile.ts
similarity index 97%
rename from src/state/models/profile-view.ts
rename to src/state/models/content/profile.ts
index eacc6a29..08616bf1 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/content/profile.ts
@@ -5,13 +5,13 @@ import {
   AppBskyActorProfile,
   RichText,
 } from '@atproto/api'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
 
 export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
 
-export class ProfileViewViewerModel {
+export class ProfileViewerModel {
   muted?: boolean
   following?: string
   followedBy?: string
@@ -21,7 +21,7 @@ export class ProfileViewViewerModel {
   }
 }
 
-export class ProfileViewModel {
+export class ProfileModel {
   // state
   isLoading = false
   isRefreshing = false
@@ -40,7 +40,7 @@ export class ProfileViewModel {
   followersCount: number = 0
   followsCount: number = 0
   postsCount: number = 0
-  viewer = new ProfileViewViewerModel()
+  viewer = new ProfileViewerModel()
 
   // added data
   descriptionRichText?: RichText = new RichText({text: ''})
diff --git a/src/state/models/suggested-posts-view.ts b/src/state/models/discovery/suggested-posts.ts
similarity index 87%
rename from src/state/models/suggested-posts-view.ts
rename to src/state/models/discovery/suggested-posts.ts
index 46bf235f..6c8de302 100644
--- a/src/state/models/suggested-posts-view.ts
+++ b/src/state/models/discovery/suggested-posts.ts
@@ -1,6 +1,6 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {RootStoreModel} from './root-store'
-import {FeedItemModel} from './feed-view'
+import {RootStoreModel} from '../root-store'
+import {PostsFeedItemModel} from '../feeds/posts'
 import {cleanError} from 'lib/strings/errors'
 import {TEAM_HANDLES} from 'lib/constants'
 import {
@@ -8,14 +8,14 @@ import {
   mergePosts,
 } from 'lib/api/build-suggested-posts'
 
-export class SuggestedPostsView {
+export class SuggestedPostsModel {
   // state
   isLoading = false
   hasLoaded = false
   error = ''
 
   // data
-  posts: FeedItemModel[] = []
+  posts: PostsFeedItemModel[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(
@@ -57,7 +57,7 @@ export class SuggestedPostsView {
         this.posts = finalPosts.map((post, i) => {
           // strip the reasons to hide that these are reposts
           delete post.reason
-          return new FeedItemModel(this.rootStore, `post-${i}`, post)
+          return new PostsFeedItemModel(this.rootStore, `post-${i}`, post)
         })
       })
       this._xIdle()
diff --git a/src/state/models/user-autocomplete-view.ts b/src/state/models/discovery/user-autocomplete.ts
similarity index 96%
rename from src/state/models/user-autocomplete-view.ts
rename to src/state/models/discovery/user-autocomplete.ts
index ad89bb08..601e10ea 100644
--- a/src/state/models/user-autocomplete-view.ts
+++ b/src/state/models/discovery/user-autocomplete.ts
@@ -1,9 +1,9 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {AppBskyActorDefs} from '@atproto/api'
 import AwaitLock from 'await-lock'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 
-export class UserAutocompleteViewModel {
+export class UserAutocompleteModel {
   // state
   isLoading = false
   isActive = false
diff --git a/src/state/models/notifications-view.ts b/src/state/models/feeds/notifications.ts
similarity index 94%
rename from src/state/models/notifications-view.ts
rename to src/state/models/feeds/notifications.ts
index 7089f012..ea353843 100644
--- a/src/state/models/notifications-view.ts
+++ b/src/state/models/feeds/notifications.ts
@@ -9,8 +9,8 @@ import {
 } from '@atproto/api'
 import AwaitLock from 'await-lock'
 import {bundleAsync} from 'lib/async/bundle'
-import {RootStoreModel} from './root-store'
-import {PostThreadViewModel} from './post-thread-view'
+import {RootStoreModel} from '../root-store'
+import {PostThreadModel} from '../content/post-thread'
 import {cleanError} from 'lib/strings/errors'
 
 const GROUPABLE_REASONS = ['like', 'repost', 'follow']
@@ -30,7 +30,7 @@ type SupportedRecord =
   | AppBskyFeedLike.Record
   | AppBskyGraphFollow.Record
 
-export class NotificationsViewItemModel {
+export class NotificationsFeedItemModel {
   // ui state
   _reactKey: string = ''
 
@@ -47,10 +47,10 @@ export class NotificationsViewItemModel {
   record?: SupportedRecord
   isRead: boolean = false
   indexedAt: string = ''
-  additional?: NotificationsViewItemModel[]
+  additional?: NotificationsFeedItemModel[]
 
   // additional data
-  additionalPost?: PostThreadViewModel
+  additionalPost?: PostThreadModel
 
   constructor(
     public rootStore: RootStoreModel,
@@ -75,7 +75,7 @@ export class NotificationsViewItemModel {
       this.additional = []
       for (const add of v.additional) {
         this.additional.push(
-          new NotificationsViewItemModel(this.rootStore, '', add),
+          new NotificationsFeedItemModel(this.rootStore, '', add),
         )
       }
     } else if (!preserve) {
@@ -171,7 +171,7 @@ export class NotificationsViewItemModel {
       postUri = this.subjectUri
     }
     if (postUri) {
-      this.additionalPost = new PostThreadViewModel(this.rootStore, {
+      this.additionalPost = new PostThreadModel(this.rootStore, {
         uri: postUri,
         depth: 0,
       })
@@ -185,7 +185,7 @@ export class NotificationsViewItemModel {
   }
 }
 
-export class NotificationsViewModel {
+export class NotificationsFeedModel {
   // state
   isLoading = false
   isRefreshing = false
@@ -199,7 +199,7 @@ export class NotificationsViewModel {
   lock = new AwaitLock()
 
   // data
-  notifications: NotificationsViewItemModel[] = []
+  notifications: NotificationsFeedItemModel[] = []
   unreadCount = 0
 
   // this is used to help trigger push notifications
@@ -416,7 +416,7 @@ export class NotificationsViewModel {
     }
   }
 
-  async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
+  async getNewMostRecent(): Promise<NotificationsFeedItemModel | undefined> {
     let old = this.mostRecentNotificationUri
     const res = await this.rootStore.agent.listNotifications({
       limit: 1,
@@ -425,7 +425,7 @@ export class NotificationsViewModel {
       return
     }
     this.mostRecentNotificationUri = res.data.notifications[0].uri
-    const notif = new NotificationsViewItemModel(
+    const notif = new NotificationsFeedItemModel(
       this.rootStore,
       'mostRecent',
       res.data.notifications[0],
@@ -467,9 +467,9 @@ export class NotificationsViewModel {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     const promises = []
-    const itemModels: NotificationsViewItemModel[] = []
+    const itemModels: NotificationsFeedItemModel[] = []
     for (const item of groupNotifications(res.data.notifications)) {
-      const itemModel = new NotificationsViewItemModel(
+      const itemModel = new NotificationsFeedItemModel(
         this.rootStore,
         `item-${_idCounter++}`,
         item,
@@ -496,7 +496,7 @@ export class NotificationsViewModel {
 
   async _prependAll(res: ListNotifications.Response) {
     const promises = []
-    const itemModels: NotificationsViewItemModel[] = []
+    const itemModels: NotificationsFeedItemModel[] = []
     const dedupedNotifs = res.data.notifications.filter(
       n1 =>
         !this.notifications.find(
@@ -504,7 +504,7 @@ export class NotificationsViewModel {
         ),
     )
     for (const item of groupNotifications(dedupedNotifs)) {
-      const itemModel = new NotificationsViewItemModel(
+      const itemModel = new NotificationsFeedItemModel(
         this.rootStore,
         `item-${_idCounter++}`,
         item,
@@ -565,7 +565,7 @@ function groupNotifications(
   return items2
 }
 
-type N = ListNotifications.Notification | NotificationsViewItemModel
+type N = ListNotifications.Notification | NotificationsFeedItemModel
 function isEq(a: N, b: N) {
   // this function has a key subtlety- the indexedAt comparison
   // the reason for this is reposts: they set the URI of the original post, not of the repost record
diff --git a/src/state/models/feed-view.ts b/src/state/models/feeds/posts.ts
similarity index 96%
rename from src/state/models/feed-view.ts
rename to src/state/models/feeds/posts.ts
index 349723fb..9e593f31 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feeds/posts.ts
@@ -10,7 +10,7 @@ import {
 import AwaitLock from 'await-lock'
 import {bundleAsync} from 'lib/async/bundle'
 import sampleSize from 'lodash.samplesize'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
 import {SUGGESTED_FOLLOWS} from 'lib/constants'
 import {
@@ -27,7 +27,7 @@ type PostView = AppBskyFeedDefs.PostView
 const PAGE_SIZE = 30
 let _idCounter = 0
 
-export class FeedItemModel {
+export class PostsFeedItemModel {
   // ui state
   _reactKey: string = ''
 
@@ -139,12 +139,12 @@ export class FeedItemModel {
   }
 }
 
-export class FeedSliceModel {
+export class PostsFeedSliceModel {
   // ui state
   _reactKey: string = ''
 
   // data
-  items: FeedItemModel[] = []
+  items: PostsFeedItemModel[] = []
 
   constructor(
     public rootStore: RootStoreModel,
@@ -154,7 +154,7 @@ export class FeedSliceModel {
     this._reactKey = reactKey
     for (const item of slice.items) {
       this.items.push(
-        new FeedItemModel(rootStore, `item-${_idCounter++}`, item),
+        new PostsFeedItemModel(rootStore, `item-${_idCounter++}`, item),
       )
     }
     makeAutoObservable(this, {rootStore: false})
@@ -206,7 +206,7 @@ export class FeedSliceModel {
   }
 }
 
-export class FeedModel {
+export class PostsFeedModel {
   // state
   isLoading = false
   isRefreshing = false
@@ -223,8 +223,8 @@ export class FeedModel {
   lock = new AwaitLock()
 
   // data
-  slices: FeedSliceModel[] = []
-  nextSlices: FeedSliceModel[] = []
+  slices: PostsFeedSliceModel[] = []
+  nextSlices: PostsFeedSliceModel[] = []
 
   constructor(
     public rootStore: RootStoreModel,
@@ -445,7 +445,11 @@ export class FeedModel {
     if (nextSlices[0]?.uri !== this.slices[0]?.uri) {
       const nextSlicesModels = nextSlices.map(
         slice =>
-          new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
+          new PostsFeedSliceModel(
+            this.rootStore,
+            `item-${_idCounter++}`,
+            slice,
+          ),
       )
       if (autoPrepend) {
         runInAction(() => {
@@ -526,9 +530,9 @@ export class FeedModel {
 
     const slices = this.tuner.tune(res.data.feed, this.feedTuners)
 
-    const toAppend: FeedSliceModel[] = []
+    const toAppend: PostsFeedSliceModel[] = []
     for (const slice of slices) {
-      const sliceModel = new FeedSliceModel(
+      const sliceModel = new PostsFeedSliceModel(
         this.rootStore,
         `item-${_idCounter++}`,
         slice,
diff --git a/src/state/models/likes-view.ts b/src/state/models/lists/likes.ts
similarity index 95%
rename from src/state/models/likes-view.ts
rename to src/state/models/lists/likes.ts
index 80e0be0e..e88389c5 100644
--- a/src/state/models/likes-view.ts
+++ b/src/state/models/lists/likes.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AtUri} from '../../third-party/uri'
+import {AtUri} from '../../../third-party/uri'
 import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
 import {bundleAsync} from 'lib/async/bundle'
 import * as apilib from 'lib/api/index'
@@ -10,7 +10,7 @@ const PAGE_SIZE = 30
 
 export type LikeItem = GetLikes.Like
 
-export class LikesViewModel {
+export class LikesModel {
   // state
   isLoading = false
   isRefreshing = false
diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/lists/reposted-by.ts
similarity index 95%
rename from src/state/models/reposted-by-view.ts
rename to src/state/models/lists/reposted-by.ts
index c9b089c7..08cdc9ef 100644
--- a/src/state/models/reposted-by-view.ts
+++ b/src/state/models/lists/reposted-by.ts
@@ -1,10 +1,10 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AtUri} from '../../third-party/uri'
+import {AtUri} from '../../../third-party/uri'
 import {
   AppBskyFeedGetRepostedBy as GetRepostedBy,
   AppBskyActorDefs,
 } from '@atproto/api'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import {bundleAsync} from 'lib/async/bundle'
 import {cleanError} from 'lib/strings/errors'
 import * as apilib from 'lib/api/index'
@@ -13,7 +13,7 @@ const PAGE_SIZE = 30
 
 export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic
 
-export class RepostedByViewModel {
+export class RepostedByModel {
   // state
   isLoading = false
   isRefreshing = false
diff --git a/src/state/models/user-followers-view.ts b/src/state/models/lists/user-followers.ts
similarity index 96%
rename from src/state/models/user-followers-view.ts
rename to src/state/models/lists/user-followers.ts
index 055032eb..2962d624 100644
--- a/src/state/models/user-followers-view.ts
+++ b/src/state/models/lists/user-followers.ts
@@ -3,7 +3,7 @@ import {
   AppBskyGraphGetFollowers as GetFollowers,
   AppBskyActorDefs as ActorDefs,
 } from '@atproto/api'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
 import {bundleAsync} from 'lib/async/bundle'
 
@@ -11,7 +11,7 @@ const PAGE_SIZE = 30
 
 export type FollowerItem = ActorDefs.ProfileViewBasic
 
-export class UserFollowersViewModel {
+export class UserFollowersModel {
   // state
   isLoading = false
   isRefreshing = false
diff --git a/src/state/models/user-follows-view.ts b/src/state/models/lists/user-follows.ts
similarity index 96%
rename from src/state/models/user-follows-view.ts
rename to src/state/models/lists/user-follows.ts
index 6d9d8459..56432a79 100644
--- a/src/state/models/user-follows-view.ts
+++ b/src/state/models/lists/user-follows.ts
@@ -3,7 +3,7 @@ import {
   AppBskyGraphGetFollows as GetFollows,
   AppBskyActorDefs as ActorDefs,
 } from '@atproto/api'
-import {RootStoreModel} from './root-store'
+import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
 import {bundleAsync} from 'lib/async/bundle'
 
@@ -11,7 +11,7 @@ const PAGE_SIZE = 30
 
 export type FollowItem = ActorDefs.ProfileViewBasic
 
-export class UserFollowsViewModel {
+export class UserFollowsModel {
   // state
   isLoading = false
   isRefreshing = false
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 5f670b8f..26f0849c 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {RootStoreModel} from './root-store'
-import {FeedModel} from './feed-view'
-import {NotificationsViewModel} from './notifications-view'
+import {PostsFeedModel} from './feeds/posts'
+import {NotificationsFeedModel} from './feeds/notifications'
 import {MyFollowsCache} from './cache/my-follows'
 import {isObj, hasProp} from 'lib/type-guards'
 
@@ -13,8 +13,8 @@ export class MeModel {
   avatar: string = ''
   followsCount: number | undefined
   followersCount: number | undefined
-  mainFeed: FeedModel
-  notifications: NotificationsViewModel
+  mainFeed: PostsFeedModel
+  notifications: NotificationsFeedModel
   follows: MyFollowsCache
 
   constructor(public rootStore: RootStoreModel) {
@@ -23,10 +23,10 @@ export class MeModel {
       {rootStore: false, serialize: false, hydrate: false},
       {autoBind: true},
     )
-    this.mainFeed = new FeedModel(this.rootStore, 'home', {
+    this.mainFeed = new PostsFeedModel(this.rootStore, 'home', {
       algorithm: 'reverse-chronological',
     })
-    this.notifications = new NotificationsViewModel(this.rootStore, {})
+    this.notifications = new NotificationsFeedModel(this.rootStore, {})
     this.follows = new MyFollowsCache(this.rootStore)
   }
 
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 0c2a31d2..d4fcbf74 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -12,9 +12,9 @@ import {isObj, hasProp} from 'lib/type-guards'
 import {LogModel} from './log'
 import {SessionModel} from './session'
 import {ShellUiModel} from './ui/shell'
-import {ProfilesViewModel} from './profiles-view'
+import {ProfilesCache} from './cache/profiles-view'
 import {LinkMetasCache} from './cache/link-metas'
-import {NotificationsViewItemModel} from './notifications-view'
+import {NotificationsFeedItemModel} from './feeds/notifications'
 import {MeModel} from './me'
 import {PreferencesModel} from './ui/preferences'
 import {resetToTab} from '../../Navigation'
@@ -36,7 +36,7 @@ export class RootStoreModel {
   shell = new ShellUiModel(this)
   preferences = new PreferencesModel()
   me = new MeModel(this)
-  profiles = new ProfilesViewModel(this)
+  profiles = new ProfilesCache(this)
   linkMetas = new LinkMetasCache(this)
   imageSizes = new ImageSizesCache()
 
@@ -205,11 +205,11 @@ export class RootStoreModel {
 
   // a notification has been queued for push
   onPushNotification(
-    handler: (notif: NotificationsViewItemModel) => void,
+    handler: (notif: NotificationsFeedItemModel) => void,
   ): EmitterSubscription {
     return DeviceEventEmitter.addListener('push-notification', handler)
   }
-  emitPushNotification(notif: NotificationsViewItemModel) {
+  emitPushNotification(notif: NotificationsFeedItemModel) {
     DeviceEventEmitter.emit('push-notification', notif)
   }
 
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index 59529aa3..d06a196f 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from '../root-store'
-import {ProfileViewModel} from '../profile-view'
-import {FeedModel} from '../feed-view'
+import {ProfileModel} from '../content/profile'
+import {PostsFeedModel} from '../feeds/posts'
 
 export enum Sections {
   Posts = 'Posts',
@@ -20,8 +20,8 @@ export class ProfileUiModel {
   static EMPTY_ITEM = {_reactKey: '__empty__'}
 
   // data
-  profile: ProfileViewModel
-  feed: FeedModel
+  profile: ProfileModel
+  feed: PostsFeedModel
 
   // ui state
   selectedViewIndex = 0
@@ -38,14 +38,14 @@ export class ProfileUiModel {
       },
       {autoBind: true},
     )
-    this.profile = new ProfileViewModel(rootStore, {actor: params.user})
-    this.feed = new FeedModel(rootStore, 'author', {
+    this.profile = new ProfileModel(rootStore, {actor: params.user})
+    this.feed = new PostsFeedModel(rootStore, 'author', {
       actor: params.user,
       limit: 10,
     })
   }
 
-  get currentView(): FeedModel {
+  get currentView(): PostsFeedModel {
     if (
       this.selectedView === Sections.Posts ||
       this.selectedView === Sections.PostsWithReplies
@@ -137,7 +137,7 @@ export class ProfileUiModel {
 
   async update() {
     const view = this.currentView
-    if (view instanceof FeedModel) {
+    if (view instanceof PostsFeedModel) {
       await view.update()
     }
   }
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 7f57d5b5..b782dd2f 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,7 +1,7 @@
 import {AppBskyEmbedRecord} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {makeAutoObservable} from 'mobx'
-import {ProfileViewModel} from '../profile-view'
+import {ProfileModel} from '../content/profile'
 import {isObj, hasProp} from 'lib/type-guards'
 import {PickedMedia} from 'lib/media/types'
 
@@ -14,7 +14,7 @@ export interface ConfirmModal {
 
 export interface EditProfileModal {
   name: 'edit-profile'
-  profileView: ProfileViewModel
+  profileView: ProfileModel
   onUpdate?: () => void
 }
 
@@ -77,7 +77,7 @@ interface LightboxModel {}
 
 export class ProfileImageLightbox implements LightboxModel {
   name = 'profile-image'
-  constructor(public profileView: ProfileViewModel) {
+  constructor(public profileView: ProfileModel) {
     makeAutoObservable(this)
   }
 }
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 6009debd..10a44542 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -15,7 +15,7 @@ import LinearGradient from 'react-native-linear-gradient'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {RichText} from '@atproto/api'
 import {useAnalytics} from 'lib/analytics'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {ExternalEmbed} from './ExternalEmbed'
 import {Text} from '../util/text/Text'
 import * as Toast from '../util/Toast'
@@ -69,8 +69,8 @@ export const ComposePost = observer(function ComposePost({
   )
   const [selectedPhotos, setSelectedPhotos] = React.useState<string[]>([])
 
-  const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
-    () => new UserAutocompleteViewModel(store),
+  const autocompleteView = React.useMemo<UserAutocompleteModel>(
+    () => new UserAutocompleteModel(store),
     [store],
   )
 
diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx
index 393d168f..ec218657 100644
--- a/src/view/com/composer/text-input/TextInput.tsx
+++ b/src/view/com/composer/text-input/TextInput.tsx
@@ -11,7 +11,7 @@ import PasteInput, {
 } from '@mattermost/react-native-paste-input'
 import {AppBskyRichtextFacet, RichText} from '@atproto/api'
 import isEqual from 'lodash.isequal'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {Autocomplete} from './mobile/Autocomplete'
 import {Text} from 'view/com/util/text/Text'
 import {useStores} from 'state/index'
@@ -36,7 +36,7 @@ interface TextInputProps {
   richtext: RichText
   placeholder: string
   suggestedLinks: Set<string>
-  autocompleteView: UserAutocompleteViewModel
+  autocompleteView: UserAutocompleteModel
   setRichText: (v: RichText) => void
   onPhotoPasted: (uri: string) => void
   onSuggestedLinksChanged: (uris: Set<string>) => void
diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx
index ad891fa5..68d0d10c 100644
--- a/src/view/com/composer/text-input/TextInput.web.tsx
+++ b/src/view/com/composer/text-input/TextInput.web.tsx
@@ -9,7 +9,7 @@ import {Paragraph} from '@tiptap/extension-paragraph'
 import {Placeholder} from '@tiptap/extension-placeholder'
 import {Text} from '@tiptap/extension-text'
 import isEqual from 'lodash.isequal'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {createSuggestion} from './web/Autocomplete'
 
 export interface TextInputRef {
@@ -21,7 +21,7 @@ interface TextInputProps {
   richtext: RichText
   placeholder: string
   suggestedLinks: Set<string>
-  autocompleteView: UserAutocompleteViewModel
+  autocompleteView: UserAutocompleteModel
   setRichText: (v: RichText) => void
   onPhotoPasted: (uri: string) => void
   onSuggestedLinksChanged: (uris: Set<string>) => void
diff --git a/src/view/com/composer/text-input/mobile/Autocomplete.tsx b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
index 293c89da..879bac07 100644
--- a/src/view/com/composer/text-input/mobile/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/mobile/Autocomplete.tsx
@@ -1,7 +1,7 @@
 import React, {useEffect} from 'react'
 import {Animated, TouchableOpacity, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Text} from 'view/com/util/text/Text'
@@ -11,7 +11,7 @@ export const Autocomplete = observer(
     view,
     onSelect,
   }: {
-    view: UserAutocompleteViewModel
+    view: UserAutocompleteModel
     onSelect: (item: string) => void
   }) => {
     const pal = usePalette('default')
diff --git a/src/view/com/composer/text-input/web/Autocomplete.tsx b/src/view/com/composer/text-input/web/Autocomplete.tsx
index fbe43896..7c6f8770 100644
--- a/src/view/com/composer/text-input/web/Autocomplete.tsx
+++ b/src/view/com/composer/text-input/web/Autocomplete.tsx
@@ -11,7 +11,7 @@ import {
   SuggestionProps,
   SuggestionKeyDownProps,
 } from '@tiptap/suggestion'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 
 interface MentionListRef {
   onKeyDown: (props: SuggestionKeyDownProps) => boolean
@@ -20,7 +20,7 @@ interface MentionListRef {
 export function createSuggestion({
   autocompleteView,
 }: {
-  autocompleteView: UserAutocompleteViewModel
+  autocompleteView: UserAutocompleteModel
 }): Omit<SuggestionOptions, 'editor'> {
   return {
     async items({query}) {
diff --git a/src/view/com/discover/SuggestedPosts.tsx b/src/view/com/discover/SuggestedPosts.tsx
index 9c7745df..6d2f3963 100644
--- a/src/view/com/discover/SuggestedPosts.tsx
+++ b/src/view/com/discover/SuggestedPosts.tsx
@@ -2,7 +2,7 @@ import React from 'react'
 import {ActivityIndicator, StyleSheet, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {useStores} from 'state/index'
-import {SuggestedPostsView} from 'state/models/suggested-posts-view'
+import {SuggestedPostsModel} from 'state/models/discovery/suggested-posts'
 import {s} from 'lib/styles'
 import {FeedItem as Post} from '../posts/FeedItem'
 import {Text} from '../util/text/Text'
@@ -11,8 +11,8 @@ import {usePalette} from 'lib/hooks/usePalette'
 export const SuggestedPosts = observer(() => {
   const pal = usePalette('default')
   const store = useStores()
-  const suggestedPostsView = React.useMemo<SuggestedPostsView>(
-    () => new SuggestedPostsView(store),
+  const suggestedPostsView = React.useMemo<SuggestedPostsModel>(
+    () => new SuggestedPostsModel(store),
     [store],
   )
 
diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx
index 0b81d7f3..4fbf7070 100644
--- a/src/view/com/modals/EditProfile.tsx
+++ b/src/view/com/modals/EditProfile.tsx
@@ -12,7 +12,7 @@ import {PickedMedia} from '../../../lib/media/picker'
 import {Text} from '../util/text/Text'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {useStores} from 'state/index'
-import {ProfileViewModel} from 'state/models/profile-view'
+import {ProfileModel} from 'state/models/content/profile'
 import {s, colors, gradients} from 'lib/styles'
 import {enforceLen} from 'lib/strings/helpers'
 import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants'
@@ -30,7 +30,7 @@ export function Component({
   profileView,
   onUpdate,
 }: {
-  profileView: ProfileViewModel
+  profileView: ProfileModel
   onUpdate?: () => void
 }) {
   const store = useStores()
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index b2fba0fc..83fa0a99 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -2,7 +2,7 @@ import React, {MutableRefObject} from 'react'
 import {observer} from 'mobx-react-lite'
 import {CenteredView, FlatList} from '../util/Views'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
-import {NotificationsViewModel} from 'state/models/notifications-view'
+import {NotificationsFeedModel} from 'state/models/feeds/notifications'
 import {FeedItem} from './FeedItem'
 import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
@@ -19,7 +19,7 @@ export const Feed = observer(function Feed({
   onPressTryAgain,
   onScroll,
 }: {
-  view: NotificationsViewModel
+  view: NotificationsFeedModel
   scrollElRef?: MutableRefObject<FlatList<any> | null>
   onPressTryAgain?: () => void
   onScroll?: OnScrollCb
diff --git a/src/view/com/notifications/FeedItem.tsx b/src/view/com/notifications/FeedItem.tsx
index 7d584e8e..6bff48f0 100644
--- a/src/view/com/notifications/FeedItem.tsx
+++ b/src/view/com/notifications/FeedItem.tsx
@@ -14,8 +14,8 @@ import {
   FontAwesomeIconStyle,
   Props,
 } from '@fortawesome/react-native-fontawesome'
-import {NotificationsViewItemModel} from 'state/models/notifications-view'
-import {PostThreadViewModel} from 'state/models/post-thread-view'
+import {NotificationsFeedItemModel} from 'state/models/feeds/notifications'
+import {PostThreadModel} from 'state/models/content/post-thread'
 import {s, colors} from 'lib/styles'
 import {ago} from 'lib/strings/time'
 import {pluralize} from 'lib/strings/helpers'
@@ -42,7 +42,7 @@ interface Author {
 export const FeedItem = observer(function FeedItem({
   item,
 }: {
-  item: NotificationsViewItemModel
+  item: NotificationsFeedItemModel
 }) {
   const pal = usePalette('default')
   const [isAuthorsExpanded, setAuthorsExpanded] = React.useState<boolean>(false)
@@ -338,7 +338,7 @@ function ExpandedAuthorsList({
 function AdditionalPostText({
   additionalPost,
 }: {
-  additionalPost?: PostThreadViewModel
+  additionalPost?: PostThreadModel
 }) {
   const pal = usePalette('default')
   if (
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 3ca147b8..1b65c04f 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -2,7 +2,7 @@ import React, {useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
 import {CenteredView, FlatList} from '../util/Views'
-import {LikesViewModel, LikeItem} from 'state/models/likes-view'
+import {LikesModel, LikeItem} from 'state/models/lists/likes'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {useStores} from 'state/index'
@@ -11,10 +11,7 @@ import {usePalette} from 'lib/hooks/usePalette'
 export const PostLikedBy = observer(function ({uri}: {uri: string}) {
   const pal = usePalette('default')
   const store = useStores()
-  const view = React.useMemo(
-    () => new LikesViewModel(store, {uri}),
-    [store, uri],
-  )
+  const view = React.useMemo(() => new LikesModel(store, {uri}), [store, uri])
 
   useEffect(() => {
     view.loadMore().catch(err => store.log.error('Failed to fetch likes', err))
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 147d0271..30f8fd44 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -2,10 +2,7 @@ import React, {useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
 import {CenteredView, FlatList} from '../util/Views'
-import {
-  RepostedByViewModel,
-  RepostedByItem,
-} from 'state/models/reposted-by-view'
+import {RepostedByModel, RepostedByItem} from 'state/models/lists/reposted-by'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {useStores} from 'state/index'
@@ -19,7 +16,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
   const pal = usePalette('default')
   const store = useStores()
   const view = React.useMemo(
-    () => new RepostedByViewModel(store, {uri}),
+    () => new RepostedByModel(store, {uri}),
     [store, uri],
   )
 
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 569c6e39..40a6f48c 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -9,9 +9,9 @@ import {
 } from 'react-native'
 import {CenteredView, FlatList} from '../util/Views'
 import {
-  PostThreadViewModel,
-  PostThreadViewPostModel,
-} from 'state/models/post-thread-view'
+  PostThreadModel,
+  PostThreadItemModel,
+} from 'state/models/content/post-thread'
 import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
@@ -31,7 +31,7 @@ const BOTTOM_BORDER = {
   _reactKey: '__bottom_border__',
   _isHighlightedPost: false,
 }
-type YieldedItem = PostThreadViewPostModel | typeof REPLY_PROMPT
+type YieldedItem = PostThreadItemModel | typeof REPLY_PROMPT
 
 export const PostThread = observer(function PostThread({
   uri,
@@ -39,7 +39,7 @@ export const PostThread = observer(function PostThread({
   onPressReply,
 }: {
   uri: string
-  view: PostThreadViewModel
+  view: PostThreadModel
   onPressReply: () => void
 }) {
   const pal = usePalette('default')
@@ -109,7 +109,7 @@ export const PostThread = observer(function PostThread({
         // I could find to get a border positioned directly under the last item
         // -prf
         return <View style={[styles.bottomBorder, pal.border]} />
-      } else if (item instanceof PostThreadViewPostModel) {
+      } else if (item instanceof PostThreadItemModel) {
         return <PostThreadItem item={item} onPostReply={onRefresh} />
       }
       return <></>
@@ -187,14 +187,14 @@ export const PostThread = observer(function PostThread({
 })
 
 function* flattenThread(
-  post: PostThreadViewPostModel,
+  post: PostThreadItemModel,
   isAscending = false,
 ): Generator<YieldedItem, void> {
   if (post.parent) {
     if ('notFound' in post.parent && post.parent.notFound) {
       // TODO render not found
     } else {
-      yield* flattenThread(post.parent as PostThreadViewPostModel, true)
+      yield* flattenThread(post.parent as PostThreadItemModel, true)
     }
   }
   yield post
@@ -206,7 +206,7 @@ function* flattenThread(
       if ('notFound' in reply && reply.notFound) {
         // TODO render not found
       } else {
-        yield* flattenThread(reply as PostThreadViewPostModel)
+        yield* flattenThread(reply as PostThreadItemModel)
       }
     }
   } else if (!isAscending && !post.parent && post.post.replyCount) {
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index cf214806..5a983698 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -7,7 +7,7 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {PostThreadViewPostModel} from 'state/models/post-thread-view'
+import {PostThreadItemModel} from 'state/models/content/post-thread'
 import {Link} from '../util/Link'
 import {RichText} from '../util/text/RichText'
 import {Text} from '../util/text/Text'
@@ -31,7 +31,7 @@ export const PostThreadItem = observer(function PostThreadItem({
   item,
   onPostReply,
 }: {
-  item: PostThreadViewPostModel
+  item: PostThreadItemModel
   onPostReply: () => void
 }) {
   const pal = usePalette('default')
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 6b3dc3ac..3312762d 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -11,7 +11,7 @@ import {observer} from 'mobx-react-lite'
 import Clipboard from '@react-native-clipboard/clipboard'
 import {AtUri} from '../../../third-party/uri'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {PostThreadViewModel} from 'state/models/post-thread-view'
+import {PostThreadModel} from 'state/models/content/post-thread'
 import {Link} from '../util/Link'
 import {UserInfoText} from '../util/UserInfoText'
 import {PostMeta} from '../util/PostMeta'
@@ -34,21 +34,21 @@ export const Post = observer(function Post({
   style,
 }: {
   uri: string
-  initView?: PostThreadViewModel
+  initView?: PostThreadModel
   showReplyLine?: boolean
   hideError?: boolean
   style?: StyleProp<ViewStyle>
 }) {
   const pal = usePalette('default')
   const store = useStores()
-  const [view, setView] = useState<PostThreadViewModel | undefined>(initView)
+  const [view, setView] = useState<PostThreadModel | undefined>(initView)
   const [deleted, setDeleted] = useState(false)
 
   useEffect(() => {
     if (initView || view?.params.uri === uri) {
       return // no change needed? or trigger refresh?
     }
-    const newView = new PostThreadViewModel(store, {uri, depth: 0})
+    const newView = new PostThreadModel(store, {uri, depth: 0})
     setView(newView)
     newView.setup().catch(err => store.log.error('Failed to fetch post', err))
   }, [initView, uri, view?.params.uri, store])
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index a460b57c..1a56a5db 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -4,7 +4,7 @@ import {StyleProp, StyleSheet, TextStyle, View} from 'react-native'
 import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {Text} from '../util/text/Text'
-import {PostModel} from 'state/models/post'
+import {PostModel} from 'state/models/content/post'
 import {useStores} from 'state/index'
 
 export const PostText = observer(function PostText({
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index d07afca3..ddebe5e0 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -11,7 +11,7 @@ import {
 import {FlatList} from '../util/Views'
 import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {FeedModel} from 'state/models/feed-view'
+import {PostsFeedModel} from 'state/models/feeds/posts'
 import {FeedSlice} from './FeedSlice'
 import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
 import {s} from 'lib/styles'
@@ -33,7 +33,7 @@ export const Feed = observer(function Feed({
   testID,
   headerOffset = 0,
 }: {
-  feed: FeedModel
+  feed: PostsFeedModel
   style?: StyleProp<ViewStyle>
   showPostFollowBtn?: boolean
   scrollElRef?: MutableRefObject<FlatList<any> | null>
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 734034a8..8a019a2e 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -7,7 +7,7 @@ import {
   FontAwesomeIcon,
   FontAwesomeIconStyle,
 } from '@fortawesome/react-native-fontawesome'
-import {FeedItemModel} from 'state/models/feed-view'
+import {PostsFeedItemModel} from 'state/models/feeds/posts'
 import {Link, DesktopWebTextLink} from '../util/Link'
 import {Text} from '../util/text/Text'
 import {UserInfoText} from '../util/UserInfoText'
@@ -30,7 +30,7 @@ export const FeedItem = observer(function ({
   showFollowBtn,
   ignoreMuteFor,
 }: {
-  item: FeedItemModel
+  item: PostsFeedItemModel
   isThreadChild?: boolean
   isThreadParent?: boolean
   showReplyLine?: boolean
diff --git a/src/view/com/posts/FeedSlice.tsx b/src/view/com/posts/FeedSlice.tsx
index 806ced20..7fcd1cd2 100644
--- a/src/view/com/posts/FeedSlice.tsx
+++ b/src/view/com/posts/FeedSlice.tsx
@@ -1,6 +1,6 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
-import {FeedSliceModel} from 'state/models/feed-view'
+import {PostsFeedSliceModel} from 'state/models/feeds/posts'
 import {AtUri} from '../../../third-party/uri'
 import {Link} from '../util/Link'
 import {Text} from '../util/text/Text'
@@ -13,7 +13,7 @@ export function FeedSlice({
   showFollowBtn,
   ignoreMuteFor,
 }: {
-  slice: FeedSliceModel
+  slice: PostsFeedSliceModel
   showFollowBtn?: boolean
   ignoreMuteFor?: string
 }) {
@@ -66,7 +66,7 @@ export function FeedSlice({
   )
 }
 
-function ViewFullThread({slice}: {slice: FeedSliceModel}) {
+function ViewFullThread({slice}: {slice: PostsFeedSliceModel}) {
   const pal = usePalette('default')
   const itemHref = React.useMemo(() => {
     const urip = new AtUri(slice.rootItem.post.uri)
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 8d489ad0..0ef652a9 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -2,9 +2,9 @@ import React, {useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
 import {
-  UserFollowersViewModel,
+  UserFollowersModel,
   FollowerItem,
-} from 'state/models/user-followers-view'
+} from 'state/models/lists/user-followers'
 import {CenteredView, FlatList} from '../util/Views'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from './ProfileCard'
@@ -19,7 +19,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
   const pal = usePalette('default')
   const store = useStores()
   const view = React.useMemo(
-    () => new UserFollowersViewModel(store, {actor: name}),
+    () => new UserFollowersModel(store, {actor: name}),
     [store, name],
   )
 
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 849b3344..54b5a319 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -2,7 +2,7 @@ import React, {useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
 import {CenteredView, FlatList} from '../util/Views'
-import {UserFollowsViewModel, FollowItem} from 'state/models/user-follows-view'
+import {UserFollowsModel, FollowItem} from 'state/models/lists/user-follows'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from './ProfileCard'
 import {useStores} from 'state/index'
@@ -16,7 +16,7 @@ export const ProfileFollows = observer(function ProfileFollows({
   const pal = usePalette('default')
   const store = useStores()
   const view = React.useMemo(
-    () => new UserFollowsViewModel(store, {actor: name}),
+    () => new UserFollowsModel(store, {actor: name}),
     [store, name],
   )
 
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 6294c627..878d837c 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -13,7 +13,7 @@ import {
 } from '@fortawesome/react-native-fontawesome'
 import {useNavigation} from '@react-navigation/native'
 import {BlurView} from '../util/BlurView'
-import {ProfileViewModel} from 'state/models/profile-view'
+import {ProfileModel} from 'state/models/content/profile'
 import {useStores} from 'state/index'
 import {ProfileImageLightbox} from 'state/models/ui/shell'
 import {pluralize} from 'lib/strings/helpers'
@@ -34,13 +34,7 @@ import {isDesktopWeb} from 'platform/detection'
 const BACK_HITSLOP = {left: 30, top: 30, right: 30, bottom: 30}
 
 export const ProfileHeader = observer(
-  ({
-    view,
-    onRefreshAll,
-  }: {
-    view: ProfileViewModel
-    onRefreshAll: () => void
-  }) => {
+  ({view, onRefreshAll}: {view: ProfileModel; onRefreshAll: () => void}) => {
     const pal = usePalette('default')
 
     // loading
@@ -91,7 +85,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
   view,
   onRefreshAll,
 }: {
-  view: ProfileViewModel
+  view: ProfileModel
   onRefreshAll: () => void
 }) {
   const pal = usePalette('default')
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 871aae9c..1f9abdaf 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -4,7 +4,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'
 import {observer} from 'mobx-react-lite'
 import useAppState from 'react-native-appstate-hook'
 import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
-import {FeedModel} from 'state/models/feed-view'
+import {PostsFeedModel} from 'state/models/feeds/posts'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {Feed} from '../com/posts/Feed'
 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
@@ -26,7 +26,7 @@ export const HomeScreen = withAuthRequired((_opts: Props) => {
   const [selectedPage, setSelectedPage] = React.useState(0)
 
   const algoFeed = React.useMemo(() => {
-    const feed = new FeedModel(store, 'goodstuff', {})
+    const feed = new PostsFeedModel(store, 'goodstuff', {})
     feed.setup()
     return feed
   }, [store])
@@ -104,7 +104,7 @@ const FeedPage = observer(
     renderEmptyState,
   }: {
     testID?: string
-    feed: FeedModel
+    feed: PostsFeedModel
     isPageFocused: boolean
     renderEmptyState?: () => JSX.Element
   }) => {
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 9bfdcc95..e3ceb0be 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -7,7 +7,7 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
 import {ComposePrompt} from 'view/com/composer/Prompt'
-import {PostThreadViewModel} from 'state/models/post-thread-view'
+import {PostThreadModel} from 'state/models/content/post-thread'
 import {useStores} from 'state/index'
 import {s} from 'lib/styles'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
@@ -22,8 +22,8 @@ export const PostThreadScreen = withAuthRequired(({route}: Props) => {
   const safeAreaInsets = useSafeAreaInsets()
   const {name, rkey} = route.params
   const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
-  const view = useMemo<PostThreadViewModel>(
-    () => new PostThreadViewModel(store, {uri}),
+  const view = useMemo<PostThreadModel>(
+    () => new PostThreadModel(store, {uri}),
     [store, uri],
   )
 
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 556578e7..e3158a97 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -8,7 +8,7 @@ import {ViewSelector} from '../com/util/ViewSelector'
 import {CenteredView} from '../com/util/Views'
 import {ProfileUiModel} from 'state/models/ui/profile'
 import {useStores} from 'state/index'
-import {FeedSliceModel} from 'state/models/feed-view'
+import {PostsFeedSliceModel} from 'state/models/feeds/posts'
 import {ProfileHeader} from '../com/profile/ProfileHeader'
 import {FeedSlice} from '../com/posts/FeedSlice'
 import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
@@ -124,7 +124,7 @@ export const ProfileScreen = withAuthRequired(
               style={styles.emptyState}
             />
           )
-        } else if (item instanceof FeedSliceModel) {
+        } else if (item instanceof PostsFeedSliceModel) {
           return <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
         }
         return <View />
diff --git a/src/view/screens/Search.tsx b/src/view/screens/Search.tsx
index e6947013..e1fb3ec0 100644
--- a/src/view/screens/Search.tsx
+++ b/src/view/screens/Search.tsx
@@ -16,7 +16,7 @@ import {
 import {observer} from 'mobx-react-lite'
 import {Text} from 'view/com/util/text/Text'
 import {useStores} from 'state/index'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {SearchUIModel} from 'state/models/ui/search'
 import {FoafsModel} from 'state/models/discovery/foafs'
 import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
@@ -37,8 +37,8 @@ export const SearchScreen = withAuthRequired(
     const onMainScroll = useOnMainScroll(store)
     const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
     const [query, setQuery] = React.useState<string>('')
-    const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
-      () => new UserAutocompleteViewModel(store),
+    const autocompleteView = React.useMemo<UserAutocompleteModel>(
+      () => new UserAutocompleteModel(store),
       [store],
     )
     const foafs = React.useMemo<FoafsModel>(
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 101840b8..1bc12add 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -1,7 +1,7 @@
 import React from 'react'
 import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native'
 import {useNavigation, StackActions} from '@react-navigation/native'
-import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
+import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
 import {observer} from 'mobx-react-lite'
 import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
@@ -16,8 +16,8 @@ export const DesktopSearch = observer(function DesktopSearch() {
   const textInput = React.useRef<TextInput>(null)
   const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
   const [query, setQuery] = React.useState<string>('')
-  const autocompleteView = React.useMemo<UserAutocompleteViewModel>(
-    () => new UserAutocompleteViewModel(store),
+  const autocompleteView = React.useMemo<UserAutocompleteModel>(
+    () => new UserAutocompleteModel(store),
     [store],
   )
   const navigation = useNavigation<NavigationProp>()