about summary refs log tree commit diff
path: root/src/state/models/my-follows.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models/my-follows.ts')
-rw-r--r--src/state/models/my-follows.ts109
1 files changed, 109 insertions, 0 deletions
diff --git a/src/state/models/my-follows.ts b/src/state/models/my-follows.ts
new file mode 100644
index 000000000..252e8a3d3
--- /dev/null
+++ b/src/state/models/my-follows.ts
@@ -0,0 +1,109 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {FollowRecord, AppBskyActorProfile, AppBskyActorRef} from '@atproto/api'
+import {RootStoreModel} from './root-store'
+import {bundleAsync} from 'lib/async/bundle'
+
+const CACHE_TTL = 1000 * 60 * 60 // hourly
+type FollowsListResponse = Awaited<ReturnType<FollowRecord['list']>>
+type FollowsListResponseRecord = FollowsListResponse['records'][0]
+type Profile =
+  | AppBskyActorProfile.ViewBasic
+  | AppBskyActorProfile.View
+  | AppBskyActorRef.WithInfo
+
+/**
+ * This model is used to maintain a synced local cache of the user's
+ * follows. It should be periodically refreshed and updated any time
+ * the user makes a change to their follows.
+ */
+export class MyFollowsModel {
+  // data
+  followDidToRecordMap: Record<string, string> = {}
+  lastSync = 0
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  // public api
+  // =
+
+  fetchIfNeeded = bundleAsync(async () => {
+    if (
+      Object.keys(this.followDidToRecordMap).length === 0 ||
+      Date.now() - this.lastSync > CACHE_TTL
+    ) {
+      return await this.fetch()
+    }
+  })
+
+  fetch = bundleAsync(async () => {
+    this.rootStore.log.debug('MyFollowsModel:fetch running full fetch')
+    let before
+    let records: FollowsListResponseRecord[] = []
+    do {
+      const res: FollowsListResponse =
+        await this.rootStore.api.app.bsky.graph.follow.list({
+          user: this.rootStore.me.did,
+          before,
+        })
+      records = records.concat(res.records)
+      before = res.cursor
+    } while (typeof before !== 'undefined')
+    runInAction(() => {
+      this.followDidToRecordMap = {}
+      for (const record of records) {
+        this.followDidToRecordMap[record.value.subject.did] = record.uri
+      }
+      this.lastSync = Date.now()
+    })
+  })
+
+  isFollowing(did: string) {
+    return !!this.followDidToRecordMap[did]
+  }
+
+  getFollowUri(did: string): string {
+    const v = this.followDidToRecordMap[did]
+    if (!v) {
+      throw new Error('Not a followed user')
+    }
+    return v
+  }
+
+  addFollow(did: string, recordUri: string) {
+    this.followDidToRecordMap[did] = recordUri
+  }
+
+  removeFollow(did: string) {
+    delete this.followDidToRecordMap[did]
+  }
+
+  /**
+   * Use this to incrementally update the cache as views provide information
+   */
+  hydrate(did: string, recordUri: string | undefined) {
+    if (recordUri) {
+      this.followDidToRecordMap[did] = recordUri
+    } else {
+      delete this.followDidToRecordMap[did]
+    }
+  }
+
+  /**
+   * Use this to incrementally update the cache as views provide information
+   */
+  hydrateProfiles(profiles: Profile[]) {
+    for (const profile of profiles) {
+      if (profile.viewer) {
+        this.hydrate(profile.did, profile.viewer.following)
+      }
+    }
+  }
+}