From f6f1fe2558cd00e4dcac9685a4209c79b224d3bb Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Wed, 22 Mar 2023 15:46:49 -0500 Subject: [PATCH] Feed updates (Closes #344) (#356) * Rework feed polling to correctly detect when new content is available (close #344) * Tweak how the tuner works for consistency * Improve the feed-update behavior after posting * Load latest notifications when opening the tab --- src/lib/api/feed-manip.ts | 25 ++++----- src/state/models/feed-view.ts | 89 +++++++++++------------------- src/view/com/composer/Composer.tsx | 2 +- src/view/screens/Home.tsx | 2 +- src/view/screens/Notifications.tsx | 3 +- 5 files changed, 49 insertions(+), 72 deletions(-) diff --git a/src/lib/api/feed-manip.ts b/src/lib/api/feed-manip.ts index a54b7d25..e9a32b7a 100644 --- a/src/lib/api/feed-manip.ts +++ b/src/lib/api/feed-manip.ts @@ -10,10 +10,12 @@ export type FeedTunerFn = ( ) => void export class FeedViewPostsSlice { + isFlattenedReply = false + constructor(public items: FeedViewPost[] = []) {} get uri() { - if (this.isReply) { + if (this.isFlattenedReply) { return this.items[1].post.uri } return this.items[0].post.uri @@ -39,12 +41,8 @@ export class FeedViewPostsSlice { return this.isThread && !this.items[0].reply } - get isReply() { - return this.items.length > 1 && !this.isThread - } - get rootItem() { - if (this.isReply) { + if (this.isFlattenedReply) { return this.items[1] } return this.items[0] @@ -70,6 +68,7 @@ export class FeedViewPostsSlice { flattenReplyParent() { if (this.items[0].reply?.parent) { + this.isFlattenedReply = true this.items.splice(0, 0, {post: this.items[0].reply?.parent}) } } @@ -105,6 +104,11 @@ export class FeedTuner { slices.unshift(new FeedViewPostsSlice([item])) } + // run the custom tuners + for (const tunerFn of tunerFns) { + tunerFn(this, slices) + } + // remove any items already "seen" const soonToBeSeenUris: Set = new Set() for (let i = slices.length - 1; i >= 0; i--) { @@ -135,11 +139,6 @@ export class FeedTuner { // sort by slice roots' timestamps slices.sort((a, b) => b.ts.localeCompare(a.ts)) - // run the custom tuners - for (const tunerFn of tunerFns) { - tunerFn(this, slices) - } - for (const slice of slices) { for (const item of slice.items) { this.seenUris.add(item.post.uri) @@ -170,12 +169,12 @@ export class FeedTuner { static likedRepliesOnly(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { // remove any replies without at least 2 likes for (let i = slices.length - 1; i >= 0; i--) { - if (slices[i].isFullThread) { + if (slices[i].isFullThread || !slices[i].rootItem.reply) { continue } const item = slices[i].rootItem const isRepost = Boolean(item.reason) - if (item.reply && !isRepost && item.post.upvoteCount < 2) { + if (!isRepost && item.post.upvoteCount < 2) { slices.splice(i, 1) } } diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index 954c19a8..083863fe 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -254,6 +254,7 @@ export class FeedModel { // data slices: FeedSliceModel[] = [] + nextSlices: FeedSliceModel[] = [] constructor( public rootStore: RootStoreModel, @@ -325,6 +326,7 @@ export class FeedModel { this.loadMoreCursor = undefined this.pollCursor = undefined this.slices = [] + this.nextSlices = [] this.tuner.reset() } @@ -422,30 +424,6 @@ export class FeedModel { } }) - /** - * Load more posts to the start of the feed - */ - loadLatest = bundleAsync(async () => { - await this.lock.acquireAsync() - try { - this.setHasNewLatest(false) - this._xLoading() - try { - const res = await this._getFeed({limit: PAGE_SIZE}) - await this._prependAll(res) - this._xIdle() - } catch (e: any) { - this._xIdle() // don't bubble the error to the user - this.rootStore.log.error('FeedView: Failed to load latest', { - params: this.params, - e, - }) - } - } finally { - this.lock.release() - } - }) - /** * Update content in-place */ @@ -487,22 +465,42 @@ export class FeedModel { /** * Check if new posts are available */ - async checkForLatest() { + async checkForLatest({autoPrepend}: {autoPrepend?: boolean} = {}) { if (this.hasNewLatest || this.feedType === 'suggested') { return } - const res = await this._getFeed({limit: 1}) - const currentLatestUri = this.pollCursor - const item = res.data.feed?.[0] - if (!item) { - return - } - if (AppBskyFeedFeedViewPost.isReasonRepost(item.reason)) { - if (item.reason.by.did === this.rootStore.me.did) { - return // ignore reposts by the user + const res = await this._getFeed({limit: PAGE_SIZE}) + const tuner = new FeedTuner() + const nextSlices = tuner.tune(res.data.feed, this.feedTuners) + if (nextSlices[0]?.uri !== this.slices[0]?.uri) { + const nextSlicesModels = nextSlices.map( + slice => + new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice), + ) + if (autoPrepend) { + this.slices = nextSlicesModels.concat( + this.slices.filter(slice1 => + nextSlicesModels.find(slice2 => slice1.uri === slice2.uri), + ), + ) + this.setHasNewLatest(false) + } else { + this.nextSlices = nextSlicesModels + this.setHasNewLatest(true) } + } else { + this.setHasNewLatest(false) } - this.setHasNewLatest(item.post.uri !== currentLatestUri) + } + + /** + * Sets the current slices to the "next slices" loaded by checkForLatest + */ + resetToLatest() { + if (this.nextSlices.length) { + this.slices = this.nextSlices + } + this.setHasNewLatest(false) } /** @@ -574,27 +572,6 @@ export class FeedModel { }) } - private async _prependAll( - res: GetTimeline.Response | GetAuthorFeed.Response, - ) { - this.pollCursor = res.data.feed[0]?.post.uri - - const slices = this.tuner.tune(res.data.feed, this.feedTuners) - - const toPrepend: FeedSliceModel[] = [] - for (const slice of slices) { - const itemModel = new FeedSliceModel( - this.rootStore, - `item-${_idCounter++}`, - slice, - ) - toPrepend.push(itemModel) - } - runInAction(() => { - this.slices = toPrepend.concat(this.slices) - }) - } - private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) { for (const item of res.data.feed) { const existingSlice = this.slices.find(slice => diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index e10e801e..572eea92 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -166,7 +166,7 @@ export const ComposePost = observer(function ComposePost({ setIsProcessing(false) return } - store.me.mainFeed.loadLatest() + store.me.mainFeed.checkForLatest({autoPrepend: true}) onPost?.() hackfixOnClose() Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 505f9e72..4f2bc4c1 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -158,7 +158,7 @@ const FeedPage = observer( }, [feed]) const onPressLoadLatest = React.useCallback(() => { - feed.refresh() + feed.resetToLatest() scrollToTop() }, [feed, scrollToTop]) diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index b704f9c4..7da56384 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -74,7 +74,8 @@ export const NotificationsScreen = withAuthRequired( React.useCallback(() => { store.log.debug('NotificationsScreen: Updating feed') const softResetSub = store.onScreenSoftReset(scrollToTop) - store.me.notifications.update() + store.me.notifications.loadUnreadCount() + store.me.notifications.loadLatest() screen('Notifications') return () => {