@stream-io/feeds-client 0.2.0 → 0.2.2

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.
Files changed (83) hide show
  1. package/@react-bindings/hooks/feed-state-hooks/index.ts +4 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/@react-bindings/hooks/feed-state-hooks/index.d.ts +4 -0
  4. package/dist/@react-bindings/hooks/feed-state-hooks/useAggregatedActivities.d.ts +11 -0
  5. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivityRead.d.ts +6 -0
  6. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivitySeen.d.ts +6 -0
  7. package/dist/@react-bindings/hooks/feed-state-hooks/useNotificationStatus.d.ts +13 -0
  8. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +1 -1
  9. package/dist/index-react-bindings.browser.cjs +505 -222
  10. package/dist/index-react-bindings.browser.cjs.map +1 -1
  11. package/dist/index-react-bindings.browser.js +502 -223
  12. package/dist/index-react-bindings.browser.js.map +1 -1
  13. package/dist/index-react-bindings.node.cjs +505 -222
  14. package/dist/index-react-bindings.node.cjs.map +1 -1
  15. package/dist/index-react-bindings.node.js +502 -223
  16. package/dist/index-react-bindings.node.js.map +1 -1
  17. package/dist/index.browser.cjs +440 -205
  18. package/dist/index.browser.cjs.map +1 -1
  19. package/dist/index.browser.js +440 -206
  20. package/dist/index.browser.js.map +1 -1
  21. package/dist/index.node.cjs +440 -205
  22. package/dist/index.node.cjs.map +1 -1
  23. package/dist/index.node.js +440 -206
  24. package/dist/index.node.js.map +1 -1
  25. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +12 -3
  26. package/dist/src/feed/event-handlers/activity/handle-activity-marked.d.ts +11 -0
  27. package/dist/src/feed/event-handlers/activity/handle-activity-pinned.d.ts +3 -0
  28. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +10 -6
  29. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +10 -6
  30. package/dist/src/feed/event-handlers/activity/handle-activity-unpinned.d.ts +3 -0
  31. package/dist/src/feed/event-handlers/activity/handle-activity-updated.d.ts +7 -3
  32. package/dist/src/feed/event-handlers/activity/index.d.ts +1 -0
  33. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +10 -6
  34. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +10 -6
  35. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +10 -6
  36. package/dist/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +8 -1
  37. package/dist/src/feed/feed.d.ts +2 -2
  38. package/dist/src/gen/models/index.d.ts +36 -1
  39. package/dist/src/test-utils/response-generators.d.ts +66 -1
  40. package/dist/src/utils/index.d.ts +1 -0
  41. package/dist/src/utils/update-entity-in-array.d.ts +27 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/feed/event-handlers/activity/activity-marked-utils.test.ts +208 -0
  45. package/src/feed/event-handlers/activity/activity-reaction-utils.test.ts +108 -96
  46. package/src/feed/event-handlers/activity/activity-utils.test.ts +84 -122
  47. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +43 -10
  48. package/src/feed/event-handlers/activity/handle-activity-marked.ts +68 -0
  49. package/src/feed/event-handlers/activity/handle-activity-pinned.test.ts +60 -0
  50. package/src/feed/event-handlers/activity/handle-activity-pinned.ts +30 -0
  51. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +157 -0
  52. package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +82 -40
  53. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +200 -0
  54. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +89 -51
  55. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +95 -0
  56. package/src/feed/event-handlers/activity/handle-activity-unpinned.ts +30 -0
  57. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +115 -0
  58. package/src/feed/event-handlers/activity/handle-activity-updated.ts +73 -35
  59. package/src/feed/event-handlers/activity/index.ts +2 -1
  60. package/src/feed/event-handlers/bookmark/bookmark-utils.test.ts +121 -109
  61. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +178 -0
  62. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +82 -39
  63. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +188 -0
  64. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +86 -48
  65. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +196 -0
  66. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +83 -44
  67. package/src/feed/event-handlers/comment/handle-comment-added.test.ts +147 -0
  68. package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +133 -0
  69. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +24 -10
  70. package/src/feed/event-handlers/comment/handle-comment-reaction.test.ts +315 -0
  71. package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +131 -0
  72. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +7 -7
  73. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +2 -2
  74. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +1 -1
  75. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +120 -0
  76. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +47 -3
  77. package/src/feed/feed.ts +4 -2
  78. package/src/gen/model-decoders/decoders.ts +14 -1
  79. package/src/gen/models/index.ts +73 -2
  80. package/src/gen/moderation/ModerationApi.ts +1 -0
  81. package/src/test-utils/response-generators.ts +383 -0
  82. package/src/utils/index.ts +1 -0
  83. package/src/utils/update-entity-in-array.ts +51 -0
@@ -0,0 +1,315 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleCommentReaction } from './handle-comment-reaction';
5
+ import {
6
+ generateCommentResponse,
7
+ generateFeedReactionResponse,
8
+ generateFeedResponse,
9
+ generateOwnUser,
10
+ getHumanId,
11
+ generateCommentReactionAddedEvent,
12
+ generateCommentReactionDeletedEvent,
13
+ } from '../../../test-utils/response-generators';
14
+
15
+ describe(handleCommentReaction.name, () => {
16
+ let feed: Feed;
17
+ let client: FeedsClient;
18
+ let currentUserId: string;
19
+ let activityId: string;
20
+
21
+ beforeEach(() => {
22
+ client = new FeedsClient('mock-api-key');
23
+ currentUserId = getHumanId();
24
+ client.state.partialNext({
25
+ connected_user: generateOwnUser({ id: currentUserId }),
26
+ });
27
+ const feedResponse = generateFeedResponse({
28
+ id: 'main',
29
+ group_id: 'user',
30
+ created_by: { id: currentUserId },
31
+ });
32
+ feed = new Feed(
33
+ client,
34
+ feedResponse.group_id,
35
+ feedResponse.id,
36
+ feedResponse,
37
+ );
38
+ activityId = `activity-${getHumanId()}`;
39
+ });
40
+
41
+ it('adds to own_reactions for current user and updates comment fields from the event', () => {
42
+ const existingComment = generateCommentResponse({
43
+ object_id: activityId,
44
+ latest_reactions: [],
45
+ reaction_groups: {},
46
+ own_reactions: [],
47
+ });
48
+ feed.state.partialNext({
49
+ comments_by_entity_id: {
50
+ [activityId]: {
51
+ comments: [existingComment],
52
+ pagination: { sort: 'first' },
53
+ },
54
+ },
55
+ });
56
+
57
+ const event = generateCommentReactionAddedEvent({
58
+ comment: {
59
+ id: existingComment.id,
60
+ object_id: activityId,
61
+ latest_reactions: [],
62
+ reaction_groups: {},
63
+ },
64
+ reaction: {
65
+ type: 'like',
66
+ user: { id: currentUserId },
67
+ activity_id: activityId,
68
+ comment_id: existingComment.id,
69
+ },
70
+ });
71
+
72
+ const stateBefore = feed.currentState;
73
+ expect(
74
+ stateBefore.comments_by_entity_id[activityId]?.comments?.[0]
75
+ ?.own_reactions,
76
+ ).toHaveLength(0);
77
+
78
+ handleCommentReaction.call(feed, event);
79
+
80
+ const stateAfter = feed.currentState;
81
+ const [updated] = stateAfter.comments_by_entity_id[activityId]!.comments!;
82
+ expect(updated.own_reactions).toHaveLength(1);
83
+ expect(updated.own_reactions[0]).toBe(event.reaction);
84
+ // ensure we used event's latest_reactions & reaction_groups (Object.is check)
85
+ expect(updated.latest_reactions).toBe(event.comment.latest_reactions);
86
+ expect(updated.reaction_groups).toBe(event.comment.reaction_groups);
87
+ });
88
+
89
+ it('does not add to own_reactions if the target reaction belongs to another user', () => {
90
+ const existingComment = generateCommentResponse({
91
+ object_id: activityId,
92
+ latest_reactions: [],
93
+ reaction_groups: {},
94
+ own_reactions: [],
95
+ });
96
+ feed.state.partialNext({
97
+ comments_by_entity_id: {
98
+ [activityId]: {
99
+ comments: [existingComment],
100
+ pagination: { sort: 'first' },
101
+ },
102
+ },
103
+ });
104
+
105
+ const event = generateCommentReactionAddedEvent({
106
+ comment: {
107
+ id: existingComment.id,
108
+ object_id: activityId,
109
+ latest_reactions: [],
110
+ reaction_groups: {},
111
+ },
112
+ reaction: {
113
+ type: 'laugh',
114
+ user: { id: 'other-user' },
115
+ activity_id: activityId,
116
+ comment_id: existingComment.id,
117
+ },
118
+ });
119
+
120
+ handleCommentReaction.call(feed, event);
121
+ const stateAfter = feed.currentState;
122
+ const [updated] = stateAfter.comments_by_entity_id[activityId]!.comments!;
123
+ expect(updated.own_reactions).toHaveLength(0);
124
+ expect(updated.latest_reactions).toBe(event.comment.latest_reactions);
125
+ expect(updated.reaction_groups).toBe(event.comment.reaction_groups);
126
+ });
127
+
128
+ it('removes from own_reactions when the target reaction belongs to the current user', () => {
129
+ const commentId = `comment-${getHumanId()}`;
130
+ const existingReaction = generateFeedReactionResponse({
131
+ type: 'heart',
132
+ user: { id: currentUserId },
133
+ activity_id: activityId,
134
+ comment_id: commentId,
135
+ });
136
+ const existingComment = generateCommentResponse({
137
+ id: commentId,
138
+ object_id: activityId,
139
+ own_reactions: [existingReaction],
140
+ latest_reactions: [],
141
+ reaction_groups: {},
142
+ });
143
+ feed.state.partialNext({
144
+ comments_by_entity_id: {
145
+ [activityId]: {
146
+ comments: [existingComment],
147
+ pagination: { sort: 'first' },
148
+ },
149
+ },
150
+ });
151
+
152
+ const event = generateCommentReactionDeletedEvent({
153
+ comment: {
154
+ ...existingComment,
155
+ latest_reactions: [],
156
+ reaction_groups: {},
157
+ },
158
+ reaction: {
159
+ type: 'heart',
160
+ user: { id: currentUserId },
161
+ activity_id: activityId,
162
+ },
163
+ });
164
+
165
+ const stateBefore = feed.currentState;
166
+ expect(
167
+ stateBefore.comments_by_entity_id[activityId]!.comments![0].own_reactions,
168
+ ).toHaveLength(1);
169
+ handleCommentReaction.call(feed, event);
170
+ const stateAfter = feed.currentState;
171
+ const [updated] = stateAfter.comments_by_entity_id[activityId]!.comments!;
172
+ expect(updated.own_reactions).toHaveLength(0);
173
+ expect(updated.latest_reactions).toBe(event.comment.latest_reactions);
174
+ expect(updated.reaction_groups).toBe(event.comment.reaction_groups);
175
+ });
176
+
177
+ it('does not remove from own_reactions when target reaction does not belong to the current user', () => {
178
+ const commentId = `comment-${getHumanId()}`;
179
+ const ownReaction = generateFeedReactionResponse({
180
+ type: 'wow',
181
+ user: { id: currentUserId },
182
+ activity_id: activityId,
183
+ comment_id: commentId,
184
+ });
185
+ const existingComment = generateCommentResponse({
186
+ id: commentId,
187
+ object_id: activityId,
188
+ own_reactions: [ownReaction],
189
+ latest_reactions: [],
190
+ reaction_groups: {},
191
+ });
192
+ feed.state.partialNext({
193
+ comments_by_entity_id: {
194
+ [activityId]: {
195
+ comments: [existingComment],
196
+ pagination: { sort: 'first' },
197
+ },
198
+ },
199
+ });
200
+
201
+ const event = generateCommentReactionDeletedEvent({
202
+ comment: {
203
+ ...existingComment,
204
+ latest_reactions: [],
205
+ reaction_groups: {},
206
+ },
207
+ reaction: {
208
+ type: 'wow',
209
+ user: { id: 'other-user' },
210
+ comment_id: existingComment.id,
211
+ },
212
+ });
213
+
214
+ handleCommentReaction.call(feed, event);
215
+ const stateAfter = feed.currentState;
216
+ const [updated] = stateAfter.comments_by_entity_id[activityId]!.comments!;
217
+ expect(updated.own_reactions).toHaveLength(1);
218
+ expect(updated.own_reactions[0]).toBe(ownReaction);
219
+ expect(updated.latest_reactions).toBe(event.comment.latest_reactions);
220
+ expect(updated.reaction_groups).toBe(event.comment.reaction_groups);
221
+ });
222
+
223
+ it('does changes to the proper entity state (prefers parent_id)', () => {
224
+ const parentId = `comment-${getHumanId()}`;
225
+ const existingComment = generateCommentResponse({
226
+ object_id: activityId,
227
+ parent_id: parentId,
228
+ latest_reactions: [],
229
+ reaction_groups: {},
230
+ own_reactions: [],
231
+ });
232
+ feed.state.partialNext({
233
+ comments_by_entity_id: {
234
+ [parentId]: {
235
+ comments: [existingComment],
236
+ pagination: { sort: 'first' },
237
+ },
238
+ },
239
+ });
240
+
241
+ const addedEvent = generateCommentReactionAddedEvent({
242
+ comment: {
243
+ id: existingComment.id,
244
+ object_id: activityId,
245
+ parent_id: parentId,
246
+ latest_reactions: [],
247
+ reaction_groups: {},
248
+ },
249
+ reaction: {
250
+ type: 'like',
251
+ user: { id: currentUserId },
252
+ activity_id: activityId,
253
+ comment_id: existingComment.id,
254
+ },
255
+ });
256
+
257
+ const deletedEvent = generateCommentReactionDeletedEvent({
258
+ comment: {
259
+ id: existingComment.id,
260
+ object_id: activityId,
261
+ parent_id: parentId,
262
+ latest_reactions: [],
263
+ reaction_groups: {},
264
+ },
265
+ reaction: {
266
+ type: 'like',
267
+ user: { id: currentUserId },
268
+ activity_id: activityId,
269
+ comment_id: existingComment.id,
270
+ },
271
+ });
272
+
273
+ const stateBefore = feed.currentState;
274
+ expect(
275
+ stateBefore.comments_by_entity_id[parentId]?.comments?.[0]?.own_reactions,
276
+ ).toHaveLength(0);
277
+
278
+ handleCommentReaction.call(feed, addedEvent);
279
+ const stateAfter1 = feed.currentState;
280
+ const [updated1] = stateAfter1.comments_by_entity_id[parentId]!.comments!;
281
+ expect(updated1.own_reactions).toHaveLength(1);
282
+ expect(updated1.own_reactions[0]).toBe(addedEvent.reaction);
283
+ expect(updated1.latest_reactions).toBe(addedEvent.comment.latest_reactions);
284
+ expect(updated1.reaction_groups).toBe(addedEvent.comment.reaction_groups);
285
+
286
+ handleCommentReaction.call(feed, deletedEvent);
287
+ const stateAfter2 = feed.currentState;
288
+ const [updated2] = stateAfter2.comments_by_entity_id[parentId]!.comments!;
289
+ expect(updated2.own_reactions).toHaveLength(0);
290
+ expect(updated2.latest_reactions).toBe(
291
+ deletedEvent.comment.latest_reactions,
292
+ );
293
+ expect(updated2.reaction_groups).toBe(deletedEvent.comment.reaction_groups);
294
+ });
295
+
296
+ it('does nothing if comment is not found in state', () => {
297
+ const addedEvent = generateCommentReactionAddedEvent({
298
+ comment: { object_id: activityId },
299
+ reaction: { user: { id: currentUserId } },
300
+ });
301
+ const deletedEvent = generateCommentReactionDeletedEvent({
302
+ comment: { object_id: activityId },
303
+ reaction: { user: { id: currentUserId } },
304
+ });
305
+ const stateBefore = feed.currentState;
306
+
307
+ handleCommentReaction.call(feed, addedEvent);
308
+ const stateAfter1 = feed.currentState;
309
+ expect(stateAfter1).toBe(stateBefore);
310
+
311
+ handleCommentReaction.call(feed, deletedEvent);
312
+ const stateAfter2 = feed.currentState;
313
+ expect(stateAfter2).toBe(stateBefore);
314
+ });
315
+ });
@@ -0,0 +1,131 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleCommentUpdated } from './handle-comment-updated';
5
+ import {
6
+ generateCommentResponse,
7
+ generateCommentUpdatedEvent,
8
+ generateFeedResponse,
9
+ generateOwnUser,
10
+ getHumanId,
11
+ } from '../../../test-utils/response-generators';
12
+
13
+ describe(handleCommentUpdated.name, () => {
14
+ let feed: Feed;
15
+ let client: FeedsClient;
16
+ let currentUserId: string;
17
+ let activityId: string;
18
+
19
+ beforeEach(() => {
20
+ client = new FeedsClient('mock-api-key');
21
+ currentUserId = getHumanId();
22
+ client.state.partialNext({
23
+ connected_user: generateOwnUser({ id: currentUserId }),
24
+ });
25
+ const feedResponse = generateFeedResponse({
26
+ id: 'main',
27
+ group_id: 'user',
28
+ created_by: { id: currentUserId },
29
+ });
30
+ feed = new Feed(
31
+ client,
32
+ feedResponse.group_id,
33
+ feedResponse.id,
34
+ feedResponse,
35
+ );
36
+ activityId = `activity-${getHumanId()}`;
37
+ });
38
+
39
+ it('replaces the existing comment (activity level)', () => {
40
+ const original = generateCommentResponse({ object_id: activityId });
41
+ feed.state.partialNext({
42
+ comments_by_entity_id: {
43
+ [activityId]: { comments: [original], pagination: { sort: 'first' } },
44
+ },
45
+ });
46
+
47
+ const event = generateCommentUpdatedEvent({
48
+ comment: {
49
+ id: original.id,
50
+ object_id: activityId,
51
+ },
52
+ });
53
+
54
+ const stateBefore = feed.currentState;
55
+ handleCommentUpdated.call(feed, event);
56
+ const stateAfter = feed.currentState;
57
+
58
+ expect(stateAfter).not.toBe(stateBefore);
59
+ const commentsAfter =
60
+ stateAfter.comments_by_entity_id[activityId]?.comments;
61
+ const commentsBefore =
62
+ stateBefore.comments_by_entity_id[activityId]?.comments;
63
+ expect(commentsAfter).not.toBe(commentsBefore);
64
+ expect(commentsAfter).toHaveLength(1);
65
+ const [replaced] = commentsAfter!;
66
+ expect(replaced).toBe(event.comment);
67
+ });
68
+
69
+ it('updates the comment in the correct parent entity (prefers parent_id)', () => {
70
+ const parentId = `comment-${getHumanId()}`;
71
+ const reply = generateCommentResponse({
72
+ object_id: activityId,
73
+ parent_id: parentId,
74
+ });
75
+
76
+ feed.state.partialNext({
77
+ comments_by_entity_id: {
78
+ [parentId]: { comments: [reply], pagination: { sort: 'first' } },
79
+ },
80
+ });
81
+
82
+ const event = generateCommentUpdatedEvent({
83
+ comment: {
84
+ id: reply.id,
85
+ object_id: activityId,
86
+ parent_id: parentId,
87
+ },
88
+ });
89
+
90
+ const stateBefore = feed.currentState;
91
+ handleCommentUpdated.call(feed, event);
92
+ const stateAfter = feed.currentState;
93
+ expect(stateAfter).not.toBe(stateBefore);
94
+ const commentsBefore =
95
+ stateBefore.comments_by_entity_id[parentId]?.comments;
96
+ const commentsAfter = stateAfter.comments_by_entity_id[parentId]?.comments;
97
+ expect(commentsAfter).not.toBe(commentsBefore);
98
+ expect(commentsAfter).toHaveLength(1);
99
+ const [updatedReply] = commentsAfter!;
100
+ expect(updatedReply).toBe(event.comment);
101
+ });
102
+
103
+ it('does nothing if entity state does not exist', () => {
104
+ const event = generateCommentUpdatedEvent({
105
+ comment: { object_id: activityId },
106
+ });
107
+ const stateBefore = feed.currentState;
108
+ handleCommentUpdated.call(feed, event);
109
+ const stateAfter = feed.currentState;
110
+ expect(stateAfter).toBe(stateBefore);
111
+ });
112
+
113
+ it('does nothing if comment not found in existing entity state', () => {
114
+ // set up state with different comment
115
+ feed.state.partialNext({
116
+ comments_by_entity_id: {
117
+ [activityId]: {
118
+ comments: [generateCommentResponse({ object_id: activityId })],
119
+ pagination: { sort: 'first' },
120
+ },
121
+ },
122
+ });
123
+ const event = generateCommentUpdatedEvent({
124
+ comment: { object_id: activityId },
125
+ });
126
+ const stateBefore = feed.currentState;
127
+ handleCommentUpdated.call(feed, event);
128
+ const stateAfter = feed.currentState;
129
+ expect(stateAfter).toBe(stateBefore);
130
+ });
131
+ });
@@ -78,10 +78,10 @@ describe('handle-follow-created', () => {
78
78
 
79
79
  expect(result.changed).toBe(true);
80
80
  expect(result.data.following).toHaveLength(1);
81
- expect(result.data.following?.[0]).toEqual(follow);
81
+ expect(result.data.following?.[0]).toBe(follow);
82
82
  expect(result.data).toMatchObject(follow.source_feed);
83
83
  expect(result.data.own_follows).toBeUndefined();
84
- expect(result.data.following_count).toEqual(1);
84
+ expect(result.data.following_count).toBe(1);
85
85
  });
86
86
 
87
87
  it('should handle when someone follows this feed', () => {
@@ -120,10 +120,10 @@ describe('handle-follow-created', () => {
120
120
 
121
121
  expect(result.changed).toBe(true);
122
122
  expect(result.data.followers).toHaveLength(1);
123
- expect(result.data.followers?.[0]).toEqual(follow);
123
+ expect(result.data.followers?.[0]).toBe(follow);
124
124
  expect(result.data).toMatchObject(follow.target_feed);
125
125
  expect(result.data.own_follows).toBeUndefined();
126
- expect(result.data.follower_count).toEqual(1);
126
+ expect(result.data.follower_count).toBe(1);
127
127
  });
128
128
 
129
129
  it('should add to own_follows when connected user is the source', () => {
@@ -161,7 +161,7 @@ describe('handle-follow-created', () => {
161
161
 
162
162
  expect(result.changed).toBe(true);
163
163
  expect(result.data.own_follows).toHaveLength(1);
164
- expect(result.data.own_follows?.[0]).toEqual(follow);
164
+ expect(result.data.own_follows?.[0]).toBe(follow);
165
165
  });
166
166
 
167
167
  it('should not update followers/following when they are undefined', () => {
@@ -243,8 +243,8 @@ describe('handle-follow-created', () => {
243
243
 
244
244
  expect(result.changed).toBe(true);
245
245
  expect(result.data.followers).toHaveLength(2);
246
- expect(result.data.followers?.[0]).toEqual(follow);
247
- expect(result.data.followers?.[1]).toEqual(existingFollow);
246
+ expect(result.data.followers?.[0]).toBe(follow);
247
+ expect(result.data.followers?.[1]).toBe(existingFollow);
248
248
  });
249
249
  });
250
250
  });
@@ -95,7 +95,7 @@ describe('handle-follow-deleted', () => {
95
95
 
96
96
  expect(result.changed).toBe(true);
97
97
  expect(result.data.followers).toHaveLength(0);
98
- expect(result.data.own_follows).toEqual(currentState.own_follows);
98
+ expect(result.data.own_follows).toBe(currentState.own_follows);
99
99
  expect(result.data).toMatchObject(follow.target_feed);
100
100
  });
101
101
 
@@ -262,7 +262,7 @@ describe('handle-follow-deleted', () => {
262
262
 
263
263
  expect(result.changed).toBe(true);
264
264
  expect(result.data.following).toHaveLength(1);
265
- expect(result.data.following?.[0]).toEqual(followToKeep);
265
+ expect(result.data.following?.[0]).toBe(followToKeep);
266
266
  });
267
267
  });
268
268
  });
@@ -148,7 +148,7 @@ describe(handleFollowUpdated.name, () => {
148
148
 
149
149
  // State should not change
150
150
  const stateAfter = feed.currentState;
151
- expect(stateAfter).toEqual(stateBefore);
151
+ expect(stateAfter).toBe(stateBefore);
152
152
  });
153
153
 
154
154
  it('allows update again after clearing stateUpdateQueue', () => {
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ NotificationFeedUpdatedEvent,
4
+ NotificationStatusResponse,
5
+ AggregatedActivityResponse,
6
+ } from '../../../gen/models';
7
+ import { updateNotificationFeedFromEvent } from './handle-notification-feed-updated';
8
+
9
+ const createMockNotificationFeedUpdatedEvent = (
10
+ overrides: Partial<NotificationFeedUpdatedEvent> = {},
11
+ ): NotificationFeedUpdatedEvent => ({
12
+ created_at: new Date(),
13
+ fid: 'user:notification',
14
+ custom: {},
15
+ type: 'feeds.notification_feed.updated',
16
+ ...overrides,
17
+ });
18
+
19
+ const createMockNotificationStatus = (
20
+ overrides: Partial<NotificationStatusResponse> = {},
21
+ ): NotificationStatusResponse => ({
22
+ unread: 0,
23
+ unseen: 0,
24
+ ...overrides,
25
+ });
26
+
27
+ const createMockAggregatedActivity = (
28
+ overrides: Partial<AggregatedActivityResponse> = {},
29
+ ): AggregatedActivityResponse => ({
30
+ activity_count: 1,
31
+ created_at: new Date(),
32
+ group: 'test-group',
33
+ score: 1,
34
+ updated_at: new Date(),
35
+ user_count: 1,
36
+ activities: [],
37
+ ...overrides,
38
+ });
39
+
40
+ describe('notification-feed-utils', () => {
41
+ describe('updateNotificationFeedFromEvent', () => {
42
+ it('should return unchanged if event has no notification_status or aggregated_activities', () => {
43
+ const event = createMockNotificationFeedUpdatedEvent();
44
+
45
+ const result = updateNotificationFeedFromEvent(event);
46
+
47
+ expect(result.changed).toBe(false);
48
+ });
49
+
50
+ it('should update notification_status when event has notification_status', () => {
51
+ const notificationStatus = createMockNotificationStatus({
52
+ unread: 5,
53
+ unseen: 3,
54
+ read_activities: ['activity1', 'activity2'],
55
+ });
56
+ const event = createMockNotificationFeedUpdatedEvent({
57
+ notification_status: notificationStatus,
58
+ });
59
+
60
+ const result = updateNotificationFeedFromEvent(event);
61
+
62
+ expect(result.changed).toBe(true);
63
+ expect(result.data?.notification_status).toBe(notificationStatus);
64
+ expect(result.data?.aggregated_activities).toBeUndefined();
65
+ });
66
+
67
+ it('should update aggregated_activities when event has aggregated_activities', () => {
68
+ const aggregatedActivities = [
69
+ createMockAggregatedActivity({ group: 'group1' }),
70
+ createMockAggregatedActivity({ group: 'group2' }),
71
+ ];
72
+ const event = createMockNotificationFeedUpdatedEvent({
73
+ aggregated_activities: aggregatedActivities,
74
+ });
75
+
76
+ const result = updateNotificationFeedFromEvent(event);
77
+
78
+ expect(result.changed).toBe(true);
79
+ expect(result.data?.aggregated_activities).toBe(aggregatedActivities);
80
+ expect(result.data?.notification_status).toBeUndefined();
81
+ });
82
+
83
+ it('should update both notification_status and aggregated_activities when event has both', () => {
84
+ const notificationStatus = createMockNotificationStatus({
85
+ unread: 2,
86
+ unseen: 1,
87
+ });
88
+ const aggregatedActivities = [
89
+ createMockAggregatedActivity({ group: 'group1' }),
90
+ ];
91
+ const event = createMockNotificationFeedUpdatedEvent({
92
+ notification_status: notificationStatus,
93
+ aggregated_activities: aggregatedActivities,
94
+ });
95
+
96
+ const result = updateNotificationFeedFromEvent(event);
97
+
98
+ expect(result.changed).toBe(true);
99
+ expect(result.data?.notification_status).toBe(notificationStatus);
100
+ expect(result.data?.aggregated_activities).toBe(aggregatedActivities);
101
+ });
102
+
103
+ it('should handle notification_status with all fields', () => {
104
+ const notificationStatus = createMockNotificationStatus({
105
+ unread: 10,
106
+ unseen: 5,
107
+ last_seen_at: new Date('2023-01-01'),
108
+ read_activities: ['activity1', 'activity2', 'activity3'],
109
+ });
110
+ const event = createMockNotificationFeedUpdatedEvent({
111
+ notification_status: notificationStatus,
112
+ });
113
+
114
+ const result = updateNotificationFeedFromEvent(event);
115
+
116
+ expect(result.changed).toBe(true);
117
+ expect(result.data?.notification_status).toBe(notificationStatus);
118
+ });
119
+ });
120
+ });
@@ -1,10 +1,54 @@
1
1
  import type { Feed } from '../../../feed';
2
- import type { EventPayload } from '../../../types-internal';
2
+ import {
3
+ AggregatedActivityResponse,
4
+ NotificationFeedUpdatedEvent,
5
+ NotificationStatusResponse,
6
+ } from '../../../gen/models';
7
+ import type { EventPayload, UpdateStateResult } from '../../../types-internal';
8
+
9
+ export const updateNotificationFeedFromEvent = (
10
+ event: NotificationFeedUpdatedEvent,
11
+ ): UpdateStateResult<{
12
+ data?: {
13
+ notification_status?: NotificationStatusResponse;
14
+ aggregated_activities?: AggregatedActivityResponse[];
15
+ };
16
+ }> => {
17
+ const updates: {
18
+ notification_status?: NotificationStatusResponse;
19
+ aggregated_activities?: AggregatedActivityResponse[];
20
+ } = {};
21
+
22
+ if (event.notification_status) {
23
+ updates.notification_status = event.notification_status;
24
+ }
25
+
26
+ if (event.aggregated_activities) {
27
+ updates.aggregated_activities = event.aggregated_activities;
28
+ }
29
+
30
+ // Only return changed if we have actual updates
31
+ if (Object.keys(updates).length > 0) {
32
+ return {
33
+ changed: true,
34
+ data: updates,
35
+ };
36
+ }
37
+
38
+ return {
39
+ changed: false,
40
+ };
41
+ };
3
42
 
4
43
  export function handleNotificationFeedUpdated(
5
44
  this: Feed,
6
45
  event: EventPayload<'feeds.notification_feed.updated'>,
7
46
  ) {
8
- console.info('notification feed updated', event);
9
- // TODO: handle notification feed updates
47
+ const result = updateNotificationFeedFromEvent(event);
48
+ if (result.changed) {
49
+ this.state.partialNext({
50
+ notification_status: result.data?.notification_status,
51
+ aggregated_activities: result.data?.aggregated_activities,
52
+ });
53
+ }
10
54
  }