Refactor lists to use new queries (#1875)
* Refactor lists queries to react-query * Delete old lists-list model * Implement list, list-members, and list-memberships react-queries * Update CreateOrEditList modal * First pass at my-follows and actor-autocomplete queries * Update ListAddUserModal to use new queries, change to ListAddRemoveUsersModal * Update UserAddRemoveLists modal * Remove old TODO * Fix indent, autocomplete query * Add a todo --------- Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
parent
05b728fffc
commit
d9e0a927c1
25 changed files with 1303 additions and 1545 deletions
|
|
@ -1,130 +0,0 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {AtUri, AppBskyGraphListitem} from '@atproto/api'
|
||||
import {runInAction} from 'mobx'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
|
||||
const PAGE_SIZE = 100
|
||||
interface Membership {
|
||||
uri: string
|
||||
value: AppBskyGraphListitem.Record
|
||||
}
|
||||
|
||||
interface ListitemRecord {
|
||||
uri: string
|
||||
value: AppBskyGraphListitem.Record
|
||||
}
|
||||
|
||||
interface ListitemListResponse {
|
||||
cursor?: string
|
||||
records: ListitemRecord[]
|
||||
}
|
||||
|
||||
export class ListMembershipModel {
|
||||
// data
|
||||
memberships: Membership[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel, public subject: string) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
async fetch() {
|
||||
// NOTE
|
||||
// this approach to determining list membership is too inefficient to work at any scale
|
||||
// it needs to be replaced with server side list membership queries
|
||||
// -prf
|
||||
let cursor
|
||||
let records: ListitemRecord[] = []
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const res: ListitemListResponse =
|
||||
await this.rootStore.agent.app.bsky.graph.listitem.list({
|
||||
repo: this.rootStore.me.did,
|
||||
cursor,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
records = records.concat(
|
||||
res.records.filter(record => record.value.subject === this.subject),
|
||||
)
|
||||
cursor = res.cursor
|
||||
if (!cursor) {
|
||||
break
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.memberships = records
|
||||
})
|
||||
}
|
||||
|
||||
getMembership(listUri: string) {
|
||||
return this.memberships.find(m => m.value.list === listUri)
|
||||
}
|
||||
|
||||
isMember(listUri: string) {
|
||||
return !!this.getMembership(listUri)
|
||||
}
|
||||
|
||||
async add(listUri: string) {
|
||||
if (this.isMember(listUri)) {
|
||||
return
|
||||
}
|
||||
const res = await this.rootStore.agent.app.bsky.graph.listitem.create(
|
||||
{
|
||||
repo: this.rootStore.me.did,
|
||||
},
|
||||
{
|
||||
subject: this.subject,
|
||||
list: listUri,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
const {rkey} = new AtUri(res.uri)
|
||||
const record = await this.rootStore.agent.app.bsky.graph.listitem.get({
|
||||
repo: this.rootStore.me.did,
|
||||
rkey,
|
||||
})
|
||||
runInAction(() => {
|
||||
this.memberships = this.memberships.concat([record])
|
||||
})
|
||||
}
|
||||
|
||||
async remove(listUri: string) {
|
||||
const membership = this.getMembership(listUri)
|
||||
if (!membership) {
|
||||
return
|
||||
}
|
||||
const {rkey} = new AtUri(membership.uri)
|
||||
await this.rootStore.agent.app.bsky.graph.listitem.delete({
|
||||
repo: this.rootStore.me.did,
|
||||
rkey,
|
||||
})
|
||||
runInAction(() => {
|
||||
this.memberships = this.memberships.filter(m => m.value.list !== listUri)
|
||||
})
|
||||
}
|
||||
|
||||
async updateTo(
|
||||
uris: string[],
|
||||
): Promise<{added: string[]; removed: string[]}> {
|
||||
const added = []
|
||||
const removed = []
|
||||
for (const uri of uris) {
|
||||
await this.add(uri)
|
||||
added.push(uri)
|
||||
}
|
||||
for (const membership of this.memberships) {
|
||||
if (!uris.includes(membership.value.list)) {
|
||||
await this.remove(membership.value.list)
|
||||
removed.push(membership.value.list)
|
||||
}
|
||||
}
|
||||
return {added, removed}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {
|
||||
AtUri,
|
||||
AppBskyActorDefs,
|
||||
AppBskyGraphGetList as GetList,
|
||||
AppBskyGraphDefs as GraphDefs,
|
||||
AppBskyGraphList,
|
||||
AppBskyGraphListitem,
|
||||
RichText,
|
||||
} from '@atproto/api'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import chunk from 'lodash.chunk'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import * as apilib from 'lib/api/index'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
import {track} from 'lib/analytics/analytics'
|
||||
import {until} from 'lib/async/until'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
|
||||
interface ListitemRecord {
|
||||
uri: string
|
||||
value: AppBskyGraphListitem.Record
|
||||
}
|
||||
|
||||
interface ListitemListResponse {
|
||||
cursor?: string
|
||||
records: ListitemRecord[]
|
||||
}
|
||||
|
||||
export class ListModel {
|
||||
// state
|
||||
isLoading = false
|
||||
isRefreshing = false
|
||||
hasLoaded = false
|
||||
error = ''
|
||||
loadMoreError = ''
|
||||
hasMore = true
|
||||
loadMoreCursor?: string
|
||||
|
||||
// data
|
||||
data: GraphDefs.ListView | null = null
|
||||
items: GraphDefs.ListItemView[] = []
|
||||
descriptionRT: RichText | null = null
|
||||
|
||||
static async createList(
|
||||
rootStore: RootStoreModel,
|
||||
{
|
||||
purpose,
|
||||
name,
|
||||
description,
|
||||
avatar,
|
||||
}: {
|
||||
purpose: string
|
||||
name: string
|
||||
description: string
|
||||
avatar: RNImage | null | undefined
|
||||
},
|
||||
) {
|
||||
if (
|
||||
purpose !== 'app.bsky.graph.defs#curatelist' &&
|
||||
purpose !== 'app.bsky.graph.defs#modlist'
|
||||
) {
|
||||
throw new Error('Invalid list purpose: must be curatelist or modlist')
|
||||
}
|
||||
const record: AppBskyGraphList.Record = {
|
||||
purpose,
|
||||
name,
|
||||
description,
|
||||
avatar: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
if (avatar) {
|
||||
const blobRes = await apilib.uploadBlob(
|
||||
rootStore,
|
||||
avatar.path,
|
||||
avatar.mime,
|
||||
)
|
||||
record.avatar = blobRes.data.blob
|
||||
}
|
||||
const res = await rootStore.agent.app.bsky.graph.list.create(
|
||||
{
|
||||
repo: rootStore.me.did,
|
||||
},
|
||||
record,
|
||||
)
|
||||
|
||||
// wait for the appview to update
|
||||
await until(
|
||||
5, // 5 tries
|
||||
1e3, // 1s delay between tries
|
||||
(v: GetList.Response, _e: any) => {
|
||||
return typeof v?.data?.list.uri === 'string'
|
||||
},
|
||||
() =>
|
||||
rootStore.agent.app.bsky.graph.getList({
|
||||
list: res.uri,
|
||||
limit: 1,
|
||||
}),
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
constructor(public rootStore: RootStoreModel, public uri: string) {
|
||||
makeAutoObservable(
|
||||
this,
|
||||
{
|
||||
rootStore: false,
|
||||
},
|
||||
{autoBind: true},
|
||||
)
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
return this.items.length > 0
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
return this.error !== ''
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.hasLoaded && !this.hasContent
|
||||
}
|
||||
|
||||
get isCuratelist() {
|
||||
return this.data?.purpose === 'app.bsky.graph.defs#curatelist'
|
||||
}
|
||||
|
||||
get isModlist() {
|
||||
return this.data?.purpose === 'app.bsky.graph.defs#modlist'
|
||||
}
|
||||
|
||||
get isOwner() {
|
||||
return this.data?.creator.did === this.rootStore.me.did
|
||||
}
|
||||
|
||||
get isBlocking() {
|
||||
return !!this.data?.viewer?.blocked
|
||||
}
|
||||
|
||||
get isMuting() {
|
||||
return !!this.data?.viewer?.muted
|
||||
}
|
||||
|
||||
get isPinned() {
|
||||
return this.rootStore.preferences.isPinnedFeed(this.uri)
|
||||
}
|
||||
|
||||
get creatorDid() {
|
||||
return this.data?.creator.did
|
||||
}
|
||||
|
||||
getMembership(did: string) {
|
||||
return this.items.find(item => item.subject.did === did)
|
||||
}
|
||||
|
||||
isMember(did: string) {
|
||||
return !!this.getMembership(did)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
async refresh() {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
await this._resolveUri()
|
||||
const res = await this.rootStore.agent.app.bsky.graph.getList({
|
||||
list: this.uri,
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
async loadAll() {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
if (!this.hasMore) {
|
||||
break
|
||||
}
|
||||
await this.loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
async updateMetadata({
|
||||
name,
|
||||
description,
|
||||
avatar,
|
||||
}: {
|
||||
name: string
|
||||
description: string
|
||||
avatar: RNImage | null | undefined
|
||||
}) {
|
||||
if (!this.data) {
|
||||
return
|
||||
}
|
||||
if (!this.isOwner) {
|
||||
throw new Error('Cannot edit this list')
|
||||
}
|
||||
await this._resolveUri()
|
||||
|
||||
// get the current record
|
||||
const {rkey} = new AtUri(this.uri)
|
||||
const {value: record} = await this.rootStore.agent.app.bsky.graph.list.get({
|
||||
repo: this.rootStore.me.did,
|
||||
rkey,
|
||||
})
|
||||
|
||||
// update the fields
|
||||
record.name = name
|
||||
record.description = description
|
||||
if (avatar) {
|
||||
const blobRes = await apilib.uploadBlob(
|
||||
this.rootStore,
|
||||
avatar.path,
|
||||
avatar.mime,
|
||||
)
|
||||
record.avatar = blobRes.data.blob
|
||||
} else if (avatar === null) {
|
||||
record.avatar = undefined
|
||||
}
|
||||
return await this.rootStore.agent.com.atproto.repo.putRecord({
|
||||
repo: this.rootStore.me.did,
|
||||
collection: 'app.bsky.graph.list',
|
||||
rkey,
|
||||
record,
|
||||
})
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (!this.data) {
|
||||
return
|
||||
}
|
||||
await this._resolveUri()
|
||||
|
||||
// fetch all the listitem records that belong to this list
|
||||
let cursor
|
||||
let records: ListitemRecord[] = []
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const res: ListitemListResponse =
|
||||
await this.rootStore.agent.app.bsky.graph.listitem.list({
|
||||
repo: this.rootStore.me.did,
|
||||
cursor,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
records = records.concat(
|
||||
res.records.filter(record => record.value.list === this.uri),
|
||||
)
|
||||
cursor = res.cursor
|
||||
if (!cursor) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// batch delete the list and listitem records
|
||||
const createDel = (uri: string) => {
|
||||
const urip = new AtUri(uri)
|
||||
return {
|
||||
$type: 'com.atproto.repo.applyWrites#delete',
|
||||
collection: urip.collection,
|
||||
rkey: urip.rkey,
|
||||
}
|
||||
}
|
||||
const writes = records
|
||||
.map(record => createDel(record.uri))
|
||||
.concat([createDel(this.uri)])
|
||||
|
||||
// apply in chunks
|
||||
for (const writesChunk of chunk(writes, 10)) {
|
||||
await this.rootStore.agent.com.atproto.repo.applyWrites({
|
||||
repo: this.rootStore.me.did,
|
||||
writes: writesChunk,
|
||||
})
|
||||
}
|
||||
|
||||
/* dont await */ this.rootStore.preferences.removeSavedFeed(this.uri)
|
||||
this.rootStore.emitListDeleted(this.uri)
|
||||
}
|
||||
|
||||
async addMember(profile: AppBskyActorDefs.ProfileViewBasic) {
|
||||
if (this.isMember(profile.did)) {
|
||||
return
|
||||
}
|
||||
await this.rootStore.agent.app.bsky.graph.listitem.create(
|
||||
{
|
||||
repo: this.rootStore.me.did,
|
||||
},
|
||||
{
|
||||
subject: profile.did,
|
||||
list: this.uri,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
)
|
||||
runInAction(() => {
|
||||
this.items = this.items.concat([
|
||||
{_reactKey: profile.did, subject: profile},
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Just adds to local cache; used to reflect changes affected elsewhere
|
||||
*/
|
||||
cacheAddMember(profile: AppBskyActorDefs.ProfileViewBasic) {
|
||||
if (!this.isMember(profile.did)) {
|
||||
this.items = this.items.concat([
|
||||
{_reactKey: profile.did, subject: profile},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Just removes from local cache; used to reflect changes affected elsewhere
|
||||
*/
|
||||
cacheRemoveMember(profile: AppBskyActorDefs.ProfileViewBasic) {
|
||||
if (this.isMember(profile.did)) {
|
||||
this.items = this.items.filter(item => item.subject.did !== profile.did)
|
||||
}
|
||||
}
|
||||
|
||||
async pin() {
|
||||
try {
|
||||
await this.rootStore.preferences.addPinnedFeed(this.uri)
|
||||
} catch (error) {
|
||||
logger.error('Failed to pin feed', {error})
|
||||
} finally {
|
||||
track('CustomFeed:Pin', {
|
||||
name: this.data?.name || '',
|
||||
uri: this.uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async togglePin() {
|
||||
if (!this.isPinned) {
|
||||
track('CustomFeed:Pin', {
|
||||
name: this.data?.name || '',
|
||||
uri: this.uri,
|
||||
})
|
||||
return this.rootStore.preferences.addPinnedFeed(this.uri)
|
||||
} else {
|
||||
track('CustomFeed:Unpin', {
|
||||
name: this.data?.name || '',
|
||||
uri: this.uri,
|
||||
})
|
||||
// TODO TEMPORARY
|
||||
// lists are temporarily piggybacking on the saved/pinned feeds preferences
|
||||
// we'll eventually replace saved feeds with the bookmarks API
|
||||
// until then, we need to unsave lists instead of just unpin them
|
||||
// -prf
|
||||
// return this.rootStore.preferences.removePinnedFeed(this.uri)
|
||||
return this.rootStore.preferences.removeSavedFeed(this.uri)
|
||||
}
|
||||
}
|
||||
|
||||
async mute() {
|
||||
if (!this.data) {
|
||||
return
|
||||
}
|
||||
await this._resolveUri()
|
||||
await this.rootStore.agent.muteModList(this.data.uri)
|
||||
track('Lists:Mute')
|
||||
runInAction(() => {
|
||||
if (this.data) {
|
||||
const d = this.data
|
||||
this.data = {...d, viewer: {...(d.viewer || {}), muted: true}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async unmute() {
|
||||
if (!this.data) {
|
||||
return
|
||||
}
|
||||
await this._resolveUri()
|
||||
await this.rootStore.agent.unmuteModList(this.data.uri)
|
||||
track('Lists:Unmute')
|
||||
runInAction(() => {
|
||||
if (this.data) {
|
||||
const d = this.data
|
||||
this.data = {...d, viewer: {...(d.viewer || {}), muted: false}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async block() {
|
||||
if (!this.data) {
|
||||
return
|
||||
}
|
||||
await this._resolveUri()
|
||||
const res = await this.rootStore.agent.blockModList(this.data.uri)
|
||||
track('Lists:Block')
|
||||
runInAction(() => {
|
||||
if (this.data) {
|
||||
const d = this.data
|
||||
this.data = {...d, viewer: {...(d.viewer || {}), blocked: res.uri}}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async unblock() {
|
||||
if (!this.data || !this.data.viewer?.blocked) {
|
||||
return
|
||||
}
|
||||
await this._resolveUri()
|
||||
await this.rootStore.agent.unblockModList(this.data.uri)
|
||||
track('Lists:Unblock')
|
||||
runInAction(() => {
|
||||
if (this.data) {
|
||||
const d = this.data
|
||||
this.data = {...d, viewer: {...(d.viewer || {}), blocked: 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) {
|
||||
logger.error('Failed to fetch user items', {error: err})
|
||||
}
|
||||
if (loadMoreErr) {
|
||||
logger.error('Failed to fetch user items', {
|
||||
error: loadMoreErr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
async _resolveUri() {
|
||||
const urip = new AtUri(this.uri)
|
||||
if (!urip.host.startsWith('did:')) {
|
||||
try {
|
||||
urip.host = await apilib.resolveName(this.rootStore, urip.host)
|
||||
} catch (e: any) {
|
||||
runInAction(() => {
|
||||
this.error = e.toString()
|
||||
})
|
||||
}
|
||||
}
|
||||
runInAction(() => {
|
||||
this.uri = urip.toString()
|
||||
})
|
||||
}
|
||||
|
||||
_replaceAll(res: GetList.Response) {
|
||||
this.items = []
|
||||
this._appendAll(res)
|
||||
}
|
||||
|
||||
_appendAll(res: GetList.Response) {
|
||||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
this.data = res.data.list
|
||||
this.items = this.items.concat(
|
||||
res.data.items.map(item => ({...item, _reactKey: item.subject.did})),
|
||||
)
|
||||
if (this.data.description) {
|
||||
this.descriptionRT = new RichText({
|
||||
text: this.data.description,
|
||||
facets: (this.data.descriptionFacets || [])?.slice(),
|
||||
})
|
||||
} else {
|
||||
this.descriptionRT = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ export class ProfileModel {
|
|||
existing.description = updates.description
|
||||
if (newUserAvatar) {
|
||||
const res = await apilib.uploadBlob(
|
||||
this.rootStore,
|
||||
this.rootStore.agent,
|
||||
newUserAvatar.path,
|
||||
newUserAvatar.mime,
|
||||
)
|
||||
|
|
@ -168,7 +168,7 @@ export class ProfileModel {
|
|||
}
|
||||
if (newUserBanner) {
|
||||
const res = await apilib.uploadBlob(
|
||||
this.rootStore,
|
||||
this.rootStore.agent,
|
||||
newUserBanner.path,
|
||||
newUserBanner.mime,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,244 +0,0 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
|
||||
import {RootStoreModel} from '../root-store'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {bundleAsync} from 'lib/async/bundle'
|
||||
import {accumulate} from 'lib/async/accumulate'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
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: 'mine' | 'my-curatelists' | '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
|
||||
}
|
||||
|
||||
get curatelists() {
|
||||
return this.lists.filter(
|
||||
list => list.purpose === 'app.bsky.graph.defs#curatelist',
|
||||
)
|
||||
}
|
||||
|
||||
get isCuratelistsEmpty() {
|
||||
return this.hasLoaded && this.curatelists.length === 0
|
||||
}
|
||||
|
||||
get modlists() {
|
||||
return this.lists.filter(
|
||||
list => list.purpose === 'app.bsky.graph.defs#modlist',
|
||||
)
|
||||
}
|
||||
|
||||
get isModlistsEmpty() {
|
||||
return this.hasLoaded && this.modlists.length === 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes posts from the feed upon deletion.
|
||||
*/
|
||||
onListDeleted(uri: string) {
|
||||
this.lists = this.lists.filter(l => l.uri !== uri)
|
||||
}
|
||||
|
||||
// public api
|
||||
// =
|
||||
|
||||
/**
|
||||
* Register any event listeners. Returns a cleanup function.
|
||||
*/
|
||||
registerListeners() {
|
||||
const sub = this.rootStore.onListDeleted(this.onListDeleted.bind(this))
|
||||
return () => sub.remove()
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
return this.loadMore(true)
|
||||
}
|
||||
|
||||
loadMore = bundleAsync(async (replace: boolean = false) => {
|
||||
if (!replace && !this.hasMore) {
|
||||
return
|
||||
}
|
||||
this._xLoading(replace)
|
||||
try {
|
||||
let cursor: string | undefined
|
||||
let lists: GraphDefs.ListView[] = []
|
||||
if (
|
||||
this.source === 'mine' ||
|
||||
this.source === 'my-curatelists' ||
|
||||
this.source === 'my-modlists'
|
||||
) {
|
||||
const promises = [
|
||||
accumulate(cursor =>
|
||||
this.rootStore.agent.app.bsky.graph
|
||||
.getLists({
|
||||
actor: this.rootStore.me.did,
|
||||
cursor,
|
||||
limit: 50,
|
||||
})
|
||||
.then(res => ({cursor: res.data.cursor, items: res.data.lists})),
|
||||
),
|
||||
]
|
||||
if (this.source === 'my-modlists') {
|
||||
promises.push(
|
||||
accumulate(cursor =>
|
||||
this.rootStore.agent.app.bsky.graph
|
||||
.getListMutes({
|
||||
cursor,
|
||||
limit: 50,
|
||||
})
|
||||
.then(res => ({
|
||||
cursor: res.data.cursor,
|
||||
items: res.data.lists,
|
||||
})),
|
||||
),
|
||||
)
|
||||
promises.push(
|
||||
accumulate(cursor =>
|
||||
this.rootStore.agent.app.bsky.graph
|
||||
.getListBlocks({
|
||||
cursor,
|
||||
limit: 50,
|
||||
})
|
||||
.then(res => ({
|
||||
cursor: res.data.cursor,
|
||||
items: res.data.lists,
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
const resultset = await Promise.all(promises)
|
||||
for (const res of resultset) {
|
||||
for (let list of res) {
|
||||
if (
|
||||
this.source === 'my-curatelists' &&
|
||||
list.purpose !== 'app.bsky.graph.defs#curatelist'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (
|
||||
this.source === 'my-modlists' &&
|
||||
list.purpose !== 'app.bsky.graph.defs#modlist'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (!lists.find(l => l.uri === list.uri)) {
|
||||
lists.push(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const res = await this.rootStore.agent.app.bsky.graph.getLists({
|
||||
actor: this.source,
|
||||
limit: PAGE_SIZE,
|
||||
cursor: replace ? undefined : this.loadMoreCursor,
|
||||
})
|
||||
lists = res.data.lists
|
||||
cursor = res.data.cursor
|
||||
}
|
||||
if (replace) {
|
||||
this._replaceAll({lists, cursor})
|
||||
} else {
|
||||
this._appendAll({lists, cursor})
|
||||
}
|
||||
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) {
|
||||
logger.error('Failed to fetch user lists', {error: err})
|
||||
}
|
||||
if (loadMoreErr) {
|
||||
logger.error('Failed to fetch user lists', {
|
||||
error: loadMoreErr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helper functions
|
||||
// =
|
||||
|
||||
_replaceAll({
|
||||
lists,
|
||||
cursor,
|
||||
}: {
|
||||
lists: GraphDefs.ListView[]
|
||||
cursor: string | undefined
|
||||
}) {
|
||||
this.lists = []
|
||||
this._appendAll({lists, cursor})
|
||||
}
|
||||
|
||||
_appendAll({
|
||||
lists,
|
||||
cursor,
|
||||
}: {
|
||||
lists: GraphDefs.ListView[]
|
||||
cursor: string | undefined
|
||||
}) {
|
||||
this.loadMoreCursor = cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
this.lists = this.lists.concat(
|
||||
lists.map(list => ({...list, _reactKey: list.uri})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import {makeAutoObservable, runInAction} from 'mobx'
|
|||
import {RootStoreModel} from '../root-store'
|
||||
import {ProfileModel} from '../content/profile'
|
||||
import {ActorFeedsModel} from '../lists/actor-feeds'
|
||||
import {ListsListModel} from '../lists/lists-list'
|
||||
import {logger} from '#/logger'
|
||||
|
||||
export enum Sections {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue