@stream-io/feeds-client 0.1.8 → 0.1.9

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 (40) hide show
  1. package/@react-bindings/hooks/util/index.ts +1 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  4. package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
  5. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
  6. package/dist/index-react-bindings.browser.cjs +346 -140
  7. package/dist/index-react-bindings.browser.cjs.map +1 -1
  8. package/dist/index-react-bindings.browser.js +346 -141
  9. package/dist/index-react-bindings.browser.js.map +1 -1
  10. package/dist/index-react-bindings.node.cjs +346 -140
  11. package/dist/index-react-bindings.node.cjs.map +1 -1
  12. package/dist/index-react-bindings.node.js +346 -141
  13. package/dist/index-react-bindings.node.js.map +1 -1
  14. package/dist/index.browser.cjs +320 -139
  15. package/dist/index.browser.cjs.map +1 -1
  16. package/dist/index.browser.js +320 -140
  17. package/dist/index.browser.js.map +1 -1
  18. package/dist/index.node.cjs +320 -139
  19. package/dist/index.node.cjs.map +1 -1
  20. package/dist/index.node.js +320 -140
  21. package/dist/index.node.js.map +1 -1
  22. package/dist/src/Feed.d.ts +40 -9
  23. package/dist/src/FeedsClient.d.ts +8 -1
  24. package/dist/src/gen-imports.d.ts +1 -1
  25. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  26. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  27. package/dist/src/utils.d.ts +1 -0
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +1 -1
  30. package/src/Feed.ts +226 -192
  31. package/src/FeedsClient.ts +75 -3
  32. package/src/gen-imports.ts +1 -1
  33. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  34. package/src/state-updates/activity-utils.test.ts +1 -0
  35. package/src/state-updates/follow-utils.test.ts +552 -0
  36. package/src/state-updates/follow-utils.ts +126 -0
  37. package/src/state-updates/state-update-queue.test.ts +53 -0
  38. package/src/state-updates/state-update-queue.ts +35 -0
  39. package/src/utils.test.ts +175 -0
  40. 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
  }
@@ -5816,7 +5996,7 @@ const selector = ({ own_follows }) => ({
5816
5996
  * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5817
5997
  * as the hook determines internally which APIs it is supposed to use, while taking the
5818
5998
  * correct ownCapabilities into account.
5819
- * @param entity - The entity to which we want to add a reaction, can be either ActivityResponse or CommentResponse.
5999
+ * @param entity - The entity to which we want to apply reaction actions, can be either ActivityResponse or CommentResponse.
5820
6000
  * @param type - The type of reaction we want to add or remove.
5821
6001
  */
5822
6002
  const useReactionActions = ({ entity, type, }) => {
@@ -5847,6 +6027,31 @@ const useReactionActions = ({ entity, type, }) => {
5847
6027
  return useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5848
6028
  };
5849
6029
 
6030
+ /**
6031
+ * A utility hook that takes in an entity and creates bookmark actions
6032
+ * that can then be used on the UI. The entity is expected to be an ActivityResponse.
6033
+ * @param entity - The entity to which we want to apply reaction actions, expects an ActivityResponse.
6034
+ */
6035
+ const useBookmarkActions = ({ entity, }) => {
6036
+ const client = useFeedsClient();
6037
+ const hasOwnBookmark = entity.own_bookmarks?.length > 0;
6038
+ const addBookmark = useStableCallback(async () => {
6039
+ await client?.addBookmark({ activity_id: entity.id });
6040
+ });
6041
+ const removeBookmark = useStableCallback(async () => {
6042
+ await client?.deleteBookmark({ activity_id: entity.id });
6043
+ });
6044
+ const toggleBookmark = useStableCallback(async () => {
6045
+ if (hasOwnBookmark) {
6046
+ await removeBookmark();
6047
+ }
6048
+ else {
6049
+ await addBookmark();
6050
+ }
6051
+ });
6052
+ return useMemo(() => ({ addBookmark, removeBookmark, toggleBookmark }), [addBookmark, removeBookmark, toggleBookmark]);
6053
+ };
6054
+
5850
6055
  const StreamFeeds = ({ client, children }) => {
5851
6056
  return (jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5852
6057
  };
@@ -5857,5 +6062,5 @@ const StreamFeed = ({ feed, children }) => {
5857
6062
  };
5858
6063
  StreamFeed.displayName = 'StreamFeed';
5859
6064
 
5860
- export { StreamFeed, StreamFeedContext, StreamFeeds, StreamFeedsContext, useClientConnectedUser, useComments, useCreateFeedsClient, useFeedActivities, useFeedContext, useFeedMetadata, useFeedsClient, useFollowers, useFollowing, useOwnCapabilities, useOwnFollows, useReactionActions, useStateStore, useWsConnectionState };
6065
+ export { StreamFeed, StreamFeedContext, StreamFeeds, StreamFeedsContext, useBookmarkActions, useClientConnectedUser, useComments, useCreateFeedsClient, useFeedActivities, useFeedContext, useFeedMetadata, useFeedsClient, useFollowers, useFollowing, useOwnCapabilities, useOwnFollows, useReactionActions, useStateStore, useWsConnectionState };
5861
6066
  //# sourceMappingURL=index-react-bindings.node.js.map