bsky-app/src/state/models/feeds/multi-feed.ts
Paul Frazee 3217c7ff32
More custom-feed behavior fixes [APP-678] (#831)
* Remove extraneous custom-feed health check

* Fixes to custom feed preference sync

* Fix lint

* Remove dead code (client-side suggested posts constructor)

* Enforce the feed-fetch limit in the client if the generator fails to observe the parameter

* Bump the number of items fetched in the multifeed per feed from 5 to 10

* Reset the currently active feed when the pinned feeds change

* Some fixes to icons

* Add a prompt to load latest to the multifeed

* Remove debug
2023-06-02 09:48:53 -05:00

225 lines
4.9 KiB
TypeScript

import {makeAutoObservable, runInAction} from 'mobx'
import {AtUri} from '@atproto/api'
import {bundleAsync} from 'lib/async/bundle'
import {RootStoreModel} from '../root-store'
import {CustomFeedModel} from './custom-feed'
import {PostsFeedModel} from './posts'
import {PostsFeedSliceModel} from './post'
const FEED_PAGE_SIZE = 10
const FEEDS_PAGE_SIZE = 3
export type MultiFeedItem =
| {
_reactKey: string
type: 'header'
}
| {
_reactKey: string
type: 'feed-header'
avatar: string | undefined
title: string
}
| {
_reactKey: string
type: 'feed-slice'
slice: PostsFeedSliceModel
}
| {
_reactKey: string
type: 'feed-loading'
}
| {
_reactKey: string
type: 'feed-error'
error: string
}
| {
_reactKey: string
type: 'feed-footer'
title: string
uri: string
}
| {
_reactKey: string
type: 'footer'
}
export class PostsMultiFeedModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
hasMore = true
// data
feedInfos: CustomFeedModel[] = []
feeds: PostsFeedModel[] = []
constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {rootStore: false}, {autoBind: true})
}
get hasContent() {
return this.feeds.length !== 0
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
get items() {
const items: MultiFeedItem[] = [{_reactKey: '__header__', type: 'header'}]
for (let i = 0; i < this.feedInfos.length; i++) {
if (!this.feeds[i]) {
break
}
const feed = this.feeds[i]
const feedInfo = this.feedInfos[i]
const urip = new AtUri(feedInfo.uri)
items.push({
_reactKey: `__feed_header_${i}__`,
type: 'feed-header',
avatar: feedInfo.data.avatar,
title: feedInfo.displayName,
})
if (feed.isLoading) {
items.push({
_reactKey: `__feed_loading_${i}__`,
type: 'feed-loading',
})
} else if (feed.hasError) {
items.push({
_reactKey: `__feed_error_${i}__`,
type: 'feed-error',
error: feed.error,
})
} else {
for (let j = 0; j < feed.slices.length; j++) {
items.push({
_reactKey: `__feed_slice_${i}_${j}__`,
type: 'feed-slice',
slice: feed.slices[j],
})
}
}
items.push({
_reactKey: `__feed_footer_${i}__`,
type: 'feed-footer',
title: feedInfo.displayName,
uri: `/profile/${feedInfo.data.creator.did}/feed/${urip.rkey}`,
})
}
if (!this.hasMore) {
items.push({_reactKey: '__footer__', type: 'footer'})
}
return items
}
// public api
// =
/**
* Nuke all data
*/
clear() {
this.rootStore.log.debug('MultiFeedModel:clear')
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = false
this.hasMore = true
this.feeds = []
}
/**
* Register any event listeners. Returns a cleanup function.
*/
registerListeners() {
const sub = this.rootStore.onPostDeleted(this.onPostDeleted.bind(this))
return () => sub.remove()
}
/**
* Reset and load
*/
async refresh() {
this.feedInfos = this.rootStore.me.savedFeeds.all.slice() // capture current feeds
await this.loadMore(true)
}
/**
* Load latest in the active feeds
*/
loadLatest() {
for (const feed of this.feeds) {
/* dont await */ feed.refresh()
}
}
/**
* Load more posts to the end of the feed
*/
loadMore = bundleAsync(async (isRefreshing: boolean = false) => {
if (!isRefreshing && !this.hasMore) {
return
}
if (isRefreshing) {
this.isRefreshing = true // set optimistically for UI
this.feeds = []
}
this._xLoading(isRefreshing)
const start = this.feeds.length
const newFeeds: PostsFeedModel[] = []
for (
let i = start;
i < start + FEEDS_PAGE_SIZE && i < this.feedInfos.length;
i++
) {
const feed = new PostsFeedModel(this.rootStore, 'custom', {
feed: this.feedInfos[i].uri,
})
feed.pageSize = FEED_PAGE_SIZE
await feed.setup()
newFeeds.push(feed)
}
runInAction(() => {
this.feeds = this.feeds.concat(newFeeds)
this.hasMore = this.feeds.length < this.feedInfos.length
})
this._xIdle()
})
/**
* Attempt to load more again after a failure
*/
async retryLoadMore() {
this.hasMore = true
return this.loadMore()
}
/**
* Removes posts from the feed upon deletion.
*/
onPostDeleted(uri: string) {
for (const f of this.feeds) {
f.onPostDeleted(uri)
}
}
// state transitions
// =
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
}
_xIdle() {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
}
// helper functions
// =
}