diff options
Diffstat (limited to 'src/state/models/discovery')
-rw-r--r-- | src/state/models/discovery/foafs.ts | 132 | ||||
-rw-r--r-- | src/state/models/discovery/suggested-actors.ts | 151 | ||||
-rw-r--r-- | src/state/models/discovery/user-autocomplete.ts | 143 |
3 files changed, 0 insertions, 426 deletions
diff --git a/src/state/models/discovery/foafs.ts b/src/state/models/discovery/foafs.ts deleted file mode 100644 index 4a647dcfe..000000000 --- a/src/state/models/discovery/foafs.ts +++ /dev/null @@ -1,132 +0,0 @@ -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 = AppBskyActorDefs.ProfileViewBasic & { - followers: AppBskyActorDefs.ProfileView[] -} - -export type ProfileViewFollows = AppBskyActorDefs.ProfileView & { - follows: AppBskyActorDefs.ProfileViewBasic[] -} - -export class FoafsModel { - isLoading = false - hasData = false - sources: string[] = [] - foafs: Map<string, ProfileViewFollows> = new Map() // FOAF stands for Friend of a Friend - popular: RefWithInfoAndFollowers[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this) - } - - get hasContent() { - if (this.popular.length > 0) { - return true - } - for (const foaf of this.foafs.values()) { - if (foaf.follows.length) { - return true - } - } - return false - } - - fetch = bundleAsync(async () => { - try { - this.isLoading = true - - // 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.byDid), - 10, - ) - }) - if (this.sources.length === 0) { - return - } - runInAction(() => { - this.foafs.clear() - this.popular.length = 0 - }) - - // fetch their profiles - const profiles = await this.rootStore.agent.getProfiles({ - actors: this.sources, - }) - - // fetch their follows - const results = await Promise.allSettled( - this.sources.map(source => - this.rootStore.agent.getFollows({actor: source}), - ), - ) - - // store the follows and construct a "most followed" set - const popular: RefWithInfoAndFollowers[] = [] - for (let i = 0; i < results.length; i++) { - const res = results[i] - if (res.status === 'fulfilled') { - this.rootStore.me.follows.hydrateMany(res.value.data.follows) - } - const profile = profiles.data.profiles[i] - const source = this.sources[i] - if (res.status === 'fulfilled' && profile) { - // filter out inappropriate suggestions - res.value.data.follows = res.value.data.follows.filter(follow => { - const viewer = follow.viewer - if (viewer) { - if ( - viewer.following || - viewer.muted || - viewer.mutedByList || - viewer.blockedBy || - viewer.blocking - ) { - return false - } - } - if (follow.did === this.rootStore.me.did) { - return false - } - return true - }) - - runInAction(() => { - this.foafs.set(source, { - ...profile, - follows: res.value.data.follows, - }) - }) - for (const follow of res.value.data.follows) { - let item = popular.find(p => p.did === follow.did) - if (!item) { - item = {...follow, followers: []} - popular.push(item) - } - item.followers.push(profile) - } - } - } - - popular.sort((a, b) => b.followers.length - a.followers.length) - runInAction(() => { - this.popular = popular.filter(p => p.followers.length > 1).slice(0, 20) - }) - this.hasData = true - } catch (e) { - console.error('Failed to fetch FOAFs', e) - } finally { - runInAction(() => { - this.isLoading = false - }) - } - }) -} diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts deleted file mode 100644 index 450786c2f..000000000 --- a/src/state/models/discovery/suggested-actors.ts +++ /dev/null @@ -1,151 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {RootStoreModel} from '../root-store' -import {cleanError} from 'lib/strings/errors' -import {bundleAsync} from 'lib/async/bundle' -import {logger} from '#/logger' - -const PAGE_SIZE = 30 - -export type SuggestedActor = - | AppBskyActorDefs.ProfileViewBasic - | AppBskyActorDefs.ProfileView - -export class SuggestedActorsModel { - // state - pageSize = PAGE_SIZE - isLoading = false - isRefreshing = false - hasLoaded = false - loadMoreCursor: string | undefined = undefined - error = '' - hasMore = false - lastInsertedAtIndex = -1 - - // data - suggestions: SuggestedActor[] = [] - - constructor(public rootStore: RootStoreModel, opts?: {pageSize?: number}) { - if (opts?.pageSize) { - this.pageSize = opts.pageSize - } - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get hasContent() { - return this.suggestions.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - // public api - // = - - async refresh() { - return this.loadMore(true) - } - - loadMore = bundleAsync(async (replace: boolean = false) => { - if (replace) { - this.hasMore = true - this.loadMoreCursor = undefined - } - if (!this.hasMore) { - return - } - this._xLoading(replace) - try { - const res = await this.rootStore.agent.app.bsky.actor.getSuggestions({ - limit: 25, - cursor: this.loadMoreCursor, - }) - let {actors, cursor} = res.data - actors = actors.filter( - actor => - !moderateProfile(actor, this.rootStore.preferences.moderationOpts) - .account.filter, - ) - this.rootStore.me.follows.hydrateMany(actors) - - runInAction(() => { - if (replace) { - this.suggestions = [] - } - this.loadMoreCursor = cursor - this.hasMore = !!cursor - this.suggestions = this.suggestions.concat( - actors.filter(actor => { - const viewer = actor.viewer - if (viewer) { - if ( - viewer.following || - viewer.muted || - viewer.mutedByList || - viewer.blockedBy || - viewer.blocking - ) { - return false - } - } - if (actor.did === this.rootStore.me.did) { - return false - } - return true - }), - ) - }) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - }) - - async insertSuggestionsByActor(actor: string, indexToInsertAt: number) { - // fetch suggestions - const res = - await this.rootStore.agent.app.bsky.graph.getSuggestedFollowsByActor({ - actor: actor, - }) - const {suggestions: moreSuggestions} = res.data - this.rootStore.me.follows.hydrateMany(moreSuggestions) - // dedupe - const toInsert = moreSuggestions.filter( - s => !this.suggestions.find(s2 => s2.did === s.did), - ) - // insert - this.suggestions.splice(indexToInsertAt + 1, 0, ...toInsert) - // update index - this.lastInsertedAtIndex = indexToInsertAt - } - - // state transitions - // = - - _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - logger.error('Failed to fetch suggested actors', {error: err}) - } - } -} diff --git a/src/state/models/discovery/user-autocomplete.ts b/src/state/models/discovery/user-autocomplete.ts deleted file mode 100644 index f28869e83..000000000 --- a/src/state/models/discovery/user-autocomplete.ts +++ /dev/null @@ -1,143 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {AppBskyActorDefs} from '@atproto/api' -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 - isActive = false - prefix = '' - lock = new AwaitLock() - - // data - knownHandles: Set<string> = new Set() - _suggestions: ProfileViewBasic[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - knownHandles: false, - }, - {autoBind: true}, - ) - } - - 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 [] - } - return this._suggestions - } - - // public api - // = - - async setup() { - this.isLoading = true - 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) - } - } - this.isLoading = false - }) - } - - setActive(v: boolean) { - this.isActive = v - } - - async setPrefix(prefix: string) { - const origPrefix = prefix.trim().toLocaleLowerCase() - this.prefix = origPrefix - await this.lock.acquireAsync() - try { - if (this.prefix) { - if (this.prefix !== origPrefix) { - return // another prefix was set before we got our chance - } - - // 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 { - runInAction(() => { - this._computeSuggestions([]) - }) - } - } finally { - this.lock.release() - } - } - - // internal - // = - - _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 - } - } -} - -function prefixMatch(prefix: string, info: ProfileViewBasic): boolean { - if (info.handle.includes(prefix)) { - return true - } - if (info.displayName?.toLocaleLowerCase().includes(prefix)) { - return true - } - return false -} |