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/suggested-posts.ts88
-rw-r--r--src/state/models/discovery/user-autocomplete.ts103
2 files changed, 191 insertions, 0 deletions
diff --git a/src/state/models/discovery/suggested-posts.ts b/src/state/models/discovery/suggested-posts.ts
new file mode 100644
index 000000000..6c8de3023
--- /dev/null
+++ b/src/state/models/discovery/suggested-posts.ts
@@ -0,0 +1,88 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {RootStoreModel} from '../root-store'
+import {PostsFeedItemModel} from '../feeds/posts'
+import {cleanError} from 'lib/strings/errors'
+import {TEAM_HANDLES} from 'lib/constants'
+import {
+  getMultipleAuthorsPosts,
+  mergePosts,
+} from 'lib/api/build-suggested-posts'
+
+export class SuggestedPostsModel {
+  // state
+  isLoading = false
+  hasLoaded = false
+  error = ''
+
+  // data
+  posts: PostsFeedItemModel[] = []
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  get hasContent() {
+    return this.posts.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async setup() {
+    this._xLoading()
+    try {
+      const responses = await getMultipleAuthorsPosts(
+        this.rootStore,
+        TEAM_HANDLES(String(this.rootStore.agent.service)),
+        undefined,
+        30,
+      )
+      runInAction(() => {
+        const finalPosts = mergePosts(responses, {repostsOnly: true})
+        // hydrate into models
+        this.posts = finalPosts.map((post, i) => {
+          // strip the reasons to hide that these are reposts
+          delete post.reason
+          return new PostsFeedItemModel(this.rootStore, `post-${i}`, post)
+        })
+      })
+      this._xIdle()
+    } catch (e: any) {
+      this.rootStore.log.error('SuggestedPostsView: Failed to load posts', {
+        e,
+      })
+      this._xIdle() // dont bubble to the user
+    }
+  }
+
+  // state transitions
+  // =
+
+  _xLoading() {
+    this.isLoading = true
+    this.error = ''
+  }
+
+  _xIdle(err?: any) {
+    this.isLoading = false
+    this.hasLoaded = true
+    this.error = cleanError(err)
+    if (err) {
+      this.rootStore.log.error('Failed to fetch suggested posts', err)
+    }
+  }
+}
diff --git a/src/state/models/discovery/user-autocomplete.ts b/src/state/models/discovery/user-autocomplete.ts
new file mode 100644
index 000000000..601e10ea0
--- /dev/null
+++ b/src/state/models/discovery/user-autocomplete.ts
@@ -0,0 +1,103 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {AppBskyActorDefs} from '@atproto/api'
+import AwaitLock from 'await-lock'
+import {RootStoreModel} from '../root-store'
+
+export class UserAutocompleteModel {
+  // state
+  isLoading = false
+  isActive = false
+  prefix = ''
+  lock = new AwaitLock()
+
+  // data
+  follows: AppBskyActorDefs.ProfileViewBasic[] = []
+  searchRes: AppBskyActorDefs.ProfileViewBasic[] = []
+  knownHandles: Set<string> = new Set()
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+        knownHandles: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  get suggestions() {
+    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,
+    }))
+  }
+
+  // public api
+  // =
+
+  async setup() {
+    await this._getFollows()
+  }
+
+  setActive(v: boolean) {
+    this.isActive = v
+  }
+
+  async setPrefix(prefix: string) {
+    const origPrefix = prefix.trim()
+    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
+        }
+        await this._search()
+      } else {
+        this.searchRes = []
+      }
+    } finally {
+      this.lock.release()
+    }
+  }
+
+  // internal
+  // =
+
+  async _getFollows() {
+    const res = await this.rootStore.agent.getFollows({
+      actor: this.rootStore.me.did || '',
+    })
+    runInAction(() => {
+      this.follows = res.data.follows
+      for (const f of this.follows) {
+        this.knownHandles.add(f.handle)
+      }
+    })
+  }
+
+  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)
+      }
+    })
+  }
+}