@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,107 @@
1
+ import {
2
+ ActivityReactionAddedEvent,
3
+ ActivityReactionDeletedEvent,
4
+ ActivityResponse,
5
+ } from '../gen/models';
6
+ import { UpdateStateResult } from '../types-internal';
7
+
8
+ const updateActivityInActivities = (
9
+ updatedActivity: ActivityResponse,
10
+ activities: ActivityResponse[],
11
+ ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
12
+ const index = activities.findIndex((a) => a.id === updatedActivity.id);
13
+ if (index !== -1) {
14
+ const newActivities = [...activities];
15
+ newActivities[index] = updatedActivity;
16
+ return { changed: true, activities: newActivities };
17
+ } else {
18
+ return { changed: false, activities };
19
+ }
20
+ };
21
+
22
+ export const addReactionToActivity = (
23
+ event: ActivityReactionAddedEvent,
24
+ activity: ActivityResponse,
25
+ isCurrentUser: boolean,
26
+ ): UpdateStateResult<ActivityResponse> => {
27
+ // Update own_reactions if the reaction is from the current user
28
+ const ownReactions = [...(activity.own_reactions || [])];
29
+ if (isCurrentUser) {
30
+ ownReactions.push(event.reaction);
31
+ }
32
+
33
+ return {
34
+ ...activity,
35
+ own_reactions: ownReactions,
36
+ latest_reactions: event.activity.latest_reactions,
37
+ reaction_groups: event.activity.reaction_groups,
38
+ changed: true,
39
+ };
40
+ };
41
+
42
+ export const removeReactionFromActivity = (
43
+ event: ActivityReactionDeletedEvent,
44
+ activity: ActivityResponse,
45
+ isCurrentUser: boolean,
46
+ ): UpdateStateResult<ActivityResponse> => {
47
+ // Update own_reactions if the reaction is from the current user
48
+ const ownReactions = isCurrentUser
49
+ ? (activity.own_reactions || []).filter(
50
+ (r) =>
51
+ !(
52
+ r.type === event.reaction.type &&
53
+ r.user.id === event.reaction.user.id
54
+ ),
55
+ )
56
+ : activity.own_reactions;
57
+
58
+ return {
59
+ ...activity,
60
+ own_reactions: ownReactions,
61
+ latest_reactions: event.activity.latest_reactions,
62
+ reaction_groups: event.activity.reaction_groups,
63
+ changed: true,
64
+ };
65
+ };
66
+
67
+ export const addReactionToActivities = (
68
+ event: ActivityReactionAddedEvent,
69
+ activities: ActivityResponse[] | undefined,
70
+ isCurrentUser: boolean,
71
+ ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
72
+ if (!activities) {
73
+ return { changed: false, activities: [] };
74
+ }
75
+
76
+ const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
77
+ if (activityIndex === -1) {
78
+ return { changed: false, activities };
79
+ }
80
+
81
+ const activity = activities[activityIndex];
82
+ const updatedActivity = addReactionToActivity(event, activity, isCurrentUser);
83
+ return updateActivityInActivities(updatedActivity, activities);
84
+ };
85
+
86
+ export const removeReactionFromActivities = (
87
+ event: ActivityReactionDeletedEvent,
88
+ activities: ActivityResponse[] | undefined,
89
+ isCurrentUser: boolean,
90
+ ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
91
+ if (!activities) {
92
+ return { changed: false, activities: [] };
93
+ }
94
+
95
+ const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
96
+ if (activityIndex === -1) {
97
+ return { changed: false, activities };
98
+ }
99
+
100
+ const activity = activities[activityIndex];
101
+ const updatedActivity = removeReactionFromActivity(
102
+ event,
103
+ activity,
104
+ isCurrentUser,
105
+ );
106
+ return updateActivityInActivities(updatedActivity, activities);
107
+ };
@@ -0,0 +1,257 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ActivityResponse, FeedsReactionResponse } from '../gen/models';
3
+ import {
4
+ addActivitiesToState,
5
+ updateActivityInState,
6
+ removeActivityFromState,
7
+ } from './activity-utils';
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
+ user: {
35
+ id: 'user1',
36
+ created_at: new Date(),
37
+ updated_at: new Date(),
38
+ banned: false,
39
+ language: 'en',
40
+ online: false,
41
+ role: 'user',
42
+ blocked_user_ids: [],
43
+ teams: [],
44
+ custom: {},
45
+ },
46
+ }) as ActivityResponse;
47
+
48
+ describe('activity-utils', () => {
49
+ describe('addActivitiesToState', () => {
50
+ const activity1 = createMockActivity('activity1');
51
+ const activity2 = createMockActivity('activity2');
52
+
53
+ it('should add activities to empty state', () => {
54
+ const result = addActivitiesToState([activity1], undefined, 'start');
55
+
56
+ expect(result.changed).toBe(true);
57
+ expect(result.activities).toHaveLength(1);
58
+ expect(result.activities[0].id).toBe('activity1');
59
+ });
60
+
61
+ it('should add activities to the start of existing activities', () => {
62
+ const existingActivities = [activity2];
63
+ const result = addActivitiesToState(
64
+ [activity1],
65
+ existingActivities,
66
+ 'start',
67
+ );
68
+
69
+ expect(result.changed).toBe(true);
70
+ expect(result.activities).toHaveLength(2);
71
+ expect(result.activities[0].id).toBe('activity1');
72
+ expect(result.activities[1].id).toBe('activity2');
73
+ });
74
+
75
+ it('should add activities to the end of existing activities', () => {
76
+ const existingActivities = [activity1];
77
+ const result = addActivitiesToState(
78
+ [activity2],
79
+ existingActivities,
80
+ 'end',
81
+ );
82
+
83
+ expect(result.changed).toBe(true);
84
+ expect(result.activities).toHaveLength(2);
85
+ expect(result.activities[0].id).toBe('activity1');
86
+ expect(result.activities[1].id).toBe('activity2');
87
+ });
88
+
89
+ it('should not add duplicate activities', () => {
90
+ const existingActivities = [activity1];
91
+ const result = addActivitiesToState(
92
+ [activity1],
93
+ existingActivities,
94
+ 'start',
95
+ );
96
+
97
+ expect(result.changed).toBe(false);
98
+ expect(result.activities).toHaveLength(1);
99
+ expect(result.activities[0].id).toBe('activity1');
100
+ });
101
+
102
+ it('should handle multiple new activities correctly', () => {
103
+ const activity3 = createMockActivity('activity3');
104
+
105
+ const existingActivities = [activity1];
106
+ const result = addActivitiesToState(
107
+ [activity2, activity3],
108
+ existingActivities,
109
+ 'start',
110
+ );
111
+
112
+ expect(result.changed).toBe(true);
113
+ expect(result.activities).toHaveLength(3);
114
+ expect(result.activities[0].id).toBe('activity2');
115
+ expect(result.activities[1].id).toBe('activity3');
116
+ expect(result.activities[2].id).toBe('activity1');
117
+ });
118
+ });
119
+
120
+ describe('updateActivityInState', () => {
121
+ it('should update an activity in the state', () => {
122
+ const originalActivity = createMockActivity('activity1', 'original text');
123
+ const updatedActivity = createMockActivity('activity1', 'updated text');
124
+ const originalActivities = [originalActivity];
125
+
126
+ const result = updateActivityInState(updatedActivity, originalActivities);
127
+
128
+ expect(result.changed).toBe(true);
129
+ expect(result.activities).toHaveLength(1);
130
+ expect(result.activities[0].id).toBe('activity1');
131
+ expect(result.activities[0].text).toBe('updated text');
132
+
133
+ // Make sure we create a new array
134
+ expect(originalActivities === result.activities).toBe(false);
135
+ });
136
+
137
+ it('should preserve reaction data when updating an activity', () => {
138
+ const originalActivity = createMockActivity('activity1', 'original text');
139
+ // Mock the reaction structure with proper types
140
+ originalActivity.own_reactions = [
141
+ {
142
+ type: 'like',
143
+ user: {
144
+ id: 'user1',
145
+ created_at: new Date(),
146
+ updated_at: new Date(),
147
+ banned: false,
148
+ language: 'en',
149
+ online: false,
150
+ role: 'user',
151
+ blocked_user_ids: [],
152
+ teams: [],
153
+ custom: {},
154
+ },
155
+ activity_id: 'activity1',
156
+ created_at: new Date(),
157
+ updated_at: new Date(),
158
+ },
159
+ ];
160
+ originalActivity.latest_reactions = {} as FeedsReactionResponse[];
161
+ (originalActivity.latest_reactions as any).like = [
162
+ {
163
+ type: 'like',
164
+ user: {
165
+ id: 'user1',
166
+ created_at: new Date(),
167
+ updated_at: new Date(),
168
+ banned: false,
169
+ language: 'en',
170
+ online: false,
171
+ role: 'user',
172
+ blocked_user_ids: [],
173
+ teams: [],
174
+ custom: {},
175
+ },
176
+ activity_id: 'activity1',
177
+ created_at: new Date(),
178
+ updated_at: new Date(),
179
+ },
180
+ ];
181
+ originalActivity.reaction_groups = {
182
+ like: {
183
+ count: 1,
184
+ first_reaction_at: new Date(),
185
+ last_reaction_at: new Date(),
186
+ },
187
+ };
188
+
189
+ const updatedActivity = createMockActivity('activity1', 'updated text');
190
+ // Reactions are not included in the updated activity from server
191
+
192
+ const result = updateActivityInState(updatedActivity, [originalActivity]);
193
+
194
+ expect(result.changed).toBe(true);
195
+ expect(result.activities[0].text).toBe('updated text');
196
+ // Check that reactions were preserved
197
+ expect(result.activities[0].own_reactions).toEqual(
198
+ originalActivity.own_reactions,
199
+ );
200
+ expect(result.activities[0].latest_reactions).toEqual(
201
+ originalActivity.latest_reactions,
202
+ );
203
+ expect(result.activities[0].reaction_groups).toEqual(
204
+ originalActivity.reaction_groups,
205
+ );
206
+ });
207
+
208
+ it('should return unchanged state if activity not found', () => {
209
+ const existingActivity = createMockActivity('activity1');
210
+ const updatedActivity = createMockActivity('activity2', 'some text');
211
+
212
+ const result = updateActivityInState(updatedActivity, [existingActivity]);
213
+
214
+ expect(result.changed).toBe(false);
215
+ expect(result.activities).toHaveLength(1);
216
+ expect(result.activities[0].id).toBe('activity1');
217
+ });
218
+ });
219
+
220
+ describe('removeActivityFromState', () => {
221
+ it('should remove an activity from the state', () => {
222
+ const activity1 = createMockActivity('activity1');
223
+ const activity2 = createMockActivity('activity2');
224
+ const activities = [activity1, activity2];
225
+
226
+ const result = removeActivityFromState(activity1, activities);
227
+
228
+ expect(result.changed).toBe(true);
229
+ expect(result.activities).toHaveLength(1);
230
+ expect(result.activities[0].id).toBe('activity2');
231
+ // Make sure we create a new array
232
+ expect(activities === result.activities).toBe(false);
233
+ });
234
+
235
+ it('should return unchanged state if activity not found', () => {
236
+ const activity1 = createMockActivity('activity1');
237
+ const activity2 = createMockActivity('activity2');
238
+ const activities = [activity1];
239
+
240
+ const result = removeActivityFromState(activity2, activities);
241
+
242
+ expect(result.changed).toBe(false);
243
+ expect(result.activities).toHaveLength(1);
244
+ expect(result.activities[0].id).toBe('activity1');
245
+ });
246
+
247
+ it('should handle empty activities array', () => {
248
+ const activity = createMockActivity('activity1');
249
+ const activities: ActivityResponse[] = [];
250
+
251
+ const result = removeActivityFromState(activity, activities);
252
+
253
+ expect(result.changed).toBe(false);
254
+ expect(result.activities).toHaveLength(0);
255
+ });
256
+ });
257
+ });
@@ -0,0 +1,80 @@
1
+ import { ActivityResponse } from '../gen/models';
2
+ import { UpdateStateResult } from '../types-internal';
3
+
4
+ export const addActivitiesToState = (
5
+ newActivities: ActivityResponse[],
6
+ activities: ActivityResponse[] | undefined,
7
+ position: 'start' | 'end',
8
+ ) => {
9
+ let result: UpdateStateResult<{ activities: ActivityResponse[] }>;
10
+ if (activities === undefined) {
11
+ activities = [];
12
+ result = {
13
+ changed: true,
14
+ activities,
15
+ };
16
+ } else {
17
+ result = {
18
+ changed: false,
19
+ activities,
20
+ };
21
+ }
22
+
23
+ const newActivitiesDeduplicated: ActivityResponse[] = [];
24
+ newActivities.forEach((newActivityResponse) => {
25
+ const index = activities.findIndex((a) => a.id === newActivityResponse.id);
26
+ if (index === -1) {
27
+ newActivitiesDeduplicated.push(newActivityResponse);
28
+ }
29
+ });
30
+
31
+ if (newActivitiesDeduplicated.length > 0) {
32
+ // TODO: since feed activities are not necessarily ordered by created_at (personalization) we don't order by created_at
33
+ // Maybe we can add a flag to the JS client to support order by created_at
34
+ const updatedActivities = [
35
+ ...(position === 'start' ? newActivitiesDeduplicated : []),
36
+ ...activities,
37
+ ...(position === 'end' ? newActivitiesDeduplicated : []),
38
+ ];
39
+ result = { changed: true, activities: updatedActivities };
40
+ }
41
+
42
+ return result;
43
+ };
44
+
45
+ export const updateActivityInState = (
46
+ updatedActivityResponse: ActivityResponse,
47
+ activities: ActivityResponse[],
48
+ ) => {
49
+ const index = activities.findIndex(
50
+ (a) => a.id === updatedActivityResponse.id,
51
+ );
52
+ if (index !== -1) {
53
+ const newActivities = [...activities];
54
+ const activity = activities[index];
55
+ newActivities[index] = {
56
+ ...updatedActivityResponse,
57
+ own_reactions: activity.own_reactions,
58
+ own_bookmarks: activity.own_bookmarks,
59
+ latest_reactions: activity.latest_reactions,
60
+ reaction_groups: activity.reaction_groups,
61
+ };
62
+ return { changed: true, activities: newActivities };
63
+ } else {
64
+ return { changed: false, activities };
65
+ }
66
+ };
67
+
68
+ export const removeActivityFromState = (
69
+ activityResponse: ActivityResponse,
70
+ activities: ActivityResponse[],
71
+ ) => {
72
+ const index = activities.findIndex((a) => a.id === activityResponse.id);
73
+ if (index !== -1) {
74
+ const newActivities = [...activities];
75
+ newActivities.splice(index, 1);
76
+ return { changed: true, activities: newActivities };
77
+ } else {
78
+ return { changed: false, activities };
79
+ }
80
+ };