Refactor feed slices (#4834)

* Copy FeedViewPost into FeedSliceItem

* Explicitly construct feed slice items by copying known fields

* Type rootItem as FeedViewPost for now

Mergefeed logic relies on that.

* Unify reason and __source for slice items

* Move feedContext out of FeedSliceItem

* Remove slice.isFlattenedReply

* Remove unnused slice.ts

* Inline slice.isFullThread

* Refactor condition for clarity

* Extract slice.includesThreadRoot

* Encapsulate more usages of slice.rootItem into slice

* Rename slice.rootItem so semi-private slice._feedPost

* Move reason into slice

* Simplify slice ctor argument

* Reorder getters to reduce diff

* Make feedContext a getter to reduce diff
zio/stable
dan 2024-07-25 23:02:37 +01:00 committed by GitHub
parent 3914025227
commit ac1538baad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 69 deletions

View File

@ -14,29 +14,33 @@ export type FeedTunerFn = (
slices: FeedViewPostsSlice[], slices: FeedViewPostsSlice[],
) => FeedViewPostsSlice[] ) => FeedViewPostsSlice[]
type FeedSliceItem = {
post: AppBskyFeedDefs.PostView
reply?: AppBskyFeedDefs.ReplyRef
}
function toSliceItem(feedViewPost: FeedViewPost): FeedSliceItem {
return {
post: feedViewPost.post,
reply: feedViewPost.reply,
}
}
export class FeedViewPostsSlice { export class FeedViewPostsSlice {
_reactKey: string _reactKey: string
isFlattenedReply = false _feedPost: FeedViewPost
items: FeedSliceItem[]
constructor(public items: FeedViewPost[]) { constructor(feedPost: FeedViewPost) {
const item = items[0] this._feedPost = feedPost
this._reactKey = `slice-${item.post.uri}-${ this._reactKey = `slice-${feedPost.post.uri}-${
item.reason?.indexedAt || item.post.indexedAt feedPost.reason?.indexedAt || feedPost.post.indexedAt
}` }`
this.items = [toSliceItem(feedPost)]
} }
get uri() { get uri() {
if (this.isFlattenedReply) { return this._feedPost.post.uri
return this.items[1].post.uri
}
return this.items[0].post.uri
}
get ts() {
if (this.items[0].reason?.indexedAt) {
return this.items[0].reason.indexedAt as string
}
return this.items[0].post.indexedAt
} }
get isThread() { get isThread() {
@ -48,31 +52,42 @@ export class FeedViewPostsSlice {
) )
} }
get isFullThread() { get isQuotePost() {
return this.isThread && !this.items[0].reply const embed = this._feedPost.post.embed
} return (
AppBskyEmbedRecord.isView(embed) ||
get rootItem() { AppBskyEmbedRecordWithMedia.isView(embed)
if (this.isFlattenedReply) { )
return this.items[1]
}
return this.items[0]
} }
get isReply() { get isReply() {
return ( return (
AppBskyFeedPost.isRecord(this.rootItem.post.record) && AppBskyFeedPost.isRecord(this._feedPost.post.record) &&
!!this.rootItem.post.record.reply !!this._feedPost.post.record.reply
) )
} }
get source(): ReasonFeedSource | undefined { get reason() {
return this.items.find(item => '__source' in item && !!item.__source) return '__source' in this._feedPost
?.__source as ReasonFeedSource ? (this._feedPost.__source as ReasonFeedSource)
: this._feedPost.reason
} }
get feedContext() { get feedContext() {
return this.items.find(item => item.feedContext)?.feedContext return this._feedPost.feedContext
}
get isRepost() {
const reason = this._feedPost.reason
return AppBskyFeedDefs.isReasonRepost(reason)
}
get includesThreadRoot() {
return !this.items[0].reply
}
get likeCount() {
return this._feedPost.post.likeCount ?? 0
} }
containsUri(uri: string) { containsUri(uri: string) {
@ -97,24 +112,24 @@ export class FeedViewPostsSlice {
if (this.items[0].reply) { if (this.items[0].reply) {
const reply = this.items[0].reply const reply = this.items[0].reply
if (AppBskyFeedDefs.isPostView(reply.parent)) { if (AppBskyFeedDefs.isPostView(reply.parent)) {
this.isFlattenedReply = true
this.items.splice(0, 0, {post: reply.parent}) this.items.splice(0, 0, {post: reply.parent})
} }
} }
} }
isFollowingAllAuthors(userDid: string) { isFollowingAllAuthors(userDid: string) {
const item = this.rootItem const feedPost = this._feedPost
if (item.post.author.did === userDid) { if (feedPost.post.author.did === userDid) {
return true return true
} }
if (AppBskyFeedDefs.isPostView(item.reply?.parent)) { if (AppBskyFeedDefs.isPostView(feedPost.reply?.parent)) {
const parent = item.reply?.parent const parent = feedPost.reply?.parent
if (parent?.author.did === userDid) { if (parent?.author.did === userDid) {
return true return true
} }
return ( return (
parent?.author.viewer?.following && item.post.author.viewer?.following parent?.author.viewer?.following &&
feedPost.post.author.viewer?.following
) )
} }
return false return false
@ -127,7 +142,7 @@ export class NoopFeedTuner {
feed: FeedViewPost[], feed: FeedViewPost[],
_opts?: {dryRun: boolean; maintainOrder: boolean}, _opts?: {dryRun: boolean; maintainOrder: boolean},
): FeedViewPostsSlice[] { ): FeedViewPostsSlice[] {
return feed.map(item => new FeedViewPostsSlice([item])) return feed.map(item => new FeedViewPostsSlice(item))
} }
} }
@ -165,7 +180,7 @@ export class FeedTuner {
}) })
if (maintainOrder) { if (maintainOrder) {
slices = feed.map(item => new FeedViewPostsSlice([item])) slices = feed.map(item => new FeedViewPostsSlice(item))
} else { } else {
// arrange the posts into thread slices // arrange the posts into thread slices
for (let i = feed.length - 1; i >= 0; i--) { for (let i = feed.length - 1; i >= 0; i--) {
@ -192,7 +207,7 @@ export class FeedTuner {
} }
} }
slices.unshift(new FeedViewPostsSlice([item])) slices.unshift(new FeedViewPostsSlice(item))
} }
} }
@ -215,7 +230,7 @@ export class FeedTuner {
// turn non-threads with reply parents into threads // turn non-threads with reply parents into threads
for (const slice of slices) { for (const slice of slices) {
if (!slice.isThread && !slice.items[0].reason && slice.items[0].reply) { if (!slice.isThread && !slice.reason && slice.items[0].reply) {
const reply = slice.items[0].reply const reply = slice.items[0].reply
if ( if (
AppBskyFeedDefs.isPostView(reply.parent) && AppBskyFeedDefs.isPostView(reply.parent) &&
@ -256,8 +271,7 @@ export class FeedTuner {
static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static removeReposts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const reason = slices[i].rootItem.reason if (slices[i].isRepost) {
if (AppBskyFeedDefs.isReasonRepost(reason)) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
@ -266,11 +280,7 @@ export class FeedTuner {
static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) { static removeQuotePosts(tuner: FeedTuner, slices: FeedViewPostsSlice[]) {
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const embed = slices[i].rootItem.post.embed if (slices[i].isQuotePost) {
if (
AppBskyEmbedRecord.isView(embed) ||
AppBskyEmbedRecordWithMedia.isView(embed)
) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
@ -315,21 +325,20 @@ export class FeedTuner {
// remove any replies without at least minLikes likes // remove any replies without at least minLikes likes
for (let i = slices.length - 1; i >= 0; i--) { for (let i = slices.length - 1; i >= 0; i--) {
const slice = slices[i] const slice = slices[i]
if (slice.isFullThread || !slice.isReply) { if (slice.isReply) {
if (slice.isThread && slice.includesThreadRoot) {
continue continue
} }
if (slice.isRepost) {
const item = slice.rootItem
const isRepost = Boolean(item.reason)
if (isRepost) {
continue continue
} }
if ((item.post.likeCount || 0) < minLikes) { if (slice.likeCount < minLikes) {
slices.splice(i, 1) slices.splice(i, 1)
} else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) { } else if (followedOnly && !slice.isFollowingAllAuthors(userDid)) {
slices.splice(i, 1) slices.splice(i, 1)
} }
} }
}
return slices return slices
} }
} }

View File

@ -251,7 +251,7 @@ class MergeFeedSource_Following extends MergeFeedSource {
dryRun: false, dryRun: false,
maintainOrder: true, maintainOrder: true,
}) })
res.data.feed = slices.map(slice => slice.rootItem) res.data.feed = slices.map(slice => slice._feedPost)
return res return res
} }
} }

View File

@ -314,7 +314,7 @@ export function usePostFeedQuery(
if (isDiscover) { if (isDiscover) {
userActionHistory.seen( userActionHistory.seen(
slice.items.map(item => ({ slice.items.map(item => ({
feedContext: item.feedContext, feedContext: slice.feedContext,
likeCount: item.post.likeCount ?? 0, likeCount: item.post.likeCount ?? 0,
repostCount: item.post.repostCount ?? 0, repostCount: item.post.repostCount ?? 0,
replyCount: item.post.replyCount ?? 0, replyCount: item.post.replyCount ?? 0,
@ -329,7 +329,7 @@ export function usePostFeedQuery(
const feedPostSlice: FeedPostSlice = { const feedPostSlice: FeedPostSlice = {
_reactKey: slice._reactKey, _reactKey: slice._reactKey,
_isFeedPostSlice: true, _isFeedPostSlice: true,
rootUri: slice.rootItem.post.uri, rootUri: slice.uri,
isThread: isThread:
slice.items.length > 1 && slice.items.length > 1 &&
slice.items.every( slice.items.every(
@ -365,11 +365,8 @@ export function usePostFeedQuery(
uri: item.post.uri, uri: item.post.uri,
post: item.post, post: item.post,
record: item.post.record, record: item.post.record,
reason: reason: slice.reason,
i === 0 && slice.source feedContext: slice.feedContext,
? slice.source
: item.reason,
feedContext: item.feedContext || slice.feedContext,
moderation: moderations[i], moderation: moderations[i],
parentAuthor, parentAuthor,
isParentBlocked, isParentBlocked,