@stream-io/feeds-client 0.1.0

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 (96) hide show
  1. package/@react-bindings/index.ts +2 -0
  2. package/CHANGELOG.md +44 -0
  3. package/LICENSE +219 -0
  4. package/README.md +9 -0
  5. package/dist/@react-bindings/hooks/useComments.d.ts +12 -0
  6. package/dist/@react-bindings/hooks/useStateStore.d.ts +3 -0
  7. package/dist/@react-bindings/index.d.ts +2 -0
  8. package/dist/index-react-bindings.browser.cjs +56 -0
  9. package/dist/index-react-bindings.browser.cjs.map +1 -0
  10. package/dist/index-react-bindings.browser.js +53 -0
  11. package/dist/index-react-bindings.browser.js.map +1 -0
  12. package/dist/index-react-bindings.node.cjs +56 -0
  13. package/dist/index-react-bindings.node.cjs.map +1 -0
  14. package/dist/index-react-bindings.node.js +53 -0
  15. package/dist/index-react-bindings.node.js.map +1 -0
  16. package/dist/index.browser.cjs +5799 -0
  17. package/dist/index.browser.cjs.map +1 -0
  18. package/dist/index.browser.js +5782 -0
  19. package/dist/index.browser.js.map +1 -0
  20. package/dist/index.d.ts +13 -0
  21. package/dist/index.node.cjs +5799 -0
  22. package/dist/index.node.cjs.map +1 -0
  23. package/dist/index.node.js +5782 -0
  24. package/dist/index.node.js.map +1 -0
  25. package/dist/src/Feed.d.ts +109 -0
  26. package/dist/src/FeedsClient.d.ts +63 -0
  27. package/dist/src/ModerationClient.d.ts +3 -0
  28. package/dist/src/common/ActivitySearchSource.d.ts +17 -0
  29. package/dist/src/common/ApiClient.d.ts +20 -0
  30. package/dist/src/common/BaseSearchSource.d.ts +87 -0
  31. package/dist/src/common/ConnectionIdManager.d.ts +11 -0
  32. package/dist/src/common/EventDispatcher.d.ts +11 -0
  33. package/dist/src/common/FeedSearchSource.d.ts +17 -0
  34. package/dist/src/common/Poll.d.ts +34 -0
  35. package/dist/src/common/SearchController.d.ts +41 -0
  36. package/dist/src/common/StateStore.d.ts +124 -0
  37. package/dist/src/common/TokenManager.d.ts +29 -0
  38. package/dist/src/common/UserSearchSource.d.ts +17 -0
  39. package/dist/src/common/gen-imports.d.ts +2 -0
  40. package/dist/src/common/rate-limit.d.ts +2 -0
  41. package/dist/src/common/real-time/StableWSConnection.d.ts +144 -0
  42. package/dist/src/common/real-time/event-models.d.ts +36 -0
  43. package/dist/src/common/types.d.ts +29 -0
  44. package/dist/src/common/utils.d.ts +54 -0
  45. package/dist/src/gen/feeds/FeedApi.d.ts +26 -0
  46. package/dist/src/gen/feeds/FeedsApi.d.ts +237 -0
  47. package/dist/src/gen/model-decoders/decoders.d.ts +3 -0
  48. package/dist/src/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
  49. package/dist/src/gen/models/index.d.ts +3437 -0
  50. package/dist/src/gen/moderation/ModerationApi.d.ts +21 -0
  51. package/dist/src/gen-imports.d.ts +3 -0
  52. package/dist/src/state-updates/activity-reaction-utils.d.ts +10 -0
  53. package/dist/src/state-updates/activity-utils.d.ts +13 -0
  54. package/dist/src/state-updates/bookmark-utils.d.ts +14 -0
  55. package/dist/src/types-internal.d.ts +4 -0
  56. package/dist/src/types.d.ts +13 -0
  57. package/dist/src/utils.d.ts +1 -0
  58. package/dist/tsconfig.tsbuildinfo +1 -0
  59. package/index.ts +13 -0
  60. package/package.json +85 -0
  61. package/src/Feed.ts +1070 -0
  62. package/src/FeedsClient.ts +352 -0
  63. package/src/ModerationClient.ts +3 -0
  64. package/src/common/ActivitySearchSource.ts +46 -0
  65. package/src/common/ApiClient.ts +197 -0
  66. package/src/common/BaseSearchSource.ts +238 -0
  67. package/src/common/ConnectionIdManager.ts +51 -0
  68. package/src/common/EventDispatcher.ts +52 -0
  69. package/src/common/FeedSearchSource.ts +94 -0
  70. package/src/common/Poll.ts +313 -0
  71. package/src/common/SearchController.ts +152 -0
  72. package/src/common/StateStore.ts +314 -0
  73. package/src/common/TokenManager.ts +112 -0
  74. package/src/common/UserSearchSource.ts +93 -0
  75. package/src/common/gen-imports.ts +2 -0
  76. package/src/common/rate-limit.ts +23 -0
  77. package/src/common/real-time/StableWSConnection.ts +761 -0
  78. package/src/common/real-time/event-models.ts +38 -0
  79. package/src/common/types.ts +40 -0
  80. package/src/common/utils.ts +194 -0
  81. package/src/gen/feeds/FeedApi.ts +129 -0
  82. package/src/gen/feeds/FeedsApi.ts +2192 -0
  83. package/src/gen/model-decoders/decoders.ts +1877 -0
  84. package/src/gen/model-decoders/event-decoder-mapping.ts +150 -0
  85. package/src/gen/models/index.ts +5882 -0
  86. package/src/gen/moderation/ModerationApi.ts +270 -0
  87. package/src/gen-imports.ts +3 -0
  88. package/src/state-updates/activity-reaction-utils.test.ts +348 -0
  89. package/src/state-updates/activity-reaction-utils.ts +107 -0
  90. package/src/state-updates/activity-utils.test.ts +257 -0
  91. package/src/state-updates/activity-utils.ts +80 -0
  92. package/src/state-updates/bookmark-utils.test.ts +383 -0
  93. package/src/state-updates/bookmark-utils.ts +157 -0
  94. package/src/types-internal.ts +5 -0
  95. package/src/types.ts +20 -0
  96. package/src/utils.ts +4 -0
@@ -0,0 +1,270 @@
1
+ import { ApiClient, StreamResponse } from '../../gen-imports';
2
+ import {
3
+ BanRequest,
4
+ BanResponse,
5
+ DeleteModerationConfigResponse,
6
+ FlagRequest,
7
+ FlagResponse,
8
+ GetConfigResponse,
9
+ MuteRequest,
10
+ MuteResponse,
11
+ QueryModerationConfigsRequest,
12
+ QueryModerationConfigsResponse,
13
+ QueryReviewQueueRequest,
14
+ QueryReviewQueueResponse,
15
+ SubmitActionRequest,
16
+ SubmitActionResponse,
17
+ UpsertConfigRequest,
18
+ UpsertConfigResponse,
19
+ } from '../models';
20
+ import { decoders } from '../model-decoders/decoders';
21
+
22
+ export class ModerationApi {
23
+ constructor(public readonly apiClient: ApiClient) {}
24
+
25
+ async ban(request: BanRequest): Promise<StreamResponse<BanResponse>> {
26
+ const body = {
27
+ target_user_id: request?.target_user_id,
28
+ banned_by_id: request?.banned_by_id,
29
+ channel_cid: request?.channel_cid,
30
+ delete_messages: request?.delete_messages,
31
+ ip_ban: request?.ip_ban,
32
+ reason: request?.reason,
33
+ shadow: request?.shadow,
34
+ timeout: request?.timeout,
35
+ banned_by: request?.banned_by,
36
+ };
37
+
38
+ const response = await this.apiClient.sendRequest<
39
+ StreamResponse<BanResponse>
40
+ >(
41
+ 'POST',
42
+ '/api/v2/moderation/ban',
43
+ undefined,
44
+ undefined,
45
+ body,
46
+ 'application/json',
47
+ );
48
+
49
+ decoders.BanResponse?.(response.body);
50
+
51
+ return { ...response.body, metadata: response.metadata };
52
+ }
53
+
54
+ async upsertConfig(
55
+ request: UpsertConfigRequest,
56
+ ): Promise<StreamResponse<UpsertConfigResponse>> {
57
+ const body = {
58
+ key: request?.key,
59
+ async: request?.async,
60
+ team: request?.team,
61
+ ai_image_config: request?.ai_image_config,
62
+ ai_text_config: request?.ai_text_config,
63
+ ai_video_config: request?.ai_video_config,
64
+ automod_platform_circumvention_config:
65
+ request?.automod_platform_circumvention_config,
66
+ automod_semantic_filters_config: request?.automod_semantic_filters_config,
67
+ automod_toxicity_config: request?.automod_toxicity_config,
68
+ aws_rekognition_config: request?.aws_rekognition_config,
69
+ block_list_config: request?.block_list_config,
70
+ bodyguard_config: request?.bodyguard_config,
71
+ google_vision_config: request?.google_vision_config,
72
+ rule_builder_config: request?.rule_builder_config,
73
+ velocity_filter_config: request?.velocity_filter_config,
74
+ video_call_rule_config: request?.video_call_rule_config,
75
+ };
76
+
77
+ const response = await this.apiClient.sendRequest<
78
+ StreamResponse<UpsertConfigResponse>
79
+ >(
80
+ 'POST',
81
+ '/api/v2/moderation/config',
82
+ undefined,
83
+ undefined,
84
+ body,
85
+ 'application/json',
86
+ );
87
+
88
+ decoders.UpsertConfigResponse?.(response.body);
89
+
90
+ return { ...response.body, metadata: response.metadata };
91
+ }
92
+
93
+ async deleteConfig(request: {
94
+ key: string;
95
+ team?: string;
96
+ }): Promise<StreamResponse<DeleteModerationConfigResponse>> {
97
+ const queryParams = {
98
+ team: request?.team,
99
+ };
100
+ const pathParams = {
101
+ key: request?.key,
102
+ };
103
+
104
+ const response = await this.apiClient.sendRequest<
105
+ StreamResponse<DeleteModerationConfigResponse>
106
+ >('DELETE', '/api/v2/moderation/config/{key}', pathParams, queryParams);
107
+
108
+ decoders.DeleteModerationConfigResponse?.(response.body);
109
+
110
+ return { ...response.body, metadata: response.metadata };
111
+ }
112
+
113
+ async getConfig(request: {
114
+ key: string;
115
+ team?: string;
116
+ }): Promise<StreamResponse<GetConfigResponse>> {
117
+ const queryParams = {
118
+ team: request?.team,
119
+ };
120
+ const pathParams = {
121
+ key: request?.key,
122
+ };
123
+
124
+ const response = await this.apiClient.sendRequest<
125
+ StreamResponse<GetConfigResponse>
126
+ >('GET', '/api/v2/moderation/config/{key}', pathParams, queryParams);
127
+
128
+ decoders.GetConfigResponse?.(response.body);
129
+
130
+ return { ...response.body, metadata: response.metadata };
131
+ }
132
+
133
+ async queryModerationConfigs(
134
+ request?: QueryModerationConfigsRequest,
135
+ ): Promise<StreamResponse<QueryModerationConfigsResponse>> {
136
+ const body = {
137
+ limit: request?.limit,
138
+ next: request?.next,
139
+ prev: request?.prev,
140
+ sort: request?.sort,
141
+ filter: request?.filter,
142
+ };
143
+
144
+ const response = await this.apiClient.sendRequest<
145
+ StreamResponse<QueryModerationConfigsResponse>
146
+ >(
147
+ 'POST',
148
+ '/api/v2/moderation/configs',
149
+ undefined,
150
+ undefined,
151
+ body,
152
+ 'application/json',
153
+ );
154
+
155
+ decoders.QueryModerationConfigsResponse?.(response.body);
156
+
157
+ return { ...response.body, metadata: response.metadata };
158
+ }
159
+
160
+ async flag(request: FlagRequest): Promise<StreamResponse<FlagResponse>> {
161
+ const body = {
162
+ entity_id: request?.entity_id,
163
+ entity_type: request?.entity_type,
164
+ entity_creator_id: request?.entity_creator_id,
165
+ reason: request?.reason,
166
+ custom: request?.custom,
167
+ moderation_payload: request?.moderation_payload,
168
+ };
169
+
170
+ const response = await this.apiClient.sendRequest<
171
+ StreamResponse<FlagResponse>
172
+ >(
173
+ 'POST',
174
+ '/api/v2/moderation/flag',
175
+ undefined,
176
+ undefined,
177
+ body,
178
+ 'application/json',
179
+ );
180
+
181
+ decoders.FlagResponse?.(response.body);
182
+
183
+ return { ...response.body, metadata: response.metadata };
184
+ }
185
+
186
+ async mute(request: MuteRequest): Promise<StreamResponse<MuteResponse>> {
187
+ const body = {
188
+ target_ids: request?.target_ids,
189
+ timeout: request?.timeout,
190
+ };
191
+
192
+ const response = await this.apiClient.sendRequest<
193
+ StreamResponse<MuteResponse>
194
+ >(
195
+ 'POST',
196
+ '/api/v2/moderation/mute',
197
+ undefined,
198
+ undefined,
199
+ body,
200
+ 'application/json',
201
+ );
202
+
203
+ decoders.MuteResponse?.(response.body);
204
+
205
+ return { ...response.body, metadata: response.metadata };
206
+ }
207
+
208
+ async queryReviewQueue(
209
+ request?: QueryReviewQueueRequest,
210
+ ): Promise<StreamResponse<QueryReviewQueueResponse>> {
211
+ const body = {
212
+ limit: request?.limit,
213
+ lock_count: request?.lock_count,
214
+ lock_duration: request?.lock_duration,
215
+ lock_items: request?.lock_items,
216
+ next: request?.next,
217
+ prev: request?.prev,
218
+ stats_only: request?.stats_only,
219
+ sort: request?.sort,
220
+ filter: request?.filter,
221
+ };
222
+
223
+ const response = await this.apiClient.sendRequest<
224
+ StreamResponse<QueryReviewQueueResponse>
225
+ >(
226
+ 'POST',
227
+ '/api/v2/moderation/review_queue',
228
+ undefined,
229
+ undefined,
230
+ body,
231
+ 'application/json',
232
+ );
233
+
234
+ decoders.QueryReviewQueueResponse?.(response.body);
235
+
236
+ return { ...response.body, metadata: response.metadata };
237
+ }
238
+
239
+ async submitAction(
240
+ request: SubmitActionRequest,
241
+ ): Promise<StreamResponse<SubmitActionResponse>> {
242
+ const body = {
243
+ action_type: request?.action_type,
244
+ item_id: request?.item_id,
245
+ ban: request?.ban,
246
+ custom: request?.custom,
247
+ delete_activity: request?.delete_activity,
248
+ delete_message: request?.delete_message,
249
+ delete_reaction: request?.delete_reaction,
250
+ delete_user: request?.delete_user,
251
+ mark_reviewed: request?.mark_reviewed,
252
+ unban: request?.unban,
253
+ };
254
+
255
+ const response = await this.apiClient.sendRequest<
256
+ StreamResponse<SubmitActionResponse>
257
+ >(
258
+ 'POST',
259
+ '/api/v2/moderation/submit_action',
260
+ undefined,
261
+ undefined,
262
+ body,
263
+ 'application/json',
264
+ );
265
+
266
+ decoders.SubmitActionResponse?.(response.body);
267
+
268
+ return { ...response.body, metadata: response.metadata };
269
+ }
270
+ }
@@ -0,0 +1,3 @@
1
+ export type { ApiClient } from './common/ApiClient';
2
+ export type { StreamResponse } from './common/types';
3
+ export { FeedsApi } from './gen/feeds/FeedsApi';
@@ -0,0 +1,348 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ ActivityReactionAddedEvent,
4
+ ActivityReactionDeletedEvent,
5
+ ActivityResponse,
6
+ FeedsReactionResponse,
7
+ } from '../gen/models';
8
+ import {
9
+ addReactionToActivity,
10
+ removeReactionFromActivity,
11
+ addReactionToActivities,
12
+ removeReactionFromActivities,
13
+ } from './activity-reaction-utils';
14
+
15
+ const createMockActivity = (id: string): ActivityResponse => ({
16
+ id,
17
+ type: 'test',
18
+ created_at: new Date(),
19
+ updated_at: new Date(),
20
+ visibility: 'public',
21
+ bookmark_count: 0,
22
+ comment_count: 0,
23
+ share_count: 0,
24
+ attachments: [],
25
+ comments: [],
26
+ feeds: [],
27
+ filter_tags: [],
28
+ interest_tags: [],
29
+ latest_reactions: [],
30
+ mentioned_users: [],
31
+ own_bookmarks: [],
32
+ own_reactions: [],
33
+ custom: {},
34
+ reaction_groups: {},
35
+ search_data: {},
36
+ popularity: 0,
37
+ score: 0,
38
+ user: {
39
+ id: 'user1',
40
+ created_at: new Date(),
41
+ updated_at: new Date(),
42
+ banned: false,
43
+ language: 'en',
44
+ online: false,
45
+ role: 'user',
46
+ blocked_user_ids: [],
47
+ teams: [],
48
+ custom: {},
49
+ },
50
+ });
51
+
52
+ const createMockReaction = (
53
+ type: string,
54
+ userId: string,
55
+ activityId: string,
56
+ ): FeedsReactionResponse => ({
57
+ type,
58
+ user: {
59
+ id: userId,
60
+ created_at: new Date(),
61
+ updated_at: new Date(),
62
+ banned: false,
63
+ language: 'en',
64
+ online: false,
65
+ role: 'user',
66
+ blocked_user_ids: [],
67
+ teams: [],
68
+ custom: {},
69
+ },
70
+ activity_id: activityId,
71
+ created_at: new Date(),
72
+ updated_at: new Date(),
73
+ });
74
+
75
+ const createMockAddedEvent = (
76
+ reaction: FeedsReactionResponse,
77
+ activity: ActivityResponse,
78
+ ): ActivityReactionAddedEvent => ({
79
+ fid: 'test-fid',
80
+ reaction,
81
+ activity,
82
+ created_at: new Date(),
83
+ custom: {},
84
+ type: 'reaction.added',
85
+ });
86
+
87
+ const createMockDeletedEvent = (
88
+ reaction: FeedsReactionResponse,
89
+ activity: ActivityResponse,
90
+ ): ActivityReactionDeletedEvent => ({
91
+ fid: 'test-fid',
92
+ reaction,
93
+ activity,
94
+ created_at: new Date(),
95
+ custom: {},
96
+ type: 'reaction.deleted',
97
+ });
98
+
99
+ describe('activity-reaction-utils', () => {
100
+ describe('addReactionToActivity', () => {
101
+ it('should add reaction to own_reactions when from current user', () => {
102
+ const activity = createMockActivity('activity1');
103
+ const reaction = createMockReaction('like', 'user1', 'activity1');
104
+ const eventActivity = { ...activity };
105
+ eventActivity.latest_reactions = [reaction];
106
+ eventActivity.reaction_groups = {
107
+ [reaction.type]: {
108
+ count: 1,
109
+ first_reaction_at: reaction.created_at,
110
+ last_reaction_at: reaction.created_at,
111
+ },
112
+ };
113
+
114
+ const event = createMockAddedEvent(reaction, eventActivity);
115
+ const result = addReactionToActivity(event, activity, true);
116
+ expect(result.changed).toBe(true);
117
+ expect(result.own_reactions).toHaveLength(1);
118
+ expect(result.own_reactions[0]).toEqual(reaction);
119
+ expect(result.latest_reactions).toHaveLength(1);
120
+ expect(result.latest_reactions[0]).toEqual(reaction);
121
+ expect(result.reaction_groups.like).toEqual({
122
+ count: 1,
123
+ first_reaction_at: reaction.created_at,
124
+ last_reaction_at: reaction.created_at,
125
+ });
126
+ });
127
+
128
+ it('should not add reaction to own_reactions when not from current user', () => {
129
+ const activity = createMockActivity('activity1');
130
+ const reaction = createMockReaction('like', 'user2', 'activity1');
131
+ const eventActivity = { ...activity };
132
+ eventActivity.latest_reactions = [reaction];
133
+ eventActivity.reaction_groups = {
134
+ [reaction.type]: {
135
+ count: 1,
136
+ first_reaction_at: reaction.created_at,
137
+ last_reaction_at: reaction.created_at,
138
+ },
139
+ };
140
+ const event = createMockAddedEvent(reaction, eventActivity);
141
+
142
+ const result = addReactionToActivity(event, activity, false);
143
+
144
+ expect(result.changed).toBe(true);
145
+ expect(result.own_reactions).toHaveLength(0);
146
+ expect(result.latest_reactions).toHaveLength(1);
147
+ expect(result.latest_reactions[0]).toEqual(reaction);
148
+ expect(result.reaction_groups.like).toEqual({
149
+ count: 1,
150
+ first_reaction_at: reaction.created_at,
151
+ last_reaction_at: reaction.created_at,
152
+ });
153
+ });
154
+ });
155
+
156
+ describe('removeReactionFromActivity', () => {
157
+ it('should remove reaction from own_reactions when from current user', () => {
158
+ const activity = createMockActivity('activity1');
159
+ const reaction = createMockReaction('like', 'user1', 'activity1');
160
+ const eventActivity = { ...activity };
161
+ eventActivity.latest_reactions = [reaction];
162
+ eventActivity.reaction_groups = {
163
+ [reaction.type]: {
164
+ count: 1,
165
+ first_reaction_at: reaction.created_at,
166
+ last_reaction_at: reaction.created_at,
167
+ },
168
+ };
169
+ const event = createMockAddedEvent(reaction, eventActivity);
170
+ const activityWithReaction = addReactionToActivity(event, activity, true);
171
+
172
+ const deleteEventActivity = { ...activityWithReaction };
173
+ deleteEventActivity.latest_reactions = [];
174
+ deleteEventActivity.reaction_groups = {};
175
+ const deleteEvent = createMockDeletedEvent(reaction, deleteEventActivity);
176
+ const result = removeReactionFromActivity(
177
+ deleteEvent,
178
+ activityWithReaction,
179
+ true,
180
+ );
181
+
182
+ expect(result.changed).toBe(true);
183
+ expect(result.own_reactions).toHaveLength(0);
184
+ expect(result.latest_reactions).toHaveLength(0);
185
+ expect(result.reaction_groups.like).toBeUndefined();
186
+ });
187
+
188
+ it('should not remove reaction from own_reactions when not from current user', () => {
189
+ const activity = createMockActivity('activity1');
190
+ const reaction = createMockReaction('like', 'user1', 'activity1');
191
+ const eventActivity = { ...activity };
192
+ eventActivity.latest_reactions = [reaction];
193
+ eventActivity.reaction_groups = {
194
+ [reaction.type]: {
195
+ count: 1,
196
+ first_reaction_at: reaction.created_at,
197
+ last_reaction_at: reaction.created_at,
198
+ },
199
+ };
200
+ const event = createMockAddedEvent(reaction, eventActivity);
201
+ const activityWithReaction = addReactionToActivity(event, activity, true);
202
+
203
+ const deleteEventActivity = { ...activityWithReaction };
204
+ deleteEventActivity.latest_reactions = [];
205
+ deleteEventActivity.reaction_groups = {};
206
+ const deleteEvent = createMockDeletedEvent(reaction, deleteEventActivity);
207
+ const result = removeReactionFromActivity(
208
+ deleteEvent,
209
+ activityWithReaction,
210
+ false,
211
+ );
212
+
213
+ expect(result.changed).toBe(true);
214
+ expect(result.own_reactions).toHaveLength(1);
215
+ expect(result.latest_reactions).toHaveLength(0);
216
+ expect(result.reaction_groups.like).toBeUndefined();
217
+ });
218
+ });
219
+
220
+ describe('addReactionToActivities', () => {
221
+ it('should add reaction to activity in activities array', () => {
222
+ const activity = createMockActivity('activity1');
223
+ const activities = [activity];
224
+ const reaction = createMockReaction('like', 'user1', 'activity1');
225
+ const eventActivity = { ...activity };
226
+ eventActivity.latest_reactions = [reaction];
227
+ eventActivity.reaction_groups = {
228
+ [reaction.type]: {
229
+ count: 1,
230
+ first_reaction_at: reaction.created_at,
231
+ last_reaction_at: reaction.created_at,
232
+ },
233
+ };
234
+ const event = createMockAddedEvent(reaction, eventActivity);
235
+
236
+ const result = addReactionToActivities(event, activities, true);
237
+
238
+ expect(result.changed).toBe(true);
239
+ expect(result.activities).toHaveLength(1);
240
+ expect(result.activities[0].own_reactions).toHaveLength(1);
241
+ expect(result.activities[0].own_reactions[0]).toEqual(reaction);
242
+ });
243
+
244
+ it('should return unchanged state if activity not found', () => {
245
+ const activity = createMockActivity('activity1');
246
+ const activities = [activity];
247
+ const reaction = createMockReaction('like', 'user1', 'activity2');
248
+ const eventActivity = createMockActivity('activity2');
249
+ eventActivity.latest_reactions = [reaction];
250
+ eventActivity.reaction_groups = {
251
+ [reaction.type]: {
252
+ count: 1,
253
+ first_reaction_at: reaction.created_at,
254
+ last_reaction_at: reaction.created_at,
255
+ },
256
+ };
257
+ const event = createMockAddedEvent(reaction, eventActivity);
258
+
259
+ const result = addReactionToActivities(event, activities, true);
260
+
261
+ expect(result.changed).toBe(false);
262
+ expect(result.activities).toBe(activities);
263
+ });
264
+
265
+ it('should handle undefined activities', () => {
266
+ const activity = createMockActivity('activity1');
267
+ const reaction = createMockReaction('like', 'user1', 'activity1');
268
+ const eventActivity = { ...activity };
269
+ eventActivity.own_reactions = [reaction];
270
+ eventActivity.latest_reactions = [reaction];
271
+ eventActivity.reaction_groups = {
272
+ [reaction.type]: {
273
+ count: 1,
274
+ first_reaction_at: reaction.created_at,
275
+ last_reaction_at: reaction.created_at,
276
+ },
277
+ };
278
+ const event = createMockAddedEvent(reaction, eventActivity);
279
+
280
+ const result = addReactionToActivities(event, undefined, true);
281
+
282
+ expect(result.changed).toBe(false);
283
+ expect(result.activities).toEqual([]);
284
+ });
285
+ });
286
+
287
+ describe('removeReactionFromActivities', () => {
288
+ it('should remove reaction from activity in activities array', () => {
289
+ const activity = createMockActivity('activity1');
290
+ const reaction = createMockReaction('like', 'user1', 'activity1');
291
+ const eventActivity = { ...activity };
292
+ eventActivity.latest_reactions = [reaction];
293
+ eventActivity.reaction_groups = {
294
+ [reaction.type]: {
295
+ count: 1,
296
+ first_reaction_at: reaction.created_at,
297
+ last_reaction_at: reaction.created_at,
298
+ },
299
+ };
300
+ const event = createMockAddedEvent(reaction, eventActivity);
301
+ const activityWithReaction = addReactionToActivity(event, activity, true);
302
+ const activities = [activityWithReaction];
303
+
304
+ const deleteEventActivity = { ...activityWithReaction };
305
+ deleteEventActivity.latest_reactions = [];
306
+ deleteEventActivity.reaction_groups = {};
307
+ const deleteEvent = createMockDeletedEvent(reaction, deleteEventActivity);
308
+ const result = removeReactionFromActivities(
309
+ deleteEvent,
310
+ activities,
311
+ true,
312
+ );
313
+
314
+ expect(result.changed).toBe(true);
315
+ expect(result.activities).toHaveLength(1);
316
+ expect(result.activities[0].own_reactions).toHaveLength(0);
317
+ });
318
+
319
+ it('should return unchanged state if activity not found', () => {
320
+ const activity = createMockActivity('activity1');
321
+ const activities = [activity];
322
+ const reaction = createMockReaction('like', 'user1', 'activity2');
323
+ const eventActivity = createMockActivity('activity2');
324
+ eventActivity.latest_reactions = [];
325
+ eventActivity.reaction_groups = {};
326
+ const event = createMockDeletedEvent(reaction, eventActivity);
327
+
328
+ const result = removeReactionFromActivities(event, activities, true);
329
+
330
+ expect(result.changed).toBe(false);
331
+ expect(result.activities).toBe(activities);
332
+ });
333
+
334
+ it('should handle undefined activities', () => {
335
+ const activity = createMockActivity('activity1');
336
+ const reaction = createMockReaction('like', 'user1', 'activity1');
337
+ const eventActivity = { ...activity };
338
+ eventActivity.latest_reactions = [];
339
+ eventActivity.reaction_groups = {};
340
+ const event = createMockDeletedEvent(reaction, eventActivity);
341
+
342
+ const result = removeReactionFromActivities(event, undefined, true);
343
+
344
+ expect(result.changed).toBe(false);
345
+ expect(result.activities).toEqual([]);
346
+ });
347
+ });
348
+ });