diff --git a/src/js/dataHelpers.js b/src/js/dataHelpers.js index 4aa3dec2..b7b1b5d9 100644 --- a/src/js/dataHelpers.js +++ b/src/js/dataHelpers.js @@ -256,6 +256,19 @@ export function createThreadViewPostFromPost(post) { }; } +// Transforms a flat list of posts into a nested threadViewPost chain +export function createNestedThreadViewPost(posts) { + let current = null; + for (const post of posts.toReversed()) { + current = { + $type: "app.bsky.feed.defs#threadViewPost", + post, + replies: current ? [current] : [], + }; + } + return current; +} + export function flattenParents(postThread) { const parents = []; let current = postThread.parent; diff --git a/src/js/dataLayer/dataStore.js b/src/js/dataLayer/dataStore.js index f5084d47..aa47381d 100644 --- a/src/js/dataLayer/dataStore.js +++ b/src/js/dataLayer/dataStore.js @@ -1,5 +1,10 @@ import { Signal, SignalMap, ReactiveStore } from "/js/signals.js"; -import { getQuotedPost, embedViewRecordToPostView } from "/js/dataHelpers.js"; +import { isNil } from "/js/utils.js"; +import { + getQuotedPost, + embedViewRecordToPostView, + getPostNumberingForPostThread, +} from "/js/dataHelpers.js"; // The store saves canonical data from the server. Patches are layered on top of this. export class DataStore extends ReactiveStore { @@ -44,8 +49,8 @@ export class DataStore extends ReactiveStore { this.$embeddedPosts = new SignalMap(); this.$postThreads = new SignalMap(); this.$postThreadOthers = new SignalMap(); - // uri -> { index, count } as numbered by the appview in feed responses - this.$feedPostNumbering = new SignalMap(); + // uri -> { index, count } + this.$postNumbering = new SignalMap(); this.$profiles = new SignalMap(); this.$detailedProfiles = new SignalMap(); // did -> statusView, normalized from profiles @@ -120,6 +125,22 @@ export class DataStore extends ReactiveStore { this.$profiles.set(profile.did, merged); } + setPostNumberingForFeed(feed) { + for (const feedItem of feed.feed) { + const { opThreadPostIndex: index, opThreadPostCount: count } = feedItem; + if (!isNil(index) && !isNil(count)) { + this.$postNumbering.set(feedItem.post.uri, { index, count }); + } + } + } + + setPostThread(postURI, postThread) { + this.$postThreads.set(postURI, postThread); + for (const [uri, numbering] of getPostNumberingForPostThread(postThread)) { + this.$postNumbering.set(uri, numbering); + } + } + _saveStatusForProfile(profile) { if (!profile?.did) { return; diff --git a/src/js/dataLayer/derived.js b/src/js/dataLayer/derived.js index 927495a2..e2296c5e 100644 --- a/src/js/dataLayer/derived.js +++ b/src/js/dataLayer/derived.js @@ -26,7 +26,6 @@ import { getJoinLinkCodeFromEmbed, isFollowingFeedUri, isStatusValid, - getPostNumberingForPostThread, } from "/js/dataHelpers.js"; import { sortBy, KeyedScheduler } from "/js/utils.js"; import { FOLLOWING_FEED_URI } from "/js/config.js"; @@ -328,25 +327,17 @@ export class Derived extends ReactiveStore { return postThread; } const hiddenReplyUris = new Set(postThreadOther.map((item) => item.uri)); - const postNumberingMap = getPostNumberingForPostThread(postThread); - const hydrated = this.hydratePostThreadNode( - postThread, - hiddenReplyUris, - postNumberingMap, - ); + const hydrated = this.hydratePostThreadNode(postThread, hiddenReplyUris); if (!hydrated) { return null; } if (postThread.parent) { - hydrated.parent = this.hydratePostThreadParent( - postThread.parent, - postNumberingMap, - ); + hydrated.parent = this.hydratePostThreadParent(postThread.parent); } return hydrated; }); - this.$feedPostNumbering = new ComputedMap((uri) => { - return this.dataStore.$feedPostNumbering.get(uri) ?? null; + this.$postNumbering = new ComputedMap((uri) => { + return this.dataStore.$postNumbering.get(uri) ?? null; }); this.$hydratedHashtagFeeds = new ComputedMap((hashtagKey) => { const data = this.dataStore.$hashtagFeeds.get(hashtagKey); @@ -978,9 +969,7 @@ export class Derived extends ReactiveStore { } hydrateFeedItem(feedItem) { - const postNumbering = this.dataStore.$feedPostNumbering.get( - feedItem.post.uri, - ); + const postNumbering = this.dataStore.$postNumbering.get(feedItem.post.uri); const hydratedFeedItem = { ...feedItem, post: this.$hydratedPosts.get(feedItem.post.uri), @@ -1019,14 +1008,14 @@ export class Derived extends ReactiveStore { return hydratedFeedItem; } - hydratePostThreadNode(node, hiddenReplyUris, postNumberingMap) { + hydratePostThreadNode(node, hiddenReplyUris) { if (!node || isEmptyPost(node)) return node; const post = this.$hydratedPosts.get(node.post.uri); if (!post) return null; // NOTE: LEXICON DEVIATION const hydrated = { post, - postNumbering: postNumberingMap.get(node.post.uri) ?? null, + postNumbering: this.dataStore.$postNumbering.get(node.post.uri) ?? null, }; if (hiddenReplyUris.has(node.post.uri)) { // NOTE: LEXICON DEVIATION @@ -1035,11 +1024,7 @@ export class Derived extends ReactiveStore { if (node.replies) { hydrated.replies = node.replies.map((reply) => { if (reply.$type === "app.bsky.feed.defs#threadViewPost") { - return this.hydratePostThreadNode( - reply, - hiddenReplyUris, - postNumberingMap, - ); + return this.hydratePostThreadNode(reply, hiddenReplyUris); } return reply; }); @@ -1047,7 +1032,7 @@ export class Derived extends ReactiveStore { return hydrated; } - hydratePostThreadParent(parent, postNumberingMap) { + hydratePostThreadParent(parent) { if (this.dataStore.$unavailablePosts.get(parent.uri) !== null) { return createUnavailablePost(parent.uri); } @@ -1061,13 +1046,10 @@ export class Derived extends ReactiveStore { $type: "app.bsky.feed.defs#threadViewPost", post: this.$hydratedPosts.get(parent.post.uri), // NOTE: LEXICON DEVIATION - postNumbering: postNumberingMap.get(parent.post.uri) ?? null, + postNumbering: this.dataStore.$postNumbering.get(parent.post.uri) ?? null, }; if (parent.parent) { - hydratedParent.parent = this.hydratePostThreadParent( - parent.parent, - postNumberingMap, - ); + hydratedParent.parent = this.hydratePostThreadParent(parent.parent); } return hydratedParent; } diff --git a/src/js/dataLayer/mutations.js b/src/js/dataLayer/mutations.js index 22c67a5f..ceb8e1fc 100644 --- a/src/js/dataLayer/mutations.js +++ b/src/js/dataLayer/mutations.js @@ -5,6 +5,7 @@ import { hasDisableEmbeddingRule, createNotFoundPost, createStatusView, + createNestedThreadViewPost, addFeedItemToFeed, pinPostInFeed, unpinPostInFeed, @@ -1528,14 +1529,10 @@ export class Mutations { if (replyTo) { const replyPostThread = this.dataStore.$postThreads.get(replyTo.uri); if (replyPostThread) { - this.dataStore.$postThreads.set(replyTo.uri, { + this.dataStore.setPostThread(replyTo.uri, { ...replyPostThread, replies: [ - { - $type: "app.bsky.feed.defs#threadViewPost", - post: rootPost, - replies: [], - }, + createNestedThreadViewPost(hydratedPosts), ...replyPostThread.replies, ], }); diff --git a/src/js/dataLayer/requests.js b/src/js/dataLayer/requests.js index 1e6aac84..d3d3a1db 100644 --- a/src/js/dataLayer/requests.js +++ b/src/js/dataLayer/requests.js @@ -18,7 +18,7 @@ import { buildProfileFromRecord, } from "/js/dataHelpers.js"; import { getLocalRefsFromDraft } from "/js/dataHelpers.js"; -import { unique, isNil } from "/js/utils.js"; +import { unique } from "/js/utils.js"; import { SignalMap, ComputedMap, ReactiveStore } from "/js/signals.js"; import { ApiError, isRecordNotFoundError } from "/js/api.js"; import { FOLLOWING_FEED_URI } from "/js/config.js"; @@ -327,7 +327,7 @@ export class Requests { } // Save post thread - this.dataStore.$postThreads.set(postURI, postThread); + this.dataStore.setPostThread(postURI, postThread); this.dataStore.$postThreadOthers.set(postURI, postThreadOther); // Note - this return value is used by loadParentChain return postThread; @@ -500,18 +500,6 @@ export class Requests { return loadedReplies; } - _saveFeedPostNumbering(feed) { - for (const feedItem of feed.feed) { - const { opThreadPostIndex: index, opThreadPostCount: count } = feedItem; - if (!isNil(index) && !isNil(count)) { - this.dataStore.$feedPostNumbering.set(feedItem.post.uri, { - index, - count, - }); - } - } - } - async loadNextFeedPage({ type, uri }, { reload = false, limit = 31 } = {}) { const cursor = reload ? "" @@ -533,7 +521,7 @@ export class Requests { const postsToSave = getPostsFromFeed(feed); await this._loadPostDependencies(postsToSave); this.dataStore.setPosts(postsToSave); - this._saveFeedPostNumbering(feed); + this.dataStore.setPostNumberingForFeed(feed); await this.events.emitAsync("feedLoaded", { feedURI: uri, feed, reload }); writePageToCollection(this.dataStore.$feeds, "feed", feed, { key: uri, @@ -865,7 +853,7 @@ export class Requests { const postsToSave = getPostsFromFeed(feed); await this._loadPostDependencies(postsToSave); this.dataStore.setPosts(postsToSave); - this._saveFeedPostNumbering(feed); + this.dataStore.setPostNumberingForFeed(feed); // Save feed writePageToCollection(this.dataStore.$authorFeeds, "feed", feed, { key: feedURI, diff --git a/src/js/views/postThread.view.js b/src/js/views/postThread.view.js index af6aaffa..83a9ac21 100644 --- a/src/js/views/postThread.view.js +++ b/src/js/views/postThread.view.js @@ -588,7 +588,7 @@ export default async function postThreadView({ return { __isPrefill: true, post, - postNumbering: dataLayer.derived.$feedPostNumbering.get(postUri), + postNumbering: dataLayer.derived.$postNumbering.get(postUri), parent: null, replies: null, }; diff --git a/tests/unit/specs/dataHelpers.test.js b/tests/unit/specs/dataHelpers.test.js index dcf8962e..94c11a5f 100644 --- a/tests/unit/specs/dataHelpers.test.js +++ b/tests/unit/specs/dataHelpers.test.js @@ -6,6 +6,7 @@ import { buildProfileFromRecord, cdnImageUrl, getPostNumberingForPostThread, + createNestedThreadViewPost, getRKey, getIsLiked, isListFeed, @@ -2545,6 +2546,33 @@ describe("getLocalRefsFromDraft", () => { }); }); +describe("createNestedThreadViewPost", () => { + it("should nest each post under the previous one", () => { + const posts = [{ uri: "p1" }, { uri: "p2" }, { uri: "p3" }]; + assert.deepEqual(createNestedThreadViewPost(posts), { + $type: "app.bsky.feed.defs#threadViewPost", + post: posts[0], + replies: [ + { + $type: "app.bsky.feed.defs#threadViewPost", + post: posts[1], + replies: [ + { + $type: "app.bsky.feed.defs#threadViewPost", + post: posts[2], + replies: [], + }, + ], + }, + ], + }); + }); + + it("should return null for an empty list", () => { + assert.equal(createNestedThreadViewPost([]), null); + }); +}); + describe("getPostNumberingForPostThread", () => { const opDid = "did:plc:op"; const otherDid = "did:plc:other"; diff --git a/tests/unit/specs/dataLayer/dataStore.test.js b/tests/unit/specs/dataLayer/dataStore.test.js index 95bda799..546b638a 100644 --- a/tests/unit/specs/dataLayer/dataStore.test.js +++ b/tests/unit/specs/dataLayer/dataStore.test.js @@ -223,6 +223,88 @@ describe("mergeProfile", () => { }); }); +describe("setPostNumberingForFeed", () => { + it("should save the numbering carried by feed items", () => { + const dataStore = new DataStore(createSessionState(null)); + + dataStore.setPostNumberingForFeed({ + feed: [ + { post: { uri: "post1" }, opThreadPostIndex: 2, opThreadPostCount: 3 }, + { post: { uri: "post2" } }, + { post: { uri: "post3" }, opThreadPostIndex: 1 }, + ], + cursor: "", + }); + + assert.deepEqual(dataStore.$postNumbering.get("post1"), { + index: 2, + count: 3, + }); + assert.equal(dataStore.$postNumbering.get("post2"), null); + assert.equal(dataStore.$postNumbering.get("post3"), null); + }); +}); + +describe("setPostThread", () => { + const opDid = "did:plc:op"; + const root = { + uri: `at://${opDid}/app.bsky.feed.post/p1`, + cid: "cid-p1", + author: { did: opDid }, + record: { text: "one" }, + replyCount: 1, + }; + const second = { + uri: `at://${opDid}/app.bsky.feed.post/p2`, + cid: "cid-p2", + author: { did: opDid }, + record: { + text: "two", + reply: { + parent: { uri: root.uri, cid: root.cid }, + root: { uri: root.uri, cid: root.cid }, + }, + }, + replyCount: 0, + }; + const threadViewPost = (post, extra) => ({ + $type: "app.bsky.feed.defs#threadViewPost", + post, + ...extra, + }); + + it("should save the thread and the numbering of its OP run", () => { + const dataStore = new DataStore(createSessionState(null)); + const postThread = threadViewPost(root, { + replies: [threadViewPost(second, { replies: [] })], + }); + + dataStore.setPostThread(root.uri, postThread); + + assert.deepEqual(dataStore.$postThreads.get(root.uri), postThread); + assert.deepEqual(dataStore.$postNumbering.get(root.uri), { + index: 1, + count: 2, + }); + assert.deepEqual(dataStore.$postNumbering.get(second.uri), { + index: 2, + count: 2, + }); + }); + + it("should leave existing numbering alone when the thread has no OP run", () => { + const dataStore = new DataStore(createSessionState(null)); + dataStore.$postNumbering.set(root.uri, { index: 1, count: 5 }); + + dataStore.setPostThread(root.uri, threadViewPost(root, { replies: [] })); + + assert.deepEqual(dataStore.$postNumbering.get(root.uri), { + index: 1, + count: 5, + }); + }); +}); + describe("setConvo", () => { it("should save the convo and prepend it to the loaded convo list", () => { const dataStore = new DataStore(createSessionState(null)); diff --git a/tests/unit/specs/dataLayer/derived.test.js b/tests/unit/specs/dataLayer/derived.test.js index 682ad80f..99ebf5e9 100644 --- a/tests/unit/specs/dataLayer/derived.test.js +++ b/tests/unit/specs/dataLayer/derived.test.js @@ -140,7 +140,7 @@ describe("$hydratedFeeds post numbering", () => { function seedThreadFeed(dataStore, { reason, root, parent, post } = {}) { dataStore.setPosts([root, parent, post]); - dataStore.$feedPostNumbering.set(post.uri, { index: 3, count: 5 }); + dataStore.$postNumbering.set(post.uri, { index: 3, count: 5 }); dataStore.$feeds.set(feedURI, { feed: [ createFeedItem({ @@ -177,7 +177,7 @@ describe("$hydratedFeeds post numbering", () => { const parent = opPost("p2", { parent: root, root }); const post = opPost("p3", { parent, root }); seedThreadFeed(dataStore, { root, parent, post }); - dataStore.$feedPostNumbering.set(post.uri, { index: 2, count: 5 }); + dataStore.$postNumbering.set(post.uri, { index: 2, count: 5 }); const item = derived.$hydratedFeeds.get(feedURI).feed[0]; assert.deepEqual(item.reply.rootPostNumbering, { index: 1, count: 5 }); @@ -261,7 +261,7 @@ describe("$hydratedAuthorFeeds post numbering", () => { authorHandle: "op.test", }); dataStore.setPosts([post]); - dataStore.$feedPostNumbering.set(post.uri, { index: 1, count: 2 }); + dataStore.$postNumbering.set(post.uri, { index: 1, count: 2 }); dataStore.$authorFeeds.set("did:plc:op-posts", { feed: [createFeedItem({ post })], cursor: "", @@ -271,13 +271,13 @@ describe("$hydratedAuthorFeeds post numbering", () => { }); }); -describe("$feedPostNumbering", () => { +describe("$postNumbering", () => { it("reads the cached feed numbering for a post", () => { const dataStore = new DataStore(createSessionState(null)); const { derived } = makeDerived(dataStore); - assert.equal(derived.$feedPostNumbering.get("uri1"), null); - dataStore.$feedPostNumbering.set("uri1", { index: 2, count: 3 }); - assert.deepEqual(derived.$feedPostNumbering.get("uri1"), { + assert.equal(derived.$postNumbering.get("uri1"), null); + dataStore.$postNumbering.set("uri1", { index: 2, count: 3 }); + assert.deepEqual(derived.$postNumbering.get("uri1"), { index: 2, count: 3, }); @@ -1971,7 +1971,7 @@ describe("$hydratedPostThreads", () => { const dataStore = new DataStore(createSessionState(null)); const { derived } = makeDerived(dataStore); assert.deepEqual(derived.$hydratedPostThreads.get(threadUri), null); - dataStore.$postThreads.set(threadUri, { + dataStore.setPostThread(threadUri, { $type: "app.bsky.feed.defs#threadViewPost", post: { uri: threadUri }, }); @@ -1986,7 +1986,7 @@ describe("$hydratedPostThreads", () => { $type: "app.bsky.feed.defs#notFoundPost", uri: threadUri, }; - dataStore.$postThreads.set(threadUri, notFound); + dataStore.setPostThread(threadUri, notFound); dataStore.$postThreadOthers.set(threadUri, []); assert.deepEqual(derived.$hydratedPostThreads.get(threadUri), notFound); }); @@ -1994,7 +1994,7 @@ describe("$hydratedPostThreads", () => { it("should return null when the thread root post is not loaded", () => { const dataStore = new DataStore(createSessionState(null)); const { derived } = makeDerived(dataStore); - dataStore.$postThreads.set(threadUri, { + dataStore.setPostThread(threadUri, { $type: "app.bsky.feed.defs#threadViewPost", post: { uri: threadUri }, }); @@ -2015,7 +2015,7 @@ describe("$hydratedPostThreads", () => { record: { text: "root" }, }); dataStore.$posts.set("reply1", { uri: "reply1", record: { text: "r1" } }); - dataStore.$postThreads.set(threadUri, { + dataStore.setPostThread(threadUri, { $type: "app.bsky.feed.defs#threadViewPost", post: { uri: threadUri }, replies: [ @@ -2039,7 +2039,7 @@ describe("$hydratedPostThreads", () => { uri: threadUri, record: { text: "root" }, }); - dataStore.$postThreads.set(threadUri, { + dataStore.setPostThread(threadUri, { $type: "app.bsky.feed.defs#threadViewPost", post: { uri: threadUri }, parent, @@ -2128,7 +2128,7 @@ describe("$hydratedPostThreads post numbering", () => { }, }); dataStore.setPosts([root, second, third, other]); - dataStore.$postThreads.set( + dataStore.setPostThread( second.uri, createThreadViewPost({ post: second, @@ -2153,7 +2153,7 @@ describe("$hydratedPostThreads post numbering", () => { const { derived } = makeDerived(dataStore); const root = opPost("p1", { replyCount: 1 }); dataStore.setPosts([root]); - dataStore.$postThreads.set( + dataStore.setPostThread( root.uri, createThreadViewPost({ post: root, replies: [] }), ); diff --git a/tests/unit/specs/dataLayer/mutations.test.js b/tests/unit/specs/dataLayer/mutations.test.js index d9e4ad00..348c008f 100644 --- a/tests/unit/specs/dataLayer/mutations.test.js +++ b/tests/unit/specs/dataLayer/mutations.test.js @@ -2848,7 +2848,12 @@ describe("createThread", () => { const currentUserDid = "did:plc:me"; const newPostUri = `at://${currentUserDid}/app.bsky.feed.post/new`; - function setup({ replyPostThread, authorFeed, replyAuthorFeed } = {}) { + function setup({ + replyPostThread, + authorFeed, + replyAuthorFeed, + extraPosts = [], + } = {}) { const dataStore = new DataStore(createSessionState(null)); const patchStore = new PatchStore(); const mockPreferencesProvider = { @@ -2869,12 +2874,12 @@ describe("createThread", () => { }; mutations.postCreator = { createThread: async () => ({ - uris: [fullPost.uri], - posts: [fullPost], + uris: [fullPost, ...extraPosts].map((post) => post.uri), + posts: [fullPost, ...extraPosts], }), }; if (replyPostThread) { - dataStore.$postThreads.set(replyPostThread.post.uri, replyPostThread); + dataStore.setPostThread(replyPostThread.post.uri, replyPostThread); } if (authorFeed) { dataStore.$authorFeeds.set(`${currentUserDid}-posts`, authorFeed); @@ -2942,6 +2947,105 @@ describe("createThread", () => { assert.deepEqual(repliesFeed.feed[0].post.uri, newPostUri); }); + it("should extend the OP run numbering when the OP replies to their own thread", async () => { + const replyTo = { + uri: `at://${currentUserDid}/app.bsky.feed.post/root`, + cid: "cid-root", + author: { did: currentUserDid }, + record: { text: "root" }, + replyCount: 0, + }; + const replyPostThread = { + $type: "app.bsky.feed.defs#threadViewPost", + post: replyTo, + replies: [], + }; + const { mutations, dataStore, fullPost } = setup({ replyPostThread }); + fullPost.record.reply = { + parent: { uri: replyTo.uri, cid: replyTo.cid }, + root: { uri: replyTo.uri, cid: replyTo.cid }, + }; + assert.equal(dataStore.$postNumbering.get(replyTo.uri), null); + + await mutations.createThread({ + posts: [{ postText: "more" }], + replyTo, + replyRoot: replyTo, + }); + + assert.deepEqual(dataStore.$postNumbering.get(replyTo.uri), { + index: 1, + count: 2, + }); + assert.deepEqual(dataStore.$postNumbering.get(newPostUri), { + index: 2, + count: 2, + }); + }); + + it("should nest a multi-post reply thread under the parent's post thread", async () => { + const replyTo = { + uri: `at://${currentUserDid}/app.bsky.feed.post/root`, + cid: "cid-root", + author: { did: currentUserDid }, + record: { text: "root" }, + replyCount: 0, + }; + const replyPostThread = { + $type: "app.bsky.feed.defs#threadViewPost", + post: replyTo, + replies: [], + }; + const secondUri = `at://${currentUserDid}/app.bsky.feed.post/second`; + const secondPost = { + uri: secondUri, + cid: "cid-second", + author: { did: currentUserDid, viewer: {} }, + record: { + text: "second", + reply: { + parent: { uri: newPostUri, cid: "cid-new" }, + root: { uri: replyTo.uri, cid: replyTo.cid }, + }, + }, + viewer: {}, + }; + const { mutations, dataStore, fullPost } = setup({ + replyPostThread, + extraPosts: [secondPost], + }); + fullPost.record.reply = { + parent: { uri: replyTo.uri, cid: replyTo.cid }, + root: { uri: replyTo.uri, cid: replyTo.cid }, + }; + + await mutations.createThread({ + posts: [{ postText: "hello" }, { postText: "second" }], + replyTo, + replyRoot: replyTo, + }); + + const updatedThread = dataStore.$postThreads.get(replyTo.uri); + assert.deepEqual(updatedThread.replies.length, 1); + const firstNode = updatedThread.replies[0]; + assert.deepEqual(firstNode.post.uri, newPostUri); + assert.deepEqual(firstNode.replies.length, 1); + assert.deepEqual(firstNode.replies[0].post.uri, secondUri); + assert.deepEqual(firstNode.replies[0].replies, []); + assert.deepEqual(dataStore.$postNumbering.get(replyTo.uri), { + index: 1, + count: 3, + }); + assert.deepEqual(dataStore.$postNumbering.get(newPostUri), { + index: 2, + count: 3, + }); + assert.deepEqual(dataStore.$postNumbering.get(secondUri), { + index: 3, + count: 3, + }); + }); + it("still resolves with uris when the app view fetch fails, without mutating stores", async () => { const { mutations, dataStore } = setup({ authorFeed: { feed: [], cursor: "c1" }, diff --git a/tests/unit/specs/dataLayer/requests.test.js b/tests/unit/specs/dataLayer/requests.test.js index d5b786c0..eb7f60d5 100644 --- a/tests/unit/specs/dataLayer/requests.test.js +++ b/tests/unit/specs/dataLayer/requests.test.js @@ -7,6 +7,7 @@ import { DraftMediaStore } from "/js/drafts.js"; import { Preferences } from "/js/preferences.js"; import { ApiError } from "/js/api.js"; import { EventEmitter } from "/js/eventEmitter.js"; +import { createPost, createThreadViewPost } from "../../../shared/factories.js"; const stubConstellation = { getLinks: async () => [] }; @@ -73,6 +74,45 @@ describe("loadPostThread", () => { assert.deepEqual(dataStore.$posts.get("reply1"), normalizedPosts[1]); }); + it("should cache post numbering for the loaded thread's OP run", async () => { + const opDid = "did:plc:op"; + const root = createPost({ + uri: `at://${opDid}/app.bsky.feed.post/p1`, + authorHandle: "op.test", + replyCount: 1, + }); + const second = createPost({ + uri: `at://${opDid}/app.bsky.feed.post/p2`, + authorHandle: "op.test", + reply: { + parent: { uri: root.uri, cid: root.cid }, + root: { uri: root.uri, cid: root.cid }, + }, + }); + const mockApi = { + getPostThread: async () => + createThreadViewPost({ + post: second, + parent: createThreadViewPost({ post: root }), + replies: [], + }), + getPostThreadOther: async () => [], + }; + const dataStore = new DataStore(createSessionState(null)); + const requests = makeRequests(mockApi, dataStore); + + await requests.loadPostThread(second.uri); + + assert.deepEqual(dataStore.$postNumbering.get(root.uri), { + index: 1, + count: 2, + }); + assert.deepEqual(dataStore.$postNumbering.get(second.uri), { + index: 2, + count: 2, + }); + }); + it("should handle empty post thread", async () => { const emptyPostThread = { post: { uri: postURI, content: "Lonely post" }, @@ -111,7 +151,11 @@ describe("loadNextFeedPage", () => { const mockApi = { getFeed: async () => ({ feed: [ - { post: { uri: "post1" }, opThreadPostIndex: 2, opThreadPostCount: 3 }, + { + post: { uri: "post1" }, + opThreadPostIndex: 2, + opThreadPostCount: 3, + }, { post: { uri: "post2" } }, { post: { uri: "post3" }, opThreadPostIndex: 1 }, ], @@ -123,12 +167,12 @@ describe("loadNextFeedPage", () => { await requests.loadNextFeedPage({ type: "feed", uri: feedURI }); - assert.deepEqual(dataStore.$feedPostNumbering.get("post1"), { + assert.deepEqual(dataStore.$postNumbering.get("post1"), { index: 2, count: 3, }); - assert.equal(dataStore.$feedPostNumbering.get("post2"), null); - assert.equal(dataStore.$feedPostNumbering.get("post3"), null); + assert.equal(dataStore.$postNumbering.get("post2"), null); + assert.equal(dataStore.$postNumbering.get("post3"), null); }); it("should load initial feed page", async () => {