about summary refs log tree commit diff
path: root/src/state/models/lists/lists-list.ts
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-05-11 16:08:21 -0500
committerGitHub <noreply@github.com>2023-05-11 16:08:21 -0500
commitebcd6333863a2073278fad482981d9898c0f20ca (patch)
tree9417a5c282fc6ce22af2251f437f02b0700c7714 /src/state/models/lists/lists-list.ts
parent34d8fa59916d87922c83a6cf93e3e288d43dadcc (diff)
downloadvoidsky-ebcd6333863a2073278fad482981d9898c0f20ca.tar.zst
[APP-635] Mutelists (#601)
* Add lists and profilelist screens

* Implement lists screen and lists-list in profiles

* Add empty states to the lists screen

* Switch (mostly) from blocklists to mutelists

* Rework: create a new moderation screen and move everything related under it

* Fix moderation screen on desktop web

* Tune the empty state code

* Change content moderation modal to content filtering

* Add CreateMuteList modal

* Implement mutelist creation

* Add lists listings

* Add the ability to create new mutelists

* Add 'add to list' tool

* Satisfy the hashtag hyphen haters

* Add update/delete/subscribe/unsubscribe to lists

* Show which list caused a mute

* Add list un/subscribe

* Add the mute override when viewing a profile's posts

* Update to latest backend

* Add simulation tests and tune some behaviors

* Fix lint

* Bump deps

* Fix list refresh after creation

* Mute list subscriptions -> Mute lists
Diffstat (limited to 'src/state/models/lists/lists-list.ts')
-rw-r--r--src/state/models/lists/lists-list.ts214
1 files changed, 214 insertions, 0 deletions
diff --git a/src/state/models/lists/lists-list.ts b/src/state/models/lists/lists-list.ts
new file mode 100644
index 000000000..309ab0e03
--- /dev/null
+++ b/src/state/models/lists/lists-list.ts
@@ -0,0 +1,214 @@
+import {makeAutoObservable} from 'mobx'
+import {
+  AppBskyGraphGetLists as GetLists,
+  AppBskyGraphGetListMutes as GetListMutes,
+  AppBskyGraphDefs as GraphDefs,
+} from '@atproto/api'
+import {RootStoreModel} from '../root-store'
+import {cleanError} from 'lib/strings/errors'
+import {bundleAsync} from 'lib/async/bundle'
+
+const PAGE_SIZE = 30
+
+export class ListsListModel {
+  // state
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
+  error = ''
+  loadMoreError = ''
+  hasMore = true
+  loadMoreCursor?: string
+
+  // data
+  lists: GraphDefs.ListView[] = []
+
+  constructor(
+    public rootStore: RootStoreModel,
+    public source: 'my-modlists' | string,
+  ) {
+    makeAutoObservable(
+      this,
+      {
+        rootStore: false,
+      },
+      {autoBind: true},
+    )
+  }
+
+  get hasContent() {
+    return this.lists.length > 0
+  }
+
+  get hasError() {
+    return this.error !== ''
+  }
+
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
+  }
+
+  // public api
+  // =
+
+  async refresh() {
+    return this.loadMore(true)
+  }
+
+  loadMore = bundleAsync(async (replace: boolean = false) => {
+    if (!replace && !this.hasMore) {
+      return
+    }
+    this._xLoading(replace)
+    try {
+      let res
+      if (this.source === 'my-modlists') {
+        res = {
+          success: true,
+          headers: {},
+          data: {
+            subject: undefined,
+            lists: [],
+          },
+        }
+        const [res1, res2] = await Promise.all([
+          fetchAllUserLists(this.rootStore, this.rootStore.me.did),
+          fetchAllMyMuteLists(this.rootStore),
+        ])
+        for (let list of res1.data.lists) {
+          if (list.purpose === 'app.bsky.graph.defs#modlist') {
+            res.data.lists.push(list)
+          }
+        }
+        for (let list of res2.data.lists) {
+          if (
+            list.purpose === 'app.bsky.graph.defs#modlist' &&
+            !res.data.lists.find(l => l.uri === list.uri)
+          ) {
+            res.data.lists.push(list)
+          }
+        }
+      } else {
+        res = await this.rootStore.agent.app.bsky.graph.getLists({
+          actor: this.source,
+          limit: PAGE_SIZE,
+          cursor: replace ? undefined : this.loadMoreCursor,
+        })
+      }
+      if (replace) {
+        this._replaceAll(res)
+      } else {
+        this._appendAll(res)
+      }
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(replace ? e : undefined, !replace ? e : undefined)
+    }
+  })
+
+  /**
+   * Attempt to load more again after a failure
+   */
+  async retryLoadMore() {
+    this.loadMoreError = ''
+    this.hasMore = true
+    return this.loadMore()
+  }
+
+  // state transitions
+  // =
+
+  _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
+    this.error = ''
+  }
+
+  _xIdle(err?: any, loadMoreErr?: any) {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = cleanError(err)
+    this.loadMoreError = cleanError(loadMoreErr)
+    if (err) {
+      this.rootStore.log.error('Failed to fetch user lists', err)
+    }
+    if (loadMoreErr) {
+      this.rootStore.log.error('Failed to fetch user lists', loadMoreErr)
+    }
+  }
+
+  // helper functions
+  // =
+
+  _replaceAll(res: GetLists.Response | GetListMutes.Response) {
+    this.lists = []
+    this._appendAll(res)
+  }
+
+  _appendAll(res: GetLists.Response | GetListMutes.Response) {
+    this.loadMoreCursor = res.data.cursor
+    this.hasMore = !!this.loadMoreCursor
+    this.lists = this.lists.concat(
+      res.data.lists.map(list => ({...list, _reactKey: list.uri})),
+    )
+  }
+}
+
+async function fetchAllUserLists(
+  store: RootStoreModel,
+  did: string,
+): Promise<GetLists.Response> {
+  let acc: GetLists.Response = {
+    success: true,
+    headers: {},
+    data: {
+      subject: undefined,
+      lists: [],
+    },
+  }
+
+  let cursor
+  for (let i = 0; i < 100; i++) {
+    const res = await store.agent.app.bsky.graph.getLists({
+      actor: did,
+      cursor,
+      limit: 50,
+    })
+    cursor = res.data.cursor
+    acc.data.lists = acc.data.lists.concat(res.data.lists)
+    if (!cursor) {
+      break
+    }
+  }
+
+  return acc
+}
+
+async function fetchAllMyMuteLists(
+  store: RootStoreModel,
+): Promise<GetListMutes.Response> {
+  let acc: GetListMutes.Response = {
+    success: true,
+    headers: {},
+    data: {
+      subject: undefined,
+      lists: [],
+    },
+  }
+
+  let cursor
+  for (let i = 0; i < 100; i++) {
+    const res = await store.agent.app.bsky.graph.getListMutes({
+      cursor,
+      limit: 50,
+    })
+    cursor = res.data.cursor
+    acc.data.lists = acc.data.lists.concat(res.data.lists)
+    if (!cursor) {
+      break
+    }
+  }
+
+  return acc
+}