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