@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
@@ -1145,6 +1145,18 @@ decoders.MuteResponse = (input) => {
1145
1145
  };
1146
1146
  return decode(typeMappings, input);
1147
1147
  };
1148
+ decoders.NotificationFeedUpdatedEvent = (input) => {
1149
+ const typeMappings = {
1150
+ created_at: { type: 'DatetimeType', isSingle: true },
1151
+ received_at: { type: 'DatetimeType', isSingle: true },
1152
+ aggregated_activities: {
1153
+ type: 'AggregatedActivityResponse',
1154
+ isSingle: false,
1155
+ },
1156
+ notification_status: { type: 'NotificationStatusResponse', isSingle: true },
1157
+ };
1158
+ return decode(typeMappings, input);
1159
+ };
1148
1160
  decoders.NotificationStatusResponse = (input) => {
1149
1161
  const typeMappings = {
1150
1162
  last_seen_at: { type: 'DatetimeType', isSingle: true },
@@ -2620,7 +2632,6 @@ class FeedsApi {
2620
2632
  }
2621
2633
  async updateLiveLocation(request) {
2622
2634
  const body = {
2623
- created_by_device_id: request?.created_by_device_id,
2624
2635
  message_id: request?.message_id,
2625
2636
  end_at: request?.end_at,
2626
2637
  latitude: request?.latitude,
@@ -3704,6 +3715,7 @@ const eventDecoderMapping = {
3704
3715
  'feeds.follow.created': (data) => decoders.FollowCreatedEvent(data),
3705
3716
  'feeds.follow.deleted': (data) => decoders.FollowDeletedEvent(data),
3706
3717
  'feeds.follow.updated': (data) => decoders.FollowUpdatedEvent(data),
3718
+ 'feeds.notification_feed.updated': (data) => decoders.NotificationFeedUpdatedEvent(data),
3707
3719
  'feeds.poll.closed': (data) => decoders.PollClosedFeedEvent(data),
3708
3720
  'feeds.poll.deleted': (data) => decoders.PollDeletedFeedEvent(data),
3709
3721
  'feeds.poll.updated': (data) => decoders.PollUpdatedFeedEvent(data),
@@ -4036,6 +4048,89 @@ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4036
4048
  return updateActivityInActivities(updatedActivity, activities);
4037
4049
  };
4038
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
+
4039
4134
  const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4040
4135
  typeof cursor === 'string';
4041
4136
  const isCommentResponse = (entity) => {
@@ -4044,11 +4139,41 @@ const isCommentResponse = (entity) => {
4044
4139
  const Constants = {
4045
4140
  DEFAULT_COMMENT_PAGINATION: 'first',
4046
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
+ };
4047
4172
 
4048
4173
  class Feed extends FeedApi {
4049
- constructor(client, groupId, id, data) {
4050
- // Need this ugly cast because fileUpload endpoints :(
4174
+ constructor(client, groupId, id, data, watch = false) {
4051
4175
  super(client, groupId, id);
4176
+ this.stateUpdateQueue = new Set();
4052
4177
  this.eventHandlers = {
4053
4178
  'feeds.activity.added': (event) => {
4054
4179
  const currentActivities = this.currentState.activities;
@@ -4194,74 +4319,14 @@ class Feed extends FeedApi {
4194
4319
  'feeds.feed_group.changed': Feed.noop,
4195
4320
  'feeds.feed_group.deleted': Feed.noop,
4196
4321
  'feeds.follow.created': (event) => {
4197
- // filter non-accepted follows (the way getOrCreate does by default)
4198
- if (event.follow.status !== 'accepted')
4199
- return;
4200
- // this feed followed someone
4201
- if (event.follow.source_feed.fid === this.fid) {
4202
- this.state.next((currentState) => {
4203
- const newState = {
4204
- ...currentState,
4205
- ...event.follow.source_feed,
4206
- };
4207
- if (!checkHasAnotherPage(currentState.following, currentState.following_pagination?.next)) {
4208
- // TODO: respect sort
4209
- newState.following = currentState.following
4210
- ? currentState.following.concat(event.follow)
4211
- : [event.follow];
4212
- }
4213
- return newState;
4214
- });
4215
- }
4216
- else if (
4217
- // someone followed this feed
4218
- event.follow.target_feed.fid === this.fid) {
4219
- const source = event.follow.source_feed;
4220
- const connectedUser = this.client.state.getLatestValue().connected_user;
4221
- this.state.next((currentState) => {
4222
- const newState = { ...currentState, ...event.follow.target_feed };
4223
- if (source.created_by.id === connectedUser?.id) {
4224
- newState.own_follows = currentState.own_follows
4225
- ? currentState.own_follows.concat(event.follow)
4226
- : [event.follow];
4227
- }
4228
- if (!checkHasAnotherPage(currentState.followers, currentState.followers_pagination?.next)) {
4229
- // TODO: respect sort
4230
- newState.followers = currentState.followers
4231
- ? currentState.followers.concat(event.follow)
4232
- : [event.follow];
4233
- }
4234
- return newState;
4235
- });
4236
- }
4322
+ this.handleFollowCreated(event.follow);
4237
4323
  },
4238
4324
  'feeds.follow.deleted': (event) => {
4239
- // this feed unfollowed someone
4240
- if (event.follow.source_feed.fid === this.fid) {
4241
- this.state.next((currentState) => {
4242
- return {
4243
- ...currentState,
4244
- ...event.follow.source_feed,
4245
- following: currentState.following?.filter((follow) => follow.target_feed.fid !== event.follow.target_feed.fid),
4246
- };
4247
- });
4248
- }
4249
- else if (
4250
- // someone unfollowed this feed
4251
- event.follow.target_feed.fid === this.fid) {
4252
- const source = event.follow.source_feed;
4253
- const connectedUser = this.client.state.getLatestValue().connected_user;
4254
- this.state.next((currentState) => {
4255
- const newState = { ...currentState, ...event.follow.target_feed };
4256
- if (source.created_by.id === connectedUser?.id) {
4257
- newState.own_follows = currentState.own_follows?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4258
- }
4259
- newState.followers = currentState.followers?.filter((follow) => follow.source_feed.fid !== event.follow.source_feed.fid);
4260
- return newState;
4261
- });
4262
- }
4325
+ this.handleFollowDeleted(event.follow);
4326
+ },
4327
+ 'feeds.follow.updated': (_event) => {
4328
+ handleFollowUpdated(this.currentState);
4263
4329
  },
4264
- 'feeds.follow.updated': Feed.noop,
4265
4330
  'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
4266
4331
  'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4267
4332
  'feeds.comment.reaction.updated': Feed.noop,
@@ -4269,13 +4334,11 @@ class Feed extends FeedApi {
4269
4334
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
4270
4335
  this.state.next((currentState) => {
4271
4336
  let newState;
4272
- if (!checkHasAnotherPage(currentState.members, currentState.member_pagination?.next)) {
4337
+ if (typeof currentState.members !== 'undefined') {
4273
4338
  newState ?? (newState = {
4274
4339
  ...currentState,
4275
4340
  });
4276
- newState.members = newState.members?.concat(event.member) ?? [
4277
- event.member,
4278
- ];
4341
+ newState.members = [event.member, ...currentState.members];
4279
4342
  }
4280
4343
  if (connectedUser?.id === event.member.user.id) {
4281
4344
  newState ?? (newState = {
@@ -4322,6 +4385,10 @@ class Feed extends FeedApi {
4322
4385
  return newState ?? currentState;
4323
4386
  });
4324
4387
  },
4388
+ 'feeds.notification_feed.updated': (event) => {
4389
+ console.info('notification feed updated', event);
4390
+ // TODO: handle notification feed updates
4391
+ },
4325
4392
  // the poll events should be removed from here
4326
4393
  'feeds.poll.closed': Feed.noop,
4327
4394
  'feeds.poll.deleted': Feed.noop,
@@ -4354,6 +4421,7 @@ class Feed extends FeedApi {
4354
4421
  is_loading: false,
4355
4422
  is_loading_activities: false,
4356
4423
  comments_by_entity_id: {},
4424
+ watch,
4357
4425
  });
4358
4426
  this.client = client;
4359
4427
  }
@@ -4433,6 +4501,8 @@ class Feed extends FeedApi {
4433
4501
  }
4434
4502
  }
4435
4503
  else {
4504
+ // Empty queue when reinitializing the state
4505
+ this.stateUpdateQueue.clear();
4436
4506
  const responseCopy = {
4437
4507
  ...response,
4438
4508
  ...response.feed,
@@ -4451,7 +4521,11 @@ class Feed extends FeedApi {
4451
4521
  if (!request?.following_pagination?.limit) {
4452
4522
  delete nextState.following;
4453
4523
  }
4524
+ if (response.members.length === 0 && response.feed.member_count > 0) {
4525
+ delete nextState.members;
4526
+ }
4454
4527
  nextState.last_get_or_create_request_config = request;
4528
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
4455
4529
  return nextState;
4456
4530
  });
4457
4531
  }
@@ -4465,6 +4539,56 @@ class Feed extends FeedApi {
4465
4539
  });
4466
4540
  }
4467
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
+ }
4468
4592
  handleBookmarkAdded(event) {
4469
4593
  const currentActivities = this.currentState.activities;
4470
4594
  const { connected_user: connectedUser } = this.client.state.getLatestValue();
@@ -4509,70 +4633,85 @@ class Feed extends FeedApi {
4509
4633
  }
4510
4634
  return commentIndex;
4511
4635
  }
4512
- getActivityIndex(activity, state) {
4513
- const { activities } = state ?? this.currentState;
4514
- if (!activities) {
4515
- return -1;
4516
- }
4517
- let activityIndex = activities.indexOf(activity);
4518
- // fast lookup failed, try slower approach
4519
- if (activityIndex === -1) {
4520
- activityIndex = activities.findIndex((activity_) => activity_.id === activity.id);
4521
- }
4522
- return activityIndex;
4523
- }
4524
- 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
+ ];
4525
4650
  this.state.next((currentState) => {
4526
- const activityIndex = this.getActivityIndex(activity, currentState);
4527
- if (activityIndex === -1)
4528
- return currentState;
4529
- const nextActivities = [...currentState.activities];
4530
- 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
+ }
4531
4686
  return {
4532
4687
  ...currentState,
4533
- activities: nextActivities,
4688
+ comments_by_entity_id: newCommentsByEntityId,
4534
4689
  };
4535
4690
  });
4536
4691
  }
4537
- async loadNextPageComments({ forId, base, sort, parentId, }) {
4692
+ async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
4538
4693
  let error;
4539
4694
  try {
4540
4695
  this.state.next((currentState) => ({
4541
4696
  ...currentState,
4542
4697
  comments_by_entity_id: {
4543
4698
  ...currentState.comments_by_entity_id,
4544
- [forId]: {
4545
- ...currentState.comments_by_entity_id[forId],
4699
+ [entityId]: {
4700
+ ...currentState.comments_by_entity_id[entityId],
4546
4701
  pagination: {
4547
- ...currentState.comments_by_entity_id[forId]?.pagination,
4702
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
4548
4703
  loading_next_page: true,
4549
4704
  },
4550
4705
  },
4551
4706
  },
4552
4707
  }));
4553
- const { next: newNextCursor, comments } = await base();
4554
- this.state.next((currentState) => {
4555
- const newPagination = {
4556
- ...currentState.comments_by_entity_id[forId]?.pagination,
4557
- next: newNextCursor,
4558
- };
4559
- if (typeof newPagination.sort === 'undefined') {
4560
- newPagination.sort = sort;
4561
- }
4562
- return {
4563
- ...currentState,
4564
- comments_by_entity_id: {
4565
- ...currentState.comments_by_entity_id,
4566
- [forId]: {
4567
- ...currentState.comments_by_entity_id[forId],
4568
- parent_id: parentId,
4569
- pagination: newPagination,
4570
- comments: currentState.comments_by_entity_id[forId]?.comments
4571
- ? currentState.comments_by_entity_id[forId].comments?.concat(comments)
4572
- : comments,
4573
- },
4574
- },
4575
- };
4708
+ const { next, comments } = await base();
4709
+ this.loadCommentsIntoState({
4710
+ entityId,
4711
+ comments,
4712
+ entityParentId,
4713
+ next,
4714
+ sort,
4576
4715
  });
4577
4716
  }
4578
4717
  catch (e) {
@@ -4583,10 +4722,10 @@ class Feed extends FeedApi {
4583
4722
  ...currentState,
4584
4723
  comments_by_entity_id: {
4585
4724
  ...currentState.comments_by_entity_id,
4586
- [forId]: {
4587
- ...currentState.comments_by_entity_id[forId],
4725
+ [entityId]: {
4726
+ ...currentState.comments_by_entity_id[entityId],
4588
4727
  pagination: {
4589
- ...currentState.comments_by_entity_id[forId]?.pagination,
4728
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
4590
4729
  loading_next_page: false,
4591
4730
  },
4592
4731
  },
@@ -4609,7 +4748,7 @@ class Feed extends FeedApi {
4609
4748
  return;
4610
4749
  }
4611
4750
  await this.loadNextPageComments({
4612
- forId: activity.id,
4751
+ entityId: activity.id,
4613
4752
  base: () => this.client.getComments({
4614
4753
  ...request,
4615
4754
  sort,
@@ -4632,7 +4771,7 @@ class Feed extends FeedApi {
4632
4771
  return;
4633
4772
  }
4634
4773
  await this.loadNextPageComments({
4635
- forId: comment.id,
4774
+ entityId: comment.id,
4636
4775
  base: () => this.client.getCommentReplies({
4637
4776
  ...request,
4638
4777
  comment_id: comment.id,
@@ -4642,7 +4781,7 @@ class Feed extends FeedApi {
4642
4781
  Constants.DEFAULT_COMMENT_PAGINATION,
4643
4782
  next: currentNextCursor,
4644
4783
  }),
4645
- parentId: comment.parent_id ?? comment.object_id,
4784
+ entityParentId: comment.parent_id ?? comment.object_id,
4646
4785
  sort,
4647
4786
  });
4648
4787
  }
@@ -4672,17 +4811,19 @@ class Feed extends FeedApi {
4672
4811
  next: currentNextCursor,
4673
4812
  sort,
4674
4813
  });
4675
- this.state.next((currentState) => ({
4676
- ...currentState,
4677
- [type]: currentState[type]
4678
- ? currentState[type].concat(follows)
4679
- : follows,
4680
- [paginationKey]: {
4681
- ...currentState[paginationKey],
4682
- next: newNextCursor,
4683
- sort,
4684
- },
4685
- }));
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
+ });
4686
4827
  }
4687
4828
  catch (e) {
4688
4829
  error = e;
@@ -4735,7 +4876,7 @@ class Feed extends FeedApi {
4735
4876
  this.state.next((currentState) => ({
4736
4877
  ...currentState,
4737
4878
  members: currentState.members
4738
- ? currentState.members.concat(members)
4879
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
4739
4880
  : members,
4740
4881
  member_pagination: {
4741
4882
  ...currentState.member_pagination,
@@ -5322,13 +5463,17 @@ class FeedsClient extends FeedsApi {
5322
5463
  };
5323
5464
  this.eventDispatcher.dispatch(networkEvent);
5324
5465
  };
5325
- this.getOrCreateActiveFeed = (group, id, data) => {
5466
+ this.getOrCreateActiveFeed = (group, id, data, watch) => {
5326
5467
  const fid = `${group}:${id}`;
5327
5468
  if (this.activeFeeds[fid]) {
5328
- return this.activeFeeds[fid];
5469
+ const feed = this.activeFeeds[fid];
5470
+ if (watch && !feed.currentState.watch) {
5471
+ feed.handleWatchStarted();
5472
+ }
5473
+ return feed;
5329
5474
  }
5330
5475
  else {
5331
- const feed = new Feed(this, group, id, data);
5476
+ const feed = new Feed(this, group, id, data, watch);
5332
5477
  this.activeFeeds[fid] = feed;
5333
5478
  return feed;
5334
5479
  }
@@ -5357,6 +5502,11 @@ class FeedsClient extends FeedsApi {
5357
5502
  }
5358
5503
  }
5359
5504
  }
5505
+ else {
5506
+ for (const activeFeed of Object.values(this.activeFeeds)) {
5507
+ activeFeed.handleWatchStopped();
5508
+ }
5509
+ }
5360
5510
  break;
5361
5511
  }
5362
5512
  case 'feeds.feed.created': {
@@ -5460,7 +5610,7 @@ class FeedsClient extends FeedsApi {
5460
5610
  }
5461
5611
  async queryFeeds(request) {
5462
5612
  const response = await this.feedsQueryFeeds(request);
5463
- 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));
5464
5614
  return {
5465
5615
  feeds,
5466
5616
  next: response.next,
@@ -5469,6 +5619,52 @@ class FeedsClient extends FeedsApi {
5469
5619
  duration: response.duration,
5470
5620
  };
5471
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
+ }
5472
5668
  findActiveFeedByActivityId(activityId) {
5473
5669
  return Object.values(this.activeFeeds).filter((feed) => feed.currentState.activities?.some((activity) => activity.id === activityId));
5474
5670
  }
@@ -5800,7 +5996,7 @@ const selector = ({ own_follows }) => ({
5800
5996
  * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5801
5997
  * as the hook determines internally which APIs it is supposed to use, while taking the
5802
5998
  * correct ownCapabilities into account.
5803
- * @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.
5804
6000
  * @param type - The type of reaction we want to add or remove.
5805
6001
  */
5806
6002
  const useReactionActions = ({ entity, type, }) => {
@@ -5831,6 +6027,31 @@ const useReactionActions = ({ entity, type, }) => {
5831
6027
  return useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5832
6028
  };
5833
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
+
5834
6055
  const StreamFeeds = ({ client, children }) => {
5835
6056
  return (jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5836
6057
  };
@@ -5841,5 +6062,5 @@ const StreamFeed = ({ feed, children }) => {
5841
6062
  };
5842
6063
  StreamFeed.displayName = 'StreamFeed';
5843
6064
 
5844
- 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 };
5845
6066
  //# sourceMappingURL=index-react-bindings.node.js.map