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