about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/models/content/post-thread.ts40
-rw-r--r--src/state/models/content/post.ts19
-rw-r--r--src/state/models/feeds/notifications.ts46
-rw-r--r--src/state/models/feeds/posts.ts19
-rw-r--r--src/state/models/muted-threads.ts29
-rw-r--r--src/state/models/root-store.ts6
6 files changed, 148 insertions, 11 deletions
diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts
index 794beae20..acc9bffa9 100644
--- a/src/state/models/content/post-thread.ts
+++ b/src/state/models/content/post-thread.ts
@@ -42,6 +42,17 @@ export class PostThreadItemModel {
     return this.postRecord?.reply?.parent.uri
   }
 
+  get rootUri(): string {
+    if (this.postRecord?.reply?.root.uri) {
+      return this.postRecord.reply.root.uri
+    }
+    return this.uri
+  }
+
+  get isThreadMuted() {
+    return this.rootStore.mutedThreads.uris.has(this.rootUri)
+  }
+
   constructor(
     public rootStore: RootStoreModel,
     reactKey: string,
@@ -188,6 +199,14 @@ export class PostThreadItemModel {
     }
   }
 
+  async toggleThreadMute() {
+    if (this.isThreadMuted) {
+      this.rootStore.mutedThreads.uris.delete(this.rootUri)
+    } else {
+      this.rootStore.mutedThreads.uris.add(this.rootUri)
+    }
+  }
+
   async delete() {
     await this.rootStore.agent.deletePost(this.post.uri)
     this.rootStore.emitPostDeleted(this.post.uri)
@@ -230,6 +249,19 @@ export class PostThreadModel {
     return this.error !== ''
   }
 
+  get rootUri(): string {
+    if (this.thread) {
+      if (this.thread.postRecord?.reply?.root.uri) {
+        return this.thread.postRecord.reply.root.uri
+      }
+    }
+    return this.resolvedUri
+  }
+
+  get isThreadMuted() {
+    return this.rootStore.mutedThreads.uris.has(this.rootUri)
+  }
+
   // public api
   // =
 
@@ -279,6 +311,14 @@ export class PostThreadModel {
     this.refresh()
   }
 
+  async toggleThreadMute() {
+    if (this.isThreadMuted) {
+      this.rootStore.mutedThreads.uris.delete(this.rootUri)
+    } else {
+      this.rootStore.mutedThreads.uris.add(this.rootUri)
+    }
+  }
+
   // state transitions
   // =
 
diff --git a/src/state/models/content/post.ts b/src/state/models/content/post.ts
index b5d95bf01..7ba633366 100644
--- a/src/state/models/content/post.ts
+++ b/src/state/models/content/post.ts
@@ -48,6 +48,17 @@ export class PostModel implements RemoveIndex<Post.Record> {
     return this.hasLoaded && !this.hasContent
   }
 
+  get rootUri(): string {
+    if (this.reply?.root.uri) {
+      return this.reply.root.uri
+    }
+    return this.uri
+  }
+
+  get isThreadMuted() {
+    return this.rootStore.mutedThreads.uris.has(this.rootUri)
+  }
+
   // public api
   // =
 
@@ -55,6 +66,14 @@ export class PostModel implements RemoveIndex<Post.Record> {
     await this._load()
   }
 
+  async toggleThreadMute() {
+    if (this.isThreadMuted) {
+      this.rootStore.mutedThreads.uris.delete(this.rootUri)
+    } else {
+      this.rootStore.mutedThreads.uris.add(this.rootUri)
+    }
+  }
+
   // state transitions
   // =
 
diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts
index ff77ab979..e2a18ea04 100644
--- a/src/state/models/feeds/notifications.ts
+++ b/src/state/models/feeds/notifications.ts
@@ -160,6 +160,13 @@ export class NotificationsFeedItemModel {
     return ''
   }
 
+  get reasonSubjectRootUri(): string | undefined {
+    if (this.additionalPost) {
+      return this.additionalPost.rootUri
+    }
+    return undefined
+  }
+
   toSupportedRecord(v: unknown): SupportedRecord | undefined {
     for (const ns of [
       AppBskyFeedPost,
@@ -227,7 +234,7 @@ export class NotificationsFeedModel {
 
   // data
   notifications: NotificationsFeedItemModel[] = []
-  queuedNotifications: undefined | ListNotifications.Notification[] = undefined
+  queuedNotifications: undefined | NotificationsFeedItemModel[] = undefined
   unreadCount = 0
 
   // this is used to help trigger push notifications
@@ -354,7 +361,13 @@ export class NotificationsFeedModel {
         queue.push(notif)
       }
 
-      this._setQueued(this._filterNotifications(queue))
+      // NOTE
+      // because filtering depends on the added information we have to fetch
+      // the full models here. this is *not* ideal performance and we need
+      // to update the notifications route to give all the info we need
+      // -prf
+      const queueModels = await this._fetchItemModels(queue)
+      this._setQueued(this._filterNotifications(queueModels))
       this._countUnread()
     } catch (e) {
       this.rootStore.log.error('NotificationsModel:syncQueue failed', {e})
@@ -452,7 +465,8 @@ export class NotificationsFeedModel {
       res.data.notifications[0],
     )
     await notif.fetchAdditionalData()
-    return notif
+    const filtered = this._filterNotifications([notif])
+    return filtered[0]
   }
 
   // state transitions
@@ -505,23 +519,26 @@ export class NotificationsFeedModel {
   }
 
   _filterNotifications(
-    items: ListNotifications.Notification[],
-  ): ListNotifications.Notification[] {
+    items: NotificationsFeedItemModel[],
+  ): NotificationsFeedItemModel[] {
     return items.filter(item => {
-      return (
-        this.rootStore.preferences.getLabelPreference(item.labels).pref !==
+      const hideByLabel =
+        this.rootStore.preferences.getLabelPreference(item.labels).pref ===
         'hide'
+      let mutedThread = !!(
+        item.reasonSubjectRootUri &&
+        this.rootStore.mutedThreads.uris.has(item.reasonSubjectRootUri)
       )
+      return !hideByLabel && !mutedThread
     })
   }
 
-  async _processNotifications(
+  async _fetchItemModels(
     items: ListNotifications.Notification[],
   ): Promise<NotificationsFeedItemModel[]> {
     const promises = []
     const itemModels: NotificationsFeedItemModel[] = []
-    items = this._filterNotifications(items)
-    for (const item of groupNotifications(items)) {
+    for (const item of items) {
       const itemModel = new NotificationsFeedItemModel(
         this.rootStore,
         `item-${_idCounter++}`,
@@ -541,7 +558,14 @@ export class NotificationsFeedModel {
     return itemModels
   }
 
-  _setQueued(queued: undefined | ListNotifications.Notification[]) {
+  async _processNotifications(
+    items: ListNotifications.Notification[],
+  ): Promise<NotificationsFeedItemModel[]> {
+    const itemModels = await this._fetchItemModels(groupNotifications(items))
+    return this._filterNotifications(itemModels)
+  }
+
+  _setQueued(queued: undefined | NotificationsFeedItemModel[]) {
     this.queuedNotifications = queued
   }
 
diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts
index 38faf658a..58167284d 100644
--- a/src/state/models/feeds/posts.ts
+++ b/src/state/models/feeds/posts.ts
@@ -72,6 +72,17 @@ export class PostsFeedItemModel {
     makeAutoObservable(this, {rootStore: false})
   }
 
+  get rootUri(): string {
+    if (this.reply?.root.uri) {
+      return this.reply.root.uri
+    }
+    return this.post.uri
+  }
+
+  get isThreadMuted() {
+    return this.rootStore.mutedThreads.uris.has(this.rootUri)
+  }
+
   copy(v: FeedViewPost) {
     this.post = v.post
     this.reply = v.reply
@@ -145,6 +156,14 @@ export class PostsFeedItemModel {
     }
   }
 
+  async toggleThreadMute() {
+    if (this.isThreadMuted) {
+      this.rootStore.mutedThreads.uris.delete(this.rootUri)
+    } else {
+      this.rootStore.mutedThreads.uris.add(this.rootUri)
+    }
+  }
+
   async delete() {
     await this.rootStore.agent.deletePost(this.post.uri)
     this.rootStore.emitPostDeleted(this.post.uri)
diff --git a/src/state/models/muted-threads.ts b/src/state/models/muted-threads.ts
new file mode 100644
index 000000000..e6f202745
--- /dev/null
+++ b/src/state/models/muted-threads.ts
@@ -0,0 +1,29 @@
+/**
+ * This is a temporary client-side system for storing muted threads
+ * When the system lands on prod we should switch to that
+ */
+
+import {makeAutoObservable} from 'mobx'
+import {isObj, hasProp, isStrArray} from 'lib/type-guards'
+
+export class MutedThreads {
+  uris: Set<string> = new Set()
+
+  constructor() {
+    makeAutoObservable(
+      this,
+      {serialize: false, hydrate: false},
+      {autoBind: true},
+    )
+  }
+
+  serialize() {
+    return {uris: Array.from(this.uris)}
+  }
+
+  hydrate(v: unknown) {
+    if (isObj(v) && hasProp(v, 'uris') && isStrArray(v.uris)) {
+      this.uris = new Set(v.uris)
+    }
+  }
+}
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 9207f27ba..b3e744a40 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -20,6 +20,7 @@ import {InvitedUsers} from './invited-users'
 import {PreferencesModel} from './ui/preferences'
 import {resetToTab} from '../../Navigation'
 import {ImageSizesCache} from './cache/image-sizes'
+import {MutedThreads} from './muted-threads'
 
 export const appInfo = z.object({
   build: z.string(),
@@ -41,6 +42,7 @@ export class RootStoreModel {
   profiles = new ProfilesCache(this)
   linkMetas = new LinkMetasCache(this)
   imageSizes = new ImageSizesCache()
+  mutedThreads = new MutedThreads()
 
   constructor(agent: BskyAgent) {
     this.agent = agent
@@ -64,6 +66,7 @@ export class RootStoreModel {
       shell: this.shell.serialize(),
       preferences: this.preferences.serialize(),
       invitedUsers: this.invitedUsers.serialize(),
+      mutedThreads: this.mutedThreads.serialize(),
     }
   }
 
@@ -90,6 +93,9 @@ export class RootStoreModel {
       if (hasProp(v, 'invitedUsers')) {
         this.invitedUsers.hydrate(v.invitedUsers)
       }
+      if (hasProp(v, 'mutedThreads')) {
+        this.mutedThreads.hydrate(v.mutedThreads)
+      }
     }
   }