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
zio/stable
Paul Frazee 2023-03-22 15:46:49 -05:00 committed by GitHub
parent 449f9243f3
commit f6f1fe2558
5 changed files with 49 additions and 72 deletions

View File

@ -10,10 +10,12 @@ export type FeedTunerFn = (
) => void ) => void
export class FeedViewPostsSlice { export class FeedViewPostsSlice {
isFlattenedReply = false
constructor(public items: FeedViewPost[] = []) {} constructor(public items: FeedViewPost[] = []) {}
get uri() { get uri() {
if (this.isReply) { if (this.isFlattenedReply) {
return this.items[1].post.uri return this.items[1].post.uri
} }
return this.items[0].post.uri return this.items[0].post.uri
@ -39,12 +41,8 @@ export class FeedViewPostsSlice {
return this.isThread && !this.items[0].reply return this.isThread && !this.items[0].reply
} }
get isReply() {
return this.items.length > 1 && !this.isThread
}
get rootItem() { get rootItem() {
if (this.isReply) { if (this.isFlattenedReply) {
return this.items[1] return this.items[1]
} }
return this.items[0] return this.items[0]
@ -70,6 +68,7 @@ export class FeedViewPostsSlice {
flattenReplyParent() { flattenReplyParent() {
if (this.items[0].reply?.parent) { if (this.items[0].reply?.parent) {
this.isFlattenedReply = true
this.items.splice(0, 0, {post: this.items[0].reply?.parent}) this.items.splice(0, 0, {post: this.items[0].reply?.parent})
} }
} }
@ -105,6 +104,11 @@ export class FeedTuner {
slices.unshift(new FeedViewPostsSlice([item])) slices.unshift(new FeedViewPostsSlice([item]))
} }
// run the custom tuners
for (const tunerFn of tunerFns) {
tunerFn(this, slices)
}
// remove any items already "seen" // remove any items already "seen"
const soonToBeSeenUris: Set<string> = new Set() const soonToBeSeenUris: Set<string> = new Set()
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
@ -135,11 +139,6 @@ export class FeedTuner {
// sort by slice roots' timestamps // sort by slice roots' timestamps
slices.sort((a, b) => b.ts.localeCompare(a.ts)) 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 slice of slices) {
for (const item of slice.items) { for (const item of slice.items) {
this.seenUris.add(item.post.uri) this.seenUris.add(item.post.uri)
@ -170,12 +169,12 @@ export class FeedTuner {
static likedRepliesOnly(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static likedRepliesOnly(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
// remove any replies without at least 2 likes // remove any replies without at least 2 likes
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
if (slices[i].isFullThread) { if (slices[i].isFullThread || !slices[i].rootItem.reply) {
continue continue
} }
const item = slices[i].rootItem const item = slices[i].rootItem
const isRepost = Boolean(item.reason) const isRepost = Boolean(item.reason)
if (item.reply && !isRepost && item.post.upvoteCount < 2) { if (!isRepost && item.post.upvoteCount < 2) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }

View File

@ -254,6 +254,7 @@ export class FeedModel {
// data // data
slices: FeedSliceModel[] = [] slices: FeedSliceModel[] = []
nextSlices: FeedSliceModel[] = []
constructor( constructor(
public rootStore: RootStoreModel, public rootStore: RootStoreModel,
@ -325,6 +326,7 @@ export class FeedModel {
this.loadMoreCursor = undefined this.loadMoreCursor = undefined
this.pollCursor = undefined this.pollCursor = undefined
this.slices = [] this.slices = []
this.nextSlices = []
this.tuner.reset() 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 * Update content in-place
*/ */
@ -487,22 +465,42 @@ export class FeedModel {
/** /**
* Check if new posts are available * Check if new posts are available
*/ */
async checkForLatest() { async checkForLatest({autoPrepend}: {autoPrepend?: boolean} = {}) {
if (this.hasNewLatest || this.feedType === 'suggested') { if (this.hasNewLatest || this.feedType === 'suggested') {
return return
} }
const res = await this._getFeed({limit: 1}) const res = await this._getFeed({limit: PAGE_SIZE})
const currentLatestUri = this.pollCursor const tuner = new FeedTuner()
const item = res.data.feed?.[0] const nextSlices = tuner.tune(res.data.feed, this.feedTuners)
if (!item) { if (nextSlices[0]?.uri !== this.slices[0]?.uri) {
return const nextSlicesModels = nextSlices.map(
} slice =>
if (AppBskyFeedFeedViewPost.isReasonRepost(item.reason)) { new FeedSliceModel(this.rootStore, `item-${_idCounter++}`, slice),
if (item.reason.by.did === this.rootStore.me.did) { )
return // ignore reposts by the user 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) { private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
for (const item of res.data.feed) { for (const item of res.data.feed) {
const existingSlice = this.slices.find(slice => const existingSlice = this.slices.find(slice =>

View File

@ -166,7 +166,7 @@ export const ComposePost = observer(function ComposePost({
setIsProcessing(false) setIsProcessing(false)
return return
} }
store.me.mainFeed.loadLatest() store.me.mainFeed.checkForLatest({autoPrepend: true})
onPost?.() onPost?.()
hackfixOnClose() hackfixOnClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)

View File

@ -158,7 +158,7 @@ const FeedPage = observer(
}, [feed]) }, [feed])
const onPressLoadLatest = React.useCallback(() => { const onPressLoadLatest = React.useCallback(() => {
feed.refresh() feed.resetToLatest()
scrollToTop() scrollToTop()
}, [feed, scrollToTop]) }, [feed, scrollToTop])

View File

@ -74,7 +74,8 @@ export const NotificationsScreen = withAuthRequired(
React.useCallback(() => { React.useCallback(() => {
store.log.debug('NotificationsScreen: Updating feed') store.log.debug('NotificationsScreen: Updating feed')
const softResetSub = store.onScreenSoftReset(scrollToTop) const softResetSub = store.onScreenSoftReset(scrollToTop)
store.me.notifications.update() store.me.notifications.loadUnreadCount()
store.me.notifications.loadLatest()
screen('Notifications') screen('Notifications')
return () => { return () => {