diff --git a/README.md b/README.md index dfccb543..d92f8f9c 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ useBufferedFeeds({feedsOptions: UseFeedOptions[]}) // preload or buffer feeds in `useFeed().reset()` clears the current feed and refreshes the latest community snapshots before rebuilding it. `useFeed().expandTimeWindow(newerThan)` broadens `newerThan` in place without constructing a different sort name, so older posts can be appended without replacing the feed instance. -Feed and reply sort names are defined by each community record, not by a fixed hooks allowlist. Omit `sortType` to use the preloaded page, or discover the published names with `getAvailablePostSortTypes(community)` and `getAvailableReplySortTypes(comment)`. A requested sort that is not published is not silently replaced with another sort. Hooks preserve protocol page order for unknown custom sorts because their scoring algorithm is not available to the client. +Feed and reply sort names are defined by each community record, not by a fixed hooks allowlist. Omit `sortType` to use the preloaded page, or discover the requestable names with `getAvailablePostSortTypes(community)` and `getAvailableReplySortTypes(comment)`. pkc-js preloads a single sort and only publishes `pageCids` once that page overflows, so when every post or reply fits in the preloaded page the standard sorts (`hot`, `new`, `active` and `top*` for posts; `best`, `new`, `old`, `newFlat` and `oldFlat` for replies) are also requestable and are sorted client-side from that page, including the time window of `top*` timeframe sorts. `getPostPageSortType(community, sortType)` and `getReplyPageSortType(comment, sortType)` return the page that serves a sort. A requested sort that is neither published nor computable client-side is not silently replaced with another sort. Hooks preserve protocol page order for unknown custom sorts because their scoring algorithm is not available to the client. #### Actions Hooks @@ -237,6 +237,10 @@ getPreloadedPostSortType(community: Community): string | undefined getPreloadedReplySortType(comment: Comment): string | undefined resolvePostSortType(community: Community, requestedSortType?: string): string | undefined resolveReplySortType(comment: Comment, requestedSortType?: string): string | undefined +getPostPageSortType(community: Community, requestedSortType?: string): string | undefined // page sort serving the request, e.g. the preloaded page a single-page community re-sorts client-side +getReplyPageSortType(comment: Comment, requestedSortType?: string): string | undefined +getSortTimeframeSeconds(sortType?: string): number | undefined // time window of top*/controversial* timeframe sorts +isFlatSortType(sortType?: string): boolean ``` `createCrosspost` requires a fully loaded comment with `comment.cid` and diff --git a/llms-full.txt b/llms-full.txt index 8f1dad0a..8a2793e8 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -202,7 +202,7 @@ useBufferedFeeds({feedsOptions: UseFeedOptions[]}) // preload or buffer feeds in `useFeed().reset()` clears the current feed and refreshes the latest community snapshots before rebuilding it. `useFeed().expandTimeWindow(newerThan)` broadens `newerThan` in place without constructing a different sort name, so older posts can be appended without replacing the feed instance. -Feed and reply sort names are defined by each community record, not by a fixed hooks allowlist. Omit `sortType` to use the preloaded page, or discover the published names with `getAvailablePostSortTypes(community)` and `getAvailableReplySortTypes(comment)`. A requested sort that is not published is not silently replaced with another sort. Hooks preserve protocol page order for unknown custom sorts because their scoring algorithm is not available to the client. +Feed and reply sort names are defined by each community record, not by a fixed hooks allowlist. Omit `sortType` to use the preloaded page, or discover the requestable names with `getAvailablePostSortTypes(community)` and `getAvailableReplySortTypes(comment)`. pkc-js preloads a single sort and only publishes `pageCids` once that page overflows, so when every post or reply fits in the preloaded page the standard sorts (`hot`, `new`, `active` and `top*` for posts; `best`, `new`, `old`, `newFlat` and `oldFlat` for replies) are also requestable and are sorted client-side from that page, including the time window of `top*` timeframe sorts. `getPostPageSortType(community, sortType)` and `getReplyPageSortType(comment, sortType)` return the page that serves a sort. A requested sort that is neither published nor computable client-side is not silently replaced with another sort. Hooks preserve protocol page order for unknown custom sorts because their scoring algorithm is not available to the client. #### Actions Hooks @@ -269,6 +269,10 @@ getPreloadedPostSortType(community: Community): string | undefined getPreloadedReplySortType(comment: Comment): string | undefined resolvePostSortType(community: Community, requestedSortType?: string): string | undefined resolveReplySortType(comment: Comment, requestedSortType?: string): string | undefined +getPostPageSortType(community: Community, requestedSortType?: string): string | undefined // page sort serving the request, e.g. the preloaded page a single-page community re-sorts client-side +getReplyPageSortType(comment: Comment, requestedSortType?: string): string | undefined +getSortTimeframeSeconds(sortType?: string): number | undefined // time window of top*/controversial* timeframe sorts +isFlatSortType(sortType?: string): boolean ``` `createCrosspost` requires a fully loaded comment with `comment.cid` and diff --git a/src/hooks/feeds/feeds.test.ts b/src/hooks/feeds/feeds.test.ts index f36c8c59..fee39188 100644 --- a/src/hooks/feeds/feeds.test.ts +++ b/src/hooks/feeds/feeds.test.ts @@ -1440,6 +1440,43 @@ describe("feeds", () => { expect(rendered.result.current.feed).toEqual([]); }); + test("serves a standard sort client-side when every post fits in the preloaded page", async () => { + const simulateUpdateEvent = Community.prototype.simulateUpdateEvent; + Community.prototype.simulateUpdateEvent = async function () { + this.posts.pages = { + hot: { + comments: [ + { cid: "newer post", communityAddress: this.address, timestamp: 200, updatedAt: 200 }, + { + cid: "bumped post", + communityAddress: this.address, + timestamp: 100, + lastReplyTimestamp: 300, + updatedAt: 300, + }, + ], + }, + }; + this.posts.pageCids = {}; + this.updatedAt = 1; + this.updatingState = "succeeded"; + this.emit("update", this); + this.emit("updatingstatechange", "succeeded"); + }; + + try { + rendered.rerender({ communityAddresses: ["single page community"], sortType: "active" }); + await waitFor(() => rendered.result.current.feed.length === 2); + expect(rendered.result.current.feed.map((post: Comment) => post.cid)).toEqual([ + "bumped post", + "newer post", + ]); + expect(rendered.result.current.hasMore).toBe(false); + } finally { + Community.prototype.simulateUpdateEvent = simulateUpdateEvent; + } + }); + describe("getPage only has 1 page", () => { const getPage = Pages.prototype.getPage; diff --git a/src/hooks/feeds/feeds.ts b/src/hooks/feeds/feeds.ts index 9e75289b..816c7b6b 100644 --- a/src/hooks/feeds/feeds.ts +++ b/src/hooks/feeds/feeds.ts @@ -27,7 +27,8 @@ import { serializeFeedKey } from "../../lib/serialize-feed-key"; /** * @param communities - The communities to fetch, e.g. [{name: 'memes.eth'}, {publicKey: '12D3KooW...'}] - * @param sortType - A sort name published by the community. Omit it to use the preloaded sort. + * @param sortType - A sort name published by the community, or a standard sort computed client-side + * when every post fits in the preloaded page. Omit it to use the preloaded sort. * @param acountName - The nickname of the account, e.g. 'Account KoXpxTwfnjA5'. If no accountName is provided, use * the active account. */ diff --git a/src/hooks/replies.test.ts b/src/hooks/replies.test.ts index a68f957a..43722d23 100644 --- a/src/hooks/replies.test.ts +++ b/src/hooks/replies.test.ts @@ -901,7 +901,7 @@ describe("replies", () => { await testUtils.resetDatabasesAndStores(); }); - test("requested missing sorts do not use the single preloaded page", async () => { + test("requested standard sorts are served from the single preloaded page", async () => { const comment = { cid: "comment cid 1", postCid: "comment cid 1", @@ -912,7 +912,10 @@ describe("replies", () => { replies: { pages: { best: { - comments: [{ cid: "best reply", communityAddress: "sub", timestamp: 1, depth: 1 }], + comments: [ + { cid: "older reply", communityAddress: "sub", timestamp: 1, depth: 1 }, + { cid: "newer reply", communityAddress: "sub", timestamp: 2, depth: 1 }, + ], }, }, pageCids: {}, @@ -920,11 +923,27 @@ describe("replies", () => { }; rendered.rerender({ comment, sortType: "new" }); - await waitFor(() => rendered.result.current.hasMore === false); - expect(rendered.result.current.replies).toEqual([]); + await waitFor(() => rendered.result.current.replies.length === 2); + expect(rendered.result.current.replies.map((reply: any) => reply.cid)).toEqual([ + "newer reply", + "older reply", + ]); + expect(rendered.result.current.hasMore).toBe(false); rendered.rerender({ comment, sortType: "old" }); - await waitFor(() => rendered.result.current.hasMore === false); + await waitFor(() => rendered.result.current.replies[0]?.cid === "older reply"); + expect(rendered.result.current.replies.map((reply: any) => reply.cid)).toEqual([ + "older reply", + "newer reply", + ]); + expect(rendered.result.current.hasMore).toBe(false); + + // a custom sort the comment does not publish is still not substituted + rendered.rerender({ comment, sortType: "customSort" }); + await waitFor( + () => + rendered.result.current.hasMore === false && rendered.result.current.replies.length === 0, + ); expect(rendered.result.current.replies).toEqual([]); }); }); @@ -1589,7 +1608,7 @@ describe("replies", () => { expect(rendered.result.current.repliesDepth3.replies.length).toBeGreaterThan(0); }); - test("nested replies do not substitute best when new is requested", async () => { + test("nested replies serve new client-side from their complete preloaded best page", async () => { // mock nested replies on pages const pageToGet = Pages.prototype.pageToGet; Pages.prototype.pageToGet = function (pageCid) { @@ -1599,11 +1618,12 @@ describe("replies", () => { rendered.rerender({ commentCid: "comment cid 1", sortType: "new" }); - // as soon as depth 1 has replies, all other depths also should - await waitFor(() => rendered.result.current.repliesDepth1.replies.length > 0); + // nested replies only preload a complete 'best' page, which also serves 'new' + await waitFor(() => rendered.result.current.repliesDepth3.replies.length > 0); expect(rendered.result.current.repliesDepth1.replies.length).toBeGreaterThan(0); - expect(rendered.result.current.repliesDepth2.replies.length).toBe(0); - expect(rendered.result.current.repliesDepth3.replies.length).toBe(0); + expect(rendered.result.current.repliesDepth2.replies.length).toBeGreaterThan(0); + expect(rendered.result.current.repliesDepth3.replies.length).toBeGreaterThan(0); + expect(rendered.result.current.repliesDepth2.replies[0].cid).toMatch("nested 1"); Pages.prototype.pageToGet = pageToGet; }); diff --git a/src/index.ts b/src/index.ts index 18b96eec..642863b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -94,6 +94,10 @@ import { getAvailableReplySortTypes, getPreloadedPostSortType, getPreloadedReplySortType, + getPostPageSortType, + getReplyPageSortType, + getSortTimeframeSeconds, + isFlatSortType, resolvePostSortType, resolveReplySortType, } from "./lib/page-sorts"; @@ -179,6 +183,10 @@ export { getAvailableReplySortTypes, getPreloadedPostSortType, getPreloadedReplySortType, + getPostPageSortType, + getReplyPageSortType, + getSortTimeframeSeconds, + isFlatSortType, resolvePostSortType, resolveReplySortType, }; @@ -261,6 +269,10 @@ const hooks = { getAvailableReplySortTypes, getPreloadedPostSortType, getPreloadedReplySortType, + getPostPageSortType, + getReplyPageSortType, + getSortTimeframeSeconds, + isFlatSortType, resolvePostSortType, resolveReplySortType, }; diff --git a/src/lib/page-sorts.test.ts b/src/lib/page-sorts.test.ts index ee3f849b..bbf2aa77 100644 --- a/src/lib/page-sorts.test.ts +++ b/src/lib/page-sorts.test.ts @@ -1,8 +1,12 @@ +import feedSorter from "../stores/feeds/feed-sorter"; import { getAvailablePostSortTypes, getAvailableReplySortTypes, + getPostPageSortType, getPreloadedPostSortType, getPreloadedReplySortType, + getReplyPageSortType, + getSortTimeframeSeconds, resolvePostSortType, resolveReplySortType, } from "./page-sorts"; @@ -36,6 +40,14 @@ describe("page sort helpers", () => { test("returns undefined instead of substituting a missing requested sort", () => { expect(resolvePostSortType(community as any, "missing")).toBeUndefined(); expect(resolveReplySortType(comment as any, "missing")).toBeUndefined(); + expect(getPostPageSortType(community as any, "missing")).toBeUndefined(); + expect(getReplyPageSortType(comment as any, "missing")).toBeUndefined(); + }); + + test("reads the published page of a requested sort", () => { + expect(getPostPageSortType(community as any, "newest")).toBe("newest"); + expect(getPostPageSortType(community as any)).toBe("sage"); + expect(getReplyPageSortType(comment as any, "nestedNewest")).toBe("nestedNewest"); }); test("falls back to the first pageCid only when no preloaded page exists", () => { @@ -45,4 +57,140 @@ describe("page sort helpers", () => { expect(getAvailablePostSortTypes()).toEqual([]); expect(getAvailableReplySortTypes()).toEqual([]); }); + + describe("complete preloaded pages", () => { + const singlePageCommunity = { posts: { pages: { hot: { comments: [{ cid: "post" }] } } } }; + const singlePageComment = { + replies: { pages: { best: { comments: [{ cid: "reply" }] } }, pageCids: {} }, + }; + + test("serves every standard sort from a complete preloaded page", () => { + expect(getAvailablePostSortTypes(singlePageCommunity as any)).toEqual([ + "hot", + "new", + "active", + "topHour", + "topDay", + "topWeek", + "topMonth", + "topYear", + "topAll", + ]); + expect(getAvailableReplySortTypes(singlePageComment as any)).toEqual([ + "best", + "new", + "old", + "newFlat", + "oldFlat", + ]); + expect(resolvePostSortType(singlePageCommunity as any, "active")).toBe("active"); + expect(getPostPageSortType(singlePageCommunity as any, "active")).toBe("hot"); + expect(getPostPageSortType(singlePageCommunity as any, "hot")).toBe("hot"); + expect(resolveReplySortType(singlePageComment as any, "old")).toBe("old"); + expect(getReplyPageSortType(singlePageComment as any, "newFlat")).toBe("best"); + expect(getPreloadedPostSortType(singlePageCommunity as any)).toBe("hot"); + expect(resolvePostSortType(singlePageCommunity as any)).toBe("hot"); + }); + + test("keeps custom sorts unavailable", () => { + expect(resolvePostSortType(singlePageCommunity as any, "sage")).toBeUndefined(); + expect(getPostPageSortType(singlePageCommunity as any, "sage")).toBeUndefined(); + expect(resolveReplySortType(singlePageComment as any, "chronological")).toBeUndefined(); + }); + + test("stays strict once a page continues or pageCids are published", () => { + const pagedCommunity = { posts: { pages: { hot: { comments: [], nextCid: "hot-next" } } } }; + const cidsCommunity = { + posts: { pages: { hot: { comments: [] } }, pageCids: { new: "new-cid" } }, + }; + expect(getAvailablePostSortTypes(pagedCommunity as any)).toEqual(["hot"]); + expect(resolvePostSortType(pagedCommunity as any, "active")).toBeUndefined(); + expect(getAvailablePostSortTypes(cidsCommunity as any)).toEqual(["hot", "new"]); + expect(getPostPageSortType(cidsCommunity as any, "new")).toBe("new"); + expect(getPostPageSortType(cidsCommunity as any, "active")).toBeUndefined(); + expect(getAvailablePostSortTypes({ posts: { pages: {} } } as any)).toEqual([]); + }); + + test("does not advertise flat sorts for a nested reply", () => { + const nestedReply = { + depth: 1, + parentCid: "post-cid", + replies: { pages: { best: { comments: [{ cid: "nested" }] } } }, + }; + expect(getAvailableReplySortTypes(nestedReply as any)).toEqual(["best", "new", "old"]); + expect(resolveReplySortType(nestedReply as any, "old")).toBe("old"); + expect(resolveReplySortType(nestedReply as any, "newFlat")).toBeUndefined(); + expect(getReplyPageSortType(nestedReply as any, "newFlat")).toBeUndefined(); + }); + + test("does not treat a timeframe-windowed preloaded page as the complete set", () => { + const windowedCommunity = { posts: { pages: { topDay: { comments: [{ cid: "post" }] } } } }; + expect(getAvailablePostSortTypes(windowedCommunity as any)).toEqual(["topDay"]); + expect(resolvePostSortType(windowedCommunity as any, "active")).toBeUndefined(); + expect(getPostPageSortType(windowedCommunity as any, "topDay")).toBe("topDay"); + expect(getPostPageSortType(windowedCommunity as any, "hot")).toBeUndefined(); + }); + + test("advertises flat sorts from a hierarchical page only when the nested tree is complete", () => { + const withNestedReplies = (replies: unknown) => ({ + depth: 0, + replies: { + pages: { best: { comments: [{ cid: "reply", replyCount: 1, replies }] } }, + }, + }); + const completeTree = withNestedReplies({ + pages: { best: { comments: [{ cid: "nested", replyCount: 0 }] } }, + }); + expect(getAvailableReplySortTypes(completeTree as any)).toEqual([ + "best", + "new", + "old", + "newFlat", + "oldFlat", + ]); + expect(getReplyPageSortType(completeTree as any, "newFlat")).toBe("best"); + + const continuedTree = withNestedReplies({ + pages: { best: { comments: [{ cid: "nested" }], nextCid: "nested-next" } }, + }); + const pagedTree = withNestedReplies({ pages: {}, pageCids: { new: "nested-new-cid" } }); + const missingTree = withNestedReplies(undefined); + for (const comment of [continuedTree, pagedTree, missingTree]) { + expect(getAvailableReplySortTypes(comment as any)).toEqual(["best", "new", "old"]); + expect(resolveReplySortType(comment as any, "newFlat")).toBeUndefined(); + expect(resolveReplySortType(comment as any, "new")).toBe("new"); + } + }); + + test("only serves flat sorts from a flat preloaded page", () => { + const flatComment = { replies: { pages: { newFlat: { comments: [] } } } }; + expect(getAvailableReplySortTypes(flatComment as any)).toEqual(["newFlat", "oldFlat"]); + expect(getReplyPageSortType(flatComment as any, "oldFlat")).toBe("newFlat"); + expect(resolveReplySortType(flatComment as any, "best")).toBeUndefined(); + }); + + test("every client-served sort has a client sorter", () => { + const feed = [ + { cid: "a", timestamp: 1, upvoteCount: 0, downvoteCount: 0 }, + { cid: "b", timestamp: 2, upvoteCount: 1, downvoteCount: 0 }, + ]; + const sortTypes = [ + ...getAvailablePostSortTypes(singlePageCommunity as any), + ...getAvailableReplySortTypes(singlePageComment as any), + ]; + for (const sortType of sortTypes) { + // the sorter returns the same array only for sort names it cannot compute + expect(feedSorter.sort(sortType, feed)).not.toBe(feed); + } + }); + + test("knows the time window of timeframe sorts", () => { + expect(getSortTimeframeSeconds("topHour")).toBe(3600); + expect(getSortTimeframeSeconds("topWeek")).toBe(604800); + expect(getSortTimeframeSeconds("controversialDay")).toBe(86400); + expect(getSortTimeframeSeconds("topAll")).toBeUndefined(); + expect(getSortTimeframeSeconds("active")).toBeUndefined(); + expect(getSortTimeframeSeconds(undefined)).toBeUndefined(); + }); + }); }); diff --git a/src/lib/page-sorts.ts b/src/lib/page-sorts.ts index f1182bd2..bde039b1 100644 --- a/src/lib/page-sorts.ts +++ b/src/lib/page-sorts.ts @@ -5,7 +5,51 @@ type PagesRecord = { pageCids?: Record; }; -const getAvailablePageSortTypes = (record?: PagesRecord): string[] => { +type PreloadedPage = { comments?: unknown; nextCid?: string } | undefined; + +// Standard pkc-js sorts (pkc-js src/pages/util.ts) whose scoring the feed sorter reproduces. pkc-js +// preloads a single sort and only publishes pageCids once that page overflows, so a record whose +// comments all fit in the preloaded page can serve any of these by re-sorting that page client-side. +const CLIENT_SORTABLE_POST_SORT_TYPES = [ + "hot", + "new", + "active", + "topHour", + "topDay", + "topWeek", + "topMonth", + "topYear", + "topAll", +]; +const CLIENT_SORTABLE_REPLY_SORT_TYPES = ["best", "new", "old", "newFlat", "oldFlat"]; + +// pkc-js only publishes flat sorts for a post's replies (REPLY_REPLIES_SORT_TYPES has none), so a +// nested reply must not advertise them while its replies still fit in the preloaded page +const getClientSortableReplySortTypes = (comment?: Comment): string[] => { + const isPost = comment?.depth !== undefined ? comment.depth === 0 : !comment?.parentCid; + return isPost + ? CLIENT_SORTABLE_REPLY_SORT_TYPES + : CLIENT_SORTABLE_REPLY_SORT_TYPES.filter((sortType) => !isFlatSortType(sortType)); +}; + +// pkc-js TIMEFRAMES_TO_SECONDS, applied client-side when a timeframe sort is computed from a +// preloaded page instead of a page the community windowed itself +const SORT_TIMEFRAMES_SECONDS: Record = { + Hour: 3600, + Day: 86400, + Week: 604800, + Month: 2629746, + Year: 31557600, +}; + +export const isFlatSortType = (sortType?: string): boolean => Boolean(sortType?.endsWith("Flat")); + +export const getSortTimeframeSeconds = (sortType?: string): number | undefined => { + const timeframe = sortType?.match(/^(?:top|controversial)(Hour|Day|Week|Month|Year)$/)?.[1]; + return timeframe ? SORT_TIMEFRAMES_SECONDS[timeframe] : undefined; +}; + +const getPublishedPageSortTypes = (record?: PagesRecord): string[] => { const sortTypes = new Set(); for (const sortType of Object.keys(record?.pages || {})) { if (sortType) sortTypes.add(sortType); @@ -16,16 +60,119 @@ const getAvailablePageSortTypes = (record?: PagesRecord): string[] => { return [...sortTypes]; }; +// preloaded pages holding the record's complete comment set: no page continues with a nextCid and +// no pageCids are published (pkc-js publishes pageCids for every sort once the preloaded page overflows) +const getCompletePreloadedPageSortTypes = (record?: PagesRecord): string[] => { + if (Object.keys(record?.pageCids || {}).length > 0) { + return []; + } + const pages = record?.pages || {}; + const sortTypes = Object.keys(pages).filter((sortType) => + Array.isArray((pages[sortType] as PreloadedPage)?.comments), + ); + if ( + !sortTypes.length || + sortTypes.some((sortType) => (pages[sortType] as PreloadedPage)?.nextCid) + ) { + return []; + } + // a page windowed by a timeframe sort only holds that window, never the complete set + return sortTypes.filter((sortType) => !getSortTimeframeSeconds(sortType)); +}; + +// a flat sort flattens the whole reply tree, so every nested reply chain must be complete too +const hasCompleteReplyTree = (page: PreloadedPage): boolean => + ((page?.comments as Comment[] | undefined) || []).every((comment) => { + const completeSortTypes = getCompletePreloadedPageSortTypes(comment?.replies); + if (!completeSortTypes.length) { + // no preloaded replies is only complete when the comment reports none, and a continued or + // paged chain is never complete + return !(comment?.replyCount > 0) && !Object.keys(comment?.replies?.pages || {}).length; + } + const hierarchicalSortType = completeSortTypes.find((sortType) => !isFlatSortType(sortType)); + return hierarchicalSortType === undefined + ? true + : hasCompleteReplyTree(comment.replies.pages[hierarchicalSortType]); + }); + +// a flat page cannot rebuild the reply tree, so it only serves flat sorts; a hierarchical page +// serves flat sorts only when its nested reply chains are complete +const getClientSortableSortTypes = ( + record: PagesRecord | undefined, + clientSortableSortTypes: string[], +): string[] => { + const completeSortTypes = getCompletePreloadedPageSortTypes(record); + if (!completeSortTypes.length) { + return []; + } + const hierarchicalSortType = completeSortTypes.find((sortType) => !isFlatSortType(sortType)); + if (hierarchicalSortType === undefined) { + return clientSortableSortTypes.filter(isFlatSortType); + } + const canFlatten = + completeSortTypes.some(isFlatSortType) || + hasCompleteReplyTree(record?.pages?.[hierarchicalSortType] as PreloadedPage); + return canFlatten + ? clientSortableSortTypes + : clientSortableSortTypes.filter((sortType) => !isFlatSortType(sortType)); +}; + +const getAvailablePageSortTypes = ( + record: PagesRecord | undefined, + clientSortableSortTypes: string[], +): string[] => { + const sortTypes = getPublishedPageSortTypes(record); + for (const sortType of getClientSortableSortTypes(record, clientSortableSortTypes)) { + if (!sortTypes.includes(sortType)) sortTypes.push(sortType); + } + return sortTypes; +}; + const getPreloadedPageSortType = (record?: PagesRecord): string | undefined => { const preloadedSortType = Object.keys(record?.pages || {}).find(Boolean); - return preloadedSortType || getAvailablePageSortTypes(record)[0]; + return preloadedSortType || getPublishedPageSortTypes(record)[0]; +}; + +const resolvePageSortType = ( + record: PagesRecord | undefined, + requestedSortType: string | undefined, + clientSortableSortTypes: string[], +): string | undefined => { + if (requestedSortType !== undefined) { + return getAvailablePageSortTypes(record, clientSortableSortTypes).includes(requestedSortType) + ? requestedSortType + : undefined; + } + return getPreloadedPageSortType(record); +}; + +// the page that serves a resolved sort: its published page, or the complete preloaded page the +// client re-sorts (a flat sort prefers a complete flat page, else flattens a hierarchical one) +const getPageSortTypeToRead = ( + record: PagesRecord | undefined, + sortType: string | undefined, +): string | undefined => { + if (sortType === undefined) { + return undefined; + } + if (getPublishedPageSortTypes(record).includes(sortType)) { + return sortType; + } + const completeSortTypes = getCompletePreloadedPageSortTypes(record); + const hierarchicalSortType = completeSortTypes.find( + (preloadedSortType) => !isFlatSortType(preloadedSortType), + ); + if (isFlatSortType(sortType)) { + return completeSortTypes.find(isFlatSortType) ?? hierarchicalSortType; + } + return hierarchicalSortType; }; export const getAvailablePostSortTypes = (community?: Community): string[] => - getAvailablePageSortTypes(community?.posts); + getAvailablePageSortTypes(community?.posts, CLIENT_SORTABLE_POST_SORT_TYPES); export const getAvailableReplySortTypes = (comment?: Comment): string[] => - getAvailablePageSortTypes(comment?.replies); + getAvailablePageSortTypes(comment?.replies, getClientSortableReplySortTypes(comment)); export const getPreloadedPostSortType = (community?: Community): string | undefined => getPreloadedPageSortType(community?.posts); @@ -36,23 +183,29 @@ export const getPreloadedReplySortType = (comment?: Comment): string | undefined export const resolvePostSortType = ( community: Community | undefined, requestedSortType?: string, -): string | undefined => { - if (requestedSortType !== undefined) { - return getAvailablePostSortTypes(community).includes(requestedSortType) - ? requestedSortType - : undefined; - } - return getPreloadedPostSortType(community); -}; +): string | undefined => + resolvePageSortType(community?.posts, requestedSortType, CLIENT_SORTABLE_POST_SORT_TYPES); export const resolveReplySortType = ( comment: Comment | undefined, requestedSortType?: string, -): string | undefined => { - if (requestedSortType !== undefined) { - return getAvailableReplySortTypes(comment).includes(requestedSortType) - ? requestedSortType - : undefined; - } - return getPreloadedReplySortType(comment); -}; +): string | undefined => + resolvePageSortType( + comment?.replies, + requestedSortType, + getClientSortableReplySortTypes(comment), + ); + +// the `community.posts` page sort that serves a request, e.g. the preloaded `hot` page when a +// single-page community serves `active` client-side; undefined when the sort cannot be served +export const getPostPageSortType = ( + community: Community | undefined, + requestedSortType?: string, +): string | undefined => + getPageSortTypeToRead(community?.posts, resolvePostSortType(community, requestedSortType)); + +export const getReplyPageSortType = ( + comment: Comment | undefined, + requestedSortType?: string, +): string | undefined => + getPageSortTypeToRead(comment?.replies, resolveReplySortType(comment, requestedSortType)); diff --git a/src/stores/communities-pages/communities-pages-store.test.ts b/src/stores/communities-pages/communities-pages-store.test.ts index 50e55865..67485d20 100644 --- a/src/stores/communities-pages/communities-pages-store.test.ts +++ b/src/stores/communities-pages/communities-pages-store.test.ts @@ -413,6 +413,26 @@ describe("communities pages store", () => { ); }); + test("getCommunityFirstPageCid reads the complete preloaded page for a client-served sort", () => { + const community = { + address: "addr", + posts: { pages: { hot: { comments: [{ cid: "c1" }] } } }, + }; + // 'active' is served from the preloaded hot page, which has no next page + expect(getCommunityFirstPageCid(community as any, "active", "posts")).toBeUndefined(); + + const pagedCommunity = { + address: "addr", + posts: { + pages: { hot: { nextCid: "hot-next", comments: [{ cid: "c1" }] } }, + pageCids: { active: "active-first-page" }, + }, + }; + expect(getCommunityFirstPageCid(pagedCommunity as any, "active", "posts")).toBe( + "active-first-page", + ); + }); + test("getCommunityFirstPageCid defaults pageType to posts", () => { const community = { address: "addr", diff --git a/src/stores/communities-pages/communities-pages-store.ts b/src/stores/communities-pages/communities-pages-store.ts index dea1f848..c1ca00bb 100644 --- a/src/stores/communities-pages/communities-pages-store.ts +++ b/src/stores/communities-pages/communities-pages-store.ts @@ -20,7 +20,7 @@ import { getPkcCreateCommunity, normalizeCommentCommunityAddress, } from "../../lib/pkc-compat"; -import { resolvePostSortType } from "../../lib/page-sorts"; +import { getPostPageSortType } from "../../lib/page-sorts"; const communitiesPagesDatabase = localForageLru.createInstance({ name: "bitsocialReactHooks-communitiesPages", @@ -416,16 +416,16 @@ export const getCommunityFirstPageCid = ( sortType === undefined || (typeof sortType === "string" && sortType.length > 0), `getCommunityFirstPageCid sortType '${sortType}' invalid`, ); - const resolvedSortType = - pageType === "posts" ? resolvePostSortType(community, sortType) : sortType; - if (!resolvedSortType) { + // posts served client-side from a complete preloaded page read that page instead of their own + const pageSortType = pageType === "posts" ? getPostPageSortType(community, sortType) : sortType; + if (!pageSortType) { return; } // community has preloaded posts for sort type - if (community[pageType]?.pages?.[resolvedSortType]?.comments) { - return community[pageType]?.pages?.[resolvedSortType]?.nextCid; + if (community[pageType]?.pages?.[pageSortType]?.comments) { + return community[pageType]?.pages?.[pageSortType]?.nextCid; } - return community[pageType]?.pageCids?.[resolvedSortType]; + return community[pageType]?.pageCids?.[pageSortType]; // TODO: if a loaded community doesn't have a first page, it's unclear what we should do // should we try to use another sort type by default, like 'hot', or should we just ignore it? diff --git a/src/stores/feeds/utils.test.ts b/src/stores/feeds/utils.test.ts index 068f3d0c..671021e4 100644 --- a/src/stores/feeds/utils.test.ts +++ b/src/stores/feeds/utils.test.ts @@ -505,32 +505,172 @@ describe("feeds utils", () => { expect(feeds.feed1).toEqual([]); }); - test("does not substitute a different single-page sort", () => { - const feedComment = { - cid: "fallback-cid", - communityAddress: "sub1", - timestamp: 1, - }; + test("sorts a complete preloaded page client-side for a standard sort", () => { const communities = { sub1: { address: "sub1", updatedAt: 1, posts: { pages: { - otherSort: { comments: [feedComment], nextCid: undefined }, + hot: { + comments: [ + { cid: "newer-post", communityAddress: "sub1", timestamp: 200 }, + { + cid: "bumped-post", + communityAddress: "sub1", + timestamp: 100, + lastReplyTimestamp: 300, + }, + ], + }, }, }, }, }; const feedsOptions = { feed1: { + communities: toCommunities(["sub1"]), + sortType: "active", + accountId: mockAccountId, + }, + feed2: { communities: toCommunities(["sub1"]), sortType: "new", accountId: mockAccountId, }, }; const feeds = getFilteredSortedFeeds(feedsOptions, communities, {}, makeMockAccounts()); + expect(feeds.feed1.map((post: any) => post.cid)).toEqual(["bumped-post", "newer-post"]); + expect(feeds.feed2.map((post: any) => post.cid)).toEqual(["newer-post", "bumped-post"]); + expect( + getFeedsHaveMore( + feedsOptions, + { feed1: [], feed2: [] }, + communities, + {}, + makeMockAccounts(), + ), + ).toEqual({ feed1: false, feed2: false }); + }); + + test("does not substitute a preloaded page for a custom sort", () => { + const communities = { + sub1: { + address: "sub1", + updatedAt: 1, + posts: { + pages: { + hot: { + comments: [{ cid: "fallback-cid", communityAddress: "sub1", timestamp: 1 }], + }, + }, + }, + }, + }; + const feedsOptions = { + feed1: { + communities: toCommunities(["sub1"]), + sortType: "customSort", + accountId: mockAccountId, + }, + }; + const feeds = getFilteredSortedFeeds(feedsOptions, communities, {}, makeMockAccounts()); expect(feeds.feed1).toEqual([]); + expect( + getFeedsHaveMore(feedsOptions, { feed1: [] }, communities, {}, makeMockAccounts()), + ).toEqual({ + feed1: false, + }); + }); + + test("applies the time window when computing a timeframe sort from the preloaded page", () => { + const now = Math.floor(Date.now() / 1000); + const communities = { + sub1: { + address: "sub1", + updatedAt: 1, + posts: { + pages: { + hot: { + comments: [ + { + cid: "old-top", + communityAddress: "sub1", + timestamp: now - 2 * 86400, + upvoteCount: 50, + downvoteCount: 0, + }, + { + cid: "recent", + communityAddress: "sub1", + timestamp: now - 60, + upvoteCount: 1, + downvoteCount: 0, + }, + { + cid: "pinned-old", + communityAddress: "sub1", + timestamp: now - 3 * 86400, + upvoteCount: 0, + downvoteCount: 0, + pinned: true, + }, + { + cid: "pinned-later", + communityAddress: "sub1", + timestamp: now - 4 * 86400, + updatedAt: now - 4 * 86400, + upvoteCount: 0, + downvoteCount: 0, + }, + ], + }, + }, + }, + }, + }; + // the fresher cached version of the old post was pinned after the page was published + const freshestComments = { + "pinned-later": { + cid: "pinned-later", + communityAddress: "sub1", + timestamp: now - 4 * 86400, + updatedAt: now, + upvoteCount: 0, + downvoteCount: 0, + pinned: true, + }, + }; + const feedsOptions = { + topDay: { + communities: toCommunities(["sub1"]), + sortType: "topDay", + accountId: mockAccountId, + }, + topAll: { + communities: toCommunities(["sub1"]), + sortType: "topAll", + accountId: mockAccountId, + }, + }; + const feeds = getFilteredSortedFeeds( + feedsOptions, + communities, + {}, + makeMockAccounts(), + freshestComments as any, + ); + expect(feeds.topDay.map((post: any) => post.cid)).toEqual([ + "pinned-old", + "pinned-later", + "recent", + ]); + expect(feeds.topAll.map((post: any) => post.cid)).toEqual([ + "pinned-old", + "pinned-later", + "old-top", + "recent", + ]); }); test("uses the resolved default active sort when filtering by time", () => { diff --git a/src/stores/feeds/utils.ts b/src/stores/feeds/utils.ts index bf907794..0989a217 100644 --- a/src/stores/feeds/utils.ts +++ b/src/stores/feeds/utils.ts @@ -25,7 +25,11 @@ import { getMatchingCommunityRefKeys, } from "../../lib/community-ref"; import Logger from "@pkcprotocol/pkc-logger"; -import { resolvePostSortType } from "../../lib/page-sorts"; +import { + getPostPageSortType, + getSortTimeframeSeconds, + resolvePostSortType, +} from "../../lib/page-sorts"; const log = Logger("bitsocial-react-hooks:feeds:stores"); const getFeedCommunityRefs = (feedOptions: Partial): CommunityLookupRef[] => @@ -174,6 +178,7 @@ export const getFilteredSortedFeeds = ( // use community preloaded posts if any const preloadedPosts = getPreloadedPosts(community, sortType); if (preloadedPosts) { + const clientTimeframeTimestamp = getClientSortTimeframeTimestamp(community, sortType); for (const post of preloadedPosts) { // posts are manually validated, could have fake communityAddress if ( @@ -182,9 +187,19 @@ export const getFilteredSortedFeeds = ( break; } const nextPost = getFeedPost(post, communityRef, community, modQueue, freshestComments); - if (nextPost) { - bufferedFeedPosts.push(nextPost); + if (!nextPost) { + continue; + } + // window the reconciled post: pinned is mutable moderation state, and like the pages a + // community windows itself, pinned posts stay regardless of age + if ( + clientTimeframeTimestamp !== undefined && + !nextPost.pinned && + nextPost.timestamp <= clientTimeframeTimestamp + ) { + continue; } + bufferedFeedPosts.push(nextPost); } } @@ -277,11 +292,21 @@ export const getFilteredSortedFeeds = ( }; const getPreloadedPosts = (community: Community, sortType?: string) => { - const resolvedSortType = resolvePostSortType(community, sortType); - if (!resolvedSortType) { + const pageSortType = getPostPageSortType(community, sortType); + if (!pageSortType) { + return; + } + return community.posts?.pages?.[pageSortType]?.comments; +}; + +// a timeframe sort computed client-side from a complete preloaded page applies its own window, +// like the pages a community publishes for that sort would +const getClientSortTimeframeTimestamp = (community: Community, sortType?: string) => { + const timeframeSeconds = getSortTimeframeSeconds(sortType); + if (!timeframeSeconds || getPostPageSortType(community, sortType) === sortType) { return; } - return community.posts?.pages?.[resolvedSortType]?.comments; + return Math.floor(Date.now() / 1000) - timeframeSeconds; }; export const getLoadedFeeds = async ( diff --git a/src/stores/replies-pages/replies-pages-store.ts b/src/stores/replies-pages/replies-pages-store.ts index 1f897fef..4e948879 100644 --- a/src/stores/replies-pages/replies-pages-store.ts +++ b/src/stores/replies-pages/replies-pages-store.ts @@ -9,7 +9,7 @@ import { addChildrenRepliesFeedsToAddToStore } from "./utils"; import localForageLru from "../../lib/localforage-lru"; import createStore from "zustand"; import assert from "assert"; -import { resolveReplySortType } from "../../lib/page-sorts"; +import { getReplyPageSortType, resolveReplySortType } from "../../lib/page-sorts"; const repliesPagesDatabase = localForageLru.createInstance({ name: "bitsocialReactHooks-repliesPages", @@ -305,15 +305,16 @@ export const getRepliesFirstPageCid = (comment: Comment, sortType?: string) => { sortType === undefined || (typeof sortType === "string" && sortType.length > 0), `getRepliesFirstPageCid sortType '${sortType}' invalid`, ); - const resolvedSortType = resolveReplySortType(comment, sortType); - if (!resolvedSortType) { + // replies served client-side from a complete preloaded page read that page instead of their own + const pageSortType = getReplyPageSortType(comment, sortType); + if (!pageSortType) { return; } // comment has preloaded replies for sort type - if (comment.replies?.pages?.[resolvedSortType]?.comments) { - return comment.replies?.pages?.[resolvedSortType]?.nextCid; + if (comment.replies?.pages?.[pageSortType]?.comments) { + return comment.replies?.pages?.[pageSortType]?.nextCid; } - return comment.replies?.pageCids?.[resolvedSortType]; + return comment.replies?.pageCids?.[pageSortType]; // TODO: if a loaded comment doesn't have a first page, it's unclear what we should do // should we try to use another sort type by default, like 'best', or should we just ignore it? diff --git a/src/stores/replies/replies-store.test.ts b/src/stores/replies/replies-store.test.ts index 1be727db..2434e300 100644 --- a/src/stores/replies/replies-store.test.ts +++ b/src/stores/replies/replies-store.test.ts @@ -368,6 +368,39 @@ describe("replies store", () => { expect(feedsForComment[0]).toContain(commentCid); }); + test("addFeedToStoreOrUpdateComment registers nested feeds through the page serving a client-side sort", async () => { + const commentCid = "client-sort-feed-unique-cid"; + const nestedCid = "nested-reply-client-sort-cid"; + const comment = new MockComment({ cid: commentCid }); + // the replies all fit in the preloaded best page, which also serves 'new' + (comment as any).replies = { + pages: { + best: { + comments: [{ cid: nestedCid, replies: { pages: {} }, depth: 1 }], + }, + }, + }; + + act(() => { + rendered.result.current.addFeedToStoreOrUpdateComment(comment, { + sortType: "new", + commentCid, + accountId: mockAccount.id, + }); + }); + const feedName = feedOptionsToFeedName({ + sortType: "new", + commentCid, + accountId: mockAccount.id, + }); + await waitFor(() => rendered.result.current.feedsOptions[feedName]); + + const feedsForNestedReply = Object.keys(rendered.result.current.feedsOptions).filter((fn) => + fn.includes(nestedCid), + ); + expect(feedsForNestedReply).toHaveLength(1); + }); + test("resetFeed resets page to 1 and clears loaded/updated", async () => { const commentCid = "reset-feed-cid"; const feedOptions = { sortType: "new", commentCid, accountId: mockAccount.id }; diff --git a/src/stores/replies/replies-store.ts b/src/stores/replies/replies-store.ts index cb9c1d3b..4c1bdb1b 100644 --- a/src/stores/replies/replies-store.ts +++ b/src/stores/replies/replies-store.ts @@ -18,6 +18,7 @@ import accountsStore from "../accounts"; import repliesCommentsStore from "./replies-comments-store"; import repliesPagesStore from "../replies-pages"; import { serializeFeedKey } from "../../lib/serialize-feed-key"; +import { getReplyPageSortType } from "../../lib/page-sorts"; import { getFeedsCommentsFirstPageCids, getLoadedFeeds, @@ -198,7 +199,10 @@ const repliesStore = createStore((setState: Function, getState: Fu // flat doesn't need nested feeds if (!feedOptions.flat) { - for (const reply of (sortType && comment.replies?.pages?.[sortType]?.comments) || []) { + // each comment serves the sort from its own page, e.g. its preloaded page re-sorted client-side + const pageSortType = getReplyPageSortType(comment, feedOptions.sortType); + for (const reply of (pageSortType && comment.replies?.pages?.[pageSortType]?.comments) || + []) { addRepliesFeedsToStoreRecursively(reply); } } diff --git a/src/stores/replies/utils.test.ts b/src/stores/replies/utils.test.ts index d7fc453b..0a340a8e 100644 --- a/src/stores/replies/utils.test.ts +++ b/src/stores/replies/utils.test.ts @@ -281,12 +281,44 @@ describe("replies utils", () => { expect(feeds.feed1.map((reply: any) => reply.cid)).toEqual(["r2", "r1"]); }); - test("does not substitute a single preloaded page for a missing requested sort", () => { - const reply = { - cid: "fallback-reply", - communityAddress: "sub1", - timestamp: 1, + test("sorts a complete preloaded page client-side for a standard sort", () => { + const comments = { + comment1: { + cid: "comment1", + communityAddress: "sub1", + updatedAt: 1, + replies: { + pages: { + best: { + comments: [ + { cid: "older-reply", communityAddress: "sub1", timestamp: 1 }, + { cid: "newer-reply", communityAddress: "sub1", timestamp: 2 }, + ], + }, + }, + }, + }, }; + const accounts = { [mockAccountId]: { pkc: {}, blockedAddresses: {}, blockedCids: {} } }; + const toFeedsOptions = (sortType: string) => ({ + feed1: { commentCid: "comment1", sortType, accountId: mockAccountId }, + }); + expect( + getFilteredSortedFeeds(toFeedsOptions("new"), comments, {}, accounts).feed1.map( + (reply: any) => reply.cid, + ), + ).toEqual(["newer-reply", "older-reply"]); + expect( + getFilteredSortedFeeds(toFeedsOptions("old"), comments, {}, accounts).feed1.map( + (reply: any) => reply.cid, + ), + ).toEqual(["older-reply", "newer-reply"]); + expect( + getFilteredSortedFeeds(toFeedsOptions("customSort"), comments, {}, accounts).feed1, + ).toEqual([]); + }); + + test("flattens a flat sort served from the preloaded hierarchical page", () => { const comments = { comment1: { cid: "comment1", @@ -294,17 +326,31 @@ describe("replies utils", () => { updatedAt: 1, replies: { pages: { - otherSort: { comments: [reply], nextCid: undefined }, + best: { + comments: [ + { + cid: "older-reply", + communityAddress: "sub1", + timestamp: 1, + replies: { + pages: { + best: { + comments: [ + { cid: "nested-reply", communityAddress: "sub1", timestamp: 3 }, + ], + }, + }, + }, + }, + { cid: "newer-reply", communityAddress: "sub1", timestamp: 2 }, + ], + }, }, }, }, }; const feedsOptions = { - feed1: { - commentCid: "comment1", - sortType: "new", - accountId: mockAccountId, - }, + feed1: { commentCid: "comment1", sortType: "newFlat", accountId: mockAccountId }, }; const feeds = getFilteredSortedFeeds( feedsOptions, @@ -312,7 +358,11 @@ describe("replies utils", () => { {}, { [mockAccountId]: { pkc: {}, blockedAddresses: {}, blockedCids: {} } }, ); - expect(feeds.feed1).toEqual([]); + expect(feeds.feed1.map((reply: any) => reply.cid)).toEqual([ + "nested-reply", + "newer-reply", + "older-reply", + ]); }); }); @@ -968,13 +1018,24 @@ describe("replies utils", () => { expect(getSortTypeFromComment(comment as any, { sortType: "custom" })).toBe("custom"); }); - test("does not substitute a similar or flat sort name", () => { + test("serves standard sorts from a complete preloaded page", () => { const comment = { replies: { pages: { topAll: { comments: [] }, newFlat: { comments: [] } }, pageCids: {}, }, }; + expect(getSortTypeFromComment(comment as any, { sortType: "best" })).toBe("best"); + expect(getSortTypeFromComment(comment as any, { sortType: "new", flat: true })).toBe("new"); + }); + + test("does not substitute a similar or flat sort name once pages continue", () => { + const comment = { + replies: { + pages: { topAll: { comments: [], nextCid: "topAll-next" } }, + pageCids: { topAll: "topAll-cid", newFlat: "newFlat-cid" }, + }, + }; expect(getSortTypeFromComment(comment as any, { sortType: "best" })).toBeUndefined(); expect( getSortTypeFromComment(comment as any, { sortType: "new", flat: true }), diff --git a/src/stores/replies/utils.ts b/src/stores/replies/utils.ts index fb468108..2b5c98f3 100644 --- a/src/stores/replies/utils.ts +++ b/src/stores/replies/utils.ts @@ -17,7 +17,7 @@ import accountsStore from "../accounts"; import { flattenCommentsPages, commentIsValid, removeInvalidComments } from "../../lib/utils"; import { areEquivalentCommunityAddresses } from "../../lib/community-address"; import Logger from "@pkcprotocol/pkc-logger"; -import { resolveReplySortType } from "../../lib/page-sorts"; +import { getReplyPageSortType, isFlatSortType, resolveReplySortType } from "../../lib/page-sorts"; const log = Logger("bitsocial-react-hooks:replies:stores"); /** @@ -46,11 +46,13 @@ export const getFilteredSortedFeeds = ( const sortType = getSortTypeFromComment(comment, feedsOptions[feedName]); const requestedSortIsUnavailable = requestedSortType !== undefined && sortType === undefined; + // the page serving the sort, re-sorted client-side when the comment preloads all of its replies + const pageSortType = comment ? getReplyPageSortType(comment, sortType) : undefined; // comment has loaded and cache not expired if (comment && !requestedSortIsUnavailable) { // use comment preloaded replies if any - const preloadedReplies = getPreloadedReplies(comment, sortType); + const preloadedReplies = getPreloadedReplies(comment, pageSortType); if (preloadedReplies) { for (const reply of preloadedReplies) { // replies are manually validated, could have fake communityAddress @@ -78,7 +80,8 @@ export const getFilteredSortedFeeds = ( } } - if (flat) { + // a flat sort served from a hierarchical page is flattened like the flat page pkc-js would publish + if (flat || (isFlatSortType(sortType) && pageSortType !== sortType)) { bufferedFeedReplies = flattenCommentsPages({ comments: bufferedFeedReplies }); } @@ -103,12 +106,11 @@ export const getFilteredSortedFeeds = ( return feeds; }; -const getPreloadedReplies = (comment: Comment, sortType?: string) => { - const resolvedSortType = resolveReplySortType(comment, sortType); - if (!resolvedSortType) { +const getPreloadedReplies = (comment: Comment, pageSortType?: string) => { + if (!pageSortType) { return; } - return comment.replies?.pages?.[resolvedSortType]?.comments; + return comment.replies?.pages?.[pageSortType]?.comments; }; const previousPageNumbers: { [feedName: string]: number } = {};