@stream-io/feeds-client 0.1.7 → 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 (46) hide show
  1. package/@react-bindings/hooks/util/index.ts +1 -0
  2. package/CHANGELOG.md +20 -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 +363 -141
  7. package/dist/index-react-bindings.browser.cjs.map +1 -1
  8. package/dist/index-react-bindings.browser.js +363 -142
  9. package/dist/index-react-bindings.browser.js.map +1 -1
  10. package/dist/index-react-bindings.node.cjs +363 -141
  11. package/dist/index-react-bindings.node.cjs.map +1 -1
  12. package/dist/index-react-bindings.node.js +363 -142
  13. package/dist/index-react-bindings.node.js.map +1 -1
  14. package/dist/index.browser.cjs +337 -140
  15. package/dist/index.browser.cjs.map +1 -1
  16. package/dist/index.browser.js +337 -141
  17. package/dist/index.browser.js.map +1 -1
  18. package/dist/index.node.cjs +337 -140
  19. package/dist/index.node.cjs.map +1 -1
  20. package/dist/index.node.js +337 -141
  21. package/dist/index.node.js.map +1 -1
  22. package/dist/src/Feed.d.ts +42 -11
  23. package/dist/src/FeedsClient.d.ts +10 -3
  24. package/dist/src/common/real-time/StableWSConnection.d.ts +3 -3
  25. package/dist/src/gen/models/index.d.ts +25 -2
  26. package/dist/src/gen-imports.d.ts +1 -1
  27. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  28. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  29. package/dist/src/utils.d.ts +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/src/Feed.ts +230 -192
  33. package/src/FeedsClient.ts +75 -3
  34. package/src/gen/feeds/FeedsApi.ts +0 -1
  35. package/src/gen/model-decoders/decoders.ts +16 -0
  36. package/src/gen/model-decoders/event-decoder-mapping.ts +3 -0
  37. package/src/gen/models/index.ts +42 -4
  38. package/src/gen-imports.ts +1 -1
  39. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  40. package/src/state-updates/activity-utils.test.ts +1 -0
  41. package/src/state-updates/follow-utils.test.ts +552 -0
  42. package/src/state-updates/follow-utils.ts +126 -0
  43. package/src/state-updates/state-update-queue.test.ts +53 -0
  44. package/src/state-updates/state-update-queue.ts +35 -0
  45. package/src/utils.test.ts +175 -0
  46. package/src/utils.ts +20 -0
@@ -1147,6 +1147,18 @@ decoders.MuteResponse = (input) => {
1147
1147
  };
1148
1148
  return decode(typeMappings, input);
1149
1149
  };
1150
+ decoders.NotificationFeedUpdatedEvent = (input) => {
1151
+ const typeMappings = {
1152
+ created_at: { type: 'DatetimeType', isSingle: true },
1153
+ received_at: { type: 'DatetimeType', isSingle: true },
1154
+ aggregated_activities: {
1155
+ type: 'AggregatedActivityResponse',
1156
+ isSingle: false,
1157
+ },
1158
+ notification_status: { type: 'NotificationStatusResponse', isSingle: true },
1159
+ };
1160
+ return decode(typeMappings, input);
1161
+ };
1150
1162
  decoders.NotificationStatusResponse = (input) => {
1151
1163
  const typeMappings = {
1152
1164
  last_seen_at: { type: 'DatetimeType', isSingle: true },
@@ -2622,7 +2634,6 @@ class FeedsApi {
2622
2634
  }
2623
2635
  async updateLiveLocation(request) {
2624
2636
  const body = {
2625
- created_by_device_id: request?.created_by_device_id,
2626
2637
  message_id: request?.message_id,
2627
2638
  end_at: request?.end_at,
2628
2639
  latitude: request?.latitude,
@@ -3706,6 +3717,7 @@ const eventDecoderMapping = {
3706
3717
  'feeds.follow.created': (data) => decoders.FollowCreatedEvent(data),
3707
3718
  'feeds.follow.deleted': (data) => decoders.FollowDeletedEvent(data),
3708
3719
  'feeds.follow.updated': (data) => decoders.FollowUpdatedEvent(data),
3720
+ 'feeds.notification_feed.updated': (data) => decoders.NotificationFeedUpdatedEvent(data),
3709
3721
  'feeds.poll.closed': (data) => decoders.PollClosedFeedEvent(data),
3710
3722
  'feeds.poll.deleted': (data) => decoders.PollDeletedFeedEvent(data),
3711
3723
  'feeds.poll.updated': (data) => decoders.PollUpdatedFeedEvent(data),
@@ -4038,6 +4050,89 @@ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4038
4050
  return updateActivityInActivities(updatedActivity, activities);
4039
4051
  };
4040
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
+
4041
4136
  const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4042
4137
  typeof cursor === 'string';
4043
4138
  const isCommentResponse = (entity) => {
@@ -4046,11 +4141,41 @@ const isCommentResponse = (entity) => {
4046
4141
  const Constants = {
4047
4142
  DEFAULT_COMMENT_PAGINATION: 'first',
4048
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
+ };
4049
4174
 
4050
4175
  class Feed extends FeedApi {
4051
- constructor(client, groupId, id, data) {
4052
- // Need this ugly cast because fileUpload endpoints :(
4176
+ constructor(client, groupId, id, data, watch = false) {
4053
4177
  super(client, groupId, id);
4178
+ this.stateUpdateQueue = new Set();
4054
4179
  this.eventHandlers = {
4055
4180
  'feeds.activity.added': (event) => {
4056
4181
  const currentActivities = this.currentState.activities;
@@ -4196,74 +4321,14 @@ class Feed extends FeedApi {
4196
4321
  'feeds.feed_group.changed': Feed.noop,
4197
4322
  'feeds.feed_group.deleted': Feed.noop,
4198
4323
  'feeds.follow.created': (event) => {
4199
- // filter non-accepted follows (the way getOrCreate does by default)
4200
- if (event.follow.status !== 'accepted')
4201
- return;
4202
- // this feed followed someone
4203
- if (event.follow.source_feed.fid === this.fid) {
4204
- this.state.next((currentState) => {
4205
- const newState = {
4206
- ...currentState,
4207
- ...event.follow.source_feed,
4208
- };
4209
- if (!checkHasAnotherPage(currentState.following, currentState.following_pagination?.next)) {
4210
- // TODO: respect sort
4211
- newState.following = currentState.following
4212
- ? currentState.following.concat(event.follow)
4213
- : [event.follow];
4214
- }
4215
- return newState;
4216
- });
4217
- }
4218
- else if (
4219
- // someone followed this feed
4220
- event.follow.target_feed.fid === this.fid) {
4221
- const source = event.follow.source_feed;
4222
- const connectedUser = this.client.state.getLatestValue().connected_user;
4223
- this.state.next((currentState) => {
4224
- const newState = { ...currentState, ...event.follow.target_feed };
4225
- if (source.created_by.id === connectedUser?.id) {
4226
- newState.own_follows = currentState.own_follows
4227
- ? currentState.own_follows.concat(event.follow)
4228
- : [event.follow];
4229
- }
4230
- if (!checkHasAnotherPage(currentState.followers, currentState.followers_pagination?.next)) {
4231
- // TODO: respect sort
4232
- newState.followers = currentState.followers
4233
- ? currentState.followers.concat(event.follow)
4234
- : [event.follow];
4235
- }
4236
- return newState;
4237
- });
4238
- }
4324
+ this.handleFollowCreated(event.follow);
4239
4325
  },
4240
4326
  'feeds.follow.deleted': (event) => {
4241
- // this feed unfollowed someone
4242
- if (event.follow.source_feed.fid === this.fid) {
4243
- this.state.next((currentState) => {
4244
- return {
4245
- ...currentState,
4246
- ...event.follow.source_feed,
4247
- following: currentState.following?.filter((follow) => follow.target_feed.fid !== event.follow.target_feed.fid),
4248
- };
4249
- });
4250
- }
4251
- else if (
4252
- // someone unfollowed this feed
4253
- event.follow.target_feed.fid === this.fid) {
4254
- const source = event.follow.source_feed;
4255
- const connectedUser = this.client.state.getLatestValue().connected_user;
4256
- this.state.next((currentState) => {
4257
- const newState = { ...currentState, ...event.follow.target_feed };
4258
- if (source.created_by.id === connectedUser?.id) {
4259
- newState.own_follows = currentState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4260
- }
4261
- newState.followers = currentState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4262
- return newState;
4263
- });
4264
- }
4327
+ this.handleFollowDeleted(event.follow);
4328
+ },
4329
+ 'feeds.follow.updated': (_event) => {
4330
+ handleFollowUpdated(this.currentState);
4265
4331
  },
4266
- 'feeds.follow.updated': Feed.noop,
4267
4332
  'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
4268
4333
  'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4269
4334
  'feeds.comment.reaction.updated': Feed.noop,
@@ -4271,13 +4336,11 @@ class Feed extends FeedApi {
4271
4336
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
4272
4337
  this.state.next((currentState) => {
4273
4338
  let newState;
4274
- if (!checkHasAnotherPage(currentState.members, currentState.member_pagination?.next)) {
4339
+ if (typeof currentState.members !== 'undefined') {
4275
4340
  newState ?? (newState = {
4276
4341
  ...currentState,
4277
4342
  });
4278
- newState.members = newState.members?.concat(event.member) ?? [
4279
- event.member,
4280
- ];
4343
+ newState.members = [event.member, ...currentState.members];
4281
4344
  }
4282
4345
  if (connectedUser?.id === event.member.user.id) {
4283
4346
  newState ?? (newState = {
@@ -4324,6 +4387,10 @@ class Feed extends FeedApi {
4324
4387
  return newState ?? currentState;
4325
4388
  });
4326
4389
  },
4390
+ 'feeds.notification_feed.updated': (event) => {
4391
+ console.info('notification feed updated', event);
4392
+ // TODO: handle notification feed updates
4393
+ },
4327
4394
  // the poll events should be removed from here
4328
4395
  'feeds.poll.closed': Feed.noop,
4329
4396
  'feeds.poll.deleted': Feed.noop,
@@ -4356,6 +4423,7 @@ class Feed extends FeedApi {
4356
4423
  is_loading: false,
4357
4424
  is_loading_activities: false,
4358
4425
  comments_by_entity_id: {},
4426
+ watch,
4359
4427
  });
4360
4428
  this.client = client;
4361
4429
  }
@@ -4435,6 +4503,8 @@ class Feed extends FeedApi {
4435
4503
  }
4436
4504
  }
4437
4505
  else {
4506
+ // Empty queue when reinitializing the state
4507
+ this.stateUpdateQueue.clear();
4438
4508
  const responseCopy = {
4439
4509
  ...response,
4440
4510
  ...response.feed,
@@ -4453,7 +4523,11 @@ class Feed extends FeedApi {
4453
4523
  if (!request?.following_pagination?.limit) {
4454
4524
  delete nextState.following;
4455
4525
  }
4526
+ if (response.members.length === 0 && response.feed.member_count > 0) {
4527
+ delete nextState.members;
4528
+ }
4456
4529
  nextState.last_get_or_create_request_config = request;
4530
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
4457
4531
  return nextState;
4458
4532
  });
4459
4533
  }
@@ -4467,6 +4541,56 @@ class Feed extends FeedApi {
4467
4541
  });
4468
4542
  }
4469
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
+ }
4470
4594
  handleBookmarkAdded(event) {
4471
4595
  const currentActivities = this.currentState.activities;
4472
4596
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
@@ -4511,70 +4635,85 @@ class Feed extends FeedApi {
4511
4635
  }
4512
4636
  return commentIndex;
4513
4637
  }
4514
- getActivityIndex(activity, state) {
4515
- const { activities } = state ?? this.currentState;
4516
- if (!activities) {
4517
- return -1;
4518
- }
4519
- let activityIndex = activities.indexOf(activity);
4520
- // fast lookup failed, try slower approach
4521
- if (activityIndex === -1) {
4522
- activityIndex = activities.findIndex((activity_) => activity_.id === activity.id);
4523
- }
4524
- return activityIndex;
4525
- }
4526
- 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
+ ];
4527
4652
  this.state.next((currentState) => {
4528
- const activityIndex = this.getActivityIndex(activity, currentState);
4529
- if (activityIndex === -1)
4530
- return currentState;
4531
- const nextActivities = [...currentState.activities];
4532
- 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
+ }
4533
4688
  return {
4534
4689
  ...currentState,
4535
- activities: nextActivities,
4690
+ comments_by_entity_id: newCommentsByEntityId,
4536
4691
  };
4537
4692
  });
4538
4693
  }
4539
- async loadNextPageComments({ forId, base, sort, parentId, }) {
4694
+ async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
4540
4695
  let error;
4541
4696
  try {
4542
4697
  this.state.next((currentState) => ({
4543
4698
  ...currentState,
4544
4699
  comments_by_entity_id: {
4545
4700
  ...currentState.comments_by_entity_id,
4546
- [forId]: {
4547
- ...currentState.comments_by_entity_id[forId],
4701
+ [entityId]: {
4702
+ ...currentState.comments_by_entity_id[entityId],
4548
4703
  pagination: {
4549
- ...currentState.comments_by_entity_id[forId]?.pagination,
4704
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
4550
4705
  loading_next_page: true,
4551
4706
  },
4552
4707
  },
4553
4708
  },
4554
4709
  }));
4555
- const { next: newNextCursor, comments } = await base();
4556
- this.state.next((currentState) => {
4557
- const newPagination = {
4558
- ...currentState.comments_by_entity_id[forId]?.pagination,
4559
- next: newNextCursor,
4560
- };
4561
- if (typeof newPagination.sort === 'undefined') {
4562
- newPagination.sort = sort;
4563
- }
4564
- return {
4565
- ...currentState,
4566
- comments_by_entity_id: {
4567
- ...currentState.comments_by_entity_id,
4568
- [forId]: {
4569
- ...currentState.comments_by_entity_id[forId],
4570
- parent_id: parentId,
4571
- pagination: newPagination,
4572
- comments: currentState.comments_by_entity_id[forId]?.comments
4573
- ? currentState.comments_by_entity_id[forId].comments?.concat(comments)
4574
- : comments,
4575
- },
4576
- },
4577
- };
4710
+ const { next, comments } = await base();
4711
+ this.loadCommentsIntoState({
4712
+ entityId,
4713
+ comments,
4714
+ entityParentId,
4715
+ next,
4716
+ sort,
4578
4717
  });
4579
4718
  }
4580
4719
  catch (e) {
@@ -4585,10 +4724,10 @@ class Feed extends FeedApi {
4585
4724
  ...currentState,
4586
4725
  comments_by_entity_id: {
4587
4726
  ...currentState.comments_by_entity_id,
4588
- [forId]: {
4589
- ...currentState.comments_by_entity_id[forId],
4727
+ [entityId]: {
4728
+ ...currentState.comments_by_entity_id[entityId],
4590
4729
  pagination: {
4591
- ...currentState.comments_by_entity_id[forId]?.pagination,
4730
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
4592
4731
  loading_next_page: false,
4593
4732
  },
4594
4733
  },
@@ -4611,7 +4750,7 @@ class Feed extends FeedApi {
4611
4750
  return;
4612
4751
  }
4613
4752
  await this.loadNextPageComments({
4614
- forId: activity.id,
4753
+ entityId: activity.id,
4615
4754
  base: () => this.client.getComments({
4616
4755
  ...request,
4617
4756
  sort,
@@ -4634,7 +4773,7 @@ class Feed extends FeedApi {
4634
4773
  return;
4635
4774
  }
4636
4775
  await this.loadNextPageComments({
4637
- forId: comment.id,
4776
+ entityId: comment.id,
4638
4777
  base: () => this.client.getCommentReplies({
4639
4778
  ...request,
4640
4779
  comment_id: comment.id,
@@ -4644,7 +4783,7 @@ class Feed extends FeedApi {
4644
4783
  Constants.DEFAULT_COMMENT_PAGINATION,
4645
4784
  next: currentNextCursor,
4646
4785
  }),
4647
- parentId: comment.parent_id ?? comment.object_id,
4786
+ entityParentId: comment.parent_id ?? comment.object_id,
4648
4787
  sort,
4649
4788
  });
4650
4789
  }
@@ -4674,17 +4813,19 @@ class Feed extends FeedApi {
4674
4813
  next: currentNextCursor,
4675
4814
  sort,
4676
4815
  });
4677
- this.state.next((currentState) => ({
4678
- ...currentState,
4679
- [type]: currentState[type]
4680
- ? currentState[type].concat(follows)
4681
- : follows,
4682
- [paginationKey]: {
4683
- ...currentState[paginationKey],
4684
- next: newNextCursor,
4685
- sort,
4686
- },
4687
- }));
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
+ });
4688
4829
  }
4689
4830
  catch (e) {
4690
4831
  error = e;
@@ -4737,7 +4878,7 @@ class Feed extends FeedApi {
4737
4878
  this.state.next((currentState) => ({
4738
4879
  ...currentState,
4739
4880
  members: currentState.members
4740
- ? currentState.members.concat(members)
4881
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
4741
4882
  : members,
4742
4883
  member_pagination: {
4743
4884
  ...currentState.member_pagination,
@@ -5324,13 +5465,17 @@ class FeedsClient extends FeedsApi {
5324
5465
  };
5325
5466
  this.eventDispatcher.dispatch(networkEvent);
5326
5467
  };
5327
- this.getOrCreateActiveFeed = (group, id, data) => {
5468
+ this.getOrCreateActiveFeed = (group, id, data, watch) => {
5328
5469
  const fid = `${group}:${id}`;
5329
5470
  if (this.activeFeeds[fid]) {
5330
- return this.activeFeeds[fid];
5471
+ const feed = this.activeFeeds[fid];
5472
+ if (watch && !feed.currentState.watch) {
5473
+ feed.handleWatchStarted();
5474
+ }
5475
+ return feed;
5331
5476
  }
5332
5477
  else {
5333
- const feed = new Feed(this, group, id, data);
5478
+ const feed = new Feed(this, group, id, data, watch);
5334
5479
  this.activeFeeds[fid] = feed;
5335
5480
  return feed;
5336
5481
  }
@@ -5359,6 +5504,11 @@ class FeedsClient extends FeedsApi {
5359
5504
  }
5360
5505
  }
5361
5506
  }
5507
+ else {
5508
+ for (const activeFeed of Object.values(this.activeFeeds)) {
5509
+ activeFeed.handleWatchStopped();
5510
+ }
5511
+ }
5362
5512
  break;
5363
5513
  }
5364
5514
  case 'feeds.feed.created': {
@@ -5462,7 +5612,7 @@ class FeedsClient extends FeedsApi {
5462
5612
  }
5463
5613
  async queryFeeds(request) {
5464
5614
  const response = await this.feedsQueryFeeds(request);
5465
- 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));
5466
5616
  return {
5467
5617
  feeds,
5468
5618
  next: response.next,
@@ -5471,6 +5621,52 @@ class FeedsClient extends FeedsApi {
5471
5621
  duration: response.duration,
5472
5622
  };
5473
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
+ }
5474
5670
  findActiveFeedByActivityId(activityId) {
5475
5671
  return Object.values(this.activeFeeds).filter((feed) => feed.currentState.activities?.some((activity) => activity.id === activityId));
5476
5672
  }
@@ -5802,7 +5998,7 @@ const selector = ({ own_follows }) => ({
5802
5998
  * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5803
5999
  * as the hook determines internally which APIs it is supposed to use, while taking the
5804
6000
  * correct ownCapabilities into account.
5805
- * @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.
5806
6002
  * @param type - The type of reaction we want to add or remove.
5807
6003
  */
5808
6004
  const useReactionActions = ({ entity, type, }) => {
@@ -5833,6 +6029,31 @@ const useReactionActions = ({ entity, type, }) => {
5833
6029
  return react.useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5834
6030
  };
5835
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
+
5836
6057
  const StreamFeeds = ({ client, children }) => {
5837
6058
  return (jsxRuntime.jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5838
6059
  };
@@ -5847,6 +6068,7 @@ exports.StreamFeed = StreamFeed;
5847
6068
  exports.StreamFeedContext = StreamFeedContext;
5848
6069
  exports.StreamFeeds = StreamFeeds;
5849
6070
  exports.StreamFeedsContext = StreamFeedsContext;
6071
+ exports.useBookmarkActions = useBookmarkActions;
5850
6072
  exports.useClientConnectedUser = useClientConnectedUser;
5851
6073
  exports.useComments = useComments;
5852
6074
  exports.useCreateFeedsClient = useCreateFeedsClient;