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,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]

View file

@ -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),

View file

@ -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
}
}
})
}
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)
for (const item of searchRes) {
if (!items.find(item2 => item2.handle === item.handle)) {
items.push({
did: item.did,
handle: item.handle,
displayName: item.displayName,
avatar: item.avatar,
})
}
}
})
this._suggestions = items
} else {
this._suggestions = this.follows
}
}
}
function prefixMatch(prefix: string, info: ProfileViewBasic): boolean {
if (info.handle.includes(prefix)) {
return true
}
if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
return true
}
return false
}