@stream-io/feeds-client 0.1.4 → 0.1.5

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 (51) hide show
  1. package/@react-bindings/hooks/client-state-hooks/index.ts +2 -0
  2. package/@react-bindings/hooks/feed-state-hooks/index.ts +5 -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 +16 -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 +5 -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/useFollowers.d.ts +16 -0
  15. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowing.d.ts +16 -0
  16. package/dist/@react-bindings/hooks/{useOwnCapabilities.d.ts → feed-state-hooks/useOwnCapabilities.d.ts} +2 -2
  17. package/dist/@react-bindings/hooks/internal/index.d.ts +1 -0
  18. package/dist/@react-bindings/hooks/internal/useStableCallback.d.ts +25 -0
  19. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  20. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +17 -0
  21. package/dist/@react-bindings/index.d.ts +5 -3
  22. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +12 -0
  23. package/dist/index-react-bindings.browser.cjs +451 -183
  24. package/dist/index-react-bindings.browser.cjs.map +1 -1
  25. package/dist/index-react-bindings.browser.js +446 -185
  26. package/dist/index-react-bindings.browser.js.map +1 -1
  27. package/dist/index-react-bindings.node.cjs +451 -183
  28. package/dist/index-react-bindings.node.cjs.map +1 -1
  29. package/dist/index-react-bindings.node.js +446 -185
  30. package/dist/index-react-bindings.node.js.map +1 -1
  31. package/dist/index.browser.cjs +167 -72
  32. package/dist/index.browser.cjs.map +1 -1
  33. package/dist/index.browser.js +164 -73
  34. package/dist/index.browser.js.map +1 -1
  35. package/dist/index.node.cjs +167 -72
  36. package/dist/index.node.cjs.map +1 -1
  37. package/dist/index.node.js +164 -73
  38. package/dist/index.node.js.map +1 -1
  39. package/dist/src/Feed.d.ts +7 -3
  40. package/dist/src/FeedsClient.d.ts +4 -3
  41. package/dist/src/types.d.ts +7 -0
  42. package/dist/src/utils.d.ts +9 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +2 -1
  45. package/src/Feed.ts +200 -89
  46. package/src/FeedsClient.ts +8 -3
  47. package/src/common/real-time/StableWSConnection.ts +4 -1
  48. package/src/types.ts +12 -1
  49. package/src/utils.ts +25 -1
  50. package/dist/@react-bindings/hooks/clientStateHooks.d.ts +0 -10
  51. package/dist/@react-bindings/hooks/useComments.d.ts +0 -12
@@ -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
  };
@@ -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);
@@ -4120,8 +4014,15 @@ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4120
4014
  return updateActivityInActivities(updatedActivity, activities);
4121
4015
  };
4122
4016
 
4123
- const END_OF_LIST = 'eol';
4124
- const DEFAULT_COMMENT_PAGINATION = 'first';
4017
+ const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4018
+ typeof cursor === 'string';
4019
+ const isCommentResponse = (entity) => {
4020
+ return typeof entity?.object_id === 'string';
4021
+ };
4022
+ const Constants = {
4023
+ DEFAULT_COMMENT_PAGINATION: 'first',
4024
+ };
4025
+
4125
4026
  class Feed extends FeedApi {
4126
4027
  constructor(client, groupId, id, data) {
4127
4028
  // Need this ugly cast because fileUpload endpoints :(
@@ -4194,7 +4095,7 @@ class Feed extends FeedApi {
4194
4095
  const entityState = currentState.comments_by_entity_id[forId];
4195
4096
  const newComments = entityState?.comments?.concat([]) ?? [];
4196
4097
  if (entityState?.pagination?.sort === 'last' &&
4197
- entityState?.pagination.next === END_OF_LIST) {
4098
+ !checkHasAnotherPage(entityState.comments, entityState?.pagination.next)) {
4198
4099
  newComments.unshift(comment);
4199
4100
  }
4200
4101
  else if (entityState?.pagination?.sort === 'first') {
@@ -4281,7 +4182,7 @@ class Feed extends FeedApi {
4281
4182
  ...currentState,
4282
4183
  ...event.follow.source_feed,
4283
4184
  };
4284
- if (currentState.following_pagination?.next === END_OF_LIST) {
4185
+ if (!checkHasAnotherPage(currentState.following, currentState.following_pagination?.next)) {
4285
4186
  // TODO: respect sort
4286
4187
  newState.following = currentState.following
4287
4188
  ? currentState.following.concat(event.follow)
@@ -4302,7 +4203,7 @@ class Feed extends FeedApi {
4302
4203
  ? currentState.own_follows.concat(event.follow)
4303
4204
  : [event.follow];
4304
4205
  }
4305
- if (currentState.followers_pagination?.next === END_OF_LIST) {
4206
+ if (!checkHasAnotherPage(currentState.followers, currentState.followers_pagination?.next)) {
4306
4207
  // TODO: respect sort
4307
4208
  newState.followers = currentState.followers
4308
4209
  ? currentState.followers.concat(event.follow)
@@ -4343,40 +4244,60 @@ class Feed extends FeedApi {
4343
4244
  'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4344
4245
  'feeds.comment.reaction.updated': Feed.noop,
4345
4246
  '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;
4247
+ const { connectedUser } = this.client.state.getLatestValue();
4350
4248
  this.state.next((currentState) => {
4351
- return {
4352
- ...currentState,
4353
- // TODO: respect sort
4354
- members: currentState.members
4355
- ? currentState.members.concat(member)
4356
- : [member],
4357
- };
4249
+ let newState;
4250
+ if (!checkHasAnotherPage(currentState.members, currentState.member_pagination?.next)) {
4251
+ newState ?? (newState = {
4252
+ ...currentState,
4253
+ });
4254
+ newState.members = newState.members?.concat(event.member) ?? [
4255
+ event.member,
4256
+ ];
4257
+ }
4258
+ if (connectedUser?.id === event.member.user.id) {
4259
+ newState ?? (newState = {
4260
+ ...currentState,
4261
+ });
4262
+ newState.own_membership = event.member;
4263
+ }
4264
+ return newState ?? currentState;
4358
4265
  });
4359
4266
  },
4360
4267
  'feeds.feed_member.removed': (event) => {
4268
+ const { connectedUser } = this.client.state.getLatestValue();
4361
4269
  this.state.next((currentState) => {
4362
- return {
4270
+ const newState = {
4363
4271
  ...currentState,
4364
4272
  members: currentState.members?.filter((member) => member.user.id !== event.user?.id),
4365
4273
  };
4274
+ if (connectedUser?.id === event.member_id) {
4275
+ delete newState.own_membership;
4276
+ }
4277
+ return newState;
4366
4278
  });
4367
4279
  },
4368
4280
  'feeds.feed_member.updated': (event) => {
4281
+ const { connectedUser } = this.client.state.getLatestValue();
4369
4282
  this.state.next((currentState) => {
4370
4283
  const memberIndex = currentState.members?.findIndex((member) => member.user.id === event.member.user.id) ?? -1;
4284
+ let newState;
4371
4285
  if (memberIndex !== -1) {
4286
+ // if there's an index, there's a member to update
4372
4287
  const newMembers = [...currentState.members];
4373
4288
  newMembers[memberIndex] = event.member;
4374
- return {
4289
+ newState ?? (newState = {
4375
4290
  ...currentState,
4376
- members: newMembers,
4377
- };
4291
+ });
4292
+ newState.members = newMembers;
4293
+ }
4294
+ if (connectedUser?.id === event.member.user.id) {
4295
+ newState ?? (newState = {
4296
+ ...currentState,
4297
+ });
4298
+ newState.own_membership = event.member;
4378
4299
  }
4379
- return currentState;
4300
+ return newState ?? currentState;
4380
4301
  });
4381
4302
  },
4382
4303
  // the poll events should be removed from here
@@ -4502,29 +4423,6 @@ class Feed extends FeedApi {
4502
4423
  ...currentState,
4503
4424
  ...responseCopy,
4504
4425
  };
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
4426
  if (!request?.followers_pagination?.limit) {
4529
4427
  delete nextState.followers;
4530
4428
  }
@@ -4615,6 +4513,7 @@ class Feed extends FeedApi {
4615
4513
  });
4616
4514
  }
4617
4515
  async loadNextPageComments({ forId, base, sort, parentId, }) {
4516
+ let error;
4618
4517
  try {
4619
4518
  this.state.next((currentState) => ({
4620
4519
  ...currentState,
@@ -4629,7 +4528,7 @@ class Feed extends FeedApi {
4629
4528
  },
4630
4529
  },
4631
4530
  }));
4632
- const { next: newNextCursor = END_OF_LIST, comments } = await base();
4531
+ const { next: newNextCursor, comments } = await base();
4633
4532
  this.state.next((currentState) => {
4634
4533
  const newPagination = {
4635
4534
  ...currentState.comments_by_entity_id[forId]?.pagination,
@@ -4654,9 +4553,8 @@ class Feed extends FeedApi {
4654
4553
  };
4655
4554
  });
4656
4555
  }
4657
- catch (error) {
4658
- console.error(error);
4659
- // TODO: figure out how to handle errorss
4556
+ catch (e) {
4557
+ error = e;
4660
4558
  }
4661
4559
  finally {
4662
4560
  this.state.next((currentState) => ({
@@ -4673,15 +4571,21 @@ class Feed extends FeedApi {
4673
4571
  },
4674
4572
  }));
4675
4573
  }
4574
+ if (error) {
4575
+ throw error;
4576
+ }
4676
4577
  }
4677
4578
  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)
4579
+ const currentEntityState = this.currentState.comments_by_entity_id[activity.id];
4580
+ const currentPagination = currentEntityState?.pagination;
4581
+ const currentNextCursor = currentPagination?.next;
4582
+ const currentSort = currentPagination?.sort;
4583
+ const isLoading = currentPagination?.loading_next_page;
4584
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4585
+ if (isLoading ||
4586
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4684
4587
  return;
4588
+ }
4685
4589
  await this.loadNextPageComments({
4686
4590
  forId: activity.id,
4687
4591
  base: () => this.client.getComments({
@@ -4695,20 +4599,25 @@ class Feed extends FeedApi {
4695
4599
  });
4696
4600
  }
4697
4601
  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)
4602
+ const currentEntityState = this.currentState.comments_by_entity_id[comment.id];
4603
+ const currentPagination = currentEntityState?.pagination;
4604
+ const currentNextCursor = currentPagination?.next;
4605
+ const currentSort = currentPagination?.sort;
4606
+ const isLoading = currentPagination?.loading_next_page;
4607
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4608
+ if (isLoading ||
4609
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4704
4610
  return;
4611
+ }
4705
4612
  await this.loadNextPageComments({
4706
4613
  forId: comment.id,
4707
4614
  base: () => this.client.getCommentReplies({
4708
4615
  ...request,
4709
4616
  comment_id: comment.id,
4710
4617
  // use known sort first (prevents broken pagination)
4711
- sort: currentSort ?? request?.sort ?? DEFAULT_COMMENT_PAGINATION,
4618
+ sort: currentSort ??
4619
+ request?.sort ??
4620
+ Constants.DEFAULT_COMMENT_PAGINATION,
4712
4621
  next: currentNextCursor,
4713
4622
  }),
4714
4623
  parentId: comment.parent_id ?? comment.object_id,
@@ -4718,10 +4627,14 @@ class Feed extends FeedApi {
4718
4627
  async loadNextPageFollows(type, request) {
4719
4628
  const paginationKey = `${type}_pagination`;
4720
4629
  const method = `query${capitalize(type)}`;
4630
+ const currentFollows = this.currentState[type];
4721
4631
  const currentNextCursor = this.currentState[paginationKey]?.next;
4722
4632
  const isLoading = this.currentState[paginationKey]?.loading_next_page;
4723
- if (isLoading || currentNextCursor === END_OF_LIST)
4633
+ const sort = this.currentState[paginationKey]?.sort ?? request.sort;
4634
+ let error;
4635
+ if (isLoading || !checkHasAnotherPage(currentFollows, currentNextCursor)) {
4724
4636
  return;
4637
+ }
4725
4638
  try {
4726
4639
  this.state.next((currentState) => {
4727
4640
  return {
@@ -4732,9 +4645,10 @@ class Feed extends FeedApi {
4732
4645
  },
4733
4646
  };
4734
4647
  });
4735
- const { next: newNextCursor = END_OF_LIST, follows } = await this[method]({
4648
+ const { next: newNextCursor, follows } = await this[method]({
4736
4649
  ...request,
4737
4650
  next: currentNextCursor,
4651
+ sort,
4738
4652
  });
4739
4653
  this.state.next((currentState) => ({
4740
4654
  ...currentState,
@@ -4744,12 +4658,12 @@ class Feed extends FeedApi {
4744
4658
  [paginationKey]: {
4745
4659
  ...currentState[paginationKey],
4746
4660
  next: newNextCursor,
4661
+ sort,
4747
4662
  },
4748
4663
  }));
4749
4664
  }
4750
- catch (error) {
4751
- console.error(error);
4752
- // TODO: figure out how to handle errorss
4665
+ catch (e) {
4666
+ error = e;
4753
4667
  }
4754
4668
  finally {
4755
4669
  this.state.next((currentState) => {
@@ -4762,6 +4676,9 @@ class Feed extends FeedApi {
4762
4676
  };
4763
4677
  });
4764
4678
  }
4679
+ if (error) {
4680
+ throw error;
4681
+ }
4765
4682
  }
4766
4683
  async loadNextPageFollowers(request) {
4767
4684
  await this.loadNextPageFollows('followers', request);
@@ -4769,6 +4686,59 @@ class Feed extends FeedApi {
4769
4686
  async loadNextPageFollowing(request) {
4770
4687
  await this.loadNextPageFollows('following', request);
4771
4688
  }
4689
+ async loadNextPageMembers(request) {
4690
+ const currentMembers = this.currentState.members;
4691
+ const currentNextCursor = this.currentState.member_pagination?.next;
4692
+ const isLoading = this.currentState.member_pagination?.loading_next_page;
4693
+ const sort = this.currentState.member_pagination?.sort ?? request.sort;
4694
+ let error;
4695
+ if (isLoading || !checkHasAnotherPage(currentMembers, currentNextCursor)) {
4696
+ return;
4697
+ }
4698
+ try {
4699
+ this.state.next((currentState) => ({
4700
+ ...currentState,
4701
+ member_pagination: {
4702
+ ...currentState.member_pagination,
4703
+ loading_next_page: true,
4704
+ },
4705
+ }));
4706
+ const { next: newNextCursor, members } = await this.client.queryFeedMembers({
4707
+ ...request,
4708
+ sort,
4709
+ feed_id: this.id,
4710
+ feed_group_id: this.group,
4711
+ next: currentNextCursor,
4712
+ });
4713
+ this.state.next((currentState) => ({
4714
+ ...currentState,
4715
+ members: currentState.members
4716
+ ? currentState.members.concat(members)
4717
+ : members,
4718
+ member_pagination: {
4719
+ ...currentState.member_pagination,
4720
+ next: newNextCursor,
4721
+ // set sort if not defined yet
4722
+ sort: currentState.member_pagination?.sort ?? request.sort,
4723
+ },
4724
+ }));
4725
+ }
4726
+ catch (e) {
4727
+ error = e;
4728
+ }
4729
+ finally {
4730
+ this.state.next((currentState) => ({
4731
+ ...currentState,
4732
+ member_pagination: {
4733
+ ...currentState.member_pagination,
4734
+ loading_next_page: false,
4735
+ },
4736
+ }));
4737
+ }
4738
+ if (error) {
4739
+ throw error;
4740
+ }
4741
+ }
4772
4742
  /**
4773
4743
  * Method which queries followers of this feed (feeds which target this feed).
4774
4744
  *
@@ -5259,6 +5229,9 @@ class FeedsClient extends FeedsApi {
5259
5229
  throw err;
5260
5230
  }
5261
5231
  };
5232
+ this.devToken = (userId) => {
5233
+ return streamDevToken(userId);
5234
+ };
5262
5235
  this.closePoll = async (request) => {
5263
5236
  return await this.updatePollPartial({
5264
5237
  poll_id: request.poll_id,
@@ -5517,18 +5490,313 @@ const useCreateFeedsClient = ({ apiKey, tokenOrProvider, userData, options, }) =
5517
5490
  return client;
5518
5491
  };
5519
5492
 
5493
+ const StreamFeedsContext = react.createContext(undefined);
5494
+ /**
5495
+ * Hook to access the nearest FeedsClient instance.
5496
+ */
5497
+ const useFeedsClient = () => {
5498
+ return react.useContext(StreamFeedsContext);
5499
+ };
5500
+
5501
+ /**
5502
+ * A React hook that returns the currently connected user on a `FeedsClient` instance and null otherwise.
5503
+ */
5504
+ const useClientConnectedUser = () => {
5505
+ const client = useFeedsClient();
5506
+ const { user } = useStateStore(client?.state, selector$5) ?? {};
5507
+ return user;
5508
+ };
5509
+ const selector$5 = (nextState) => ({
5510
+ user: nextState.connectedUser,
5511
+ });
5512
+
5513
+ /**
5514
+ * A React hook that returns the websocket connection state of `FeedsClient`.
5515
+ */
5516
+ const useWsConnectionState = () => {
5517
+ const client = useFeedsClient();
5518
+ const { isHealthy } = useStateStore(client?.state, selector$4) ?? {};
5519
+ return { isHealthy };
5520
+ };
5521
+ const selector$4 = (nextState) => ({
5522
+ isHealthy: nextState.isWsConnectionHealthy,
5523
+ });
5524
+
5525
+ const StreamFeedContext = react.createContext(undefined);
5526
+ /**
5527
+ * Hook to access the nearest Feed instance.
5528
+ */
5529
+ const useFeedContext = () => {
5530
+ return react.useContext(StreamFeedContext);
5531
+ };
5532
+
5533
+ /**
5534
+ * A utility hook implementing a stable callback. It takes in an unstable method that
5535
+ * is supposed to be invoked somewhere deeper in the DOM tree without making it
5536
+ * change its reference every time the parent component rerenders. It will also return
5537
+ * the value of the callback if it does return one.
5538
+ * A common use-case would be having a function whose invocation depends on state
5539
+ * somewhere high up in the DOM tree and wanting to use the same function deeper
5540
+ * down, for example in a leaf node and simply using useCallback results in
5541
+ * cascading dependency hell. If we wrap it in useStableCallback, we would be able
5542
+ * to:
5543
+ * - Use the same function as a dependency of another hook (since it is stable)
5544
+ * - Still invoke it and get the latest state
5545
+ *
5546
+ * **Caveats:**
5547
+ * - Never wrap a function that is supposed to return a React.ReactElement in
5548
+ * useStableCallback, since React will not know that the DOM needs to be updated
5549
+ * whenever the callback value changes (for example, renderItem from FlatList must
5550
+ * never be wrapped in this hook)
5551
+ * - Always prefer using a standard useCallback/stable function wherever possible
5552
+ * (the purpose of useStableCallback is to bridge the gap between top level contexts
5553
+ * and cascading rereders in downstream components - **not** as an escape hatch)
5554
+ * @param callback - the callback we want to stabilize
5555
+ */
5556
+ const useStableCallback = (callback) => {
5557
+ const ref = react.useRef(callback);
5558
+ ref.current = callback;
5559
+ return react.useCallback((...args) => {
5560
+ return ref.current(...args);
5561
+ }, []);
5562
+ };
5563
+
5564
+ /**
5565
+ * A React hook that returns a reactive object containing the current activities,
5566
+ * loading state and whether there is a next page to paginate to or not.
5567
+ */
5568
+ const useFeedActivities = (feedFromProps) => {
5569
+ const feedFromContext = useFeedContext();
5570
+ const feed = feedFromProps ?? feedFromContext;
5571
+ const data = useStateStore(feed?.state, selector$3);
5572
+ const loadNextPage = useStableCallback(async () => {
5573
+ if (!feed || !data?.hasNextPage || data?.isLoading) {
5574
+ return;
5575
+ }
5576
+ await feed.getNextPage();
5577
+ });
5578
+ return react.useMemo(() => ({ ...data, loadNextPage }), [data, loadNextPage]);
5579
+ };
5580
+ const selector$3 = (nextState) => ({
5581
+ isLoading: nextState.is_loading_activities,
5582
+ hasNextPage: typeof nextState.next !== 'undefined',
5583
+ activities: nextState.activities ?? [],
5584
+ });
5585
+
5586
+ function useComments({ feed: feedFromProps, parent, }) {
5587
+ const feedFromContext = useFeedContext();
5588
+ const feed = feedFromProps ?? feedFromContext;
5589
+ const selector = react.useCallback((state) => ({
5590
+ comments: state.comments_by_entity_id?.[parent.id]?.comments,
5591
+ comments_pagination: state.comments_by_entity_id?.[parent.id]?.pagination,
5592
+ }), [parent.id]);
5593
+ const data = useStateStore(feed?.state, selector);
5594
+ const loadNextPage = react.useMemo(() => {
5595
+ if (!feed)
5596
+ return undefined;
5597
+ return (request) => {
5598
+ if (isCommentResponse(parent)) {
5599
+ return feed.loadNextPageCommentReplies(parent, request);
5600
+ }
5601
+ else {
5602
+ return feed.loadNextPageActivityComments(parent, request);
5603
+ }
5604
+ };
5605
+ }, [feed, parent]);
5606
+ return react.useMemo(() => {
5607
+ if (!data) {
5608
+ return undefined;
5609
+ }
5610
+ return {
5611
+ ...data,
5612
+ hasNextPage: checkHasAnotherPage(data.comments, data.comments_pagination?.next),
5613
+ isLoadingNextPage: data?.comments_pagination?.loading_next_page ?? false,
5614
+ loadNextPage,
5615
+ };
5616
+ }, [data, loadNextPage]);
5617
+ }
5618
+
5619
+ const FeedOwnCapability = {
5620
+ ADD_ACTIVITY: 'add-activity',
5621
+ ADD_ACTIVITY_REACTION: 'add-activity-reaction',
5622
+ ADD_COMMENT: 'add-comment',
5623
+ ADD_COMMENT_REACTION: 'add-comment-reaction',
5624
+ BOOKMARK_ACTIVITY: 'bookmark-activity',
5625
+ CREATE_FEED: 'create-feed',
5626
+ DELETE_BOOKMARK: 'delete-bookmark',
5627
+ DELETE_COMMENT: 'delete-comment',
5628
+ DELETE_FEED: 'delete-feed',
5629
+ EDIT_BOOKMARK: 'edit-bookmark',
5630
+ FOLLOW: 'follow',
5631
+ INVITE_FEED: 'invite-feed',
5632
+ JOIN_FEED: 'join-feed',
5633
+ LEAVE_FEED: 'leave-feed',
5634
+ MANAGE_FEED_GROUP: 'manage-feed-group',
5635
+ MARK_ACTIVITY: 'mark-activity',
5636
+ PIN_ACTIVITY: 'pin-activity',
5637
+ QUERY_FEED_MEMBERS: 'query-feed-members',
5638
+ QUERY_FOLLOWS: 'query-follows',
5639
+ READ_ACTIVITIES: 'read-activities',
5640
+ READ_FEED: 'read-feed',
5641
+ REMOVE_ACTIVITY: 'remove-activity',
5642
+ REMOVE_ACTIVITY_REACTION: 'remove-activity-reaction',
5643
+ REMOVE_COMMENT_REACTION: 'remove-comment-reaction',
5644
+ UNFOLLOW: 'unfollow',
5645
+ UPDATE_ACTIVITY: 'update-activity',
5646
+ UPDATE_COMMENT: 'update-comment',
5647
+ UPDATE_FEED: 'update-feed',
5648
+ UPDATE_FEED_FOLLOWERS: 'update-feed-followers',
5649
+ UPDATE_FEED_MEMBERS: 'update-feed-members',
5650
+ };
5651
+
5652
+ const stableEmptyArray = [];
5653
+ const selector$2 = (currentState) => ({
5654
+ oc: currentState.own_capabilities ?? stableEmptyArray,
5655
+ });
5656
+ const useOwnCapabilities = (feedFromProps) => {
5657
+ const feedFromContext = useFeedContext();
5658
+ const feed = feedFromProps ?? feedFromContext;
5659
+ const { oc = stableEmptyArray } = useStateStore(feed?.state, selector$2) ?? {};
5660
+ return react.useMemo(() => ({
5661
+ canAddActivity: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY) > -1,
5662
+ canAddActivityReaction: oc.indexOf(FeedOwnCapability.ADD_ACTIVITY_REACTION) > -1,
5663
+ canAddComment: oc.indexOf(FeedOwnCapability.ADD_COMMENT) > -1,
5664
+ canAddCommentReaction: oc.indexOf(FeedOwnCapability.ADD_COMMENT_REACTION) > -1,
5665
+ canBookmarkActivity: oc.indexOf(FeedOwnCapability.BOOKMARK_ACTIVITY) > -1,
5666
+ canCreateFeed: oc.indexOf(FeedOwnCapability.CREATE_FEED) > -1,
5667
+ canDeleteBookmark: oc.indexOf(FeedOwnCapability.DELETE_BOOKMARK) > -1,
5668
+ canDeleteComment: oc.indexOf(FeedOwnCapability.DELETE_COMMENT) > -1,
5669
+ canDeleteFeed: oc.indexOf(FeedOwnCapability.DELETE_FEED) > -1,
5670
+ canEditBookmark: oc.indexOf(FeedOwnCapability.EDIT_BOOKMARK) > -1,
5671
+ canFollow: oc.indexOf(FeedOwnCapability.FOLLOW) > -1,
5672
+ canRemoveActivity: oc.indexOf(FeedOwnCapability.REMOVE_ACTIVITY) > -1,
5673
+ canRemoveActivityReaction: oc.indexOf(FeedOwnCapability.REMOVE_ACTIVITY_REACTION) > -1,
5674
+ canRemoveCommentReaction: oc.indexOf(FeedOwnCapability.REMOVE_COMMENT_REACTION) > -1,
5675
+ canUnfollow: oc.indexOf(FeedOwnCapability.UNFOLLOW) > -1,
5676
+ canUpdateFeed: oc.indexOf(FeedOwnCapability.UPDATE_FEED) > -1,
5677
+ canInviteFeed: oc.indexOf(FeedOwnCapability.INVITE_FEED) > -1,
5678
+ canJoinFeed: oc.indexOf(FeedOwnCapability.JOIN_FEED) > -1,
5679
+ canLeaveFeed: oc.indexOf(FeedOwnCapability.LEAVE_FEED) > -1,
5680
+ canManageFeedGroup: oc.indexOf(FeedOwnCapability.MANAGE_FEED_GROUP) > -1,
5681
+ canMarkActivity: oc.indexOf(FeedOwnCapability.MARK_ACTIVITY) > -1,
5682
+ canPinActivity: oc.indexOf(FeedOwnCapability.PIN_ACTIVITY) > -1,
5683
+ canQueryFeedMembers: oc.indexOf(FeedOwnCapability.QUERY_FEED_MEMBERS) > -1,
5684
+ canQueryFollows: oc.indexOf(FeedOwnCapability.QUERY_FOLLOWS) > -1,
5685
+ canReadActivities: oc.indexOf(FeedOwnCapability.READ_ACTIVITIES) > -1,
5686
+ canReadFeed: oc.indexOf(FeedOwnCapability.READ_FEED) > -1,
5687
+ canUpdateActivity: oc.indexOf(FeedOwnCapability.UPDATE_ACTIVITY) > -1,
5688
+ canUpdateComment: oc.indexOf(FeedOwnCapability.UPDATE_COMMENT) > -1,
5689
+ canUpdateFeedFollowers: oc.indexOf(FeedOwnCapability.UPDATE_FEED_FOLLOWERS) > -1,
5690
+ canUpdateFeedMembers: oc.indexOf(FeedOwnCapability.UPDATE_FEED_MEMBERS) > -1,
5691
+ }), [oc]);
5692
+ };
5693
+
5694
+ const selector$1 = ({ follower_count, followers, followers_pagination, }) => ({
5695
+ follower_count,
5696
+ followers,
5697
+ followers_pagination,
5698
+ });
5699
+ function useFollowers(feedFromProps) {
5700
+ const feedFromContext = useFeedContext();
5701
+ const feed = feedFromProps ?? feedFromContext;
5702
+ const data = useStateStore(feed?.state, selector$1);
5703
+ const loadNextPage = react.useCallback((...options) => feed?.loadNextPageFollowers(...options), [feed]);
5704
+ return react.useMemo(() => {
5705
+ if (!data) {
5706
+ return undefined;
5707
+ }
5708
+ return {
5709
+ ...data,
5710
+ isLoadingNextPage: data.followers_pagination?.loading_next_page ?? false,
5711
+ hasNextPage: checkHasAnotherPage(data.followers, data.followers_pagination?.next),
5712
+ loadNextPage,
5713
+ };
5714
+ }, [data, loadNextPage]);
5715
+ }
5716
+
5717
+ const selector = ({ following_count, following, following_pagination, }) => ({
5718
+ following_count,
5719
+ following,
5720
+ following_pagination,
5721
+ });
5722
+ function useFollowing(feedFromProps) {
5723
+ const feedFromContext = useFeedContext();
5724
+ const feed = feedFromProps ?? feedFromContext;
5725
+ const data = useStateStore(feed?.state, selector);
5726
+ const loadNextPage = react.useCallback((...options) => feed?.loadNextPageFollowing(...options), [feed]);
5727
+ return react.useMemo(() => {
5728
+ if (!data) {
5729
+ return undefined;
5730
+ }
5731
+ return {
5732
+ ...data,
5733
+ isLoadingNextPage: data.following_pagination?.loading_next_page ?? false,
5734
+ hasNextPage: checkHasAnotherPage(data.following, data.following_pagination?.next),
5735
+ loadNextPage,
5736
+ };
5737
+ }, [data, loadNextPage]);
5738
+ }
5739
+
5740
+ /**
5741
+ * A utility hook that takes in an entity and a reaction type, and creates reaction actions
5742
+ * that can then be used on the UI. The entity can be either an ActivityResponse or a CommentResponse
5743
+ * as the hook determines internally which APIs it is supposed to use, while taking the
5744
+ * correct ownCapabilities into account.
5745
+ * @param entity - The entity to which we want to add a reaction, can be either ActivityResponse or CommentResponse.
5746
+ * @param type - The type of reaction we want to add or remove.
5747
+ */
5748
+ const useReactionActions = ({ entity, type, }) => {
5749
+ const client = useFeedsClient();
5750
+ const isComment = isCommentResponse(entity);
5751
+ const hasOwnReaction = react.useMemo(() => !!entity.own_reactions?.find((r) => r.type === type), [entity.own_reactions, type]);
5752
+ const addReaction = useStableCallback(async () => {
5753
+ await (isComment
5754
+ ? client?.addCommentReaction({ comment_id: entity.id, type })
5755
+ : client?.addReaction({ activity_id: entity.id, type }));
5756
+ });
5757
+ const removeReaction = useStableCallback(async () => {
5758
+ await (isComment
5759
+ ? client?.deleteCommentReaction({ comment_id: entity.id, type })
5760
+ : client?.deleteActivityReaction({
5761
+ activity_id: entity.id,
5762
+ type,
5763
+ }));
5764
+ });
5765
+ const toggleReaction = useStableCallback(async () => {
5766
+ if (hasOwnReaction) {
5767
+ await removeReaction();
5768
+ }
5769
+ else {
5770
+ await addReaction();
5771
+ }
5772
+ });
5773
+ return react.useMemo(() => ({ addReaction, removeReaction, toggleReaction }), [addReaction, removeReaction, toggleReaction]);
5774
+ };
5775
+
5520
5776
  const StreamFeeds = ({ client, children }) => {
5521
5777
  return (jsxRuntime.jsx(StreamFeedsContext.Provider, { value: client, children: children }));
5522
5778
  };
5523
5779
  StreamFeeds.displayName = 'StreamFeeds';
5524
5780
 
5781
+ const StreamFeed = ({ feed, children }) => {
5782
+ return (jsxRuntime.jsx(StreamFeedContext.Provider, { value: feed, children: children }));
5783
+ };
5784
+ StreamFeed.displayName = 'StreamFeed';
5785
+
5786
+ exports.StreamFeed = StreamFeed;
5787
+ exports.StreamFeedContext = StreamFeedContext;
5525
5788
  exports.StreamFeeds = StreamFeeds;
5526
5789
  exports.StreamFeedsContext = StreamFeedsContext;
5527
5790
  exports.useClientConnectedUser = useClientConnectedUser;
5528
5791
  exports.useComments = useComments;
5529
5792
  exports.useCreateFeedsClient = useCreateFeedsClient;
5793
+ exports.useFeedActivities = useFeedActivities;
5794
+ exports.useFeedContext = useFeedContext;
5530
5795
  exports.useFeedsClient = useFeedsClient;
5796
+ exports.useFollowers = useFollowers;
5797
+ exports.useFollowing = useFollowing;
5531
5798
  exports.useOwnCapabilities = useOwnCapabilities;
5799
+ exports.useReactionActions = useReactionActions;
5532
5800
  exports.useStateStore = useStateStore;
5533
5801
  exports.useWsConnectionState = useWsConnectionState;
5534
5802
  //# sourceMappingURL=index-react-bindings.node.cjs.map