about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-10-05 16:44:05 -0700
committerGitHub <noreply@github.com>2023-10-05 16:44:05 -0700
commitbd7db8af26bfbf94a80972671ca714a143bee28e (patch)
treecf022bddc4f6b164bea51aeb3c57479d72b73355 /src
parent19f8389fc777c7ff41466748f1238f4e0a4b0619 (diff)
downloadvoidsky-bd7db8af26bfbf94a80972671ca714a143bee28e.tar.zst
Improve typeahead search with inclusion of followed users (temporary solution) (#1612)
* Update follows cache to maintain some user info

* Prioritize follows in composer autocomplete

* Clean up logic and add new autocomplete to search

* Update follow hook
Diffstat (limited to 'src')
-rw-r--r--src/lib/hooks/useFollowProfile.ts (renamed from src/lib/hooks/useFollowDid.ts)24
-rw-r--r--src/state/models/cache/my-follows.ts99
-rw-r--r--src/state/models/content/profile.ts4
-rw-r--r--src/state/models/discovery/foafs.ts36
-rw-r--r--src/state/models/discovery/suggested-actors.ts4
-rw-r--r--src/state/models/discovery/user-autocomplete.ts115
-rw-r--r--src/state/models/feeds/posts.ts2
-rw-r--r--src/state/models/invited-users.ts2
-rw-r--r--src/state/models/lists/likes.ts2
-rw-r--r--src/state/models/lists/reposted-by.ts2
-rw-r--r--src/state/models/lists/user-followers.ts2
-rw-r--r--src/state/models/lists/user-follows.ts2
-rw-r--r--src/state/models/ui/search.ts2
-rw-r--r--src/view/com/auth/onboarding/RecommendedFollowsItem.tsx2
-rw-r--r--src/view/com/notifications/InvitedUsers.tsx2
-rw-r--r--src/view/com/profile/FollowButton.tsx9
-rw-r--r--src/view/com/profile/ProfileCard.tsx2
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx6
-rw-r--r--src/view/screens/SearchMobile.tsx12
-rw-r--r--src/view/shell/desktop/Search.tsx4
20 files changed, 194 insertions, 139 deletions
diff --git a/src/lib/hooks/useFollowDid.ts b/src/lib/hooks/useFollowProfile.ts
index 223adb047..6220daba8 100644
--- a/src/lib/hooks/useFollowDid.ts
+++ b/src/lib/hooks/useFollowProfile.ts
@@ -1,11 +1,11 @@
 import React from 'react'
-
+import {AppBskyActorDefs} from '@atproto/api'
 import {useStores} from 'state/index'
 import {FollowState} from 'state/models/cache/my-follows'
 
-export function useFollowDid({did}: {did: string}) {
+export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
   const store = useStores()
-  const state = store.me.follows.getFollowState(did)
+  const state = store.me.follows.getFollowState(profile.did)
 
   return {
     state,
@@ -13,8 +13,10 @@ export function useFollowDid({did}: {did: string}) {
     toggle: React.useCallback(async () => {
       if (state === FollowState.Following) {
         try {
-          await store.agent.deleteFollow(store.me.follows.getFollowUri(did))
-          store.me.follows.removeFollow(did)
+          await store.agent.deleteFollow(
+            store.me.follows.getFollowUri(profile.did),
+          )
+          store.me.follows.removeFollow(profile.did)
           return {
             state: FollowState.NotFollowing,
             following: false,
@@ -25,8 +27,14 @@ export function useFollowDid({did}: {did: string}) {
         }
       } else if (state === FollowState.NotFollowing) {
         try {
-          const res = await store.agent.follow(did)
-          store.me.follows.addFollow(did, res.uri)
+          const res = await store.agent.follow(profile.did)
+          store.me.follows.addFollow(profile.did, {
+            followRecordUri: res.uri,
+            did: profile.did,
+            handle: profile.handle,
+            displayName: profile.displayName,
+            avatar: profile.avatar,
+          })
           return {
             state: FollowState.Following,
             following: true,
@@ -41,6 +49,6 @@ export function useFollowDid({did}: {did: string}) {
         state: FollowState.Unknown,
         following: false,
       }
-    }, [store, did, state]),
+    }, [store, profile, state]),
   }
 }
diff --git a/src/state/models/cache/my-follows.ts b/src/state/models/cache/my-follows.ts
index 10f88c4a9..14dc9895d 100644
--- a/src/state/models/cache/my-follows.ts
+++ b/src/state/models/cache/my-follows.ts
@@ -1,7 +1,14 @@
 import {makeAutoObservable} from 'mobx'
-import {AppBskyActorDefs} from '@atproto/api'
+import {
+  AppBskyActorDefs,
+  AppBskyGraphGetFollows as GetFollows,
+  moderateProfile,
+} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 
+const MAX_SYNC_PAGES = 10
+const SYNC_TTL = 60e3 * 10 // 10 minutes
+
 type Profile = AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView
 
 export enum FollowState {
@@ -10,6 +17,14 @@ export enum FollowState {
   Unknown,
 }
 
+export interface FollowInfo {
+  did: string
+  followRecordUri: string | undefined
+  handle: string
+  displayName: string | undefined
+  avatar: string | undefined
+}
+
 /**
  * This model is used to maintain a synced local cache of the user's
  * follows. It should be periodically refreshed and updated any time
@@ -17,9 +32,8 @@ export enum FollowState {
  */
 export class MyFollowsCache {
   // data
-  followDidToRecordMap: Record<string, string | boolean> = {}
+  byDid: Record<string, FollowInfo> = {}
   lastSync = 0
-  myDid?: string
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(
@@ -35,16 +49,45 @@ export class MyFollowsCache {
   // =
 
   clear() {
-    this.followDidToRecordMap = {}
-    this.lastSync = 0
-    this.myDid = undefined
+    this.byDid = {}
+  }
+
+  /**
+   * Syncs a subset of the user's follows
+   * for performance reasons, caps out at 1000 follows
+   */
+  async syncIfNeeded() {
+    if (this.lastSync > Date.now() - SYNC_TTL) {
+      return
+    }
+
+    let cursor
+    for (let i = 0; i < MAX_SYNC_PAGES; i++) {
+      const res: GetFollows.Response = await this.rootStore.agent.getFollows({
+        actor: this.rootStore.me.did,
+        cursor,
+        limit: 100,
+      })
+      res.data.follows = res.data.follows.filter(
+        profile =>
+          !moderateProfile(profile, this.rootStore.preferences.moderationOpts)
+            .account.filter,
+      )
+      this.hydrateMany(res.data.follows)
+      if (!res.data.cursor) {
+        break
+      }
+      cursor = res.data.cursor
+    }
+
+    this.lastSync = Date.now()
   }
 
   getFollowState(did: string): FollowState {
-    if (typeof this.followDidToRecordMap[did] === 'undefined') {
+    if (typeof this.byDid[did] === 'undefined') {
       return FollowState.Unknown
     }
-    if (typeof this.followDidToRecordMap[did] === 'string') {
+    if (typeof this.byDid[did].followRecordUri === 'string') {
       return FollowState.Following
     }
     return FollowState.NotFollowing
@@ -53,49 +96,41 @@ export class MyFollowsCache {
   async fetchFollowState(did: string): Promise<FollowState> {
     // TODO: can we get a more efficient method for this? getProfile fetches more data than we need -prf
     const res = await this.rootStore.agent.getProfile({actor: did})
-    if (res.data.viewer?.following) {
-      this.addFollow(did, res.data.viewer.following)
-    } else {
-      this.removeFollow(did)
-    }
+    this.hydrate(did, res.data)
     return this.getFollowState(did)
   }
 
   getFollowUri(did: string): string {
-    const v = this.followDidToRecordMap[did]
+    const v = this.byDid[did]
     if (typeof v === 'string') {
       return v
     }
     throw new Error('Not a followed user')
   }
 
-  addFollow(did: string, recordUri: string) {
-    this.followDidToRecordMap[did] = recordUri
+  addFollow(did: string, info: FollowInfo) {
+    this.byDid[did] = info
   }
 
   removeFollow(did: string) {
-    this.followDidToRecordMap[did] = false
+    if (this.byDid[did]) {
+      this.byDid[did].followRecordUri = undefined
+    }
   }
 
-  /**
-   * Use this to incrementally update the cache as views provide information
-   */
-  hydrate(did: string, recordUri: string | undefined) {
-    if (recordUri) {
-      this.followDidToRecordMap[did] = recordUri
-    } else {
-      this.followDidToRecordMap[did] = false
+  hydrate(did: string, profile: Profile) {
+    this.byDid[did] = {
+      did,
+      followRecordUri: profile.viewer?.following,
+      handle: profile.handle,
+      displayName: profile.displayName,
+      avatar: profile.avatar,
     }
   }
 
-  /**
-   * Use this to incrementally update the cache as views provide information
-   */
-  hydrateProfiles(profiles: Profile[]) {
+  hydrateMany(profiles: Profile[]) {
     for (const profile of profiles) {
-      if (profile.viewer) {
-        this.hydrate(profile.did, profile.viewer.following)
-      }
+      this.hydrate(profile.did, profile)
     }
   }
 }
diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts
index 26fa6008c..906f84c28 100644
--- a/src/state/models/content/profile.ts
+++ b/src/state/models/content/profile.ts
@@ -137,7 +137,7 @@ export class ProfileModel {
       runInAction(() => {
         this.followersCount++
         this.viewer.following = res.uri
-        this.rootStore.me.follows.addFollow(this.did, res.uri)
+        this.rootStore.me.follows.hydrate(this.did, this)
       })
       track('Profile:Follow', {
         username: this.handle,
@@ -290,8 +290,8 @@ export class ProfileModel {
     this.labels = res.data.labels
     if (res.data.viewer) {
       Object.assign(this.viewer, res.data.viewer)
-      this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
     }
+    this.rootStore.me.follows.hydrate(this.did, res.data)
   }
 
   async _createRichText() {
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts
index 580145f65..4a647dcfe 100644
--- a/src/state/models/discovery/foafs.ts
+++ b/src/state/models/discovery/foafs.ts
@@ -1,8 +1,4 @@
-import {
-  AppBskyActorDefs,
-  AppBskyGraphGetFollows as GetFollows,
-  moderateProfile,
-} from '@atproto/api'
+import {AppBskyActorDefs} from '@atproto/api'
 import {makeAutoObservable, runInAction} from 'mobx'
 import sampleSize from 'lodash.samplesize'
 import {bundleAsync} from 'lib/async/bundle'
@@ -43,35 +39,13 @@ export class FoafsModel {
     try {
       this.isLoading = true
 
-      // fetch & hydrate up to 1000 follows
-      {
-        let cursor
-        for (let i = 0; i < 10; i++) {
-          const res: GetFollows.Response =
-            await this.rootStore.agent.getFollows({
-              actor: this.rootStore.me.did,
-              cursor,
-              limit: 100,
-            })
-          res.data.follows = res.data.follows.filter(
-            profile =>
-              !moderateProfile(
-                profile,
-                this.rootStore.preferences.moderationOpts,
-              ).account.filter,
-          )
-          this.rootStore.me.follows.hydrateProfiles(res.data.follows)
-          if (!res.data.cursor) {
-            break
-          }
-          cursor = res.data.cursor
-        }
-      }
+      // fetch some of the user's follows
+      await this.rootStore.me.follows.syncIfNeeded()
 
       // grab 10 of the users followed by the user
       runInAction(() => {
         this.sources = sampleSize(
-          Object.keys(this.rootStore.me.follows.followDidToRecordMap),
+          Object.keys(this.rootStore.me.follows.byDid),
           10,
         )
       })
@@ -100,7 +74,7 @@ export class FoafsModel {
       for (let i = 0; i < results.length; i++) {
         const res = results[i]
         if (res.status === 'fulfilled') {
-          this.rootStore.me.follows.hydrateProfiles(res.value.data.follows)
+          this.rootStore.me.follows.hydrateMany(res.value.data.follows)
         }
         const profile = profiles.data.profiles[i]
         const source = this.sources[i]
diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts
index afa5e74e3..d270267ee 100644
--- a/src/state/models/discovery/suggested-actors.ts
+++ b/src/state/models/discovery/suggested-actors.ts
@@ -76,7 +76,7 @@ export class SuggestedActorsModel {
           !moderateProfile(actor, this.rootStore.preferences.moderationOpts)
             .account.filter,
       )
-      this.rootStore.me.follows.hydrateProfiles(actors)
+      this.rootStore.me.follows.hydrateMany(actors)
 
       runInAction(() => {
         if (replace) {
@@ -118,7 +118,7 @@ export class SuggestedActorsModel {
         actor: actor,
       })
     const {suggestions: moreSuggestions} = res.data
-    this.rootStore.me.follows.hydrateProfiles(moreSuggestions)
+    this.rootStore.me.follows.hydrateMany(moreSuggestions)
     // dedupe
     const toInsert = moreSuggestions.filter(
       s => !this.suggestions.find(s2 => s2.did === s.did),
diff --git a/src/state/models/discovery/user-autocomplete.ts b/src/state/models/discovery/user-autocomplete.ts
index 461073e45..25ce859d2 100644
--- a/src/state/models/discovery/user-autocomplete.ts
+++ b/src/state/models/discovery/user-autocomplete.ts
@@ -4,6 +4,8 @@ import AwaitLock from 'await-lock'
 import {RootStoreModel} from '../root-store'
 import {isInvalidHandle} from 'lib/strings/handles'
 
+type ProfileViewBasic = AppBskyActorDefs.ProfileViewBasic
+
 export class UserAutocompleteModel {
   // state
   isLoading = false
@@ -12,9 +14,8 @@ export class UserAutocompleteModel {
   lock = new AwaitLock()
 
   // data
-  follows: AppBskyActorDefs.ProfileViewBasic[] = []
-  searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
   knownHandles: Set<string> = new Set()
+  _suggestions: ProfileViewBasic[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(
@@ -27,29 +28,35 @@ export class UserAutocompleteModel {
     )
   }
 
-  get suggestions() {
+  get follows(): ProfileViewBasic[] {
+    return Object.values(this.rootStore.me.follows.byDid).map(item => ({
+      did: item.did,
+      handle: item.handle,
+      displayName: item.displayName,
+      avatar: item.avatar,
+    }))
+  }
+
+  get suggestions(): ProfileViewBasic[] {
     if (!this.isActive) {
       return []
     }
-    if (this.prefix) {
-      return this.searchRes.map(user => ({
-        handle: user.handle,
-        displayName: user.displayName,
-        avatar: user.avatar,
-      }))
-    }
-    return this.follows.map(follow => ({
-      handle: follow.handle,
-      displayName: follow.displayName,
-      avatar: follow.avatar,
-    }))
+    return this._suggestions
   }
 
   // public api
   // =
 
   async setup() {
-    await this._getFollows()
+    await this.rootStore.me.follows.syncIfNeeded()
+    runInAction(() => {
+      for (const did in this.rootStore.me.follows.byDid) {
+        const info = this.rootStore.me.follows.byDid[did]
+        if (!isInvalidHandle(info.handle)) {
+          this.knownHandles.add(info.handle)
+        }
+      }
+    })
   }
 
   setActive(v: boolean) {
@@ -57,7 +64,7 @@ export class UserAutocompleteModel {
   }
 
   async setPrefix(prefix: string) {
-    const origPrefix = prefix.trim()
+    const origPrefix = prefix.trim().toLocaleLowerCase()
     this.prefix = origPrefix
     await this.lock.acquireAsync()
     try {
@@ -65,9 +72,27 @@ export class UserAutocompleteModel {
         if (this.prefix !== origPrefix) {
           return // another prefix was set before we got our chance
         }
-        await this._search()
+
+        // reset to follow results
+        this._computeSuggestions([])
+
+        // ask backend
+        const res = await this.rootStore.agent.searchActorsTypeahead({
+          term: this.prefix,
+          limit: 8,
+        })
+        this._computeSuggestions(res.data.actors)
+
+        // update known handles
+        runInAction(() => {
+          for (const u of res.data.actors) {
+            this.knownHandles.add(u.handle)
+          }
+        })
       } else {
-        this.searchRes = []
+        runInAction(() => {
+          this._computeSuggestions([])
+        })
       }
     } finally {
       this.lock.release()
@@ -77,28 +102,40 @@ export class UserAutocompleteModel {
   // internal
   // =
 
-  async _getFollows() {
-    const res = await this.rootStore.agent.getFollows({
-      actor: this.rootStore.me.did || '',
-    })
-    runInAction(() => {
-      this.follows = res.data.follows.filter(f => !isInvalidHandle(f.handle))
-      for (const f of this.follows) {
-        this.knownHandles.add(f.handle)
+  _computeSuggestions(searchRes: AppBskyActorDefs.ProfileViewBasic[] = []) {
+    if (this.prefix) {
+      const items: ProfileViewBasic[] = []
+      for (const item of this.follows) {
+        if (prefixMatch(this.prefix, item)) {
+          items.push(item)
+        }
+        if (items.length >= 8) {
+          break
+        }
       }
-    })
+      for (const item of searchRes) {
+        if (!items.find(item2 => item2.handle === item.handle)) {
+          items.push({
+            did: item.did,
+            handle: item.handle,
+            displayName: item.displayName,
+            avatar: item.avatar,
+          })
+        }
+      }
+      this._suggestions = items
+    } else {
+      this._suggestions = this.follows
+    }
   }
+}
 
-  async _search() {
-    const res = await this.rootStore.agent.searchActorsTypeahead({
-      term: this.prefix,
-      limit: 8,
-    })
-    runInAction(() => {
-      this.searchRes = res.data.actors
-      for (const u of this.searchRes) {
-        this.knownHandles.add(u.handle)
-      }
-    })
+function prefixMatch(prefix: string, info: ProfileViewBasic): boolean {
+  if (info.handle.includes(prefix)) {
+    return true
+  }
+  if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
+    return true
   }
+  return false
 }
diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts
index 2a7170325..2462689b1 100644
--- a/src/state/models/feeds/posts.ts
+++ b/src/state/models/feeds/posts.ts
@@ -316,7 +316,7 @@ export class PostsFeedModel {
       this.emptyFetches = 0
     }
 
-    this.rootStore.me.follows.hydrateProfiles(
+    this.rootStore.me.follows.hydrateMany(
       res.feed.map(item => item.post.author),
     )
     for (const item of res.feed) {
diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts
index a28e0309a..cd3667062 100644
--- a/src/state/models/invited-users.ts
+++ b/src/state/models/invited-users.ts
@@ -61,7 +61,7 @@ export class InvitedUsers {
             profile => !profile.viewer?.following,
           )
         })
-        this.rootStore.me.follows.hydrateProfiles(this.profiles)
+        this.rootStore.me.follows.hydrateMany(this.profiles)
       } catch (e) {
         this.rootStore.log.error(
           'Failed to fetch profiles for invited users',
diff --git a/src/state/models/lists/likes.ts b/src/state/models/lists/likes.ts
index 39882d73a..dd3cf18a3 100644
--- a/src/state/models/lists/likes.ts
+++ b/src/state/models/lists/likes.ts
@@ -126,7 +126,7 @@ export class LikesModel {
   _appendAll(res: GetLikes.Response) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
-    this.rootStore.me.follows.hydrateProfiles(
+    this.rootStore.me.follows.hydrateMany(
       res.data.likes.map(like => like.actor),
     )
     this.likes = this.likes.concat(res.data.likes)
diff --git a/src/state/models/lists/reposted-by.ts b/src/state/models/lists/reposted-by.ts
index a70375bdc..5d4fc107d 100644
--- a/src/state/models/lists/reposted-by.ts
+++ b/src/state/models/lists/reposted-by.ts
@@ -130,6 +130,6 @@ export class RepostedByModel {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
-    this.rootStore.me.follows.hydrateProfiles(res.data.repostedBy)
+    this.rootStore.me.follows.hydrateMany(res.data.repostedBy)
   }
 }
diff --git a/src/state/models/lists/user-followers.ts b/src/state/models/lists/user-followers.ts
index 2962d6242..1f817c33c 100644
--- a/src/state/models/lists/user-followers.ts
+++ b/src/state/models/lists/user-followers.ts
@@ -115,6 +115,6 @@ export class UserFollowersModel {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.followers = this.followers.concat(res.data.followers)
-    this.rootStore.me.follows.hydrateProfiles(res.data.followers)
+    this.rootStore.me.follows.hydrateMany(res.data.followers)
   }
 }
diff --git a/src/state/models/lists/user-follows.ts b/src/state/models/lists/user-follows.ts
index 56432a796..c9630eba8 100644
--- a/src/state/models/lists/user-follows.ts
+++ b/src/state/models/lists/user-follows.ts
@@ -115,6 +115,6 @@ export class UserFollowsModel {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     this.follows = this.follows.concat(res.data.follows)
-    this.rootStore.me.follows.hydrateProfiles(res.data.follows)
+    this.rootStore.me.follows.hydrateMany(res.data.follows)
   }
 }
diff --git a/src/state/models/ui/search.ts b/src/state/models/ui/search.ts
index 4ab9db513..2b2036751 100644
--- a/src/state/models/ui/search.ts
+++ b/src/state/models/ui/search.ts
@@ -59,7 +59,7 @@ export class SearchUIModel {
       } while (profilesSearch.length)
     }
 
-    this.rootStore.me.follows.hydrateProfiles(profiles)
+    this.rootStore.me.follows.hydrateMany(profiles)
 
     runInAction(() => {
       this.profiles = profiles
diff --git a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
index 51e3bc382..2b26918d0 100644
--- a/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
+++ b/src/view/com/auth/onboarding/RecommendedFollowsItem.tsx
@@ -89,7 +89,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
         </View>
 
         <FollowButton
-          did={profile.did}
+          profile={profile}
           labelStyle={styles.followButton}
           onToggleFollow={async isFollow => {
             if (isFollow) {
diff --git a/src/view/com/notifications/InvitedUsers.tsx b/src/view/com/notifications/InvitedUsers.tsx
index 89a0da47f..aaf358b87 100644
--- a/src/view/com/notifications/InvitedUsers.tsx
+++ b/src/view/com/notifications/InvitedUsers.tsx
@@ -75,7 +75,7 @@ function InvitedUser({
           <FollowButton
             unfollowedType="primary"
             followedType="primary-light"
-            did={profile.did}
+            profile={profile}
           />
           <Button
             testID="dismissBtn"
diff --git a/src/view/com/profile/FollowButton.tsx b/src/view/com/profile/FollowButton.tsx
index 217d326e8..adb496f6d 100644
--- a/src/view/com/profile/FollowButton.tsx
+++ b/src/view/com/profile/FollowButton.tsx
@@ -1,25 +1,26 @@
 import React from 'react'
 import {StyleProp, TextStyle, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
+import {AppBskyActorDefs} from '@atproto/api'
 import {Button, ButtonType} from '../util/forms/Button'
 import * as Toast from '../util/Toast'
 import {FollowState} from 'state/models/cache/my-follows'
-import {useFollowDid} from 'lib/hooks/useFollowDid'
+import {useFollowProfile} from 'lib/hooks/useFollowProfile'
 
 export const FollowButton = observer(function FollowButtonImpl({
   unfollowedType = 'inverted',
   followedType = 'default',
-  did,
+  profile,
   onToggleFollow,
   labelStyle,
 }: {
   unfollowedType?: ButtonType
   followedType?: ButtonType
-  did: string
+  profile: AppBskyActorDefs.ProfileViewBasic
   onToggleFollow?: (v: boolean) => void
   labelStyle?: StyleProp<TextStyle>
 }) {
-  const {state, following, toggle} = useFollowDid({did})
+  const {state, following, toggle} = useFollowProfile(profile)
 
   const onPress = React.useCallback(async () => {
     try {
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index e0c8ad21a..d1aed8934 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -200,7 +200,7 @@ export const ProfileCardWithFollowBtn = observer(
         noBorder={noBorder}
         followers={followers}
         renderButton={
-          isMe ? undefined : () => <FollowButton did={profile.did} />
+          isMe ? undefined : () => <FollowButton profile={profile} />
         }
       />
     )
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index b9d66a6fe..41e4022d5 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -19,7 +19,7 @@ import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {Text} from 'view/com/util/text/Text'
 import {UserAvatar} from 'view/com/util/UserAvatar'
-import {useFollowDid} from 'lib/hooks/useFollowDid'
+import {useFollowProfile} from 'lib/hooks/useFollowProfile'
 import {Button} from 'view/com/util/forms/Button'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
@@ -83,7 +83,7 @@ export function ProfileHeaderSuggestedFollows({
           return []
         }
 
-        store.me.follows.hydrateProfiles(suggestions)
+        store.me.follows.hydrateMany(suggestions)
 
         return suggestions
       } catch (e) {
@@ -218,7 +218,7 @@ const SuggestedFollow = observer(function SuggestedFollowImpl({
   const {track} = useAnalytics()
   const pal = usePalette('default')
   const store = useStores()
-  const {following, toggle} = useFollowDid({did: profile.did})
+  const {following, toggle} = useFollowProfile(profile)
   const moderation = moderateProfile(profile, store.preferences.moderationOpts)
 
   const onPress = React.useCallback(async () => {
diff --git a/src/view/screens/SearchMobile.tsx b/src/view/screens/SearchMobile.tsx
index b545a643d..b80c1667f 100644
--- a/src/view/screens/SearchMobile.tsx
+++ b/src/view/screens/SearchMobile.tsx
@@ -148,18 +148,18 @@ export const SearchScreen = withAuthRequired(
               style={pal.view}
               onScroll={onMainScroll}
               scrollEventThrottle={100}>
-              {query && autocompleteView.searchRes.length ? (
+              {query && autocompleteView.suggestions.length ? (
                 <>
-                  {autocompleteView.searchRes.map((profile, index) => (
+                  {autocompleteView.suggestions.map((suggestion, index) => (
                     <ProfileCard
-                      key={profile.did}
-                      testID={`searchAutoCompleteResult-${profile.handle}`}
-                      profile={profile}
+                      key={suggestion.did}
+                      testID={`searchAutoCompleteResult-${suggestion.handle}`}
+                      profile={suggestion}
                       noBorder={index === 0}
                     />
                   ))}
                 </>
-              ) : query && !autocompleteView.searchRes.length ? (
+              ) : query && !autocompleteView.suggestions.length ? (
                 <View>
                   <Text style={[pal.textLight, styles.searchPrompt]}>
                     No results found for {autocompleteView.prefix}
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index dfd4f50bf..53a58c39d 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -90,9 +90,9 @@ export const DesktopSearch = observer(function DesktopSearch() {
 
       {query !== '' && (
         <View style={[pal.view, pal.borderDark, styles.resultsContainer]}>
-          {autocompleteView.searchRes.length ? (
+          {autocompleteView.suggestions.length ? (
             <>
-              {autocompleteView.searchRes.map((item, i) => (
+              {autocompleteView.suggestions.map((item, i) => (
                 <ProfileCard key={item.did} profile={item} noBorder={i === 0} />
               ))}
             </>