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/me.ts15
-rw-r--r--src/state/models/notifications-view.ts103
-rw-r--r--src/state/models/post-thread-view.ts11
3 files changed, 97 insertions, 32 deletions
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 78c6d2e76..e3405b80d 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -1,6 +1,7 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {RootStoreModel} from './root-store'
 import {MembershipsViewModel} from './memberships-view'
+import {NotificationsViewModel} from './notifications-view'
 
 export class MeModel {
   did?: string
@@ -9,9 +10,11 @@ export class MeModel {
   description?: string
   notificationCount: number = 0
   memberships?: MembershipsViewModel
+  notifications: NotificationsViewModel
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {rootStore: false}, {autoBind: true})
+    this.notifications = new NotificationsViewModel(this.rootStore, {})
   }
 
   clear() {
@@ -43,7 +46,12 @@ export class MeModel {
       this.memberships = new MembershipsViewModel(this.rootStore, {
         actor: this.did,
       })
-      await this.memberships?.setup()
+      await this.memberships?.setup().catch(e => {
+        console.error('Failed to setup memberships model', e)
+      })
+      await this.notifications.setup().catch(e => {
+        console.error('Failed to setup notifications model', e)
+      })
     } else {
       this.clear()
     }
@@ -56,7 +64,12 @@ export class MeModel {
   async fetchStateUpdate() {
     const res = await this.rootStore.api.app.bsky.notification.getCount()
     runInAction(() => {
+      const newNotifications = this.notificationCount !== res.data.count
       this.notificationCount = res.data.count
+      if (newNotifications) {
+        // trigger pre-emptive fetch on new notifications
+        this.notifications.refresh()
+      }
     })
   }
 
diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts
index 80e5c80c6..e81f31a25 100644
--- a/src/state/models/notifications-view.ts
+++ b/src/state/models/notifications-view.ts
@@ -1,6 +1,7 @@
-import {makeAutoObservable} from 'mobx'
+import {makeAutoObservable, runInAction} from 'mobx'
 import * as ListNotifications from '../../third-party/api/src/client/types/app/bsky/notification/list'
 import {RootStoreModel} from './root-store'
+import {PostThreadViewModel} from './post-thread-view'
 import {Declaration} from './_common'
 import {hasProp} from '../lib/type-guards'
 import {APP_BSKY_GRAPH} from '../../third-party/api'
@@ -34,6 +35,9 @@ export class NotificationsViewItemModel implements GroupedNotification {
   indexedAt: string = ''
   additional?: NotificationsViewItemModel[]
 
+  // additional data
+  additionalPost?: PostThreadViewModel
+
   constructor(
     public rootStore: RootStoreModel,
     reactKey: string,
@@ -89,6 +93,13 @@ export class NotificationsViewItemModel implements GroupedNotification {
     return this.reason === 'assertion'
   }
 
+  get needsAdditionalData() {
+    if (this.isUpvote || this.isRepost || this.isTrend || this.isReply) {
+      return !this.additionalPost
+    }
+    return false
+  }
+
   get isInvite() {
     return (
       this.isAssertion && this.record.assertion === APP_BSKY_GRAPH.AssertMember
@@ -107,6 +118,27 @@ export class NotificationsViewItemModel implements GroupedNotification {
     }
     return ''
   }
+
+  async fetchAdditionalData() {
+    if (!this.needsAdditionalData) {
+      return
+    }
+    let postUri
+    if (this.isReply) {
+      postUri = this.uri
+    } else if (this.isUpvote || this.isRead || this.isTrend) {
+      postUri = this.subjectUri
+    }
+    if (postUri) {
+      this.additionalPost = new PostThreadViewModel(this.rootStore, {
+        uri: postUri,
+        depth: 0,
+      })
+      await this.additionalPost.setup().catch(e => {
+        console.error('Failed to load post needed by notification', e)
+      })
+    }
+  }
 }
 
 export class NotificationsViewModel {
@@ -171,7 +203,6 @@ export class NotificationsViewModel {
     await this._pendingWork()
     this._loadPromise = this._initialLoad(isRefreshing)
     await this._loadPromise
-    this._updateReadState()
     this._loadPromise = undefined
   }
 
@@ -208,6 +239,20 @@ export class NotificationsViewModel {
     this._updatePromise = undefined
   }
 
+  /**
+   * Update read/unread state
+   */
+  async updateReadState() {
+    try {
+      await this.rootStore.api.app.bsky.notification.updateSeen({
+        seenAt: new Date().toISOString(),
+      })
+      this.rootStore.me.clearNotificationCount()
+    } catch (e) {
+      console.log('Failed to update notifications read state', e)
+    }
+  }
+
   // state transitions
   // =
 
@@ -246,7 +291,7 @@ export class NotificationsViewModel {
         limit: PAGE_SIZE,
       })
       const res = await this.rootStore.api.app.bsky.notification.list(params)
-      this._replaceAll(res)
+      await this._replaceAll(res)
       this._xIdle()
     } catch (e: any) {
       this._xIdle(`Failed to load notifications: ${e.toString()}`)
@@ -264,7 +309,7 @@ export class NotificationsViewModel {
         before: this.loadMoreCursor,
       })
       const res = await this.rootStore.api.app.bsky.notification.list(params)
-      this._appendAll(res)
+      await this._appendAll(res)
       this._xIdle()
     } catch (e: any) {
       this._xIdle(`Failed to load notifications: ${e.toString()}`)
@@ -296,25 +341,40 @@ export class NotificationsViewModel {
     }
   }
 
-  private _replaceAll(res: ListNotifications.Response) {
-    this.notifications.length = 0
-    this._appendAll(res)
+  private async _replaceAll(res: ListNotifications.Response) {
+    return this._appendAll(res, true)
   }
 
-  private _appendAll(res: ListNotifications.Response) {
+  private async _appendAll(res: ListNotifications.Response, replace = false) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     let counter = this.notifications.length
+    const promises = []
+    const itemModels: NotificationsViewItemModel[] = []
     for (const item of groupNotifications(res.data.notifications)) {
-      this._append(counter++, item)
+      const itemModel = new NotificationsViewItemModel(
+        this.rootStore,
+        `item-${counter++}`,
+        item,
+      )
+      if (itemModel.needsAdditionalData) {
+        promises.push(itemModel.fetchAdditionalData())
+      }
+      itemModels.push(itemModel)
     }
-  }
-
-  private _append(keyId: number, item: GroupedNotification) {
-    // TODO: validate .record
-    this.notifications.push(
-      new NotificationsViewItemModel(this.rootStore, `item-${keyId}`, item),
-    )
+    await Promise.all(promises).catch(e => {
+      console.error(
+        'Uncaught failure during notifications-view _appendAll()',
+        e,
+      )
+    })
+    runInAction(() => {
+      if (replace) {
+        this.notifications = itemModels
+      } else {
+        this.notifications = this.notifications.concat(itemModels)
+      }
+    })
   }
 
   private _updateAll(res: ListNotifications.Response) {
@@ -330,17 +390,6 @@ export class NotificationsViewModel {
       }
     }
   }
-
-  private async _updateReadState() {
-    try {
-      await this.rootStore.api.app.bsky.notification.updateSeen({
-        seenAt: new Date().toISOString(),
-      })
-      this.rootStore.me.clearNotificationCount()
-    } catch (e) {
-      console.log('Failed to update notifications read state', e)
-    }
-  }
 }
 
 function groupNotifications(
diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts
index 70e34537f..5c0e0a4e8 100644
--- a/src/state/models/post-thread-view.ts
+++ b/src/state/models/post-thread-view.ts
@@ -1,5 +1,5 @@
 import {makeAutoObservable, runInAction} from 'mobx'
-import * as GetPostThread from '../../third-party/api/src/client/types/app/bsky/feed/getPostThread'
+import {AppBskyFeedGetPostThread as GetPostThread} from '../../third-party/api'
 import {AtUri} from '../../third-party/uri'
 import _omit from 'lodash.omit'
 import {RootStoreModel} from './root-store'
@@ -216,6 +216,7 @@ export class PostThreadViewModel {
   isRefreshing = false
   hasLoaded = false
   error = ''
+  notFound = false
   resolvedUri = ''
   params: GetPostThread.QueryParams
 
@@ -286,13 +287,15 @@ export class PostThreadViewModel {
     this.isLoading = true
     this.isRefreshing = isRefreshing
     this.error = ''
+    this.notFound = false
   }
 
-  private _xIdle(err: string = '') {
+  private _xIdle(err: any = undefined) {
     this.isLoading = false
     this.isRefreshing = false
     this.hasLoaded = true
-    this.error = err
+    this.error = err ? err.toString() : ''
+    this.notFound = err instanceof GetPostThread.NotFoundError
   }
 
   // loader functions
@@ -317,7 +320,7 @@ export class PostThreadViewModel {
       this._replaceAll(res)
       this._xIdle()
     } catch (e: any) {
-      this._xIdle(`Failed to load thread: ${e.toString()}`)
+      this._xIdle(e)
     }
   }