@stream-io/feeds-client 0.1.8 → 0.1.10

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 (59) hide show
  1. package/@react-bindings/hooks/search-state-hooks/index.ts +3 -0
  2. package/@react-bindings/hooks/util/index.ts +1 -0
  3. package/@react-bindings/index.ts +5 -0
  4. package/CHANGELOG.md +22 -0
  5. package/dist/@react-bindings/contexts/StreamSearchContext.d.ts +12 -0
  6. package/dist/@react-bindings/contexts/StreamSearchResultsContext.d.ts +12 -0
  7. package/dist/@react-bindings/hooks/search-state-hooks/index.d.ts +3 -0
  8. package/dist/@react-bindings/hooks/search-state-hooks/useSearchQuery.d.ts +4 -0
  9. package/dist/@react-bindings/hooks/search-state-hooks/useSearchResult.d.ts +8 -0
  10. package/dist/@react-bindings/hooks/search-state-hooks/useSearchSources.d.ts +4 -0
  11. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  12. package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
  13. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
  14. package/dist/@react-bindings/index.d.ts +5 -0
  15. package/dist/@react-bindings/wrappers/StreamSearch.d.ts +12 -0
  16. package/dist/@react-bindings/wrappers/StreamSearchResults.d.ts +12 -0
  17. package/dist/index-react-bindings.browser.cjs +431 -156
  18. package/dist/index-react-bindings.browser.cjs.map +1 -1
  19. package/dist/index-react-bindings.browser.js +422 -157
  20. package/dist/index-react-bindings.browser.js.map +1 -1
  21. package/dist/index-react-bindings.node.cjs +431 -156
  22. package/dist/index-react-bindings.node.cjs.map +1 -1
  23. package/dist/index-react-bindings.node.js +422 -157
  24. package/dist/index-react-bindings.node.js.map +1 -1
  25. package/dist/index.browser.cjs +346 -264
  26. package/dist/index.browser.cjs.map +1 -1
  27. package/dist/index.browser.js +346 -265
  28. package/dist/index.browser.js.map +1 -1
  29. package/dist/index.node.cjs +346 -264
  30. package/dist/index.node.cjs.map +1 -1
  31. package/dist/index.node.js +346 -265
  32. package/dist/index.node.js.map +1 -1
  33. package/dist/src/Feed.d.ts +40 -9
  34. package/dist/src/FeedsClient.d.ts +8 -1
  35. package/dist/src/common/BaseSearchSource.d.ts +3 -1
  36. package/dist/src/common/FeedSearchSource.d.ts +5 -1
  37. package/dist/src/common/SearchController.d.ts +2 -0
  38. package/dist/src/gen-imports.d.ts +1 -1
  39. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  40. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  41. package/dist/src/utils.d.ts +1 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/Feed.ts +226 -192
  45. package/src/FeedsClient.ts +75 -3
  46. package/src/common/ActivitySearchSource.ts +5 -15
  47. package/src/common/BaseSearchSource.ts +9 -9
  48. package/src/common/FeedSearchSource.ts +20 -65
  49. package/src/common/SearchController.ts +2 -0
  50. package/src/common/UserSearchSource.ts +9 -61
  51. package/src/gen-imports.ts +1 -1
  52. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  53. package/src/state-updates/activity-utils.test.ts +1 -0
  54. package/src/state-updates/follow-utils.test.ts +552 -0
  55. package/src/state-updates/follow-utils.ts +126 -0
  56. package/src/state-updates/state-update-queue.test.ts +53 -0
  57. package/src/state-updates/state-update-queue.ts +35 -0
  58. package/src/utils.test.ts +175 -0
  59. 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.8",
3
+ "version": "0.1.10",
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) {
@@ -535,9 +460,9 @@ export class Feed extends FeedApi {
535
460
  groupId: 'user' | 'timeline' | (string & {}),
536
461
  id: string,
537
462
  data?: FeedResponse,
463
+ watch = false,
538
464
  ) {
539
- // Need this ugly cast because fileUpload endpoints :(
540
- super(client as unknown as FeedsApi, groupId, id);
465
+ super(client, groupId, id);
541
466
  this.state = new StateStore<FeedState>({
542
467
  fid: `${groupId}:${id}`,
543
468
  group_id: groupId,
@@ -546,6 +471,7 @@ export class Feed extends FeedApi {
546
471
  is_loading: false,
547
472
  is_loading_activities: false,
548
473
  comments_by_entity_id: {},
474
+ watch,
549
475
  });
550
476
  this.client = client;
551
477
  }
@@ -656,6 +582,8 @@ export class Feed extends FeedApi {
656
582
  });
657
583
  }
658
584
  } else {
585
+ // Empty queue when reinitializing the state
586
+ this.stateUpdateQueue.clear();
659
587
  const responseCopy: Partial<
660
588
  StreamResponse<GetOrCreateFeedResponse>['feed'] &
661
589
  StreamResponse<GetOrCreateFeedResponse>
@@ -680,8 +608,12 @@ export class Feed extends FeedApi {
680
608
  if (!request?.following_pagination?.limit) {
681
609
  delete nextState.following;
682
610
  }
611
+ if (response.members.length === 0 && response.feed.member_count > 0) {
612
+ delete nextState.members;
613
+ }
683
614
 
684
615
  nextState.last_get_or_create_request_config = request;
616
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
685
617
 
686
618
  return nextState;
687
619
  });
@@ -698,6 +630,80 @@ export class Feed extends FeedApi {
698
630
  }
699
631
  }
700
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
+
701
707
  private handleBookmarkAdded(event: BookmarkAddedEvent) {
702
708
  const currentActivities = this.currentState.activities;
703
709
  const { connected_user: connectedUser } =
@@ -775,57 +781,93 @@ export class Feed extends FeedApi {
775
781
  return commentIndex;
776
782
  }
777
783
 
778
- private getActivityIndex(activity: ActivityResponse, state?: FeedState) {
779
- const { activities } = state ?? this.currentState;
780
-
781
- if (!activities) {
782
- return -1;
783
- }
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
+ ];
784
804
 
785
- let activityIndex = activities.indexOf(activity);
805
+ this.state.next((currentState) => {
806
+ const newCommentsByEntityId = {
807
+ ...currentState.comments_by_entity_id,
808
+ };
786
809
 
787
- // fast lookup failed, try slower approach
788
- if (activityIndex === -1) {
789
- activityIndex = activities.findIndex(
790
- (activity_) => activity_.id === activity.id,
791
- );
792
- }
810
+ while (traverseArray.length) {
811
+ const item = traverseArray.pop()!;
793
812
 
794
- return activityIndex;
795
- }
813
+ const entityId = item.entityId;
796
814
 
797
- private updateActivityInState(
798
- activity: ActivityResponse,
799
- patch: Patch<FromArray<FeedState['activities']>>,
800
- ) {
801
- this.state.next((currentState) => {
802
- 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;
803
819
 
804
- 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
+ });
805
827
 
806
- 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
+ );
807
835
 
808
- nextActivities[activityIndex] = patch(
809
- currentState.activities![activityIndex],
810
- );
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
+ }
811
849
 
812
850
  return {
813
851
  ...currentState,
814
- activities: nextActivities,
852
+ comments_by_entity_id: newCommentsByEntityId,
815
853
  };
816
854
  });
817
855
  }
818
856
 
819
857
  private async loadNextPageComments({
820
- forId,
858
+ entityId,
821
859
  base,
822
860
  sort,
823
- parentId,
861
+ entityParentId,
824
862
  }: {
825
- parentId?: string;
826
- forId: string;
863
+ entityParentId?: string;
864
+ entityId: string;
827
865
  sort: string;
828
- base: () => Promise<PagerResponse & { comments: CommentResponse[] }>;
866
+ base: () => Promise<
867
+ PagerResponse & {
868
+ comments: Array<CommentResponse & ThreadedCommentResponse>;
869
+ }
870
+ >;
829
871
  }) {
830
872
  let error: unknown;
831
873
 
@@ -834,44 +876,24 @@ export class Feed extends FeedApi {
834
876
  ...currentState,
835
877
  comments_by_entity_id: {
836
878
  ...currentState.comments_by_entity_id,
837
- [forId]: {
838
- ...currentState.comments_by_entity_id[forId],
879
+ [entityId]: {
880
+ ...currentState.comments_by_entity_id[entityId],
839
881
  pagination: {
840
- ...currentState.comments_by_entity_id[forId]?.pagination,
882
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
841
883
  loading_next_page: true,
842
884
  },
843
885
  },
844
886
  },
845
887
  }));
846
888
 
847
- const { next: newNextCursor, comments } = await base();
889
+ const { next, comments } = await base();
848
890
 
849
- this.state.next((currentState) => {
850
- const newPagination = {
851
- ...currentState.comments_by_entity_id[forId]?.pagination,
852
- next: newNextCursor,
853
- };
854
-
855
- if (typeof newPagination.sort === 'undefined') {
856
- newPagination.sort = sort;
857
- }
858
-
859
- return {
860
- ...currentState,
861
- comments_by_entity_id: {
862
- ...currentState.comments_by_entity_id,
863
- [forId]: {
864
- ...currentState.comments_by_entity_id[forId],
865
- parent_id: parentId,
866
- pagination: newPagination,
867
- comments: currentState.comments_by_entity_id[forId]?.comments
868
- ? currentState.comments_by_entity_id[forId].comments?.concat(
869
- comments,
870
- )
871
- : comments,
872
- },
873
- },
874
- };
891
+ this.loadCommentsIntoState({
892
+ entityId,
893
+ comments,
894
+ entityParentId,
895
+ next,
896
+ sort,
875
897
  });
876
898
  } catch (e) {
877
899
  error = e;
@@ -880,10 +902,10 @@ export class Feed extends FeedApi {
880
902
  ...currentState,
881
903
  comments_by_entity_id: {
882
904
  ...currentState.comments_by_entity_id,
883
- [forId]: {
884
- ...currentState.comments_by_entity_id[forId],
905
+ [entityId]: {
906
+ ...currentState.comments_by_entity_id[entityId],
885
907
  pagination: {
886
- ...currentState.comments_by_entity_id[forId]?.pagination,
908
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
887
909
  loading_next_page: false,
888
910
  },
889
911
  },
@@ -920,7 +942,7 @@ export class Feed extends FeedApi {
920
942
  }
921
943
 
922
944
  await this.loadNextPageComments({
923
- forId: activity.id,
945
+ entityId: activity.id,
924
946
  base: () =>
925
947
  this.client.getComments({
926
948
  ...request,
@@ -955,7 +977,7 @@ export class Feed extends FeedApi {
955
977
  }
956
978
 
957
979
  await this.loadNextPageComments({
958
- forId: comment.id,
980
+ entityId: comment.id,
959
981
  base: () =>
960
982
  this.client.getCommentReplies({
961
983
  ...request,
@@ -967,7 +989,7 @@ export class Feed extends FeedApi {
967
989
  Constants.DEFAULT_COMMENT_PAGINATION,
968
990
  next: currentNextCursor,
969
991
  }),
970
- parentId: comment.parent_id ?? comment.object_id,
992
+ entityParentId: comment.parent_id ?? comment.object_id,
971
993
  sort,
972
994
  });
973
995
  }
@@ -1006,17 +1028,25 @@ export class Feed extends FeedApi {
1006
1028
  sort,
1007
1029
  });
1008
1030
 
1009
- this.state.next((currentState) => ({
1010
- ...currentState,
1011
- [type]: currentState[type]
1012
- ? currentState[type].concat(follows)
1013
- : follows,
1014
- [paginationKey]: {
1015
- ...currentState[paginationKey],
1016
- next: newNextCursor,
1017
- sort,
1018
- },
1019
- }));
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
+ });
1020
1050
  } catch (e) {
1021
1051
  error = e;
1022
1052
  } finally {
@@ -1036,11 +1066,15 @@ export class Feed extends FeedApi {
1036
1066
  }
1037
1067
  }
1038
1068
 
1039
- async loadNextPageFollowers(request: Pick<QueryFollowsRequest, 'limit'>) {
1069
+ async loadNextPageFollowers(
1070
+ request: Pick<QueryFollowsRequest, 'limit' | 'sort'>,
1071
+ ) {
1040
1072
  await this.loadNextPageFollows('followers', request);
1041
1073
  }
1042
1074
 
1043
- async loadNextPageFollowing(request: Pick<QueryFollowsRequest, 'limit'>) {
1075
+ async loadNextPageFollowing(
1076
+ request: Pick<QueryFollowsRequest, 'limit' | 'sort'>,
1077
+ ) {
1044
1078
  await this.loadNextPageFollows('following', request);
1045
1079
  }
1046
1080
 
@@ -1078,7 +1112,7 @@ export class Feed extends FeedApi {
1078
1112
  this.state.next((currentState) => ({
1079
1113
  ...currentState,
1080
1114
  members: currentState.members
1081
- ? currentState.members.concat(members)
1115
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
1082
1116
  : members,
1083
1117
  member_pagination: {
1084
1118
  ...currentState.member_pagination,