about summary refs log tree commit diff
path: root/src/state/models/content/profile.ts
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-04-03 15:21:17 -0500
committerGitHub <noreply@github.com>2023-04-03 15:21:17 -0500
commit2045c615a8f8a39ee9f54638a234f3d45f028399 (patch)
tree059b4435bb1c6720e40e8767c3eb0dae8d894e67 /src/state/models/content/profile.ts
parent9652d994dd207585fb1b8f3452382478f204f70a (diff)
downloadvoidsky-2045c615a8f8a39ee9f54638a234f3d45f028399.tar.zst
Reorganize state models for clarity (#378)
Diffstat (limited to 'src/state/models/content/profile.ts')
-rw-r--r--src/state/models/content/profile.ts224
1 files changed, 224 insertions, 0 deletions
diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts
new file mode 100644
index 000000000..08616bf18
--- /dev/null
+++ b/src/state/models/content/profile.ts
@@ -0,0 +1,224 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {PickedMedia} from 'lib/media/picker'
+import {
+  AppBskyActorGetProfile as GetProfile,
+  AppBskyActorProfile,
+  RichText,
+} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import * as apilib from 'lib/api/index'
+import {cleanError} from 'lib/strings/errors'
+
+export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
+
+export class ProfileViewerModel {
+  muted?: boolean
+  following?: string
+  followedBy?: string
+
+  constructor() {
+    makeAutoObservable(this)
+  }
+}
+
+export class ProfileModel {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  params: GetProfile.QueryParams
+
+  // data
+  did: string = ''
+  handle: string = ''
+  creator: string = ''
+  displayName?: string = ''
+  description?: string = ''
+  avatar?: string = ''
+  banner?: string = ''
+  followersCount: number = 0
+  followsCount: number = 0
+  postsCount: number = 0
+  viewer = new ProfileViewerModel()
+
+  // added data
+  descriptionRichText?: RichText = new RichText({text: ''})
+
+  constructor(
+    public rootStore: RootStoreModel,
+    params: GetProfile.QueryParams,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+        params: false,
+      },
+      {autoBind: true},
+    )
+    this.params = params
+  }
+
+  get hasContent() {
+    return this.did !== ''
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async setup() {
+    await this._load()
+  }
+
+  async refresh() {
+    await this._load(true)
+  }
+
+  async toggleFollowing() {
+    if (!this.rootStore.me.did) {
+      throw new Error('Not logged in')
+    }
+
+    const follows = this.rootStore.me.follows
+    const followUri = follows.isFollowing(this.did)
+      ? follows.getFollowUri(this.did)
+      : undefined
+
+    // guard against this view getting out of sync with the follows cache
+    if (followUri !== this.viewer.following) {
+      this.viewer.following = followUri
+      return
+    }
+
+    if (followUri) {
+      await this.rootStore.agent.deleteFollow(followUri)
+      runInAction(() => {
+        this.followersCount--
+        this.viewer.following = undefined
+        this.rootStore.me.follows.removeFollow(this.did)
+      })
+    } else {
+      const res = await this.rootStore.agent.follow(this.did)
+      runInAction(() => {
+        this.followersCount++
+        this.viewer.following = res.uri
+        this.rootStore.me.follows.addFollow(this.did, res.uri)
+      })
+    }
+  }
+
+  async updateProfile(
+    updates: AppBskyActorProfile.Record,
+    newUserAvatar: PickedMedia | undefined | null,
+    newUserBanner: PickedMedia | undefined | null,
+  ) {
+    await this.rootStore.agent.upsertProfile(async existing => {
+      existing = existing || {}
+      existing.displayName = updates.displayName
+      existing.description = updates.description
+      if (newUserAvatar) {
+        const res = await apilib.uploadBlob(
+          this.rootStore,
+          newUserAvatar.path,
+          newUserAvatar.mime,
+        )
+        existing.avatar = res.data.blob
+      } else if (newUserAvatar === null) {
+        existing.avatar = undefined
+      }
+      if (newUserBanner) {
+        const res = await apilib.uploadBlob(
+          this.rootStore,
+          newUserBanner.path,
+          newUserBanner.mime,
+        )
+        existing.banner = res.data.blob
+      } else if (newUserBanner === null) {
+        existing.banner = undefined
+      }
+      return existing
+    })
+    await this.rootStore.me.load()
+    await this.refresh()
+  }
+
+  async muteAccount() {
+    await this.rootStore.agent.mute(this.did)
+    this.viewer.muted = true
+    await this.refresh()
+  }
+
+  async unmuteAccount() {
+    await this.rootStore.agent.unmute(this.did)
+    this.viewer.muted = false
+    await this.refresh()
+  }
+
+  // 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) {
+      this.rootStore.log.error('Failed to fetch profile', err)
+    }
+  }
+
+  // loader functions
+  // =
+
+  async _load(isRefreshing = false) {
+    this._xLoading(isRefreshing)
+    try {
+      const res = await this.rootStore.agent.getProfile(this.params)
+      this.rootStore.profiles.overwrite(this.params.actor, res) // cache invalidation
+      this._replaceAll(res)
+      await this._createRichText()
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(e)
+    }
+  }
+
+  _replaceAll(res: GetProfile.Response) {
+    this.did = res.data.did
+    this.handle = res.data.handle
+    this.displayName = res.data.displayName
+    this.description = res.data.description
+    this.avatar = res.data.avatar
+    this.banner = res.data.banner
+    this.followersCount = res.data.followersCount || 0
+    this.followsCount = res.data.followsCount || 0
+    this.postsCount = res.data.postsCount || 0
+    if (res.data.viewer) {
+      Object.assign(this.viewer, res.data.viewer)
+      this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
+    }
+  }
+
+  async _createRichText() {
+    this.descriptionRichText = new RichText(
+      {text: this.description || ''},
+      {cleanNewlines: true},
+    )
+    await this.descriptionRichText.detectFacets(this.rootStore.agent)
+  }
+}