[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
This commit is contained in:
parent
34d8fa5991
commit
ebcd633386
48 changed files with 2984 additions and 151 deletions
214
src/state/models/lists/lists-list.ts
Normal file
214
src/state/models/lists/lists-list.ts
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue