about summary refs log tree commit diff
path: root/src/state/models/discovery
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models/discovery')
-rw-r--r--src/state/models/discovery/feeds.ts148
-rw-r--r--src/state/models/discovery/foafs.ts132
-rw-r--r--src/state/models/discovery/onboarding.ts106
-rw-r--r--src/state/models/discovery/suggested-actors.ts151
-rw-r--r--src/state/models/discovery/user-autocomplete.ts143
5 files changed, 0 insertions, 680 deletions
diff --git a/src/state/models/discovery/feeds.ts b/src/state/models/discovery/feeds.ts
deleted file mode 100644
index a7c94e40d..000000000
--- a/src/state/models/discovery/feeds.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {AppBskyUnspeccedGetPopularFeedGenerators} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-import {cleanError} from 'lib/strings/errors'
-import {FeedSourceModel} from '../content/feed-source'
-import {logger} from '#/logger'
-
-const DEFAULT_LIMIT = 50
-
-export class FeedsDiscoveryModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  loadMoreCursor: string | undefined = undefined
-
-  // data
-  feeds: FeedSourceModel[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasMore() {
-    if (this.loadMoreCursor) {
-      return true
-    }
-    return false
-  }
-
-  get hasContent() {
-    return this.feeds.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  refresh = bundleAsync(async () => {
-    this._xLoading()
-    try {
-      const res =
-        await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({
-          limit: DEFAULT_LIMIT,
-        })
-      this._replaceAll(res)
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  loadMore = bundleAsync(async () => {
-    if (!this.hasMore) {
-      return
-    }
-    this._xLoading()
-    try {
-      const res =
-        await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({
-          limit: DEFAULT_LIMIT,
-          cursor: this.loadMoreCursor,
-        })
-      this._append(res)
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-    this._xIdle()
-  })
-
-  search = async (query: string) => {
-    this._xLoading(false)
-    try {
-      const results =
-        await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators({
-          limit: DEFAULT_LIMIT,
-          query: query,
-        })
-      this._replaceAll(results)
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-    this._xIdle()
-  }
-
-  clear() {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = false
-    this.error = ''
-    this.feeds = []
-  }
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = true) {
-    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 popular feeds', {error: err})
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) {
-    // 1. set feeds data to empty array
-    this.feeds = []
-    // 2. call this._append()
-    this._append(res)
-  }
-
-  _append(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) {
-    // 1. push data into feeds array
-    for (const f of res.data.feeds) {
-      const model = new FeedSourceModel(this.rootStore, f.uri)
-      model.hydrateFeedGenerator(f)
-      this.feeds.push(model)
-    }
-    // 2. set loadMoreCursor
-    this.loadMoreCursor = res.data.cursor
-  }
-}
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/onboarding.ts b/src/state/models/discovery/onboarding.ts
deleted file mode 100644
index 3638e7f0d..000000000
--- a/src/state/models/discovery/onboarding.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {RootStoreModel} from '../root-store'
-import {hasProp} from 'lib/type-guards'
-import {track} from 'lib/analytics/analytics'
-import {SuggestedActorsModel} from './suggested-actors'
-
-export const OnboardingScreenSteps = {
-  Welcome: 'Welcome',
-  RecommendedFeeds: 'RecommendedFeeds',
-  RecommendedFollows: 'RecommendedFollows',
-  Home: 'Home',
-} as const
-
-type OnboardingStep =
-  (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
-const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
-export class OnboardingModel {
-  // state
-  step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
-
-  // data
-  suggestedActors: SuggestedActorsModel
-
-  constructor(public rootStore: RootStoreModel) {
-    this.suggestedActors = new SuggestedActorsModel(this.rootStore)
-    makeAutoObservable(this, {
-      rootStore: false,
-      hydrate: false,
-      serialize: false,
-    })
-  }
-
-  serialize(): unknown {
-    return {
-      step: this.step,
-    }
-  }
-
-  hydrate(v: unknown) {
-    if (typeof v === 'object' && v !== null) {
-      if (
-        hasProp(v, 'step') &&
-        typeof v.step === 'string' &&
-        OnboardingStepsArray.includes(v.step as OnboardingStep)
-      ) {
-        this.step = v.step as OnboardingStep
-      }
-    } else {
-      // if there is no valid state, we'll just reset
-      this.reset()
-    }
-  }
-
-  /**
-   * Returns the name of the next screen in the onboarding process based on the current step or screen name provided.
-   * @param {OnboardingStep} [currentScreenName]
-   * @returns name of next screen in the onboarding process
-   */
-  next(currentScreenName?: OnboardingStep) {
-    currentScreenName = currentScreenName || this.step
-    if (currentScreenName === 'Welcome') {
-      this.step = 'RecommendedFeeds'
-      return this.step
-    } else if (this.step === 'RecommendedFeeds') {
-      this.step = 'RecommendedFollows'
-      // prefetch recommended follows
-      this.suggestedActors.loadMore(true)
-      return this.step
-    } else if (this.step === 'RecommendedFollows') {
-      this.finish()
-      return this.step
-    } else {
-      // if we get here, we're in an invalid state, let's just go Home
-      return 'Home'
-    }
-  }
-
-  start() {
-    this.step = 'Welcome'
-    track('Onboarding:Begin')
-  }
-
-  finish() {
-    this.rootStore.me.mainFeed.refresh() // load the selected content
-    this.step = 'Home'
-    track('Onboarding:Complete')
-  }
-
-  reset() {
-    this.step = 'Welcome'
-    track('Onboarding:Reset')
-  }
-
-  skip() {
-    this.step = 'Home'
-    track('Onboarding:Skipped')
-  }
-
-  get isComplete() {
-    return this.step === 'Home'
-  }
-
-  get isActive() {
-    return !this.isComplete
-  }
-}
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
-}