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
This commit is contained in:
Paul Frazee 2023-10-05 16:44:05 -07:00 committed by GitHub
parent 19f8389fc7
commit bd7db8af26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 197 additions and 142 deletions

View file

@ -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
}
/**
* 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
if (this.byDid[did]) {
this.byDid[did].followRecordUri = undefined
}
}
/**
* Use this to incrementally update the cache as views provide information
*/
hydrateProfiles(profiles: Profile[]) {
hydrate(did: string, profile: Profile) {
this.byDid[did] = {
did,
followRecordUri: profile.viewer?.following,
handle: profile.handle,
displayName: profile.displayName,
avatar: profile.avatar,
}
}
hydrateMany(profiles: Profile[]) {
for (const profile of profiles) {
if (profile.viewer) {
this.hydrate(profile.did, profile.viewer.following)
}
this.hydrate(profile.did, profile)
}
}
}