@stream-io/feeds-client 0.2.0 → 0.2.1

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 (58) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/index-react-bindings.browser.cjs +365 -207
  3. package/dist/index-react-bindings.browser.cjs.map +1 -1
  4. package/dist/index-react-bindings.browser.js +365 -207
  5. package/dist/index-react-bindings.browser.js.map +1 -1
  6. package/dist/index-react-bindings.node.cjs +365 -207
  7. package/dist/index-react-bindings.node.cjs.map +1 -1
  8. package/dist/index-react-bindings.node.js +365 -207
  9. package/dist/index-react-bindings.node.js.map +1 -1
  10. package/dist/index.browser.cjs +366 -207
  11. package/dist/index.browser.cjs.map +1 -1
  12. package/dist/index.browser.js +366 -208
  13. package/dist/index.browser.js.map +1 -1
  14. package/dist/index.node.cjs +366 -207
  15. package/dist/index.node.cjs.map +1 -1
  16. package/dist/index.node.js +366 -208
  17. package/dist/index.node.js.map +1 -1
  18. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +12 -3
  19. package/dist/src/feed/event-handlers/activity/handle-activity-pinned.d.ts +3 -0
  20. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +10 -6
  21. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +10 -6
  22. package/dist/src/feed/event-handlers/activity/handle-activity-unpinned.d.ts +3 -0
  23. package/dist/src/feed/event-handlers/activity/handle-activity-updated.d.ts +7 -3
  24. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +10 -6
  25. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +10 -6
  26. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +10 -6
  27. package/dist/src/gen/models/index.d.ts +36 -1
  28. package/dist/src/test-utils/response-generators.d.ts +46 -1
  29. package/dist/src/utils/index.d.ts +1 -0
  30. package/dist/src/utils/update-entity-in-array.d.ts +27 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/src/feed/event-handlers/activity/activity-reaction-utils.test.ts +108 -96
  34. package/src/feed/event-handlers/activity/activity-utils.test.ts +84 -122
  35. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +43 -10
  36. package/src/feed/event-handlers/activity/handle-activity-pinned.test.ts +60 -0
  37. package/src/feed/event-handlers/activity/handle-activity-pinned.ts +30 -0
  38. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +157 -0
  39. package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +82 -40
  40. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +200 -0
  41. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +89 -51
  42. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +94 -0
  43. package/src/feed/event-handlers/activity/handle-activity-unpinned.ts +30 -0
  44. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +115 -0
  45. package/src/feed/event-handlers/activity/handle-activity-updated.ts +73 -35
  46. package/src/feed/event-handlers/bookmark/bookmark-utils.test.ts +121 -109
  47. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +178 -0
  48. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +82 -39
  49. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +188 -0
  50. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +86 -48
  51. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +196 -0
  52. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +83 -44
  53. package/src/gen/model-decoders/decoders.ts +13 -0
  54. package/src/gen/models/index.ts +73 -2
  55. package/src/gen/moderation/ModerationApi.ts +1 -0
  56. package/src/test-utils/response-generators.ts +260 -0
  57. package/src/utils/index.ts +1 -0
  58. package/src/utils/update-entity-in-array.ts +51 -0
@@ -1,63 +1,106 @@
1
1
  import type { Feed } from '../../../feed';
2
- import type { ActivityResponse, BookmarkAddedEvent } from '../../../gen/models';
3
- import type { EventPayload, UpdateStateResult } from '../../../types-internal';
2
+ import type {
3
+ ActivityPinResponse,
4
+ ActivityResponse,
5
+ BookmarkAddedEvent,
6
+ } from '../../../gen/models';
7
+ import type { EventPayload } from '../../../types-internal';
8
+ import { updateEntityInArray } from '../../../utils';
4
9
 
5
- import { updateActivityInState } from '../activity';
10
+ const sharedUpdateActivity = ({
11
+ currentActivity,
12
+ event,
13
+ eventBelongsToCurrentUser,
14
+ }: {
15
+ currentActivity: ActivityResponse;
16
+ event: BookmarkAddedEvent;
17
+ eventBelongsToCurrentUser: boolean;
18
+ }): ActivityResponse => {
19
+ let newOwnBookmarks = currentActivity.own_bookmarks;
6
20
 
7
- export const addBookmarkToActivity = (
8
- event: BookmarkAddedEvent,
9
- activity: ActivityResponse,
10
- isCurrentUser: boolean,
11
- ): UpdateStateResult<ActivityResponse> => {
12
- // Update own_bookmarks if the bookmark is from the current user
13
- const ownBookmarks = [...(activity.own_bookmarks || [])];
14
- if (isCurrentUser) {
15
- ownBookmarks.push(event.bookmark);
21
+ if (eventBelongsToCurrentUser) {
22
+ newOwnBookmarks = [...newOwnBookmarks, event.bookmark];
16
23
  }
17
24
 
18
25
  return {
19
- ...activity,
20
- own_bookmarks: ownBookmarks,
21
- changed: true,
26
+ ...event.bookmark.activity,
27
+ own_bookmarks: newOwnBookmarks,
28
+ own_reactions: currentActivity.own_reactions,
22
29
  };
23
30
  };
24
31
 
25
32
  export const addBookmarkToActivities = (
26
33
  event: BookmarkAddedEvent,
27
34
  activities: ActivityResponse[] | undefined,
28
- isCurrentUser: boolean,
29
- ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
30
- if (!activities) {
31
- return { changed: false, activities: [] };
32
- }
35
+ eventBelongsToCurrentUser: boolean,
36
+ ) =>
37
+ updateEntityInArray({
38
+ entities: activities,
39
+ matcher: (activity) => activity.id === event.bookmark.activity.id,
40
+ updater: (matchedActivity) =>
41
+ sharedUpdateActivity({
42
+ currentActivity: matchedActivity,
43
+ event,
44
+ eventBelongsToCurrentUser,
45
+ }),
46
+ });
33
47
 
34
- const activityIndex = activities.findIndex(
35
- (a) => a.id === event.bookmark.activity.id,
36
- );
37
- if (activityIndex === -1) {
38
- return { changed: false, activities };
39
- }
48
+ export const addBookmarkToPinnedActivities = (
49
+ event: BookmarkAddedEvent,
50
+ pinnedActivities: ActivityPinResponse[] | undefined,
51
+ eventBelongsToCurrentUser: boolean,
52
+ ) =>
53
+ updateEntityInArray({
54
+ entities: pinnedActivities,
55
+ matcher: (pinnedActivity) =>
56
+ pinnedActivity.activity.id === event.bookmark.activity.id,
57
+ updater: (matchedPinnedActivity) => {
58
+ const newActivity = sharedUpdateActivity({
59
+ currentActivity: matchedPinnedActivity.activity,
60
+ event,
61
+ eventBelongsToCurrentUser,
62
+ });
40
63
 
41
- const activity = activities[activityIndex];
42
- const updatedActivity = addBookmarkToActivity(event, activity, isCurrentUser);
43
- return updateActivityInState(updatedActivity, activities, true);
44
- };
64
+ if (newActivity === matchedPinnedActivity.activity) {
65
+ return matchedPinnedActivity;
66
+ }
67
+
68
+ return {
69
+ ...matchedPinnedActivity,
70
+ activity: newActivity,
71
+ };
72
+ },
73
+ });
45
74
 
46
75
  export function handleBookmarkAdded(
47
76
  this: Feed,
48
77
  event: EventPayload<'feeds.bookmark.added'>,
49
78
  ) {
50
- const currentActivities = this.currentState.activities;
79
+ const {
80
+ activities: currentActivities,
81
+ pinned_activities: currentPinnedActivities,
82
+ } = this.currentState;
51
83
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
52
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
84
+ const eventBelongsToCurrentUser =
85
+ event.bookmark.user.id === connectedUser?.id;
53
86
 
54
- const result = addBookmarkToActivities(
55
- event,
56
- currentActivities,
57
- isCurrentUser,
58
- );
87
+ const [result1, result2] = [
88
+ addBookmarkToActivities(
89
+ event,
90
+ currentActivities,
91
+ eventBelongsToCurrentUser,
92
+ ),
93
+ addBookmarkToPinnedActivities(
94
+ event,
95
+ currentPinnedActivities,
96
+ eventBelongsToCurrentUser,
97
+ ),
98
+ ];
59
99
 
60
- if (result.changed) {
61
- this.state.partialNext({ activities: result.activities });
100
+ if (result1.changed || result2.changed) {
101
+ this.state.partialNext({
102
+ activities: result1.entities,
103
+ pinned_activities: result2.entities,
104
+ });
62
105
  }
63
106
  }
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleBookmarkDeleted } from './handle-bookmark-deleted';
5
+ import {
6
+ generateActivityPinResponse,
7
+ generateActivityResponse,
8
+ generateFeedResponse,
9
+ generateOwnUser,
10
+ getHumanId,
11
+ generateFeedReactionResponse,
12
+ generateBookmarkDeletedEvent,
13
+ generateBookmarkResponse,
14
+ } from '../../../test-utils/response-generators';
15
+
16
+ describe(handleBookmarkDeleted.name, () => {
17
+ let feed: Feed;
18
+ let client: FeedsClient;
19
+ let currentUserId: 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
+ });
39
+
40
+ it('removes a bookmark for the current user and updates activities', () => {
41
+ const event = generateBookmarkDeletedEvent({
42
+ bookmark: {
43
+ activity: {
44
+ own_reactions: [],
45
+ bookmark_count: 0,
46
+ },
47
+ user: { id: currentUserId },
48
+ },
49
+ });
50
+ const activity = generateActivityResponse({
51
+ id: event.bookmark.activity.id,
52
+ bookmark_count: 1,
53
+ own_bookmarks: [
54
+ generateBookmarkResponse({
55
+ activity: { id: event.bookmark.activity.id },
56
+ user: { id: currentUserId },
57
+ }),
58
+ ],
59
+ own_reactions: [generateFeedReactionResponse()],
60
+ });
61
+ const activityPin = generateActivityPinResponse({
62
+ activity: { ...activity },
63
+ });
64
+ feed.state.partialNext({
65
+ activities: [activity],
66
+ pinned_activities: [activityPin],
67
+ });
68
+
69
+ const stateBefore = feed.currentState;
70
+ expect(stateBefore.activities![0].own_bookmarks).toHaveLength(1);
71
+ expect(
72
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
73
+ ).toHaveLength(1);
74
+ expect(stateBefore.activities![0].bookmark_count).toEqual(1);
75
+ expect(stateBefore.pinned_activities![0].activity.bookmark_count).toEqual(
76
+ 1,
77
+ );
78
+
79
+ handleBookmarkDeleted.call(feed, event);
80
+
81
+ const stateAfter = feed.currentState;
82
+ expect(stateAfter.activities![0].own_bookmarks).toHaveLength(0);
83
+ expect(
84
+ stateAfter.pinned_activities![0].activity.own_bookmarks,
85
+ ).toHaveLength(0);
86
+ expect(stateAfter.activities![0].own_reactions).toEqual(
87
+ stateBefore.activities![0].own_reactions,
88
+ );
89
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toEqual(
90
+ stateBefore.pinned_activities![0].activity.own_reactions,
91
+ );
92
+ expect(stateAfter.activities![0].bookmark_count).toEqual(0);
93
+ expect(stateAfter.pinned_activities![0].activity.bookmark_count).toEqual(0);
94
+ });
95
+
96
+ it('does not remove from own_bookmarks if bookmark is from another user but still updates activity', () => {
97
+ const event = generateBookmarkDeletedEvent({
98
+ bookmark: {
99
+ activity: {
100
+ own_reactions: [],
101
+ bookmark_count: 0,
102
+ },
103
+ user: { id: 'other-user-id' },
104
+ },
105
+ });
106
+ const activity = generateActivityResponse({
107
+ id: event.bookmark.activity.id,
108
+ bookmark_count: 1,
109
+ own_bookmarks: [
110
+ generateBookmarkResponse({
111
+ activity: { id: event.bookmark.activity.id },
112
+ user: { id: currentUserId },
113
+ }),
114
+ ],
115
+ own_reactions: [generateFeedReactionResponse()],
116
+ });
117
+ const activityPin = generateActivityPinResponse({
118
+ activity: { ...activity },
119
+ });
120
+ feed.state.partialNext({
121
+ activities: [activity],
122
+ pinned_activities: [activityPin],
123
+ });
124
+
125
+ const stateBefore = feed.currentState;
126
+ expect(stateBefore.activities![0].own_bookmarks).toHaveLength(1);
127
+ expect(
128
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
129
+ ).toHaveLength(1);
130
+ expect(stateBefore.activities![0].bookmark_count).toEqual(1);
131
+ expect(stateBefore.pinned_activities![0].activity.bookmark_count).toEqual(
132
+ 1,
133
+ );
134
+
135
+ handleBookmarkDeleted.call(feed, event);
136
+
137
+ const stateAfter = feed.currentState;
138
+ expect(stateAfter.activities![0].own_bookmarks).toEqual(
139
+ stateBefore.activities![0].own_bookmarks,
140
+ );
141
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toEqual(
142
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
143
+ );
144
+ expect(stateAfter.activities![0].own_reactions).toEqual(
145
+ stateBefore.activities![0].own_reactions,
146
+ );
147
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toEqual(
148
+ stateBefore.pinned_activities![0].activity.own_reactions,
149
+ );
150
+ expect(stateAfter.activities![0].bookmark_count).toEqual(0);
151
+ expect(stateAfter.pinned_activities![0].activity.bookmark_count).toEqual(0);
152
+ });
153
+
154
+ it('does nothing if activity is not found', () => {
155
+ const event = generateBookmarkDeletedEvent({
156
+ bookmark: {
157
+ activity: {
158
+ own_reactions: [],
159
+ bookmark_count: 0,
160
+ },
161
+ user: { id: currentUserId },
162
+ },
163
+ });
164
+ const activity = generateActivityResponse({
165
+ id: 'another-activity-id',
166
+ bookmark_count: 1,
167
+ own_bookmarks: [
168
+ generateBookmarkResponse({
169
+ activity: { id: 'another-activity-id', },
170
+ user: { id: currentUserId },
171
+ }),
172
+ ],
173
+ own_reactions: [generateFeedReactionResponse()],
174
+ });
175
+ const activityPin = generateActivityPinResponse({
176
+ activity: { ...activity },
177
+ });
178
+ feed.state.partialNext({
179
+ activities: [activity],
180
+ pinned_activities: [activityPin],
181
+ });
182
+
183
+ const stateBefore = feed.currentState;
184
+ handleBookmarkDeleted.call(feed, event);
185
+ const stateAfter = feed.currentState;
186
+ expect(stateAfter).toBe(stateBefore);
187
+ });
188
+ });
@@ -1,12 +1,12 @@
1
1
  import type { Feed } from '../../../feed';
2
2
  import type {
3
+ ActivityPinResponse,
3
4
  ActivityResponse,
4
5
  BookmarkDeletedEvent,
5
6
  BookmarkResponse,
6
7
  } from '../../../gen/models';
7
- import type { EventPayload, UpdateStateResult } from '../../../types-internal';
8
-
9
- import { updateActivityInState } from '../activity';
8
+ import type { EventPayload } from '../../../types-internal';
9
+ import { updateEntityInArray } from '../../../utils';
10
10
 
11
11
  // Helper function to check if two bookmarks are the same
12
12
  // A bookmark is identified by activity_id + folder_id + user_id
@@ -21,64 +21,102 @@ export const isSameBookmark = (
21
21
  );
22
22
  };
23
23
 
24
- export const removeBookmarkFromActivities = (
25
- event: BookmarkDeletedEvent,
26
- activities: ActivityResponse[] | undefined,
27
- isCurrentUser: boolean,
28
- ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
29
- if (!activities) {
30
- return { changed: false, activities: [] };
31
- }
24
+ const sharedUpdateActivity = ({
25
+ currentActivity,
26
+ event,
27
+ eventBelongsToCurrentUser,
28
+ }: {
29
+ currentActivity: ActivityResponse;
30
+ event: BookmarkDeletedEvent;
31
+ eventBelongsToCurrentUser: boolean;
32
+ }): ActivityResponse => {
33
+ let newOwnBookmarks = currentActivity.own_bookmarks;
32
34
 
33
- const activityIndex = activities.findIndex(
34
- (a) => a.id === event.bookmark.activity.id,
35
- );
36
- if (activityIndex === -1) {
37
- return { changed: false, activities };
35
+ if (eventBelongsToCurrentUser) {
36
+ newOwnBookmarks = currentActivity.own_bookmarks.filter(
37
+ (bookmark) => !isSameBookmark(bookmark, event.bookmark),
38
+ );
38
39
  }
39
40
 
40
- const activity = activities[activityIndex];
41
- const updatedActivity = removeBookmarkFromActivity(
42
- event,
43
- activity,
44
- isCurrentUser,
45
- );
46
- return updateActivityInState(updatedActivity, activities, true);
41
+ return {
42
+ ...event.bookmark.activity,
43
+ own_bookmarks: newOwnBookmarks,
44
+ own_reactions: currentActivity.own_reactions,
45
+ };
47
46
  };
48
47
 
49
- export const removeBookmarkFromActivity = (
48
+ export const removeBookmarkFromActivities = (
50
49
  event: BookmarkDeletedEvent,
51
- activity: ActivityResponse,
52
- isCurrentUser: boolean,
53
- ): UpdateStateResult<ActivityResponse> => {
54
- // Update own_bookmarks if the bookmark is from the current user
55
- const ownBookmarks = isCurrentUser
56
- ? (activity.own_bookmarks || []).filter(
57
- (bookmark) => !isSameBookmark(bookmark, event.bookmark),
58
- )
59
- : activity.own_bookmarks;
50
+ activities: ActivityResponse[] | undefined,
51
+ eventBelongsToCurrentUser: boolean,
52
+ ) =>
53
+ updateEntityInArray({
54
+ entities: activities,
55
+ matcher: (activity) => activity.id === event.bookmark.activity.id,
56
+ updater: (matchedActivity) =>
57
+ sharedUpdateActivity({
58
+ currentActivity: matchedActivity,
59
+ event,
60
+ eventBelongsToCurrentUser,
61
+ }),
62
+ });
60
63
 
61
- return {
62
- ...activity,
63
- own_bookmarks: ownBookmarks,
64
- changed: true,
65
- };
66
- };
64
+ export const removeBookmarkFromPinnedActivities = (
65
+ event: BookmarkDeletedEvent,
66
+ pinnedActivities: ActivityPinResponse[] | undefined,
67
+ eventBelongsToCurrentUser: boolean,
68
+ ) =>
69
+ updateEntityInArray({
70
+ entities: pinnedActivities,
71
+ matcher: (pinnedActivity) =>
72
+ pinnedActivity.activity.id === event.bookmark.activity.id,
73
+ updater: (matchedPinnedActivity) => {
74
+ const newActivity = sharedUpdateActivity({
75
+ currentActivity: matchedPinnedActivity.activity,
76
+ event,
77
+ eventBelongsToCurrentUser,
78
+ });
79
+
80
+ if (newActivity === matchedPinnedActivity.activity) {
81
+ return matchedPinnedActivity;
82
+ }
83
+
84
+ return {
85
+ ...matchedPinnedActivity,
86
+ activity: newActivity,
87
+ };
88
+ },
89
+ });
67
90
 
68
91
  export function handleBookmarkDeleted(
69
92
  this: Feed,
70
93
  event: EventPayload<'feeds.bookmark.deleted'>,
71
94
  ) {
72
- const currentActivities = this.currentState.activities;
95
+ const {
96
+ activities: currentActivities,
97
+ pinned_activities: currentPinnedActivities,
98
+ } = this.currentState;
73
99
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
74
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
100
+ const eventBelongsToCurrentUser =
101
+ event.bookmark.user.id === connectedUser?.id;
75
102
 
76
- const result = removeBookmarkFromActivities(
77
- event,
78
- currentActivities,
79
- isCurrentUser,
80
- );
81
- if (result.changed) {
82
- this.state.partialNext({ activities: result.activities });
103
+ const [result1, result2] = [
104
+ removeBookmarkFromActivities(
105
+ event,
106
+ currentActivities,
107
+ eventBelongsToCurrentUser,
108
+ ),
109
+ removeBookmarkFromPinnedActivities(
110
+ event,
111
+ currentPinnedActivities,
112
+ eventBelongsToCurrentUser,
113
+ ),
114
+ ];
115
+
116
+ if (result1.changed || result2.changed) {
117
+ this.state.partialNext({
118
+ activities: result1.entities,
119
+ pinned_activities: result2.entities,
120
+ });
83
121
  }
84
122
  }
@@ -0,0 +1,196 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleBookmarkUpdated } from './handle-bookmark-updated';
5
+ import {
6
+ generateActivityPinResponse,
7
+ generateActivityResponse,
8
+ generateFeedResponse,
9
+ generateOwnUser,
10
+ getHumanId,
11
+ generateFeedReactionResponse,
12
+ generateBookmarkUpdatedEvent,
13
+ generateBookmarkResponse,
14
+ } from '../../../test-utils/response-generators';
15
+
16
+ describe(handleBookmarkUpdated.name, () => {
17
+ let feed: Feed;
18
+ let client: FeedsClient;
19
+ let currentUserId: 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
+ });
39
+
40
+ it('updates a bookmark for the current user and updates activities', () => {
41
+ const event = generateBookmarkUpdatedEvent({
42
+ bookmark: {
43
+ activity: {
44
+ own_reactions: [],
45
+ bookmark_count: 1,
46
+ },
47
+ user: { id: currentUserId },
48
+ updated_at: new Date('2025-08-06T12:00:00Z'),
49
+ },
50
+ });
51
+ const activity = generateActivityResponse({
52
+ id: event.bookmark.activity.id,
53
+ bookmark_count: 1,
54
+ own_bookmarks: [
55
+ generateBookmarkResponse({
56
+ activity: { id: event.bookmark.activity.id },
57
+ user: { id: currentUserId },
58
+ updated_at: new Date('2025-08-05T12:00:00Z'),
59
+ }),
60
+ ],
61
+ own_reactions: [generateFeedReactionResponse()],
62
+ });
63
+ const activityPin = generateActivityPinResponse({
64
+ activity: { ...activity },
65
+ });
66
+ feed.state.partialNext({
67
+ activities: [activity],
68
+ pinned_activities: [activityPin],
69
+ });
70
+
71
+ const stateBefore = feed.currentState;
72
+ expect(stateBefore.activities![0].own_bookmarks).toHaveLength(1);
73
+ expect(
74
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
75
+ ).toHaveLength(1);
76
+ expect(stateBefore.activities![0].own_bookmarks[0].updated_at).not.toEqual(
77
+ event.bookmark.updated_at,
78
+ );
79
+ expect(
80
+ stateBefore.pinned_activities![0].activity.own_bookmarks[0].updated_at,
81
+ ).not.toEqual(event.bookmark.updated_at);
82
+
83
+ handleBookmarkUpdated.call(feed, event);
84
+
85
+ const stateAfter = feed.currentState;
86
+ expect(stateAfter.activities![0].own_bookmarks).toHaveLength(1);
87
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toHaveLength(1);
88
+ expect(stateAfter.activities![0].own_bookmarks[0]).toEqual(event.bookmark);
89
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks[0]).toEqual(
90
+ event.bookmark,
91
+ );
92
+ expect(stateAfter.activities![0].own_reactions).toEqual(
93
+ stateBefore.activities![0].own_reactions,
94
+ );
95
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toEqual(
96
+ stateBefore.pinned_activities![0].activity.own_reactions,
97
+ );
98
+ expect(stateAfter.activities![0].bookmark_count).toEqual(1);
99
+ expect(stateAfter.pinned_activities![0].activity.bookmark_count).toEqual(1);
100
+ });
101
+
102
+ it('does not update own_bookmarks if bookmark is from another user but still updates activity', () => {
103
+ const event = generateBookmarkUpdatedEvent({
104
+ bookmark: {
105
+ activity: {
106
+ own_reactions: [],
107
+ bookmark_count: 2,
108
+ },
109
+ user: { id: 'other-user-id' },
110
+ updated_at: new Date('2025-08-06T12:00:00Z'),
111
+ },
112
+ });
113
+ const activity = generateActivityResponse({
114
+ id: event.bookmark.activity.id,
115
+ bookmark_count: 1,
116
+ own_bookmarks: [
117
+ generateBookmarkResponse({
118
+ activity: { id: event.bookmark.activity.id },
119
+ user: { id: currentUserId },
120
+ updated_at: new Date('2025-08-05T12:00:00Z'),
121
+ }),
122
+ ],
123
+ own_reactions: [generateFeedReactionResponse()],
124
+ });
125
+ const activityPin = generateActivityPinResponse({
126
+ activity: { ...activity },
127
+ });
128
+ feed.state.partialNext({
129
+ activities: [activity],
130
+ pinned_activities: [activityPin],
131
+ });
132
+
133
+ const stateBefore = feed.currentState;
134
+ expect(stateBefore.activities![0].own_bookmarks).toHaveLength(1);
135
+ expect(
136
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
137
+ ).toHaveLength(1);
138
+ expect(stateBefore.activities![0].bookmark_count).toEqual(1);
139
+ expect(stateBefore.pinned_activities![0].activity.bookmark_count).toEqual(
140
+ 1,
141
+ );
142
+
143
+ handleBookmarkUpdated.call(feed, event);
144
+
145
+ const stateAfter = feed.currentState;
146
+ expect(stateAfter.activities![0].own_bookmarks).toEqual(
147
+ stateBefore.activities![0].own_bookmarks,
148
+ );
149
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toEqual(
150
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
151
+ );
152
+ expect(stateAfter.activities![0].own_reactions).toEqual(
153
+ stateBefore.activities![0].own_reactions,
154
+ );
155
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toEqual(
156
+ stateBefore.pinned_activities![0].activity.own_reactions,
157
+ );
158
+ expect(stateAfter.activities![0].bookmark_count).toEqual(2);
159
+ expect(stateAfter.pinned_activities![0].activity.bookmark_count).toEqual(2);
160
+ });
161
+
162
+ it('does nothing if activity is not found', () => {
163
+ const event = generateBookmarkUpdatedEvent({
164
+ bookmark: {
165
+ activity: {
166
+ own_reactions: [],
167
+ bookmark_count: 1,
168
+ },
169
+ user: { id: currentUserId },
170
+ },
171
+ });
172
+ const activity = generateActivityResponse({
173
+ id: 'unrelated-activity-id',
174
+ bookmark_count: 1,
175
+ own_bookmarks: [
176
+ generateBookmarkResponse({
177
+ activity: { id: 'unrelated-activity-id' },
178
+ user: { id: currentUserId },
179
+ }),
180
+ ],
181
+ own_reactions: [generateFeedReactionResponse()],
182
+ });
183
+ const activityPin = generateActivityPinResponse({
184
+ activity: { ...activity },
185
+ });
186
+ feed.state.partialNext({
187
+ activities: [activity],
188
+ pinned_activities: [activityPin],
189
+ });
190
+
191
+ const stateBefore = feed.currentState;
192
+ handleBookmarkUpdated.call(feed, event);
193
+ const stateAfter = feed.currentState;
194
+ expect(stateAfter).toBe(stateBefore);
195
+ });
196
+ });