diff --git a/src/utils/atproto-adapter.js b/src/utils/atproto-adapter.js index 4959b766..9e79119c 100644 --- a/src/utils/atproto-adapter.js +++ b/src/utils/atproto-adapter.js @@ -340,6 +340,28 @@ function isPostView(value) { return !!(value?.uri && value?.author && value?.record); } +function replyContextSourceForPost(feedItem, post) { + if (!feedItem?.reply || feedItem.post?.uri === post.uri) return feedItem; + const parent = feedItem.reply.parent; + if ( + parent?.uri === post.uri && + feedItem.reply.grandparentAuthor && + post.record?.reply?.parent?.uri + ) { + return { + post, + reply: { + root: feedItem.reply.root, + parent: { + ...post.record.reply.parent, + author: feedItem.reply.grandparentAuthor, + }, + }, + }; + } + return post; +} + function feedItemToStatuses(feedItem, agent) { const post = feedItem?.post || feedItem; if (feedItem?.reason?.$type === 'app.bsky.feed.defs#reasonRepost') { @@ -354,8 +376,14 @@ function feedItemToStatuses(feedItem, agent) { statuses.push(postToStatus(statusSource, agent)); }; - addPost(feedItem?.reply?.root); - addPost(feedItem?.reply?.parent); + addPost( + feedItem?.reply?.root, + replyContextSourceForPost(feedItem, feedItem?.reply?.root), + ); + addPost( + feedItem?.reply?.parent, + replyContextSourceForPost(feedItem, feedItem?.reply?.parent), + ); addPost(post, feedItem); return statuses; } diff --git a/tests/atproto-replies.spec.js b/tests/atproto-replies.spec.js index ba050d0e..cbe528de 100644 --- a/tests/atproto-replies.spec.js +++ b/tests/atproto-replies.spec.js @@ -183,4 +183,73 @@ test.describe('ATProto reply mapping', () => { username: 'parent.test', }); }); + + test('uses grandparent author for payload-hydrated middle replies', () => { + const grandparentUri = 'at://did:plc:grandparent/app.bsky.feed.post/root'; + const item = feedReply({ + post: { + record: { + $type: 'app.bsky.feed.post', + text: 'reply text', + createdAt: '2026-05-08T00:02:00.000Z', + reply: { + root: { uri: grandparentUri, cid: 'grandparent-cid' }, + parent: { uri: parentUri, cid: 'parent-cid' }, + }, + }, + }, + reply: { + root: { + uri: grandparentUri, + cid: 'grandparent-cid', + author: { + did: 'did:plc:grandparent', + handle: 'grandparent.test', + displayName: 'Grandparent', + }, + record: { + $type: 'app.bsky.feed.post', + text: 'grandparent text', + createdAt: '2026-05-08T00:00:00.000Z', + }, + }, + parent: { + uri: parentUri, + cid: 'parent-cid', + author: { + did: 'did:plc:parent', + handle: 'parent.test', + displayName: 'Parent', + }, + record: { + $type: 'app.bsky.feed.post', + text: 'parent text', + createdAt: '2026-05-08T00:01:00.000Z', + reply: { + root: { uri: grandparentUri, cid: 'grandparent-cid' }, + parent: { uri: grandparentUri, cid: 'grandparent-cid' }, + }, + }, + }, + grandparentAuthor: { + did: 'did:plc:grandparent', + handle: 'grandparent.test', + displayName: 'Grandparent', + }, + }, + }); + + const statuses = feedToStatuses([item]); + + expect(statuses.map((status) => status.uri)).toEqual([ + grandparentUri, + parentUri, + childUri, + ]); + expect(statuses[1]._atproto.replyParentUnavailable).toBe(false); + expect(statuses[1]._atproto.replyParentAccount).toMatchObject({ + id: 'did:plc:grandparent', + username: 'grandparent.test', + }); + }); });