diff options
Diffstat (limited to 'src/state/models')
-rw-r--r-- | src/state/models/feeds/posts-slice.ts | 91 | ||||
-rw-r--r-- | src/state/models/feeds/posts.ts | 429 | ||||
-rw-r--r-- | src/state/models/me.ts | 10 | ||||
-rw-r--r-- | src/state/models/ui/profile.ts | 1 |
4 files changed, 0 insertions, 531 deletions
diff --git a/src/state/models/feeds/posts-slice.ts b/src/state/models/feeds/posts-slice.ts deleted file mode 100644 index 2501cef6f..000000000 --- a/src/state/models/feeds/posts-slice.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {makeAutoObservable} from 'mobx' -import {RootStoreModel} from '../root-store' -import {FeedViewPostsSlice} from 'lib/api/feed-manip' -import {PostsFeedItemModel} from './post' -import {FeedSourceInfo} from 'lib/api/feed/types' - -export class PostsFeedSliceModel { - // ui state - _reactKey: string = '' - - // data - items: PostsFeedItemModel[] = [] - source: FeedSourceInfo | undefined - - constructor(public rootStore: RootStoreModel, slice: FeedViewPostsSlice) { - this._reactKey = slice._reactKey - this.source = slice.source - for (let i = 0; i < slice.items.length; i++) { - this.items.push( - new PostsFeedItemModel( - rootStore, - `${this._reactKey} - ${i}`, - slice.items[i], - ), - ) - } - makeAutoObservable(this, {rootStore: false}) - } - - get uri() { - if (this.isReply) { - return this.items[1].post.uri - } - return this.items[0].post.uri - } - - get isThread() { - return ( - this.items.length > 1 && - this.items.every( - item => item.post.author.did === this.items[0].post.author.did, - ) - ) - } - - get isReply() { - return this.items.length > 1 && !this.isThread - } - - get rootItem() { - if (this.isReply) { - return this.items[1] - } - return this.items[0] - } - - get moderation() { - // prefer the most stringent item - const topItem = this.items.find(item => item.moderation.content.filter) - if (topItem) { - return topItem.moderation - } - // otherwise just use the first one - return this.items[0].moderation - } - - shouldFilter(ignoreFilterForDid: string | undefined): boolean { - const mods = this.items - .filter(item => item.post.author.did !== ignoreFilterForDid) - .map(item => item.moderation) - return !!mods.find(mod => mod.content.filter) - } - - containsUri(uri: string) { - return !!this.items.find(item => item.post.uri === uri) - } - - isThreadParentAt(i: number) { - if (this.items.length === 1) { - return false - } - return i < this.items.length - 1 - } - - isThreadChildAt(i: number) { - if (this.items.length === 1) { - return false - } - return i > 0 - } -} diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts deleted file mode 100644 index 0a06c581c..000000000 --- a/src/state/models/feeds/posts.ts +++ /dev/null @@ -1,429 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import { - AppBskyFeedGetTimeline as GetTimeline, - AppBskyFeedGetAuthorFeed as GetAuthorFeed, - AppBskyFeedGetFeed as GetCustomFeed, - AppBskyFeedGetActorLikes as GetActorLikes, - AppBskyFeedGetListFeed as GetListFeed, -} from '@atproto/api' -import AwaitLock from 'await-lock' -import {bundleAsync} from 'lib/async/bundle' -import {RootStoreModel} from '../root-store' -import {cleanError} from 'lib/strings/errors' -import {FeedTuner} from 'lib/api/feed-manip' -import {PostsFeedSliceModel} from './posts-slice' -import {track} from 'lib/analytics/analytics' -import {FeedViewPostsSlice} from 'lib/api/feed-manip' - -import {FeedAPI, FeedAPIResponse} from 'lib/api/feed/types' -import {FollowingFeedAPI} from 'lib/api/feed/following' -import {AuthorFeedAPI} from 'lib/api/feed/author' -import {LikesFeedAPI} from 'lib/api/feed/likes' -import {CustomFeedAPI} from 'lib/api/feed/custom' -import {ListFeedAPI} from 'lib/api/feed/list' -import {MergeFeedAPI} from 'lib/api/feed/merge' -import {logger} from '#/logger' - -const PAGE_SIZE = 30 - -type FeedType = 'home' | 'following' | 'author' | 'custom' | 'likes' | 'list' - -export enum KnownError { - FeedgenDoesNotExist, - FeedgenMisconfigured, - FeedgenBadResponse, - FeedgenOffline, - FeedgenUnknown, - Unknown, -} - -type Options = { - /** - * Formats the feed in a flat array with no threading of replies, just - * top-level posts. - */ - isSimpleFeed?: boolean -} - -type QueryParams = - | GetTimeline.QueryParams - | GetAuthorFeed.QueryParams - | GetActorLikes.QueryParams - | GetCustomFeed.QueryParams - | GetListFeed.QueryParams - -export class PostsFeedModel { - // state - isLoading = false - isRefreshing = false - hasNewLatest = false - hasLoaded = false - isBlocking = false - isBlockedBy = false - error = '' - knownError: KnownError | undefined - loadMoreError = '' - params: QueryParams - hasMore = true - pollCursor: string | undefined - api: FeedAPI - tuner = new FeedTuner() - pageSize = PAGE_SIZE - options: Options = {} - - // used to linearize async modifications to state - lock = new AwaitLock() - - // used to track if a feed is coming up empty - emptyFetches = 0 - - // data - slices: PostsFeedSliceModel[] = [] - - constructor( - public rootStore: RootStoreModel, - public feedType: FeedType, - params: QueryParams, - options?: Options, - ) { - makeAutoObservable( - this, - { - rootStore: false, - params: false, - }, - {autoBind: true}, - ) - this.params = params - this.options = options || {} - if (feedType === 'home') { - this.api = new MergeFeedAPI(rootStore) - } else if (feedType === 'following') { - this.api = new FollowingFeedAPI(rootStore) - } else if (feedType === 'author') { - this.api = new AuthorFeedAPI( - rootStore, - params as GetAuthorFeed.QueryParams, - ) - } else if (feedType === 'likes') { - this.api = new LikesFeedAPI( - rootStore, - params as GetActorLikes.QueryParams, - ) - } else if (feedType === 'custom') { - this.api = new CustomFeedAPI( - rootStore, - params as GetCustomFeed.QueryParams, - ) - } else if (feedType === 'list') { - this.api = new ListFeedAPI(rootStore, params as GetListFeed.QueryParams) - } else { - this.api = new FollowingFeedAPI(rootStore) - } - } - - get reactKey() { - if (this.feedType === 'author') { - return (this.params as GetAuthorFeed.QueryParams).actor - } - if (this.feedType === 'custom') { - return (this.params as GetCustomFeed.QueryParams).feed - } - if (this.feedType === 'list') { - return (this.params as GetListFeed.QueryParams).list - } - return this.feedType - } - - get hasContent() { - return this.slices.length !== 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - get isLoadingMore() { - return this.isLoading && !this.isRefreshing && this.hasContent - } - - setHasNewLatest(v: boolean) { - this.hasNewLatest = v - } - - // public api - // = - - /** - * Nuke all data - */ - clear() { - logger.debug('FeedModel:clear') - this.isLoading = false - this.isRefreshing = false - this.hasNewLatest = false - this.hasLoaded = false - this.error = '' - this.hasMore = true - this.pollCursor = undefined - this.slices = [] - this.tuner.reset() - } - - /** - * Load for first render - */ - setup = bundleAsync(async (isRefreshing: boolean = false) => { - logger.debug('FeedModel:setup', {isRefreshing}) - if (isRefreshing) { - this.isRefreshing = true // set optimistically for UI - } - await this.lock.acquireAsync() - try { - this.setHasNewLatest(false) - this.api.reset() - this.tuner.reset() - this._xLoading(isRefreshing) - try { - const res = await this.api.fetchNext({limit: this.pageSize}) - await this._replaceAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - } finally { - this.lock.release() - } - }) - - /** - * 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() { - await this.setup(true) - } - - /** - * Load more posts to the end of the feed - */ - loadMore = bundleAsync(async () => { - await this.lock.acquireAsync() - try { - if (!this.hasMore || this.hasError) { - return - } - this._xLoading() - try { - const res = await this.api.fetchNext({ - limit: this.pageSize, - }) - await this._appendAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle(undefined, e) - runInAction(() => { - this.hasMore = false - }) - } - } finally { - this.lock.release() - if (this.feedType === 'custom') { - track('CustomFeed:LoadMore') - } - } - }) - - /** - * Attempt to load more again after a failure - */ - async retryLoadMore() { - this.loadMoreError = '' - this.hasMore = true - return this.loadMore() - } - - /** - * Check if new posts are available - */ - async checkForLatest() { - if (!this.hasLoaded || this.hasNewLatest || this.isLoading) { - return - } - const post = await this.api.peekLatest() - if (post) { - const slices = this.tuner.tune( - [post], - this.rootStore.preferences.getFeedTuners(this.feedType), - { - dryRun: true, - maintainOrder: true, - }, - ) - if (slices[0]) { - const sliceModel = new PostsFeedSliceModel(this.rootStore, slices[0]) - if (sliceModel.moderation.content.filter) { - return - } - this.setHasNewLatest(sliceModel.uri !== this.pollCursor) - } - } - } - - /** - * Updates the UI after the user has created a post - */ - onPostCreated() { - if (!this.slices.length) { - return this.refresh() - } else { - this.setHasNewLatest(true) - } - } - - /** - * Removes posts from the feed upon deletion. - */ - onPostDeleted(uri: string) { - let i - do { - i = this.slices.findIndex(slice => slice.containsUri(uri)) - if (i !== -1) { - this.slices.splice(i, 1) - } - } while (i !== -1) - } - - // state transitions - // = - - _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - this.knownError = undefined - } - - _xIdle(error?: any, loadMoreError?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.isBlocking = error instanceof GetAuthorFeed.BlockedActorError - this.isBlockedBy = error instanceof GetAuthorFeed.BlockedByActorError - this.error = cleanError(error) - this.knownError = detectKnownError(this.feedType, error) - this.loadMoreError = cleanError(loadMoreError) - if (error) { - logger.error('Posts feed request failed', {error}) - } - if (loadMoreError) { - logger.error('Posts feed load-more request failed', { - error: loadMoreError, - }) - } - } - - // helper functions - // = - - async _replaceAll(res: FeedAPIResponse) { - this.pollCursor = res.feed[0]?.post.uri - return this._appendAll(res, true) - } - - async _appendAll(res: FeedAPIResponse, replace = false) { - this.hasMore = !!res.cursor && res.feed.length > 0 - if (replace) { - this.emptyFetches = 0 - } - - this.rootStore.me.follows.hydrateMany( - res.feed.map(item => item.post.author), - ) - for (const item of res.feed) { - this.rootStore.posts.fromFeedItem(item) - } - - const slices = this.options.isSimpleFeed - ? res.feed.map(item => new FeedViewPostsSlice([item])) - : this.tuner.tune( - res.feed, - this.rootStore.preferences.getFeedTuners(this.feedType), - ) - - const toAppend: PostsFeedSliceModel[] = [] - for (const slice of slices) { - const sliceModel = new PostsFeedSliceModel(this.rootStore, slice) - const dupTest = (item: PostsFeedSliceModel) => - item._reactKey === sliceModel._reactKey - // sanity check - // if a duplicate _reactKey passes through, the UI breaks hard - if (!replace) { - if (this.slices.find(dupTest) || toAppend.find(dupTest)) { - continue - } - } - toAppend.push(sliceModel) - } - runInAction(() => { - if (replace) { - this.slices = toAppend - } else { - this.slices = this.slices.concat(toAppend) - } - if (toAppend.length === 0) { - this.emptyFetches++ - if (this.emptyFetches >= 10) { - this.hasMore = false - } - } - }) - } -} - -function detectKnownError( - feedType: FeedType, - error: any, -): KnownError | undefined { - if (!error) { - return undefined - } - if (typeof error !== 'string') { - error = error.toString() - } - if (feedType !== 'custom') { - return KnownError.Unknown - } - if (error.includes('could not find feed')) { - return KnownError.FeedgenDoesNotExist - } - if (error.includes('feed unavailable')) { - return KnownError.FeedgenOffline - } - if (error.includes('invalid did document')) { - return KnownError.FeedgenMisconfigured - } - if (error.includes('could not resolve did document')) { - return KnownError.FeedgenMisconfigured - } - if ( - error.includes('invalid feed generator service details in did document') - ) { - return KnownError.FeedgenMisconfigured - } - if (error.includes('feed provided an invalid response')) { - return KnownError.FeedgenBadResponse - } - return KnownError.FeedgenUnknown -} diff --git a/src/state/models/me.ts b/src/state/models/me.ts index d3061f166..4bbb5a04b 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -4,7 +4,6 @@ import { ComAtprotoServerListAppPasswords, } from '@atproto/api' import {RootStoreModel} from './root-store' -import {PostsFeedModel} from './feeds/posts' import {NotificationsFeedModel} from './feeds/notifications' import {MyFeedsUIModel} from './ui/my-feeds' import {MyFollowsCache} from './cache/my-follows' @@ -22,7 +21,6 @@ export class MeModel { avatar: string = '' followsCount: number | undefined followersCount: number | undefined - mainFeed: PostsFeedModel notifications: NotificationsFeedModel myFeeds: MyFeedsUIModel follows: MyFollowsCache @@ -41,16 +39,12 @@ export class MeModel { {rootStore: false, serialize: false, hydrate: false}, {autoBind: true}, ) - this.mainFeed = new PostsFeedModel(this.rootStore, 'home', { - algorithm: 'reverse-chronological', - }) this.notifications = new NotificationsFeedModel(this.rootStore) this.myFeeds = new MyFeedsUIModel(this.rootStore) this.follows = new MyFollowsCache(this.rootStore) } clear() { - this.mainFeed.clear() this.notifications.clear() this.myFeeds.clear() this.follows.clear() @@ -109,10 +103,6 @@ export class MeModel { if (sess.hasSession) { this.did = sess.currentSession?.did || '' await this.fetchProfile() - this.mainFeed.clear() - /* dont await */ this.mainFeed.setup().catch(e => { - logger.error('Failed to setup main feed model', {error: e}) - }) /* dont await */ this.notifications.setup().catch(e => { logger.error('Failed to setup notifications model', { error: e, diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index f96340c65..0ef592928 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -1,7 +1,6 @@ import {makeAutoObservable, runInAction} from 'mobx' import {RootStoreModel} from '../root-store' import {ProfileModel} from '../content/profile' -import {PostsFeedModel} from '../feeds/posts' import {ActorFeedsModel} from '../lists/actor-feeds' import {ListsListModel} from '../lists/lists-list' import {logger} from '#/logger' |