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