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.ts702
1 files changed, 0 insertions, 702 deletions
diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts
deleted file mode 100644
index 6e43198a3..000000000
--- a/src/state/models/ui/preferences.ts
+++ /dev/null
@@ -1,702 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {
-  LabelPreference as APILabelPreference,
-  BskyFeedViewPreference,
-  BskyThreadViewPreference,
-} from '@atproto/api'
-import AwaitLock from 'await-lock'
-import isEqual from 'lodash.isequal'
-import {isObj, hasProp} from 'lib/type-guards'
-import {RootStoreModel} from '../root-store'
-import {ModerationOpts} from '@atproto/api'
-import {DEFAULT_FEEDS} from 'lib/constants'
-import {deviceLocales} from 'platform/detection'
-import {getAge} from 'lib/strings/time'
-import {FeedTuner} from 'lib/api/feed-manip'
-import {LANGUAGES} from '../../../locale/languages'
-import {logger} from '#/logger'
-
-// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
-export type LabelPreference = APILabelPreference | 'show'
-export type FeedViewPreference = BskyFeedViewPreference & {
-  lab_mergeFeedEnabled?: boolean | undefined
-}
-export type ThreadViewPreference = BskyThreadViewPreference & {
-  lab_treeViewEnabled?: boolean | undefined
-}
-const LABEL_GROUPS = [
-  'nsfw',
-  'nudity',
-  'suggestive',
-  'gore',
-  'hate',
-  'spam',
-  'impersonation',
-]
-const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
-const DEFAULT_LANG_CODES = (deviceLocales || [])
-  .concat(['en', 'ja', 'pt', 'de'])
-  .slice(0, 6)
-const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random']
-
-interface LegacyPreferences {
-  hideReplies?: boolean
-  hideRepliesByLikeCount?: number
-  hideReposts?: boolean
-  hideQuotePosts?: boolean
-}
-
-export class LabelPreferencesModel {
-  nsfw: LabelPreference = 'hide'
-  nudity: LabelPreference = 'warn'
-  suggestive: LabelPreference = 'warn'
-  gore: LabelPreference = 'warn'
-  hate: LabelPreference = 'hide'
-  spam: LabelPreference = 'hide'
-  impersonation: LabelPreference = 'warn'
-
-  constructor() {
-    makeAutoObservable(this, {}, {autoBind: true})
-  }
-}
-
-export class PreferencesModel {
-  adultContentEnabled = false
-  primaryLanguage: string = deviceLocales[0] || 'en'
-  contentLanguages: string[] = deviceLocales || []
-  postLanguage: string = deviceLocales[0] || 'en'
-  postLanguageHistory: string[] = DEFAULT_LANG_CODES
-  contentLabels = new LabelPreferencesModel()
-  savedFeeds: string[] = []
-  pinnedFeeds: string[] = []
-  birthDate: Date | undefined = undefined
-  homeFeed: FeedViewPreference = {
-    hideReplies: false,
-    hideRepliesByUnfollowed: false,
-    hideRepliesByLikeCount: 0,
-    hideReposts: false,
-    hideQuotePosts: false,
-    lab_mergeFeedEnabled: false, // experimental
-  }
-  thread: ThreadViewPreference = {
-    sort: 'oldest',
-    prioritizeFollowedUsers: true,
-    lab_treeViewEnabled: false, // experimental
-  }
-  requireAltTextEnabled: boolean = false
-
-  // used to help with transitions from device-stored to server-stored preferences
-  legacyPreferences: LegacyPreferences | undefined
-
-  // used to linearize async modifications to state
-  lock = new AwaitLock()
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(this, {lock: false}, {autoBind: true})
-  }
-
-  get userAge(): number | undefined {
-    if (!this.birthDate) {
-      return undefined
-    }
-    return getAge(this.birthDate)
-  }
-
-  serialize() {
-    return {
-      primaryLanguage: this.primaryLanguage,
-      contentLanguages: this.contentLanguages,
-      postLanguage: this.postLanguage,
-      postLanguageHistory: this.postLanguageHistory,
-      contentLabels: this.contentLabels,
-      savedFeeds: this.savedFeeds,
-      pinnedFeeds: this.pinnedFeeds,
-      requireAltTextEnabled: this.requireAltTextEnabled,
-    }
-  }
-
-  /**
-   * 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 (
-        hasProp(v, 'primaryLanguage') &&
-        typeof v.primaryLanguage === 'string'
-      ) {
-        this.primaryLanguage = v.primaryLanguage
-      } else {
-        // default to the device languages
-        this.primaryLanguage = deviceLocales[0] || 'en'
-      }
-      // check if content languages in preferences exist, otherwise default to device languages
-      if (
-        hasProp(v, 'contentLanguages') &&
-        Array.isArray(v.contentLanguages) &&
-        typeof v.contentLanguages.every(item => typeof item === 'string')
-      ) {
-        this.contentLanguages = v.contentLanguages
-      } else {
-        // default to the device languages
-        this.contentLanguages = deviceLocales
-      }
-      if (hasProp(v, 'postLanguage') && typeof v.postLanguage === 'string') {
-        this.postLanguage = v.postLanguage
-      } else {
-        // default to the device languages
-        this.postLanguage = deviceLocales[0] || 'en'
-      }
-      if (
-        hasProp(v, 'postLanguageHistory') &&
-        Array.isArray(v.postLanguageHistory) &&
-        typeof v.postLanguageHistory.every(item => typeof item === 'string')
-      ) {
-        this.postLanguageHistory = v.postLanguageHistory
-          .concat(DEFAULT_LANG_CODES)
-          .slice(0, 6)
-      } else {
-        // default to a starter set
-        this.postLanguageHistory = DEFAULT_LANG_CODES
-      }
-      // check if content labels in preferences exist, then hydrate
-      if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
-        Object.assign(this.contentLabels, v.contentLabels)
-      }
-      // check if saved feeds in preferences, then hydrate
-      if (
-        hasProp(v, 'savedFeeds') &&
-        Array.isArray(v.savedFeeds) &&
-        typeof v.savedFeeds.every(item => typeof item === 'string')
-      ) {
-        this.savedFeeds = v.savedFeeds
-      }
-      // check if pinned feeds in preferences exist, then hydrate
-      if (
-        hasProp(v, 'pinnedFeeds') &&
-        Array.isArray(v.pinnedFeeds) &&
-        typeof v.pinnedFeeds.every(item => typeof item === 'string')
-      ) {
-        this.pinnedFeeds = v.pinnedFeeds
-      }
-      // check if requiring alt text is enabled in preferences, then hydrate
-      if (
-        hasProp(v, 'requireAltTextEnabled') &&
-        typeof v.requireAltTextEnabled === 'boolean'
-      ) {
-        this.requireAltTextEnabled = v.requireAltTextEnabled
-      }
-      // grab legacy values
-      this.legacyPreferences = getLegacyPreferences(v)
-    }
-  }
-
-  /**
-   * This function fetches preferences and sets defaults for missing items.
-   */
-  async sync() {
-    await this.lock.acquireAsync()
-    try {
-      // fetch preferences
-      const prefs = await this.rootStore.agent.getPreferences()
-
-      runInAction(() => {
-        if (prefs.feedViewPrefs.home) {
-          this.homeFeed = prefs.feedViewPrefs.home
-        }
-        this.thread = prefs.threadViewPrefs
-        this.adultContentEnabled = prefs.adultContentEnabled
-        for (const label in prefs.contentLabels) {
-          if (
-            LABEL_GROUPS.includes(label) &&
-            VISIBILITY_VALUES.includes(prefs.contentLabels[label])
-          ) {
-            this.contentLabels[label as keyof LabelPreferencesModel] =
-              prefs.contentLabels[label]
-          }
-        }
-        if (prefs.feeds.saved && !isEqual(this.savedFeeds, prefs.feeds.saved)) {
-          this.savedFeeds = prefs.feeds.saved
-        }
-        if (
-          prefs.feeds.pinned &&
-          !isEqual(this.pinnedFeeds, prefs.feeds.pinned)
-        ) {
-          this.pinnedFeeds = prefs.feeds.pinned
-        }
-        this.birthDate = prefs.birthDate
-      })
-
-      // sync legacy values if needed
-      await this.syncLegacyPreferences()
-
-      // set defaults on missing items
-      if (typeof prefs.feeds.saved === 'undefined') {
-        try {
-          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
-          })
-          await this.rootStore.agent.setSavedFeeds(saved, pinned)
-        } catch (error) {
-          logger.error('Failed to set default feeds', {error})
-        }
-      }
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async syncLegacyPreferences() {
-    if (this.legacyPreferences) {
-      this.homeFeed = {...this.homeFeed, ...this.legacyPreferences}
-      this.legacyPreferences = undefined
-      await this.rootStore.agent.setFeedViewPrefs('home', this.homeFeed)
-    }
-  }
-
-  /**
-   * This function resets the preferences to an empty array of no preferences.
-   */
-  async reset() {
-    await this.lock.acquireAsync()
-    try {
-      runInAction(() => {
-        this.contentLabels = new LabelPreferencesModel()
-        this.contentLanguages = deviceLocales
-        this.postLanguage = deviceLocales ? deviceLocales.join(',') : 'en'
-        this.postLanguageHistory = DEFAULT_LANG_CODES
-        this.savedFeeds = []
-        this.pinnedFeeds = []
-      })
-      await this.rootStore.agent.app.bsky.actor.putPreferences({
-        preferences: [],
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  // languages
-  // =
-
-  hasContentLanguage(code2: string) {
-    return this.contentLanguages.includes(code2)
-  }
-
-  toggleContentLanguage(code2: string) {
-    if (this.hasContentLanguage(code2)) {
-      this.contentLanguages = this.contentLanguages.filter(
-        lang => lang !== code2,
-      )
-    } else {
-      this.contentLanguages = this.contentLanguages.concat([code2])
-    }
-  }
-
-  /**
-   * A getter that splits `this.postLanguage` into an array of strings.
-   *
-   * This was previously the main field on this model, but now we're
-   * concatenating lang codes to make multi-selection a little better.
-   */
-  get postLanguages() {
-    // filter out empty strings if exist
-    return this.postLanguage.split(',').filter(Boolean)
-  }
-
-  hasPostLanguage(code2: string) {
-    return this.postLanguages.includes(code2)
-  }
-
-  togglePostLanguage(code2: string) {
-    if (this.hasPostLanguage(code2)) {
-      this.postLanguage = this.postLanguages
-        .filter(lang => lang !== code2)
-        .join(',')
-    } else {
-      // sort alphabetically for deterministic comparison in context menu
-      this.postLanguage = this.postLanguages
-        .concat([code2])
-        .sort((a, b) => a.localeCompare(b))
-        .join(',')
-    }
-  }
-
-  setPostLanguage(commaSeparatedLangCodes: string) {
-    this.postLanguage = commaSeparatedLangCodes
-  }
-
-  /**
-   * Saves whatever language codes are currently selected into a history array,
-   * which is then used to populate the language selector menu.
-   */
-  savePostLanguageToHistory() {
-    // filter out duplicate `this.postLanguage` if exists, and prepend
-    // value to start of array
-    this.postLanguageHistory = [this.postLanguage]
-      .concat(
-        this.postLanguageHistory.filter(
-          commaSeparatedLangCodes =>
-            commaSeparatedLangCodes !== this.postLanguage,
-        ),
-      )
-      .slice(0, 6)
-  }
-
-  getReadablePostLanguages() {
-    const all = this.postLanguages.map(code2 => {
-      const lang = LANGUAGES.find(l => l.code2 === code2)
-      return lang ? lang.name : code2
-    })
-    return all.join(', ')
-  }
-
-  // moderation
-  // =
-
-  async setContentLabelPref(
-    key: keyof LabelPreferencesModel,
-    value: LabelPreference,
-  ) {
-    this.contentLabels[key] = value
-    await this.rootStore.agent.setContentLabelPref(key, value)
-  }
-
-  async setAdultContentEnabled(v: boolean) {
-    this.adultContentEnabled = v
-    await this.rootStore.agent.setAdultContentEnabled(v)
-  }
-
-  get moderationOpts(): ModerationOpts {
-    return {
-      userDid: this.rootStore.session.currentSession?.did || '',
-      adultContentEnabled: this.adultContentEnabled,
-      labels: {
-        // TEMP translate old settings until this UI can be migrated -prf
-        porn: tempfixLabelPref(this.contentLabels.nsfw),
-        sexual: tempfixLabelPref(this.contentLabels.suggestive),
-        nudity: tempfixLabelPref(this.contentLabels.nudity),
-        nsfl: tempfixLabelPref(this.contentLabels.gore),
-        corpse: tempfixLabelPref(this.contentLabels.gore),
-        gore: tempfixLabelPref(this.contentLabels.gore),
-        torture: tempfixLabelPref(this.contentLabels.gore),
-        'self-harm': tempfixLabelPref(this.contentLabels.gore),
-        'intolerant-race': tempfixLabelPref(this.contentLabels.hate),
-        'intolerant-gender': tempfixLabelPref(this.contentLabels.hate),
-        'intolerant-sexual-orientation': tempfixLabelPref(
-          this.contentLabels.hate,
-        ),
-        'intolerant-religion': tempfixLabelPref(this.contentLabels.hate),
-        intolerant: tempfixLabelPref(this.contentLabels.hate),
-        'icon-intolerant': tempfixLabelPref(this.contentLabels.hate),
-        spam: tempfixLabelPref(this.contentLabels.spam),
-        impersonation: tempfixLabelPref(this.contentLabels.impersonation),
-        scam: 'warn',
-      },
-      labelers: [
-        {
-          labeler: {
-            did: '',
-            displayName: 'Bluesky Social',
-          },
-          labels: {},
-        },
-      ],
-    }
-  }
-
-  // feeds
-  // =
-
-  isPinnedFeed(uri: string) {
-    return this.pinnedFeeds.includes(uri)
-  }
-
-  async _optimisticUpdateSavedFeeds(
-    saved: string[],
-    pinned: string[],
-    cb: () => Promise<{saved: string[]; pinned: string[]}>,
-  ) {
-    const oldSaved = this.savedFeeds
-    const oldPinned = this.pinnedFeeds
-    this.savedFeeds = saved
-    this.pinnedFeeds = pinned
-    await this.lock.acquireAsync()
-    try {
-      const res = await cb()
-      runInAction(() => {
-        this.savedFeeds = res.saved
-        this.pinnedFeeds = res.pinned
-      })
-    } catch (e) {
-      runInAction(() => {
-        this.savedFeeds = oldSaved
-        this.pinnedFeeds = oldPinned
-      })
-      throw e
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async setSavedFeeds(saved: string[], pinned: string[]) {
-    return this._optimisticUpdateSavedFeeds(saved, pinned, () =>
-      this.rootStore.agent.setSavedFeeds(saved, pinned),
-    )
-  }
-
-  async addSavedFeed(v: string) {
-    return this._optimisticUpdateSavedFeeds(
-      [...this.savedFeeds.filter(uri => uri !== v), v],
-      this.pinnedFeeds,
-      () => this.rootStore.agent.addSavedFeed(v),
-    )
-  }
-
-  async removeSavedFeed(v: string) {
-    return this._optimisticUpdateSavedFeeds(
-      this.savedFeeds.filter(uri => uri !== v),
-      this.pinnedFeeds.filter(uri => uri !== v),
-      () => this.rootStore.agent.removeSavedFeed(v),
-    )
-  }
-
-  async addPinnedFeed(v: string) {
-    return this._optimisticUpdateSavedFeeds(
-      [...this.savedFeeds.filter(uri => uri !== v), v],
-      [...this.pinnedFeeds.filter(uri => uri !== v), v],
-      () => this.rootStore.agent.addPinnedFeed(v),
-    )
-  }
-
-  async removePinnedFeed(v: string) {
-    return this._optimisticUpdateSavedFeeds(
-      this.savedFeeds,
-      this.pinnedFeeds.filter(uri => uri !== v),
-      () => this.rootStore.agent.removePinnedFeed(v),
-    )
-  }
-
-  // other
-  // =
-
-  async setBirthDate(birthDate: Date) {
-    this.birthDate = birthDate
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setPersonalDetails({birthDate})
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideReplies() {
-    this.homeFeed.hideReplies = !this.homeFeed.hideReplies
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideReplies: this.homeFeed.hideReplies,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideRepliesByUnfollowed() {
-    this.homeFeed.hideRepliesByUnfollowed =
-      !this.homeFeed.hideRepliesByUnfollowed
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideRepliesByUnfollowed: this.homeFeed.hideRepliesByUnfollowed,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async setHomeFeedHideRepliesByLikeCount(threshold: number) {
-    this.homeFeed.hideRepliesByLikeCount = threshold
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideRepliesByLikeCount: this.homeFeed.hideRepliesByLikeCount,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideReposts() {
-    this.homeFeed.hideReposts = !this.homeFeed.hideReposts
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideReposts: this.homeFeed.hideReposts,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedHideQuotePosts() {
-    this.homeFeed.hideQuotePosts = !this.homeFeed.hideQuotePosts
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        hideQuotePosts: this.homeFeed.hideQuotePosts,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleHomeFeedMergeFeedEnabled() {
-    this.homeFeed.lab_mergeFeedEnabled = !this.homeFeed.lab_mergeFeedEnabled
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setFeedViewPrefs('home', {
-        lab_mergeFeedEnabled: this.homeFeed.lab_mergeFeedEnabled,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async setThreadSort(v: string) {
-    if (THREAD_SORT_VALUES.includes(v)) {
-      this.thread.sort = v
-      await this.lock.acquireAsync()
-      try {
-        await this.rootStore.agent.setThreadViewPrefs({sort: v})
-      } finally {
-        this.lock.release()
-      }
-    }
-  }
-
-  async togglePrioritizedFollowedUsers() {
-    this.thread.prioritizeFollowedUsers = !this.thread.prioritizeFollowedUsers
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setThreadViewPrefs({
-        prioritizeFollowedUsers: this.thread.prioritizeFollowedUsers,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  async toggleThreadTreeViewEnabled() {
-    this.thread.lab_treeViewEnabled = !this.thread.lab_treeViewEnabled
-    await this.lock.acquireAsync()
-    try {
-      await this.rootStore.agent.setThreadViewPrefs({
-        lab_treeViewEnabled: this.thread.lab_treeViewEnabled,
-      })
-    } finally {
-      this.lock.release()
-    }
-  }
-
-  toggleRequireAltTextEnabled() {
-    this.requireAltTextEnabled = !this.requireAltTextEnabled
-  }
-
-  setPrimaryLanguage(lang: string) {
-    this.primaryLanguage = lang
-  }
-
-  getFeedTuners(
-    feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes',
-  ) {
-    if (feedType === 'custom') {
-      return [
-        FeedTuner.dedupReposts,
-        FeedTuner.preferredLangOnly(this.contentLanguages),
-      ]
-    }
-    if (feedType === 'list') {
-      return [FeedTuner.dedupReposts]
-    }
-    if (feedType === 'home' || feedType === 'following') {
-      const feedTuners = []
-
-      if (this.homeFeed.hideReposts) {
-        feedTuners.push(FeedTuner.removeReposts)
-      } else {
-        feedTuners.push(FeedTuner.dedupReposts)
-      }
-
-      if (this.homeFeed.hideReplies) {
-        feedTuners.push(FeedTuner.removeReplies)
-      } else {
-        feedTuners.push(
-          FeedTuner.thresholdRepliesOnly({
-            userDid: this.rootStore.session.data?.did || '',
-            minLikes: this.homeFeed.hideRepliesByLikeCount,
-            followedOnly: !!this.homeFeed.hideRepliesByUnfollowed,
-          }),
-        )
-      }
-
-      if (this.homeFeed.hideQuotePosts) {
-        feedTuners.push(FeedTuner.removeQuotePosts)
-      }
-
-      return feedTuners
-    }
-    return []
-  }
-}
-
-// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
-function tempfixLabelPref(pref: LabelPreference): APILabelPreference {
-  if (pref === 'show') {
-    return 'ignore'
-  }
-  return pref
-}
-
-function getLegacyPreferences(
-  v: Record<string, unknown>,
-): LegacyPreferences | undefined {
-  const legacyPreferences: LegacyPreferences = {}
-  if (
-    hasProp(v, 'homeFeedRepliesEnabled') &&
-    typeof v.homeFeedRepliesEnabled === 'boolean'
-  ) {
-    legacyPreferences.hideReplies = !v.homeFeedRepliesEnabled
-  }
-  if (
-    hasProp(v, 'homeFeedRepliesThreshold') &&
-    typeof v.homeFeedRepliesThreshold === 'number'
-  ) {
-    legacyPreferences.hideRepliesByLikeCount = v.homeFeedRepliesThreshold
-  }
-  if (
-    hasProp(v, 'homeFeedRepostsEnabled') &&
-    typeof v.homeFeedRepostsEnabled === 'boolean'
-  ) {
-    legacyPreferences.hideReposts = !v.homeFeedRepostsEnabled
-  }
-  if (
-    hasProp(v, 'homeFeedQuotePostsEnabled') &&
-    typeof v.homeFeedQuotePostsEnabled === 'boolean'
-  ) {
-    legacyPreferences.hideQuotePosts = !v.homeFeedQuotePostsEnabled
-  }
-  if (Object.keys(legacyPreferences).length) {
-    return legacyPreferences
-  }
-  return undefined
-}