@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
@@ -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
  }
@@ -5818,7 +5998,7 @@ const selector = ({ own_follows }) => ({
5818
5998
  * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5819
5999
  * as the hook determines internally which APIs it is supposed to use, while taking the
5820
6000
  * correct ownCapabilities into account.
5821
- * @param entity - The entity to which we want to add a reaction, can be either ActivityResponse or CommentResponse.
6001
+ * @param entity - The entity to which we want to apply reaction actions, can be either ActivityResponse or CommentResponse.
5822
6002
  * @param type - The type of reaction we want to add or remove.
5823
6003
  */
5824
6004
  const useReactionActions = ({ entity, type, }) => {
@@ -5849,6 +6029,31 @@ const useReactionActions = ({ entity, type, }) => {
5849
6029
  return react.useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5850
6030
  };
5851
6031
 
6032
+ /**
6033
+ * A utility hook that takes in an entity and creates bookmark actions
6034
+ * that can then be used on the UI. The entity is expected to be an ActivityResponse.
6035
+ * @param entity - The entity to which we want to apply reaction actions, expects an ActivityResponse.
6036
+ */
6037
+ const useBookmarkActions = ({ entity, }) => {
6038
+ const client = useFeedsClient();
6039
+ const hasOwnBookmark = entity.own_bookmarks?.length > 0;
6040
+ const addBookmark = useStableCallback(async () => {
6041
+ await client?.addBookmark({ activity_id: entity.id });
6042
+ });
6043
+ const removeBookmark = useStableCallback(async () => {
6044
+ await client?.deleteBookmark({ activity_id: entity.id });
6045
+ });
6046
+ const toggleBookmark = useStableCallback(async () => {
6047
+ if (hasOwnBookmark) {
6048
+ await removeBookmark();
6049
+ }
6050
+ else {
6051
+ await addBookmark();
6052
+ }
6053
+ });
6054
+ return react.useMemo(() => ({ addBookmark, removeBookmark, toggleBookmark }), [addBookmark, removeBookmark, toggleBookmark]);
6055
+ };
6056
+
5852
6057
  const StreamFeeds = ({ client, children }) => {
5853
6058
  return (jsxRuntime.jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5854
6059
  };
@@ -5863,6 +6068,7 @@ exports.StreamFeed = StreamFeed;
5863
6068
  exports.StreamFeedContext = StreamFeedContext;
5864
6069
  exports.StreamFeeds = StreamFeeds;
5865
6070
  exports.StreamFeedsContext = StreamFeedsContext;
6071
+ exports.useBookmarkActions = useBookmarkActions;
5866
6072
  exports.useClientConnectedUser = useClientConnectedUser;
5867
6073
  exports.useComments = useComments;
5868
6074
  exports.useCreateFeedsClient = useCreateFeedsClient;