@stream-io/feeds-client 0.1.8 → 0.1.10

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 (59) hide show
  1. package/@react-bindings/hooks/search-state-hooks/index.ts +3 -0
  2. package/@react-bindings/hooks/util/index.ts +1 -0
  3. package/@react-bindings/index.ts +5 -0
  4. package/CHANGELOG.md +22 -0
  5. package/dist/@react-bindings/contexts/StreamSearchContext.d.ts +12 -0
  6. package/dist/@react-bindings/contexts/StreamSearchResultsContext.d.ts +12 -0
  7. package/dist/@react-bindings/hooks/search-state-hooks/index.d.ts +3 -0
  8. package/dist/@react-bindings/hooks/search-state-hooks/useSearchQuery.d.ts +4 -0
  9. package/dist/@react-bindings/hooks/search-state-hooks/useSearchResult.d.ts +8 -0
  10. package/dist/@react-bindings/hooks/search-state-hooks/useSearchSources.d.ts +4 -0
  11. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  12. package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
  13. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
  14. package/dist/@react-bindings/index.d.ts +5 -0
  15. package/dist/@react-bindings/wrappers/StreamSearch.d.ts +12 -0
  16. package/dist/@react-bindings/wrappers/StreamSearchResults.d.ts +12 -0
  17. package/dist/index-react-bindings.browser.cjs +431 -156
  18. package/dist/index-react-bindings.browser.cjs.map +1 -1
  19. package/dist/index-react-bindings.browser.js +422 -157
  20. package/dist/index-react-bindings.browser.js.map +1 -1
  21. package/dist/index-react-bindings.node.cjs +431 -156
  22. package/dist/index-react-bindings.node.cjs.map +1 -1
  23. package/dist/index-react-bindings.node.js +422 -157
  24. package/dist/index-react-bindings.node.js.map +1 -1
  25. package/dist/index.browser.cjs +346 -264
  26. package/dist/index.browser.cjs.map +1 -1
  27. package/dist/index.browser.js +346 -265
  28. package/dist/index.browser.js.map +1 -1
  29. package/dist/index.node.cjs +346 -264
  30. package/dist/index.node.cjs.map +1 -1
  31. package/dist/index.node.js +346 -265
  32. package/dist/index.node.js.map +1 -1
  33. package/dist/src/Feed.d.ts +40 -9
  34. package/dist/src/FeedsClient.d.ts +8 -1
  35. package/dist/src/common/BaseSearchSource.d.ts +3 -1
  36. package/dist/src/common/FeedSearchSource.d.ts +5 -1
  37. package/dist/src/common/SearchController.d.ts +2 -0
  38. package/dist/src/gen-imports.d.ts +1 -1
  39. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  40. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  41. package/dist/src/utils.d.ts +1 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/Feed.ts +226 -192
  45. package/src/FeedsClient.ts +75 -3
  46. package/src/common/ActivitySearchSource.ts +5 -15
  47. package/src/common/BaseSearchSource.ts +9 -9
  48. package/src/common/FeedSearchSource.ts +20 -65
  49. package/src/common/SearchController.ts +2 -0
  50. package/src/common/UserSearchSource.ts +9 -61
  51. package/src/gen-imports.ts +1 -1
  52. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  53. package/src/state-updates/activity-utils.test.ts +1 -0
  54. package/src/state-updates/follow-utils.test.ts +552 -0
  55. package/src/state-updates/follow-utils.ts +126 -0
  56. package/src/state-updates/state-update-queue.test.ts +53 -0
  57. package/src/state-updates/state-update-queue.ts +35 -0
  58. package/src/utils.test.ts +175 -0
  59. package/src/utils.ts +20 -0
@@ -4050,6 +4050,89 @@ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4050
4050
  return updateActivityInActivities(updatedActivity, activities);
4051
4051
  };
4052
4052
 
4053
+ const isFeedResponse = (follow) => {
4054
+ return 'created_by' in follow;
4055
+ };
4056
+ const handleFollowCreated = (follow, currentState, currentFeedId, connectedUserId) => {
4057
+ // filter non-accepted follows (the way getOrCreate does by default)
4058
+ if (follow.status !== 'accepted') {
4059
+ return { changed: false, data: currentState };
4060
+ }
4061
+ let newState = { ...currentState };
4062
+ // this feed followed someone
4063
+ if (follow.source_feed.fid === currentFeedId) {
4064
+ newState = {
4065
+ ...newState,
4066
+ // Update FeedResponse fields, that has the new follower/following count
4067
+ ...follow.source_feed,
4068
+ };
4069
+ // Only update if following array already exists
4070
+ if (currentState.following !== undefined) {
4071
+ newState.following = [follow, ...currentState.following];
4072
+ }
4073
+ }
4074
+ else if (
4075
+ // someone followed this feed
4076
+ follow.target_feed.fid === currentFeedId) {
4077
+ const source = follow.source_feed;
4078
+ newState = {
4079
+ ...newState,
4080
+ // Update FeedResponse fields, that has the new follower/following count
4081
+ ...follow.target_feed,
4082
+ };
4083
+ if (source.created_by.id === connectedUserId) {
4084
+ newState.own_follows = currentState.own_follows
4085
+ ? currentState.own_follows.concat(follow)
4086
+ : [follow];
4087
+ }
4088
+ // Only update if followers array already exists
4089
+ if (currentState.followers !== undefined) {
4090
+ newState.followers = [follow, ...currentState.followers];
4091
+ }
4092
+ }
4093
+ return { changed: true, data: newState };
4094
+ };
4095
+ const handleFollowDeleted = (follow, currentState, currentFeedId, connectedUserId) => {
4096
+ let newState = { ...currentState };
4097
+ // this feed unfollowed someone
4098
+ if (follow.source_feed.fid === currentFeedId) {
4099
+ newState = {
4100
+ ...newState,
4101
+ // Update FeedResponse fields, that has the new follower/following count
4102
+ ...follow.source_feed,
4103
+ };
4104
+ // Only update if following array already exists
4105
+ if (currentState.following !== undefined) {
4106
+ newState.following = currentState.following.filter((followItem) => followItem.target_feed.fid !== follow.target_feed.fid);
4107
+ }
4108
+ }
4109
+ else if (
4110
+ // someone unfollowed this feed
4111
+ follow.target_feed.fid === currentFeedId) {
4112
+ const source = follow.source_feed;
4113
+ newState = {
4114
+ ...newState,
4115
+ // Update FeedResponse fields, that has the new follower/following count
4116
+ ...follow.target_feed,
4117
+ };
4118
+ if (isFeedResponse(source) &&
4119
+ source.created_by.id === connectedUserId &&
4120
+ currentState.own_follows !== undefined) {
4121
+ newState.own_follows = currentState.own_follows.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4122
+ }
4123
+ // Only update if followers array already exists
4124
+ if (currentState.followers !== undefined) {
4125
+ newState.followers = currentState.followers.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4126
+ }
4127
+ }
4128
+ return { changed: true, data: newState };
4129
+ };
4130
+ const handleFollowUpdated = (currentState) => {
4131
+ // For now, we'll treat follow updates as no-ops since the current implementation does
4132
+ // This can be enhanced later if needed
4133
+ return { changed: false, data: currentState };
4134
+ };
4135
+
4053
4136
  const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4054
4137
  typeof cursor === 'string';
4055
4138
  const isCommentResponse = (entity) => {
@@ -4058,11 +4141,41 @@ const isCommentResponse = (entity) => {
4058
4141
  const Constants = {
4059
4142
  DEFAULT_COMMENT_PAGINATION: 'first',
4060
4143
  };
4144
+ const uniqueArrayMerge = (existingArray, arrayToMerge, getKey) => {
4145
+ const existing = new Set();
4146
+ existingArray.forEach((value) => {
4147
+ const key = getKey(value);
4148
+ existing.add(key);
4149
+ });
4150
+ const filteredArrayToMerge = arrayToMerge.filter((value) => {
4151
+ const key = getKey(value);
4152
+ return !existing.has(key);
4153
+ });
4154
+ return existingArray.concat(filteredArrayToMerge);
4155
+ };
4156
+
4157
+ const shouldUpdateState = ({ stateUpdateId, stateUpdateQueue, watch, }) => {
4158
+ if (!watch) {
4159
+ return true;
4160
+ }
4161
+ if (watch && stateUpdateQueue.has(stateUpdateId)) {
4162
+ stateUpdateQueue.delete(stateUpdateId);
4163
+ return false;
4164
+ }
4165
+ stateUpdateQueue.add(stateUpdateId);
4166
+ return true;
4167
+ };
4168
+ const getStateUpdateQueueIdForFollow = (follow) => {
4169
+ return `follow${follow.source_feed.fid}-${follow.target_feed.fid}`;
4170
+ };
4171
+ const getStateUpdateQueueIdForUnfollow = (follow) => {
4172
+ return `unfollow${follow.source_feed.fid}-${follow.target_feed.fid}`;
4173
+ };
4061
4174
 
4062
4175
  class Feed extends FeedApi {
4063
- constructor(client, groupId, id, data) {
4064
- // Need this ugly cast because fileUpload endpoints :(
4176
+ constructor(client, groupId, id, data, watch = false) {
4065
4177
  super(client, groupId, id);
4178
+ this.stateUpdateQueue = new Set();
4066
4179
  this.eventHandlers = {
4067
4180
  'feeds.activity.added': (event) => {
4068
4181
  const currentActivities = this.currentState.activities;
@@ -4208,74 +4321,14 @@ class Feed extends FeedApi {
4208
4321
  'feeds.feed_group.changed': Feed.noop,
4209
4322
  'feeds.feed_group.deleted': Feed.noop,
4210
4323
  'feeds.follow.created': (event) => {
4211
- // filter non-accepted follows (the way getOrCreate does by default)
4212
- if (event.follow.status !== 'accepted')
4213
- return;
4214
- // this feed followed someone
4215
- if (event.follow.source_feed.fid === this.fid) {
4216
- this.state.next((currentState) => {
4217
- const newState = {
4218
- ...currentState,
4219
- ...event.follow.source_feed,
4220
- };
4221
- if (!checkHasAnotherPage(currentState.following, currentState.following_pagination?.next)) {
4222
- // TODO: respect sort
4223
- newState.following = currentState.following
4224
- ? currentState.following.concat(event.follow)
4225
- : [event.follow];
4226
- }
4227
- return newState;
4228
- });
4229
- }
4230
- else if (
4231
- // someone followed this feed
4232
- event.follow.target_feed.fid === this.fid) {
4233
- const source = event.follow.source_feed;
4234
- const connectedUser = this.client.state.getLatestValue().connected_user;
4235
- this.state.next((currentState) => {
4236
- const newState = { ...currentState, ...event.follow.target_feed };
4237
- if (source.created_by.id === connectedUser?.id) {
4238
- newState.own_follows = currentState.own_follows
4239
- ? currentState.own_follows.concat(event.follow)
4240
- : [event.follow];
4241
- }
4242
- if (!checkHasAnotherPage(currentState.followers, currentState.followers_pagination?.next)) {
4243
- // TODO: respect sort
4244
- newState.followers = currentState.followers
4245
- ? currentState.followers.concat(event.follow)
4246
- : [event.follow];
4247
- }
4248
- return newState;
4249
- });
4250
- }
4324
+ this.handleFollowCreated(event.follow);
4251
4325
  },
4252
4326
  'feeds.follow.deleted': (event) => {
4253
- // this feed unfollowed someone
4254
- if (event.follow.source_feed.fid === this.fid) {
4255
- this.state.next((currentState) => {
4256
- return {
4257
- ...currentState,
4258
- ...event.follow.source_feed,
4259
- following: currentState.following?.filter((follow) => follow.target_feed.fid !== event.follow.target_feed.fid),
4260
- };
4261
- });
4262
- }
4263
- else if (
4264
- // someone unfollowed this feed
4265
- event.follow.target_feed.fid === this.fid) {
4266
- const source = event.follow.source_feed;
4267
- const connectedUser = this.client.state.getLatestValue().connected_user;
4268
- this.state.next((currentState) => {
4269
- const newState = { ...currentState, ...event.follow.target_feed };
4270
- if (source.created_by.id === connectedUser?.id) {
4271
- newState.own_follows = currentState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4272
- }
4273
- newState.followers = currentState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4274
- return newState;
4275
- });
4276
- }
4327
+ this.handleFollowDeleted(event.follow);
4328
+ },
4329
+ 'feeds.follow.updated': (_event) => {
4330
+ handleFollowUpdated(this.currentState);
4277
4331
  },
4278
- 'feeds.follow.updated': Feed.noop,
4279
4332
  'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
4280
4333
  'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4281
4334
  'feeds.comment.reaction.updated': Feed.noop,
@@ -4283,13 +4336,11 @@ class Feed extends FeedApi {
4283
4336
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
4284
4337
  this.state.next((currentState) => {
4285
4338
  let newState;
4286
- if (!checkHasAnotherPage(currentState.members, currentState.member_pagination?.next)) {
4339
+ if (typeof currentState.members !== 'undefined') {
4287
4340
  newState ?? (newState = {
4288
4341
  ...currentState,
4289
4342
  });
4290
- newState.members = newState.members?.concat(event.member) ?? [
4291
- event.member,
4292
- ];
4343
+ newState.members = [event.member, ...currentState.members];
4293
4344
  }
4294
4345
  if (connectedUser?.id === event.member.user.id) {
4295
4346
  newState ?? (newState = {
@@ -4372,6 +4423,7 @@ class Feed extends FeedApi {
4372
4423
  is_loading: false,
4373
4424
  is_loading_activities: false,
4374
4425
  comments_by_entity_id: {},
4426
+ watch,
4375
4427
  });
4376
4428
  this.client = client;
4377
4429
  }
@@ -4451,6 +4503,8 @@ class Feed extends FeedApi {
4451
4503
  }
4452
4504
  }
4453
4505
  else {
4506
+ // Empty queue when reinitializing the state
4507
+ this.stateUpdateQueue.clear();
4454
4508
  const responseCopy = {
4455
4509
  ...response,
4456
4510
  ...response.feed,
@@ -4469,7 +4523,11 @@ class Feed extends FeedApi {
4469
4523
  if (!request?.following_pagination?.limit) {
4470
4524
  delete nextState.following;
4471
4525
  }
4526
+ if (response.members.length === 0 && response.feed.member_count > 0) {
4527
+ delete nextState.members;
4528
+ }
4472
4529
  nextState.last_get_or_create_request_config = request;
4530
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
4473
4531
  return nextState;
4474
4532
  });
4475
4533
  }
@@ -4483,6 +4541,56 @@ class Feed extends FeedApi {
4483
4541
  });
4484
4542
  }
4485
4543
  }
4544
+ /**
4545
+ * @internal
4546
+ */
4547
+ handleFollowCreated(follow) {
4548
+ if (!shouldUpdateState({
4549
+ stateUpdateId: getStateUpdateQueueIdForFollow(follow),
4550
+ stateUpdateQueue: this.stateUpdateQueue,
4551
+ watch: this.currentState.watch,
4552
+ })) {
4553
+ return;
4554
+ }
4555
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4556
+ const result = handleFollowCreated(follow, this.currentState, this.fid, connectedUser?.id);
4557
+ if (result.changed) {
4558
+ this.state.next(result.data);
4559
+ }
4560
+ }
4561
+ /**
4562
+ * @internal
4563
+ */
4564
+ handleFollowDeleted(follow) {
4565
+ if (!shouldUpdateState({
4566
+ stateUpdateId: getStateUpdateQueueIdForUnfollow(follow),
4567
+ stateUpdateQueue: this.stateUpdateQueue,
4568
+ watch: this.currentState.watch,
4569
+ })) {
4570
+ return;
4571
+ }
4572
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4573
+ const result = handleFollowDeleted(follow, this.currentState, this.fid, connectedUser?.id);
4574
+ {
4575
+ this.state.next(result.data);
4576
+ }
4577
+ }
4578
+ /**
4579
+ * @internal
4580
+ */
4581
+ handleWatchStopped() {
4582
+ this.state.partialNext({
4583
+ watch: false,
4584
+ });
4585
+ }
4586
+ /**
4587
+ * @internal
4588
+ */
4589
+ handleWatchStarted() {
4590
+ this.state.partialNext({
4591
+ watch: true,
4592
+ });
4593
+ }
4486
4594
  handleBookmarkAdded(event) {
4487
4595
  const currentActivities = this.currentState.activities;
4488
4596
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
@@ -4527,70 +4635,85 @@ class Feed extends FeedApi {
4527
4635
  }
4528
4636
  return commentIndex;
4529
4637
  }
4530
- getActivityIndex(activity, state) {
4531
- const { activities } = state ?? this.currentState;
4532
- if (!activities) {
4533
- return -1;
4534
- }
4535
- let activityIndex = activities.indexOf(activity);
4536
- // fast lookup failed, try slower approach
4537
- if (activityIndex === -1) {
4538
- activityIndex = activities.findIndex((activity_) => activity_.id === activity.id);
4539
- }
4540
- return activityIndex;
4541
- }
4542
- updateActivityInState(activity, patch) {
4638
+ /**
4639
+ * Load child comments of entity (activity or comment) into the state, if the target entity is comment,
4640
+ * `entityParentId` should be provided (`CommentResponse.parent_id ?? CommentResponse.object_id`).
4641
+ */
4642
+ loadCommentsIntoState(data) {
4643
+ // add initial (top level) object for processing
4644
+ const traverseArray = [
4645
+ {
4646
+ entityId: data.entityId,
4647
+ entityParentId: data.entityParentId,
4648
+ comments: data.comments,
4649
+ next: data.next,
4650
+ },
4651
+ ];
4543
4652
  this.state.next((currentState) => {
4544
- const activityIndex = this.getActivityIndex(activity, currentState);
4545
- if (activityIndex === -1)
4546
- return currentState;
4547
- const nextActivities = [...currentState.activities];
4548
- nextActivities[activityIndex] = patch(currentState.activities[activityIndex]);
4653
+ const newCommentsByEntityId = {
4654
+ ...currentState.comments_by_entity_id,
4655
+ };
4656
+ while (traverseArray.length) {
4657
+ const item = traverseArray.pop();
4658
+ const entityId = item.entityId;
4659
+ // go over entity comments and generate new objects
4660
+ // for further processing if there are any replies
4661
+ item.comments.forEach((comment) => {
4662
+ if (!comment.replies?.length)
4663
+ return;
4664
+ traverseArray.push({
4665
+ entityId: comment.id,
4666
+ entityParentId: entityId,
4667
+ comments: comment.replies,
4668
+ next: comment.meta?.next_cursor,
4669
+ });
4670
+ });
4671
+ // omit replies & meta from the comments (transform ThreadedCommentResponse to CommentResponse)
4672
+ // this is somehow faster than copying the whole
4673
+ // object and deleting the desired properties
4674
+ const newComments = item.comments.map(({ replies: _r, meta: _m, ...restOfTheCommentResponse }) => restOfTheCommentResponse);
4675
+ newCommentsByEntityId[entityId] = {
4676
+ ...newCommentsByEntityId[entityId],
4677
+ entity_parent_id: item.entityParentId,
4678
+ pagination: {
4679
+ ...newCommentsByEntityId[entityId]?.pagination,
4680
+ next: item.next,
4681
+ sort: data.sort,
4682
+ },
4683
+ comments: newCommentsByEntityId[entityId]?.comments
4684
+ ? newCommentsByEntityId[entityId].comments?.concat(newComments)
4685
+ : newComments,
4686
+ };
4687
+ }
4549
4688
  return {
4550
4689
  ...currentState,
4551
- activities: nextActivities,
4690
+ comments_by_entity_id: newCommentsByEntityId,
4552
4691
  };
4553
4692
  });
4554
4693
  }
4555
- async loadNextPageComments({ forId, base, sort, parentId, }) {
4694
+ async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
4556
4695
  let error;
4557
4696
  try {
4558
4697
  this.state.next((currentState) => ({
4559
4698
  ...currentState,
4560
4699
  comments_by_entity_id: {
4561
4700
  ...currentState.comments_by_entity_id,
4562
- [forId]: {
4563
- ...currentState.comments_by_entity_id[forId],
4701
+ [entityId]: {
4702
+ ...currentState.comments_by_entity_id[entityId],
4564
4703
  pagination: {
4565
- ...currentState.comments_by_entity_id[forId]?.pagination,
4704
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
4566
4705
  loading_next_page: true,
4567
4706
  },
4568
4707
  },
4569
4708
  },
4570
4709
  }));
4571
- const { next: newNextCursor, comments } = await base();
4572
- this.state.next((currentState) => {
4573
- const newPagination = {
4574
- ...currentState.comments_by_entity_id[forId]?.pagination,
4575
- next: newNextCursor,
4576
- };
4577
- if (typeof newPagination.sort === 'undefined') {
4578
- newPagination.sort = sort;
4579
- }
4580
- return {
4581
- ...currentState,
4582
- comments_by_entity_id: {
4583
- ...currentState.comments_by_entity_id,
4584
- [forId]: {
4585
- ...currentState.comments_by_entity_id[forId],
4586
- parent_id: parentId,
4587
- pagination: newPagination,
4588
- comments: currentState.comments_by_entity_id[forId]?.comments
4589
- ? currentState.comments_by_entity_id[forId].comments?.concat(comments)
4590
- : comments,
4591
- },
4592
- },
4593
- };
4710
+ const { next, comments } = await base();
4711
+ this.loadCommentsIntoState({
4712
+ entityId,
4713
+ comments,
4714
+ entityParentId,
4715
+ next,
4716
+ sort,
4594
4717
  });
4595
4718
  }
4596
4719
  catch (e) {
@@ -4601,10 +4724,10 @@ class Feed extends FeedApi {
4601
4724
  ...currentState,
4602
4725
  comments_by_entity_id: {
4603
4726
  ...currentState.comments_by_entity_id,
4604
- [forId]: {
4605
- ...currentState.comments_by_entity_id[forId],
4727
+ [entityId]: {
4728
+ ...currentState.comments_by_entity_id[entityId],
4606
4729
  pagination: {
4607
- ...currentState.comments_by_entity_id[forId]?.pagination,
4730
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
4608
4731
  loading_next_page: false,
4609
4732
  },
4610
4733
  },
@@ -4627,7 +4750,7 @@ class Feed extends FeedApi {
4627
4750
  return;
4628
4751
  }
4629
4752
  await this.loadNextPageComments({
4630
- forId: activity.id,
4753
+ entityId: activity.id,
4631
4754
  base: () => this.client.getComments({
4632
4755
  ...request,
4633
4756
  sort,
@@ -4650,7 +4773,7 @@ class Feed extends FeedApi {
4650
4773
  return;
4651
4774
  }
4652
4775
  await this.loadNextPageComments({
4653
- forId: comment.id,
4776
+ entityId: comment.id,
4654
4777
  base: () => this.client.getCommentReplies({
4655
4778
  ...request,
4656
4779
  comment_id: comment.id,
@@ -4660,7 +4783,7 @@ class Feed extends FeedApi {
4660
4783
  Constants.DEFAULT_COMMENT_PAGINATION,
4661
4784
  next: currentNextCursor,
4662
4785
  }),
4663
- parentId: comment.parent_id ?? comment.object_id,
4786
+ entityParentId: comment.parent_id ?? comment.object_id,
4664
4787
  sort,
4665
4788
  });
4666
4789
  }
@@ -4690,17 +4813,19 @@ class Feed extends FeedApi {
4690
4813
  next: currentNextCursor,
4691
4814
  sort,
4692
4815
  });
4693
- this.state.next((currentState) => ({
4694
- ...currentState,
4695
- [type]: currentState[type]
4696
- ? currentState[type].concat(follows)
4697
- : follows,
4698
- [paginationKey]: {
4699
- ...currentState[paginationKey],
4700
- next: newNextCursor,
4701
- sort,
4702
- },
4703
- }));
4816
+ this.state.next((currentState) => {
4817
+ return {
4818
+ ...currentState,
4819
+ [type]: currentState[type] === undefined
4820
+ ? follows
4821
+ : uniqueArrayMerge(currentState[type], follows, (follow) => `${follow.source_feed.fid}-${follow.target_feed.fid}`),
4822
+ [paginationKey]: {
4823
+ ...currentState[paginationKey],
4824
+ next: newNextCursor,
4825
+ sort,
4826
+ },
4827
+ };
4828
+ });
4704
4829
  }
4705
4830
  catch (e) {
4706
4831
  error = e;
@@ -4753,7 +4878,7 @@ class Feed extends FeedApi {
4753
4878
  this.state.next((currentState) => ({
4754
4879
  ...currentState,
4755
4880
  members: currentState.members
4756
- ? currentState.members.concat(members)
4881
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
4757
4882
  : members,
4758
4883
  member_pagination: {
4759
4884
  ...currentState.member_pagination,
@@ -5340,13 +5465,17 @@ class FeedsClient extends FeedsApi {
5340
5465
  };
5341
5466
  this.eventDispatcher.dispatch(networkEvent);
5342
5467
  };
5343
- this.getOrCreateActiveFeed = (group, id, data) => {
5468
+ this.getOrCreateActiveFeed = (group, id, data, watch) => {
5344
5469
  const fid = `${group}:${id}`;
5345
5470
  if (this.activeFeeds[fid]) {
5346
- return this.activeFeeds[fid];
5471
+ const feed = this.activeFeeds[fid];
5472
+ if (watch && !feed.currentState.watch) {
5473
+ feed.handleWatchStarted();
5474
+ }
5475
+ return feed;
5347
5476
  }
5348
5477
  else {
5349
- const feed = new Feed(this, group, id, data);
5478
+ const feed = new Feed(this, group, id, data, watch);
5350
5479
  this.activeFeeds[fid] = feed;
5351
5480
  return feed;
5352
5481
  }
@@ -5375,6 +5504,11 @@ class FeedsClient extends FeedsApi {
5375
5504
  }
5376
5505
  }
5377
5506
  }
5507
+ else {
5508
+ for (const activeFeed of Object.values(this.activeFeeds)) {
5509
+ activeFeed.handleWatchStopped();
5510
+ }
5511
+ }
5378
5512
  break;
5379
5513
  }
5380
5514
  case 'feeds.feed.created': {
@@ -5478,7 +5612,7 @@ class FeedsClient extends FeedsApi {
5478
5612
  }
5479
5613
  async queryFeeds(request) {
5480
5614
  const response = await this.feedsQueryFeeds(request);
5481
- const feeds = response.feeds.map((f) => this.getOrCreateActiveFeed(f.group_id, f.id, f));
5615
+ const feeds = response.feeds.map((f) => this.getOrCreateActiveFeed(f.group_id, f.id, f, request?.watch));
5482
5616
  return {
5483
5617
  feeds,
5484
5618
  next: response.next,
@@ -5487,6 +5621,52 @@ class FeedsClient extends FeedsApi {
5487
5621
  duration: response.duration,
5488
5622
  };
5489
5623
  }
5624
+ // For follow API endpoints we update the state after HTTP response to allow queryFeeds with watch: false
5625
+ async follow(request) {
5626
+ const response = await super.follow(request);
5627
+ [response.follow.source_feed.fid, response.follow.target_feed.fid].forEach((fid) => {
5628
+ const feed = this.activeFeeds[fid];
5629
+ if (feed) {
5630
+ feed.handleFollowCreated(response.follow);
5631
+ }
5632
+ });
5633
+ return response;
5634
+ }
5635
+ async followBatch(request) {
5636
+ const response = await super.followBatch(request);
5637
+ response.follows.forEach((follow) => {
5638
+ const feed = this.activeFeeds[follow.source_feed.fid];
5639
+ if (feed) {
5640
+ feed.handleFollowCreated(follow);
5641
+ }
5642
+ });
5643
+ return response;
5644
+ }
5645
+ async unfollow(request) {
5646
+ const response = await super.unfollow(request);
5647
+ [request.source, request.target].forEach((fid) => {
5648
+ const feed = this.activeFeeds[fid];
5649
+ if (feed) {
5650
+ feed.handleFollowDeleted({
5651
+ source_feed: { fid: request.source },
5652
+ target_feed: { fid: request.target },
5653
+ });
5654
+ }
5655
+ });
5656
+ return response;
5657
+ }
5658
+ async stopWatchingFeed(request) {
5659
+ const connectionId = await this.connectionIdManager.getConnectionId();
5660
+ const response = await super.stopWatchingFeed({
5661
+ ...request,
5662
+ connection_id: connectionId,
5663
+ });
5664
+ const feed = this.activeFeeds[`${request.feed_group_id}:${request.feed_id}`];
5665
+ if (feed) {
5666
+ feed.handleWatchStopped();
5667
+ }
5668
+ return response;
5669
+ }
5490
5670
  findActiveFeedByActivityId(activityId) {
5491
5671
  return Object.values(this.activeFeeds).filter((feed) => feed.currentState.activities?.some((activity) => activity.id === activityId));
5492
5672
  }
@@ -5549,10 +5729,10 @@ const useFeedsClient = () => {
5549
5729
  */
5550
5730
  const useClientConnectedUser = () => {
5551
5731
  const client = useFeedsClient();
5552
- const { user } = useStateStore(client?.state, selector$7) ?? {};
5732
+ const { user } = useStateStore(client?.state, selector$a) ?? {};
5553
5733
  return user;
5554
5734
  };
5555
- const selector$7 = (nextState) => ({
5735
+ const selector$a = (nextState) => ({
5556
5736
  user: nextState.connected_user,
5557
5737
  });
5558
5738
 
@@ -5561,10 +5741,10 @@ const selector$7 = (nextState) => ({
5561
5741
  */
5562
5742
  const useWsConnectionState = () => {
5563
5743
  const client = useFeedsClient();
5564
- const { is_healthy } = useStateStore(client?.state, selector$6) ?? {};
5744
+ const { is_healthy } = useStateStore(client?.state, selector$9) ?? {};
5565
5745
  return { is_healthy };
5566
5746
  };
5567
- const selector$6 = (nextState) => ({
5747
+ const selector$9 = (nextState) => ({
5568
5748
  is_healthy: nextState.is_ws_connection_healthy,
5569
5749
  });
5570
5750
 
@@ -5614,7 +5794,7 @@ const useStableCallback = (callback) => {
5614
5794
  const useFeedActivities = (feedFromProps) => {
5615
5795
  const feedFromContext = useFeedContext();
5616
5796
  const feed = feedFromProps ?? feedFromContext;
5617
- const data = useStateStore(feed?.state, selector$5);
5797
+ const data = useStateStore(feed?.state, selector$8);
5618
5798
  const loadNextPage = useStableCallback(async () => {
5619
5799
  if (!feed || !data?.has_next_page || data?.is_loading) {
5620
5800
  return;
@@ -5623,7 +5803,7 @@ const useFeedActivities = (feedFromProps) => {
5623
5803
  });
5624
5804
  return react.useMemo(() => ({ ...data, loadNextPage }), [data, loadNextPage]);
5625
5805
  };
5626
- const selector$5 = ({ is_loading_activities, next, activities = [] }) => ({
5806
+ const selector$8 = ({ is_loading_activities, next, activities = [] }) => ({
5627
5807
  is_loading: is_loading_activities,
5628
5808
  has_next_page: typeof next !== 'undefined',
5629
5809
  activities,
@@ -5696,13 +5876,13 @@ const FeedOwnCapability = {
5696
5876
  };
5697
5877
 
5698
5878
  const stableEmptyArray = [];
5699
- const selector$4 = (currentState) => ({
5879
+ const selector$7 = (currentState) => ({
5700
5880
  oc: currentState.own_capabilities ?? stableEmptyArray,
5701
5881
  });
5702
5882
  const useOwnCapabilities = (feedFromProps) => {
5703
5883
  const feedFromContext = useFeedContext();
5704
5884
  const feed = feedFromProps ?? feedFromContext;
5705
- const { oc = stableEmptyArray } = useStateStore(feed?.state, selector$4) ?? {};
5885
+ const { oc = stableEmptyArray } = useStateStore(feed?.state, selector$7) ?? {};
5706
5886
  return react.useMemo(() => ({
5707
5887
  can_add_activity: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY) > -1,
5708
5888
  can_add_activity_reaction: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY_REACTION) > -1,
@@ -5737,7 +5917,7 @@ const useOwnCapabilities = (feedFromProps) => {
5737
5917
  }), [oc]);
5738
5918
  };
5739
5919
 
5740
- const selector$3 = ({ follower_count, followers, followers_pagination, }) => ({
5920
+ const selector$6 = ({ follower_count, followers, followers_pagination, }) => ({
5741
5921
  follower_count,
5742
5922
  followers,
5743
5923
  followers_pagination,
@@ -5745,7 +5925,7 @@ const selector$3 = ({ follower_count, followers, followers_pagination, }) => ({
5745
5925
  function useFollowers(feedFromProps) {
5746
5926
  const feedFromContext = useFeedContext();
5747
5927
  const feed = feedFromProps ?? feedFromContext;
5748
- const data = useStateStore(feed?.state, selector$3);
5928
+ const data = useStateStore(feed?.state, selector$6);
5749
5929
  const loadNextPage = react.useCallback((...options) => feed?.loadNextPageFollowers(...options), [feed]);
5750
5930
  return react.useMemo(() => {
5751
5931
  if (!data) {
@@ -5760,7 +5940,7 @@ function useFollowers(feedFromProps) {
5760
5940
  }, [data, loadNextPage]);
5761
5941
  }
5762
5942
 
5763
- const selector$2 = ({ following_count, following, following_pagination, }) => ({
5943
+ const selector$5 = ({ following_count, following, following_pagination, }) => ({
5764
5944
  following_count,
5765
5945
  following,
5766
5946
  following_pagination,
@@ -5768,7 +5948,7 @@ const selector$2 = ({ following_count, following, following_pagination, }) => ({
5768
5948
  function useFollowing(feedFromProps) {
5769
5949
  const feedFromContext = useFeedContext();
5770
5950
  const feed = feedFromProps ?? feedFromContext;
5771
- const data = useStateStore(feed?.state, selector$2);
5951
+ const data = useStateStore(feed?.state, selector$5);
5772
5952
  const loadNextPage = react.useCallback((...options) => feed?.loadNextPageFollowing(...options), [feed]);
5773
5953
  return react.useMemo(() => {
5774
5954
  if (!data) {
@@ -5790,9 +5970,9 @@ function useFollowing(feedFromProps) {
5790
5970
  const useFeedMetadata = (feedFromProps) => {
5791
5971
  const feedFromContext = useFeedContext();
5792
5972
  const feed = feedFromProps ?? feedFromContext;
5793
- return useStateStore(feed?.state, selector$1);
5973
+ return useStateStore(feed?.state, selector$4);
5794
5974
  };
5795
- const selector$1 = ({ follower_count = 0, following_count = 0, created_by, created_at, updated_at, }) => ({
5975
+ const selector$4 = ({ follower_count = 0, following_count = 0, created_by, created_at, updated_at, }) => ({
5796
5976
  created_by,
5797
5977
  follower_count,
5798
5978
  following_count,
@@ -5807,18 +5987,68 @@ const selector$1 = ({ follower_count = 0, following_count = 0, created_by, creat
5807
5987
  const useOwnFollows = (feedFromProps) => {
5808
5988
  const feedFromContext = useFeedContext();
5809
5989
  const feed = feedFromProps ?? feedFromContext;
5810
- return useStateStore(feed?.state, selector);
5990
+ return useStateStore(feed?.state, selector$3);
5811
5991
  };
5812
- const selector = ({ own_follows }) => ({
5992
+ const selector$3 = ({ own_follows }) => ({
5813
5993
  own_follows,
5814
5994
  });
5815
5995
 
5996
+ const StreamSearchResultsContext = react.createContext(undefined);
5997
+ /**
5998
+ * Hook to access the nearest SearchSource instance.
5999
+ */
6000
+ const useSearchResultsContext = () => {
6001
+ return react.useContext(StreamSearchResultsContext);
6002
+ };
6003
+
6004
+ const useSearchResult = (sourceFromProps) => {
6005
+ const sourceFromContext = useSearchResultsContext();
6006
+ const source = sourceFromProps ?? sourceFromContext;
6007
+ const { items, error, isLoading, hasNext } = useStateStore(source?.state, selector$2) ?? {};
6008
+ const loadMore = useStableCallback(async () => {
6009
+ source?.search();
6010
+ });
6011
+ return react.useMemo(() => ({ items, error, isLoading, hasNext, loadMore }), [error, hasNext, isLoading, items, loadMore]);
6012
+ };
6013
+ const selector$2 = ({ items, isLoading, hasNext, lastQueryError, }) => ({
6014
+ items,
6015
+ isLoading,
6016
+ hasNext,
6017
+ error: lastQueryError,
6018
+ });
6019
+
6020
+ const StreamSearchContext = react.createContext(undefined);
6021
+ /**
6022
+ * Hook to access the nearest SearchController instance.
6023
+ */
6024
+ const useSearchContext = () => {
6025
+ return react.useContext(StreamSearchContext);
6026
+ };
6027
+
6028
+ const useSearchQuery = (controllerFromProps) => {
6029
+ const controllerFromState = useSearchContext();
6030
+ const controller = controllerFromProps ?? controllerFromState;
6031
+ return useStateStore(controller?.state, selector$1);
6032
+ };
6033
+ const selector$1 = ({ searchQuery }) => ({
6034
+ searchQuery,
6035
+ });
6036
+
6037
+ const useSearchSources = (controllerFromProps) => {
6038
+ const controllerFromState = useSearchContext();
6039
+ const controller = controllerFromProps ?? controllerFromState;
6040
+ return useStateStore(controller?.state, selector);
6041
+ };
6042
+ const selector = ({ sources }) => ({
6043
+ sources,
6044
+ });
6045
+
5816
6046
  /**
5817
6047
  * A utility hook that takes in an entity and a reaction type, and creates reaction actions
5818
6048
  * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5819
6049
  * as the hook determines internally which APIs it is supposed to use, while taking the
5820
6050
  * correct ownCapabilities into account.
5821
- * @param entity - The entity to which we want to add a reaction, can be either ActivityResponse or CommentResponse.
6051
+ * @param entity - The entity to which we want to apply reaction actions, can be either ActivityResponse or CommentResponse.
5822
6052
  * @param type - The type of reaction we want to add or remove.
5823
6053
  */
5824
6054
  const useReactionActions = ({ entity, type, }) => {
@@ -5849,6 +6079,31 @@ const useReactionActions = ({ entity, type, }) => {
5849
6079
  return react.useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5850
6080
  };
5851
6081
 
6082
+ /**
6083
+ * A utility hook that takes in an entity and creates bookmark actions
6084
+ * that can then be used on the UI. The entity is expected to be an ActivityResponse.
6085
+ * @param entity - The entity to which we want to apply reaction actions, expects an ActivityResponse.
6086
+ */
6087
+ const useBookmarkActions = ({ entity, }) => {
6088
+ const client = useFeedsClient();
6089
+ const hasOwnBookmark = entity.own_bookmarks?.length > 0;
6090
+ const addBookmark = useStableCallback(async () => {
6091
+ await client?.addBookmark({ activity_id: entity.id });
6092
+ });
6093
+ const removeBookmark = useStableCallback(async () => {
6094
+ await client?.deleteBookmark({ activity_id: entity.id });
6095
+ });
6096
+ const toggleBookmark = useStableCallback(async () => {
6097
+ if (hasOwnBookmark) {
6098
+ await removeBookmark();
6099
+ }
6100
+ else {
6101
+ await addBookmark();
6102
+ }
6103
+ });
6104
+ return react.useMemo(() => ({ addBookmark, removeBookmark, toggleBookmark }), [addBookmark, removeBookmark, toggleBookmark]);
6105
+ };
6106
+
5852
6107
  const StreamFeeds = ({ client, children }) => {
5853
6108
  return (jsxRuntime.jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5854
6109
  };
@@ -5859,10 +6114,25 @@ const StreamFeed = ({ feed, children }) => {
5859
6114
  };
5860
6115
  StreamFeed.displayName = 'StreamFeed';
5861
6116
 
6117
+ const StreamSearch = ({ searchController, children, }) => {
6118
+ return (jsxRuntime.jsx(StreamSearchContext.Provider, { value: searchController, children: children }));
6119
+ };
6120
+ StreamSearch.displayName = 'StreamSearch';
6121
+
6122
+ const StreamSearchResults = ({ source, children, }) => {
6123
+ return (jsxRuntime.jsx(StreamSearchResultsContext.Provider, { value: source, children: children }));
6124
+ };
6125
+ StreamSearchResults.displayName = 'StreamSearchResults';
6126
+
5862
6127
  exports.StreamFeed = StreamFeed;
5863
6128
  exports.StreamFeedContext = StreamFeedContext;
5864
6129
  exports.StreamFeeds = StreamFeeds;
5865
6130
  exports.StreamFeedsContext = StreamFeedsContext;
6131
+ exports.StreamSearch = StreamSearch;
6132
+ exports.StreamSearchContext = StreamSearchContext;
6133
+ exports.StreamSearchResults = StreamSearchResults;
6134
+ exports.StreamSearchResultsContext = StreamSearchResultsContext;
6135
+ exports.useBookmarkActions = useBookmarkActions;
5866
6136
  exports.useClientConnectedUser = useClientConnectedUser;
5867
6137
  exports.useComments = useComments;
5868
6138
  exports.useCreateFeedsClient = useCreateFeedsClient;
@@ -5875,6 +6145,11 @@ exports.useFollowing = useFollowing;
5875
6145
  exports.useOwnCapabilities = useOwnCapabilities;
5876
6146
  exports.useOwnFollows = useOwnFollows;
5877
6147
  exports.useReactionActions = useReactionActions;
6148
+ exports.useSearchContext = useSearchContext;
6149
+ exports.useSearchQuery = useSearchQuery;
6150
+ exports.useSearchResult = useSearchResult;
6151
+ exports.useSearchResultsContext = useSearchResultsContext;
6152
+ exports.useSearchSources = useSearchSources;
5878
6153
  exports.useStateStore = useStateStore;
5879
6154
  exports.useWsConnectionState = useWsConnectionState;
5880
6155
  //# sourceMappingURL=index-react-bindings.node.cjs.map