about summary refs log tree commit diff
path: root/src/state/models
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models')
-rw-r--r--src/state/models/invited-users.ts70
-rw-r--r--src/state/models/me.ts90
-rw-r--r--src/state/models/root-store.ts8
-rw-r--r--src/state/models/ui/shell.ts5
4 files changed, 154 insertions, 19 deletions
diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts
new file mode 100644
index 000000000..121161a32
--- /dev/null
+++ b/src/state/models/invited-users.ts
@@ -0,0 +1,70 @@
+import {makeAutoObservable, runInAction} from 'mobx'
+import {ComAtprotoServerDefs, AppBskyActorDefs} from '@atproto/api'
+import {RootStoreModel} from './root-store'
+import {isObj, hasProp, isStrArray} from 'lib/type-guards'
+
+export class InvitedUsers {
+  seenDids: string[] = []
+  profiles: AppBskyActorDefs.ProfileViewDetailed[] = []
+
+  get numNotifs() {
+    return this.profiles.length
+  }
+
+  constructor(public rootStore: RootStoreModel) {
+    makeAutoObservable(
+      this,
+      {rootStore: false, serialize: false, hydrate: false},
+      {autoBind: true},
+    )
+  }
+
+  serialize() {
+    return {seenDids: this.seenDids}
+  }
+
+  hydrate(v: unknown) {
+    if (isObj(v) && hasProp(v, 'seenDids') && isStrArray(v.seenDids)) {
+      this.seenDids = v.seenDids
+    }
+  }
+
+  async fetch(invites: ComAtprotoServerDefs.InviteCode[]) {
+    // pull the dids of invited users not marked seen
+    const dids = []
+    for (const invite of invites) {
+      for (const use of invite.uses) {
+        if (!this.seenDids.includes(use.usedBy)) {
+          dids.push(use.usedBy)
+        }
+      }
+    }
+
+    // fetch their profiles
+    this.profiles = []
+    if (dids.length) {
+      try {
+        const res = await this.rootStore.agent.app.bsky.actor.getProfiles({
+          actors: dids,
+        })
+        runInAction(() => {
+          // save the ones following -- these are the ones we want to notify the user about
+          this.profiles = res.data.profiles.filter(
+            profile => !profile.viewer?.following,
+          )
+        })
+        this.rootStore.me.follows.hydrateProfiles(this.profiles)
+      } catch (e) {
+        this.rootStore.log.error(
+          'Failed to fetch profiles for invited users',
+          e,
+        )
+      }
+    }
+  }
+
+  markSeen(did: string) {
+    this.seenDids.push(did)
+    this.profiles = this.profiles.filter(profile => profile.did !== did)
+  }
+}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 3adbc7c6c..1dcccb6f1 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -1,10 +1,13 @@
 import {makeAutoObservable, runInAction} from 'mobx'
+import {ComAtprotoServerDefs} from '@atproto/api'
 import {RootStoreModel} from './root-store'
 import {PostsFeedModel} from './feeds/posts'
 import {NotificationsFeedModel} from './feeds/notifications'
 import {MyFollowsCache} from './cache/my-follows'
 import {isObj, hasProp} from 'lib/type-guards'
 
+const PROFILE_UPDATE_INTERVAL = 10 * 60 * 1e3 // 10min
+
 export class MeModel {
   did: string = ''
   handle: string = ''
@@ -16,6 +19,12 @@ export class MeModel {
   mainFeed: PostsFeedModel
   notifications: NotificationsFeedModel
   follows: MyFollowsCache
+  invites: ComAtprotoServerDefs.InviteCode[] = []
+  lastProfileStateUpdate = Date.now()
+
+  get invitesAvailable() {
+    return this.invites.filter(isInviteAvailable).length
+  }
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(
@@ -39,6 +48,7 @@ export class MeModel {
     this.displayName = ''
     this.description = ''
     this.avatar = ''
+    this.invites = []
   }
 
   serialize(): unknown {
@@ -85,24 +95,7 @@ export class MeModel {
     if (sess.hasSession) {
       this.did = sess.currentSession?.did || ''
       this.handle = sess.currentSession?.handle || ''
-      const profile = await this.rootStore.agent.getProfile({
-        actor: this.did,
-      })
-      runInAction(() => {
-        if (profile?.data) {
-          this.displayName = profile.data.displayName || ''
-          this.description = profile.data.description || ''
-          this.avatar = profile.data.avatar || ''
-          this.followsCount = profile.data.followsCount
-          this.followersCount = profile.data.followersCount
-        } else {
-          this.displayName = ''
-          this.description = ''
-          this.avatar = ''
-          this.followsCount = profile.data.followsCount
-          this.followersCount = undefined
-        }
-      })
+      await this.fetchProfile()
       this.mainFeed.clear()
       await Promise.all([
         this.mainFeed.setup().catch(e => {
@@ -113,8 +106,69 @@ export class MeModel {
         }),
       ])
       this.rootStore.emitSessionLoaded()
+      await this.fetchInviteCodes()
     } else {
       this.clear()
     }
   }
+
+  async updateIfNeeded() {
+    if (Date.now() - this.lastProfileStateUpdate > PROFILE_UPDATE_INTERVAL) {
+      this.rootStore.log.debug('Updating me profile information')
+      await this.fetchProfile()
+      await this.fetchInviteCodes()
+    }
+    await this.notifications.loadUnreadCount()
+  }
+
+  async fetchProfile() {
+    const profile = await this.rootStore.agent.getProfile({
+      actor: this.did,
+    })
+    runInAction(() => {
+      if (profile?.data) {
+        this.displayName = profile.data.displayName || ''
+        this.description = profile.data.description || ''
+        this.avatar = profile.data.avatar || ''
+        this.followsCount = profile.data.followsCount
+        this.followersCount = profile.data.followersCount
+      } else {
+        this.displayName = ''
+        this.description = ''
+        this.avatar = ''
+        this.followsCount = profile.data.followsCount
+        this.followersCount = undefined
+      }
+    })
+  }
+
+  async fetchInviteCodes() {
+    if (this.rootStore.session) {
+      try {
+        const res =
+          await this.rootStore.agent.com.atproto.server.getAccountInviteCodes(
+            {},
+          )
+        runInAction(() => {
+          this.invites = res.data.codes
+          this.invites.sort((a, b) => {
+            if (!isInviteAvailable(a)) {
+              return 1
+            }
+            if (!isInviteAvailable(b)) {
+              return -1
+            }
+            return 0
+          })
+        })
+      } catch (e) {
+        this.rootStore.log.error('Failed to fetch user invite codes', e)
+      }
+      await this.rootStore.invitedUsers.fetch(this.invites)
+    }
+  }
+}
+
+function isInviteAvailable(invite: ComAtprotoServerDefs.InviteCode): boolean {
+  return invite.available - invite.uses.length > 0 && !invite.disabled
 }
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 0d893415f..9207f27ba 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -16,6 +16,7 @@ import {ProfilesCache} from './cache/profiles-view'
 import {LinkMetasCache} from './cache/link-metas'
 import {NotificationsFeedItemModel} from './feeds/notifications'
 import {MeModel} from './me'
+import {InvitedUsers} from './invited-users'
 import {PreferencesModel} from './ui/preferences'
 import {resetToTab} from '../../Navigation'
 import {ImageSizesCache} from './cache/image-sizes'
@@ -36,6 +37,7 @@ export class RootStoreModel {
   shell = new ShellUiModel(this)
   preferences = new PreferencesModel()
   me = new MeModel(this)
+  invitedUsers = new InvitedUsers(this)
   profiles = new ProfilesCache(this)
   linkMetas = new LinkMetasCache(this)
   imageSizes = new ImageSizesCache()
@@ -61,6 +63,7 @@ export class RootStoreModel {
       me: this.me.serialize(),
       shell: this.shell.serialize(),
       preferences: this.preferences.serialize(),
+      invitedUsers: this.invitedUsers.serialize(),
     }
   }
 
@@ -84,6 +87,9 @@ export class RootStoreModel {
       if (hasProp(v, 'preferences')) {
         this.preferences.hydrate(v.preferences)
       }
+      if (hasProp(v, 'invitedUsers')) {
+        this.invitedUsers.hydrate(v.invitedUsers)
+      }
     }
   }
 
@@ -141,7 +147,7 @@ export class RootStoreModel {
       return
     }
     try {
-      await this.me.notifications.loadUnreadCount()
+      await this.me.updateIfNeeded()
     } catch (e: any) {
       this.log.error('Failed to fetch latest state', e)
     }
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index b782dd2f7..917e7a09f 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -61,6 +61,10 @@ export interface WaitlistModal {
   name: 'waitlist'
 }
 
+export interface InviteCodesModal {
+  name: 'invite-codes'
+}
+
 export type Modal =
   | ConfirmModal
   | EditProfileModal
@@ -72,6 +76,7 @@ export type Modal =
   | RepostModal
   | ChangeHandleModal
   | WaitlistModal
+  | InviteCodesModal
 
 interface LightboxModel {}