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:
parent
19f8389fc7
commit
bd7db8af26
20 changed files with 197 additions and 142 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue