@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
@@ -259,6 +259,14 @@ decoders.BookmarkDeletedEvent = (input) => {
259
259
  };
260
260
  return decode(typeMappings, input);
261
261
  };
262
+ decoders.BookmarkFolderDeletedEvent = (input) => {
263
+ const typeMappings = {
264
+ created_at: { type: 'DatetimeType', isSingle: true },
265
+ bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true },
266
+ received_at: { type: 'DatetimeType', isSingle: true },
267
+ };
268
+ return decode(typeMappings, input);
269
+ };
262
270
  decoders.BookmarkFolderResponse = (input) => {
263
271
  const typeMappings = {
264
272
  created_at: { type: 'DatetimeType', isSingle: true },
@@ -266,6 +274,14 @@ decoders.BookmarkFolderResponse = (input) => {
266
274
  };
267
275
  return decode(typeMappings, input);
268
276
  };
277
+ decoders.BookmarkFolderUpdatedEvent = (input) => {
278
+ const typeMappings = {
279
+ created_at: { type: 'DatetimeType', isSingle: true },
280
+ bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true },
281
+ received_at: { type: 'DatetimeType', isSingle: true },
282
+ };
283
+ return decode(typeMappings, input);
284
+ };
269
285
  decoders.BookmarkResponse = (input) => {
270
286
  const typeMappings = {
271
287
  created_at: { type: 'DatetimeType', isSingle: true },
@@ -1225,6 +1241,12 @@ decoders.UpdateBlockListResponse = (input) => {
1225
1241
  };
1226
1242
  return decode(typeMappings, input);
1227
1243
  };
1244
+ decoders.UpdateBookmarkFolderResponse = (input) => {
1245
+ const typeMappings = {
1246
+ bookmark_folder: { type: 'BookmarkFolderResponse', isSingle: true },
1247
+ };
1248
+ return decode(typeMappings, input);
1249
+ };
1228
1250
  decoders.UpdateBookmarkResponse = (input) => {
1229
1251
  const typeMappings = {
1230
1252
  bookmark: { type: 'BookmarkResponse', isSingle: true },
@@ -1645,6 +1667,26 @@ class FeedsApi {
1645
1667
  decoders.QueryBookmarkFoldersResponse?.(response.body);
1646
1668
  return { ...response.body, metadata: response.metadata };
1647
1669
  }
1670
+ async deleteBookmarkFolder(request) {
1671
+ const pathParams = {
1672
+ folder_id: request?.folder_id,
1673
+ };
1674
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/bookmark_folders/{folder_id}', pathParams, undefined);
1675
+ decoders.DeleteBookmarkFolderResponse?.(response.body);
1676
+ return { ...response.body, metadata: response.metadata };
1677
+ }
1678
+ async updateBookmarkFolder(request) {
1679
+ const pathParams = {
1680
+ folder_id: request?.folder_id,
1681
+ };
1682
+ const body = {
1683
+ name: request?.name,
1684
+ custom: request?.custom,
1685
+ };
1686
+ const response = await this.apiClient.sendRequest('PATCH', '/api/v2/feeds/bookmark_folders/{folder_id}', pathParams, undefined, body, 'application/json');
1687
+ decoders.UpdateBookmarkFolderResponse?.(response.body);
1688
+ return { ...response.body, metadata: response.metadata };
1689
+ }
1648
1690
  async queryBookmarks(request) {
1649
1691
  const body = {
1650
1692
  limit: request?.limit,
@@ -2321,14 +2363,9 @@ class StateStore {
2321
2363
  let previouslySelectedValues;
2322
2364
  const wrappedHandler = (nextValue) => {
2323
2365
  const newlySelectedValues = selector(nextValue);
2324
- let hasUpdatedValues = typeof previouslySelectedValues === 'undefined';
2325
- for (const key in previouslySelectedValues) {
2326
- if (previouslySelectedValues[key] === newlySelectedValues[key])
2327
- continue;
2328
- hasUpdatedValues = true;
2329
- break;
2330
- }
2331
- if (!hasUpdatedValues)
2366
+ // shallow comparison of previouslySelectedValues and newlySelectedValues
2367
+ const selectionsAreEqual = StateStore.doSelectionsEqual(previouslySelectedValues, newlySelectedValues);
2368
+ if (selectionsAreEqual)
2332
2369
  return;
2333
2370
  // save a copy of previouslySelectedValues before running
2334
2371
  // handler - if previouslySelectedValues are set to
@@ -2359,7 +2396,7 @@ class StateStore {
2359
2396
  ? newValueOrPatch(this.value)
2360
2397
  : newValueOrPatch;
2361
2398
  // do not notify subscribers if the value hasn't changed
2362
- if (newValue === this.value)
2399
+ if (Object.is(newValue, this.value))
2363
2400
  return;
2364
2401
  this.preprocessors.forEach((preprocessor) => preprocessor(newValue, this.value));
2365
2402
  const oldValue = this.value;
@@ -2376,6 +2413,19 @@ class StateStore {
2376
2413
  this.handlers.delete(handler);
2377
2414
  };
2378
2415
  }
2416
+ static doSelectionsEqual(previouslySelectedValues, newlySelectedValues) {
2417
+ let selectionsAreEqual;
2418
+ if ((selectionsAreEqual = typeof previouslySelectedValues !== 'undefined')) {
2419
+ for (const key in newlySelectedValues) {
2420
+ if (Object.is(previouslySelectedValues[key], newlySelectedValues[key])) {
2421
+ continue;
2422
+ }
2423
+ selectionsAreEqual = false;
2424
+ break;
2425
+ }
2426
+ }
2427
+ return selectionsAreEqual;
2428
+ }
2379
2429
  /**
2380
2430
  * Registers a preprocessor function that will be called before the state is updated.
2381
2431
  *
@@ -2601,7 +2651,7 @@ function getRandomValuesWithMathRandom(bytes) {
2601
2651
  }
2602
2652
  }
2603
2653
  const getRandomValues = (() => {
2604
- if (typeof crypto?.getRandomValues !== 'undefined') {
2654
+ if (typeof crypto !== 'undefined' && typeof crypto?.getRandomValues !== 'undefined') {
2605
2655
  return crypto.getRandomValues.bind(crypto);
2606
2656
  }
2607
2657
  else if (typeof msCrypto !== 'undefined') {
@@ -3493,7 +3543,10 @@ class ApiClient {
3493
3543
  const encodedBody = requestContentType === 'multipart/form-data' ? new FormData() : body;
3494
3544
  if (requestContentType === 'multipart/form-data') {
3495
3545
  Object.keys(body).forEach((key) => {
3496
- encodedBody.append(key, body[key]);
3546
+ const value = body[key];
3547
+ if (value != null) {
3548
+ encodedBody.append(key, value);
3549
+ }
3497
3550
  });
3498
3551
  }
3499
3552
  try {
@@ -3502,8 +3555,11 @@ class ApiClient {
3502
3555
  method,
3503
3556
  headers,
3504
3557
  params: queryParams,
3505
- paramsSerializer: params => this.queryParamsStringify(params),
3558
+ paramsSerializer: (params) => this.queryParamsStringify(params),
3506
3559
  data: encodedBody,
3560
+ timeout:
3561
+ // multipart/form-data requests should not have a timeout allowing ample time for file uploads
3562
+ requestContentType === 'multipart/form-data' ? 0 : this.timeout,
3507
3563
  });
3508
3564
  const metadata = this.getRequestMetadata(client_request_id, response);
3509
3565
  return { body: response.data, metadata };
@@ -3567,9 +3623,9 @@ class ApiClient {
3567
3623
  };
3568
3624
  };
3569
3625
  this.baseUrl = options?.base_url ?? 'https://video.stream-io-api.com';
3626
+ this.timeout = options?.timeout ?? 3000;
3570
3627
  this.axiosInstance = axios.create({
3571
3628
  baseURL: this.baseUrl,
3572
- timeout: options?.timeout ?? 3000,
3573
3629
  });
3574
3630
  }
3575
3631
  get webSocketBaseUrl() {
@@ -3610,6 +3666,8 @@ const eventDecoderMapping = {
3610
3666
  'feeds.bookmark.added': (data) => decoders.BookmarkAddedEvent(data),
3611
3667
  'feeds.bookmark.deleted': (data) => decoders.BookmarkDeletedEvent(data),
3612
3668
  'feeds.bookmark.updated': (data) => decoders.BookmarkUpdatedEvent(data),
3669
+ 'feeds.bookmark_folder.deleted': (data) => decoders.BookmarkFolderDeletedEvent(data),
3670
+ 'feeds.bookmark_folder.updated': (data) => decoders.BookmarkFolderUpdatedEvent(data),
3613
3671
  'feeds.comment.added': (data) => decoders.CommentAddedEvent(data),
3614
3672
  'feeds.comment.deleted': (data) => decoders.CommentDeletedEvent(data),
3615
3673
  'feeds.comment.reaction.added': (data) => decoders.CommentReactionAddedEvent(data),
@@ -4012,6 +4070,8 @@ class Feed extends FeedApi {
4012
4070
  'feeds.bookmark.added': this.handleBookmarkAdded.bind(this),
4013
4071
  'feeds.bookmark.deleted': this.handleBookmarkDeleted.bind(this),
4014
4072
  'feeds.bookmark.updated': this.handleBookmarkUpdated.bind(this),
4073
+ 'feeds.bookmark_folder.deleted': Feed.noop,
4074
+ 'feeds.bookmark_folder.updated': Feed.noop,
4015
4075
  'feeds.comment.added': (event) => {
4016
4076
  const { comment } = event;
4017
4077
  const forId = comment.parent_id ?? comment.object_id;
@@ -4101,16 +4161,19 @@ class Feed extends FeedApi {
4101
4161
  return;
4102
4162
  // this feed followed someone
4103
4163
  if (event.follow.source_feed.fid === this.fid) {
4104
- if (this.currentState.following_pagination?.next === END_OF_LIST) {
4105
- this.state.next((currentState) => ({
4164
+ this.state.next((currentState) => {
4165
+ const newState = {
4106
4166
  ...currentState,
4107
4167
  ...event.follow.source_feed,
4168
+ };
4169
+ if (currentState.following_pagination?.next === END_OF_LIST) {
4108
4170
  // TODO: respect sort
4109
- following: currentState.following
4171
+ newState.following = currentState.following
4110
4172
  ? currentState.following.concat(event.follow)
4111
- : [event.follow],
4112
- }));
4113
- }
4173
+ : [event.follow];
4174
+ }
4175
+ return newState;
4176
+ });
4114
4177
  }
4115
4178
  else if (
4116
4179
  // someone followed this feed
@@ -4120,14 +4183,14 @@ class Feed extends FeedApi {
4120
4183
  this.state.next((currentState) => {
4121
4184
  const newState = { ...currentState, ...event.follow.target_feed };
4122
4185
  if (source.created_by.id === connectedUser?.id) {
4123
- newState.own_follows = newState.own_follows
4124
- ? newState.own_follows.concat(event.follow)
4186
+ newState.own_follows = currentState.own_follows
4187
+ ? currentState.own_follows.concat(event.follow)
4125
4188
  : [event.follow];
4126
4189
  }
4127
4190
  if (currentState.followers_pagination?.next === END_OF_LIST) {
4128
4191
  // TODO: respect sort
4129
- newState.followers = newState.followers
4130
- ? newState.followers.concat(event.follow)
4192
+ newState.followers = currentState.followers
4193
+ ? currentState.followers.concat(event.follow)
4131
4194
  : [event.follow];
4132
4195
  }
4133
4196
  return newState;
@@ -4153,9 +4216,9 @@ class Feed extends FeedApi {
4153
4216
  this.state.next((currentState) => {
4154
4217
  const newState = { ...currentState, ...event.follow.target_feed };
4155
4218
  if (source.created_by.id === connectedUser?.id) {
4156
- newState.own_follows = newState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4219
+ newState.own_follows = currentState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4157
4220
  }
4158
- newState.followers = newState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4221
+ newState.followers = currentState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4159
4222
  return newState;
4160
4223
  });
4161
4224
  }
@@ -4282,6 +4345,12 @@ class Feed extends FeedApi {
4282
4345
  };
4283
4346
  });
4284
4347
  }
4348
+ async synchronize() {
4349
+ const { last_get_or_create_request_config } = this.state.getLatestValue();
4350
+ if (last_get_or_create_request_config?.watch) {
4351
+ await this.getOrCreate(last_get_or_create_request_config);
4352
+ }
4353
+ }
4285
4354
  async getOrCreate(request) {
4286
4355
  if (this.currentState.is_loading_activities) {
4287
4356
  throw new Error('Only one getOrCreate call is allowed at a time');
@@ -4347,6 +4416,7 @@ class Feed extends FeedApi {
4347
4416
  if (!request?.following_pagination?.limit) {
4348
4417
  delete nextState.following;
4349
4418
  }
4419
+ nextState.last_get_or_create_request_config = request;
4350
4420
  return nextState;
4351
4421
  });
4352
4422
  }
@@ -4633,7 +4703,7 @@ class Feed extends FeedApi {
4633
4703
  }
4634
4704
  async getNextPage() {
4635
4705
  const currentState = this.currentState;
4636
- const response = await this.getOrCreate({
4706
+ return await this.getOrCreate({
4637
4707
  member_pagination: {
4638
4708
  limit: 0,
4639
4709
  },
@@ -4644,8 +4714,8 @@ class Feed extends FeedApi {
4644
4714
  limit: 0,
4645
4715
  },
4646
4716
  next: currentState.next,
4717
+ limit: currentState.last_get_or_create_request_config?.limit ?? 20,
4647
4718
  });
4648
- return response;
4649
4719
  }
4650
4720
  addActivity(request) {
4651
4721
  return this.feedsApi.addActivity({
@@ -5048,6 +5118,7 @@ class FeedsClient extends FeedsApi {
5048
5118
  super(apiClient);
5049
5119
  this.eventDispatcher = new EventDispatcher();
5050
5120
  this.activeFeeds = {};
5121
+ this.healthyConnectionChangedEventCount = 0;
5051
5122
  this.pollFromState = (id) => this.polls_by_id.get(id);
5052
5123
  this.connectUser = async (user, tokenProvider) => {
5053
5124
  if (this.state.getLatestValue().connectedUser !== undefined ||
@@ -5063,7 +5134,10 @@ class FeedsClient extends FeedsApi {
5063
5134
  }, this.tokenManager, this.connectionIdManager, [decodeWSEvent]);
5064
5135
  this.wsConnection.on('all', (event) => this.eventDispatcher.dispatch(event));
5065
5136
  const connectedEvent = await this.wsConnection.connect();
5066
- this.state.partialNext({ connectedUser: connectedEvent?.me });
5137
+ this.state.partialNext({
5138
+ connectedUser: connectedEvent?.me,
5139
+ isWsConnectionHealthy: this.wsConnection.isHealthy,
5140
+ });
5067
5141
  }
5068
5142
  catch (err) {
5069
5143
  await this.disconnectUser();
@@ -5118,13 +5192,20 @@ class FeedsClient extends FeedsApi {
5118
5192
  removeConnectionEventListeners(this.updateNetworkConnectionStatus);
5119
5193
  this.connectionIdManager.reset();
5120
5194
  this.tokenManager.reset();
5121
- this.state.partialNext({ connectedUser: undefined });
5195
+ this.state.partialNext({ connectedUser: undefined, isWsConnectionHealthy: false });
5122
5196
  };
5123
5197
  this.on = this.eventDispatcher.on;
5124
5198
  this.off = this.eventDispatcher.off;
5125
5199
  this.feed = (groupId, id) => {
5126
5200
  return this.getOrCreateActiveFeed(groupId, id);
5127
5201
  };
5202
+ this.updateNetworkConnectionStatus = (event) => {
5203
+ const networkEvent = {
5204
+ type: 'network.changed',
5205
+ online: event.type === 'online',
5206
+ };
5207
+ this.eventDispatcher.dispatch(networkEvent);
5208
+ };
5128
5209
  this.getOrCreateActiveFeed = (group, id, data) => {
5129
5210
  const fid = `${group}:${id}`;
5130
5211
  if (this.activeFeeds[fid]) {
@@ -5136,15 +5217,9 @@ class FeedsClient extends FeedsApi {
5136
5217
  return feed;
5137
5218
  }
5138
5219
  };
5139
- this.updateNetworkConnectionStatus = (event) => {
5140
- const networkEvent = {
5141
- type: 'network.changed',
5142
- online: event.type === 'online',
5143
- };
5144
- this.eventDispatcher.dispatch(networkEvent);
5145
- };
5146
5220
  this.state = new StateStore({
5147
5221
  connectedUser: undefined,
5222
+ isWsConnectionHealthy: false,
5148
5223
  });
5149
5224
  this.moderation = new ModerationClient(apiClient);
5150
5225
  this.tokenManager = tokenManager;
@@ -5154,6 +5229,20 @@ class FeedsClient extends FeedsApi {
5154
5229
  const fid = event.fid;
5155
5230
  const feed = typeof fid === 'string' ? this.activeFeeds[fid] : undefined;
5156
5231
  switch (event.type) {
5232
+ case 'connection.changed': {
5233
+ const { online } = event;
5234
+ this.state.partialNext({ isWsConnectionHealthy: online });
5235
+ if (online) {
5236
+ this.healthyConnectionChangedEventCount++;
5237
+ // we skip the first event as we could potentially be querying twice
5238
+ if (this.healthyConnectionChangedEventCount > 1) {
5239
+ for (const activeFeed of Object.values(this.activeFeeds)) {
5240
+ activeFeed.synchronize();
5241
+ }
5242
+ }
5243
+ }
5244
+ break;
5245
+ }
5157
5246
  case 'feeds.feed.created': {
5158
5247
  if (feed)
5159
5248
  break;