Skip to content

Commit 3462aad

Browse files
committed
🐛 Fixed newly posted replies sometimes not appearing
no issue Caching can serve a stale replies response immediately after creation, so we preserve the POSTed reply while still using the refetch to pick up concurrent replies.
1 parent 7b7db30 commit 3462aad

4 files changed

Lines changed: 116 additions & 6 deletions

File tree

apps/comments-ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tryghost/comments-ui",
3-
"version": "1.5.4",
3+
"version": "1.5.5",
44
"license": "MIT",
55
"repository": "https://github.com/TryGhost/Ghost",
66
"author": "Ghost Foundation",

apps/comments-ui/src/actions.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,21 @@ async function addReply({state, api, data: {reply, parent}}: {state: EditableApp
136136
const newComment = data.comments[0];
137137

138138
const allReplies = await api.comments.replies({commentId: parent.id, limit: 'all'});
139+
// Caching can serve a stale replies response immediately after creation, so
140+
// keep the refetch for concurrent replies but preserve the POSTed reply.
141+
const replies = allReplies.comments.some(replyComment => replyComment.id === newComment.id)
142+
? allReplies.comments
143+
: [...allReplies.comments, newComment];
139144

140145
return {
141146
comments: state.comments.map((c) => {
142147
if (c.id === parent.id) {
143148
return {
144149
...c,
145-
replies: allReplies.comments,
150+
replies,
146151
count: {
147152
...c.count,
148-
replies: allReplies.comments.length
153+
replies: replies.length
149154
}
150155
};
151156
}

apps/comments-ui/test/unit/actions.test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,110 @@ describe('Actions', function () {
8181
});
8282
});
8383

84+
describe('addReply', function () {
85+
it('adds the created reply when the replies refetch is stale', async function () {
86+
const newReply = makeComment({
87+
id: 'new-reply',
88+
html: '<p>New reply</p>',
89+
parent_id: 'comment-1'
90+
});
91+
const existingReply = makeComment({
92+
id: 'existing-reply',
93+
html: '<p>Existing reply</p>',
94+
parent_id: 'comment-1'
95+
});
96+
const state = {
97+
commentCount: 1,
98+
comments: [
99+
makeComment({
100+
id: 'comment-1',
101+
replies: [],
102+
count: {
103+
replies: 0,
104+
likes: 0
105+
}
106+
})
107+
]
108+
};
109+
const api = {
110+
comments: {
111+
add: vi.fn(() => Promise.resolve({comments: [newReply]})),
112+
replies: vi.fn(() => Promise.resolve({comments: [existingReply]}))
113+
}
114+
};
115+
116+
const newState = await Actions.addReply({
117+
state,
118+
api,
119+
data: {
120+
parent: state.comments[0],
121+
reply: {
122+
post_id: 'post-1',
123+
html: '<p>New reply</p>',
124+
status: 'published'
125+
}
126+
}
127+
});
128+
129+
expect(api.comments.add).toHaveBeenCalledWith({
130+
comment: {
131+
post_id: 'post-1',
132+
html: '<p>New reply</p>',
133+
status: 'published',
134+
parent_id: 'comment-1'
135+
}
136+
});
137+
expect(api.comments.replies).toHaveBeenCalledWith({commentId: 'comment-1', limit: 'all'});
138+
expect(newState.comments[0].replies.map(reply => reply.id)).toEqual(['existing-reply', 'new-reply']);
139+
expect(newState.comments[0].count.replies).toBe(2);
140+
expect(newState.commentCount).toBe(2);
141+
expect(newState.commentIdToScrollTo).toBe('new-reply');
142+
});
143+
144+
it('does not duplicate the created reply when the replies refetch includes it', async function () {
145+
const newReply = makeComment({
146+
id: 'new-reply',
147+
html: '<p>New reply</p>',
148+
parent_id: 'comment-1'
149+
});
150+
const state = {
151+
commentCount: 1,
152+
comments: [
153+
makeComment({
154+
id: 'comment-1',
155+
replies: [],
156+
count: {
157+
replies: 0,
158+
likes: 0
159+
}
160+
})
161+
]
162+
};
163+
const api = {
164+
comments: {
165+
add: vi.fn(() => Promise.resolve({comments: [newReply]})),
166+
replies: vi.fn(() => Promise.resolve({comments: [newReply]}))
167+
}
168+
};
169+
170+
const newState = await Actions.addReply({
171+
state,
172+
api,
173+
data: {
174+
parent: state.comments[0],
175+
reply: {
176+
post_id: 'post-1',
177+
html: '<p>New reply</p>',
178+
status: 'published'
179+
}
180+
}
181+
});
182+
183+
expect(newState.comments[0].replies.map(reply => reply.id)).toEqual(['new-reply']);
184+
expect(newState.comments[0].count.replies).toBe(1);
185+
});
186+
});
187+
84188
describe('deleteComment', function () {
85189
it('keeps a deleted reply as a tombstone when it has descendants', async function () {
86190
const state = {

apps/comments-ui/test/utils/mocked-api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,12 @@ export class MockedApi {
7474
if (parent) {
7575
parent.replies.push(fixture);
7676
parent.count.replies = parent.replies.length;
77-
return;
77+
return fixture;
7878
}
7979
}
8080

8181
this.comments.push(fixture);
82+
return fixture;
8283
}
8384

8485
buildReply(overrides: any = {}) {
@@ -361,15 +362,15 @@ export class MockedApi {
361362
const payload = JSON.parse(route.request().postData());
362363

363364
this.#lastCommentDate = new Date();
364-
this.addComment({
365+
const comment = this.addComment({
365366
...payload.comments[0],
366367
member: this.member
367368
});
368369
return await route.fulfill({
369370
status: 200,
370371
body: JSON.stringify({
371372
comments: [
372-
this.comments[this.comments.length - 1]
373+
comment
373374
]
374375
})
375376
});

0 commit comments

Comments
 (0)