about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-03-31 13:17:26 -0500
committerGitHub <noreply@github.com>2023-03-31 13:17:26 -0500
commita3334a01a221877d3e06e02f960fda441f3460bd (patch)
tree64cdbb1232d1a3c00750c346b6e3ae529b51d1b0 /src/state
parent19f3a2fa92a61ddb785fc4e42d73792c1d0e772c (diff)
downloadvoidsky-a3334a01a221877d3e06e02f960fda441f3460bd.tar.zst
Lex refactor (#362)
* Remove the hackcheck for upgrades

* Rename the PostEmbeds folder to match the codebase style

* Updates to latest lex refactor

* Update to use new bsky agent

* Update to use api package's richtext library

* Switch to upsertProfile

* Add TextEncoder/TextDecoder polyfill

* Add Intl.Segmenter polyfill

* Update composer to calculate lengths by grapheme

* Fix detox

* Fix login in e2e

* Create account e2e passing

* Implement an e2e mocking framework

* Don't use private methods on mobx models as mobx can't track them

* Add tooling for e2e-specific builds and add e2e media-picker mock

* Add some tests and fix some bugs around profile editing

* Add shell tests

* Add home screen tests

* Add thread screen tests

* Add tests for other user profile screens

* Add search screen tests

* Implement profile imagery change tools and tests

* Update to new embed behaviors

* Add post tests

* Fix to profile-screen test

* Fix session resumption

* Update web composer to new api

* 1.11.0

* Fix pagination cursor parameters

* Add quote posts to notifications

* Fix embed layouts

* Remove youtube inline player and improve tap handling on link cards

* Reset minimal shell mode on all screen loads and feed swipes (close #299)

* Update podfile.lock

* Improve post notfound UI (close #366)

* Bump atproto packages
Diffstat (limited to 'src/state')
-rw-r--r--src/state/index.ts4
-rw-r--r--src/state/models/cache/image-sizes.ts2
-rw-r--r--src/state/models/cache/my-follows.ts21
-rw-r--r--src/state/models/discovery/foafs.ts14
-rw-r--r--src/state/models/discovery/suggested-actors.ts20
-rw-r--r--src/state/models/feed-view.ts153
-rw-r--r--src/state/models/likes-view.ts (renamed from src/state/models/votes-view.ts)30
-rw-r--r--src/state/models/log.ts8
-rw-r--r--src/state/models/me.ts2
-rw-r--r--src/state/models/notifications-view.ts76
-rw-r--r--src/state/models/post-thread-view.ts135
-rw-r--r--src/state/models/post.ts12
-rw-r--r--src/state/models/profile-view.ts116
-rw-r--r--src/state/models/profiles-view.ts2
-rw-r--r--src/state/models/reposted-by-view.ts18
-rw-r--r--src/state/models/root-store.ts41
-rw-r--r--src/state/models/session.ts35
-rw-r--r--src/state/models/suggested-posts-view.ts4
-rw-r--r--src/state/models/ui/create-account.ts4
-rw-r--r--src/state/models/ui/profile.ts12
-rw-r--r--src/state/models/ui/search.ts8
-rw-r--r--src/state/models/ui/shell.ts2
-rw-r--r--src/state/models/user-autocomplete-view.ts18
-rw-r--r--src/state/models/user-followers-view.ts19
-rw-r--r--src/state/models/user-follows-view.ts19
25 files changed, 329 insertions, 446 deletions
diff --git a/src/state/index.ts b/src/state/index.ts
index f0713efeb..4755c28f4 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -1,6 +1,6 @@
 import {autorun} from 'mobx'
 import {AppState, Platform} from 'react-native'
-import {AtpAgent} from '@atproto/api'
+import {BskyAgent} from '@atproto/api'
 import {RootStoreModel} from './models/root-store'
 import * as apiPolyfill from 'lib/api/api-polyfill'
 import * as storage from 'lib/storage'
@@ -19,7 +19,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
 
   apiPolyfill.doPolyfill()
 
-  rootStore = new RootStoreModel(new AtpAgent({service: serviceUri}))
+  rootStore = new RootStoreModel(new BskyAgent({service: serviceUri}))
   try {
     data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
     rootStore.log.debug('Initial hydrate', {hasSession: !!data.session})
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts
index ff0486278..2fd6e0013 100644
--- a/src/state/models/cache/image-sizes.ts
+++ b/src/state/models/cache/image-sizes.ts
@@ -3,7 +3,7 @@ import {Dim} from 'lib/media/manip'
 
 export class ImageSizesCache {
   sizes: Map<string, Dim> = new Map()
-  private activeRequests: Map<string, Promise<Dim>> = new Map()
+  activeRequests: Map<string, Promise<Dim>> = new Map()
 
   constructor() {}
 
diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts
index 725b7841e..14eeaae21 100644
--- a/src/state/models/cache/my-follows.ts
+++ b/src/state/models/cache/my-follows.ts
@@ -1,15 +1,12 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
+import {FollowRecord, AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {bundleAsync} from 'lib/async/bundle'
 
 const CACHE_TTL = 1000 * 60 * 60 // hourly
 type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>>
 type FollowsListResponseRecord = FollowsListResponse['records'][0]
-type Profile =
-  | AppBskyActorProfile.ViewBasic
-  | AppBskyActorProfile.View
-  | AppBskyActorRef.WithInfo
+type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
 
 /**
  * This model is used to maintain a synced local cache of the user's
@@ -53,21 +50,21 @@ export class MyFollowsCache {
 
   fetch = bundleAsync(async () => {
     this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
-    let before
+    let rkeyStart
     let records: FollowsListResponseRecord[] = []
     do {
       const res: FollowsListResponse =
-        await this.rootStore.api.app.bsky.graph.follow.list({
-          user: this.rootStore.me.did,
-          before,
+        await this.rootStore.agent.app.bsky.graph.follow.list({
+          repo: this.rootStore.me.did,
+          rkeyStart,
         })
       records = records.concat(res.records)
-      before = res.cursor
-    } while (typeof before !== 'undefined')
+      rkeyStart = res.cursor
+    } while (typeof rkeyStart !== 'undefined')
     runInAction(() => {
       this.followDidToRecordMap = {}
       for (const record of records) {
-        this.followDidToRecordMap[record.value.subject.did] = record.uri
+        this.followDidToRecordMap[record.value.subject] = record.uri
       }
       this.lastSync = Date.now()
       this.myDid = this.rootStore.me.did
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts
index 241338a16..27cee8503 100644
--- a/src/state/models/discovery/foafs.ts
+++ b/src/state/models/discovery/foafs.ts
@@ -1,15 +1,15 @@
-import {AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
+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 = AppBskyActorRef.WithInfo & {
-  followers: AppBskyActorProfile.View[]
+export type RefWithInfoAndFollowers = AppBskyActorDefs.ProfileViewBasic & {
+  followers: AppBskyActorDefs.ProfileView[]
 }
 
-export type ProfileViewFollows = AppBskyActorProfile.View & {
-  follows: AppBskyActorRef.WithInfo[]
+export type ProfileViewFollows = AppBskyActorDefs.ProfileView & {
+  follows: AppBskyActorDefs.ProfileViewBasic[]
 }
 
 export class FoafsModel {
@@ -51,14 +51,14 @@ export class FoafsModel {
       this.popular.length = 0
 
       // fetch their profiles
-      const profiles = await this.rootStore.api.app.bsky.actor.getProfiles({
+      const profiles = await this.rootStore.agent.getProfiles({
         actors: this.sources,
       })
 
       // fetch their follows
       const results = await Promise.allSettled(
         this.sources.map(source =>
-          this.rootStore.api.app.bsky.graph.getFollows({user: source}),
+          this.rootStore.agent.getFollows({actor: source}),
         ),
       )
 
diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts
index cf8e2dd7b..91c5efd02 100644
--- a/src/state/models/discovery/suggested-actors.ts
+++ b/src/state/models/discovery/suggested-actors.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyActorProfile as Profile} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import shuffle from 'lodash.shuffle'
 import {RootStoreModel} from '../root-store'
 import {cleanError} from 'lib/strings/errors'
@@ -8,7 +8,9 @@ import {SUGGESTED_FOLLOWS} from 'lib/constants'
 
 const PAGE_SIZE = 30
 
-export type SuggestedActor = Profile.ViewBasic | Profile.View
+export type SuggestedActor =
+  | AppBskyActorDefs.ProfileViewBasic
+  | AppBskyActorDefs.ProfileView
 
 export class SuggestedActorsModel {
   // state
@@ -20,7 +22,7 @@ export class SuggestedActorsModel {
   hasMore = true
   loadMoreCursor?: string
 
-  private hardCodedSuggestions: SuggestedActor[] | undefined
+  hardCodedSuggestions: SuggestedActor[] | undefined
 
   // data
   suggestions: SuggestedActor[] = []
@@ -82,7 +84,7 @@ export class SuggestedActorsModel {
           this.loadMoreCursor = undefined
         } else {
           // pull from the PDS' algo
-          res = await this.rootStore.api.app.bsky.actor.getSuggestions({
+          res = await this.rootStore.agent.app.bsky.actor.getSuggestions({
             limit: this.pageSize,
             cursor: this.loadMoreCursor,
           })
@@ -104,7 +106,7 @@ export class SuggestedActorsModel {
     }
   })
 
-  private async fetchHardcodedSuggestions() {
+  async fetchHardcodedSuggestions() {
     if (this.hardCodedSuggestions) {
       return
     }
@@ -118,9 +120,9 @@ export class SuggestedActorsModel {
       ]
 
       // fetch the profiles in chunks of 25 (the limit allowed by `getProfiles`)
-      let profiles: Profile.View[] = []
+      let profiles: AppBskyActorDefs.ProfileView[] = []
       do {
-        const res = await this.rootStore.api.app.bsky.actor.getProfiles({
+        const res = await this.rootStore.agent.getProfiles({
           actors: actors.splice(0, 25),
         })
         profiles = profiles.concat(res.data.profiles)
@@ -152,13 +154,13 @@ export class SuggestedActorsModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 083863fe2..8b62c958f 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -1,32 +1,29 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {
   AppBskyFeedGetTimeline as GetTimeline,
-  AppBskyFeedFeedViewPost,
+  AppBskyFeedDefs,
   AppBskyFeedPost,
   AppBskyFeedGetAuthorFeed as GetAuthorFeed,
+  RichText,
 } from '@atproto/api'
 import AwaitLock from 'await-lock'
 import {bundleAsync} from 'lib/async/bundle'
 import sampleSize from 'lodash.samplesize'
-type FeedViewPost = AppBskyFeedFeedViewPost.Main
-type ReasonRepost = AppBskyFeedFeedViewPost.ReasonRepost
-type PostView = AppBskyFeedPost.View
-import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from './root-store'
-import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
-import {RichText} from 'lib/strings/rich-text'
 import {SUGGESTED_FOLLOWS} from 'lib/constants'
 import {
   getCombinedCursors,
   getMultipleAuthorsPosts,
   mergePosts,
 } from 'lib/api/build-suggested-posts'
-
 import {FeedTuner, FeedViewPostsSlice} from 'lib/api/feed-manip'
 
-const PAGE_SIZE = 30
+type FeedViewPost = AppBskyFeedDefs.FeedViewPost
+type ReasonRepost = AppBskyFeedDefs.ReasonRepost
+type PostView = AppBskyFeedDefs.PostView
 
+const PAGE_SIZE = 30
 let _idCounter = 0
 
 export class FeedItemModel {
@@ -51,11 +48,7 @@ export class FeedItemModel {
       const valid = AppBskyFeedPost.validateRecord(this.post.record)
       if (valid.success) {
         this.postRecord = this.post.record
-        this.richText = new RichText(
-          this.postRecord.text,
-          this.postRecord.entities,
-          {cleanNewlines: true},
-        )
+        this.richText = new RichText(this.postRecord, {cleanNewlines: true})
       } else {
         rootStore.log.warn(
           'Received an invalid app.bsky.feed.post record',
@@ -82,7 +75,7 @@ export class FeedItemModel {
   copyMetrics(v: FeedViewPost) {
     this.post.replyCount = v.post.replyCount
     this.post.repostCount = v.post.repostCount
-    this.post.upvoteCount = v.post.upvoteCount
+    this.post.likeCount = v.post.likeCount
     this.post.viewer = v.post.viewer
   }
 
@@ -92,68 +85,43 @@ export class FeedItemModel {
     }
   }
 
-  async toggleUpvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasUpvoted ? 'none' : 'up',
-    })
-    runInAction(() => {
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      }
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      } else {
-        this.post.upvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
-  }
-
-  async toggleDownvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasDownvoted ? 'none' : 'down',
-    })
-    runInAction(() => {
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      }
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      } else {
-        this.post.downvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
+  async toggleLike() {
+    if (this.post.viewer?.like) {
+      await this.rootStore.agent.deleteLike(this.post.viewer.like)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount--
+        this.post.viewer.like = undefined
+      })
+    } else {
+      const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount++
+        this.post.viewer.like = res.uri
+      })
+    }
   }
 
   async toggleRepost() {
-    if (this.post.viewer.repost) {
-      await apilib.unrepost(this.rootStore, this.post.viewer.repost)
+    if (this.post.viewer?.repost) {
+      await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount--
         this.post.viewer.repost = undefined
       })
     } else {
-      const res = await apilib.repost(
-        this.rootStore,
+      const res = await this.rootStore.agent.repost(
         this.post.uri,
         this.post.cid,
       )
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount++
         this.post.viewer.repost = res.uri
       })
@@ -161,10 +129,7 @@ export class FeedItemModel {
   }
 
   async delete() {
-    await this.rootStore.api.app.bsky.feed.post.delete({
-      did: this.post.author.did,
-      rkey: new AtUri(this.post.uri).rkey,
-    })
+    await this.rootStore.agent.deletePost(this.post.uri)
     this.rootStore.emitPostDeleted(this.post.uri)
   }
 }
@@ -250,7 +215,7 @@ export class FeedModel {
   tuner = new FeedTuner()
 
   // used to linearize async modifications to state
-  private lock = new AwaitLock()
+  lock = new AwaitLock()
 
   // data
   slices: FeedSliceModel[] = []
@@ -291,8 +256,8 @@ export class FeedModel {
         const params = this.params as GetAuthorFeed.QueryParams
         const item = slice.rootItem
         const isRepost =
-          item?.reasonRepost?.by?.handle === params.author ||
-          item?.reasonRepost?.by?.did === params.author
+          item?.reasonRepost?.by?.handle === params.actor ||
+          item?.reasonRepost?.by?.did === params.actor
         return (
           !item.reply || // not a reply
           isRepost || // but allow if it's a repost
@@ -338,7 +303,7 @@ export class FeedModel {
     return this.setup()
   }
 
-  private get feedTuners() {
+  get feedTuners() {
     if (this.feedType === 'goodstuff') {
       return [
         FeedTuner.dedupReposts,
@@ -406,7 +371,7 @@ export class FeedModel {
       this._xLoading()
       try {
         const res = await this._getFeed({
-          before: this.loadMoreCursor,
+          cursor: this.loadMoreCursor,
           limit: PAGE_SIZE,
         })
         await this._appendAll(res)
@@ -439,7 +404,7 @@ export class FeedModel {
       try {
         do {
           const res: GetTimeline.Response = await this._getFeed({
-            before: cursor,
+            cursor,
             limit: Math.min(numToFetch, 100),
           })
           if (res.data.feed.length === 0) {
@@ -478,14 +443,18 @@ export class FeedModel {
           new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
       )
       if (autoPrepend) {
-        this.slices = nextSlicesModels.concat(
-          this.slices.filter(slice1 =>
-            nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
-          ),
-        )
-        this.setHasNewLatest(false)
+        runInAction(() => {
+          this.slices = nextSlicesModels.concat(
+            this.slices.filter(slice1 =>
+              nextSlicesModels.find(slice2 => slice1.uri === slice2.uri),
+            ),
+          )
+          this.setHasNewLatest(false)
+        })
       } else {
-        this.nextSlices = nextSlicesModels
+        runInAction(() => {
+          this.nextSlices = nextSlicesModels
+        })
         this.setHasNewLatest(true)
       }
     } else {
@@ -519,13 +488,13 @@ export class FeedModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -538,14 +507,12 @@ export class FeedModel {
   // helper functions
   // =
 
-  private async _replaceAll(
-    res: GetTimeline.Response | GetAuthorFeed.Response,
-  ) {
+  async _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
     this.pollCursor = res.data.feed[0]?.post.uri
     return this._appendAll(res, true)
   }
 
-  private async _appendAll(
+  async _appendAll(
     res: GetTimeline.Response | GetAuthorFeed.Response,
     replace = false,
   ) {
@@ -572,7 +539,7 @@ export class FeedModel {
     })
   }
 
-  private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
+  _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
     for (const item of res.data.feed) {
       const existingSlice = this.slices.find(slice =>
         slice.containsUri(item.post.uri),
@@ -596,7 +563,7 @@ export class FeedModel {
       const responses = await getMultipleAuthorsPosts(
         this.rootStore,
         sampleSize(SUGGESTED_FOLLOWS(String(this.rootStore.agent.service)), 20),
-        params.before,
+        params.cursor,
         20,
       )
       const combinedCursor = getCombinedCursors(responses)
@@ -611,9 +578,7 @@ export class FeedModel {
         headers: lastHeaders,
       }
     } else if (this.feedType === 'home') {
-      return this.rootStore.api.app.bsky.feed.getTimeline(
-        params as GetTimeline.QueryParams,
-      )
+      return this.rootStore.agent.getTimeline(params as GetTimeline.QueryParams)
     } else if (this.feedType === 'goodstuff') {
       const res = await getGoodStuff(
         this.rootStore.session.currentSession?.accessJwt || '',
@@ -624,7 +589,7 @@ export class FeedModel {
       )
       return res
     } else {
-      return this.rootStore.api.app.bsky.feed.getAuthorFeed(
+      return this.rootStore.agent.getAuthorFeed(
         params as GetAuthorFeed.QueryParams,
       )
     }
diff --git a/src/state/models/votes-view.ts b/src/state/models/likes-view.ts
index ad8698d21..5f9df692e 100644
--- a/src/state/models/votes-view.ts
+++ b/src/state/models/likes-view.ts
@@ -1,6 +1,6 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {AtUri} from '../../third-party/uri'
-import {AppBskyFeedGetVotes as GetVotes} from '@atproto/api'
+import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {cleanError} from 'lib/strings/errors'
 import {bundleAsync} from 'lib/async/bundle'
@@ -8,24 +8,24 @@ import * as apilib from 'lib/api/index'
 
 const PAGE_SIZE = 30
 
-export type VoteItem = GetVotes.Vote
+export type LikeItem = GetLikes.Like
 
-export class VotesViewModel {
+export class LikesViewModel {
   // state
   isLoading = false
   isRefreshing = false
   hasLoaded = false
   error = ''
   resolvedUri = ''
-  params: GetVotes.QueryParams
+  params: GetLikes.QueryParams
   hasMore = true
   loadMoreCursor?: string
 
   // data
   uri: string = ''
-  votes: VoteItem[] = []
+  likes: LikeItem[] = []
 
-  constructor(public rootStore: RootStoreModel, params: GetVotes.QueryParams) {
+  constructor(public rootStore: RootStoreModel, params: GetLikes.QueryParams) {
     makeAutoObservable(
       this,
       {
@@ -68,9 +68,9 @@ export class VotesViewModel {
       const params = Object.assign({}, this.params, {
         uri: this.resolvedUri,
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.feed.getVotes(params)
+      const res = await this.rootStore.agent.getLikes(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -85,13 +85,13 @@ export class VotesViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -104,7 +104,7 @@ export class VotesViewModel {
   // helper functions
   // =
 
-  private async _resolveUri() {
+  async _resolveUri() {
     const urip = new AtUri(this.params.uri)
     if (!urip.host.startsWith('did:')) {
       try {
@@ -118,14 +118,14 @@ export class VotesViewModel {
     })
   }
 
-  private _replaceAll(res: GetVotes.Response) {
-    this.votes = []
+  _replaceAll(res: GetLikes.Response) {
+    this.likes = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetVotes.Response) {
+  _appendAll(res: GetLikes.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
-    this.votes = this.votes.concat(res.data.votes)
+    this.likes = this.likes.concat(res.data.likes)
   }
 }
diff --git a/src/state/models/log.ts b/src/state/models/log.ts
index ed701dc61..d80617139 100644
--- a/src/state/models/log.ts
+++ b/src/state/models/log.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable} from 'mobx'
-import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc'
+// import {XRPCError, XRPCInvalidResponseError} from '@atproto/xrpc' TODO
 
 const MAX_ENTRIES = 300
 
@@ -32,7 +32,7 @@ export class LogModel {
     makeAutoObservable(this)
   }
 
-  private add(entry: LogEntry) {
+  add(entry: LogEntry) {
     this.entries.push(entry)
     while (this.entries.length > MAX_ENTRIES) {
       this.entries = this.entries.slice(50)
@@ -79,14 +79,14 @@ export class LogModel {
 function detailsToStr(details?: any) {
   if (details && typeof details !== 'string') {
     if (
-      details instanceof XRPCInvalidResponseError ||
+      // details instanceof XRPCInvalidResponseError || TODO
       details.constructor.name === 'XRPCInvalidResponseError'
     ) {
       return `The server gave an ill-formatted response.\nMethod: ${
         details.lexiconNsid
       }.\nError: ${details.validationError.toString()}`
     } else if (
-      details instanceof XRPCError ||
+      // details instanceof XRPCError || TODO
       details.constructor.name === 'XRPCError'
     ) {
       return `An XRPC error occurred.\nStatus: ${details.status}\nError: ${details.error}\nMessage: ${details.message}`
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 120749155..5f670b8f9 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -85,7 +85,7 @@ export class MeModel {
     if (sess.hasSession) {
       this.did = sess.currentSession?.did || ''
       this.handle = sess.currentSession?.handle || ''
-      const profile = await this.rootStore.api.app.bsky.actor.getProfile({
+      const profile = await this.rootStore.agent.getProfile({
         actor: this.did,
       })
       runInAction(() => {
diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts
index e88af590b..4f7a52fd9 100644
--- a/src/state/models/notifications-view.ts
+++ b/src/state/models/notifications-view.ts
@@ -1,11 +1,10 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {
-  AppBskyNotificationList as ListNotifications,
-  AppBskyActorRef as ActorRef,
+  AppBskyNotificationListNotifications as ListNotifications,
+  AppBskyActorDefs,
   AppBskyFeedPost,
   AppBskyFeedRepost,
-  AppBskyFeedVote,
-  AppBskyGraphAssertion,
+  AppBskyFeedLike,
   AppBskyGraphFollow,
 } from '@atproto/api'
 import AwaitLock from 'await-lock'
@@ -28,8 +27,7 @@ export interface GroupedNotification extends ListNotifications.Notification {
 type SupportedRecord =
   | AppBskyFeedPost.Record
   | AppBskyFeedRepost.Record
-  | AppBskyFeedVote.Record
-  | AppBskyGraphAssertion.Record
+  | AppBskyFeedLike.Record
   | AppBskyGraphFollow.Record
 
 export class NotificationsViewItemModel {
@@ -39,11 +37,10 @@ export class NotificationsViewItemModel {
   // data
   uri: string = ''
   cid: string = ''
-  author: ActorRef.WithInfo = {
+  author: AppBskyActorDefs.ProfileViewBasic = {
     did: '',
     handle: '',
     avatar: '',
-    declaration: {cid: '', actorType: ''},
   }
   reason: string = ''
   reasonSubject?: string
@@ -86,8 +83,8 @@ export class NotificationsViewItemModel {
     }
   }
 
-  get isUpvote() {
-    return this.reason === 'vote'
+  get isLike() {
+    return this.reason === 'like'
   }
 
   get isRepost() {
@@ -102,16 +99,22 @@ export class NotificationsViewItemModel {
     return this.reason === 'reply'
   }
 
-  get isFollow() {
-    return this.reason === 'follow'
+  get isQuote() {
+    return this.reason === 'quote'
   }
 
-  get isAssertion() {
-    return this.reason === 'assertion'
+  get isFollow() {
+    return this.reason === 'follow'
   }
 
   get needsAdditionalData() {
-    if (this.isUpvote || this.isRepost || this.isReply || this.isMention) {
+    if (
+      this.isLike ||
+      this.isRepost ||
+      this.isReply ||
+      this.isQuote ||
+      this.isMention
+    ) {
       return !this.additionalPost
     }
     return false
@@ -124,7 +127,7 @@ export class NotificationsViewItemModel {
     const record = this.record
     if (
       AppBskyFeedRepost.isRecord(record) ||
-      AppBskyFeedVote.isRecord(record)
+      AppBskyFeedLike.isRecord(record)
     ) {
       return record.subject.uri
     }
@@ -135,8 +138,7 @@ export class NotificationsViewItemModel {
     for (const ns of [
       AppBskyFeedPost,
       AppBskyFeedRepost,
-      AppBskyFeedVote,
-      AppBskyGraphAssertion,
+      AppBskyFeedLike,
       AppBskyGraphFollow,
     ]) {
       if (ns.isRecord(v)) {
@@ -163,9 +165,9 @@ export class NotificationsViewItemModel {
       return
     }
     let postUri
-    if (this.isReply || this.isMention) {
+    if (this.isReply || this.isQuote || this.isMention) {
       postUri = this.uri
-    } else if (this.isUpvote || this.isRepost) {
+    } else if (this.isLike || this.isRepost) {
       postUri = this.subjectUri
     }
     if (postUri) {
@@ -194,7 +196,7 @@ export class NotificationsViewModel {
   loadMoreCursor?: string
 
   // used to linearize async modifications to state
-  private lock = new AwaitLock()
+  lock = new AwaitLock()
 
   // data
   notifications: NotificationsViewItemModel[] = []
@@ -266,7 +268,7 @@ export class NotificationsViewModel {
         const params = Object.assign({}, this.params, {
           limit: PAGE_SIZE,
         })
-        const res = await this.rootStore.api.app.bsky.notification.list(params)
+        const res = await this.rootStore.agent.listNotifications(params)
         await this._replaceAll(res)
         this._xIdle()
       } catch (e: any) {
@@ -297,9 +299,9 @@ export class NotificationsViewModel {
       try {
         const params = Object.assign({}, this.params, {
           limit: PAGE_SIZE,
-          before: this.loadMoreCursor,
+          cursor: this.loadMoreCursor,
         })
-        const res = await this.rootStore.api.app.bsky.notification.list(params)
+        const res = await this.rootStore.agent.listNotifications(params)
         await this._appendAll(res)
         this._xIdle()
       } catch (e: any) {
@@ -325,7 +327,7 @@ export class NotificationsViewModel {
     try {
       this._xLoading()
       try {
-        const res = await this.rootStore.api.app.bsky.notification.list({
+        const res = await this.rootStore.agent.listNotifications({
           limit: PAGE_SIZE,
         })
         await this._prependAll(res)
@@ -357,8 +359,8 @@ export class NotificationsViewModel {
       try {
         do {
           const res: ListNotifications.Response =
-            await this.rootStore.api.app.bsky.notification.list({
-              before: cursor,
+            await this.rootStore.agent.listNotifications({
+              cursor,
               limit: Math.min(numToFetch, 100),
             })
           if (res.data.notifications.length === 0) {
@@ -390,7 +392,7 @@ export class NotificationsViewModel {
    */
   loadUnreadCount = bundleAsync(async () => {
     const old = this.unreadCount
-    const res = await this.rootStore.api.app.bsky.notification.getCount()
+    const res = await this.rootStore.agent.countUnreadNotifications()
     runInAction(() => {
       this.unreadCount = res.data.count
     })
@@ -408,9 +410,7 @@ export class NotificationsViewModel {
       for (const notif of this.notifications) {
         notif.isRead = true
       }
-      await this.rootStore.api.app.bsky.notification.updateSeen({
-        seenAt: new Date().toISOString(),
-      })
+      await this.rootStore.agent.updateSeenNotifications()
     } catch (e: any) {
       this.rootStore.log.warn('Failed to update notifications read state', e)
     }
@@ -418,7 +418,7 @@ export class NotificationsViewModel {
 
   async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
     let old = this.mostRecentNotificationUri
-    const res = await this.rootStore.api.app.bsky.notification.list({
+    const res = await this.rootStore.agent.listNotifications({
       limit: 1,
     })
     if (!res.data.notifications[0] || old === res.data.notifications[0].uri) {
@@ -437,13 +437,13 @@ export class NotificationsViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -456,14 +456,14 @@ export class NotificationsViewModel {
   // helper functions
   // =
 
-  private async _replaceAll(res: ListNotifications.Response) {
+  async _replaceAll(res: ListNotifications.Response) {
     if (res.data.notifications[0]) {
       this.mostRecentNotificationUri = res.data.notifications[0].uri
     }
     return this._appendAll(res, true)
   }
 
-  private async _appendAll(res: ListNotifications.Response, replace = false) {
+  async _appendAll(res: ListNotifications.Response, replace = false) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     const promises = []
@@ -494,7 +494,7 @@ export class NotificationsViewModel {
     })
   }
 
-  private async _prependAll(res: ListNotifications.Response) {
+  async _prependAll(res: ListNotifications.Response) {
     const promises = []
     const itemModels: NotificationsViewItemModel[] = []
     const dedupedNotifs = res.data.notifications.filter(
@@ -525,7 +525,7 @@ export class NotificationsViewModel {
     })
   }
 
-  private _updateAll(res: ListNotifications.Response) {
+  _updateAll(res: ListNotifications.Response) {
     for (const item of res.data.notifications) {
       const existingItem = this.notifications.find(item2 => isEq(item, item2))
       if (existingItem) {
diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts
index d58ee691b..c5395b9c8 100644
--- a/src/state/models/post-thread-view.ts
+++ b/src/state/models/post-thread-view.ts
@@ -2,12 +2,13 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {
   AppBskyFeedGetPostThread as GetPostThread,
   AppBskyFeedPost as FeedPost,
+  AppBskyFeedDefs,
+  RichText,
 } from '@atproto/api'
 import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from './root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
-import {RichText} from 'lib/strings/rich-text'
 
 function* reactKeyGenerator(): Generator<string> {
   let counter = 0
@@ -26,10 +27,10 @@ export class PostThreadViewPostModel {
   _hasMore = false
 
   // data
-  post: FeedPost.View
+  post: AppBskyFeedDefs.PostView
   postRecord?: FeedPost.Record
-  parent?: PostThreadViewPostModel | GetPostThread.NotFoundPost
-  replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
+  parent?: PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost
+  replies?: (PostThreadViewPostModel | AppBskyFeedDefs.NotFoundPost)[]
   richText?: RichText
 
   get uri() {
@@ -43,7 +44,7 @@ export class PostThreadViewPostModel {
   constructor(
     public rootStore: RootStoreModel,
     reactKey: string,
-    v: GetPostThread.ThreadViewPost,
+    v: AppBskyFeedDefs.ThreadViewPost,
   ) {
     this._reactKey = reactKey
     this.post = v.post
@@ -51,11 +52,7 @@ export class PostThreadViewPostModel {
       const valid = FeedPost.validateRecord(this.post.record)
       if (valid.success) {
         this.postRecord = this.post.record
-        this.richText = new RichText(
-          this.postRecord.text,
-          this.postRecord.entities,
-          {cleanNewlines: true},
-        )
+        this.richText = new RichText(this.postRecord, {cleanNewlines: true})
       } else {
         rootStore.log.warn(
           'Received an invalid app.bsky.feed.post record',
@@ -74,14 +71,14 @@ export class PostThreadViewPostModel {
 
   assignTreeModels(
     keyGen: Generator<string>,
-    v: GetPostThread.ThreadViewPost,
+    v: AppBskyFeedDefs.ThreadViewPost,
     higlightedPostUri: string,
     includeParent = true,
     includeChildren = true,
   ) {
     // parents
     if (includeParent && v.parent) {
-      if (GetPostThread.isThreadViewPost(v.parent)) {
+      if (AppBskyFeedDefs.isThreadViewPost(v.parent)) {
         const parentModel = new PostThreadViewPostModel(
           this.rootStore,
           keyGen.next().value,
@@ -100,7 +97,7 @@ export class PostThreadViewPostModel {
           )
         }
         this.parent = parentModel
-      } else if (GetPostThread.isNotFoundPost(v.parent)) {
+      } else if (AppBskyFeedDefs.isNotFoundPost(v.parent)) {
         this.parent = v.parent
       }
     }
@@ -108,7 +105,7 @@ export class PostThreadViewPostModel {
     if (includeChildren && v.replies) {
       const replies = []
       for (const item of v.replies) {
-        if (GetPostThread.isThreadViewPost(item)) {
+        if (AppBskyFeedDefs.isThreadViewPost(item)) {
           const itemModel = new PostThreadViewPostModel(
             this.rootStore,
             keyGen.next().value,
@@ -128,7 +125,7 @@ export class PostThreadViewPostModel {
             )
           }
           replies.push(itemModel)
-        } else if (GetPostThread.isNotFoundPost(item)) {
+        } else if (AppBskyFeedDefs.isNotFoundPost(item)) {
           replies.push(item)
         }
       }
@@ -136,68 +133,43 @@ export class PostThreadViewPostModel {
     }
   }
 
-  async toggleUpvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasUpvoted ? 'none' : 'up',
-    })
-    runInAction(() => {
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      }
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      } else {
-        this.post.upvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
-  }
-
-  async toggleDownvote() {
-    const wasUpvoted = !!this.post.viewer.upvote
-    const wasDownvoted = !!this.post.viewer.downvote
-    const res = await this.rootStore.api.app.bsky.feed.setVote({
-      subject: {
-        uri: this.post.uri,
-        cid: this.post.cid,
-      },
-      direction: wasDownvoted ? 'none' : 'down',
-    })
-    runInAction(() => {
-      if (wasUpvoted) {
-        this.post.upvoteCount--
-      }
-      if (wasDownvoted) {
-        this.post.downvoteCount--
-      } else {
-        this.post.downvoteCount++
-      }
-      this.post.viewer.upvote = res.data.upvote
-      this.post.viewer.downvote = res.data.downvote
-    })
+  async toggleLike() {
+    if (this.post.viewer?.like) {
+      await this.rootStore.agent.deleteLike(this.post.viewer.like)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount--
+        this.post.viewer.like = undefined
+      })
+    } else {
+      const res = await this.rootStore.agent.like(this.post.uri, this.post.cid)
+      runInAction(() => {
+        this.post.likeCount = this.post.likeCount || 0
+        this.post.viewer = this.post.viewer || {}
+        this.post.likeCount++
+        this.post.viewer.like = res.uri
+      })
+    }
   }
 
   async toggleRepost() {
-    if (this.post.viewer.repost) {
-      await apilib.unrepost(this.rootStore, this.post.viewer.repost)
+    if (this.post.viewer?.repost) {
+      await this.rootStore.agent.deleteRepost(this.post.viewer.repost)
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount--
         this.post.viewer.repost = undefined
       })
     } else {
-      const res = await apilib.repost(
-        this.rootStore,
+      const res = await this.rootStore.agent.repost(
         this.post.uri,
         this.post.cid,
       )
       runInAction(() => {
+        this.post.repostCount = this.post.repostCount || 0
+        this.post.viewer = this.post.viewer || {}
         this.post.repostCount++
         this.post.viewer.repost = res.uri
       })
@@ -205,10 +177,7 @@ export class PostThreadViewPostModel {
   }
 
   async delete() {
-    await this.rootStore.api.app.bsky.feed.post.delete({
-      did: this.post.author.did,
-      rkey: new AtUri(this.post.uri).rkey,
-    })
+    await this.rootStore.agent.deletePost(this.post.uri)
     this.rootStore.emitPostDeleted(this.post.uri)
   }
 }
@@ -301,14 +270,14 @@ export class PostThreadViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
     this.notFound = false
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -322,7 +291,7 @@ export class PostThreadViewModel {
   // loader functions
   // =
 
-  private async _resolveUri() {
+  async _resolveUri() {
     const urip = new AtUri(this.params.uri)
     if (!urip.host.startsWith('did:')) {
       try {
@@ -336,10 +305,10 @@ export class PostThreadViewModel {
     })
   }
 
-  private async _load(isRefreshing = false) {
+  async _load(isRefreshing = false) {
     this._xLoading(isRefreshing)
     try {
-      const res = await this.rootStore.api.app.bsky.feed.getPostThread(
+      const res = await this.rootStore.agent.getPostThread(
         Object.assign({}, this.params, {uri: this.resolvedUri}),
       )
       this._replaceAll(res)
@@ -349,18 +318,18 @@ export class PostThreadViewModel {
     }
   }
 
-  private _replaceAll(res: GetPostThread.Response) {
+  _replaceAll(res: GetPostThread.Response) {
     sortThread(res.data.thread)
     const keyGen = reactKeyGenerator()
     const thread = new PostThreadViewPostModel(
       this.rootStore,
       keyGen.next().value,
-      res.data.thread as GetPostThread.ThreadViewPost,
+      res.data.thread as AppBskyFeedDefs.ThreadViewPost,
     )
     thread._isHighlightedPost = true
     thread.assignTreeModels(
       keyGen,
-      res.data.thread as GetPostThread.ThreadViewPost,
+      res.data.thread as AppBskyFeedDefs.ThreadViewPost,
       thread.uri,
     )
     this.thread = thread
@@ -368,25 +337,25 @@ export class PostThreadViewModel {
 }
 
 type MaybePost =
-  | GetPostThread.ThreadViewPost
-  | GetPostThread.NotFoundPost
+  | AppBskyFeedDefs.ThreadViewPost
+  | AppBskyFeedDefs.NotFoundPost
   | {[k: string]: unknown; $type: string}
 function sortThread(post: MaybePost) {
   if (post.notFound) {
     return
   }
-  post = post as GetPostThread.ThreadViewPost
+  post = post as AppBskyFeedDefs.ThreadViewPost
   if (post.replies) {
     post.replies.sort((a: MaybePost, b: MaybePost) => {
-      post = post as GetPostThread.ThreadViewPost
+      post = post as AppBskyFeedDefs.ThreadViewPost
       if (a.notFound) {
         return 1
       }
       if (b.notFound) {
         return -1
       }
-      a = a as GetPostThread.ThreadViewPost
-      b = b as GetPostThread.ThreadViewPost
+      a = a as AppBskyFeedDefs.ThreadViewPost
+      b = b as AppBskyFeedDefs.ThreadViewPost
       const aIsByOp = a.post.author.did === post.post.author.did
       const bIsByOp = b.post.author.did === post.post.author.did
       if (aIsByOp && bIsByOp) {
diff --git a/src/state/models/post.ts b/src/state/models/post.ts
index 749e98bb0..c7f2896ba 100644
--- a/src/state/models/post.ts
+++ b/src/state/models/post.ts
@@ -58,12 +58,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
   // state transitions
   // =
 
-  private _xLoading() {
+  _xLoading() {
     this.isLoading = true
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.hasLoaded = true
     this.error = cleanError(err)
@@ -75,12 +75,12 @@ export class PostModel implements RemoveIndex<Post.Record> {
   // loader functions
   // =
 
-  private async _load() {
+  async _load() {
     this._xLoading()
     try {
       const urip = new AtUri(this.uri)
-      const res = await this.rootStore.api.app.bsky.feed.post.get({
-        user: urip.host,
+      const res = await this.rootStore.agent.getPost({
+        repo: urip.host,
         rkey: urip.rkey,
       })
       // TODO
@@ -94,7 +94,7 @@ export class PostModel implements RemoveIndex<Post.Record> {
     }
   }
 
-  private _replaceAll(res: Post.Record) {
+  _replaceAll(res: Post.Record) {
     this.text = res.text
     this.entities = res.entities
     this.reply = res.reply
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index 9d3eeff58..eacc6a298 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -2,15 +2,12 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {PickedMedia} from 'lib/media/picker'
 import {
   AppBskyActorGetProfile as GetProfile,
-  AppBskySystemDeclRef,
-  AppBskyActorUpdateProfile,
+  AppBskyActorProfile,
+  RichText,
 } from '@atproto/api'
-type DeclRef = AppBskySystemDeclRef.Main
-import {extractEntities} from 'lib/strings/rich-text-detection'
 import {RootStoreModel} from './root-store'
 import * as apilib from 'lib/api/index'
 import {cleanError} from 'lib/strings/errors'
-import {RichText} from 'lib/strings/rich-text'
 
 export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
 
@@ -35,22 +32,18 @@ export class ProfileViewModel {
   // data
   did: string = ''
   handle: string = ''
-  declaration: DeclRef = {
-    cid: '',
-    actorType: '',
-  }
   creator: string = ''
-  displayName?: string
-  description?: string
-  avatar?: string
-  banner?: string
+  displayName?: string = ''
+  description?: string = ''
+  avatar?: string = ''
+  banner?: string = ''
   followersCount: number = 0
   followsCount: number = 0
   postsCount: number = 0
   viewer = new ProfileViewViewerModel()
 
   // added data
-  descriptionRichText?: RichText
+  descriptionRichText?: RichText = new RichText({text: ''})
 
   constructor(
     public rootStore: RootStoreModel,
@@ -79,10 +72,6 @@ export class ProfileViewModel {
     return this.hasLoaded && !this.hasContent
   }
 
-  get isUser() {
-    return this.declaration.actorType === ACTOR_TYPE_USER
-  }
-
   // public api
   // =
 
@@ -111,18 +100,14 @@ export class ProfileViewModel {
     }
 
     if (followUri) {
-      await apilib.unfollow(this.rootStore, followUri)
+      await this.rootStore.agent.deleteFollow(followUri)
       runInAction(() => {
         this.followersCount--
         this.viewer.following = undefined
         this.rootStore.me.follows.removeFollow(this.did)
       })
     } else {
-      const res = await apilib.follow(
-        this.rootStore,
-        this.did,
-        this.declaration.cid,
-      )
+      const res = await this.rootStore.agent.follow(this.did)
       runInAction(() => {
         this.followersCount++
         this.viewer.following = res.uri
@@ -132,49 +117,48 @@ export class ProfileViewModel {
   }
 
   async updateProfile(
-    updates: AppBskyActorUpdateProfile.InputSchema,
+    updates: AppBskyActorProfile.Record,
     newUserAvatar: PickedMedia | undefined | null,
     newUserBanner: PickedMedia | undefined | null,
   ) {
-    if (newUserAvatar) {
-      const res = await apilib.uploadBlob(
-        this.rootStore,
-        newUserAvatar.path,
-        newUserAvatar.mime,
-      )
-      updates.avatar = {
-        cid: res.data.cid,
-        mimeType: newUserAvatar.mime,
+    await this.rootStore.agent.upsertProfile(async existing => {
+      existing = existing || {}
+      existing.displayName = updates.displayName
+      existing.description = updates.description
+      if (newUserAvatar) {
+        const res = await apilib.uploadBlob(
+          this.rootStore,
+          newUserAvatar.path,
+          newUserAvatar.mime,
+        )
+        existing.avatar = res.data.blob
+      } else if (newUserAvatar === null) {
+        existing.avatar = undefined
       }
-    } else if (newUserAvatar === null) {
-      updates.avatar = null
-    }
-    if (newUserBanner) {
-      const res = await apilib.uploadBlob(
-        this.rootStore,
-        newUserBanner.path,
-        newUserBanner.mime,
-      )
-      updates.banner = {
-        cid: res.data.cid,
-        mimeType: newUserBanner.mime,
+      if (newUserBanner) {
+        const res = await apilib.uploadBlob(
+          this.rootStore,
+          newUserBanner.path,
+          newUserBanner.mime,
+        )
+        existing.banner = res.data.blob
+      } else if (newUserBanner === null) {
+        existing.banner = undefined
       }
-    } else if (newUserBanner === null) {
-      updates.banner = null
-    }
-    await this.rootStore.api.app.bsky.actor.updateProfile(updates)
+      return existing
+    })
     await this.rootStore.me.load()
     await this.refresh()
   }
 
   async muteAccount() {
-    await this.rootStore.api.app.bsky.graph.mute({user: this.did})
+    await this.rootStore.agent.mute(this.did)
     this.viewer.muted = true
     await this.refresh()
   }
 
   async unmuteAccount() {
-    await this.rootStore.api.app.bsky.graph.unmute({user: this.did})
+    await this.rootStore.agent.unmute(this.did)
     this.viewer.muted = false
     await this.refresh()
   }
@@ -182,13 +166,13 @@ export class ProfileViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -201,40 +185,40 @@ export class ProfileViewModel {
   // loader functions
   // =
 
-  private async _load(isRefreshing = false) {
+  async _load(isRefreshing = false) {
     this._xLoading(isRefreshing)
     try {
-      const res = await this.rootStore.api.app.bsky.actor.getProfile(
-        this.params,
-      )
+      const res = await this.rootStore.agent.getProfile(this.params)
       this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation
       this._replaceAll(res)
+      await this._createRichText()
       this._xIdle()
     } catch (e: any) {
       this._xIdle(e)
     }
   }
 
-  private _replaceAll(res: GetProfile.Response) {
+  _replaceAll(res: GetProfile.Response) {
     this.did = res.data.did
     this.handle = res.data.handle
-    Object.assign(this.declaration, res.data.declaration)
-    this.creator = res.data.creator
     this.displayName = res.data.displayName
     this.description = res.data.description
     this.avatar = res.data.avatar
     this.banner = res.data.banner
-    this.followersCount = res.data.followersCount
-    this.followsCount = res.data.followsCount
-    this.postsCount = res.data.postsCount
+    this.followersCount = res.data.followersCount || 0
+    this.followsCount = res.data.followsCount || 0
+    this.postsCount = res.data.postsCount || 0
     if (res.data.viewer) {
       Object.assign(this.viewer, res.data.viewer)
       this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
     }
+  }
+
+  async _createRichText() {
     this.descriptionRichText = new RichText(
-      this.description || '',
-      extractEntities(this.description || ''),
+      {text: this.description || ''},
       {cleanNewlines: true},
     )
+    await this.descriptionRichText.detectFacets(this.rootStore.agent)
   }
 }
diff --git a/src/state/models/profiles-view.ts b/src/state/models/profiles-view.ts
index 4241e50e1..30e6d0442 100644
--- a/src/state/models/profiles-view.ts
+++ b/src/state/models/profiles-view.ts
@@ -31,7 +31,7 @@ export class ProfilesViewModel {
       }
     }
     try {
-      const promise = this.rootStore.api.app.bsky.actor.getProfile({
+      const promise = this.rootStore.agent.getProfile({
         actor: did,
       })
       this.cache.set(did, promise)
diff --git a/src/state/models/reposted-by-view.ts b/src/state/models/reposted-by-view.ts
index 69a728d6f..c9b089c70 100644
--- a/src/state/models/reposted-by-view.ts
+++ b/src/state/models/reposted-by-view.ts
@@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction} from 'mobx'
 import {AtUri} from '../../third-party/uri'
 import {
   AppBskyFeedGetRepostedBy as GetRepostedBy,
-  AppBskyActorRef as ActorRef,
+  AppBskyActorDefs,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {bundleAsync} from 'lib/async/bundle'
@@ -11,7 +11,7 @@ import * as apilib from 'lib/api/index'
 
 const PAGE_SIZE = 30
 
-export type RepostedByItem = ActorRef.WithInfo
+export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic
 
 export class RepostedByViewModel {
   // state
@@ -71,9 +71,9 @@ export class RepostedByViewModel {
       const params = Object.assign({}, this.params, {
         uri: this.resolvedUri,
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.feed.getRepostedBy(params)
+      const res = await this.rootStore.agent.getRepostedBy(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -88,13 +88,13 @@ export class RepostedByViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -107,7 +107,7 @@ export class RepostedByViewModel {
   // helper functions
   // =
 
-  private async _resolveUri() {
+  async _resolveUri() {
     const urip = new AtUri(this.params.uri)
     if (!urip.host.startsWith('did:')) {
       try {
@@ -121,12 +121,12 @@ export class RepostedByViewModel {
     })
   }
 
-  private _replaceAll(res: GetRepostedBy.Response) {
+  _replaceAll(res: GetRepostedBy.Response) {
     this.repostedBy = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetRepostedBy.Response) {
+  _appendAll(res: GetRepostedBy.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index d8336d005..0c2a31d28 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -2,8 +2,8 @@
  * The root store is the base of all modeled state.
  */
 
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AtpAgent} from '@atproto/api'
+import {makeAutoObservable} from 'mobx'
+import {BskyAgent} from '@atproto/api'
 import {createContext, useContext} from 'react'
 import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
 import * as BgScheduler from 'lib/bg-scheduler'
@@ -29,7 +29,7 @@ export const appInfo = z.object({
 export type AppInfo = z.infer<typeof appInfo>
 
 export class RootStoreModel {
-  agent: AtpAgent
+  agent: BskyAgent
   appInfo?: AppInfo
   log = new LogModel()
   session = new SessionModel(this)
@@ -40,41 +40,16 @@ export class RootStoreModel {
   linkMetas = new LinkMetasCache(this)
   imageSizes = new ImageSizesCache()
 
-  // HACK
-  // this flag is to track the lexicon breaking refactor
-  // it should be removed once we get that done
-  // -prf
-  hackUpgradeNeeded = false
-  async hackCheckIfUpgradeNeeded() {
-    try {
-      this.log.debug('hackCheckIfUpgradeNeeded()')
-      const res = await fetch('https://bsky.social/xrpc/app.bsky.feed.getLikes')
-      await res.text()
-      runInAction(() => {
-        this.hackUpgradeNeeded = res.status !== 501
-        this.log.debug(
-          `hackCheckIfUpgradeNeeded() said ${this.hackUpgradeNeeded}`,
-        )
-      })
-    } catch (e) {
-      this.log.error('Failed to hackCheckIfUpgradeNeeded', {e})
-    }
-  }
-
-  constructor(agent: AtpAgent) {
+  constructor(agent: BskyAgent) {
     this.agent = agent
     makeAutoObservable(this, {
-      api: false,
+      agent: false,
       serialize: false,
       hydrate: false,
     })
     this.initBgFetch()
   }
 
-  get api() {
-    return this.agent.api
-  }
-
   setAppInfo(info: AppInfo) {
     this.appInfo = info
   }
@@ -131,7 +106,7 @@ export class RootStoreModel {
   /**
    * Called by the session model. Refreshes session-oriented state.
    */
-  async handleSessionChange(agent: AtpAgent) {
+  async handleSessionChange(agent: BskyAgent) {
     this.log.debug('RootStoreModel:handleSessionChange')
     this.agent = agent
     this.me.clear()
@@ -259,7 +234,7 @@ export class RootStoreModel {
   async onBgFetch(taskId: string) {
     this.log.debug(`Background fetch fired for task ${taskId}`)
     if (this.session.hasSession) {
-      const res = await this.api.app.bsky.notification.getCount()
+      const res = await this.agent.countUnreadNotifications()
       const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count
       this.emitUnreadNotifications(res.data.count)
       this.log.debug(
@@ -286,7 +261,7 @@ export class RootStoreModel {
 }
 
 const throwawayInst = new RootStoreModel(
-  new AtpAgent({service: 'http://localhost'}),
+  new BskyAgent({service: 'http://localhost'}),
 ) // this will be replaced by the loader, we just need to supply a value at init
 const RootStoreContext = createContext<RootStoreModel>(throwawayInst)
 export const RootStoreProvider = RootStoreContext.Provider
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index e131b2b2c..c2e10880d 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -1,9 +1,9 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {
-  AtpAgent,
+  BskyAgent,
   AtpSessionEvent,
   AtpSessionData,
-  ComAtprotoServerGetAccountsConfig as GetAccountsConfig,
+  ComAtprotoServerDescribeServer as DescribeServer,
 } from '@atproto/api'
 import normalizeUrl from 'normalize-url'
 import {isObj, hasProp} from 'lib/type-guards'
@@ -11,7 +11,7 @@ import {networkRetry} from 'lib/async/retry'
 import {z} from 'zod'
 import {RootStoreModel} from './root-store'
 
-export type ServiceDescription = GetAccountsConfig.OutputSchema
+export type ServiceDescription = DescribeServer.OutputSchema
 
 export const activeSession = z.object({
   service: z.string(),
@@ -40,7 +40,7 @@ export class SessionModel {
   // emergency log facility to help us track down this logout issue
   // remove when resolved
   // -prf
-  private _log(message: string, details?: Record<string, any>) {
+  _log(message: string, details?: Record<string, any>) {
     details = details || {}
     details.state = {
       data: this.data,
@@ -73,6 +73,7 @@ export class SessionModel {
       rootStore: false,
       serialize: false,
       hydrate: false,
+      hasSession: false,
     })
   }
 
@@ -154,7 +155,7 @@ export class SessionModel {
   /**
    * Sets the active session
    */
-  async setActiveSession(agent: AtpAgent, did: string) {
+  async setActiveSession(agent: BskyAgent, did: string) {
     this._log('SessionModel:setActiveSession')
     this.data = {
       service: agent.service.toString(),
@@ -166,7 +167,7 @@ export class SessionModel {
   /**
    * Upserts a session into the accounts
    */
-  private persistSession(
+  persistSession(
     service: string,
     did: string,
     event: AtpSessionEvent,
@@ -225,7 +226,7 @@ export class SessionModel {
   /**
    * Clears any session tokens from the accounts; used on logout.
    */
-  private clearSessionTokens() {
+  clearSessionTokens() {
     this._log('SessionModel:clearSessionTokens')
     this.accounts = this.accounts.map(acct => ({
       service: acct.service,
@@ -239,10 +240,8 @@ export class SessionModel {
   /**
    * Fetches additional information about an account on load.
    */
-  private async loadAccountInfo(agent: AtpAgent, did: string) {
-    const res = await agent.api.app.bsky.actor
-      .getProfile({actor: did})
-      .catch(_e => undefined)
+  async loadAccountInfo(agent: BskyAgent, did: string) {
+    const res = await agent.getProfile({actor: did}).catch(_e => undefined)
     if (res) {
       return {
         dispayName: res.data.displayName,
@@ -255,8 +254,8 @@ export class SessionModel {
    * Helper to fetch the accounts config settings from an account.
    */
   async describeService(service: string): Promise<ServiceDescription> {
-    const agent = new AtpAgent({service})
-    const res = await agent.api.com.atproto.server.getAccountsConfig({})
+    const agent = new BskyAgent({service})
+    const res = await agent.com.atproto.server.describeServer({})
     return res.data
   }
 
@@ -272,7 +271,7 @@ export class SessionModel {
       return false
     }
 
-    const agent = new AtpAgent({
+    const agent = new BskyAgent({
       service: account.service,
       persistSession: (evt: AtpSessionEvent, sess?: AtpSessionData) => {
         this.persistSession(account.service, account.did, evt, sess)
@@ -321,7 +320,7 @@ export class SessionModel {
     password: string
   }) {
     this._log('SessionModel:login')
-    const agent = new AtpAgent({service})
+    const agent = new BskyAgent({service})
     await agent.login({identifier, password})
     if (!agent.session) {
       throw new Error('Failed to establish session')
@@ -355,7 +354,7 @@ export class SessionModel {
     inviteCode?: string
   }) {
     this._log('SessionModel:createAccount')
-    const agent = new AtpAgent({service})
+    const agent = new BskyAgent({service})
     await agent.createAccount({
       handle,
       password,
@@ -389,7 +388,7 @@ export class SessionModel {
     // need to evaluate why deleting the session has caused errors at times
     // -prf
     /*if (this.hasSession) {
-      this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
+      this.rootStore.agent.com.atproto.session.delete().catch((e: any) => {
         this.rootStore.log.warn(
           '(Minor issue) Failed to delete session on the server',
           e,
@@ -415,7 +414,7 @@ export class SessionModel {
     if (!sess) {
       return
     }
-    const res = await this.rootStore.api.app.bsky.actor
+    const res = await this.rootStore.agent
       .getProfile({actor: sess.did})
       .catch(_e => undefined)
     if (res?.success) {
diff --git a/src/state/models/suggested-posts-view.ts b/src/state/models/suggested-posts-view.ts
index 7a5ca81b9..46bf235ff 100644
--- a/src/state/models/suggested-posts-view.ts
+++ b/src/state/models/suggested-posts-view.ts
@@ -72,12 +72,12 @@ export class SuggestedPostsView {
   // state transitions
   // =
 
-  private _xLoading() {
+  _xLoading() {
     this.isLoading = true
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.hasLoaded = true
     this.error = cleanError(err)
diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts
index a212fe05e..e661cb59d 100644
--- a/src/state/models/ui/create-account.ts
+++ b/src/state/models/ui/create-account.ts
@@ -2,7 +2,7 @@ import {makeAutoObservable} from 'mobx'
 import {RootStoreModel} from '../root-store'
 import {ServiceDescription} from '../session'
 import {DEFAULT_SERVICE} from 'state/index'
-import {ComAtprotoAccountCreate} from '@atproto/api'
+import {ComAtprotoServerCreateAccount} from '@atproto/api'
 import * as EmailValidator from 'email-validator'
 import {createFullHandle} from 'lib/strings/handles'
 import {cleanError} from 'lib/strings/errors'
@@ -99,7 +99,7 @@ export class CreateAccountModel {
       })
     } catch (e: any) {
       let errMsg = e.toString()
-      if (e instanceof ComAtprotoAccountCreate.InvalidInviteCodeError) {
+      if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
         errMsg =
           'Invite code not accepted. Check that you input it correctly and try again.'
       }
diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts
index 280541b74..59529aa39 100644
--- a/src/state/models/ui/profile.ts
+++ b/src/state/models/ui/profile.ts
@@ -40,7 +40,7 @@ export class ProfileUiModel {
     )
     this.profile = new ProfileViewModel(rootStore, {actor: params.user})
     this.feed = new FeedModel(rootStore, 'author', {
-      author: params.user,
+      actor: params.user,
       limit: 10,
     })
   }
@@ -64,16 +64,8 @@ export class ProfileUiModel {
     return this.profile.isRefreshing || this.currentView.isRefreshing
   }
 
-  get isUser() {
-    return this.profile.isUser
-  }
-
   get selectorItems() {
-    if (this.isUser) {
-      return USER_SELECTOR_ITEMS
-    } else {
-      return USER_SELECTOR_ITEMS
-    }
+    return USER_SELECTOR_ITEMS
   }
 
   get selectedView() {
diff --git a/src/state/models/ui/search.ts b/src/state/models/ui/search.ts
index 91e1b24bf..8436b0984 100644
--- a/src/state/models/ui/search.ts
+++ b/src/state/models/ui/search.ts
@@ -1,6 +1,6 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {searchProfiles, searchPosts} from 'lib/api/search'
-import {AppBskyActorProfile as Profile} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 
 export class SearchUIModel {
@@ -8,7 +8,7 @@ export class SearchUIModel {
   isProfilesLoading = false
   query: string = ''
   postUris: string[] = []
-  profiles: Profile.View[] = []
+  profiles: AppBskyActorDefs.ProfileView[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this)
@@ -34,10 +34,10 @@ export class SearchUIModel {
       this.isPostsLoading = false
     })
 
-    let profiles: Profile.View[] = []
+    let profiles: AppBskyActorDefs.ProfileView[] = []
     if (profilesSearch?.length) {
       do {
-        const res = await this.rootStore.api.app.bsky.actor.getProfiles({
+        const res = await this.rootStore.agent.getProfiles({
           actors: profilesSearch.splice(0, 25).map(p => p.did),
         })
         profiles = profiles.concat(res.data.profiles)
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index fec1e2899..7f57d5b54 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,3 +1,4 @@
+import {AppBskyEmbedRecord} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {makeAutoObservable} from 'mobx'
 import {ProfileViewModel} from '../profile-view'
@@ -111,6 +112,7 @@ export interface ComposerOptsQuote {
     displayName?: string
     avatar?: string
   }
+  embeds?: AppBskyEmbedRecord.ViewRecord['embeds']
 }
 export interface ComposerOpts {
   replyTo?: ComposerOptsPostRef
diff --git a/src/state/models/user-autocomplete-view.ts b/src/state/models/user-autocomplete-view.ts
index 8e4211c27..ad89bb08b 100644
--- a/src/state/models/user-autocomplete-view.ts
+++ b/src/state/models/user-autocomplete-view.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import {AppBskyActorRef} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import AwaitLock from 'await-lock'
 import {RootStoreModel} from './root-store'
 
@@ -11,8 +11,8 @@ export class UserAutocompleteViewModel {
   lock = new AwaitLock()
 
   // data
-  follows: AppBskyActorRef.WithInfo[] = []
-  searchRes: AppBskyActorRef.WithInfo[] = []
+  follows: AppBskyActorDefs.ProfileViewBasic[] = []
+  searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
   knownHandles: Set<string> = new Set()
 
   constructor(public rootStore: RootStoreModel) {
@@ -76,9 +76,9 @@ export class UserAutocompleteViewModel {
   // internal
   // =
 
-  private async _getFollows() {
-    const res = await this.rootStore.api.app.bsky.graph.getFollows({
-      user: this.rootStore.me.did || '',
+  async _getFollows() {
+    const res = await this.rootStore.agent.getFollows({
+      actor: this.rootStore.me.did || '',
     })
     runInAction(() => {
       this.follows = res.data.follows
@@ -88,13 +88,13 @@ export class UserAutocompleteViewModel {
     })
   }
 
-  private async _search() {
-    const res = await this.rootStore.api.app.bsky.actor.searchTypeahead({
+  async _search() {
+    const res = await this.rootStore.agent.searchActorsTypeahead({
       term: this.prefix,
       limit: 8,
     })
     runInAction(() => {
-      this.searchRes = res.data.users
+      this.searchRes = res.data.actors
       for (const u of this.searchRes) {
         this.knownHandles.add(u.handle)
       }
diff --git a/src/state/models/user-followers-view.ts b/src/state/models/user-followers-view.ts
index 7400262a4..055032eb7 100644
--- a/src/state/models/user-followers-view.ts
+++ b/src/state/models/user-followers-view.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable} from 'mobx'
 import {
   AppBskyGraphGetFollowers as GetFollowers,
-  AppBskyActorRef as ActorRef,
+  AppBskyActorDefs as ActorDefs,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {cleanError} from 'lib/strings/errors'
@@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
 
 const PAGE_SIZE = 30
 
-export type FollowerItem = ActorRef.WithInfo
+export type FollowerItem = ActorDefs.ProfileViewBasic
 
 export class UserFollowersViewModel {
   // state
@@ -22,10 +22,9 @@ export class UserFollowersViewModel {
   loadMoreCursor?: string
 
   // data
-  subject: ActorRef.WithInfo = {
+  subject: ActorDefs.ProfileViewBasic = {
     did: '',
     handle: '',
-    declaration: {cid: '', actorType: ''},
   }
   followers: FollowerItem[] = []
 
@@ -71,9 +70,9 @@ export class UserFollowersViewModel {
     try {
       const params = Object.assign({}, this.params, {
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.graph.getFollowers(params)
+      const res = await this.rootStore.agent.getFollowers(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -88,13 +87,13 @@ export class UserFollowersViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -107,12 +106,12 @@ export class UserFollowersViewModel {
   // helper functions
   // =
 
-  private _replaceAll(res: GetFollowers.Response) {
+  _replaceAll(res: GetFollowers.Response) {
     this.followers = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetFollowers.Response) {
+  _appendAll(res: GetFollowers.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.followers = this.followers.concat(res.data.followers)
diff --git a/src/state/models/user-follows-view.ts b/src/state/models/user-follows-view.ts
index 7d28d7ebd..6d9d84592 100644
--- a/src/state/models/user-follows-view.ts
+++ b/src/state/models/user-follows-view.ts
@@ -1,7 +1,7 @@
 import {makeAutoObservable} from 'mobx'
 import {
   AppBskyGraphGetFollows as GetFollows,
-  AppBskyActorRef as ActorRef,
+  AppBskyActorDefs as ActorDefs,
 } from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {cleanError} from 'lib/strings/errors'
@@ -9,7 +9,7 @@ import {bundleAsync} from 'lib/async/bundle'
 
 const PAGE_SIZE = 30
 
-export type FollowItem = ActorRef.WithInfo
+export type FollowItem = ActorDefs.ProfileViewBasic
 
 export class UserFollowsViewModel {
   // state
@@ -22,10 +22,9 @@ export class UserFollowsViewModel {
   loadMoreCursor?: string
 
   // data
-  subject: ActorRef.WithInfo = {
+  subject: ActorDefs.ProfileViewBasic = {
     did: '',
     handle: '',
-    declaration: {cid: '', actorType: ''},
   }
   follows: FollowItem[] = []
 
@@ -71,9 +70,9 @@ export class UserFollowsViewModel {
     try {
       const params = Object.assign({}, this.params, {
         limit: PAGE_SIZE,
-        before: replace ? undefined : this.loadMoreCursor,
+        cursor: replace ? undefined : this.loadMoreCursor,
       })
-      const res = await this.rootStore.api.app.bsky.graph.getFollows(params)
+      const res = await this.rootStore.agent.getFollows(params)
       if (replace) {
         this._replaceAll(res)
       } else {
@@ -88,13 +87,13 @@ export class UserFollowsViewModel {
   // state transitions
   // =
 
-  private _xLoading(isRefreshing = false) {
+  _xLoading(isRefreshing = false) {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
   }
 
-  private _xIdle(err?: any) {
+  _xIdle(err?: any) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
@@ -107,12 +106,12 @@ export class UserFollowsViewModel {
   // helper functions
   // =
 
-  private _replaceAll(res: GetFollows.Response) {
+  _replaceAll(res: GetFollows.Response) {
     this.follows = []
     this._appendAll(res)
   }
 
-  private _appendAll(res: GetFollows.Response) {
+  _appendAll(res: GetFollows.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.follows = this.follows.concat(res.data.follows)