@stream-io/feeds-client 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/@react-bindings/hooks/util/index.ts +1 -0
  2. package/CHANGELOG.md +20 -0
  3. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  4. package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
  5. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
  6. package/dist/index-react-bindings.browser.cjs +363 -141
  7. package/dist/index-react-bindings.browser.cjs.map +1 -1
  8. package/dist/index-react-bindings.browser.js +363 -142
  9. package/dist/index-react-bindings.browser.js.map +1 -1
  10. package/dist/index-react-bindings.node.cjs +363 -141
  11. package/dist/index-react-bindings.node.cjs.map +1 -1
  12. package/dist/index-react-bindings.node.js +363 -142
  13. package/dist/index-react-bindings.node.js.map +1 -1
  14. package/dist/index.browser.cjs +337 -140
  15. package/dist/index.browser.cjs.map +1 -1
  16. package/dist/index.browser.js +337 -141
  17. package/dist/index.browser.js.map +1 -1
  18. package/dist/index.node.cjs +337 -140
  19. package/dist/index.node.cjs.map +1 -1
  20. package/dist/index.node.js +337 -141
  21. package/dist/index.node.js.map +1 -1
  22. package/dist/src/Feed.d.ts +42 -11
  23. package/dist/src/FeedsClient.d.ts +10 -3
  24. package/dist/src/common/real-time/StableWSConnection.d.ts +3 -3
  25. package/dist/src/gen/models/index.d.ts +25 -2
  26. package/dist/src/gen-imports.d.ts +1 -1
  27. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  28. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  29. package/dist/src/utils.d.ts +1 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +1 -1
  32. package/src/Feed.ts +230 -192
  33. package/src/FeedsClient.ts +75 -3
  34. package/src/gen/feeds/FeedsApi.ts +0 -1
  35. package/src/gen/model-decoders/decoders.ts +16 -0
  36. package/src/gen/model-decoders/event-decoder-mapping.ts +3 -0
  37. package/src/gen/models/index.ts +42 -4
  38. package/src/gen-imports.ts +1 -1
  39. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  40. package/src/state-updates/activity-utils.test.ts +1 -0
  41. package/src/state-updates/follow-utils.test.ts +552 -0
  42. package/src/state-updates/follow-utils.ts +126 -0
  43. package/src/state-updates/state-update-queue.test.ts +53 -0
  44. package/src/state-updates/state-update-queue.ts +35 -0
  45. package/src/utils.test.ts +175 -0
  46. package/src/utils.ts +20 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/feeds-client",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/index.node.js",
6
6
  "exports": {
package/src/Feed.ts CHANGED
@@ -16,8 +16,10 @@ import {
16
16
  BookmarkUpdatedEvent,
17
17
  QueryFeedMembersRequest,
18
18
  SortParamRequest,
19
+ FollowResponse,
20
+ ThreadedCommentResponse,
19
21
  } from './gen/models';
20
- import { Patch, StateStore } from './common/StateStore';
22
+ import { StateStore } from './common/StateStore';
21
23
  import { EventDispatcher } from './common/EventDispatcher';
22
24
  import { FeedApi } from './gen/feeds/FeedApi';
23
25
  import { FeedsClient } from './FeedsClient';
@@ -35,7 +37,12 @@ import {
35
37
  removeBookmarkFromActivities,
36
38
  updateBookmarkInActivities,
37
39
  } from './state-updates/bookmark-utils';
38
- import { FeedsApi, StreamResponse } from './gen-imports';
40
+ import {
41
+ handleFollowCreated,
42
+ handleFollowDeleted,
43
+ handleFollowUpdated,
44
+ } from './state-updates/follow-utils';
45
+ import { StreamResponse } from './gen-imports';
39
46
  import { capitalize } from './common/utils';
40
47
  import type {
41
48
  ActivityIdOrCommentId,
@@ -44,8 +51,12 @@ import type {
44
51
  LoadingStates,
45
52
  PagerResponseWithLoadingStates,
46
53
  } from './types';
47
- import type { FromArray } from './types-internal';
48
- import { checkHasAnotherPage, Constants } from './utils';
54
+ import { checkHasAnotherPage, Constants, uniqueArrayMerge } from './utils';
55
+ import {
56
+ getStateUpdateQueueIdForFollow,
57
+ getStateUpdateQueueIdForUnfollow,
58
+ shouldUpdateState,
59
+ } from './state-updates/state-update-queue';
49
60
 
50
61
  export type FeedState = Omit<
51
62
  Partial<GetOrCreateFeedResponse & FeedResponse>,
@@ -90,17 +101,17 @@ export type FeedState = Omit<
90
101
  * comments_by_entity_id: {
91
102
  * 'activity-1': {
92
103
  * comments: [comment1],
93
- * parent_id: undefined,
104
+ * entity_parent_id: undefined,
94
105
  * },
95
106
  * 'comment-1': {
96
107
  * comments: [comment2],
97
- * parent_id: 'activity-1', // parent store where "comment-1" is located in "comments" array
108
+ * entity_parent_id: 'activity-1', // parent store where "comment-1" is located in "comments" array
98
109
  * }
99
110
  * }
100
111
  * }
101
112
  * ```
102
113
  */
103
- parent_id?: ActivityIdOrCommentId;
114
+ entity_parent_id?: ActivityIdOrCommentId;
104
115
  comments?: CommentResponse[];
105
116
  }
106
117
  | undefined
@@ -113,6 +124,11 @@ export type FeedState = Omit<
113
124
  member_pagination?: LoadingStates & { sort?: SortParamRequest[] };
114
125
 
115
126
  last_get_or_create_request_config?: GetOrCreateFeedRequest;
127
+
128
+ /**
129
+ * `true` if the feed is receiving real-time updates via WebSocket
130
+ */
131
+ watch: boolean;
116
132
  };
117
133
 
118
134
  type EventHandlerByEventType = {
@@ -127,6 +143,7 @@ type EventHandlerByEventType = {
127
143
  export class Feed extends FeedApi {
128
144
  readonly state: StateStore<FeedState>;
129
145
  private static readonly noop = () => {};
146
+ private readonly stateUpdateQueue: Set<string> = new Set();
130
147
 
131
148
  private readonly eventHandlers: EventHandlerByEventType = {
132
149
  'feeds.activity.added': (event) => {
@@ -314,102 +331,17 @@ export class Feed extends FeedApi {
314
331
  'feeds.feed_group.changed': Feed.noop,
315
332
  'feeds.feed_group.deleted': Feed.noop,
316
333
  'feeds.follow.created': (event) => {
317
- // filter non-accepted follows (the way getOrCreate does by default)
318
- if (event.follow.status !== 'accepted') return;
319
-
320
- // this feed followed someone
321
- if (event.follow.source_feed.fid === this.fid) {
322
- this.state.next((currentState) => {
323
- const newState = {
324
- ...currentState,
325
- ...event.follow.source_feed,
326
- };
327
-
328
- if (
329
- !checkHasAnotherPage(
330
- currentState.following,
331
- currentState.following_pagination?.next,
332
- )
333
- ) {
334
- // TODO: respect sort
335
- newState.following = currentState.following
336
- ? currentState.following.concat(event.follow)
337
- : [event.follow];
338
- }
339
-
340
- return newState;
341
- });
342
- } else if (
343
- // someone followed this feed
344
- event.follow.target_feed.fid === this.fid
345
- ) {
346
- const source = event.follow.source_feed;
347
- const connectedUser = this.client.state.getLatestValue().connected_user;
348
-
349
- this.state.next((currentState) => {
350
- const newState = { ...currentState, ...event.follow.target_feed };
351
-
352
- if (source.created_by.id === connectedUser?.id) {
353
- newState.own_follows = currentState.own_follows
354
- ? currentState.own_follows.concat(event.follow)
355
- : [event.follow];
356
- }
357
-
358
- if (
359
- !checkHasAnotherPage(
360
- currentState.followers,
361
- currentState.followers_pagination?.next,
362
- )
363
- ) {
364
- // TODO: respect sort
365
- newState.followers = currentState.followers
366
- ? currentState.followers.concat(event.follow)
367
- : [event.follow];
368
- }
369
-
370
- return newState;
371
- });
372
- }
334
+ this.handleFollowCreated(event.follow);
373
335
  },
374
336
  'feeds.follow.deleted': (event) => {
375
- // this feed unfollowed someone
376
- if (event.follow.source_feed.fid === this.fid) {
377
- this.state.next((currentState) => {
378
- return {
379
- ...currentState,
380
- ...event.follow.source_feed,
381
- following: currentState.following?.filter(
382
- (follow) =>
383
- follow.target_feed.fid !== event.follow.target_feed.fid,
384
- ),
385
- };
386
- });
387
- } else if (
388
- // someone unfollowed this feed
389
- event.follow.target_feed.fid === this.fid
390
- ) {
391
- const source = event.follow.source_feed;
392
- const connectedUser = this.client.state.getLatestValue().connected_user;
393
-
394
- this.state.next((currentState) => {
395
- const newState = { ...currentState, ...event.follow.target_feed };
396
-
397
- if (source.created_by.id === connectedUser?.id) {
398
- newState.own_follows = currentState.own_follows?.filter(
399
- (follow) =>
400
- follow.source_feed.fid !== event.follow.source_feed.fid,
401
- );
402
- }
403
-
404
- newState.followers = currentState.followers?.filter(
405
- (follow) => follow.source_feed.fid !== event.follow.source_feed.fid,
406
- );
407
-
408
- return newState;
409
- });
337
+ this.handleFollowDeleted(event.follow);
338
+ },
339
+ 'feeds.follow.updated': (_event) => {
340
+ const result = handleFollowUpdated(this.currentState);
341
+ if (result.changed) {
342
+ this.state.next(result.data);
410
343
  }
411
344
  },
412
- 'feeds.follow.updated': Feed.noop,
413
345
  'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
414
346
  'feeds.comment.reaction.deleted':
415
347
  this.handleCommentReactionEvent.bind(this),
@@ -421,19 +353,12 @@ export class Feed extends FeedApi {
421
353
  this.state.next((currentState) => {
422
354
  let newState: FeedState | undefined;
423
355
 
424
- if (
425
- !checkHasAnotherPage(
426
- currentState.members,
427
- currentState.member_pagination?.next,
428
- )
429
- ) {
356
+ if (typeof currentState.members !== 'undefined') {
430
357
  newState ??= {
431
358
  ...currentState,
432
359
  };
433
360
 
434
- newState.members = newState.members?.concat(event.member) ?? [
435
- event.member,
436
- ];
361
+ newState.members = [event.member, ...currentState.members];
437
362
  }
438
363
 
439
364
  if (connectedUser?.id === event.member.user.id) {
@@ -501,6 +426,10 @@ export class Feed extends FeedApi {
501
426
  return newState ?? currentState;
502
427
  });
503
428
  },
429
+ 'feeds.notification_feed.updated': (event) => {
430
+ console.info('notification feed updated', event);
431
+ // TODO: handle notification feed updates
432
+ },
504
433
  // the poll events should be removed from here
505
434
  'feeds.poll.closed': Feed.noop,
506
435
  'feeds.poll.deleted': Feed.noop,
@@ -531,9 +460,9 @@ export class Feed extends FeedApi {
531
460
  groupId: 'user' | 'timeline' | (string & {}),
532
461
  id: string,
533
462
  data?: FeedResponse,
463
+ watch = false,
534
464
  ) {
535
- // Need this ugly cast because fileUpload endpoints :(
536
- super(client as unknown as FeedsApi, groupId, id);
465
+ super(client, groupId, id);
537
466
  this.state = new StateStore<FeedState>({
538
467
  fid: `${groupId}:${id}`,
539
468
  group_id: groupId,
@@ -542,6 +471,7 @@ export class Feed extends FeedApi {
542
471
  is_loading: false,
543
472
  is_loading_activities: false,
544
473
  comments_by_entity_id: {},
474
+ watch,
545
475
  });
546
476
  this.client = client;
547
477
  }
@@ -652,6 +582,8 @@ export class Feed extends FeedApi {
652
582
  });
653
583
  }
654
584
  } else {
585
+ // Empty queue when reinitializing the state
586
+ this.stateUpdateQueue.clear();
655
587
  const responseCopy: Partial<
656
588
  StreamResponse<GetOrCreateFeedResponse>['feed'] &
657
589
  StreamResponse<GetOrCreateFeedResponse>
@@ -676,8 +608,12 @@ export class Feed extends FeedApi {
676
608
  if (!request?.following_pagination?.limit) {
677
609
  delete nextState.following;
678
610
  }
611
+ if (response.members.length === 0 && response.feed.member_count > 0) {
612
+ delete nextState.members;
613
+ }
679
614
 
680
615
  nextState.last_get_or_create_request_config = request;
616
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
681
617
 
682
618
  return nextState;
683
619
  });
@@ -694,6 +630,80 @@ export class Feed extends FeedApi {
694
630
  }
695
631
  }
696
632
 
633
+ /**
634
+ * @internal
635
+ */
636
+ handleFollowCreated(follow: FollowResponse) {
637
+ if (
638
+ !shouldUpdateState({
639
+ stateUpdateId: getStateUpdateQueueIdForFollow(follow),
640
+ stateUpdateQueue: this.stateUpdateQueue,
641
+ watch: this.currentState.watch,
642
+ })
643
+ ) {
644
+ return;
645
+ }
646
+ const connectedUser = this.client.state.getLatestValue().connected_user;
647
+ const result = handleFollowCreated(
648
+ follow,
649
+ this.currentState,
650
+ this.fid,
651
+ connectedUser?.id,
652
+ );
653
+ if (result.changed) {
654
+ this.state.next(result.data);
655
+ }
656
+ }
657
+
658
+ /**
659
+ * @internal
660
+ */
661
+ handleFollowDeleted(
662
+ follow:
663
+ | FollowResponse
664
+ // Backend doesn't return the follow in delete follow response https://getstream.slack.com/archives/C06RK9WCR09/p1753176937507209
665
+ | { source_feed: { fid: string }; target_feed: { fid: string } },
666
+ ) {
667
+ if (
668
+ !shouldUpdateState({
669
+ stateUpdateId: getStateUpdateQueueIdForUnfollow(follow),
670
+ stateUpdateQueue: this.stateUpdateQueue,
671
+ watch: this.currentState.watch,
672
+ })
673
+ ) {
674
+ return;
675
+ }
676
+
677
+ const connectedUser = this.client.state.getLatestValue().connected_user;
678
+ const result = handleFollowDeleted(
679
+ follow,
680
+ this.currentState,
681
+ this.fid,
682
+ connectedUser?.id,
683
+ );
684
+ if (result.changed) {
685
+ this.state.next(result.data);
686
+ }
687
+ }
688
+
689
+ /**
690
+ * @internal
691
+ */
692
+ handleWatchStopped() {
693
+ this.state.partialNext({
694
+ watch: false,
695
+ });
696
+ }
697
+
698
+ /**
699
+ * @internal
700
+ */
701
+ handleWatchStarted() {
702
+ this.state.partialNext({
703
+ watch: true,
704
+ });
705
+ }
706
+
697
707
  private handleBookmarkAdded(event: BookmarkAddedEvent) {
698
708
  const currentActivities = this.currentState.activities;
699
709
  const { connected_user: connectedUser } =
@@ -771,57 +781,93 @@ export class Feed extends FeedApi {
771
781
  return commentIndex;
772
782
  }
773
783
 
774
- private getActivityIndex(activity: ActivityResponse, state?: FeedState) {
775
- const { activities } = state ?? this.currentState;
776
-
777
- if (!activities) {
778
- return -1;
779
- }
784
+ /**
785
+ * Load child comments of entity (activity or comment) into the state, if the target entity is comment,
786
+ * `entityParentId` should be provided (`CommentResponse.parent_id ?? CommentResponse.object_id`).
787
+ */
788
+ private loadCommentsIntoState(data: {
789
+ entityParentId?: string;
790
+ entityId: string;
791
+ comments: Array<CommentResponse & ThreadedCommentResponse>;
792
+ next?: string;
793
+ sort: string;
794
+ }) {
795
+ // add initial (top level) object for processing
796
+ const traverseArray = [
797
+ {
798
+ entityId: data.entityId,
799
+ entityParentId: data.entityParentId,
800
+ comments: data.comments,
801
+ next: data.next,
802
+ },
803
+ ];
780
804
 
781
- let activityIndex = activities.indexOf(activity);
805
+ this.state.next((currentState) => {
806
+ const newCommentsByEntityId = {
807
+ ...currentState.comments_by_entity_id,
808
+ };
782
809
 
783
- // fast lookup failed, try slower approach
784
- if (activityIndex === -1) {
785
- activityIndex = activities.findIndex(
786
- (activity_) => activity_.id === activity.id,
787
- );
788
- }
810
+ while (traverseArray.length) {
811
+ const item = traverseArray.pop()!;
789
812
 
790
- return activityIndex;
791
- }
813
+ const entityId = item.entityId;
792
814
 
793
- private updateActivityInState(
794
- activity: ActivityResponse,
795
- patch: Patch<FromArray<FeedState['activities']>>,
796
- ) {
797
- this.state.next((currentState) => {
798
- const activityIndex = this.getActivityIndex(activity, currentState);
815
+ // go over entity comments and generate new objects
816
+ // for further processing if there are any replies
817
+ item.comments.forEach((comment) => {
818
+ if (!comment.replies?.length) return;
799
819
 
800
- if (activityIndex === -1) return currentState;
820
+ traverseArray.push({
821
+ entityId: comment.id,
822
+ entityParentId: entityId,
823
+ comments: comment.replies,
824
+ next: comment.meta?.next_cursor,
825
+ });
826
+ });
801
827
 
802
- const nextActivities = [...currentState.activities!];
828
+ // omit replies & meta from the comments (transform ThreadedCommentResponse to CommentResponse)
829
+ // this is somehow faster than copying the whole
830
+ // object and deleting the desired properties
831
+ const newComments: CommentResponse[] = item.comments.map(
832
+ ({ replies: _r, meta: _m, ...restOfTheCommentResponse }) =>
833
+ restOfTheCommentResponse,
834
+ );
803
835
 
804
- nextActivities[activityIndex] = patch(
805
- currentState.activities![activityIndex],
806
- );
836
+ newCommentsByEntityId[entityId] = {
837
+ ...newCommentsByEntityId[entityId],
838
+ entity_parent_id: item.entityParentId,
839
+ pagination: {
840
+ ...newCommentsByEntityId[entityId]?.pagination,
841
+ next: item.next,
842
+ sort: data.sort,
843
+ },
844
+ comments: newCommentsByEntityId[entityId]?.comments
845
+ ? newCommentsByEntityId[entityId].comments?.concat(newComments)
846
+ : newComments,
847
+ };
848
+ }
807
849
 
808
850
  return {
809
851
  ...currentState,
810
- activities: nextActivities,
852
+ comments_by_entity_id: newCommentsByEntityId,
811
853
  };
812
854
  });
813
855
  }
814
856
 
815
857
  private async loadNextPageComments({
816
- forId,
858
+ entityId,
817
859
  base,
818
860
  sort,
819
- parentId,
861
+ entityParentId,
820
862
  }: {
821
- parentId?: string;
822
- forId: string;
863
+ entityParentId?: string;
864
+ entityId: string;
823
865
  sort: string;
824
- base: () => Promise<PagerResponse & { comments: CommentResponse[] }>;
866
+ base: () => Promise<
867
+ PagerResponse & {
868
+ comments: Array<CommentResponse & ThreadedCommentResponse>;
869
+ }
870
+ >;
825
871
  }) {
826
872
  let error: unknown;
827
873
 
@@ -830,44 +876,24 @@ export class Feed extends FeedApi {
830
876
  ...currentState,
831
877
  comments_by_entity_id: {
832
878
  ...currentState.comments_by_entity_id,
833
- [forId]: {
834
- ...currentState.comments_by_entity_id[forId],
879
+ [entityId]: {
880
+ ...currentState.comments_by_entity_id[entityId],
835
881
  pagination: {
836
- ...currentState.comments_by_entity_id[forId]?.pagination,
882
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
837
883
  loading_next_page: true,
838
884
  },
839
885
  },
840
886
  },
841
887
  }));
842
888
 
843
- const { next: newNextCursor, comments } = await base();
889
+ const { next, comments } = await base();
844
890
 
845
- this.state.next((currentState) => {
846
- const newPagination = {
847
- ...currentState.comments_by_entity_id[forId]?.pagination,
848
- next: newNextCursor,
849
- };
850
-
851
- if (typeof newPagination.sort === 'undefined') {
852
- newPagination.sort = sort;
853
- }
854
-
855
- return {
856
- ...currentState,
857
- comments_by_entity_id: {
858
- ...currentState.comments_by_entity_id,
859
- [forId]: {
860
- ...currentState.comments_by_entity_id[forId],
861
- parent_id: parentId,
862
- pagination: newPagination,
863
- comments: currentState.comments_by_entity_id[forId]?.comments
864
- ? currentState.comments_by_entity_id[forId].comments?.concat(
865
- comments,
866
- )
867
- : comments,
868
- },
869
- },
870
- };
891
+ this.loadCommentsIntoState({
892
+ entityId,
893
+ comments,
894
+ entityParentId,
895
+ next,
896
+ sort,
871
897
  });
872
898
  } catch (e) {
873
899
  error = e;
@@ -876,10 +902,10 @@ export class Feed extends FeedApi {
876
902
  ...currentState,
877
903
  comments_by_entity_id: {
878
904
  ...currentState.comments_by_entity_id,
879
- [forId]: {
880
- ...currentState.comments_by_entity_id[forId],
905
+ [entityId]: {
906
+ ...currentState.comments_by_entity_id[entityId],
881
907
  pagination: {
882
- ...currentState.comments_by_entity_id[forId]?.pagination,
908
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
883
909
  loading_next_page: false,
884
910
  },
885
911
  },
@@ -916,7 +942,7 @@ export class Feed extends FeedApi {
916
942
  }
917
943
 
918
944
  await this.loadNextPageComments({
919
- forId: activity.id,
945
+ entityId: activity.id,
920
946
  base: () =>
921
947
  this.client.getComments({
922
948
  ...request,
@@ -951,7 +977,7 @@ export class Feed extends FeedApi {
951
977
  }
952
978
 
953
979
  await this.loadNextPageComments({
954
- forId: comment.id,
980
+ entityId: comment.id,
955
981
  base: () =>
956
982
  this.client.getCommentReplies({
957
983
  ...request,
@@ -963,7 +989,7 @@ export class Feed extends FeedApi {
963
989
  Constants.DEFAULT_COMMENT_PAGINATION,
964
990
  next: currentNextCursor,
965
991
  }),
966
- parentId: comment.parent_id ?? comment.object_id,
992
+ entityParentId: comment.parent_id ?? comment.object_id,
967
993
  sort,
968
994
  });
969
995
  }
@@ -1002,17 +1028,25 @@ export class Feed extends FeedApi {
1002
1028
  sort,
1003
1029
  });
1004
1030
 
1005
- this.state.next((currentState) => ({
1006
- ...currentState,
1007
- [type]: currentState[type]
1008
- ? currentState[type].concat(follows)
1009
- : follows,
1010
- [paginationKey]: {
1011
- ...currentState[paginationKey],
1012
- next: newNextCursor,
1013
- sort,
1014
- },
1015
- }));
1031
+ this.state.next((currentState) => {
1032
+ return {
1033
+ ...currentState,
1034
+ [type]:
1035
+ currentState[type] === undefined
1036
+ ? follows
1037
+ : uniqueArrayMerge(
1038
+ currentState[type],
1039
+ follows,
1040
+ (follow) =>
1041
+ `${follow.source_feed.fid}-${follow.target_feed.fid}`,
1042
+ ),
1043
+ [paginationKey]: {
1044
+ ...currentState[paginationKey],
1045
+ next: newNextCursor,
1046
+ sort,
1047
+ },
1048
+ };
1049
+ });
1016
1050
  } catch (e) {
1017
1051
  error = e;
1018
1052
  } finally {
@@ -1032,11 +1066,15 @@ export class Feed extends FeedApi {
1032
1066
  }
1033
1067
  }
1034
1068
 
1035
- async loadNextPageFollowers(request: Pick<QueryFollowsRequest, 'limit'>) {
1069
+ async loadNextPageFollowers(
1070
+ request: Pick<QueryFollowsRequest, 'limit' | 'sort'>,
1071
+ ) {
1036
1072
  await this.loadNextPageFollows('followers', request);
1037
1073
  }
1038
1074
 
1039
- async loadNextPageFollowing(request: Pick<QueryFollowsRequest, 'limit'>) {
1075
+ async loadNextPageFollowing(
1076
+ request: Pick<QueryFollowsRequest, 'limit' | 'sort'>,
1077
+ ) {
1040
1078
  await this.loadNextPageFollows('following', request);
1041
1079
  }
1042
1080
 
@@ -1074,7 +1112,7 @@ export class Feed extends FeedApi {
1074
1112
  this.state.next((currentState) => ({
1075
1113
  ...currentState,
1076
1114
  members: currentState.members
1077
- ? currentState.members.concat(members)
1115
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
1078
1116
  : members,
1079
1117
  member_pagination: {
1080
1118
  ...currentState.member_pagination,