@stream-io/feeds-client 0.2.1 → 0.2.3

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 (92) 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/contexts/StreamSearchContext.d.ts +1 -1
  4. package/dist/@react-bindings/contexts/StreamSearchResultsContext.d.ts +1 -1
  5. package/dist/@react-bindings/hooks/feed-state-hooks/index.d.ts +4 -0
  6. package/dist/@react-bindings/hooks/feed-state-hooks/useAggregatedActivities.d.ts +11 -0
  7. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivityRead.d.ts +6 -0
  8. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivitySeen.d.ts +6 -0
  9. package/dist/@react-bindings/hooks/feed-state-hooks/useNotificationStatus.d.ts +13 -0
  10. package/dist/@react-bindings/hooks/search-state-hooks/useSearchQuery.d.ts +1 -1
  11. package/dist/@react-bindings/hooks/search-state-hooks/useSearchResult.d.ts +1 -1
  12. package/dist/@react-bindings/hooks/search-state-hooks/useSearchSources.d.ts +2 -2
  13. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +1 -1
  14. package/dist/@react-bindings/wrappers/StreamSearch.d.ts +1 -1
  15. package/dist/@react-bindings/wrappers/StreamSearchResults.d.ts +1 -1
  16. package/dist/index-react-bindings.browser.cjs +178 -35
  17. package/dist/index-react-bindings.browser.cjs.map +1 -1
  18. package/dist/index-react-bindings.browser.js +175 -36
  19. package/dist/index-react-bindings.browser.js.map +1 -1
  20. package/dist/index-react-bindings.node.cjs +178 -35
  21. package/dist/index-react-bindings.node.cjs.map +1 -1
  22. package/dist/index-react-bindings.node.js +175 -36
  23. package/dist/index-react-bindings.node.js.map +1 -1
  24. package/dist/index.browser.cjs +328 -180
  25. package/dist/index.browser.cjs.map +1 -1
  26. package/dist/index.browser.js +328 -181
  27. package/dist/index.browser.js.map +1 -1
  28. package/dist/index.d.ts +1 -5
  29. package/dist/index.node.cjs +328 -180
  30. package/dist/index.node.cjs.map +1 -1
  31. package/dist/index.node.js +328 -181
  32. package/dist/index.node.js.map +1 -1
  33. package/dist/src/common/{ActivitySearchSource.d.ts → search/ActivitySearchSource.d.ts} +3 -3
  34. package/dist/src/common/{BaseSearchSource.d.ts → search/BaseSearchSource.d.ts} +41 -35
  35. package/dist/src/common/{FeedSearchSource.d.ts → search/FeedSearchSource.d.ts} +3 -3
  36. package/dist/src/common/{SearchController.d.ts → search/SearchController.d.ts} +1 -3
  37. package/dist/src/common/{UserSearchSource.d.ts → search/UserSearchSource.d.ts} +4 -4
  38. package/dist/src/common/search/index.d.ts +6 -0
  39. package/dist/src/common/search/types.d.ts +22 -0
  40. package/dist/src/common/types.d.ts +1 -0
  41. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +5 -12
  42. package/dist/src/feed/event-handlers/activity/handle-activity-marked.d.ts +11 -0
  43. package/dist/src/feed/event-handlers/activity/index.d.ts +1 -0
  44. package/dist/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +8 -1
  45. package/dist/src/feed/feed.d.ts +2 -2
  46. package/dist/src/gen/models/index.d.ts +58 -26
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/index.ts +1 -5
  49. package/package.json +1 -1
  50. package/src/common/{ActivitySearchSource.ts → search/ActivitySearchSource.ts} +3 -3
  51. package/src/common/{BaseSearchSource.ts → search/BaseSearchSource.ts} +137 -69
  52. package/src/common/{FeedSearchSource.ts → search/FeedSearchSource.ts} +3 -3
  53. package/src/common/{SearchController.ts → search/SearchController.ts} +2 -7
  54. package/src/common/{UserSearchSource.ts → search/UserSearchSource.ts} +3 -3
  55. package/src/common/search/index.ts +6 -0
  56. package/src/common/search/types.ts +21 -0
  57. package/src/common/types.ts +2 -0
  58. package/src/feed/event-handlers/activity/activity-marked-utils.test.ts +208 -0
  59. package/src/feed/event-handlers/activity/activity-utils.test.ts +2 -2
  60. package/src/feed/event-handlers/activity/handle-activity-added.test.ts +86 -0
  61. package/src/feed/event-handlers/activity/handle-activity-deleted.test.ts +117 -0
  62. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +8 -4
  63. package/src/feed/event-handlers/activity/handle-activity-marked.ts +68 -0
  64. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +15 -15
  65. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +14 -14
  66. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +4 -3
  67. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +4 -4
  68. package/src/feed/event-handlers/activity/index.ts +2 -1
  69. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +14 -14
  70. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +14 -14
  71. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +16 -16
  72. package/src/feed/event-handlers/comment/handle-comment-added.test.ts +147 -0
  73. package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +133 -0
  74. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +24 -10
  75. package/src/feed/event-handlers/comment/handle-comment-reaction.test.ts +315 -0
  76. package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +131 -0
  77. package/src/feed/event-handlers/feed-member/handle-feed-member-added.test.ts +75 -0
  78. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.test.ts +82 -0
  79. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +19 -9
  80. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.test.ts +84 -0
  81. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +7 -7
  82. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +2 -2
  83. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +1 -1
  84. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +120 -0
  85. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +47 -3
  86. package/src/feed/feed.ts +4 -2
  87. package/src/gen/feeds/FeedsApi.ts +6 -0
  88. package/src/gen/model-decoders/decoders.ts +12 -0
  89. package/src/gen/models/index.ts +90 -34
  90. package/src/test-utils/response-generators.ts +230 -0
  91. package/dist/src/test-utils/index.d.ts +0 -1
  92. package/dist/src/test-utils/response-generators.d.ts +0 -54
@@ -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
+ });
@@ -0,0 +1,75 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleFeedMemberAdded } from './handle-feed-member-added';
5
+ import {
6
+ generateFeedResponse,
7
+ generateFeedMemberAddedEvent,
8
+ generateFeedMemberResponse,
9
+ generateOwnUser,
10
+ getHumanId,
11
+ } from '../../../test-utils/response-generators';
12
+
13
+ describe(handleFeedMemberAdded.name, () => {
14
+ let feed: Feed;
15
+ let client: FeedsClient;
16
+ let currentUserId: string;
17
+
18
+ beforeEach(() => {
19
+ client = new FeedsClient('mock-api-key');
20
+ currentUserId = getHumanId();
21
+ client.state.partialNext({
22
+ connected_user: generateOwnUser({ id: currentUserId }),
23
+ });
24
+ const feedResponse = generateFeedResponse({
25
+ id: 'main',
26
+ group_id: 'user',
27
+ created_by: { id: currentUserId },
28
+ });
29
+ feed = new Feed(
30
+ client,
31
+ feedResponse.group_id,
32
+ feedResponse.id,
33
+ feedResponse,
34
+ );
35
+ });
36
+
37
+ it('prepends to members if members array exists', () => {
38
+ const existingMember = generateFeedMemberResponse();
39
+ feed.state.partialNext({ members: [existingMember] });
40
+
41
+ const event = generateFeedMemberAddedEvent();
42
+
43
+ handleFeedMemberAdded.call(feed, event);
44
+
45
+ const stateAfter = feed.currentState;
46
+ expect(stateAfter.members).toHaveLength(2);
47
+ expect(stateAfter.members?.[0]).toBe(event.member);
48
+ });
49
+
50
+ it('does not create members array if it does not exist', () => {
51
+ const event = generateFeedMemberAddedEvent();
52
+
53
+ const stateBefore = feed.currentState;
54
+ expect(stateBefore.members).toBeUndefined();
55
+
56
+ handleFeedMemberAdded.call(feed, event);
57
+
58
+ const stateAfter = feed.currentState;
59
+ expect(stateAfter).toBe(stateBefore);
60
+ });
61
+
62
+ it('sets own_membership when the added member is the connected user', () => {
63
+ const event = generateFeedMemberAddedEvent({
64
+ member: { user: { id: currentUserId } },
65
+ });
66
+
67
+ const stateBefore = feed.currentState;
68
+ expect(stateBefore.own_membership).toBeUndefined();
69
+
70
+ handleFeedMemberAdded.call(feed, event);
71
+
72
+ const stateAfter = feed.currentState;
73
+ expect(stateAfter.own_membership).toBe(event.member);
74
+ });
75
+ });
@@ -0,0 +1,82 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleFeedMemberRemoved } from './handle-feed-member-removed';
5
+ import {
6
+ generateFeedResponse,
7
+ generateFeedMemberRemovedEvent,
8
+ generateFeedMemberResponse,
9
+ generateOwnUser,
10
+ getHumanId,
11
+ } from '../../../test-utils/response-generators';
12
+
13
+ describe(handleFeedMemberRemoved.name, () => {
14
+ let feed: Feed;
15
+ let client: FeedsClient;
16
+ let currentUserId: string;
17
+
18
+ beforeEach(() => {
19
+ client = new FeedsClient('mock-api-key');
20
+ currentUserId = getHumanId();
21
+ client.state.partialNext({
22
+ connected_user: generateOwnUser({ id: currentUserId }),
23
+ });
24
+ const feedResponse = generateFeedResponse({
25
+ id: 'main',
26
+ group_id: 'user',
27
+ created_by: { id: currentUserId },
28
+ });
29
+ feed = new Feed(
30
+ client,
31
+ feedResponse.group_id,
32
+ feedResponse.id,
33
+ feedResponse,
34
+ );
35
+ });
36
+
37
+ it('removes from members if present', () => {
38
+ const memberToRemove = generateFeedMemberResponse();
39
+ const memberToKeep = generateFeedMemberResponse();
40
+ feed.state.partialNext({ members: [memberToRemove, memberToKeep] });
41
+
42
+ const event = generateFeedMemberRemovedEvent({
43
+ member_id: memberToRemove.user.id,
44
+ });
45
+
46
+ handleFeedMemberRemoved.call(feed, event);
47
+
48
+ const stateAfter = feed.currentState;
49
+ expect(stateAfter.members).toHaveLength(1);
50
+ expect(stateAfter.members?.[0]).toBe(memberToKeep);
51
+ });
52
+
53
+ it('does not change state when members array is undefined', () => {
54
+ const event = generateFeedMemberRemovedEvent();
55
+
56
+ const stateBefore = feed.currentState;
57
+ expect(stateBefore.members).toBeUndefined();
58
+
59
+ handleFeedMemberRemoved.call(feed, event);
60
+
61
+ const stateAfter = feed.currentState;
62
+ expect(stateAfter).toBe(stateBefore);
63
+ });
64
+
65
+ it('deletes own_membership when the removed member is the connected user', () => {
66
+ const ownMember = generateFeedMemberResponse({
67
+ user: { id: currentUserId },
68
+ });
69
+ feed.state.partialNext({ own_membership: ownMember, members: [] });
70
+
71
+ const event = generateFeedMemberRemovedEvent({ member_id: currentUserId });
72
+
73
+ const stateBefore = feed.currentState;
74
+ expect(stateBefore.own_membership).toBe(ownMember);
75
+
76
+ handleFeedMemberRemoved.call(feed, event);
77
+
78
+ const stateAfter = feed.currentState;
79
+ expect(stateAfter.own_membership).toBeUndefined();
80
+ expect(stateAfter.members).toBe(stateBefore.members);
81
+ });
82
+ });
@@ -1,4 +1,4 @@
1
- import { Feed } from '../../../feed';
1
+ import { Feed, FeedState } from '../../../feed';
2
2
  import { EventPayload } from '../../../types-internal';
3
3
 
4
4
  export function handleFeedMemberRemoved(
@@ -8,17 +8,27 @@ export function handleFeedMemberRemoved(
8
8
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
9
9
 
10
10
  this.state.next((currentState) => {
11
- const newState = {
12
- ...currentState,
13
- members: currentState.members?.filter(
14
- (member) => member.user.id !== event.user?.id,
15
- ),
16
- };
11
+ let newState: FeedState | undefined;
17
12
 
18
- if (connectedUser?.id === event.member_id) {
13
+ if (typeof currentState.members !== 'undefined') {
14
+ const filtered = currentState.members.filter(
15
+ (member) => member.user.id !== event.member_id,
16
+ );
17
+
18
+ if (filtered.length !== currentState.members.length) {
19
+ newState ??= { ...currentState };
20
+ newState.members = filtered;
21
+ }
22
+ }
23
+
24
+ if (
25
+ connectedUser?.id === event.member_id &&
26
+ typeof currentState.own_membership !== 'undefined'
27
+ ) {
28
+ newState ??= { ...currentState };
19
29
  delete newState.own_membership;
20
30
  }
21
31
 
22
- return newState;
32
+ return newState ?? currentState;
23
33
  });
24
34
  }