@stream-io/feeds-client 0.1.11 → 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 (87) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/index-react-bindings.browser.cjs +537 -331
  3. package/dist/index-react-bindings.browser.cjs.map +1 -1
  4. package/dist/index-react-bindings.browser.js +537 -331
  5. package/dist/index-react-bindings.browser.js.map +1 -1
  6. package/dist/index-react-bindings.node.cjs +537 -331
  7. package/dist/index-react-bindings.node.cjs.map +1 -1
  8. package/dist/index-react-bindings.node.js +537 -331
  9. package/dist/index-react-bindings.node.js.map +1 -1
  10. package/dist/index.browser.cjs +536 -329
  11. package/dist/index.browser.cjs.map +1 -1
  12. package/dist/index.browser.js +536 -330
  13. package/dist/index.browser.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.node.cjs +536 -329
  16. package/dist/index.node.cjs.map +1 -1
  17. package/dist/index.node.js +536 -330
  18. package/dist/index.node.js.map +1 -1
  19. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +12 -3
  20. package/dist/src/feed/event-handlers/activity/handle-activity-pinned.d.ts +3 -0
  21. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +10 -6
  22. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +10 -6
  23. package/dist/src/feed/event-handlers/activity/handle-activity-unpinned.d.ts +3 -0
  24. package/dist/src/feed/event-handlers/activity/handle-activity-updated.d.ts +7 -3
  25. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +10 -6
  26. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +10 -6
  27. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +10 -6
  28. package/dist/src/feed/event-handlers/index.d.ts +1 -0
  29. package/dist/src/feed/event-handlers/watch/handle-watch-started.d.ts +2 -0
  30. package/dist/src/feed/event-handlers/watch/handle-watch-stopped.d.ts +2 -0
  31. package/dist/src/feed/event-handlers/watch/index.d.ts +2 -0
  32. package/dist/src/feed/feed.d.ts +4 -12
  33. package/dist/src/feeds-client/event-handlers/index.d.ts +1 -0
  34. package/dist/src/feeds-client/event-handlers/user/handle-user-updated.d.ts +3 -0
  35. package/dist/src/{feeds-client.d.ts → feeds-client/feeds-client.d.ts} +16 -16
  36. package/dist/src/feeds-client/index.d.ts +2 -0
  37. package/dist/src/gen/feeds/FeedsApi.d.ts +27 -23
  38. package/dist/src/gen/models/index.d.ts +198 -23
  39. package/dist/src/test-utils/response-generators.d.ts +46 -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/index.ts +1 -1
  44. package/package.json +2 -2
  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-pinned.test.ts +60 -0
  49. package/src/feed/event-handlers/activity/handle-activity-pinned.ts +30 -0
  50. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +157 -0
  51. package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +82 -40
  52. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +200 -0
  53. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +89 -51
  54. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +94 -0
  55. package/src/feed/event-handlers/activity/handle-activity-unpinned.ts +30 -0
  56. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +115 -0
  57. package/src/feed/event-handlers/activity/handle-activity-updated.ts +73 -35
  58. package/src/feed/event-handlers/bookmark/bookmark-utils.test.ts +121 -109
  59. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +178 -0
  60. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +82 -39
  61. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +188 -0
  62. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +86 -48
  63. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +196 -0
  64. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +83 -44
  65. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +16 -12
  66. package/src/feed/event-handlers/follow/handle-follow-created.ts +4 -7
  67. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +19 -15
  68. package/src/feed/event-handlers/follow/handle-follow-deleted.ts +6 -6
  69. package/src/feed/event-handlers/follow/handle-follow-updated.ts +7 -10
  70. package/src/feed/event-handlers/index.ts +2 -1
  71. package/src/feed/event-handlers/watch/handle-watch-started.ts +5 -0
  72. package/src/feed/event-handlers/watch/handle-watch-stopped.ts +5 -0
  73. package/src/feed/event-handlers/watch/index.ts +2 -0
  74. package/src/feed/feed.ts +15 -33
  75. package/src/feeds-client/event-handlers/index.ts +1 -0
  76. package/src/feeds-client/event-handlers/user/handle-user-updated.test.ts +53 -0
  77. package/src/feeds-client/event-handlers/user/handle-user-updated.ts +28 -0
  78. package/src/{feeds-client.ts → feeds-client/feeds-client.ts} +48 -39
  79. package/src/feeds-client/index.ts +2 -0
  80. package/src/gen/feeds/FeedsApi.ts +164 -138
  81. package/src/gen/model-decoders/decoders.ts +28 -0
  82. package/src/gen/models/index.ts +349 -29
  83. package/src/gen/moderation/ModerationApi.ts +1 -0
  84. package/src/test-utils/response-generators.ts +270 -11
  85. package/src/utils/index.ts +1 -0
  86. package/src/utils/state-update-queue.ts +1 -1
  87. 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
+ });