@stream-io/feeds-client 0.1.1 → 0.1.3

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 (44) hide show
  1. package/@react-bindings/index.ts +13 -1
  2. package/CHANGELOG.md +31 -0
  3. package/dist/@react-bindings/contexts/StreamFeedsContext.d.ts +15 -0
  4. package/dist/@react-bindings/hooks/clientStateHooks.d.ts +10 -0
  5. package/dist/@react-bindings/hooks/useCreateFeedsClient.d.ts +13 -0
  6. package/dist/@react-bindings/hooks/useStateStore.d.ts +1 -1
  7. package/dist/@react-bindings/index.d.ts +4 -0
  8. package/dist/@react-bindings/wrappers/StreamFeeds.d.ts +6 -0
  9. package/dist/index-react-bindings.browser.cjs +5411 -12
  10. package/dist/index-react-bindings.browser.cjs.map +1 -1
  11. package/dist/index-react-bindings.browser.js +5407 -14
  12. package/dist/index-react-bindings.browser.js.map +1 -1
  13. package/dist/index-react-bindings.node.cjs +5411 -12
  14. package/dist/index-react-bindings.node.cjs.map +1 -1
  15. package/dist/index-react-bindings.node.js +5407 -14
  16. package/dist/index-react-bindings.node.js.map +1 -1
  17. package/dist/index.browser.cjs +125 -36
  18. package/dist/index.browser.cjs.map +1 -1
  19. package/dist/index.browser.js +125 -36
  20. package/dist/index.browser.js.map +1 -1
  21. package/dist/index.node.cjs +125 -36
  22. package/dist/index.node.cjs.map +1 -1
  23. package/dist/index.node.js +125 -36
  24. package/dist/index.node.js.map +1 -1
  25. package/dist/src/Feed.d.ts +4 -2
  26. package/dist/src/FeedsClient.d.ts +9 -5
  27. package/dist/src/common/ApiClient.d.ts +1 -0
  28. package/dist/src/common/StateStore.d.ts +1 -0
  29. package/dist/src/common/real-time/StableWSConnection.d.ts +3 -3
  30. package/dist/src/gen/feeds/FeedsApi.d.ts +7 -1
  31. package/dist/src/gen/models/index.d.ts +41 -0
  32. package/dist/src/types.d.ts +2 -0
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +2 -6
  35. package/src/Feed.ts +33 -15
  36. package/src/FeedsClient.ts +37 -17
  37. package/src/common/ApiClient.ts +10 -3
  38. package/src/common/StateStore.ts +30 -12
  39. package/src/common/utils.ts +1 -1
  40. package/src/gen/feeds/FeedsApi.ts +51 -0
  41. package/src/gen/model-decoders/decoders.ts +29 -0
  42. package/src/gen/model-decoders/event-decoder-mapping.ts +6 -0
  43. package/src/gen/models/index.ts +60 -0
  44. package/src/types.ts +4 -0
@@ -261,6 +261,14 @@ decoders.BookmarkDeletedEvent = (input) => {
261
261
  };
262
262
  return decode(typeMappings, input);
263
263
  };
264
+ decoders.BookmarkFolderDeletedEvent = (input) => {
265
+ const typeMappings = {
266
+ created_at: { type: 'DatetimeType', isSingle: true },
267
+ bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true },
268
+ received_at: { type: 'DatetimeType', isSingle: true },
269
+ };
270
+ return decode(typeMappings, input);
271
+ };
264
272
  decoders.BookmarkFolderResponse = (input) => {
265
273
  const typeMappings = {
266
274
  created_at: { type: 'DatetimeType', isSingle: true },
@@ -268,6 +276,14 @@ decoders.BookmarkFolderResponse = (input) => {
268
276
  };
269
277
  return decode(typeMappings, input);
270
278
  };
279
+ decoders.BookmarkFolderUpdatedEvent = (input) => {
280
+ const typeMappings = {
281
+ created_at: { type: 'DatetimeType', isSingle: true },
282
+ bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true },
283
+ received_at: { type: 'DatetimeType', isSingle: true },
284
+ };
285
+ return decode(typeMappings, input);
286
+ };
271
287
  decoders.BookmarkResponse = (input) => {
272
288
  const typeMappings = {
273
289
  created_at: { type: 'DatetimeType', isSingle: true },
@@ -1227,6 +1243,12 @@ decoders.UpdateBlockListResponse = (input) => {
1227
1243
  };
1228
1244
  return decode(typeMappings, input);
1229
1245
  };
1246
+ decoders.UpdateBookmarkFolderResponse = (input) => {
1247
+ const typeMappings = {
1248
+ bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true },
1249
+ };
1250
+ return decode(typeMappings, input);
1251
+ };
1230
1252
  decoders.UpdateBookmarkResponse = (input) => {
1231
1253
  const typeMappings = {
1232
1254
  bookmark: { type: 'BookmarkResponse', isSingle: true },
@@ -1647,6 +1669,26 @@ class FeedsApi {
1647
1669
  decoders.QueryBookmarkFoldersResponse?.(response.body);
1648
1670
  return { ...response.body, metadata: response.metadata };
1649
1671
  }
1672
+ async deleteBookmarkFolder(request) {
1673
+ const pathParams = {
1674
+ folder_id: request?.folder_id,
1675
+ };
1676
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/bookmark_folders/{folder_id}', pathParams, undefined);
1677
+ decoders.DeleteBookmarkFolderResponse?.(response.body);
1678
+ return { ...response.body, metadata: response.metadata };
1679
+ }
1680
+ async updateBookmarkFolder(request) {
1681
+ const pathParams = {
1682
+ folder_id: request?.folder_id,
1683
+ };
1684
+ const body = {
1685
+ name: request?.name,
1686
+ custom: request?.custom,
1687
+ };
1688
+ const response = await this.apiClient.sendRequest('PATCH', '/api/v2/feeds/bookmark_folders/{folder_id}', pathParams, undefined, body, 'application/json');
1689
+ decoders.UpdateBookmarkFolderResponse?.(response.body);
1690
+ return { ...response.body, metadata: response.metadata };
1691
+ }
1650
1692
  async queryBookmarks(request) {
1651
1693
  const body = {
1652
1694
  limit: request?.limit,
@@ -2323,14 +2365,9 @@ class StateStore {
2323
2365
  let previouslySelectedValues;
2324
2366
  const wrappedHandler = (nextValue) => {
2325
2367
  const newlySelectedValues = selector(nextValue);
2326
- let hasUpdatedValues = typeof previouslySelectedValues === 'undefined';
2327
- for (const key in previouslySelectedValues) {
2328
- if (previouslySelectedValues[key] === newlySelectedValues[key])
2329
- continue;
2330
- hasUpdatedValues = true;
2331
- break;
2332
- }
2333
- if (!hasUpdatedValues)
2368
+ // shallow comparison of previouslySelectedValues and newlySelectedValues
2369
+ const selectionsAreEqual = StateStore.doSelectionsEqual(previouslySelectedValues, newlySelectedValues);
2370
+ if (selectionsAreEqual)
2334
2371
  return;
2335
2372
  // save a copy of previouslySelectedValues before running
2336
2373
  // handler - if previouslySelectedValues are set to
@@ -2361,7 +2398,7 @@ class StateStore {
2361
2398
  ? newValueOrPatch(this.value)
2362
2399
  : newValueOrPatch;
2363
2400
  // do not notify subscribers if the value hasn't changed
2364
- if (newValue === this.value)
2401
+ if (Object.is(newValue, this.value))
2365
2402
  return;
2366
2403
  this.preprocessors.forEach((preprocessor) => preprocessor(newValue, this.value));
2367
2404
  const oldValue = this.value;
@@ -2378,6 +2415,19 @@ class StateStore {
2378
2415
  this.handlers.delete(handler);
2379
2416
  };
2380
2417
  }
2418
+ static doSelectionsEqual(previouslySelectedValues, newlySelectedValues) {
2419
+ let selectionsAreEqual;
2420
+ if ((selectionsAreEqual = typeof previouslySelectedValues !== 'undefined')) {
2421
+ for (const key in newlySelectedValues) {
2422
+ if (Object.is(previouslySelectedValues[key], newlySelectedValues[key])) {
2423
+ continue;
2424
+ }
2425
+ selectionsAreEqual = false;
2426
+ break;
2427
+ }
2428
+ }
2429
+ return selectionsAreEqual;
2430
+ }
2381
2431
  /**
2382
2432
  * Registers a preprocessor function that will be called before the state is updated.
2383
2433
  *
@@ -2603,7 +2653,7 @@ function getRandomValuesWithMathRandom(bytes) {
2603
2653
  }
2604
2654
  }
2605
2655
  const getRandomValues = (() => {
2606
- if (typeof crypto?.getRandomValues !== 'undefined') {
2656
+ if (typeof crypto !== 'undefined' && typeof crypto?.getRandomValues !== 'undefined') {
2607
2657
  return crypto.getRandomValues.bind(crypto);
2608
2658
  }
2609
2659
  else if (typeof msCrypto !== 'undefined') {
@@ -3495,7 +3545,10 @@ class ApiClient {
3495
3545
  const encodedBody = requestContentType === 'multipart/form-data' ? new FormData() : body;
3496
3546
  if (requestContentType === 'multipart/form-data') {
3497
3547
  Object.keys(body).forEach((key) => {
3498
- encodedBody.append(key, body[key]);
3548
+ const value = body[key];
3549
+ if (value != null) {
3550
+ encodedBody.append(key, value);
3551
+ }
3499
3552
  });
3500
3553
  }
3501
3554
  try {
@@ -3504,8 +3557,11 @@ class ApiClient {
3504
3557
  method,
3505
3558
  headers,
3506
3559
  params: queryParams,
3507
- paramsSerializer: params => this.queryParamsStringify(params),
3560
+ paramsSerializer: (params) => this.queryParamsStringify(params),
3508
3561
  data: encodedBody,
3562
+ timeout:
3563
+ // multipart/form-data requests should not have a timeout allowing ample time for file uploads
3564
+ requestContentType === 'multipart/form-data' ? 0 : this.timeout,
3509
3565
  });
3510
3566
  const metadata = this.getRequestMetadata(client_request_id, response);
3511
3567
  return { body: response.data, metadata };
@@ -3569,9 +3625,9 @@ class ApiClient {
3569
3625
  };
3570
3626
  };
3571
3627
  this.baseUrl = options?.base_url ?? 'https://video.stream-io-api.com';
3628
+ this.timeout = options?.timeout ?? 3000;
3572
3629
  this.axiosInstance = axios.create({
3573
3630
  baseURL: this.baseUrl,
3574
- timeout: options?.timeout ?? 3000,
3575
3631
  });
3576
3632
  }
3577
3633
  get webSocketBaseUrl() {
@@ -3612,6 +3668,8 @@ const eventDecoderMapping = {
3612
3668
  'feeds.bookmark.added': (data) => decoders.BookmarkAddedEvent(data),
3613
3669
  'feeds.bookmark.deleted': (data) => decoders.BookmarkDeletedEvent(data),
3614
3670
  'feeds.bookmark.updated': (data) => decoders.BookmarkUpdatedEvent(data),
3671
+ 'feeds.bookmark_folder.deleted': (data) => decoders.BookmarkFolderDeletedEvent(data),
3672
+ 'feeds.bookmark_folder.updated': (data) => decoders.BookmarkFolderUpdatedEvent(data),
3615
3673
  'feeds.comment.added': (data) => decoders.CommentAddedEvent(data),
3616
3674
  'feeds.comment.deleted': (data) => decoders.CommentDeletedEvent(data),
3617
3675
  'feeds.comment.reaction.added': (data) => decoders.CommentReactionAddedEvent(data),
@@ -4014,6 +4072,8 @@ class Feed extends FeedApi {
4014
4072
  'feeds.bookmark.added': this.handleBookmarkAdded.bind(this),
4015
4073
  'feeds.bookmark.deleted': this.handleBookmarkDeleted.bind(this),
4016
4074
  'feeds.bookmark.updated': this.handleBookmarkUpdated.bind(this),
4075
+ 'feeds.bookmark_folder.deleted': Feed.noop,
4076
+ 'feeds.bookmark_folder.updated': Feed.noop,
4017
4077
  'feeds.comment.added': (event) => {
4018
4078
  const { comment } = event;
4019
4079
  const forId = comment.parent_id ?? comment.object_id;
@@ -4103,16 +4163,19 @@ class Feed extends FeedApi {
4103
4163
  return;
4104
4164
  // this feed followed someone
4105
4165
  if (event.follow.source_feed.fid === this.fid) {
4106
- if (this.currentState.following_pagination?.next === END_OF_LIST) {
4107
- this.state.next((currentState) => ({
4166
+ this.state.next((currentState) => {
4167
+ const newState = {
4108
4168
  ...currentState,
4109
4169
  ...event.follow.source_feed,
4170
+ };
4171
+ if (currentState.following_pagination?.next === END_OF_LIST) {
4110
4172
  // TODO: respect sort
4111
- following: currentState.following
4173
+ newState.following = currentState.following
4112
4174
  ? currentState.following.concat(event.follow)
4113
- : [event.follow],
4114
- }));
4115
- }
4175
+ : [event.follow];
4176
+ }
4177
+ return newState;
4178
+ });
4116
4179
  }
4117
4180
  else if (
4118
4181
  // someone followed this feed
@@ -4122,14 +4185,14 @@ class Feed extends FeedApi {
4122
4185
  this.state.next((currentState) => {
4123
4186
  const newState = { ...currentState, ...event.follow.target_feed };
4124
4187
  if (source.created_by.id === connectedUser?.id) {
4125
- newState.own_follows = newState.own_follows
4126
- ? newState.own_follows.concat(event.follow)
4188
+ newState.own_follows = currentState.own_follows
4189
+ ? currentState.own_follows.concat(event.follow)
4127
4190
  : [event.follow];
4128
4191
  }
4129
4192
  if (currentState.followers_pagination?.next === END_OF_LIST) {
4130
4193
  // TODO: respect sort
4131
- newState.followers = newState.followers
4132
- ? newState.followers.concat(event.follow)
4194
+ newState.followers = currentState.followers
4195
+ ? currentState.followers.concat(event.follow)
4133
4196
  : [event.follow];
4134
4197
  }
4135
4198
  return newState;
@@ -4155,9 +4218,9 @@ class Feed extends FeedApi {
4155
4218
  this.state.next((currentState) => {
4156
4219
  const newState = { ...currentState, ...event.follow.target_feed };
4157
4220
  if (source.created_by.id === connectedUser?.id) {
4158
- newState.own_follows = newState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4221
+ newState.own_follows = currentState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4159
4222
  }
4160
- newState.followers = newState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4223
+ newState.followers = currentState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4161
4224
  return newState;
4162
4225
  });
4163
4226
  }
@@ -4284,6 +4347,12 @@ class Feed extends FeedApi {
4284
4347
  };
4285
4348
  });
4286
4349
  }
4350
+ async synchronize() {
4351
+ const { last_get_or_create_request_config } = this.state.getLatestValue();
4352
+ if (last_get_or_create_request_config?.watch) {
4353
+ await this.getOrCreate(last_get_or_create_request_config);
4354
+ }
4355
+ }
4287
4356
  async getOrCreate(request) {
4288
4357
  if (this.currentState.is_loading_activities) {
4289
4358
  throw new Error('Only one getOrCreate call is allowed at a time');
@@ -4349,6 +4418,7 @@ class Feed extends FeedApi {
4349
4418
  if (!request?.following_pagination?.limit) {
4350
4419
  delete nextState.following;
4351
4420
  }
4421
+ nextState.last_get_or_create_request_config = request;
4352
4422
  return nextState;
4353
4423
  });
4354
4424
  }
@@ -4635,7 +4705,7 @@ class Feed extends FeedApi {
4635
4705
  }
4636
4706
  async getNextPage() {
4637
4707
  const currentState = this.currentState;
4638
- const response = await this.getOrCreate({
4708
+ return await this.getOrCreate({
4639
4709
  member_pagination: {
4640
4710
  limit: 0,
4641
4711
  },
@@ -4646,8 +4716,8 @@ class Feed extends FeedApi {
4646
4716
  limit: 0,
4647
4717
  },
4648
4718
  next: currentState.next,
4719
+ limit: currentState.last_get_or_create_request_config?.limit ?? 20,
4649
4720
  });
4650
- return response;
4651
4721
  }
4652
4722
  addActivity(request) {
4653
4723
  return this.feedsApi.addActivity({
@@ -5050,6 +5120,7 @@ class FeedsClient extends FeedsApi {
5050
5120
  super(apiClient);
5051
5121
  this.eventDispatcher = new EventDispatcher();
5052
5122
  this.activeFeeds = {};
5123
+ this.healthyConnectionChangedEventCount = 0;
5053
5124
  this.pollFromState = (id) => this.polls_by_id.get(id);
5054
5125
  this.connectUser = async (user, tokenProvider) => {
5055
5126
  if (this.state.getLatestValue().connectedUser !== undefined ||
@@ -5065,7 +5136,10 @@ class FeedsClient extends FeedsApi {
5065
5136
  }, this.tokenManager, this.connectionIdManager, [decodeWSEvent]);
5066
5137
  this.wsConnection.on('all', (event) => this.eventDispatcher.dispatch(event));
5067
5138
  const connectedEvent = await this.wsConnection.connect();
5068
- this.state.partialNext({ connectedUser: connectedEvent?.me });
5139
+ this.state.partialNext({
5140
+ connectedUser: connectedEvent?.me,
5141
+ isWsConnectionHealthy: this.wsConnection.isHealthy,
5142
+ });
5069
5143
  }
5070
5144
  catch (err) {
5071
5145
  await this.disconnectUser();
@@ -5120,13 +5194,20 @@ class FeedsClient extends FeedsApi {
5120
5194
  removeConnectionEventListeners(this.updateNetworkConnectionStatus);
5121
5195
  this.connectionIdManager.reset();
5122
5196
  this.tokenManager.reset();
5123
- this.state.partialNext({ connectedUser: undefined });
5197
+ this.state.partialNext({ connectedUser: undefined, isWsConnectionHealthy: false });
5124
5198
  };
5125
5199
  this.on = this.eventDispatcher.on;
5126
5200
  this.off = this.eventDispatcher.off;
5127
5201
  this.feed = (groupId, id) => {
5128
5202
  return this.getOrCreateActiveFeed(groupId, id);
5129
5203
  };
5204
+ this.updateNetworkConnectionStatus = (event) => {
5205
+ const networkEvent = {
5206
+ type: 'network.changed',
5207
+ online: event.type === 'online',
5208
+ };
5209
+ this.eventDispatcher.dispatch(networkEvent);
5210
+ };
5130
5211
  this.getOrCreateActiveFeed = (group, id, data) => {
5131
5212
  const fid = `${group}:${id}`;
5132
5213
  if (this.activeFeeds[fid]) {
@@ -5138,15 +5219,9 @@ class FeedsClient extends FeedsApi {
5138
5219
  return feed;
5139
5220
  }
5140
5221
  };
5141
- this.updateNetworkConnectionStatus = (event) => {
5142
- const networkEvent = {
5143
- type: 'network.changed',
5144
- online: event.type === 'online',
5145
- };
5146
- this.eventDispatcher.dispatch(networkEvent);
5147
- };
5148
5222
  this.state = new StateStore({
5149
5223
  connectedUser: undefined,
5224
+ isWsConnectionHealthy: false,
5150
5225
  });
5151
5226
  this.moderation = new ModerationClient(apiClient);
5152
5227
  this.tokenManager = tokenManager;
@@ -5156,6 +5231,20 @@ class FeedsClient extends FeedsApi {
5156
5231
  const fid = event.fid;
5157
5232
  const feed = typeof fid === 'string' ? this.activeFeeds[fid] : undefined;
5158
5233
  switch (event.type) {
5234
+ case 'connection.changed': {
5235
+ const { online } = event;
5236
+ this.state.partialNext({ isWsConnectionHealthy: online });
5237
+ if (online) {
5238
+ this.healthyConnectionChangedEventCount++;
5239
+ // we skip the first event as we could potentially be querying twice
5240
+ if (this.healthyConnectionChangedEventCount > 1) {
5241
+ for (const activeFeed of Object.values(this.activeFeeds)) {
5242
+ activeFeed.synchronize();
5243
+ }
5244
+ }
5245
+ }
5246
+ break;
5247
+ }
5159
5248
  case 'feeds.feed.created': {
5160
5249
  if (feed)
5161
5250
  break;