@stream-io/feeds-client 0.1.4 → 0.1.6

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 (62) hide show
  1. package/@react-bindings/hooks/client-state-hooks/index.ts +2 -0
  2. package/@react-bindings/hooks/feed-state-hooks/index.ts +7 -0
  3. package/@react-bindings/hooks/internal/index.ts +1 -0
  4. package/@react-bindings/hooks/util/index.ts +1 -0
  5. package/@react-bindings/index.ts +6 -3
  6. package/CHANGELOG.md +31 -0
  7. package/dist/@react-bindings/contexts/StreamFeedContext.d.ts +12 -0
  8. package/dist/@react-bindings/hooks/client-state-hooks/index.d.ts +2 -0
  9. package/dist/@react-bindings/hooks/client-state-hooks/useClientConnectedUser.d.ts +4 -0
  10. package/dist/@react-bindings/hooks/client-state-hooks/useWsConnectionState.d.ts +6 -0
  11. package/dist/@react-bindings/hooks/feed-state-hooks/index.d.ts +7 -0
  12. package/dist/@react-bindings/hooks/feed-state-hooks/useComments.d.ts +19 -0
  13. package/dist/@react-bindings/hooks/feed-state-hooks/useFeedActivities.d.ts +11 -0
  14. package/dist/@react-bindings/hooks/feed-state-hooks/useFeedMetadata.d.ts +12 -0
  15. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowers.d.ts +16 -0
  16. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowing.d.ts +16 -0
  17. package/dist/@react-bindings/hooks/feed-state-hooks/useOwnCapabilities.d.ts +33 -0
  18. package/dist/@react-bindings/hooks/feed-state-hooks/useOwnFollows.d.ts +8 -0
  19. package/dist/@react-bindings/hooks/internal/index.d.ts +1 -0
  20. package/dist/@react-bindings/hooks/internal/useStableCallback.d.ts +25 -0
  21. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  22. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +17 -0
  23. package/dist/@react-bindings/index.d.ts +5 -3
  24. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +12 -0
  25. package/dist/index-react-bindings.browser.cjs +521 -210
  26. package/dist/index-react-bindings.browser.cjs.map +1 -1
  27. package/dist/index-react-bindings.browser.js +514 -212
  28. package/dist/index-react-bindings.browser.js.map +1 -1
  29. package/dist/index-react-bindings.node.cjs +521 -210
  30. package/dist/index-react-bindings.node.cjs.map +1 -1
  31. package/dist/index-react-bindings.node.js +514 -212
  32. package/dist/index-react-bindings.node.js.map +1 -1
  33. package/dist/index.browser.cjs +212 -106
  34. package/dist/index.browser.cjs.map +1 -1
  35. package/dist/index.browser.js +209 -107
  36. package/dist/index.browser.js.map +1 -1
  37. package/dist/index.node.cjs +212 -106
  38. package/dist/index.node.cjs.map +1 -1
  39. package/dist/index.node.js +209 -107
  40. package/dist/index.node.js.map +1 -1
  41. package/dist/src/Feed.d.ts +7 -3
  42. package/dist/src/FeedsClient.d.ts +6 -5
  43. package/dist/src/types.d.ts +7 -0
  44. package/dist/src/utils.d.ts +9 -1
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +6 -2
  47. package/src/Feed.ts +214 -97
  48. package/src/FeedsClient.ts +22 -12
  49. package/src/common/ActivitySearchSource.ts +5 -5
  50. package/src/common/ApiClient.ts +1 -1
  51. package/src/common/FeedSearchSource.ts +1 -1
  52. package/src/common/Poll.ts +35 -10
  53. package/src/common/TokenManager.ts +2 -3
  54. package/src/common/UserSearchSource.ts +1 -1
  55. package/src/common/real-time/StableWSConnection.ts +4 -1
  56. package/src/state-updates/bookmark-utils.test.ts +134 -8
  57. package/src/state-updates/bookmark-utils.ts +17 -7
  58. package/src/types.ts +12 -1
  59. package/src/utils.ts +25 -1
  60. package/dist/@react-bindings/hooks/clientStateHooks.d.ts +0 -10
  61. package/dist/@react-bindings/hooks/useComments.d.ts +0 -12
  62. package/dist/@react-bindings/hooks/useOwnCapabilities.d.ts +0 -33
@@ -275,122 +275,6 @@ function useStateStore(store, selector) {
275
275
  return state;
276
276
  }
277
277
 
278
- const useComments = (feed,
279
- /**
280
- * The parent (activity or comment) for which to fetch comments.
281
- */
282
- parent) => {
283
- const selector = react.useCallback((state) => ({
284
- comments: state.comments_by_entity_id?.[parent.id]?.comments ?? [],
285
- comment_pagination: state.comments_by_entity_id?.[parent.id]?.pagination,
286
- }), [parent.id]);
287
- return useStateStore(feed.state, selector);
288
- };
289
-
290
- const FeedOwnCapability = {
291
- ADD_ACTIVITY: 'add-activity',
292
- ADD_ACTIVITY_REACTION: 'add-activity-reaction',
293
- ADD_COMMENT: 'add-comment',
294
- ADD_COMMENT_REACTION: 'add-comment-reaction',
295
- BOOKMARK_ACTIVITY: 'bookmark-activity',
296
- CREATE_FEED: 'create-feed',
297
- DELETE_BOOKMARK: 'delete-bookmark',
298
- DELETE_COMMENT: 'delete-comment',
299
- DELETE_FEED: 'delete-feed',
300
- EDIT_BOOKMARK: 'edit-bookmark',
301
- FOLLOW: 'follow',
302
- INVITE_FEED: 'invite-feed',
303
- JOIN_FEED: 'join-feed',
304
- LEAVE_FEED: 'leave-feed',
305
- MANAGE_FEED_GROUP: 'manage-feed-group',
306
- MARK_ACTIVITY: 'mark-activity',
307
- PIN_ACTIVITY: 'pin-activity',
308
- QUERY_FEED_MEMBERS: 'query-feed-members',
309
- QUERY_FOLLOWS: 'query-follows',
310
- READ_ACTIVITIES: 'read-activities',
311
- READ_FEED: 'read-feed',
312
- REMOVE_ACTIVITY: 'remove-activity',
313
- REMOVE_ACTIVITY_REACTION: 'remove-activity-reaction',
314
- REMOVE_COMMENT_REACTION: 'remove-comment-reaction',
315
- UNFOLLOW: 'unfollow',
316
- UPDATE_ACTIVITY: 'update-activity',
317
- UPDATE_COMMENT: 'update-comment',
318
- UPDATE_FEED: 'update-feed',
319
- UPDATE_FEED_FOLLOWERS: 'update-feed-followers',
320
- UPDATE_FEED_MEMBERS: 'update-feed-members',
321
- };
322
-
323
- const stableEmptyArray = [];
324
- const selector = (currentState) => ({
325
- oc: currentState.own_capabilities ?? stableEmptyArray,
326
- });
327
- const useOwnCapabilities = (feed) => {
328
- const { oc = stableEmptyArray } = useStateStore(feed?.state, selector) ?? {};
329
- return react.useMemo(() => ({
330
- canAddActivity: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY) > -1,
331
- canAddActivityReaction: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY_REACTION) > -1,
332
- canAddComment: oc.indexOf(FeedOwnCapability.ADD_COMMENT) > -1,
333
- canAddCommentReaction: oc.indexOf(FeedOwnCapability.ADD_COMMENT_REACTION) > -1,
334
- canBookmarkActivity: oc.indexOf(FeedOwnCapability.BOOKMARK_ACTIVITY) > -1,
335
- canCreateFeed: oc.indexOf(FeedOwnCapability.CREATE_FEED) > -1,
336
- canDeleteBookmark: oc.indexOf(FeedOwnCapability.DELETE_BOOKMARK) > -1,
337
- canDeleteComment: oc.indexOf(FeedOwnCapability.DELETE_COMMENT) > -1,
338
- canDeleteFeed: oc.indexOf(FeedOwnCapability.DELETE_FEED) > -1,
339
- canEditBookmark: oc.indexOf(FeedOwnCapability.EDIT_BOOKMARK) > -1,
340
- canFollow: oc.indexOf(FeedOwnCapability.FOLLOW) > -1,
341
- canRemoveActivity: oc.indexOf(FeedOwnCapability.REMOVE_ACTIVITY) > -1,
342
- canRemoveActivityReaction: oc.indexOf(FeedOwnCapability.REMOVE_ACTIVITY_REACTION) > -1,
343
- canRemoveCommentReaction: oc.indexOf(FeedOwnCapability.REMOVE_COMMENT_REACTION) > -1,
344
- canUnfollow: oc.indexOf(FeedOwnCapability.UNFOLLOW) > -1,
345
- canUpdateFeed: oc.indexOf(FeedOwnCapability.UPDATE_FEED) > -1,
346
- canInviteFeed: oc.indexOf(FeedOwnCapability.INVITE_FEED) > -1,
347
- canJoinFeed: oc.indexOf(FeedOwnCapability.JOIN_FEED) > -1,
348
- canLeaveFeed: oc.indexOf(FeedOwnCapability.LEAVE_FEED) > -1,
349
- canManageFeedGroup: oc.indexOf(FeedOwnCapability.MANAGE_FEED_GROUP) > -1,
350
- canMarkActivity: oc.indexOf(FeedOwnCapability.MARK_ACTIVITY) > -1,
351
- canPinActivity: oc.indexOf(FeedOwnCapability.PIN_ACTIVITY) > -1,
352
- canQueryFeedMembers: oc.indexOf(FeedOwnCapability.QUERY_FEED_MEMBERS) > -1,
353
- canQueryFollows: oc.indexOf(FeedOwnCapability.QUERY_FOLLOWS) > -1,
354
- canReadActivities: oc.indexOf(FeedOwnCapability.READ_ACTIVITIES) > -1,
355
- canReadFeed: oc.indexOf(FeedOwnCapability.READ_FEED) > -1,
356
- canUpdateActivity: oc.indexOf(FeedOwnCapability.UPDATE_ACTIVITY) > -1,
357
- canUpdateComment: oc.indexOf(FeedOwnCapability.UPDATE_COMMENT) > -1,
358
- canUpdateFeedFollowers: oc.indexOf(FeedOwnCapability.UPDATE_FEED_FOLLOWERS) > -1,
359
- canUpdateFeedMembers: oc.indexOf(FeedOwnCapability.UPDATE_FEED_MEMBERS) > -1,
360
- }), [oc]);
361
- };
362
-
363
- const StreamFeedsContext = react.createContext(undefined);
364
- /**
365
- * Hook to access the nearest FeedsClient instance.
366
- */
367
- const useFeedsClient = () => {
368
- return react.useContext(StreamFeedsContext);
369
- };
370
-
371
- /**
372
- * A React hook that returns the currently connected user on a `FeedsClient` instance and null otherwise.
373
- */
374
- const useClientConnectedUser = () => {
375
- const client = useFeedsClient();
376
- const { user } = useStateStore(client?.state, clientConnectedUserSelector) ?? {};
377
- return user;
378
- };
379
- /**
380
- * A React hook that returns the websocket connection state of `FeedsClient`.
381
- */
382
- const useWsConnectionState = () => {
383
- const client = useFeedsClient();
384
- const { isHealthy } = useStateStore(client?.state, wsConnectionStateSelector) ?? {};
385
- return { isHealthy };
386
- };
387
- const clientConnectedUserSelector = (nextState) => ({
388
- user: nextState.connectedUser,
389
- });
390
- const wsConnectionStateSelector = (nextState) => ({
391
- isHealthy: nextState.isWsConnectionHealthy,
392
- });
393
-
394
278
  const decoders = {};
395
279
  const decodeDatetimeType = (input) => typeof input === 'number'
396
280
  ? new Date(Math.floor(input / 1000000))
@@ -2835,6 +2719,13 @@ function removeConnectionEventListeners(cb) {
2835
2719
  window.removeEventListener('online', cb);
2836
2720
  }
2837
2721
  }
2722
+ const streamDevToken = (userId) => {
2723
+ return [
2724
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9', // {"alg": "HS256", "typ": "JWT"}
2725
+ window.btoa(JSON.stringify({ user_id: userId })),
2726
+ 'devtoken', // hardcoded signature
2727
+ ].join('.');
2728
+ };
2838
2729
  const capitalize = (s) => {
2839
2730
  return `${s.charAt(0).toLocaleUpperCase()}${s.slice(1)}`;
2840
2731
  };
@@ -2892,7 +2783,7 @@ class TokenManager {
2892
2783
  }
2893
2784
  catch (e) {
2894
2785
  const numberOfFailures = ++previousFailuresCount;
2895
- await sleep(retryInterval(numberOfFailures));
2786
+ await sleep(1000);
2896
2787
  if (numberOfFailures === 3) {
2897
2788
  this.loadTokenPromise = null;
2898
2789
  return reject(new Error(`Stream error: tried to get token ${numberOfFailures} times, but it failed with ${e}. Check your token provider`, { cause: e }));
@@ -3105,7 +2996,10 @@ class StableWSConnection {
3105
2996
  this.onmessage = (wsID, event) => {
3106
2997
  if (this.wsID !== wsID)
3107
2998
  return;
3108
- this._log('onmessage() - onmessage callback', { event, wsID });
2999
+ this._log('onmessage() - onmessage callback', {
3000
+ event: { ...event, data: JSON.parse(event.data) },
3001
+ wsID,
3002
+ });
3109
3003
  let data = typeof event.data === 'string' ? JSON.parse(event.data) : null;
3110
3004
  this.decoders.forEach((decode) => {
3111
3005
  data = decode(data);
@@ -3737,7 +3631,7 @@ class ApiClient {
3737
3631
  rate_limit: getRateLimitFromResponseHeader(response_headers),
3738
3632
  };
3739
3633
  };
3740
- this.baseUrl = options?.base_url ?? 'https://video.stream-io-api.com';
3634
+ this.baseUrl = options?.base_url ?? 'https://feeds.stream-io-api.com';
3741
3635
  this.timeout = options?.timeout ?? 3000;
3742
3636
  this.axiosInstance = axios.create({
3743
3637
  baseURL: this.baseUrl,
@@ -4031,6 +3925,13 @@ const removeReactionFromActivities = (event, activities, isCurrentUser) => {
4031
3925
  return updateActivityInActivities$1(updatedActivity, activities);
4032
3926
  };
4033
3927
 
3928
+ // Helper function to check if two bookmarks are the same
3929
+ // A bookmark is identified by activity_id + folder_id + user_id
3930
+ const isSameBookmark = (bookmark1, bookmark2) => {
3931
+ return (bookmark1.user.id === bookmark2.user.id &&
3932
+ bookmark1.activity.id === bookmark2.activity.id &&
3933
+ bookmark1.folder?.id === bookmark2.folder?.id);
3934
+ };
4034
3935
  const updateActivityInActivities = (updatedActivity, activities) => {
4035
3936
  const index = activities.findIndex((a) => a.id === updatedActivity.id);
4036
3937
  if (index !== -1) {
@@ -4057,8 +3958,7 @@ const addBookmarkToActivity = (event, activity, isCurrentUser) => {
4057
3958
  const removeBookmarkFromActivity = (event, activity, isCurrentUser) => {
4058
3959
  // Update own_bookmarks if the bookmark is from the current user
4059
3960
  const ownBookmarks = isCurrentUser
4060
- ? (activity.own_bookmarks || []).filter((bookmark) => bookmark.user.id !== event.bookmark.user.id ||
4061
- bookmark.activity.id !== event.bookmark.activity.id)
3961
+ ? (activity.own_bookmarks || []).filter((bookmark) => !isSameBookmark(bookmark, event.bookmark))
4062
3962
  : activity.own_bookmarks;
4063
3963
  return {
4064
3964
  ...activity,
@@ -4070,8 +3970,7 @@ const updateBookmarkInActivity = (event, activity, isCurrentUser) => {
4070
3970
  // Update own_bookmarks if the bookmark is from the current user
4071
3971
  let ownBookmarks = activity.own_bookmarks || [];
4072
3972
  if (isCurrentUser) {
4073
- const bookmarkIndex = ownBookmarks.findIndex((bookmark) => bookmark.user.id === event.bookmark.user.id &&
4074
- bookmark.activity.id === event.bookmark.activity.id);
3973
+ const bookmarkIndex = ownBookmarks.findIndex((bookmark) => isSameBookmark(bookmark, event.bookmark));
4075
3974
  if (bookmarkIndex !== -1) {
4076
3975
  ownBookmarks = [...ownBookmarks];
4077
3976
  ownBookmarks[bookmarkIndex] = event.bookmark;
@@ -4120,8 +4019,15 @@ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4120
4019
  return updateActivityInActivities(updatedActivity, activities);
4121
4020
  };
4122
4021
 
4123
- const END_OF_LIST = 'eol';
4124
- const DEFAULT_COMMENT_PAGINATION = 'first';
4022
+ const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4023
+ typeof cursor === 'string';
4024
+ const isCommentResponse = (entity) => {
4025
+ return typeof entity?.object_id === 'string';
4026
+ };
4027
+ const Constants = {
4028
+ DEFAULT_COMMENT_PAGINATION: 'first',
4029
+ };
4030
+
4125
4031
  class Feed extends FeedApi {
4126
4032
  constructor(client, groupId, id, data) {
4127
4033
  // Need this ugly cast because fileUpload endpoints :(
@@ -4146,7 +4052,7 @@ class Feed extends FeedApi {
4146
4052
  },
4147
4053
  'feeds.activity.reaction.added': (event) => {
4148
4054
  const currentActivities = this.currentState.activities;
4149
- const connectedUser = this.client.state.getLatestValue().connectedUser;
4055
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4150
4056
  const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4151
4057
  const result = addReactionToActivities(event, currentActivities, isCurrentUser);
4152
4058
  if (result.changed) {
@@ -4155,7 +4061,7 @@ class Feed extends FeedApi {
4155
4061
  },
4156
4062
  'feeds.activity.reaction.deleted': (event) => {
4157
4063
  const currentActivities = this.currentState.activities;
4158
- const connectedUser = this.client.state.getLatestValue().connectedUser;
4064
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4159
4065
  const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4160
4066
  const result = removeReactionFromActivities(event, currentActivities, isCurrentUser);
4161
4067
  if (result.changed) {
@@ -4194,7 +4100,7 @@ class Feed extends FeedApi {
4194
4100
  const entityState = currentState.comments_by_entity_id[forId];
4195
4101
  const newComments = entityState?.comments?.concat([]) ?? [];
4196
4102
  if (entityState?.pagination?.sort === 'last' &&
4197
- entityState?.pagination.next === END_OF_LIST) {
4103
+ !checkHasAnotherPage(entityState.comments, entityState?.pagination.next)) {
4198
4104
  newComments.unshift(comment);
4199
4105
  }
4200
4106
  else if (entityState?.pagination?.sort === 'first') {
@@ -4281,7 +4187,7 @@ class Feed extends FeedApi {
4281
4187
  ...currentState,
4282
4188
  ...event.follow.source_feed,
4283
4189
  };
4284
- if (currentState.following_pagination?.next === END_OF_LIST) {
4190
+ if (!checkHasAnotherPage(currentState.following, currentState.following_pagination?.next)) {
4285
4191
  // TODO: respect sort
4286
4192
  newState.following = currentState.following
4287
4193
  ? currentState.following.concat(event.follow)
@@ -4294,7 +4200,7 @@ class Feed extends FeedApi {
4294
4200
  // someone followed this feed
4295
4201
  event.follow.target_feed.fid === this.fid) {
4296
4202
  const source = event.follow.source_feed;
4297
- const connectedUser = this.client.state.getLatestValue().connectedUser;
4203
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4298
4204
  this.state.next((currentState) => {
4299
4205
  const newState = { ...currentState, ...event.follow.target_feed };
4300
4206
  if (source.created_by.id === connectedUser?.id) {
@@ -4302,7 +4208,7 @@ class Feed extends FeedApi {
4302
4208
  ? currentState.own_follows.concat(event.follow)
4303
4209
  : [event.follow];
4304
4210
  }
4305
- if (currentState.followers_pagination?.next === END_OF_LIST) {
4211
+ if (!checkHasAnotherPage(currentState.followers, currentState.followers_pagination?.next)) {
4306
4212
  // TODO: respect sort
4307
4213
  newState.followers = currentState.followers
4308
4214
  ? currentState.followers.concat(event.follow)
@@ -4327,7 +4233,7 @@ class Feed extends FeedApi {
4327
4233
  // someone unfollowed this feed
4328
4234
  event.follow.target_feed.fid === this.fid) {
4329
4235
  const source = event.follow.source_feed;
4330
- const connectedUser = this.client.state.getLatestValue().connectedUser;
4236
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4331
4237
  this.state.next((currentState) => {
4332
4238
  const newState = { ...currentState, ...event.follow.target_feed };
4333
4239
  if (source.created_by.id === connectedUser?.id) {
@@ -4343,40 +4249,60 @@ class Feed extends FeedApi {
4343
4249
  'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4344
4250
  'feeds.comment.reaction.updated': Feed.noop,
4345
4251
  'feeds.feed_member.added': (event) => {
4346
- const { member } = event;
4347
- // do not add a member if the pagination has reached the end of the list
4348
- if (this.currentState.member_pagination?.next !== END_OF_LIST)
4349
- return;
4252
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4350
4253
  this.state.next((currentState) => {
4351
- return {
4352
- ...currentState,
4353
- // TODO: respect sort
4354
- members: currentState.members
4355
- ? currentState.members.concat(member)
4356
- : [member],
4357
- };
4254
+ let newState;
4255
+ if (!checkHasAnotherPage(currentState.members, currentState.member_pagination?.next)) {
4256
+ newState ?? (newState = {
4257
+ ...currentState,
4258
+ });
4259
+ newState.members = newState.members?.concat(event.member) ?? [
4260
+ event.member,
4261
+ ];
4262
+ }
4263
+ if (connectedUser?.id === event.member.user.id) {
4264
+ newState ?? (newState = {
4265
+ ...currentState,
4266
+ });
4267
+ newState.own_membership = event.member;
4268
+ }
4269
+ return newState ?? currentState;
4358
4270
  });
4359
4271
  },
4360
4272
  'feeds.feed_member.removed': (event) => {
4273
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4361
4274
  this.state.next((currentState) => {
4362
- return {
4275
+ const newState = {
4363
4276
  ...currentState,
4364
4277
  members: currentState.members?.filter((member) => member.user.id !== event.user?.id),
4365
4278
  };
4279
+ if (connectedUser?.id === event.member_id) {
4280
+ delete newState.own_membership;
4281
+ }
4282
+ return newState;
4366
4283
  });
4367
4284
  },
4368
4285
  'feeds.feed_member.updated': (event) => {
4286
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4369
4287
  this.state.next((currentState) => {
4370
4288
  const memberIndex = currentState.members?.findIndex((member) => member.user.id === event.member.user.id) ?? -1;
4289
+ let newState;
4371
4290
  if (memberIndex !== -1) {
4291
+ // if there's an index, there's a member to update
4372
4292
  const newMembers = [...currentState.members];
4373
4293
  newMembers[memberIndex] = event.member;
4374
- return {
4294
+ newState ?? (newState = {
4375
4295
  ...currentState,
4376
- members: newMembers,
4377
- };
4296
+ });
4297
+ newState.members = newMembers;
4378
4298
  }
4379
- return currentState;
4299
+ if (connectedUser?.id === event.member.user.id) {
4300
+ newState ?? (newState = {
4301
+ ...currentState,
4302
+ });
4303
+ newState.own_membership = event.member;
4304
+ }
4305
+ return newState ?? currentState;
4380
4306
  });
4381
4307
  },
4382
4308
  // the poll events should be removed from here
@@ -4422,7 +4348,7 @@ class Feed extends FeedApi {
4422
4348
  }
4423
4349
  handleCommentReactionEvent(event) {
4424
4350
  const { comment, reaction } = event;
4425
- const connectedUser = this.client.state.getLatestValue().connectedUser;
4351
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4426
4352
  this.state.next((currentState) => {
4427
4353
  const forId = comment.parent_id ?? comment.object_id;
4428
4354
  const entityState = currentState.comments_by_entity_id[forId];
@@ -4502,29 +4428,6 @@ class Feed extends FeedApi {
4502
4428
  ...currentState,
4503
4429
  ...responseCopy,
4504
4430
  };
4505
- // if there is no next cursor, set it to END_OF_LIST
4506
- // request has to have a limit set for this to work
4507
- if ((request?.followers_pagination?.limit ?? 0) > 0 &&
4508
- typeof nextState.followers_pagination?.next === 'undefined') {
4509
- nextState.followers_pagination = {
4510
- ...nextState.followers_pagination,
4511
- next: END_OF_LIST,
4512
- };
4513
- }
4514
- if ((request?.following_pagination?.limit ?? 0) > 0 &&
4515
- typeof nextState.following_pagination?.next === 'undefined') {
4516
- nextState.following_pagination = {
4517
- ...nextState.following_pagination,
4518
- next: END_OF_LIST,
4519
- };
4520
- }
4521
- if ((request?.member_pagination?.limit ?? 0) > 0 &&
4522
- typeof nextState.member_pagination?.next === 'undefined') {
4523
- nextState.member_pagination = {
4524
- ...nextState.member_pagination,
4525
- next: END_OF_LIST,
4526
- };
4527
- }
4528
4431
  if (!request?.followers_pagination?.limit) {
4529
4432
  delete nextState.followers;
4530
4433
  }
@@ -4547,7 +4450,7 @@ class Feed extends FeedApi {
4547
4450
  }
4548
4451
  handleBookmarkAdded(event) {
4549
4452
  const currentActivities = this.currentState.activities;
4550
- const { connectedUser } = this.client.state.getLatestValue();
4453
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4551
4454
  const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4552
4455
  const result = addBookmarkToActivities(event, currentActivities, isCurrentUser);
4553
4456
  if (result.changed) {
@@ -4556,7 +4459,7 @@ class Feed extends FeedApi {
4556
4459
  }
4557
4460
  handleBookmarkDeleted(event) {
4558
4461
  const currentActivities = this.currentState.activities;
4559
- const { connectedUser } = this.client.state.getLatestValue();
4462
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4560
4463
  const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4561
4464
  const result = removeBookmarkFromActivities(event, currentActivities, isCurrentUser);
4562
4465
  if (result.changed) {
@@ -4565,7 +4468,7 @@ class Feed extends FeedApi {
4565
4468
  }
4566
4469
  handleBookmarkUpdated(event) {
4567
4470
  const currentActivities = this.currentState.activities;
4568
- const { connectedUser } = this.client.state.getLatestValue();
4471
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4569
4472
  const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4570
4473
  const result = updateBookmarkInActivities(event, currentActivities, isCurrentUser);
4571
4474
  if (result.changed) {
@@ -4615,6 +4518,7 @@ class Feed extends FeedApi {
4615
4518
  });
4616
4519
  }
4617
4520
  async loadNextPageComments({ forId, base, sort, parentId, }) {
4521
+ let error;
4618
4522
  try {
4619
4523
  this.state.next((currentState) => ({
4620
4524
  ...currentState,
@@ -4629,7 +4533,7 @@ class Feed extends FeedApi {
4629
4533
  },
4630
4534
  },
4631
4535
  }));
4632
- const { next: newNextCursor = END_OF_LIST, comments } = await base();
4536
+ const { next: newNextCursor, comments } = await base();
4633
4537
  this.state.next((currentState) => {
4634
4538
  const newPagination = {
4635
4539
  ...currentState.comments_by_entity_id[forId]?.pagination,
@@ -4654,9 +4558,8 @@ class Feed extends FeedApi {
4654
4558
  };
4655
4559
  });
4656
4560
  }
4657
- catch (error) {
4658
- console.error(error);
4659
- // TODO: figure out how to handle errorss
4561
+ catch (e) {
4562
+ error = e;
4660
4563
  }
4661
4564
  finally {
4662
4565
  this.state.next((currentState) => ({
@@ -4673,15 +4576,21 @@ class Feed extends FeedApi {
4673
4576
  },
4674
4577
  }));
4675
4578
  }
4579
+ if (error) {
4580
+ throw error;
4581
+ }
4676
4582
  }
4677
4583
  async loadNextPageActivityComments(activity, request) {
4678
- const pagination = this.currentState.comments_by_entity_id[activity.id]?.pagination;
4679
- const currentNextCursor = pagination?.next;
4680
- const currentSort = pagination?.sort;
4681
- const isLoading = pagination?.loading_next_page;
4682
- const sort = currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION;
4683
- if (isLoading || currentNextCursor === END_OF_LIST)
4584
+ const currentEntityState = this.currentState.comments_by_entity_id[activity.id];
4585
+ const currentPagination = currentEntityState?.pagination;
4586
+ const currentNextCursor = currentPagination?.next;
4587
+ const currentSort = currentPagination?.sort;
4588
+ const isLoading = currentPagination?.loading_next_page;
4589
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4590
+ if (isLoading ||
4591
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4684
4592
  return;
4593
+ }
4685
4594
  await this.loadNextPageComments({
4686
4595
  forId: activity.id,
4687
4596
  base: () => this.client.getComments({
@@ -4695,20 +4604,25 @@ class Feed extends FeedApi {
4695
4604
  });
4696
4605
  }
4697
4606
  async loadNextPageCommentReplies(comment, request) {
4698
- const pagination = this.currentState.comments_by_entity_id[comment.id]?.pagination;
4699
- const currentNextCursor = pagination?.next;
4700
- const currentSort = pagination?.sort;
4701
- const isLoading = pagination?.loading_next_page;
4702
- const sort = currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION;
4703
- if (isLoading || currentNextCursor === END_OF_LIST)
4607
+ const currentEntityState = this.currentState.comments_by_entity_id[comment.id];
4608
+ const currentPagination = currentEntityState?.pagination;
4609
+ const currentNextCursor = currentPagination?.next;
4610
+ const currentSort = currentPagination?.sort;
4611
+ const isLoading = currentPagination?.loading_next_page;
4612
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4613
+ if (isLoading ||
4614
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4704
4615
  return;
4616
+ }
4705
4617
  await this.loadNextPageComments({
4706
4618
  forId: comment.id,
4707
4619
  base: () => this.client.getCommentReplies({
4708
4620
  ...request,
4709
4621
  comment_id: comment.id,
4710
4622
  // use known sort first (prevents broken pagination)
4711
- sort: currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION,
4623
+ sort: currentSort ??
4624
+ request?.sort ??
4625
+ Constants.DEFAULT_COMMENT_PAGINATION,
4712
4626
  next: currentNextCursor,
4713
4627
  }),
4714
4628
  parentId: comment.parent_id ?? comment.object_id,
@@ -4718,10 +4632,14 @@ class Feed extends FeedApi {
4718
4632
  async loadNextPageFollows(type, request) {
4719
4633
  const paginationKey = `${type}_pagination`;
4720
4634
  const method = `query${capitalize(type)}`;
4635
+ const currentFollows = this.currentState[type];
4721
4636
  const currentNextCursor = this.currentState[paginationKey]?.next;
4722
4637
  const isLoading = this.currentState[paginationKey]?.loading_next_page;
4723
- if (isLoading || currentNextCursor === END_OF_LIST)
4638
+ const sort = this.currentState[paginationKey]?.sort ?? request.sort;
4639
+ let error;
4640
+ if (isLoading || !checkHasAnotherPage(currentFollows, currentNextCursor)) {
4724
4641
  return;
4642
+ }
4725
4643
  try {
4726
4644
  this.state.next((currentState) => {
4727
4645
  return {
@@ -4732,9 +4650,10 @@ class Feed extends FeedApi {
4732
4650
  },
4733
4651
  };
4734
4652
  });
4735
- const { next: newNextCursor = END_OF_LIST, follows } = await this[method]({
4653
+ const { next: newNextCursor, follows } = await this[method]({
4736
4654
  ...request,
4737
4655
  next: currentNextCursor,
4656
+ sort,
4738
4657
  });
4739
4658
  this.state.next((currentState) => ({
4740
4659
  ...currentState,
@@ -4744,12 +4663,12 @@ class Feed extends FeedApi {
4744
4663
  [paginationKey]: {
4745
4664
  ...currentState[paginationKey],
4746
4665
  next: newNextCursor,
4666
+ sort,
4747
4667
  },
4748
4668
  }));
4749
4669
  }
4750
- catch (error) {
4751
- console.error(error);
4752
- // TODO: figure out how to handle errorss
4670
+ catch (e) {
4671
+ error = e;
4753
4672
  }
4754
4673
  finally {
4755
4674
  this.state.next((currentState) => {
@@ -4762,6 +4681,9 @@ class Feed extends FeedApi {
4762
4681
  };
4763
4682
  });
4764
4683
  }
4684
+ if (error) {
4685
+ throw error;
4686
+ }
4765
4687
  }
4766
4688
  async loadNextPageFollowers(request) {
4767
4689
  await this.loadNextPageFollows('followers', request);
@@ -4769,6 +4691,59 @@ class Feed extends FeedApi {
4769
4691
  async loadNextPageFollowing(request) {
4770
4692
  await this.loadNextPageFollows('following', request);
4771
4693
  }
4694
+ async loadNextPageMembers(request) {
4695
+ const currentMembers = this.currentState.members;
4696
+ const currentNextCursor = this.currentState.member_pagination?.next;
4697
+ const isLoading = this.currentState.member_pagination?.loading_next_page;
4698
+ const sort = this.currentState.member_pagination?.sort ?? request.sort;
4699
+ let error;
4700
+ if (isLoading || !checkHasAnotherPage(currentMembers, currentNextCursor)) {
4701
+ return;
4702
+ }
4703
+ try {
4704
+ this.state.next((currentState) => ({
4705
+ ...currentState,
4706
+ member_pagination: {
4707
+ ...currentState.member_pagination,
4708
+ loading_next_page: true,
4709
+ },
4710
+ }));
4711
+ const { next: newNextCursor, members } = await this.client.queryFeedMembers({
4712
+ ...request,
4713
+ sort,
4714
+ feed_id: this.id,
4715
+ feed_group_id: this.group,
4716
+ next: currentNextCursor,
4717
+ });
4718
+ this.state.next((currentState) => ({
4719
+ ...currentState,
4720
+ members: currentState.members
4721
+ ? currentState.members.concat(members)
4722
+ : members,
4723
+ member_pagination: {
4724
+ ...currentState.member_pagination,
4725
+ next: newNextCursor,
4726
+ // set sort if not defined yet
4727
+ sort: currentState.member_pagination?.sort ?? request.sort,
4728
+ },
4729
+ }));
4730
+ }
4731
+ catch (e) {
4732
+ error = e;
4733
+ }
4734
+ finally {
4735
+ this.state.next((currentState) => ({
4736
+ ...currentState,
4737
+ member_pagination: {
4738
+ ...currentState.member_pagination,
4739
+ loading_next_page: false,
4740
+ },
4741
+ }));
4742
+ }
4743
+ if (error) {
4744
+ throw error;
4745
+ }
4746
+ }
4772
4747
  /**
4773
4748
  * Method which queries followers of this feed (feeds which target this feed).
4774
4749
  *
@@ -5048,7 +5023,8 @@ class StreamPoll {
5048
5023
  if (!isPollVoteCastedEvent(event))
5049
5024
  return;
5050
5025
  const currentState = this.data;
5051
- const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
5026
+ const isOwnVote = event.poll_vote.user_id ===
5027
+ this.client.state.getLatestValue().connected_user?.id;
5052
5028
  let latestAnswers = [...currentState.latest_answers];
5053
5029
  let ownAnswer = currentState.own_answer;
5054
5030
  const ownVotesByOptionId = currentState.own_votes_by_option_id;
@@ -5072,7 +5048,7 @@ class StreamPoll {
5072
5048
  else {
5073
5049
  maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5074
5050
  }
5075
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
5051
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5076
5052
  this.state.partialNext({
5077
5053
  answers_count,
5078
5054
  // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
@@ -5093,7 +5069,8 @@ class StreamPoll {
5093
5069
  if (!isPollVoteChangedEvent(event))
5094
5070
  return;
5095
5071
  const currentState = this.data;
5096
- const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
5072
+ const isOwnVote = event.poll_vote.user_id ===
5073
+ this.client.state.getLatestValue().connected_user?.id;
5097
5074
  let latestAnswers = [...currentState.latest_answers];
5098
5075
  let ownAnswer = currentState.own_answer;
5099
5076
  let ownVotesByOptionId = currentState.own_votes_by_option_id;
@@ -5140,7 +5117,7 @@ class StreamPoll {
5140
5117
  else {
5141
5118
  maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5142
5119
  }
5143
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
5120
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5144
5121
  this.state.partialNext({
5145
5122
  answers_count,
5146
5123
  // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
@@ -5160,7 +5137,8 @@ class StreamPoll {
5160
5137
  if (!isPollVoteRemovedEvent(event))
5161
5138
  return;
5162
5139
  const currentState = this.data;
5163
- const isOwnVote = event.poll_vote.user_id === this.client.state.getLatestValue().connectedUser?.id;
5140
+ const isOwnVote = event.poll_vote.user_id ===
5141
+ this.client.state.getLatestValue().connected_user?.id;
5164
5142
  let latestAnswers = [...currentState.latest_answers];
5165
5143
  let ownAnswer = currentState.own_answer;
5166
5144
  const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
@@ -5178,7 +5156,7 @@ class StreamPoll {
5178
5156
  delete ownVotesByOptionId[event.poll_vote.option_id];
5179
5157
  }
5180
5158
  }
5181
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option } = event.poll;
5159
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5182
5160
  this.state.partialNext({
5183
5161
  answers_count,
5184
5162
  // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
@@ -5236,7 +5214,7 @@ class FeedsClient extends FeedsApi {
5236
5214
  this.healthyConnectionChangedEventCount = 0;
5237
5215
  this.pollFromState = (id) => this.polls_by_id.get(id);
5238
5216
  this.connectUser = async (user, tokenProvider) => {
5239
- if (this.state.getLatestValue().connectedUser !== undefined ||
5217
+ if (this.state.getLatestValue().connected_user !== undefined ||
5240
5218
  this.wsConnection) {
5241
5219
  throw new Error(`Can't connect a new user, call "disconnectUser" first`);
5242
5220
  }
@@ -5250,8 +5228,8 @@ class FeedsClient extends FeedsApi {
5250
5228
  this.wsConnection.on('all', (event) => this.eventDispatcher.dispatch(event));
5251
5229
  const connectedEvent = await this.wsConnection.connect();
5252
5230
  this.state.partialNext({
5253
- connectedUser: connectedEvent?.me,
5254
- isWsConnectionHealthy: this.wsConnection.isHealthy,
5231
+ connected_user: connectedEvent?.me,
5232
+ is_ws_connection_healthy: !!this.wsConnection?.isHealthy,
5255
5233
  });
5256
5234
  }
5257
5235
  catch (err) {
@@ -5259,6 +5237,9 @@ class FeedsClient extends FeedsApi {
5259
5237
  throw err;
5260
5238
  }
5261
5239
  };
5240
+ this.devToken = (userId) => {
5241
+ return streamDevToken(userId);
5242
+ };
5262
5243
  this.closePoll = async (request) => {
5263
5244
  return await this.updatePollPartial({
5264
5245
  poll_id: request.poll_id,
@@ -5307,7 +5288,10 @@ class FeedsClient extends FeedsApi {
5307
5288
  removeConnectionEventListeners(this.updateNetworkConnectionStatus);
5308
5289
  this.connectionIdManager.reset();
5309
5290
  this.tokenManager.reset();
5310
- this.state.partialNext({ connectedUser: undefined, isWsConnectionHealthy: false });
5291
+ this.state.partialNext({
5292
+ connected_user: undefined,
5293
+ is_ws_connection_healthy: false,
5294
+ });
5311
5295
  };
5312
5296
  this.on = this.eventDispatcher.on;
5313
5297
  this.off = this.eventDispatcher.off;
@@ -5333,8 +5317,8 @@ class FeedsClient extends FeedsApi {
5333
5317
  }
5334
5318
  };
5335
5319
  this.state = new StateStore({
5336
- connectedUser: undefined,
5337
- isWsConnectionHealthy: false,
5320
+ connected_user: undefined,
5321
+ is_ws_connection_healthy: false,
5338
5322
  });
5339
5323
  this.moderation = new ModerationClient(apiClient);
5340
5324
  this.tokenManager = tokenManager;
@@ -5346,7 +5330,7 @@ class FeedsClient extends FeedsApi {
5346
5330
  switch (event.type) {
5347
5331
  case 'connection.changed': {
5348
5332
  const { online } = event;
5349
- this.state.partialNext({ isWsConnectionHealthy: online });
5333
+ this.state.partialNext({ is_ws_connection_healthy: online });
5350
5334
  if (online) {
5351
5335
  this.healthyConnectionChangedEventCount++;
5352
5336
  // we skip the first event as we could potentially be querying twice
@@ -5517,18 +5501,345 @@ const useCreateFeedsClient = ({ apiKey, tokenOrProvider, userData, options, }) =
5517
5501
  return client;
5518
5502
  };
5519
5503
 
5504
+ const StreamFeedsContext = react.createContext(undefined);
5505
+ /**
5506
+ * Hook to access the nearest FeedsClient instance.
5507
+ */
5508
+ const useFeedsClient = () => {
5509
+ return react.useContext(StreamFeedsContext);
5510
+ };
5511
+
5512
+ /**
5513
+ * A React hook that returns the currently connected user on a `FeedsClient` instance and null otherwise.
5514
+ */
5515
+ const useClientConnectedUser = () => {
5516
+ const client = useFeedsClient();
5517
+ const { user } = useStateStore(client?.state, selector$7) ?? {};
5518
+ return user;
5519
+ };
5520
+ const selector$7 = (nextState) => ({
5521
+ user: nextState.connected_user,
5522
+ });
5523
+
5524
+ /**
5525
+ * A React hook that returns the websocket connection state of `FeedsClient`.
5526
+ */
5527
+ const useWsConnectionState = () => {
5528
+ const client = useFeedsClient();
5529
+ const { is_healthy } = useStateStore(client?.state, selector$6) ?? {};
5530
+ return { is_healthy };
5531
+ };
5532
+ const selector$6 = (nextState) => ({
5533
+ is_healthy: nextState.is_ws_connection_healthy,
5534
+ });
5535
+
5536
+ const StreamFeedContext = react.createContext(undefined);
5537
+ /**
5538
+ * Hook to access the nearest Feed instance.
5539
+ */
5540
+ const useFeedContext = () => {
5541
+ return react.useContext(StreamFeedContext);
5542
+ };
5543
+
5544
+ /**
5545
+ * A utility hook implementing a stable callback. It takes in an unstable method that
5546
+ * is supposed to be invoked somewhere deeper in the DOM tree without making it
5547
+ * change its reference every time the parent component rerenders. It will also return
5548
+ * the value of the callback if it does return one.
5549
+ * A common use-case would be having a function whose invocation depends on state
5550
+ * somewhere high up in the DOM tree and wanting to use the same function deeper
5551
+ * down, for example in a leaf node and simply using useCallback results in
5552
+ * cascading dependency hell. If we wrap it in useStableCallback, we would be able
5553
+ * to:
5554
+ * - Use the same function as a dependency of another hook (since it is stable)
5555
+ * - Still invoke it and get the latest state
5556
+ *
5557
+ * **Caveats:**
5558
+ * - Never wrap a function that is supposed to return a React.ReactElement in
5559
+ * useStableCallback, since React will not know that the DOM needs to be updated
5560
+ * whenever the callback value changes (for example, renderItem from FlatList must
5561
+ * never be wrapped in this hook)
5562
+ * - Always prefer using a standard useCallback/stable function wherever possible
5563
+ * (the purpose of useStableCallback is to bridge the gap between top level contexts
5564
+ * and cascading rereders in downstream components - **not** as an escape hatch)
5565
+ * @param callback - the callback we want to stabilize
5566
+ */
5567
+ const useStableCallback = (callback) => {
5568
+ const ref = react.useRef(callback);
5569
+ ref.current = callback;
5570
+ return react.useCallback((...args) => {
5571
+ return ref.current(...args);
5572
+ }, []);
5573
+ };
5574
+
5575
+ /**
5576
+ * A React hook that returns a reactive object containing the current activities,
5577
+ * loading state and whether there is a next page to paginate to or not.
5578
+ */
5579
+ const useFeedActivities = (feedFromProps) => {
5580
+ const feedFromContext = useFeedContext();
5581
+ const feed = feedFromProps ?? feedFromContext;
5582
+ const data = useStateStore(feed?.state, selector$5);
5583
+ const loadNextPage = useStableCallback(async () => {
5584
+ if (!feed || !data?.has_next_page || data?.is_loading) {
5585
+ return;
5586
+ }
5587
+ await feed.getNextPage();
5588
+ });
5589
+ return react.useMemo(() => ({ ...data, loadNextPage }), [data, loadNextPage]);
5590
+ };
5591
+ const selector$5 = ({ is_loading_activities, next, activities = [] }) => ({
5592
+ is_loading: is_loading_activities,
5593
+ has_next_page: typeof next !== 'undefined',
5594
+ activities,
5595
+ });
5596
+
5597
+ function useComments({ feed: feedFromProps, parent, }) {
5598
+ const feedFromContext = useFeedContext();
5599
+ const feed = feedFromProps ?? feedFromContext;
5600
+ const selector = react.useCallback((state) => ({
5601
+ comments: state.comments_by_entity_id?.[parent.id]?.comments,
5602
+ comments_pagination: state.comments_by_entity_id?.[parent.id]?.pagination,
5603
+ }), [parent.id]);
5604
+ const data = useStateStore(feed?.state, selector);
5605
+ const loadNextPage = react.useMemo(() => {
5606
+ if (!feed)
5607
+ return undefined;
5608
+ return (request) => {
5609
+ if (isCommentResponse(parent)) {
5610
+ return feed.loadNextPageCommentReplies(parent, request);
5611
+ }
5612
+ else {
5613
+ return feed.loadNextPageActivityComments(parent, request);
5614
+ }
5615
+ };
5616
+ }, [feed, parent]);
5617
+ return react.useMemo(() => {
5618
+ if (!data) {
5619
+ return undefined;
5620
+ }
5621
+ return {
5622
+ ...data,
5623
+ has_next_page: checkHasAnotherPage(data.comments, data.comments_pagination?.next),
5624
+ is_loading_next_page: data?.comments_pagination?.loading_next_page ?? false,
5625
+ loadNextPage,
5626
+ };
5627
+ }, [data, loadNextPage]);
5628
+ }
5629
+
5630
+ const FeedOwnCapability = {
5631
+ ADD_ACTIVITY: 'add-activity',
5632
+ ADD_ACTIVITY_REACTION: 'add-activity-reaction',
5633
+ ADD_COMMENT: 'add-comment',
5634
+ ADD_COMMENT_REACTION: 'add-comment-reaction',
5635
+ BOOKMARK_ACTIVITY: 'bookmark-activity',
5636
+ CREATE_FEED: 'create-feed',
5637
+ DELETE_BOOKMARK: 'delete-bookmark',
5638
+ DELETE_COMMENT: 'delete-comment',
5639
+ DELETE_FEED: 'delete-feed',
5640
+ EDIT_BOOKMARK: 'edit-bookmark',
5641
+ FOLLOW: 'follow',
5642
+ INVITE_FEED: 'invite-feed',
5643
+ JOIN_FEED: 'join-feed',
5644
+ LEAVE_FEED: 'leave-feed',
5645
+ MANAGE_FEED_GROUP: 'manage-feed-group',
5646
+ MARK_ACTIVITY: 'mark-activity',
5647
+ PIN_ACTIVITY: 'pin-activity',
5648
+ QUERY_FEED_MEMBERS: 'query-feed-members',
5649
+ QUERY_FOLLOWS: 'query-follows',
5650
+ READ_ACTIVITIES: 'read-activities',
5651
+ READ_FEED: 'read-feed',
5652
+ REMOVE_ACTIVITY: 'remove-activity',
5653
+ REMOVE_ACTIVITY_REACTION: 'remove-activity-reaction',
5654
+ REMOVE_COMMENT_REACTION: 'remove-comment-reaction',
5655
+ UNFOLLOW: 'unfollow',
5656
+ UPDATE_ACTIVITY: 'update-activity',
5657
+ UPDATE_COMMENT: 'update-comment',
5658
+ UPDATE_FEED: 'update-feed',
5659
+ UPDATE_FEED_FOLLOWERS: 'update-feed-followers',
5660
+ UPDATE_FEED_MEMBERS: 'update-feed-members',
5661
+ };
5662
+
5663
+ const stableEmptyArray = [];
5664
+ const selector$4 = (currentState) => ({
5665
+ oc: currentState.own_capabilities ?? stableEmptyArray,
5666
+ });
5667
+ const useOwnCapabilities = (feedFromProps) => {
5668
+ const feedFromContext = useFeedContext();
5669
+ const feed = feedFromProps ?? feedFromContext;
5670
+ const { oc = stableEmptyArray } = useStateStore(feed?.state, selector$4) ?? {};
5671
+ return react.useMemo(() => ({
5672
+ can_add_activity: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY) > -1,
5673
+ can_add_activity_reaction: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY_REACTION) > -1,
5674
+ can_add_comment: oc.indexOf(FeedOwnCapability.ADD_COMMENT) > -1,
5675
+ can_add_comment_reaction: oc.indexOf(FeedOwnCapability.ADD_COMMENT_REACTION) > -1,
5676
+ can_bookmark_activity: oc.indexOf(FeedOwnCapability.BOOKMARK_ACTIVITY) > -1,
5677
+ can_create_feed: oc.indexOf(FeedOwnCapability.CREATE_FEED) > -1,
5678
+ can_delete_bookmark: oc.indexOf(FeedOwnCapability.DELETE_BOOKMARK) > -1,
5679
+ can_delete_comment: oc.indexOf(FeedOwnCapability.DELETE_COMMENT) > -1,
5680
+ can_delete_feed: oc.indexOf(FeedOwnCapability.DELETE_FEED) > -1,
5681
+ can_edit_bookmark: oc.indexOf(FeedOwnCapability.EDIT_BOOKMARK) > -1,
5682
+ can_follow: oc.indexOf(FeedOwnCapability.FOLLOW) > -1,
5683
+ can_remove_activity: oc.indexOf(FeedOwnCapability.REMOVE_ACTIVITY) > -1,
5684
+ can_remove_activity_reaction: oc.indexOf(FeedOwnCapability.REMOVE_ACTIVITY_REACTION) > -1,
5685
+ can_remove_comment_reaction: oc.indexOf(FeedOwnCapability.REMOVE_COMMENT_REACTION) > -1,
5686
+ can_unfollow: oc.indexOf(FeedOwnCapability.UNFOLLOW) > -1,
5687
+ can_update_feed: oc.indexOf(FeedOwnCapability.UPDATE_FEED) > -1,
5688
+ can_invite_feed: oc.indexOf(FeedOwnCapability.INVITE_FEED) > -1,
5689
+ can_join_feed: oc.indexOf(FeedOwnCapability.JOIN_FEED) > -1,
5690
+ can_leave_feed: oc.indexOf(FeedOwnCapability.LEAVE_FEED) > -1,
5691
+ can_manage_feed_group: oc.indexOf(FeedOwnCapability.MANAGE_FEED_GROUP) > -1,
5692
+ can_mark_activity: oc.indexOf(FeedOwnCapability.MARK_ACTIVITY) > -1,
5693
+ can_pin_activity: oc.indexOf(FeedOwnCapability.PIN_ACTIVITY) > -1,
5694
+ can_query_feed_members: oc.indexOf(FeedOwnCapability.QUERY_FEED_MEMBERS) > -1,
5695
+ can_query_follows: oc.indexOf(FeedOwnCapability.QUERY_FOLLOWS) > -1,
5696
+ can_read_activities: oc.indexOf(FeedOwnCapability.READ_ACTIVITIES) > -1,
5697
+ can_read_feed: oc.indexOf(FeedOwnCapability.READ_FEED) > -1,
5698
+ can_update_activity: oc.indexOf(FeedOwnCapability.UPDATE_ACTIVITY) > -1,
5699
+ can_update_comment: oc.indexOf(FeedOwnCapability.UPDATE_COMMENT) > -1,
5700
+ can_update_feed_followers: oc.indexOf(FeedOwnCapability.UPDATE_FEED_FOLLOWERS) > -1,
5701
+ can_update_feed_members: oc.indexOf(FeedOwnCapability.UPDATE_FEED_MEMBERS) > -1,
5702
+ }), [oc]);
5703
+ };
5704
+
5705
+ const selector$3 = ({ follower_count, followers, followers_pagination, }) => ({
5706
+ follower_count,
5707
+ followers,
5708
+ followers_pagination,
5709
+ });
5710
+ function useFollowers(feedFromProps) {
5711
+ const feedFromContext = useFeedContext();
5712
+ const feed = feedFromProps ?? feedFromContext;
5713
+ const data = useStateStore(feed?.state, selector$3);
5714
+ const loadNextPage = react.useCallback((...options) => feed?.loadNextPageFollowers(...options), [feed]);
5715
+ return react.useMemo(() => {
5716
+ if (!data) {
5717
+ return undefined;
5718
+ }
5719
+ return {
5720
+ ...data,
5721
+ is_loading_next_page: data.followers_pagination?.loading_next_page ?? false,
5722
+ has_next_page: checkHasAnotherPage(data.followers, data.followers_pagination?.next),
5723
+ loadNextPage,
5724
+ };
5725
+ }, [data, loadNextPage]);
5726
+ }
5727
+
5728
+ const selector$2 = ({ following_count, following, following_pagination, }) => ({
5729
+ following_count,
5730
+ following,
5731
+ following_pagination,
5732
+ });
5733
+ function useFollowing(feedFromProps) {
5734
+ const feedFromContext = useFeedContext();
5735
+ const feed = feedFromProps ?? feedFromContext;
5736
+ const data = useStateStore(feed?.state, selector$2);
5737
+ const loadNextPage = react.useCallback((...options) => feed?.loadNextPageFollowing(...options), [feed]);
5738
+ return react.useMemo(() => {
5739
+ if (!data) {
5740
+ return undefined;
5741
+ }
5742
+ return {
5743
+ ...data,
5744
+ is_loading_next_page: data.following_pagination?.loading_next_page ?? false,
5745
+ has_next_page: checkHasAnotherPage(data.following, data.following_pagination?.next),
5746
+ loadNextPage,
5747
+ };
5748
+ }, [data, loadNextPage]);
5749
+ }
5750
+
5751
+ /**
5752
+ * A React hook that returns a reactive object containing some often used
5753
+ * metadata for a feed.
5754
+ */
5755
+ const useFeedMetadata = (feedFromProps) => {
5756
+ const feedFromContext = useFeedContext();
5757
+ const feed = feedFromProps ?? feedFromContext;
5758
+ return useStateStore(feed?.state, selector$1);
5759
+ };
5760
+ const selector$1 = ({ follower_count = 0, following_count = 0, created_by, created_at, updated_at, }) => ({
5761
+ created_by,
5762
+ follower_count,
5763
+ following_count,
5764
+ created_at,
5765
+ updated_at,
5766
+ });
5767
+
5768
+ /**
5769
+ * A React hook that returns a reactive array of feeds that the current user
5770
+ * owns and are following the respective feed that we are observing.
5771
+ */
5772
+ const useOwnFollows = (feedFromProps) => {
5773
+ const feedFromContext = useFeedContext();
5774
+ const feed = feedFromProps ?? feedFromContext;
5775
+ return useStateStore(feed?.state, selector);
5776
+ };
5777
+ const selector = ({ own_follows }) => ({
5778
+ own_follows,
5779
+ });
5780
+
5781
+ /**
5782
+ * A utility hook that takes in an entity and a reaction type, and creates reaction actions
5783
+ * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5784
+ * as the hook determines internally which APIs it is supposed to use, while taking the
5785
+ * correct ownCapabilities into account.
5786
+ * @param entity - The entity to which we want to add a reaction, can be either ActivityResponse or CommentResponse.
5787
+ * @param type - The type of reaction we want to add or remove.
5788
+ */
5789
+ const useReactionActions = ({ entity, type, }) => {
5790
+ const client = useFeedsClient();
5791
+ const isComment = isCommentResponse(entity);
5792
+ const hasOwnReaction = react.useMemo(() => !!entity.own_reactions?.find((r) => r.type === type), [entity.own_reactions, type]);
5793
+ const addReaction = useStableCallback(async () => {
5794
+ await (isComment
5795
+ ? client?.addCommentReaction({ comment_id: entity.id, type })
5796
+ : client?.addReaction({ activity_id: entity.id, type }));
5797
+ });
5798
+ const removeReaction = useStableCallback(async () => {
5799
+ await (isComment
5800
+ ? client?.deleteCommentReaction({ comment_id: entity.id, type })
5801
+ : client?.deleteActivityReaction({
5802
+ activity_id: entity.id,
5803
+ type,
5804
+ }));
5805
+ });
5806
+ const toggleReaction = useStableCallback(async () => {
5807
+ if (hasOwnReaction) {
5808
+ await removeReaction();
5809
+ }
5810
+ else {
5811
+ await addReaction();
5812
+ }
5813
+ });
5814
+ return react.useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5815
+ };
5816
+
5520
5817
  const StreamFeeds = ({ client, children }) => {
5521
5818
  return (jsxRuntime.jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5522
5819
  };
5523
5820
  StreamFeeds.displayName = 'StreamFeeds';
5524
5821
 
5822
+ const StreamFeed = ({ feed, children }) => {
5823
+ return (jsxRuntime.jsx(StreamFeedContext.Provider, { value: feed, children: children }));
5824
+ };
5825
+ StreamFeed.displayName = 'StreamFeed';
5826
+
5827
+ exports.StreamFeed = StreamFeed;
5828
+ exports.StreamFeedContext = StreamFeedContext;
5525
5829
  exports.StreamFeeds = StreamFeeds;
5526
5830
  exports.StreamFeedsContext = StreamFeedsContext;
5527
5831
  exports.useClientConnectedUser = useClientConnectedUser;
5528
5832
  exports.useComments = useComments;
5529
5833
  exports.useCreateFeedsClient = useCreateFeedsClient;
5834
+ exports.useFeedActivities = useFeedActivities;
5835
+ exports.useFeedContext = useFeedContext;
5836
+ exports.useFeedMetadata = useFeedMetadata;
5530
5837
  exports.useFeedsClient = useFeedsClient;
5838
+ exports.useFollowers = useFollowers;
5839
+ exports.useFollowing = useFollowing;
5531
5840
  exports.useOwnCapabilities = useOwnCapabilities;
5841
+ exports.useOwnFollows = useOwnFollows;
5842
+ exports.useReactionActions = useReactionActions;
5532
5843
  exports.useStateStore = useStateStore;
5533
5844
  exports.useWsConnectionState = useWsConnectionState;
5534
5845
  //# sourceMappingURL=index-react-bindings.node.cjs.map