@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
@@ -1,55 +1,19 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { ActivityResponse, FeedsReactionResponse } from '../../../gen/models';
2
+ import { ActivityResponse } from '../../../gen/models';
3
3
  import {
4
4
  addActivitiesToState,
5
5
  updateActivityInState,
6
6
  removeActivityFromState,
7
7
  } from './';
8
-
9
- const createMockActivity = (id: string, text?: string): ActivityResponse =>
10
- ({
11
- id,
12
- type: 'test',
13
- created_at: new Date(),
14
- updated_at: new Date(),
15
- visibility: 'public',
16
- bookmark_count: 0,
17
- comment_count: 0,
18
- share_count: 0,
19
- attachments: [],
20
- comments: [],
21
- feeds: [],
22
- filter_tags: [],
23
- interest_tags: [],
24
- latest_reactions: [],
25
- mentioned_users: [],
26
- own_bookmarks: [],
27
- own_reactions: [],
28
- custom: {},
29
- reaction_groups: {},
30
- search_data: {},
31
- text: text,
32
- popularity: 0,
33
- score: 0,
34
- reaction_count: 0,
35
- user: {
36
- id: 'user1',
37
- created_at: new Date(),
38
- updated_at: new Date(),
39
- banned: false,
40
- language: 'en',
41
- online: false,
42
- role: 'user',
43
- blocked_user_ids: [],
44
- teams: [],
45
- custom: {},
46
- },
47
- }) as ActivityResponse;
8
+ import {
9
+ generateActivityResponse,
10
+ generateFeedReactionResponse,
11
+ } from '../../../test-utils';
48
12
 
49
13
  describe('activity-utils', () => {
50
14
  describe('addActivitiesToState', () => {
51
- const activity1 = createMockActivity('activity1');
52
- const activity2 = createMockActivity('activity2');
15
+ const activity1 = generateActivityResponse({ id: 'activity1' });
16
+ const activity2 = generateActivityResponse({ id: 'activity2' });
53
17
 
54
18
  it('should add activities to empty state', () => {
55
19
  const result = addActivitiesToState([activity1], undefined, 'start');
@@ -101,7 +65,7 @@ describe('activity-utils', () => {
101
65
  });
102
66
 
103
67
  it('should handle multiple new activities correctly', () => {
104
- const activity3 = createMockActivity('activity3');
68
+ const activity3 = generateActivityResponse({ id: 'activity3' });
105
69
 
106
70
  const existingActivities = [activity1];
107
71
  const result = addActivitiesToState(
@@ -120,109 +84,107 @@ describe('activity-utils', () => {
120
84
 
121
85
  describe('updateActivityInState', () => {
122
86
  it('should update an activity in the state', () => {
123
- const originalActivity = createMockActivity('activity1', 'original text');
124
- const updatedActivity = createMockActivity('activity1', 'updated text');
87
+ const originalActivity = generateActivityResponse({
88
+ id: 'activity1',
89
+ text: 'original text',
90
+ });
91
+ const updatedActivity = { ...originalActivity, text: 'updated text' };
125
92
  const originalActivities = [originalActivity];
126
93
 
127
- const result = updateActivityInState(updatedActivity, originalActivities);
94
+ const result = updateActivityInState(
95
+ {
96
+ activity: updatedActivity,
97
+ created_at: new Date(),
98
+ fid: '',
99
+ type: '',
100
+ custom: {},
101
+ },
102
+ originalActivities,
103
+ );
128
104
 
129
105
  expect(result.changed).toBe(true);
130
- expect(result.activities).toHaveLength(1);
131
- expect(result.activities[0].id).toBe('activity1');
132
- expect(result.activities[0].text).toBe('updated text');
106
+ expect(result.entities).toHaveLength(1);
107
+ expect(result.entities![0].id).toBe('activity1');
108
+ expect(result.entities![0].text).toBe('updated text');
133
109
 
134
110
  // Make sure we create a new array
135
- expect(originalActivities === result.activities).toBe(false);
111
+ expect(originalActivities === result.entities).toBe(false);
136
112
  });
137
113
 
138
- it('should preserve reaction data when updating an activity', () => {
139
- const originalActivity = createMockActivity('activity1', 'original text');
140
- // Mock the reaction structure with proper types
141
- originalActivity.own_reactions = [
142
- {
143
- type: 'like',
144
- user: {
145
- id: 'user1',
146
- created_at: new Date(),
147
- updated_at: new Date(),
148
- banned: false,
149
- language: 'en',
150
- online: false,
151
- role: 'user',
152
- blocked_user_ids: [],
153
- teams: [],
154
- custom: {},
155
- },
156
- activity_id: 'activity1',
157
- created_at: new Date(),
158
- updated_at: new Date(),
114
+ it('should preserve reaction data (own_reaction) when updating an activity', () => {
115
+ const r = generateFeedReactionResponse({
116
+ activity_id: 'activity1',
117
+ user: {
118
+ id: 'user1',
159
119
  },
160
- ];
161
- originalActivity.latest_reactions = {} as FeedsReactionResponse[];
162
- (originalActivity.latest_reactions as any).like = [
120
+ });
121
+ const originalActivity = generateActivityResponse({
122
+ id: 'activity1',
123
+ text: 'original text',
124
+ own_reactions: [r],
125
+ latest_reactions: [r],
126
+ });
127
+
128
+ const updatedActivity = generateActivityResponse({
129
+ id: 'activity1',
130
+ text: 'updated text',
131
+ own_reactions: [],
132
+ latest_reactions: [
133
+ r,
134
+ generateFeedReactionResponse({
135
+ activity_id: 'activity1',
136
+ user: { id: 'user2' },
137
+ }),
138
+ ],
139
+ });
140
+
141
+ const result = updateActivityInState(
163
142
  {
164
- type: 'like',
165
- user: {
166
- id: 'user1',
167
- created_at: new Date(),
168
- updated_at: new Date(),
169
- banned: false,
170
- language: 'en',
171
- online: false,
172
- role: 'user',
173
- blocked_user_ids: [],
174
- teams: [],
175
- custom: {},
176
- },
177
- activity_id: 'activity1',
143
+ activity: updatedActivity,
178
144
  created_at: new Date(),
179
- updated_at: new Date(),
180
- },
181
- ];
182
- originalActivity.reaction_groups = {
183
- like: {
184
- sum_scores: 0,
185
- count: 1,
186
- first_reaction_at: new Date(),
187
- last_reaction_at: new Date(),
145
+ fid: '',
146
+ type: '',
147
+ custom: {},
188
148
  },
189
- };
190
-
191
- const updatedActivity = createMockActivity('activity1', 'updated text');
192
- // Reactions are not included in the updated activity from server
193
-
194
- const result = updateActivityInState(updatedActivity, [originalActivity]);
149
+ [originalActivity],
150
+ );
195
151
 
196
152
  expect(result.changed).toBe(true);
197
- expect(result.activities[0].text).toBe('updated text');
153
+ expect(result.entities![0].text).toBe('updated text');
198
154
  // Check that reactions were preserved
199
- expect(result.activities[0].own_reactions).toEqual(
155
+ expect(result.entities![0].own_reactions).toEqual(
200
156
  originalActivity.own_reactions,
201
157
  );
202
- expect(result.activities[0].latest_reactions).toEqual(
203
- originalActivity.latest_reactions,
204
- );
205
- expect(result.activities[0].reaction_groups).toEqual(
206
- originalActivity.reaction_groups,
207
- );
208
158
  });
209
159
 
210
160
  it('should return unchanged state if activity not found', () => {
211
- const existingActivity = createMockActivity('activity1');
212
- const updatedActivity = createMockActivity('activity2', 'some text');
161
+ const existingActivity = generateActivityResponse({ id: 'activity1' });
162
+ const updatedActivity = generateActivityResponse({
163
+ id: 'activity2',
164
+ text: 'some text',
165
+ });
213
166
 
214
- const result = updateActivityInState(updatedActivity, [existingActivity]);
167
+ const result = updateActivityInState(
168
+ {
169
+ activity: updatedActivity,
170
+ created_at: new Date(),
171
+ fid: '',
172
+ type: '',
173
+ custom: {},
174
+ },
175
+ [existingActivity],
176
+ );
215
177
 
216
178
  expect(result.changed).toBe(false);
217
- expect(result.activities).toHaveLength(1);
218
- expect(result.activities[0].id).toBe('activity1');
179
+ expect(result.entities).toHaveLength(1);
180
+ expect(result.entities![0].id).toBe('activity1');
219
181
  });
220
182
  });
221
183
 
222
184
  describe('removeActivityFromState', () => {
223
185
  it('should remove an activity from the state', () => {
224
- const activity1 = createMockActivity('activity1');
225
- const activity2 = createMockActivity('activity2');
186
+ const activity1 = generateActivityResponse({ id: 'activity1' });
187
+ const activity2 = generateActivityResponse({ id: 'activity2' });
226
188
  const activities = [activity1, activity2];
227
189
 
228
190
  const result = removeActivityFromState(activity1, activities);
@@ -235,8 +197,8 @@ describe('activity-utils', () => {
235
197
  });
236
198
 
237
199
  it('should return unchanged state if activity not found', () => {
238
- const activity1 = createMockActivity('activity1');
239
- const activity2 = createMockActivity('activity2');
200
+ const activity1 = generateActivityResponse({ id: 'activity1' });
201
+ const activity2 = generateActivityResponse({ id: 'activity2' });
240
202
  const activities = [activity1];
241
203
 
242
204
  const result = removeActivityFromState(activity2, activities);
@@ -247,7 +209,7 @@ describe('activity-utils', () => {
247
209
  });
248
210
 
249
211
  it('should handle empty activities array', () => {
250
- const activity = createMockActivity('activity1');
212
+ const activity = generateActivityResponse({ id: 'activity1' });
251
213
  const activities: ActivityResponse[] = [];
252
214
 
253
215
  const result = removeActivityFromState(activity, activities);
@@ -1,14 +1,20 @@
1
1
  import type { Feed } from '../../../feed';
2
- import type { ActivityResponse } from '../../../gen/models';
2
+ import type {
3
+ ActivityPinResponse,
4
+ ActivityResponse,
5
+ } from '../../../gen/models';
3
6
  import type { EventPayload } from '../../../types-internal';
4
7
 
5
8
  export const removeActivityFromState = (
6
9
  activityResponse: ActivityResponse,
7
- activities: ActivityResponse[],
10
+ activities: ActivityResponse[] | undefined,
8
11
  ) => {
9
- const index = activities.findIndex((a) => a.id === activityResponse.id);
12
+ const index =
13
+ activities?.findIndex((activity) => activity.id === activityResponse.id) ??
14
+ -1;
15
+
10
16
  if (index !== -1) {
11
- const newActivities = [...activities];
17
+ const newActivities = [...activities!];
12
18
  newActivities.splice(index, 1);
13
19
  return { changed: true, activities: newActivities };
14
20
  } else {
@@ -16,15 +22,42 @@ export const removeActivityFromState = (
16
22
  }
17
23
  };
18
24
 
25
+ export const removePinnedActivityFromState = (
26
+ activityResponse: ActivityResponse,
27
+ pinnedActivities: ActivityPinResponse[] | undefined,
28
+ ) => {
29
+ const index =
30
+ pinnedActivities?.findIndex(
31
+ (pinnedActivity) => pinnedActivity.activity.id === activityResponse.id,
32
+ ) ?? -1;
33
+
34
+ if (index !== -1) {
35
+ const newActivities = [...pinnedActivities!];
36
+ newActivities.splice(index, 1);
37
+ return { changed: true, activities: newActivities };
38
+ } else {
39
+ return { changed: false, pinned_activities: pinnedActivities };
40
+ }
41
+ };
42
+
19
43
  export function handleActivityDeleted(
20
44
  this: Feed,
21
45
  event: EventPayload<'feeds.activity.deleted'>,
22
46
  ) {
23
- const currentActivities = this.currentState.activities;
24
- if (currentActivities) {
25
- const result = removeActivityFromState(event.activity, currentActivities);
26
- if (result.changed) {
27
- this.state.partialNext({ activities: result.activities });
28
- }
47
+ const {
48
+ activities: currentActivities,
49
+ pinned_activities: currentPinnedActivities,
50
+ } = this.currentState;
51
+
52
+ const [result1, result2] = [
53
+ removeActivityFromState(event.activity, currentActivities),
54
+ removePinnedActivityFromState(event.activity, currentPinnedActivities),
55
+ ];
56
+
57
+ if (result1.changed || result2.changed) {
58
+ this.state.partialNext({
59
+ activities: result1.activities,
60
+ pinned_activities: result2.pinned_activities,
61
+ });
29
62
  }
30
63
  }
@@ -0,0 +1,68 @@
1
+ import {
2
+ ActivityMarkEvent,
3
+ NotificationStatusResponse,
4
+ } from '../../../gen/models';
5
+ import { EventPayload, UpdateStateResult } from '../../../types-internal';
6
+ import { Feed } from '../../feed';
7
+
8
+ export const updateNotificationStatusFromActivityMarked = (
9
+ event: ActivityMarkEvent,
10
+ currentNotificationStatus: NotificationStatusResponse | undefined,
11
+ aggregatedActivities: Array<{ group: string }> = [],
12
+ ): UpdateStateResult<{
13
+ data?: { notification_status: NotificationStatusResponse };
14
+ }> => {
15
+ if (!currentNotificationStatus) {
16
+ return {
17
+ changed: false,
18
+ };
19
+ }
20
+
21
+ const newState = {
22
+ ...currentNotificationStatus,
23
+ };
24
+
25
+ if (event.mark_all_read) {
26
+ const allGroupIds = aggregatedActivities.map((activity) => activity.group);
27
+ newState.read_activities = [
28
+ ...new Set([
29
+ ...(currentNotificationStatus.read_activities ?? []),
30
+ ...allGroupIds,
31
+ ]),
32
+ ];
33
+ }
34
+
35
+ if (event.mark_read && event.mark_read.length > 0) {
36
+ newState.read_activities = [
37
+ ...new Set([
38
+ ...(currentNotificationStatus?.read_activities ?? []),
39
+ ...event.mark_read,
40
+ ]),
41
+ ];
42
+ }
43
+
44
+ if (event.mark_all_seen) {
45
+ newState.last_seen_at = new Date();
46
+ }
47
+
48
+ return {
49
+ changed: true,
50
+ data: { notification_status: newState },
51
+ };
52
+ };
53
+
54
+ export function handleActivityMarked(
55
+ this: Feed,
56
+ event: EventPayload<'feeds.activity.marked'>,
57
+ ) {
58
+ const result = updateNotificationStatusFromActivityMarked(
59
+ event,
60
+ this.currentState.notification_status,
61
+ this.currentState.aggregated_activities,
62
+ );
63
+ if (result.changed) {
64
+ this.state.partialNext({
65
+ notification_status: result.data?.notification_status,
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+
3
+ import { Feed } from '../../../feed';
4
+ import { FeedsClient } from '../../../feeds-client';
5
+ import { handleActivityPinned } from './handle-activity-pinned';
6
+ import {
7
+ generateActivityPinnedEvent,
8
+ generateActivityPinResponse,
9
+ generateFeedResponse,
10
+ } from '../../../test-utils/response-generators';
11
+ import { ActivityPinResponse } from '../../../gen/models';
12
+
13
+ describe(handleActivityPinned.name, () => {
14
+ let feed: Feed;
15
+ let client: FeedsClient;
16
+ let pinnedActivity: ActivityPinResponse;
17
+ let otherPinnedActivity: ActivityPinResponse;
18
+
19
+ beforeEach(() => {
20
+ client = new FeedsClient('mock-api-key');
21
+ const feedResponse = generateFeedResponse({ id: 'main', group_id: 'user' });
22
+ feed = new Feed(
23
+ client,
24
+ feedResponse.group_id,
25
+ feedResponse.id,
26
+ feedResponse,
27
+ );
28
+ pinnedActivity = generateActivityPinResponse();
29
+ otherPinnedActivity = generateActivityPinResponse();
30
+ feed.state.next((currentState) => ({
31
+ ...currentState,
32
+ pinned_activities: [otherPinnedActivity],
33
+ }));
34
+ });
35
+
36
+ it('adds a new activity to pinned_activities', () => {
37
+ const event = generateActivityPinnedEvent({
38
+ pinned_activity: pinnedActivity,
39
+ });
40
+ handleActivityPinned.call(feed, event);
41
+ const { pinned_activities } = feed.currentState;
42
+ expect(pinned_activities).toHaveLength(2);
43
+ expect(pinned_activities![0].activity.id).toBe(pinnedActivity.activity.id);
44
+ expect(pinned_activities![1]).toBe(otherPinnedActivity);
45
+ });
46
+
47
+ it('creates pinned_activities if it was undefined', () => {
48
+ feed.state.next((currentState) => ({
49
+ ...currentState,
50
+ pinned_activities: undefined,
51
+ }));
52
+ const event = generateActivityPinnedEvent({
53
+ pinned_activity: pinnedActivity,
54
+ });
55
+ handleActivityPinned.call(feed, event);
56
+ const { pinned_activities } = feed.currentState;
57
+ expect(pinned_activities).toHaveLength(1);
58
+ expect(pinned_activities![0].activity.id).toBe(pinnedActivity.activity.id);
59
+ });
60
+ });
@@ -0,0 +1,30 @@
1
+ import { ActivityPinResponse } from '../../../gen/models';
2
+ import { EventPayload } from '../../../types-internal';
3
+ import { Feed } from '../../feed';
4
+
5
+ export function handleActivityPinned(
6
+ this: Feed,
7
+ event: EventPayload<'feeds.activity.pinned'>,
8
+ ) {
9
+ this.state.next((currentState) => {
10
+ const newState = {
11
+ ...currentState,
12
+ };
13
+
14
+ // FIXME: type mismatch PinActivityResponse vs ActivityPinResponse (almost identical but not quite)
15
+
16
+ // re-map the event value to match the ActivityPinResponse type
17
+ const pinnedActivity: ActivityPinResponse = {
18
+ ...event.pinned_activity,
19
+ user: event.user!,
20
+ feed: event.fid,
21
+ updated_at: new Date(),
22
+ };
23
+
24
+ newState.pinned_activities = currentState.pinned_activities
25
+ ? [pinnedActivity, ...currentState.pinned_activities]
26
+ : [pinnedActivity];
27
+
28
+ return newState;
29
+ });
30
+ }
@@ -0,0 +1,157 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleActivityReactionAdded } from './handle-activity-reaction-added';
5
+ import {
6
+ generateActivityPinResponse,
7
+ generateActivityResponse,
8
+ generateFeedResponse,
9
+ generateOwnUser,
10
+ generateActivityReactionAddedEvent,
11
+ getHumanId,
12
+ } from '../../../test-utils/response-generators';
13
+
14
+ describe(handleActivityReactionAdded.name, () => {
15
+ let feed: Feed;
16
+ let client: FeedsClient;
17
+ let currentUserId: 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
+ });
37
+
38
+ it('adds a reaction to the correct activity for current user & updates activities with event.activity', () => {
39
+ const event = generateActivityReactionAddedEvent({
40
+ reaction: {
41
+ user: { id: currentUserId },
42
+ },
43
+ activity: {
44
+ reaction_count: 1,
45
+ },
46
+ });
47
+ const activity = generateActivityResponse({
48
+ id: event.activity.id,
49
+ reaction_count: 0,
50
+ });
51
+ const activityPin = generateActivityPinResponse({
52
+ activity: { ...activity },
53
+ });
54
+ feed.state.partialNext({
55
+ activities: [activity],
56
+ pinned_activities: [activityPin],
57
+ });
58
+
59
+ const stateBefore = feed.currentState;
60
+
61
+ expect(stateBefore.activities![0].reaction_count).toBe(0);
62
+ expect(stateBefore.pinned_activities![0].activity.reaction_count).toBe(
63
+ 0,
64
+ );
65
+
66
+ handleActivityReactionAdded.call(feed, event);
67
+
68
+ const stateAfter = feed.currentState;
69
+
70
+ expect(stateAfter.activities![0].own_reactions).toContain(event.reaction);
71
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toContain(
72
+ event.reaction,
73
+ );
74
+ expect(stateAfter.activities![0].own_bookmarks).toBe(
75
+ stateBefore.activities![0].own_bookmarks,
76
+ );
77
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toBe(
78
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
79
+ );
80
+ expect(stateAfter.activities![0].reaction_count).toBe(1);
81
+ expect(stateAfter.pinned_activities![0].activity.reaction_count).toBe(1);
82
+ });
83
+
84
+ it('does not add to own_reactions if reaction is from another user but still updates activity', () => {
85
+ const event = generateActivityReactionAddedEvent({
86
+ reaction: { user: { id: 'other-user-id' } },
87
+ activity: {
88
+ reaction_count: 1,
89
+ },
90
+ });
91
+ const activity = generateActivityResponse({
92
+ id: event.activity.id,
93
+ reaction_count: 0,
94
+ });
95
+ const activityPin = generateActivityPinResponse({
96
+ activity: { ...activity },
97
+ });
98
+ feed.state.partialNext({
99
+ activities: [activity],
100
+ pinned_activities: [activityPin],
101
+ });
102
+
103
+ const stateBefore = feed.currentState;
104
+
105
+ expect(stateBefore.activities![0].reaction_count).toBe(0);
106
+ expect(stateBefore.pinned_activities![0].activity.reaction_count).toBe(
107
+ 0,
108
+ );
109
+
110
+ handleActivityReactionAdded.call(feed, event);
111
+
112
+ const stateAfter = feed.currentState;
113
+
114
+ expect(stateAfter.activities![0].own_reactions).toHaveLength(0);
115
+ expect(
116
+ stateAfter.pinned_activities![0].activity.own_reactions,
117
+ ).toHaveLength(0);
118
+ expect(stateAfter.activities![0].reaction_count).toBe(1);
119
+ expect(stateAfter.pinned_activities![0].activity.reaction_count).toBe(1);
120
+ expect(stateAfter.activities![0].own_bookmarks).toBe(
121
+ stateBefore.activities![0].own_bookmarks,
122
+ );
123
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toBe(
124
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
125
+ );
126
+ expect(stateAfter.activities![0].own_reactions).toBe(
127
+ stateBefore.activities![0].own_reactions,
128
+ );
129
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toBe(
130
+ stateBefore.pinned_activities![0].activity.own_reactions,
131
+ );
132
+ });
133
+
134
+ it('does nothing if activity is not found', () => {
135
+ const event = generateActivityReactionAddedEvent({
136
+ reaction: { user: { id: currentUserId } },
137
+ });
138
+ const activity = generateActivityResponse({
139
+ id: 'unrelated',
140
+ });
141
+ const activityPin = generateActivityPinResponse({
142
+ activity: { ...activity },
143
+ });
144
+ feed.state.partialNext({
145
+ activities: [activity],
146
+ pinned_activities: [activityPin],
147
+ });
148
+
149
+ const stateBefore = feed.currentState;
150
+
151
+ handleActivityReactionAdded.call(feed, event);
152
+
153
+ const stateAfter = feed.currentState;
154
+
155
+ expect(stateAfter).toBe(stateBefore);
156
+ });
157
+ });