@stream-io/feeds-client 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/react-bindings.js +1 -1
- package/dist/es/index.mjs +3 -2
- package/dist/es/react-bindings.mjs +1 -1
- package/dist/{index-B0Mm2xFU.js → index-CFv0uza2.js} +273 -51
- package/dist/index-CFv0uza2.js.map +1 -0
- package/dist/{index-rSXIDTdA.mjs → index-DP0C8psw.mjs} +273 -51
- package/dist/index-DP0C8psw.mjs.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +4 -5
- package/dist/types/feed/event-handlers/activity/handle-activity-reaction-added.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +1 -2
- package/dist/types/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts +6 -5
- package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/comment/handle-comment-added.d.ts +4 -3
- package/dist/types/feed/event-handlers/comment/handle-comment-added.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/comment/handle-comment-deleted.d.ts +4 -3
- package/dist/types/feed/event-handlers/comment/handle-comment-deleted.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/comment/handle-comment-reaction-added.d.ts +5 -0
- package/dist/types/feed/event-handlers/comment/handle-comment-reaction-added.d.ts.map +1 -0
- package/dist/types/feed/event-handlers/comment/handle-comment-reaction-deleted.d.ts +5 -0
- package/dist/types/feed/event-handlers/comment/handle-comment-reaction-deleted.d.ts.map +1 -0
- package/dist/types/feed/event-handlers/comment/handle-comment-updated.d.ts +4 -3
- package/dist/types/feed/event-handlers/comment/handle-comment-updated.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/comment/index.d.ts +2 -1
- package/dist/types/feed/event-handlers/comment/index.d.ts.map +1 -1
- package/dist/types/feed/event-handlers/comment/utils/index.d.ts +2 -0
- package/dist/types/feed/event-handlers/comment/utils/index.d.ts.map +1 -0
- package/dist/types/feed/event-handlers/comment/utils/update-comment-count.d.ts +8 -0
- package/dist/types/feed/event-handlers/comment/utils/update-comment-count.d.ts.map +1 -0
- package/dist/types/feed/feed.d.ts.map +1 -1
- package/dist/types/feeds-client/feeds-client.d.ts +19 -1
- package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
- package/dist/types/types-internal.d.ts +4 -2
- package/dist/types/types-internal.d.ts.map +1 -1
- package/dist/types/utils/ensure-exhausted.d.ts +2 -0
- package/dist/types/utils/ensure-exhausted.d.ts.map +1 -0
- package/dist/types/utils/event-triggered-by-connected-user.d.ts +6 -0
- package/dist/types/utils/event-triggered-by-connected-user.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/logger.d.ts +10 -1
- package/dist/types/utils/logger.d.ts.map +1 -1
- package/dist/types/utils/state-update-queue.d.ts +22 -2
- package/dist/types/utils/state-update-queue.d.ts.map +1 -1
- package/dist/types/utils/type-assertions.d.ts +2 -5
- package/dist/types/utils/type-assertions.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +15 -10
- package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +1 -1
- package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +1 -1
- package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +131 -1
- package/src/feed/event-handlers/activity/handle-activity-updated.ts +38 -15
- package/src/feed/event-handlers/comment/handle-comment-added.test.ts +131 -7
- package/src/feed/event-handlers/comment/handle-comment-added.ts +24 -4
- package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +124 -2
- package/src/feed/event-handlers/comment/handle-comment-deleted.ts +29 -3
- package/src/feed/event-handlers/comment/{handle-comment-reaction.test.ts → handle-comment-reaction-added.test.ts} +152 -138
- package/src/feed/event-handlers/comment/handle-comment-reaction-added.ts +72 -0
- package/src/feed/event-handlers/comment/handle-comment-reaction-deleted.test.ts +343 -0
- package/src/feed/event-handlers/comment/handle-comment-reaction-deleted.ts +74 -0
- package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +137 -1
- package/src/feed/event-handlers/comment/handle-comment-updated.ts +29 -4
- package/src/feed/event-handlers/comment/index.ts +3 -1
- package/src/feed/event-handlers/comment/utils/index.ts +1 -0
- package/src/feed/event-handlers/comment/utils/update-comment-count.test.ts +320 -0
- package/src/feed/event-handlers/comment/utils/update-comment-count.ts +51 -0
- package/src/feed/event-handlers/follow/handle-follow-deleted.ts +1 -1
- package/src/feed/feed.ts +4 -3
- package/src/feeds-client/feeds-client.ts +104 -0
- package/src/test-utils/response-generators.ts +18 -1
- package/src/types-internal.ts +4 -4
- package/src/utils/ensure-exhausted.ts +5 -0
- package/src/utils/event-triggered-by-connected-user.test.ts +73 -0
- package/src/utils/event-triggered-by-connected-user.ts +15 -0
- package/src/utils/index.ts +2 -1
- package/src/utils/logger.ts +2 -1
- package/src/utils/state-update-queue.ts +89 -25
- package/src/utils/type-assertions.ts +2 -3
- package/dist/index-B0Mm2xFU.js.map +0 -1
- package/dist/index-rSXIDTdA.mjs.map +0 -1
- package/dist/types/feed/event-handlers/comment/handle-comment-reaction.d.ts +0 -4
- package/dist/types/feed/event-handlers/comment/handle-comment-reaction.d.ts.map +0 -1
- package/src/feed/event-handlers/comment/handle-comment-reaction.ts +0 -61
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { updateCommentCount as updateCommentCountInternal } from './update-comment-count';
|
|
4
|
+
import * as commentHandlers from '../handle-comment-updated';
|
|
5
|
+
import * as activityHandlers from '../../activity';
|
|
6
|
+
import { FeedsClient } from '../../../../feeds-client';
|
|
7
|
+
import { Feed } from '../../../feed';
|
|
8
|
+
import { ActivityResponse, CommentResponse } from '../../../../gen/models';
|
|
9
|
+
import {
|
|
10
|
+
generateCommentResponse,
|
|
11
|
+
generateFeedResponse,
|
|
12
|
+
generateOwnUser,
|
|
13
|
+
getHumanId,
|
|
14
|
+
} from '../../../../test-utils';
|
|
15
|
+
|
|
16
|
+
vi.mock('../../activity', async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal<typeof import('../../activity')>();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
handleActivityUpdated: vi.fn(),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('../handle-comment-updated', async (importOriginal) => {
|
|
25
|
+
const actual = await importOriginal<typeof import('../handle-comment-updated')>();
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
handleCommentUpdated: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const handleCommentUpdated = commentHandlers.handleCommentUpdated as unknown as ReturnType<typeof vi.fn>;
|
|
33
|
+
const handleActivityUpdated = activityHandlers.handleActivityUpdated as unknown as ReturnType<typeof vi.fn>;
|
|
34
|
+
|
|
35
|
+
describe('updateCommentCount', () => {
|
|
36
|
+
let feed: Feed;
|
|
37
|
+
let client: FeedsClient;
|
|
38
|
+
let currentUserId: string;
|
|
39
|
+
let activityId: string;
|
|
40
|
+
let existingActivity: ActivityResponse;
|
|
41
|
+
|
|
42
|
+
let updateCommentCount: OmitThisParameter<typeof updateCommentCountInternal>;
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
client = new FeedsClient('mock-api-key');
|
|
46
|
+
currentUserId = getHumanId();
|
|
47
|
+
|
|
48
|
+
client.state.partialNext({
|
|
49
|
+
connected_user: generateOwnUser({ id: currentUserId }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const feedResponse = generateFeedResponse({
|
|
53
|
+
id: 'main',
|
|
54
|
+
group_id: 'user',
|
|
55
|
+
created_by: { id: currentUserId },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
feed = new Feed(
|
|
59
|
+
client,
|
|
60
|
+
feedResponse.group_id,
|
|
61
|
+
feedResponse.id,
|
|
62
|
+
feedResponse,
|
|
63
|
+
);
|
|
64
|
+
feed.state.partialNext({ watch: false });
|
|
65
|
+
|
|
66
|
+
updateCommentCount = updateCommentCountInternal.bind(feed);
|
|
67
|
+
|
|
68
|
+
activityId = `activity-${getHumanId()}`;
|
|
69
|
+
existingActivity = { id: activityId, comment_count: 0 } as ActivityResponse;
|
|
70
|
+
|
|
71
|
+
feed.state.partialNext({ activities: [existingActivity] });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => {
|
|
75
|
+
vi.resetAllMocks();
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('correctly updates parent comment reply_count and comment_count (with watch: false)', () => {
|
|
79
|
+
const parentCommentId = 'c1';
|
|
80
|
+
|
|
81
|
+
const existingComment = generateCommentResponse({
|
|
82
|
+
id: parentCommentId,
|
|
83
|
+
object_id: activityId,
|
|
84
|
+
reply_count: 0,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
feed.state.partialNext({
|
|
88
|
+
activities: [{ ...existingActivity, comment_count: 1 }],
|
|
89
|
+
comments_by_entity_id: {
|
|
90
|
+
[activityId]: {
|
|
91
|
+
comments: [existingComment],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const incomingReply = generateCommentResponse({
|
|
97
|
+
id: 'c2',
|
|
98
|
+
object_id: activityId,
|
|
99
|
+
parent_id: parentCommentId,
|
|
100
|
+
reply_count: 0,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
updateCommentCount({
|
|
104
|
+
activity: { ...existingActivity, comment_count: 11 },
|
|
105
|
+
comment: incomingReply,
|
|
106
|
+
replyCountUpdater: (prev: number) => prev + 1,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(handleCommentUpdated).toHaveBeenCalledTimes(1);
|
|
110
|
+
const [commentArg, secondArg] = handleCommentUpdated.mock.calls[0];
|
|
111
|
+
expect(secondArg).toBe(false);
|
|
112
|
+
expect(commentArg).toEqual({
|
|
113
|
+
comment: {
|
|
114
|
+
...existingComment,
|
|
115
|
+
reply_count: 1,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(handleActivityUpdated).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(handleActivityUpdated).toHaveBeenCalledWith({
|
|
121
|
+
activity: { id: activityId, comment_count: 11 },
|
|
122
|
+
}, false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('updates parent comment reply_count when grandparent is missing (falls back to activity store)', () => {
|
|
126
|
+
const parentCommentId = 'c1';
|
|
127
|
+
|
|
128
|
+
const existingComment = generateCommentResponse({
|
|
129
|
+
id: parentCommentId,
|
|
130
|
+
object_id: activityId,
|
|
131
|
+
reply_count: 5,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
feed.state.partialNext({
|
|
135
|
+
activities: [{ ...existingActivity, comment_count: 0 }],
|
|
136
|
+
comments_by_entity_id: {
|
|
137
|
+
[activityId]: {
|
|
138
|
+
comments: [existingComment],
|
|
139
|
+
pagination: { sort: 'first' },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const incomingReply = generateCommentResponse({
|
|
145
|
+
id: 'c2',
|
|
146
|
+
object_id: activityId,
|
|
147
|
+
parent_id: parentCommentId,
|
|
148
|
+
reply_count: 0,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
updateCommentCount({
|
|
152
|
+
activity: { ...existingActivity, comment_count: 1 },
|
|
153
|
+
comment: incomingReply,
|
|
154
|
+
replyCountUpdater: (prev: number) => prev - 2,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(handleCommentUpdated).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(handleCommentUpdated).toHaveBeenCalledWith(
|
|
159
|
+
{ comment: { ...existingComment, reply_count: 3 } },
|
|
160
|
+
false,
|
|
161
|
+
);
|
|
162
|
+
expect(handleActivityUpdated).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(handleActivityUpdated).toHaveBeenCalledWith({
|
|
164
|
+
activity: { id: activityId, comment_count: 1 },
|
|
165
|
+
}, false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('updates the correct parent when replying to a depth-3 comment (entity_parent_id points to the parent comment store)', () => {
|
|
169
|
+
const c1: CommentResponse = generateCommentResponse({ id: 'c1', object_id: activityId, reply_count: 0 });
|
|
170
|
+
const c2: CommentResponse = generateCommentResponse({ id: 'c2', object_id: activityId, parent_id: 'c1', reply_count: 7 });
|
|
171
|
+
|
|
172
|
+
feed.state.partialNext({
|
|
173
|
+
activities: [{ ...existingActivity, comment_count: 1 }],
|
|
174
|
+
comments_by_entity_id: {
|
|
175
|
+
[activityId]: {
|
|
176
|
+
comments: [c1],
|
|
177
|
+
},
|
|
178
|
+
['c1']: { entity_parent_id: activityId, comments: [c2] },
|
|
179
|
+
['c2']: { entity_parent_id: 'c1', comments: [] },
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const incomingReplyToC2: CommentResponse = generateCommentResponse({
|
|
184
|
+
id: 'c3',
|
|
185
|
+
object_id: activityId,
|
|
186
|
+
parent_id: 'c2',
|
|
187
|
+
reply_count: 0,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
updateCommentCount({
|
|
191
|
+
activity: { ...existingActivity, comment_count: 1 },
|
|
192
|
+
comment: incomingReplyToC2,
|
|
193
|
+
replyCountUpdater: (prev: number) => prev + 5,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
expect(handleCommentUpdated).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(handleCommentUpdated).toHaveBeenCalledWith(
|
|
198
|
+
{ comment: { ...c2, reply_count: 12 } },
|
|
199
|
+
false,
|
|
200
|
+
);
|
|
201
|
+
expect(handleActivityUpdated).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(handleActivityUpdated).toHaveBeenCalledWith({
|
|
203
|
+
activity: { id: activityId, comment_count: 1 },
|
|
204
|
+
}, false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('does nothing on parent reply when parent comment is not found in the resolved store', () => {
|
|
208
|
+
feed.state.partialNext({
|
|
209
|
+
activities: [{ ...existingActivity, comment_count: 4 }],
|
|
210
|
+
comments_by_entity_id: {
|
|
211
|
+
[activityId]: { comments: [], },
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const incomingReply: CommentResponse = generateCommentResponse({
|
|
216
|
+
id: 'child',
|
|
217
|
+
object_id: activityId,
|
|
218
|
+
parent_id: 'missing-parent',
|
|
219
|
+
reply_count: 0,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
updateCommentCount({
|
|
223
|
+
activity: { ...existingActivity, comment_count: 5 },
|
|
224
|
+
comment: incomingReply,
|
|
225
|
+
replyCountUpdater: (prev: number) => prev + 1,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(handleCommentUpdated).not.toHaveBeenCalled();
|
|
229
|
+
// the activity still updates, as watch === true
|
|
230
|
+
expect(handleActivityUpdated).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(handleActivityUpdated).toHaveBeenCalledWith({
|
|
232
|
+
activity: { id: activityId, comment_count: 5 },
|
|
233
|
+
}, false);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('skips parent reply update when comment has no parent_id (top-level comment)', () => {
|
|
237
|
+
feed.state.partialNext({
|
|
238
|
+
activities: [{ ...existingActivity, comment_count: 1 }],
|
|
239
|
+
comments_by_entity_id: {
|
|
240
|
+
[activityId]: { comments: [], },
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const topLevel: CommentResponse = generateCommentResponse({
|
|
245
|
+
id: 'cTop',
|
|
246
|
+
object_id: activityId,
|
|
247
|
+
reply_count: 0,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
updateCommentCount({
|
|
251
|
+
activity: { ...existingActivity, comment_count: 3 },
|
|
252
|
+
comment: topLevel,
|
|
253
|
+
replyCountUpdater: (prev: number) => prev + 100,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(handleCommentUpdated).not.toHaveBeenCalled();
|
|
257
|
+
expect(handleActivityUpdated).toHaveBeenCalledTimes(1);
|
|
258
|
+
expect(handleActivityUpdated).toHaveBeenCalledWith({
|
|
259
|
+
activity: { id: activityId, comment_count: 3 },
|
|
260
|
+
}, false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('no-ops any update if neither activity nor comment are found', () => {
|
|
264
|
+
feed.state.partialNext({
|
|
265
|
+
activities: [{ id: 'other-1', comment_count: 1 } as ActivityResponse, { id: 'other-2', comment_count: 3 } as ActivityResponse],
|
|
266
|
+
comments_by_entity_id: {},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const topLevel: CommentResponse = generateCommentResponse({
|
|
270
|
+
id: 'cTop',
|
|
271
|
+
object_id: 'missing-activity',
|
|
272
|
+
reply_count: 0,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
updateCommentCount({
|
|
276
|
+
activity: { ...existingActivity, comment_count: 3 },
|
|
277
|
+
comment: topLevel,
|
|
278
|
+
replyCountUpdater: (prev: number) => prev + 1,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(handleActivityUpdated).not.toHaveBeenCalled();
|
|
282
|
+
expect(handleCommentUpdated).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('can update both parent reply_count and activity comment_count in the same call', () => {
|
|
286
|
+
const parentCommentId = 'c1';
|
|
287
|
+
const c1: CommentResponse = generateCommentResponse({ id: parentCommentId, object_id: activityId, reply_count: 10 });
|
|
288
|
+
|
|
289
|
+
feed.state.partialNext({
|
|
290
|
+
activities: [{ ...existingActivity, comment_count: 100 }],
|
|
291
|
+
comments_by_entity_id: {
|
|
292
|
+
[activityId]: { comments: [c1] },
|
|
293
|
+
[parentCommentId]: { entity_parent_id: activityId },
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const incomingReply: CommentResponse = generateCommentResponse({
|
|
298
|
+
id: 'c2',
|
|
299
|
+
object_id: activityId,
|
|
300
|
+
parent_id: parentCommentId,
|
|
301
|
+
reply_count: 0,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
updateCommentCount({
|
|
305
|
+
activity: { ...existingActivity, comment_count: 97 },
|
|
306
|
+
comment: incomingReply,
|
|
307
|
+
replyCountUpdater: (prev: number) => prev + 2,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(handleCommentUpdated).toHaveBeenCalledTimes(1);
|
|
311
|
+
expect(handleCommentUpdated).toHaveBeenCalledWith(
|
|
312
|
+
{ comment: { ...c1, reply_count: 12 } },
|
|
313
|
+
false,
|
|
314
|
+
);
|
|
315
|
+
expect(handleActivityUpdated).toHaveBeenCalledTimes(1);
|
|
316
|
+
expect(handleActivityUpdated).toHaveBeenCalledWith({
|
|
317
|
+
activity: { id: activityId, comment_count: 97 },
|
|
318
|
+
}, false);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ActivityResponse, Feed } from '@self';
|
|
2
|
+
import { CommentResponse } from '@self';
|
|
3
|
+
import { handleCommentUpdated } from '../handle-comment-updated';
|
|
4
|
+
import { handleActivityUpdated } from '../../activity';
|
|
5
|
+
|
|
6
|
+
export function updateCommentCount(
|
|
7
|
+
this: Feed,
|
|
8
|
+
{
|
|
9
|
+
activity,
|
|
10
|
+
comment,
|
|
11
|
+
replyCountUpdater,
|
|
12
|
+
}: {
|
|
13
|
+
activity: ActivityResponse;
|
|
14
|
+
comment: CommentResponse;
|
|
15
|
+
replyCountUpdater: (prevCount: number) => number;
|
|
16
|
+
},
|
|
17
|
+
) {
|
|
18
|
+
const parentActivityId = comment.object_id;
|
|
19
|
+
// we update a comment if the new one is depth 2 or deeper
|
|
20
|
+
if (comment?.parent_id) {
|
|
21
|
+
// if the comment has a parent comment, we need to update the reply count on it
|
|
22
|
+
// if the parent has a parent comment as well, parent comment is stored in a list indexed by grandparent comment id
|
|
23
|
+
const grandparentCommentId =
|
|
24
|
+
this.currentState.comments_by_entity_id[comment?.parent_id ?? '']
|
|
25
|
+
?.entity_parent_id;
|
|
26
|
+
// pick the grandparent of the comment we are trying to add so that we
|
|
27
|
+
// can update its state; if it doesn't exist the parent is an activity
|
|
28
|
+
const idToUpdate = grandparentCommentId ?? parentActivityId;
|
|
29
|
+
const commentToUpdate = this.currentState.comments_by_entity_id[
|
|
30
|
+
idToUpdate
|
|
31
|
+
]?.comments?.find((c) => c.id === comment.parent_id);
|
|
32
|
+
if (commentToUpdate) {
|
|
33
|
+
handleCommentUpdated.bind(this)(
|
|
34
|
+
{
|
|
35
|
+
comment: {
|
|
36
|
+
...commentToUpdate,
|
|
37
|
+
reply_count: replyCountUpdater(commentToUpdate.reply_count),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
false,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// finally, update the activity comment_count if it exists
|
|
46
|
+
if (this.hasActivity(activity.id)) {
|
|
47
|
+
handleActivityUpdated.bind(this)({
|
|
48
|
+
activity,
|
|
49
|
+
}, false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -74,7 +74,7 @@ export function handleFollowDeleted(
|
|
|
74
74
|
|
|
75
75
|
if (
|
|
76
76
|
!shouldUpdateState({
|
|
77
|
-
stateUpdateQueueId: getStateUpdateQueueId(follow, 'deleted'),
|
|
77
|
+
stateUpdateQueueId: getStateUpdateQueueId(follow, 'follow-deleted'),
|
|
78
78
|
stateUpdateQueue: this.stateUpdateQueue,
|
|
79
79
|
watch: this.currentState.watch,
|
|
80
80
|
fromWs,
|
package/src/feed/feed.ts
CHANGED
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
handleFeedMemberAdded,
|
|
35
35
|
handleFeedMemberRemoved,
|
|
36
36
|
handleFeedMemberUpdated,
|
|
37
|
-
handleCommentReaction,
|
|
38
37
|
handleBookmarkAdded,
|
|
39
38
|
handleActivityDeleted,
|
|
40
39
|
handleActivityRemovedFromFeed,
|
|
@@ -43,6 +42,8 @@ import {
|
|
|
43
42
|
handleActivityMarked,
|
|
44
43
|
handleActivityReactionAdded,
|
|
45
44
|
handleActivityReactionDeleted,
|
|
45
|
+
handleCommentReactionAdded,
|
|
46
|
+
handleCommentReactionDeleted,
|
|
46
47
|
} from './event-handlers';
|
|
47
48
|
import { capitalize } from '../common/utils';
|
|
48
49
|
import type {
|
|
@@ -167,8 +168,8 @@ export class Feed extends FeedApi {
|
|
|
167
168
|
'feeds.follow.created': handleFollowCreated.bind(this),
|
|
168
169
|
'feeds.follow.deleted': handleFollowDeleted.bind(this),
|
|
169
170
|
'feeds.follow.updated': handleFollowUpdated.bind(this),
|
|
170
|
-
'feeds.comment.reaction.added':
|
|
171
|
-
'feeds.comment.reaction.deleted':
|
|
171
|
+
'feeds.comment.reaction.added': handleCommentReactionAdded.bind(this),
|
|
172
|
+
'feeds.comment.reaction.deleted': handleCommentReactionDeleted.bind(this),
|
|
172
173
|
'feeds.comment.reaction.updated': Feed.noop,
|
|
173
174
|
'feeds.feed_member.added': handleFeedMemberAdded.bind(this),
|
|
174
175
|
'feeds.feed_member.removed': handleFeedMemberRemoved.bind(this),
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { FeedsApi } from '../gen/feeds/FeedsApi';
|
|
2
2
|
import {
|
|
3
3
|
ActivityResponse,
|
|
4
|
+
AddCommentReactionRequest,
|
|
5
|
+
AddCommentReactionResponse,
|
|
6
|
+
AddCommentRequest,
|
|
7
|
+
AddCommentResponse,
|
|
4
8
|
AddReactionRequest,
|
|
5
9
|
DeleteActivityReactionResponse,
|
|
10
|
+
DeleteCommentReactionResponse,
|
|
11
|
+
DeleteCommentResponse,
|
|
6
12
|
FeedResponse,
|
|
7
13
|
FileUploadRequest,
|
|
8
14
|
FollowBatchRequest,
|
|
@@ -13,6 +19,10 @@ import {
|
|
|
13
19
|
PollVotesResponse,
|
|
14
20
|
QueryFeedsRequest,
|
|
15
21
|
QueryPollVotesRequest,
|
|
22
|
+
UpdateActivityRequest,
|
|
23
|
+
UpdateActivityResponse,
|
|
24
|
+
UpdateCommentRequest,
|
|
25
|
+
UpdateCommentResponse,
|
|
16
26
|
UpdateFollowRequest,
|
|
17
27
|
UserRequest,
|
|
18
28
|
WSEvent,
|
|
@@ -41,6 +51,12 @@ import {
|
|
|
41
51
|
Feed,
|
|
42
52
|
handleActivityReactionAdded,
|
|
43
53
|
handleActivityReactionDeleted,
|
|
54
|
+
handleActivityUpdated,
|
|
55
|
+
handleCommentAdded,
|
|
56
|
+
handleCommentDeleted,
|
|
57
|
+
handleCommentReactionAdded,
|
|
58
|
+
handleCommentReactionDeleted,
|
|
59
|
+
handleCommentUpdated,
|
|
44
60
|
handleFeedUpdated,
|
|
45
61
|
handleFollowCreated,
|
|
46
62
|
handleFollowDeleted,
|
|
@@ -53,6 +69,7 @@ import {
|
|
|
53
69
|
SyncFailure,
|
|
54
70
|
UnhandledErrorType,
|
|
55
71
|
} from '../common/real-time/event-models';
|
|
72
|
+
import { updateCommentCount } from '../feed/event-handlers/comment/utils';
|
|
56
73
|
import { configureLoggers } from '../utils/logger';
|
|
57
74
|
|
|
58
75
|
export type FeedsClientState = {
|
|
@@ -331,6 +348,72 @@ export class FeedsClient extends FeedsApi {
|
|
|
331
348
|
});
|
|
332
349
|
};
|
|
333
350
|
|
|
351
|
+
updateActivity = async (
|
|
352
|
+
request: UpdateActivityRequest & {
|
|
353
|
+
id: string;
|
|
354
|
+
},
|
|
355
|
+
): Promise<StreamResponse<UpdateActivityResponse>> => {
|
|
356
|
+
const response = await super.updateActivity(request);
|
|
357
|
+
for (const feed of Object.values(this.activeFeeds)) {
|
|
358
|
+
handleActivityUpdated.bind(feed)(response, false);
|
|
359
|
+
}
|
|
360
|
+
return response;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
addComment = async (
|
|
364
|
+
request: AddCommentRequest,
|
|
365
|
+
): Promise<StreamResponse<AddCommentResponse>> => {
|
|
366
|
+
const response = await super.addComment(request);
|
|
367
|
+
const { comment } = response;
|
|
368
|
+
for (const feed of Object.values(this.activeFeeds)) {
|
|
369
|
+
handleCommentAdded.bind(feed)(response, false);
|
|
370
|
+
const parentActivityId = comment.object_id;
|
|
371
|
+
if (feed.hasActivity(parentActivityId)) {
|
|
372
|
+
const activityToUpdate = feed.currentState.activities?.find(
|
|
373
|
+
(activity) => activity.id === parentActivityId,
|
|
374
|
+
);
|
|
375
|
+
if (activityToUpdate) {
|
|
376
|
+
updateCommentCount.bind(feed)({
|
|
377
|
+
activity: {
|
|
378
|
+
...activityToUpdate,
|
|
379
|
+
comment_count: activityToUpdate.comment_count + 1,
|
|
380
|
+
},
|
|
381
|
+
comment,
|
|
382
|
+
replyCountUpdater: (prevCount) => prevCount + 1,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return response;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
updateComment = async (
|
|
391
|
+
request: UpdateCommentRequest & { id: string },
|
|
392
|
+
): Promise<StreamResponse<UpdateCommentResponse>> => {
|
|
393
|
+
const response = await super.updateComment(request);
|
|
394
|
+
for (const feed of Object.values(this.activeFeeds)) {
|
|
395
|
+
handleCommentUpdated.bind(feed)(response, false);
|
|
396
|
+
}
|
|
397
|
+
return response;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
deleteComment = async (request: {
|
|
401
|
+
id: string;
|
|
402
|
+
hard_delete?: boolean;
|
|
403
|
+
}): Promise<StreamResponse<DeleteCommentResponse>> => {
|
|
404
|
+
const response = await super.deleteComment(request);
|
|
405
|
+
const { activity, comment } = response;
|
|
406
|
+
for (const feed of Object.values(this.activeFeeds)) {
|
|
407
|
+
handleCommentDeleted.bind(feed)({ comment }, false);
|
|
408
|
+
updateCommentCount.bind(feed)({
|
|
409
|
+
activity,
|
|
410
|
+
comment,
|
|
411
|
+
replyCountUpdater: (prevCount) => prevCount - 1,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return response;
|
|
415
|
+
};
|
|
416
|
+
|
|
334
417
|
addReaction = async (
|
|
335
418
|
request: AddReactionRequest & {
|
|
336
419
|
activity_id: string;
|
|
@@ -354,6 +437,27 @@ export class FeedsClient extends FeedsApi {
|
|
|
354
437
|
return response;
|
|
355
438
|
};
|
|
356
439
|
|
|
440
|
+
addCommentReaction = async (
|
|
441
|
+
request: AddCommentReactionRequest & { id: string },
|
|
442
|
+
): Promise<StreamResponse<AddCommentReactionResponse>> => {
|
|
443
|
+
const response = await super.addCommentReaction(request);
|
|
444
|
+
for (const feed of Object.values(this.activeFeeds)) {
|
|
445
|
+
handleCommentReactionAdded.bind(feed)(response, false);
|
|
446
|
+
}
|
|
447
|
+
return response;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
deleteCommentReaction = async (request: {
|
|
451
|
+
id: string;
|
|
452
|
+
type: string;
|
|
453
|
+
}): Promise<StreamResponse<DeleteCommentReactionResponse>> => {
|
|
454
|
+
const response = await super.deleteCommentReaction(request);
|
|
455
|
+
for (const feed of Object.values(this.activeFeeds)) {
|
|
456
|
+
handleCommentReactionDeleted.bind(feed)(response, false);
|
|
457
|
+
}
|
|
458
|
+
return response;
|
|
459
|
+
};
|
|
460
|
+
|
|
357
461
|
queryPollAnswers = async (
|
|
358
462
|
request: QueryPollVotesRequest & { poll_id: string; user_id?: string },
|
|
359
463
|
): Promise<StreamResponse<PollVotesResponse>> => {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
OwnUser,
|
|
12
12
|
OwnUserResponse,
|
|
13
13
|
PinActivityResponse,
|
|
14
|
-
UserResponse,
|
|
14
|
+
UserResponse, UserResponseCommonFields,
|
|
15
15
|
} from '../gen/models';
|
|
16
16
|
import { humanId } from 'human-id';
|
|
17
17
|
import { EventPayload } from '../types-internal';
|
|
@@ -35,6 +35,23 @@ export const generateUserResponse = (
|
|
|
35
35
|
...overrides,
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
export const generateUserResponseCommonFields = (
|
|
39
|
+
overrides: Partial<UserResponseCommonFields> = {},
|
|
40
|
+
): UserResponseCommonFields => ({
|
|
41
|
+
id: `user-${getHumanId()}`,
|
|
42
|
+
name: humanId({ separator: ' ' }),
|
|
43
|
+
created_at: new Date(),
|
|
44
|
+
updated_at: new Date(),
|
|
45
|
+
banned: false,
|
|
46
|
+
language: 'en',
|
|
47
|
+
online: false,
|
|
48
|
+
role: 'user',
|
|
49
|
+
blocked_user_ids: [],
|
|
50
|
+
teams: [],
|
|
51
|
+
custom: {},
|
|
52
|
+
...overrides,
|
|
53
|
+
});
|
|
54
|
+
|
|
38
55
|
export const generateOwnUserResponse = (
|
|
39
56
|
overrides: Partial<OwnUserResponse> = {},
|
|
40
57
|
): OwnUserResponse => ({
|
package/src/types-internal.ts
CHANGED
|
@@ -11,10 +11,10 @@ export type EventPayload<T extends WSEvent['type']> = Extract<
|
|
|
11
11
|
{ type: T }
|
|
12
12
|
>;
|
|
13
13
|
|
|
14
|
-
export type PartializeAllBut<T, K extends keyof T> =
|
|
15
|
-
[
|
|
16
|
-
};
|
|
14
|
+
export type PartializeAllBut<T, K extends keyof T> = {
|
|
15
|
+
[P in K]-?: T[P];
|
|
16
|
+
} & { [P in Exclude<keyof T, K>]?: T[P] };
|
|
17
17
|
|
|
18
18
|
export type CommonProps<A, B> = {
|
|
19
|
-
[K in keyof A & keyof B]: A[K] & B[K]
|
|
19
|
+
[K in keyof A & keyof B]: A[K] & B[K];
|
|
20
20
|
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { eventTriggeredByConnectedUser as eventTriggeredByConnectedUserInternal } from './event-triggered-by-connected-user';
|
|
4
|
+
import { FeedsClient } from '../feeds-client';
|
|
5
|
+
import { Feed } from '../feed';
|
|
6
|
+
import {
|
|
7
|
+
generateFeedResponse,
|
|
8
|
+
generateOwnUser, generateUserResponse,
|
|
9
|
+
getHumanId,
|
|
10
|
+
} from '../test-utils';
|
|
11
|
+
|
|
12
|
+
describe('eventTriggeredByConnectedUser', () => {
|
|
13
|
+
let feed: Feed;
|
|
14
|
+
let client: FeedsClient;
|
|
15
|
+
let currentUserId: string;
|
|
16
|
+
|
|
17
|
+
let eventTriggeredByConnectedUser: OmitThisParameter<typeof eventTriggeredByConnectedUserInternal>
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
client = new FeedsClient('mock-api-key');
|
|
21
|
+
currentUserId = getHumanId();
|
|
22
|
+
|
|
23
|
+
client.state.partialNext({
|
|
24
|
+
connected_user: generateOwnUser({ id: currentUserId }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const feedResponse = generateFeedResponse({
|
|
28
|
+
id: 'main',
|
|
29
|
+
group_id: 'user',
|
|
30
|
+
created_by: { id: currentUserId },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
feed = new Feed(
|
|
34
|
+
client,
|
|
35
|
+
feedResponse.group_id,
|
|
36
|
+
feedResponse.id,
|
|
37
|
+
feedResponse,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
eventTriggeredByConnectedUser = eventTriggeredByConnectedUserInternal.bind(feed);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
vi.resetAllMocks();
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns true when payload.user.id matches connected_user.id', () => {
|
|
48
|
+
const user = generateUserResponse({ id: currentUserId });
|
|
49
|
+
const result = eventTriggeredByConnectedUser({ user });
|
|
50
|
+
expect(result).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns true when payload.user is undefined and connected_user exists', () => {
|
|
54
|
+
const result = eventTriggeredByConnectedUser({});
|
|
55
|
+
expect(result).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns false when payload.user.id differs from connected_user.id', () => {
|
|
59
|
+
const user = generateUserResponse({ id: getHumanId() });
|
|
60
|
+
const result = eventTriggeredByConnectedUser({ user });
|
|
61
|
+
expect(result).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('returns false when connected_user is undefined (even if payload.user present)', () => {
|
|
65
|
+
// @ts-expect-error using protected value only in tests
|
|
66
|
+
feed.client.state.partialNext({ connected_user: undefined });
|
|
67
|
+
const user = generateUserResponse({ id: currentUserId });
|
|
68
|
+
const result1 = eventTriggeredByConnectedUser({ user });
|
|
69
|
+
const result2 = eventTriggeredByConnectedUser({});
|
|
70
|
+
expect(result1).toBe(false);
|
|
71
|
+
expect(result2).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Feed } from '../feed';
|
|
2
|
+
import { UserResponseCommonFields } from '../gen/models';
|
|
3
|
+
|
|
4
|
+
export function eventTriggeredByConnectedUser(
|
|
5
|
+
this: Feed,
|
|
6
|
+
payload: { user?: UserResponseCommonFields },
|
|
7
|
+
) {
|
|
8
|
+
const connectedUser = this.client.state.getLatestValue().connected_user;
|
|
9
|
+
const payloadUser = payload.user ?? connectedUser;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
typeof connectedUser !== 'undefined' &&
|
|
13
|
+
connectedUser?.id === payloadUser?.id
|
|
14
|
+
);
|
|
15
|
+
}
|