about summary refs log tree commit diff
path: root/src/state/models/ui/preferences.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/models/ui/preferences.ts')
-rw-r--r--src/state/models/ui/preferences.ts157
1 files changed, 153 insertions, 4 deletions
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
index 1471420fc..dcf6b9a7a 100644
--- a/src/state/models/ui/preferences.ts
+++ b/src/state/models/ui/preferences.ts
@@ -11,6 +11,7 @@ import {
   ALWAYS_FILTER_LABEL_GROUP,
   ALWAYS_WARN_LABEL_GROUP,
 } from 'lib/labeling/const'
+import {DEFAULT_FEEDS} from 'lib/constants'
 import {isIOS} from 'platform/detection'
 
 const deviceLocales = getLocales()
@@ -25,6 +26,7 @@ const LABEL_GROUPS = [
   'spam',
   'impersonation',
 ]
+const VISIBILITY_VALUES = ['show', 'warn', 'hide']
 
 export class LabelPreferencesModel {
   nsfw: LabelPreference = 'hide'
@@ -45,6 +47,8 @@ export class PreferencesModel {
   contentLanguages: string[] =
     deviceLocales?.map?.(locale => locale.languageCode) || []
   contentLabels = new LabelPreferencesModel()
+  savedFeeds: string[] = []
+  pinnedFeeds: string[] = []
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {}, {autoBind: true})
@@ -54,9 +58,16 @@ export class PreferencesModel {
     return {
       contentLanguages: this.contentLanguages,
       contentLabels: this.contentLabels,
+      savedFeeds: this.savedFeeds,
+      pinnedFeeds: this.pinnedFeeds,
     }
   }
 
+  /**
+   * The function hydrates an object with properties related to content languages, labels, saved feeds,
+   * and pinned feeds that it gets from the parameter `v` (probably local storage)
+   * @param {unknown} v - the data object to hydrate from
+   */
   hydrate(v: unknown) {
     if (isObj(v)) {
       if (
@@ -72,10 +83,29 @@ export class PreferencesModel {
         // default to the device languages
         this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
       }
+      if (
+        hasProp(v, 'savedFeeds') &&
+        Array.isArray(v.savedFeeds) &&
+        typeof v.savedFeeds.every(item => typeof item === 'string')
+      ) {
+        this.savedFeeds = v.savedFeeds
+      }
+      if (
+        hasProp(v, 'pinnedFeeds') &&
+        Array.isArray(v.pinnedFeeds) &&
+        typeof v.pinnedFeeds.every(item => typeof item === 'string')
+      ) {
+        this.pinnedFeeds = v.pinnedFeeds
+      }
     }
   }
 
+  /**
+   * This function fetches preferences and sets defaults for missing items.
+   */
   async sync() {
+    // fetch preferences
+    let hasSavedFeedsPref = false
     const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
     runInAction(() => {
       for (const pref of res.data.preferences) {
@@ -88,22 +118,83 @@ export class PreferencesModel {
           AppBskyActorDefs.isContentLabelPref(pref) &&
           AppBskyActorDefs.validateAdultContentPref(pref).success
         ) {
-          if (LABEL_GROUPS.includes(pref.label)) {
-            this.contentLabels[pref.label] = pref.visibility
+          if (
+            LABEL_GROUPS.includes(pref.label) &&
+            VISIBILITY_VALUES.includes(pref.visibility)
+          ) {
+            this.contentLabels[pref.label as keyof LabelPreferencesModel] =
+              pref.visibility as LabelPreference
           }
+        } else if (
+          AppBskyActorDefs.isSavedFeedsPref(pref) &&
+          AppBskyActorDefs.validateSavedFeedsPref(pref).success
+        ) {
+          this.savedFeeds = pref.saved
+          this.pinnedFeeds = pref.pinned
+          hasSavedFeedsPref = true
         }
       }
     })
+
+    // set defaults on missing items
+    if (!hasSavedFeedsPref) {
+      const {saved, pinned} = await DEFAULT_FEEDS(
+        this.rootStore.agent.service.toString(),
+        (handle: string) =>
+          this.rootStore.agent
+            .resolveHandle({handle})
+            .then(({data}) => data.did),
+      )
+      runInAction(() => {
+        this.savedFeeds = saved
+        this.pinnedFeeds = pinned
+      })
+      res.data.preferences.push({
+        $type: 'app.bsky.actor.defs#savedFeedsPref',
+        saved,
+        pinned,
+      })
+      await this.rootStore.agent.app.bsky.actor.putPreferences({
+        preferences: res.data.preferences,
+      })
+      /* dont await */ this.rootStore.me.savedFeeds.refresh()
+    }
   }
 
-  async update(cb: (prefs: AppBskyActorDefs.Preferences) => void) {
+  /**
+   * This function updates the preferences of a user and allows for a callback function to be executed
+   * before the update.
+   * @param cb - cb is a callback function that takes in a single parameter of type
+   * AppBskyActorDefs.Preferences and returns either a boolean or void. This callback function is used to
+   * update the preferences of the user. The function is called with the current preferences as an
+   * argument and if the callback returns false, the preferences are not updated.
+   * @returns void
+   */
+  async update(cb: (prefs: AppBskyActorDefs.Preferences) => boolean | void) {
     const res = await this.rootStore.agent.app.bsky.actor.getPreferences({})
-    cb(res.data.preferences)
+    if (cb(res.data.preferences) === false) {
+      return
+    }
     await this.rootStore.agent.app.bsky.actor.putPreferences({
       preferences: res.data.preferences,
     })
   }
 
+  /**
+   * This function resets the preferences to an empty array of no preferences.
+   */
+  async reset() {
+    runInAction(() => {
+      this.contentLabels = new LabelPreferencesModel()
+      this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
+      this.savedFeeds = []
+      this.pinnedFeeds = []
+    })
+    await this.rootStore.agent.app.bsky.actor.putPreferences({
+      preferences: [],
+    })
+  }
+
   hasContentLanguage(code2: string) {
     return this.contentLanguages.includes(code2)
   }
@@ -200,4 +291,62 @@ export class PreferencesModel {
     }
     return res
   }
+
+  setFeeds(saved: string[], pinned: string[]) {
+    this.savedFeeds = saved
+    this.pinnedFeeds = pinned
+  }
+
+  async setSavedFeeds(saved: string[], pinned: string[]) {
+    const oldSaved = this.savedFeeds
+    const oldPinned = this.pinnedFeeds
+    this.setFeeds(saved, pinned)
+    try {
+      await this.update((prefs: AppBskyActorDefs.Preferences) => {
+        const existing = prefs.find(
+          pref =>
+            AppBskyActorDefs.isSavedFeedsPref(pref) &&
+            AppBskyActorDefs.validateSavedFeedsPref(pref).success,
+        )
+        if (existing) {
+          existing.saved = saved
+          existing.pinned = pinned
+        } else {
+          prefs.push({
+            $type: 'app.bsky.actor.defs#savedFeedsPref',
+            saved,
+            pinned,
+          })
+        }
+      })
+    } catch (e) {
+      runInAction(() => {
+        this.savedFeeds = oldSaved
+        this.pinnedFeeds = oldPinned
+      })
+      throw e
+    }
+  }
+
+  async addSavedFeed(v: string) {
+    return this.setSavedFeeds([...this.savedFeeds, v], this.pinnedFeeds)
+  }
+
+  async removeSavedFeed(v: string) {
+    return this.setSavedFeeds(
+      this.savedFeeds.filter(uri => uri !== v),
+      this.pinnedFeeds.filter(uri => uri !== v),
+    )
+  }
+
+  async addPinnedFeed(v: string) {
+    return this.setSavedFeeds(this.savedFeeds, [...this.pinnedFeeds, v])
+  }
+
+  async removePinnedFeed(v: string) {
+    return this.setSavedFeeds(
+      this.savedFeeds,
+      this.pinnedFeeds.filter(uri => uri !== v),
+    )
+  }
 }