@stream-io/feeds-client 0.1.10 → 0.1.11

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 (152) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/@react-bindings/contexts/StreamFeedContext.d.ts +1 -1
  3. package/dist/@react-bindings/contexts/StreamFeedsContext.d.ts +1 -1
  4. package/dist/@react-bindings/hooks/feed-state-hooks/useComments.d.ts +1 -1
  5. package/dist/@react-bindings/hooks/feed-state-hooks/useFeedActivities.d.ts +1 -1
  6. package/dist/@react-bindings/hooks/feed-state-hooks/useFeedMetadata.d.ts +1 -1
  7. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowers.d.ts +1 -1
  8. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowing.d.ts +1 -1
  9. package/dist/@react-bindings/hooks/feed-state-hooks/useOwnCapabilities.d.ts +1 -1
  10. package/dist/@react-bindings/hooks/feed-state-hooks/useOwnFollows.d.ts +1 -1
  11. package/dist/@react-bindings/hooks/useCreateFeedsClient.d.ts +1 -1
  12. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +1 -1
  13. package/dist/index-react-bindings.browser.cjs +1589 -1518
  14. package/dist/index-react-bindings.browser.cjs.map +1 -1
  15. package/dist/index-react-bindings.browser.js +1589 -1518
  16. package/dist/index-react-bindings.browser.js.map +1 -1
  17. package/dist/index-react-bindings.node.cjs +1589 -1518
  18. package/dist/index-react-bindings.node.cjs.map +1 -1
  19. package/dist/index-react-bindings.node.js +1589 -1518
  20. package/dist/index-react-bindings.node.js.map +1 -1
  21. package/dist/index.browser.cjs +1607 -1533
  22. package/dist/index.browser.cjs.map +1 -1
  23. package/dist/index.browser.js +1605 -1534
  24. package/dist/index.browser.js.map +1 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.node.cjs +1607 -1533
  27. package/dist/index.node.cjs.map +1 -1
  28. package/dist/index.node.js +1605 -1534
  29. package/dist/index.node.js.map +1 -1
  30. package/dist/src/common/ActivitySearchSource.d.ts +1 -1
  31. package/dist/src/common/FeedSearchSource.d.ts +2 -2
  32. package/dist/src/common/Poll.d.ts +1 -1
  33. package/dist/src/common/UserSearchSource.d.ts +1 -1
  34. package/dist/src/common/real-time/StableWSConnection.d.ts +3 -3
  35. package/dist/src/feed/event-handlers/activity/handle-activity-added.d.ts +7 -0
  36. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +8 -0
  37. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +8 -0
  38. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +8 -0
  39. package/dist/src/feed/event-handlers/activity/handle-activity-removed-from-feed.d.ts +3 -0
  40. package/dist/src/feed/event-handlers/activity/handle-activity-updated.d.ts +8 -0
  41. package/dist/src/feed/event-handlers/activity/index.d.ts +6 -0
  42. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +8 -0
  43. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +9 -0
  44. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +8 -0
  45. package/dist/src/feed/event-handlers/bookmark/index.d.ts +3 -0
  46. package/dist/src/feed/event-handlers/comment/handle-comment-added.d.ts +3 -0
  47. package/dist/src/feed/event-handlers/comment/handle-comment-deleted.d.ts +3 -0
  48. package/dist/src/feed/event-handlers/comment/handle-comment-reaction.d.ts +3 -0
  49. package/dist/src/feed/event-handlers/comment/handle-comment-updated.d.ts +3 -0
  50. package/dist/src/feed/event-handlers/comment/index.d.ts +4 -0
  51. package/dist/src/feed/event-handlers/feed/handle-feed-updated.d.ts +3 -0
  52. package/dist/src/feed/event-handlers/feed/index.d.ts +1 -0
  53. package/dist/src/feed/event-handlers/feed-member/handle-feed-member-added.d.ts +3 -0
  54. package/dist/src/feed/event-handlers/feed-member/handle-feed-member-removed.d.ts +3 -0
  55. package/dist/src/feed/event-handlers/feed-member/handle-feed-member-updated.d.ts +3 -0
  56. package/dist/src/feed/event-handlers/feed-member/index.d.ts +3 -0
  57. package/dist/src/feed/event-handlers/follow/handle-follow-created.d.ts +7 -0
  58. package/dist/src/feed/event-handlers/follow/handle-follow-deleted.d.ts +7 -0
  59. package/dist/src/feed/event-handlers/follow/handle-follow-updated.d.ts +3 -0
  60. package/dist/src/feed/event-handlers/follow/index.d.ts +3 -0
  61. package/dist/src/feed/event-handlers/index.d.ts +7 -0
  62. package/dist/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +3 -0
  63. package/dist/src/feed/event-handlers/notification-feed/index.d.ts +1 -0
  64. package/dist/src/{Feed.d.ts → feed/feed.d.ts} +15 -34
  65. package/dist/src/feed/index.d.ts +2 -0
  66. package/dist/src/{FeedsClient.d.ts → feeds-client.d.ts} +6 -5
  67. package/dist/src/gen/models/index.d.ts +5 -0
  68. package/dist/src/gen-imports.d.ts +1 -1
  69. package/dist/src/test-utils/index.d.ts +1 -0
  70. package/dist/src/test-utils/response-generators.d.ts +9 -0
  71. package/dist/src/types-internal.d.ts +7 -0
  72. package/dist/src/types.d.ts +1 -1
  73. package/dist/src/utils/check-has-another-page.d.ts +1 -0
  74. package/dist/src/utils/constants.d.ts +3 -0
  75. package/dist/src/utils/index.d.ts +5 -0
  76. package/dist/src/utils/state-update-queue.d.ts +6 -0
  77. package/dist/src/utils/type-assertions.d.ts +7 -0
  78. package/dist/src/utils/unique-array-merge.d.ts +1 -0
  79. package/dist/tsconfig.tsbuildinfo +1 -1
  80. package/index.ts +2 -2
  81. package/package.json +2 -1
  82. package/src/common/ActivitySearchSource.ts +1 -1
  83. package/src/common/FeedSearchSource.ts +2 -2
  84. package/src/common/Poll.ts +1 -1
  85. package/src/common/UserSearchSource.ts +1 -1
  86. package/src/{state-updates → feed/event-handlers/activity}/activity-reaction-utils.test.ts +12 -2
  87. package/src/{state-updates → feed/event-handlers/activity}/activity-utils.test.ts +3 -2
  88. package/src/{state-updates/activity-utils.ts → feed/event-handlers/activity/handle-activity-added.ts} +16 -36
  89. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +30 -0
  90. package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +67 -0
  91. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +75 -0
  92. package/src/feed/event-handlers/activity/handle-activity-removed-from-feed.ts +16 -0
  93. package/src/feed/event-handlers/activity/handle-activity-updated.ts +47 -0
  94. package/src/feed/event-handlers/activity/index.ts +6 -0
  95. package/src/{state-updates → feed/event-handlers/bookmark}/bookmark-utils.test.ts +2 -2
  96. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +63 -0
  97. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +84 -0
  98. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +76 -0
  99. package/src/feed/event-handlers/bookmark/index.ts +3 -0
  100. package/src/feed/event-handlers/comment/handle-comment-added.ts +38 -0
  101. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +35 -0
  102. package/src/feed/event-handlers/comment/handle-comment-reaction.ts +61 -0
  103. package/src/feed/event-handlers/comment/handle-comment-updated.ts +35 -0
  104. package/src/feed/event-handlers/comment/index.ts +4 -0
  105. package/src/feed/event-handlers/feed/handle-feed-updated.ts +9 -0
  106. package/src/feed/event-handlers/feed/index.ts +1 -0
  107. package/src/feed/event-handlers/feed-member/handle-feed-member-added.ts +31 -0
  108. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +24 -0
  109. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.ts +40 -0
  110. package/src/feed/event-handlers/feed-member/index.ts +3 -0
  111. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +246 -0
  112. package/src/feed/event-handlers/follow/handle-follow-created.ts +93 -0
  113. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +264 -0
  114. package/src/feed/event-handlers/follow/handle-follow-deleted.ts +95 -0
  115. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +174 -0
  116. package/src/feed/event-handlers/follow/handle-follow-updated.ts +88 -0
  117. package/src/feed/event-handlers/follow/index.ts +3 -0
  118. package/src/feed/event-handlers/index.ts +7 -0
  119. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +10 -0
  120. package/src/feed/event-handlers/notification-feed/index.ts +1 -0
  121. package/src/{Feed.ts → feed/feed.ts} +72 -483
  122. package/src/feed/index.ts +2 -0
  123. package/src/{FeedsClient.ts → feeds-client.ts} +26 -8
  124. package/src/gen/model-decoders/decoders.ts +7 -0
  125. package/src/gen/models/index.ts +10 -0
  126. package/src/gen-imports.ts +1 -1
  127. package/src/test-utils/index.ts +1 -0
  128. package/src/test-utils/response-generators.ts +102 -0
  129. package/src/types-internal.ts +11 -0
  130. package/src/types.ts +1 -1
  131. package/src/utils/check-has-another-page.ts +6 -0
  132. package/src/utils/constants.ts +3 -0
  133. package/src/utils/index.ts +5 -0
  134. package/src/{state-updates → utils}/state-update-queue.test.ts +6 -6
  135. package/src/utils/state-update-queue.ts +42 -0
  136. package/src/utils/type-assertions.ts +22 -0
  137. package/src/{utils.test.ts → utils/unique-array-merge.test.ts} +7 -3
  138. package/src/utils/unique-array-merge.ts +19 -0
  139. package/dist/src/state-updates/activity-reaction-utils.d.ts +0 -10
  140. package/dist/src/state-updates/activity-utils.d.ts +0 -13
  141. package/dist/src/state-updates/bookmark-utils.d.ts +0 -14
  142. package/dist/src/state-updates/follow-utils.d.ts +0 -19
  143. package/dist/src/state-updates/state-update-queue.d.ts +0 -15
  144. package/dist/src/utils.d.ts +0 -10
  145. package/src/state-updates/activity-reaction-utils.ts +0 -107
  146. package/src/state-updates/bookmark-utils.ts +0 -167
  147. package/src/state-updates/follow-utils.test.ts +0 -552
  148. package/src/state-updates/follow-utils.ts +0 -126
  149. package/src/state-updates/state-update-queue.ts +0 -35
  150. package/src/utils.ts +0 -48
  151. /package/dist/src/{ModerationClient.d.ts → moderation-client.d.ts} +0 -0
  152. /package/src/{ModerationClient.ts → moderation-client.ts} +0 -0
@@ -1502,6 +1502,12 @@ decoders.ThreadedCommentResponse = (input) => {
1502
1502
  };
1503
1503
  return decode(typeMappings, input);
1504
1504
  };
1505
+ decoders.UnfollowResponse = (input) => {
1506
+ const typeMappings = {
1507
+ follow: { type: 'FollowResponse', isSingle: true },
1508
+ };
1509
+ return decode(typeMappings, input);
1510
+ };
1505
1511
  decoders.UnpinActivityResponse = (input) => {
1506
1512
  const typeMappings = {
1507
1513
  activity: { type: 'ActivityResponse', isSingle: true },
@@ -3741,1625 +3747,1683 @@ const decodeWSEvent = (data) => {
3741
3747
  }
3742
3748
  };
3743
3749
 
3744
- class FeedApi {
3745
- constructor(feedsApi, group, id) {
3746
- this.feedsApi = feedsApi;
3747
- this.group = group;
3748
- this.id = id;
3749
- }
3750
- delete(request) {
3751
- return this.feedsApi.deleteFeed({
3752
- feed_id: this.id,
3753
- feed_group_id: this.group,
3754
- ...request,
3755
- });
3756
- }
3757
- getOrCreate(request) {
3758
- return this.feedsApi.getOrCreateFeed({
3759
- feed_id: this.id,
3760
- feed_group_id: this.group,
3761
- ...request,
3762
- });
3763
- }
3764
- update(request) {
3765
- return this.feedsApi.updateFeed({
3766
- feed_id: this.id,
3767
- feed_group_id: this.group,
3768
- ...request,
3769
- });
3770
- }
3771
- markActivity(request) {
3772
- return this.feedsApi.markActivity({
3773
- feed_id: this.id,
3774
- feed_group_id: this.group,
3775
- ...request,
3776
- });
3777
- }
3778
- unpinActivity(request) {
3779
- return this.feedsApi.unpinActivity({
3780
- feed_id: this.id,
3781
- feed_group_id: this.group,
3782
- ...request,
3783
- });
3784
- }
3785
- pinActivity(request) {
3786
- return this.feedsApi.pinActivity({
3787
- feed_id: this.id,
3788
- feed_group_id: this.group,
3789
- ...request,
3790
- });
3791
- }
3792
- updateFeedMembers(request) {
3793
- return this.feedsApi.updateFeedMembers({
3794
- feed_id: this.id,
3795
- feed_group_id: this.group,
3796
- ...request,
3797
- });
3798
- }
3799
- acceptFeedMemberInvite(request) {
3800
- return this.feedsApi.acceptFeedMemberInvite({
3801
- feed_id: this.id,
3802
- feed_group_id: this.group,
3803
- ...request,
3804
- });
3805
- }
3806
- queryFeedMembers(request) {
3807
- return this.feedsApi.queryFeedMembers({
3808
- feed_id: this.id,
3809
- feed_group_id: this.group,
3810
- ...request,
3811
- });
3750
+ class ModerationApi {
3751
+ constructor(apiClient) {
3752
+ this.apiClient = apiClient;
3812
3753
  }
3813
- rejectFeedMemberInvite(request) {
3814
- return this.feedsApi.rejectFeedMemberInvite({
3815
- feed_id: this.id,
3816
- feed_group_id: this.group,
3817
- ...request,
3818
- });
3754
+ async ban(request) {
3755
+ const body = {
3756
+ target_user_id: request?.target_user_id,
3757
+ banned_by_id: request?.banned_by_id,
3758
+ channel_cid: request?.channel_cid,
3759
+ delete_messages: request?.delete_messages,
3760
+ ip_ban: request?.ip_ban,
3761
+ reason: request?.reason,
3762
+ shadow: request?.shadow,
3763
+ timeout: request?.timeout,
3764
+ banned_by: request?.banned_by,
3765
+ };
3766
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/ban', undefined, undefined, body, 'application/json');
3767
+ decoders.BanResponse?.(response.body);
3768
+ return { ...response.body, metadata: response.metadata };
3819
3769
  }
3820
- stopWatching(request) {
3821
- return this.feedsApi.stopWatchingFeed({
3822
- feed_id: this.id,
3823
- feed_group_id: this.group,
3824
- ...request,
3825
- });
3770
+ async upsertConfig(request) {
3771
+ const body = {
3772
+ key: request?.key,
3773
+ async: request?.async,
3774
+ team: request?.team,
3775
+ ai_image_config: request?.ai_image_config,
3776
+ ai_text_config: request?.ai_text_config,
3777
+ ai_video_config: request?.ai_video_config,
3778
+ automod_platform_circumvention_config: request?.automod_platform_circumvention_config,
3779
+ automod_semantic_filters_config: request?.automod_semantic_filters_config,
3780
+ automod_toxicity_config: request?.automod_toxicity_config,
3781
+ aws_rekognition_config: request?.aws_rekognition_config,
3782
+ block_list_config: request?.block_list_config,
3783
+ bodyguard_config: request?.bodyguard_config,
3784
+ google_vision_config: request?.google_vision_config,
3785
+ rule_builder_config: request?.rule_builder_config,
3786
+ velocity_filter_config: request?.velocity_filter_config,
3787
+ video_call_rule_config: request?.video_call_rule_config,
3788
+ };
3789
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/config', undefined, undefined, body, 'application/json');
3790
+ decoders.UpsertConfigResponse?.(response.body);
3791
+ return { ...response.body, metadata: response.metadata };
3826
3792
  }
3827
- }
3828
-
3829
- const addActivitiesToState = (newActivities, activities, position) => {
3830
- let result;
3831
- if (activities === undefined) {
3832
- activities = [];
3833
- result = {
3834
- changed: true,
3835
- activities,
3793
+ async deleteConfig(request) {
3794
+ const queryParams = {
3795
+ team: request?.team,
3796
+ };
3797
+ const pathParams = {
3798
+ key: request?.key,
3836
3799
  };
3800
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/moderation/config/{key}', pathParams, queryParams);
3801
+ decoders.DeleteModerationConfigResponse?.(response.body);
3802
+ return { ...response.body, metadata: response.metadata };
3837
3803
  }
3838
- else {
3839
- result = {
3840
- changed: false,
3841
- activities,
3804
+ async getConfig(request) {
3805
+ const queryParams = {
3806
+ team: request?.team,
3807
+ };
3808
+ const pathParams = {
3809
+ key: request?.key,
3842
3810
  };
3811
+ const response = await this.apiClient.sendRequest('GET', '/api/v2/moderation/config/{key}', pathParams, queryParams);
3812
+ decoders.GetConfigResponse?.(response.body);
3813
+ return { ...response.body, metadata: response.metadata };
3843
3814
  }
3844
- const newActivitiesDeduplicated = [];
3845
- newActivities.forEach((newActivityResponse) => {
3846
- const index = activities.findIndex((a) => a.id === newActivityResponse.id);
3847
- if (index === -1) {
3848
- newActivitiesDeduplicated.push(newActivityResponse);
3849
- }
3850
- });
3851
- if (newActivitiesDeduplicated.length > 0) {
3852
- // TODO: since feed activities are not necessarily ordered by created_at (personalization) we don't order by created_at
3853
- // Maybe we can add a flag to the JS client to support order by created_at
3854
- const updatedActivities = [
3855
- ...(position === 'start' ? newActivitiesDeduplicated : []),
3856
- ...activities,
3857
- ...(position === 'end' ? newActivitiesDeduplicated : []),
3858
- ];
3859
- result = { changed: true, activities: updatedActivities };
3815
+ async queryModerationConfigs(request) {
3816
+ const body = {
3817
+ limit: request?.limit,
3818
+ next: request?.next,
3819
+ prev: request?.prev,
3820
+ sort: request?.sort,
3821
+ filter: request?.filter,
3822
+ };
3823
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/configs', undefined, undefined, body, 'application/json');
3824
+ decoders.QueryModerationConfigsResponse?.(response.body);
3825
+ return { ...response.body, metadata: response.metadata };
3860
3826
  }
3861
- return result;
3862
- };
3863
- const updateActivityInState = (updatedActivityResponse, activities) => {
3864
- const index = activities.findIndex((a) => a.id === updatedActivityResponse.id);
3865
- if (index !== -1) {
3866
- const newActivities = [...activities];
3867
- const activity = activities[index];
3868
- newActivities[index] = {
3869
- ...updatedActivityResponse,
3870
- own_reactions: activity.own_reactions,
3871
- own_bookmarks: activity.own_bookmarks,
3872
- latest_reactions: activity.latest_reactions,
3873
- reaction_groups: activity.reaction_groups,
3827
+ async flag(request) {
3828
+ const body = {
3829
+ entity_id: request?.entity_id,
3830
+ entity_type: request?.entity_type,
3831
+ entity_creator_id: request?.entity_creator_id,
3832
+ reason: request?.reason,
3833
+ custom: request?.custom,
3834
+ moderation_payload: request?.moderation_payload,
3874
3835
  };
3875
- return { changed: true, activities: newActivities };
3836
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/flag', undefined, undefined, body, 'application/json');
3837
+ decoders.FlagResponse?.(response.body);
3838
+ return { ...response.body, metadata: response.metadata };
3876
3839
  }
3877
- else {
3878
- return { changed: false, activities };
3840
+ async mute(request) {
3841
+ const body = {
3842
+ target_ids: request?.target_ids,
3843
+ timeout: request?.timeout,
3844
+ };
3845
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/mute', undefined, undefined, body, 'application/json');
3846
+ decoders.MuteResponse?.(response.body);
3847
+ return { ...response.body, metadata: response.metadata };
3879
3848
  }
3880
- };
3881
- const removeActivityFromState = (activityResponse, activities) => {
3882
- const index = activities.findIndex((a) => a.id === activityResponse.id);
3883
- if (index !== -1) {
3884
- const newActivities = [...activities];
3885
- newActivities.splice(index, 1);
3886
- return { changed: true, activities: newActivities };
3849
+ async queryReviewQueue(request) {
3850
+ const body = {
3851
+ limit: request?.limit,
3852
+ lock_count: request?.lock_count,
3853
+ lock_duration: request?.lock_duration,
3854
+ lock_items: request?.lock_items,
3855
+ next: request?.next,
3856
+ prev: request?.prev,
3857
+ stats_only: request?.stats_only,
3858
+ sort: request?.sort,
3859
+ filter: request?.filter,
3860
+ };
3861
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/review_queue', undefined, undefined, body, 'application/json');
3862
+ decoders.QueryReviewQueueResponse?.(response.body);
3863
+ return { ...response.body, metadata: response.metadata };
3887
3864
  }
3888
- else {
3889
- return { changed: false, activities };
3865
+ async submitAction(request) {
3866
+ const body = {
3867
+ action_type: request?.action_type,
3868
+ item_id: request?.item_id,
3869
+ ban: request?.ban,
3870
+ custom: request?.custom,
3871
+ delete_activity: request?.delete_activity,
3872
+ delete_message: request?.delete_message,
3873
+ delete_reaction: request?.delete_reaction,
3874
+ delete_user: request?.delete_user,
3875
+ mark_reviewed: request?.mark_reviewed,
3876
+ unban: request?.unban,
3877
+ };
3878
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/submit_action', undefined, undefined, body, 'application/json');
3879
+ decoders.SubmitActionResponse?.(response.body);
3880
+ return { ...response.body, metadata: response.metadata };
3890
3881
  }
3891
- };
3882
+ }
3892
3883
 
3893
- const updateActivityInActivities$1 = (updatedActivity, activities) => {
3894
- const index = activities.findIndex((a) => a.id === updatedActivity.id);
3895
- if (index !== -1) {
3896
- const newActivities = [...activities];
3897
- newActivities[index] = updatedActivity;
3898
- return { changed: true, activities: newActivities };
3899
- }
3900
- else {
3901
- return { changed: false, activities };
3902
- }
3903
- };
3904
- const addReactionToActivity = (event, activity, isCurrentUser) => {
3905
- // Update own_reactions if the reaction is from the current user
3906
- const ownReactions = [...(activity.own_reactions || [])];
3907
- if (isCurrentUser) {
3908
- ownReactions.push(event.reaction);
3909
- }
3910
- return {
3911
- ...activity,
3912
- own_reactions: ownReactions,
3913
- latest_reactions: event.activity.latest_reactions,
3914
- reaction_groups: event.activity.reaction_groups,
3915
- changed: true,
3916
- };
3917
- };
3918
- const removeReactionFromActivity = (event, activity, isCurrentUser) => {
3919
- // Update own_reactions if the reaction is from the current user
3920
- const ownReactions = isCurrentUser
3921
- ? (activity.own_reactions || []).filter((r) => !(r.type === event.reaction.type &&
3922
- r.user.id === event.reaction.user.id))
3923
- : activity.own_reactions;
3924
- return {
3925
- ...activity,
3926
- own_reactions: ownReactions,
3927
- latest_reactions: event.activity.latest_reactions,
3928
- reaction_groups: event.activity.reaction_groups,
3929
- changed: true,
3930
- };
3931
- };
3932
- const addReactionToActivities = (event, activities, isCurrentUser) => {
3933
- if (!activities) {
3934
- return { changed: false, activities: [] };
3935
- }
3936
- const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
3937
- if (activityIndex === -1) {
3938
- return { changed: false, activities };
3939
- }
3940
- const activity = activities[activityIndex];
3941
- const updatedActivity = addReactionToActivity(event, activity, isCurrentUser);
3942
- return updateActivityInActivities$1(updatedActivity, activities);
3943
- };
3944
- const removeReactionFromActivities = (event, activities, isCurrentUser) => {
3945
- if (!activities) {
3946
- return { changed: false, activities: [] };
3947
- }
3948
- const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
3949
- if (activityIndex === -1) {
3950
- return { changed: false, activities };
3951
- }
3952
- const activity = activities[activityIndex];
3953
- const updatedActivity = removeReactionFromActivity(event, activity, isCurrentUser);
3954
- return updateActivityInActivities$1(updatedActivity, activities);
3955
- };
3956
-
3957
- // Helper function to check if two bookmarks are the same
3958
- // A bookmark is identified by activity_id + folder_id + user_id
3959
- const isSameBookmark = (bookmark1, bookmark2) => {
3960
- return (bookmark1.user.id === bookmark2.user.id &&
3961
- bookmark1.activity.id === bookmark2.activity.id &&
3962
- bookmark1.folder?.id === bookmark2.folder?.id);
3963
- };
3964
- const updateActivityInActivities = (updatedActivity, activities) => {
3965
- const index = activities.findIndex((a) => a.id === updatedActivity.id);
3966
- if (index !== -1) {
3967
- const newActivities = [...activities];
3968
- newActivities[index] = updatedActivity;
3969
- return { changed: true, activities: newActivities };
3970
- }
3971
- else {
3972
- return { changed: false, activities };
3973
- }
3974
- };
3975
- const addBookmarkToActivity = (event, activity, isCurrentUser) => {
3976
- // Update own_bookmarks if the bookmark is from the current user
3977
- const ownBookmarks = [...(activity.own_bookmarks || [])];
3978
- if (isCurrentUser) {
3979
- ownBookmarks.push(event.bookmark);
3980
- }
3981
- return {
3982
- ...activity,
3983
- own_bookmarks: ownBookmarks,
3984
- changed: true,
3985
- };
3986
- };
3987
- const removeBookmarkFromActivity = (event, activity, isCurrentUser) => {
3988
- // Update own_bookmarks if the bookmark is from the current user
3989
- const ownBookmarks = isCurrentUser
3990
- ? (activity.own_bookmarks || []).filter((bookmark) => !isSameBookmark(bookmark, event.bookmark))
3991
- : activity.own_bookmarks;
3992
- return {
3993
- ...activity,
3994
- own_bookmarks: ownBookmarks,
3995
- changed: true,
3996
- };
3997
- };
3998
- const updateBookmarkInActivity = (event, activity, isCurrentUser) => {
3999
- // Update own_bookmarks if the bookmark is from the current user
4000
- let ownBookmarks = activity.own_bookmarks || [];
4001
- if (isCurrentUser) {
4002
- const bookmarkIndex = ownBookmarks.findIndex((bookmark) => isSameBookmark(bookmark, event.bookmark));
4003
- if (bookmarkIndex !== -1) {
4004
- ownBookmarks = [...ownBookmarks];
4005
- ownBookmarks[bookmarkIndex] = event.bookmark;
4006
- }
4007
- }
4008
- return {
4009
- ...activity,
4010
- own_bookmarks: ownBookmarks,
4011
- changed: true,
4012
- };
4013
- };
4014
- const addBookmarkToActivities = (event, activities, isCurrentUser) => {
4015
- if (!activities) {
4016
- return { changed: false, activities: [] };
4017
- }
4018
- const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4019
- if (activityIndex === -1) {
4020
- return { changed: false, activities };
4021
- }
4022
- const activity = activities[activityIndex];
4023
- const updatedActivity = addBookmarkToActivity(event, activity, isCurrentUser);
4024
- return updateActivityInActivities(updatedActivity, activities);
4025
- };
4026
- const removeBookmarkFromActivities = (event, activities, isCurrentUser) => {
4027
- if (!activities) {
4028
- return { changed: false, activities: [] };
4029
- }
4030
- const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4031
- if (activityIndex === -1) {
4032
- return { changed: false, activities };
4033
- }
4034
- const activity = activities[activityIndex];
4035
- const updatedActivity = removeBookmarkFromActivity(event, activity, isCurrentUser);
4036
- return updateActivityInActivities(updatedActivity, activities);
4037
- };
4038
- const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4039
- if (!activities) {
4040
- return { changed: false, activities: [] };
4041
- }
4042
- const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4043
- if (activityIndex === -1) {
4044
- return { changed: false, activities };
4045
- }
4046
- const activity = activities[activityIndex];
4047
- const updatedActivity = updateBookmarkInActivity(event, activity, isCurrentUser);
4048
- return updateActivityInActivities(updatedActivity, activities);
4049
- };
3884
+ class ModerationClient extends ModerationApi {
3885
+ }
4050
3886
 
4051
- const isFeedResponse = (follow) => {
4052
- return 'created_by' in follow;
4053
- };
4054
- const handleFollowCreated = (follow, currentState, currentFeedId, connectedUserId) => {
4055
- // filter non-accepted follows (the way getOrCreate does by default)
4056
- if (follow.status !== 'accepted') {
4057
- return { changed: false, data: currentState };
4058
- }
4059
- let newState = { ...currentState };
4060
- // this feed followed someone
4061
- if (follow.source_feed.fid === currentFeedId) {
4062
- newState = {
4063
- ...newState,
4064
- // Update FeedResponse fields, that has the new follower/following count
4065
- ...follow.source_feed,
4066
- };
4067
- // Only update if following array already exists
4068
- if (currentState.following !== undefined) {
4069
- newState.following = [follow, ...currentState.following];
4070
- }
4071
- }
4072
- else if (
4073
- // someone followed this feed
4074
- follow.target_feed.fid === currentFeedId) {
4075
- const source = follow.source_feed;
4076
- newState = {
4077
- ...newState,
4078
- // Update FeedResponse fields, that has the new follower/following count
4079
- ...follow.target_feed,
3887
+ const isPollUpdatedEvent = (e) => e.type === 'feeds.poll.updated';
3888
+ const isPollClosedEventEvent = (e) => e.type === 'feeds.poll.closed';
3889
+ const isPollVoteCastedEvent = (e) => e.type === 'feeds.poll.vote_casted';
3890
+ const isPollVoteChangedEvent = (e) => e.type === 'feeds.poll.vote_changed';
3891
+ const isPollVoteRemovedEvent = (e) => e.type === 'feeds.poll.vote_removed';
3892
+ const isVoteAnswer = (vote) => !!vote?.answer_text;
3893
+ class StreamPoll {
3894
+ constructor({ client, poll }) {
3895
+ this.getInitialStateFromPollResponse = (poll) => {
3896
+ const { own_votes, id, ...pollResponseForState } = poll;
3897
+ const { ownAnswer, ownVotes } = own_votes?.reduce((acc, voteOrAnswer) => {
3898
+ if (isVoteAnswer(voteOrAnswer)) {
3899
+ acc.ownAnswer = voteOrAnswer;
3900
+ }
3901
+ else {
3902
+ acc.ownVotes.push(voteOrAnswer);
3903
+ }
3904
+ return acc;
3905
+ }, { ownVotes: [] }) ?? { ownVotes: [] };
3906
+ return {
3907
+ ...pollResponseForState,
3908
+ last_activity_at: new Date(),
3909
+ max_voted_option_ids: getMaxVotedOptionIds(pollResponseForState.vote_counts_by_option),
3910
+ own_answer: ownAnswer,
3911
+ own_votes_by_option_id: getOwnVotesByOptionId(ownVotes),
3912
+ };
4080
3913
  };
4081
- if (source.created_by.id === connectedUserId) {
4082
- newState.own_follows = currentState.own_follows
4083
- ? currentState.own_follows.concat(follow)
4084
- : [follow];
4085
- }
4086
- // Only update if followers array already exists
4087
- if (currentState.followers !== undefined) {
4088
- newState.followers = [follow, ...currentState.followers];
4089
- }
4090
- }
4091
- return { changed: true, data: newState };
4092
- };
4093
- const handleFollowDeleted = (follow, currentState, currentFeedId, connectedUserId) => {
4094
- let newState = { ...currentState };
4095
- // this feed unfollowed someone
4096
- if (follow.source_feed.fid === currentFeedId) {
4097
- newState = {
4098
- ...newState,
4099
- // Update FeedResponse fields, that has the new follower/following count
4100
- ...follow.source_feed,
3914
+ this.reinitializeState = (poll) => {
3915
+ this.state.partialNext(this.getInitialStateFromPollResponse(poll));
4101
3916
  };
4102
- // Only update if following array already exists
4103
- if (currentState.following !== undefined) {
4104
- newState.following = currentState.following.filter((followItem) => followItem.target_feed.fid !== follow.target_feed.fid);
4105
- }
4106
- }
4107
- else if (
4108
- // someone unfollowed this feed
4109
- follow.target_feed.fid === currentFeedId) {
4110
- const source = follow.source_feed;
4111
- newState = {
4112
- ...newState,
4113
- // Update FeedResponse fields, that has the new follower/following count
4114
- ...follow.target_feed,
3917
+ this.handlePollUpdated = (event) => {
3918
+ if (event.poll?.id && event.poll.id !== this.id)
3919
+ return;
3920
+ if (!isPollUpdatedEvent(event))
3921
+ return;
3922
+ const { id, ...pollData } = event.poll;
3923
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3924
+ this.state.partialNext({
3925
+ ...pollData,
3926
+ last_activity_at: new Date(event.created_at),
3927
+ });
4115
3928
  };
4116
- if (isFeedResponse(source) &&
4117
- source.created_by.id === connectedUserId &&
4118
- currentState.own_follows !== undefined) {
4119
- newState.own_follows = currentState.own_follows.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4120
- }
4121
- // Only update if followers array already exists
4122
- if (currentState.followers !== undefined) {
4123
- newState.followers = currentState.followers.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4124
- }
4125
- }
4126
- return { changed: true, data: newState };
4127
- };
4128
- const handleFollowUpdated = (currentState) => {
4129
- // For now, we'll treat follow updates as no-ops since the current implementation does
4130
- // This can be enhanced later if needed
4131
- return { changed: false, data: currentState };
4132
- };
4133
-
4134
- const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4135
- typeof cursor === 'string';
4136
- const isCommentResponse = (entity) => {
4137
- return typeof entity?.object_id === 'string';
4138
- };
4139
- const Constants = {
4140
- DEFAULT_COMMENT_PAGINATION: 'first',
4141
- };
4142
- const uniqueArrayMerge = (existingArray, arrayToMerge, getKey) => {
4143
- const existing = new Set();
4144
- existingArray.forEach((value) => {
4145
- const key = getKey(value);
4146
- existing.add(key);
4147
- });
4148
- const filteredArrayToMerge = arrayToMerge.filter((value) => {
4149
- const key = getKey(value);
4150
- return !existing.has(key);
4151
- });
4152
- return existingArray.concat(filteredArrayToMerge);
4153
- };
4154
-
4155
- const shouldUpdateState = ({ stateUpdateId, stateUpdateQueue, watch, }) => {
4156
- if (!watch) {
4157
- return true;
4158
- }
4159
- if (watch && stateUpdateQueue.has(stateUpdateId)) {
4160
- stateUpdateQueue.delete(stateUpdateId);
4161
- return false;
4162
- }
4163
- stateUpdateQueue.add(stateUpdateId);
4164
- return true;
4165
- };
4166
- const getStateUpdateQueueIdForFollow = (follow) => {
4167
- return `follow${follow.source_feed.fid}-${follow.target_feed.fid}`;
4168
- };
4169
- const getStateUpdateQueueIdForUnfollow = (follow) => {
4170
- return `unfollow${follow.source_feed.fid}-${follow.target_feed.fid}`;
4171
- };
4172
-
4173
- class Feed extends FeedApi {
4174
- constructor(client, groupId, id, data, watch = false) {
4175
- super(client, groupId, id);
4176
- this.stateUpdateQueue = new Set();
4177
- this.eventHandlers = {
4178
- 'feeds.activity.added': (event) => {
4179
- const currentActivities = this.currentState.activities;
4180
- const result = addActivitiesToState([event.activity], currentActivities, 'start');
4181
- if (result.changed) {
4182
- this.client.hydratePollCache([event.activity]);
4183
- this.state.partialNext({ activities: result.activities });
4184
- }
4185
- },
4186
- 'feeds.activity.deleted': (event) => {
4187
- const currentActivities = this.currentState.activities;
4188
- if (currentActivities) {
4189
- const result = removeActivityFromState(event.activity, currentActivities);
4190
- if (result.changed) {
4191
- this.state.partialNext({ activities: result.activities });
4192
- }
4193
- }
4194
- },
4195
- 'feeds.activity.reaction.added': (event) => {
4196
- const currentActivities = this.currentState.activities;
4197
- const connectedUser = this.client.state.getLatestValue().connected_user;
4198
- const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4199
- const result = addReactionToActivities(event, currentActivities, isCurrentUser);
4200
- if (result.changed) {
4201
- this.state.partialNext({ activities: result.activities });
4202
- }
4203
- },
4204
- 'feeds.activity.reaction.deleted': (event) => {
4205
- const currentActivities = this.currentState.activities;
4206
- const connectedUser = this.client.state.getLatestValue().connected_user;
4207
- const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4208
- const result = removeReactionFromActivities(event, currentActivities, isCurrentUser);
4209
- if (result.changed) {
4210
- this.state.partialNext({ activities: result.activities });
4211
- }
4212
- },
4213
- 'feeds.activity.reaction.updated': Feed.noop,
4214
- 'feeds.activity.removed_from_feed': (event) => {
4215
- const currentActivities = this.currentState.activities;
4216
- if (currentActivities) {
4217
- const result = removeActivityFromState(event.activity, currentActivities);
4218
- if (result.changed) {
4219
- this.state.partialNext({ activities: result.activities });
4220
- }
4221
- }
4222
- },
4223
- 'feeds.activity.updated': (event) => {
4224
- const currentActivities = this.currentState.activities;
4225
- if (currentActivities) {
4226
- const result = updateActivityInState(event.activity, currentActivities);
4227
- if (result.changed) {
4228
- this.client.hydratePollCache([event.activity]);
4229
- this.state.partialNext({ activities: result.activities });
4230
- }
4231
- }
4232
- },
4233
- 'feeds.bookmark.added': this.handleBookmarkAdded.bind(this),
4234
- 'feeds.bookmark.deleted': this.handleBookmarkDeleted.bind(this),
4235
- 'feeds.bookmark.updated': this.handleBookmarkUpdated.bind(this),
4236
- 'feeds.bookmark_folder.deleted': Feed.noop,
4237
- 'feeds.bookmark_folder.updated': Feed.noop,
4238
- 'feeds.comment.added': (event) => {
4239
- const { comment } = event;
4240
- const forId = comment.parent_id ?? comment.object_id;
4241
- this.state.next((currentState) => {
4242
- const entityState = currentState.comments_by_entity_id[forId];
4243
- const newComments = entityState?.comments?.concat([]) ?? [];
4244
- if (entityState?.pagination?.sort === 'last' &&
4245
- !checkHasAnotherPage(entityState.comments, entityState?.pagination.next)) {
4246
- newComments.unshift(comment);
4247
- }
4248
- else if (entityState?.pagination?.sort === 'first') {
4249
- newComments.push(comment);
4250
- }
4251
- else {
4252
- // no other sorting option is supported yet
4253
- return currentState;
4254
- }
4255
- return {
4256
- ...currentState,
4257
- comments_by_entity_id: {
4258
- ...currentState.comments_by_entity_id,
4259
- [forId]: {
4260
- ...currentState.comments_by_entity_id[forId],
4261
- comments: newComments,
4262
- },
4263
- },
4264
- };
4265
- });
4266
- },
4267
- 'feeds.comment.deleted': ({ comment }) => {
4268
- const forId = comment.parent_id ?? comment.object_id;
4269
- this.state.next((currentState) => {
4270
- const newCommentsByEntityId = {
4271
- ...currentState.comments_by_entity_id,
4272
- [forId]: {
4273
- ...currentState.comments_by_entity_id[forId],
4274
- },
4275
- };
4276
- const index = this.getCommentIndex(comment, currentState);
4277
- if (newCommentsByEntityId?.[forId]?.comments?.length && index !== -1) {
4278
- newCommentsByEntityId[forId].comments = [
4279
- ...newCommentsByEntityId[forId].comments,
4280
- ];
4281
- newCommentsByEntityId[forId]?.comments?.splice(index, 1);
4282
- }
4283
- delete newCommentsByEntityId[comment.id];
4284
- return {
4285
- ...currentState,
4286
- comments_by_entity_id: newCommentsByEntityId,
4287
- };
4288
- });
4289
- },
4290
- 'feeds.comment.updated': (event) => {
4291
- const { comment } = event;
4292
- const forId = comment.parent_id ?? comment.object_id;
4293
- this.state.next((currentState) => {
4294
- const entityState = currentState.comments_by_entity_id[forId];
4295
- if (!entityState?.comments?.length)
4296
- return currentState;
4297
- const index = this.getCommentIndex(comment, currentState);
4298
- if (index === -1)
4299
- return currentState;
4300
- const newComments = [...entityState.comments];
4301
- newComments[index] = comment;
4302
- return {
4303
- ...currentState,
4304
- comments_by_entity_id: {
4305
- ...currentState.comments_by_entity_id,
4306
- [forId]: {
4307
- ...currentState.comments_by_entity_id[forId],
4308
- comments: newComments,
4309
- },
4310
- },
4311
- };
4312
- });
4313
- },
4314
- 'feeds.feed.created': Feed.noop,
4315
- 'feeds.feed.deleted': Feed.noop,
4316
- 'feeds.feed.updated': (event) => {
4317
- this.state.partialNext({ ...event.feed });
4318
- },
4319
- 'feeds.feed_group.changed': Feed.noop,
4320
- 'feeds.feed_group.deleted': Feed.noop,
4321
- 'feeds.follow.created': (event) => {
4322
- this.handleFollowCreated(event.follow);
4323
- },
4324
- 'feeds.follow.deleted': (event) => {
4325
- this.handleFollowDeleted(event.follow);
4326
- },
4327
- 'feeds.follow.updated': (_event) => {
4328
- handleFollowUpdated(this.currentState);
4329
- },
4330
- 'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
4331
- 'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4332
- 'feeds.comment.reaction.updated': Feed.noop,
4333
- 'feeds.feed_member.added': (event) => {
4334
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4335
- this.state.next((currentState) => {
4336
- let newState;
4337
- if (typeof currentState.members !== 'undefined') {
4338
- newState ?? (newState = {
4339
- ...currentState,
4340
- });
4341
- newState.members = [event.member, ...currentState.members];
4342
- }
4343
- if (connectedUser?.id === event.member.user.id) {
4344
- newState ?? (newState = {
4345
- ...currentState,
4346
- });
4347
- newState.own_membership = event.member;
4348
- }
4349
- return newState ?? currentState;
4350
- });
4351
- },
4352
- 'feeds.feed_member.removed': (event) => {
4353
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4354
- this.state.next((currentState) => {
4355
- const newState = {
4356
- ...currentState,
4357
- members: currentState.members?.filter((member) => member.user.id !== event.user?.id),
4358
- };
4359
- if (connectedUser?.id === event.member_id) {
4360
- delete newState.own_membership;
4361
- }
4362
- return newState;
4363
- });
4364
- },
4365
- 'feeds.feed_member.updated': (event) => {
4366
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4367
- this.state.next((currentState) => {
4368
- const memberIndex = currentState.members?.findIndex((member) => member.user.id === event.member.user.id) ?? -1;
4369
- let newState;
4370
- if (memberIndex !== -1) {
4371
- // if there's an index, there's a member to update
4372
- const newMembers = [...currentState.members];
4373
- newMembers[memberIndex] = event.member;
4374
- newState ?? (newState = {
4375
- ...currentState,
4376
- });
4377
- newState.members = newMembers;
4378
- }
4379
- if (connectedUser?.id === event.member.user.id) {
4380
- newState ?? (newState = {
4381
- ...currentState,
4382
- });
4383
- newState.own_membership = event.member;
4384
- }
4385
- return newState ?? currentState;
4386
- });
4387
- },
4388
- 'feeds.notification_feed.updated': (event) => {
4389
- console.info('notification feed updated', event);
4390
- // TODO: handle notification feed updates
4391
- },
4392
- // the poll events should be removed from here
4393
- 'feeds.poll.closed': Feed.noop,
4394
- 'feeds.poll.deleted': Feed.noop,
4395
- 'feeds.poll.updated': Feed.noop,
4396
- 'feeds.poll.vote_casted': Feed.noop,
4397
- 'feeds.poll.vote_changed': Feed.noop,
4398
- 'feeds.poll.vote_removed': Feed.noop,
4399
- 'feeds.activity.pinned': Feed.noop,
4400
- 'feeds.activity.unpinned': Feed.noop,
4401
- 'feeds.activity.marked': Feed.noop,
4402
- 'moderation.custom_action': Feed.noop,
4403
- 'moderation.flagged': Feed.noop,
4404
- 'moderation.mark_reviewed': Feed.noop,
4405
- 'health.check': Feed.noop,
4406
- 'app.updated': Feed.noop,
4407
- 'user.banned': Feed.noop,
4408
- 'user.deactivated': Feed.noop,
4409
- 'user.muted': Feed.noop,
4410
- 'user.reactivated': Feed.noop,
4411
- 'user.updated': Feed.noop,
3929
+ this.handlePollClosed = (event) => {
3930
+ if (event.poll?.id && event.poll.id !== this.id)
3931
+ return;
3932
+ if (!isPollClosedEventEvent(event))
3933
+ return;
3934
+ this.state.partialNext({
3935
+ is_closed: true,
3936
+ last_activity_at: new Date(event.created_at),
3937
+ });
4412
3938
  };
4413
- this.eventDispatcher = new EventDispatcher();
4414
- this.on = this.eventDispatcher.on;
4415
- this.off = this.eventDispatcher.off;
4416
- this.state = new StateStore({
4417
- fid: `${groupId}:${id}`,
4418
- group_id: groupId,
4419
- id,
4420
- ...(data ?? {}),
4421
- is_loading: false,
4422
- is_loading_activities: false,
4423
- comments_by_entity_id: {},
4424
- watch,
4425
- });
4426
- this.client = client;
4427
- }
4428
- get fid() {
4429
- return `${this.group}:${this.id}`;
4430
- }
4431
- get currentState() {
4432
- return this.state.getLatestValue();
4433
- }
4434
- handleCommentReactionEvent(event) {
4435
- const { comment, reaction } = event;
4436
- const connectedUser = this.client.state.getLatestValue().connected_user;
4437
- this.state.next((currentState) => {
4438
- const forId = comment.parent_id ?? comment.object_id;
4439
- const entityState = currentState.comments_by_entity_id[forId];
4440
- const commentIndex = this.getCommentIndex(comment, currentState);
4441
- if (commentIndex === -1)
4442
- return currentState;
4443
- const newComments = entityState?.comments?.concat([]) ?? [];
4444
- const commentCopy = { ...comment };
4445
- delete commentCopy.own_reactions;
4446
- const newComment = {
4447
- ...newComments[commentIndex],
4448
- ...commentCopy,
4449
- // TODO: FIXME this should be handled by the backend
4450
- latest_reactions: commentCopy.latest_reactions ?? [],
4451
- reaction_groups: commentCopy.reaction_groups ?? {},
4452
- };
4453
- newComments[commentIndex] = newComment;
4454
- if (reaction.user.id === connectedUser?.id) {
4455
- if (event.type === 'feeds.comment.reaction.added') {
4456
- newComment.own_reactions = newComment.own_reactions.concat(reaction) ?? [reaction];
3939
+ this.handleVoteCasted = (event) => {
3940
+ if (event.poll?.id && event.poll.id !== this.id)
3941
+ return;
3942
+ if (!isPollVoteCastedEvent(event))
3943
+ return;
3944
+ const currentState = this.data;
3945
+ const isOwnVote = event.poll_vote.user_id ===
3946
+ this.client.state.getLatestValue().connected_user?.id;
3947
+ let latestAnswers = [...currentState.latest_answers];
3948
+ let ownAnswer = currentState.own_answer;
3949
+ const ownVotesByOptionId = currentState.own_votes_by_option_id;
3950
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
3951
+ if (isOwnVote) {
3952
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3953
+ if (isVoteAnswer(event.poll_vote)) {
3954
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3955
+ ownAnswer = event.poll_vote;
4457
3956
  }
4458
- else if (event.type === 'feeds.comment.reaction.deleted') {
4459
- newComment.own_reactions = newComment.own_reactions.filter((r) => r.type !== reaction.type);
3957
+ else if (event.poll_vote.option_id) {
3958
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3959
+ ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
4460
3960
  }
4461
3961
  }
4462
- return {
4463
- ...currentState,
4464
- comments_by_entity_id: {
4465
- ...currentState.comments_by_entity_id,
4466
- [forId]: {
4467
- ...entityState,
4468
- comments: newComments,
4469
- },
4470
- },
4471
- };
4472
- });
4473
- }
4474
- async synchronize() {
4475
- const { last_get_or_create_request_config } = this.state.getLatestValue();
4476
- if (last_get_or_create_request_config?.watch) {
4477
- await this.getOrCreate(last_get_or_create_request_config);
4478
- }
4479
- }
4480
- async getOrCreate(request) {
4481
- if (this.currentState.is_loading_activities) {
4482
- throw new Error('Only one getOrCreate call is allowed at a time');
4483
- }
4484
- this.state.partialNext({
4485
- is_loading: !request?.next,
4486
- is_loading_activities: true,
4487
- });
4488
- // TODO: pull comments/comment_pagination from activities and comment_sort from request
4489
- // and pre-populate comments_by_entity_id (once comment_sort and comment_limit are supported)
4490
- try {
4491
- const response = await super.getOrCreate(request);
4492
- if (request?.next) {
4493
- const { activities: currentActivities = [] } = this.currentState;
4494
- const result = addActivitiesToState(response.activities, currentActivities, 'end');
4495
- if (result.changed) {
4496
- this.state.partialNext({
4497
- activities: result.activities,
4498
- next: response.next,
4499
- prev: response.prev,
4500
- });
3962
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3963
+ if (isVoteAnswer(event.poll_vote)) {
3964
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3965
+ latestAnswers = [event.poll_vote, ...latestAnswers];
3966
+ }
3967
+ else {
3968
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
3969
+ }
3970
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
3971
+ this.state.partialNext({
3972
+ answers_count,
3973
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3974
+ latest_votes_by_option,
3975
+ vote_count,
3976
+ vote_counts_by_option,
3977
+ latest_answers: latestAnswers,
3978
+ last_activity_at: new Date(event.created_at),
3979
+ own_answer: ownAnswer,
3980
+ own_votes_by_option_id: ownVotesByOptionId,
3981
+ max_voted_option_ids: maxVotedOptionIds,
3982
+ });
3983
+ };
3984
+ this.handleVoteChanged = (event) => {
3985
+ // this event is triggered only when event.poll.enforce_unique_vote === true
3986
+ if (event.poll?.id && event.poll.id !== this.id)
3987
+ return;
3988
+ if (!isPollVoteChangedEvent(event))
3989
+ return;
3990
+ const currentState = this.data;
3991
+ const isOwnVote = event.poll_vote.user_id ===
3992
+ this.client.state.getLatestValue().connected_user?.id;
3993
+ let latestAnswers = [...currentState.latest_answers];
3994
+ let ownAnswer = currentState.own_answer;
3995
+ let ownVotesByOptionId = currentState.own_votes_by_option_id;
3996
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
3997
+ if (isOwnVote) {
3998
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3999
+ if (isVoteAnswer(event.poll_vote)) {
4000
+ latestAnswers = [
4001
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4002
+ event.poll_vote,
4003
+ ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id),
4004
+ ];
4005
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4006
+ ownAnswer = event.poll_vote;
4501
4007
  }
4502
- }
4503
- else {
4504
- // Empty queue when reinitializing the state
4505
- this.stateUpdateQueue.clear();
4506
- const responseCopy = {
4507
- ...response,
4508
- ...response.feed,
4509
- };
4510
- delete responseCopy.feed;
4511
- delete responseCopy.metadata;
4512
- delete responseCopy.duration;
4513
- this.state.next((currentState) => {
4514
- const nextState = {
4515
- ...currentState,
4516
- ...responseCopy,
4517
- };
4518
- if (!request?.followers_pagination?.limit) {
4519
- delete nextState.followers;
4008
+ else if (event.poll_vote.option_id) {
4009
+ if (event.poll.enforce_unique_vote) {
4010
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4011
+ ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
4520
4012
  }
4521
- if (!request?.following_pagination?.limit) {
4522
- delete nextState.following;
4013
+ else {
4014
+ ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce((acc, [optionId, vote]) => {
4015
+ if (optionId !== event.poll_vote.option_id &&
4016
+ vote.id === event.poll_vote.id) {
4017
+ return acc;
4018
+ }
4019
+ acc[optionId] = vote;
4020
+ return acc;
4021
+ }, {});
4022
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4023
+ ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
4523
4024
  }
4524
- if (response.members.length === 0 && response.feed.member_count > 0) {
4525
- delete nextState.members;
4025
+ if (ownAnswer?.id === event.poll_vote.id) {
4026
+ ownAnswer = undefined;
4526
4027
  }
4527
- nextState.last_get_or_create_request_config = request;
4528
- nextState.watch = request?.watch ? request.watch : currentState.watch;
4529
- return nextState;
4530
- });
4028
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4029
+ }
4030
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4531
4031
  }
4532
- this.client.hydratePollCache(response.activities);
4533
- return response;
4534
- }
4535
- finally {
4032
+ else if (isVoteAnswer(event.poll_vote)) {
4033
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4034
+ latestAnswers = [event.poll_vote, ...latestAnswers];
4035
+ }
4036
+ else {
4037
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4038
+ }
4039
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
4536
4040
  this.state.partialNext({
4537
- is_loading: false,
4538
- is_loading_activities: false,
4041
+ answers_count,
4042
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4043
+ latest_votes_by_option,
4044
+ vote_count,
4045
+ vote_counts_by_option,
4046
+ latest_answers: latestAnswers,
4047
+ last_activity_at: new Date(event.created_at),
4048
+ own_answer: ownAnswer,
4049
+ own_votes_by_option_id: ownVotesByOptionId,
4050
+ max_voted_option_ids: maxVotedOptionIds,
4539
4051
  });
4540
- }
4052
+ };
4053
+ this.handleVoteRemoved = (event) => {
4054
+ if (event.poll?.id && event.poll.id !== this.id)
4055
+ return;
4056
+ if (!isPollVoteRemovedEvent(event))
4057
+ return;
4058
+ const currentState = this.data;
4059
+ const isOwnVote = event.poll_vote.user_id ===
4060
+ this.client.state.getLatestValue().connected_user?.id;
4061
+ let latestAnswers = [...currentState.latest_answers];
4062
+ let ownAnswer = currentState.own_answer;
4063
+ const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
4064
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
4065
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4066
+ if (isVoteAnswer(event.poll_vote)) {
4067
+ latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
4068
+ if (isOwnVote) {
4069
+ ownAnswer = undefined;
4070
+ }
4071
+ }
4072
+ else {
4073
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4074
+ if (isOwnVote && event.poll_vote.option_id) {
4075
+ delete ownVotesByOptionId[event.poll_vote.option_id];
4076
+ }
4077
+ }
4078
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
4079
+ this.state.partialNext({
4080
+ answers_count,
4081
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4082
+ latest_votes_by_option,
4083
+ vote_count,
4084
+ vote_counts_by_option,
4085
+ latest_answers: latestAnswers,
4086
+ last_activity_at: new Date(event.created_at),
4087
+ own_answer: ownAnswer,
4088
+ own_votes_by_option_id: ownVotesByOptionId,
4089
+ max_voted_option_ids: maxVotedOptionIds,
4090
+ });
4091
+ };
4092
+ this.client = client;
4093
+ this.id = poll.id;
4094
+ this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
4541
4095
  }
4542
- /**
4543
- * @internal
4544
- */
4545
- handleFollowCreated(follow) {
4546
- if (!shouldUpdateState({
4547
- stateUpdateId: getStateUpdateQueueIdForFollow(follow),
4548
- stateUpdateQueue: this.stateUpdateQueue,
4549
- watch: this.currentState.watch,
4550
- })) {
4551
- return;
4552
- }
4553
- const connectedUser = this.client.state.getLatestValue().connected_user;
4554
- const result = handleFollowCreated(follow, this.currentState, this.fid, connectedUser?.id);
4555
- if (result.changed) {
4556
- this.state.next(result.data);
4557
- }
4096
+ get data() {
4097
+ return this.state.getLatestValue();
4558
4098
  }
4559
- /**
4560
- * @internal
4561
- */
4562
- handleFollowDeleted(follow) {
4563
- if (!shouldUpdateState({
4564
- stateUpdateId: getStateUpdateQueueIdForUnfollow(follow),
4565
- stateUpdateQueue: this.stateUpdateQueue,
4566
- watch: this.currentState.watch,
4567
- })) {
4568
- return;
4099
+ }
4100
+ function getMaxVotedOptionIds(voteCountsByOption) {
4101
+ let maxVotes = 0;
4102
+ let winningOptions = [];
4103
+ for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
4104
+ if (count > maxVotes) {
4105
+ winningOptions = [id];
4106
+ maxVotes = count;
4569
4107
  }
4570
- const connectedUser = this.client.state.getLatestValue().connected_user;
4571
- const result = handleFollowDeleted(follow, this.currentState, this.fid, connectedUser?.id);
4572
- {
4573
- this.state.next(result.data);
4108
+ else if (count === maxVotes) {
4109
+ winningOptions.push(id);
4574
4110
  }
4575
4111
  }
4576
- /**
4577
- * @internal
4578
- */
4579
- handleWatchStopped() {
4580
- this.state.partialNext({
4581
- watch: false,
4112
+ return winningOptions;
4113
+ }
4114
+ function getOwnVotesByOptionId(ownVotes) {
4115
+ return !ownVotes
4116
+ ? {}
4117
+ : ownVotes.reduce((acc, vote) => {
4118
+ if (isVoteAnswer(vote) || !vote.option_id)
4119
+ return acc;
4120
+ acc[vote.option_id] = vote;
4121
+ return acc;
4122
+ }, {});
4123
+ }
4124
+
4125
+ class FeedApi {
4126
+ constructor(feedsApi, group, id) {
4127
+ this.feedsApi = feedsApi;
4128
+ this.group = group;
4129
+ this.id = id;
4130
+ }
4131
+ delete(request) {
4132
+ return this.feedsApi.deleteFeed({
4133
+ feed_id: this.id,
4134
+ feed_group_id: this.group,
4135
+ ...request,
4582
4136
  });
4583
4137
  }
4584
- /**
4585
- * @internal
4586
- */
4587
- handleWatchStarted() {
4588
- this.state.partialNext({
4589
- watch: true,
4138
+ getOrCreate(request) {
4139
+ return this.feedsApi.getOrCreateFeed({
4140
+ feed_id: this.id,
4141
+ feed_group_id: this.group,
4142
+ ...request,
4590
4143
  });
4591
4144
  }
4592
- handleBookmarkAdded(event) {
4593
- const currentActivities = this.currentState.activities;
4594
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4595
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4596
- const result = addBookmarkToActivities(event, currentActivities, isCurrentUser);
4597
- if (result.changed) {
4598
- this.state.partialNext({ activities: result.activities });
4599
- }
4145
+ update(request) {
4146
+ return this.feedsApi.updateFeed({
4147
+ feed_id: this.id,
4148
+ feed_group_id: this.group,
4149
+ ...request,
4150
+ });
4600
4151
  }
4601
- handleBookmarkDeleted(event) {
4602
- const currentActivities = this.currentState.activities;
4603
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4604
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4605
- const result = removeBookmarkFromActivities(event, currentActivities, isCurrentUser);
4606
- if (result.changed) {
4607
- this.state.partialNext({ activities: result.activities });
4608
- }
4152
+ markActivity(request) {
4153
+ return this.feedsApi.markActivity({
4154
+ feed_id: this.id,
4155
+ feed_group_id: this.group,
4156
+ ...request,
4157
+ });
4609
4158
  }
4610
- handleBookmarkUpdated(event) {
4611
- const currentActivities = this.currentState.activities;
4612
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4613
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4614
- const result = updateBookmarkInActivities(event, currentActivities, isCurrentUser);
4615
- if (result.changed) {
4616
- this.state.partialNext({ activities: result.activities });
4617
- }
4159
+ unpinActivity(request) {
4160
+ return this.feedsApi.unpinActivity({
4161
+ feed_id: this.id,
4162
+ feed_group_id: this.group,
4163
+ ...request,
4164
+ });
4618
4165
  }
4619
- /**
4620
- * Returns index of the provided comment object.
4621
- */
4622
- getCommentIndex(comment, state) {
4623
- const { comments_by_entity_id = {} } = state ?? this.currentState;
4624
- const currentComments = comments_by_entity_id[comment.parent_id ?? comment.object_id]?.comments;
4625
- if (!currentComments?.length) {
4626
- return -1;
4627
- }
4628
- // @ts-expect-error this will just fail if the comment is not object from state
4629
- let commentIndex = currentComments.indexOf(comment);
4630
- // fast lookup failed, try slower approach
4631
- if (commentIndex === -1) {
4632
- commentIndex = currentComments.findIndex((comment_) => comment_.id === comment.id);
4633
- }
4634
- return commentIndex;
4166
+ pinActivity(request) {
4167
+ return this.feedsApi.pinActivity({
4168
+ feed_id: this.id,
4169
+ feed_group_id: this.group,
4170
+ ...request,
4171
+ });
4172
+ }
4173
+ updateFeedMembers(request) {
4174
+ return this.feedsApi.updateFeedMembers({
4175
+ feed_id: this.id,
4176
+ feed_group_id: this.group,
4177
+ ...request,
4178
+ });
4179
+ }
4180
+ acceptFeedMemberInvite(request) {
4181
+ return this.feedsApi.acceptFeedMemberInvite({
4182
+ feed_id: this.id,
4183
+ feed_group_id: this.group,
4184
+ ...request,
4185
+ });
4186
+ }
4187
+ queryFeedMembers(request) {
4188
+ return this.feedsApi.queryFeedMembers({
4189
+ feed_id: this.id,
4190
+ feed_group_id: this.group,
4191
+ ...request,
4192
+ });
4193
+ }
4194
+ rejectFeedMemberInvite(request) {
4195
+ return this.feedsApi.rejectFeedMemberInvite({
4196
+ feed_id: this.id,
4197
+ feed_group_id: this.group,
4198
+ ...request,
4199
+ });
4635
4200
  }
4636
- /**
4637
- * Load child comments of entity (activity or comment) into the state, if the target entity is comment,
4638
- * `entityParentId` should be provided (`CommentResponse.parent_id ?? CommentResponse.object_id`).
4639
- */
4640
- loadCommentsIntoState(data) {
4641
- // add initial (top level) object for processing
4642
- const traverseArray = [
4643
- {
4644
- entityId: data.entityId,
4645
- entityParentId: data.entityParentId,
4646
- comments: data.comments,
4647
- next: data.next,
4648
- },
4649
- ];
4650
- this.state.next((currentState) => {
4651
- const newCommentsByEntityId = {
4652
- ...currentState.comments_by_entity_id,
4653
- };
4654
- while (traverseArray.length) {
4655
- const item = traverseArray.pop();
4656
- const entityId = item.entityId;
4657
- // go over entity comments and generate new objects
4658
- // for further processing if there are any replies
4659
- item.comments.forEach((comment) => {
4660
- if (!comment.replies?.length)
4661
- return;
4662
- traverseArray.push({
4663
- entityId: comment.id,
4664
- entityParentId: entityId,
4665
- comments: comment.replies,
4666
- next: comment.meta?.next_cursor,
4667
- });
4668
- });
4669
- // omit replies & meta from the comments (transform ThreadedCommentResponse to CommentResponse)
4670
- // this is somehow faster than copying the whole
4671
- // object and deleting the desired properties
4672
- const newComments = item.comments.map(({ replies: _r, meta: _m, ...restOfTheCommentResponse }) => restOfTheCommentResponse);
4673
- newCommentsByEntityId[entityId] = {
4674
- ...newCommentsByEntityId[entityId],
4675
- entity_parent_id: item.entityParentId,
4676
- pagination: {
4677
- ...newCommentsByEntityId[entityId]?.pagination,
4678
- next: item.next,
4679
- sort: data.sort,
4680
- },
4681
- comments: newCommentsByEntityId[entityId]?.comments
4682
- ? newCommentsByEntityId[entityId].comments?.concat(newComments)
4683
- : newComments,
4684
- };
4685
- }
4686
- return {
4687
- ...currentState,
4688
- comments_by_entity_id: newCommentsByEntityId,
4689
- };
4201
+ stopWatching(request) {
4202
+ return this.feedsApi.stopWatchingFeed({
4203
+ feed_id: this.id,
4204
+ feed_group_id: this.group,
4205
+ ...request,
4690
4206
  });
4691
4207
  }
4692
- async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
4693
- let error;
4694
- try {
4695
- this.state.next((currentState) => ({
4696
- ...currentState,
4697
- comments_by_entity_id: {
4698
- ...currentState.comments_by_entity_id,
4699
- [entityId]: {
4700
- ...currentState.comments_by_entity_id[entityId],
4701
- pagination: {
4702
- ...currentState.comments_by_entity_id[entityId]?.pagination,
4703
- loading_next_page: true,
4704
- },
4705
- },
4706
- },
4707
- }));
4708
- const { next, comments } = await base();
4709
- this.loadCommentsIntoState({
4710
- entityId,
4711
- comments,
4712
- entityParentId,
4713
- next,
4714
- sort,
4715
- });
4716
- }
4717
- catch (e) {
4718
- error = e;
4719
- }
4720
- finally {
4721
- this.state.next((currentState) => ({
4722
- ...currentState,
4723
- comments_by_entity_id: {
4724
- ...currentState.comments_by_entity_id,
4725
- [entityId]: {
4726
- ...currentState.comments_by_entity_id[entityId],
4727
- pagination: {
4728
- ...currentState.comments_by_entity_id[entityId]?.pagination,
4729
- loading_next_page: false,
4730
- },
4731
- },
4732
- },
4733
- }));
4734
- }
4735
- if (error) {
4736
- throw error;
4737
- }
4208
+ }
4209
+
4210
+ const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4211
+ typeof cursor === 'string';
4212
+
4213
+ const uniqueArrayMerge = (existingArray, arrayToMerge, getKey) => {
4214
+ const existing = new Set();
4215
+ existingArray.forEach((value) => {
4216
+ const key = getKey(value);
4217
+ existing.add(key);
4218
+ });
4219
+ const filteredArrayToMerge = arrayToMerge.filter((value) => {
4220
+ const key = getKey(value);
4221
+ return !existing.has(key);
4222
+ });
4223
+ return existingArray.concat(filteredArrayToMerge);
4224
+ };
4225
+
4226
+ const Constants = {
4227
+ DEFAULT_COMMENT_PAGINATION: 'first',
4228
+ };
4229
+
4230
+ const isFollowResponse = (data) => {
4231
+ return 'source_feed' in data && 'target_feed' in data;
4232
+ };
4233
+ const isCommentResponse = (entity) => {
4234
+ return typeof entity?.object_id === 'string';
4235
+ };
4236
+
4237
+ const shouldUpdateState = ({ stateUpdateQueueId, stateUpdateQueue, watch, }) => {
4238
+ if (!watch) {
4239
+ return true;
4738
4240
  }
4739
- async loadNextPageActivityComments(activity, request) {
4740
- const currentEntityState = this.currentState.comments_by_entity_id[activity.id];
4741
- const currentPagination = currentEntityState?.pagination;
4742
- const currentNextCursor = currentPagination?.next;
4743
- const currentSort = currentPagination?.sort;
4744
- const isLoading = currentPagination?.loading_next_page;
4745
- const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4746
- if (isLoading ||
4747
- !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4748
- return;
4241
+ if (watch && stateUpdateQueue.has(stateUpdateQueueId)) {
4242
+ stateUpdateQueue.delete(stateUpdateQueueId);
4243
+ return false;
4244
+ }
4245
+ stateUpdateQueue.add(stateUpdateQueueId);
4246
+ return true;
4247
+ };
4248
+ function getStateUpdateQueueId(data, prefix) {
4249
+ if (isFollowResponse(data)) {
4250
+ const toJoin = [data.source_feed.fid, data.target_feed.fid];
4251
+ if (prefix) {
4252
+ toJoin.unshift(prefix);
4749
4253
  }
4750
- await this.loadNextPageComments({
4751
- entityId: activity.id,
4752
- base: () => this.client.getComments({
4753
- ...request,
4754
- sort,
4755
- object_id: activity.id,
4756
- object_type: 'activity',
4757
- next: currentNextCursor,
4758
- }),
4759
- sort,
4760
- });
4254
+ return toJoin.join('-');
4761
4255
  }
4762
- async loadNextPageCommentReplies(comment, request) {
4763
- const currentEntityState = this.currentState.comments_by_entity_id[comment.id];
4764
- const currentPagination = currentEntityState?.pagination;
4765
- const currentNextCursor = currentPagination?.next;
4766
- const currentSort = currentPagination?.sort;
4767
- const isLoading = currentPagination?.loading_next_page;
4768
- const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4769
- if (isLoading ||
4770
- !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4771
- return;
4256
+ // else if (isMemberResponse(data)) {
4257
+ // }
4258
+ throw new Error(`Cannot create state update queueId for data: ${JSON.stringify(data)}`);
4259
+ }
4260
+
4261
+ const updateStateFollowCreated = (follow, currentState, currentFeedId, connectedUserId) => {
4262
+ // filter non-accepted follows (the way getOrCreate does by default)
4263
+ if (follow.status !== 'accepted') {
4264
+ return { changed: false, data: currentState };
4265
+ }
4266
+ let newState = { ...currentState };
4267
+ // this feed followed someone
4268
+ if (follow.source_feed.fid === currentFeedId) {
4269
+ newState = {
4270
+ ...newState,
4271
+ // Update FeedResponse fields, that has the new follower/following count
4272
+ ...follow.source_feed,
4273
+ };
4274
+ // Only update if following array already exists
4275
+ if (currentState.following !== undefined) {
4276
+ newState.following = [follow, ...currentState.following];
4772
4277
  }
4773
- await this.loadNextPageComments({
4774
- entityId: comment.id,
4775
- base: () => this.client.getCommentReplies({
4776
- ...request,
4777
- comment_id: comment.id,
4778
- // use known sort first (prevents broken pagination)
4779
- sort: currentSort ??
4780
- request?.sort ??
4781
- Constants.DEFAULT_COMMENT_PAGINATION,
4782
- next: currentNextCursor,
4783
- }),
4784
- entityParentId: comment.parent_id ?? comment.object_id,
4785
- sort,
4786
- });
4787
4278
  }
4788
- async loadNextPageFollows(type, request) {
4789
- const paginationKey = `${type}_pagination`;
4790
- const method = `query${capitalize(type)}`;
4791
- const currentFollows = this.currentState[type];
4792
- const currentNextCursor = this.currentState[paginationKey]?.next;
4793
- const isLoading = this.currentState[paginationKey]?.loading_next_page;
4794
- const sort = this.currentState[paginationKey]?.sort ?? request.sort;
4795
- let error;
4796
- if (isLoading || !checkHasAnotherPage(currentFollows, currentNextCursor)) {
4797
- return;
4279
+ else if (
4280
+ // someone followed this feed
4281
+ follow.target_feed.fid === currentFeedId) {
4282
+ const source = follow.source_feed;
4283
+ newState = {
4284
+ ...newState,
4285
+ // Update FeedResponse fields, that has the new follower/following count
4286
+ ...follow.target_feed,
4287
+ };
4288
+ if (source.created_by.id === connectedUserId) {
4289
+ newState.own_follows = currentState.own_follows
4290
+ ? currentState.own_follows.concat(follow)
4291
+ : [follow];
4798
4292
  }
4799
- try {
4800
- this.state.next((currentState) => {
4801
- return {
4802
- ...currentState,
4803
- [paginationKey]: {
4804
- ...currentState[paginationKey],
4805
- loading_next_page: true,
4806
- },
4807
- };
4808
- });
4809
- const { next: newNextCursor, follows } = await this[method]({
4810
- ...request,
4811
- next: currentNextCursor,
4812
- sort,
4813
- });
4814
- this.state.next((currentState) => {
4815
- return {
4816
- ...currentState,
4817
- [type]: currentState[type] === undefined
4818
- ? follows
4819
- : uniqueArrayMerge(currentState[type], follows, (follow) => `${follow.source_feed.fid}-${follow.target_feed.fid}`),
4820
- [paginationKey]: {
4821
- ...currentState[paginationKey],
4822
- next: newNextCursor,
4823
- sort,
4824
- },
4825
- };
4826
- });
4293
+ // Only update if followers array already exists
4294
+ if (currentState.followers !== undefined) {
4295
+ newState.followers = [follow, ...currentState.followers];
4827
4296
  }
4828
- catch (e) {
4829
- error = e;
4297
+ }
4298
+ return { changed: true, data: newState };
4299
+ };
4300
+ function handleFollowCreated(eventOrResponse) {
4301
+ const follow = eventOrResponse.follow;
4302
+ if (!shouldUpdateState({
4303
+ stateUpdateQueueId: getStateUpdateQueueId(follow, 'created'),
4304
+ stateUpdateQueue: this.stateUpdateQueue,
4305
+ watch: this.currentState.watch,
4306
+ })) {
4307
+ return;
4308
+ }
4309
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4310
+ const result = updateStateFollowCreated(follow, this.currentState, this.fid, connectedUser?.id);
4311
+ if (result.changed) {
4312
+ this.state.next(result.data);
4313
+ }
4314
+ }
4315
+
4316
+ const updateStateFollowDeleted = (follow, currentState, currentFeedId, connectedUserId) => {
4317
+ let newState = { ...currentState };
4318
+ // this feed unfollowed someone
4319
+ if (follow.source_feed.fid === currentFeedId) {
4320
+ newState = {
4321
+ ...newState,
4322
+ // Update FeedResponse fields, that has the new follower/following count
4323
+ ...follow.source_feed,
4324
+ };
4325
+ // Only update if following array already exists
4326
+ if (currentState.following !== undefined) {
4327
+ newState.following = currentState.following.filter((followItem) => followItem.target_feed.fid !== follow.target_feed.fid);
4830
4328
  }
4831
- finally {
4832
- this.state.next((currentState) => {
4833
- return {
4834
- ...currentState,
4835
- [paginationKey]: {
4836
- ...currentState[paginationKey],
4837
- loading_next_page: false,
4838
- },
4839
- };
4840
- });
4329
+ }
4330
+ else if (
4331
+ // someone unfollowed this feed
4332
+ follow.target_feed.fid === currentFeedId) {
4333
+ const source = follow.source_feed;
4334
+ newState = {
4335
+ ...newState,
4336
+ // Update FeedResponse fields, that has the new follower/following count
4337
+ ...follow.target_feed,
4338
+ };
4339
+ if (source.created_by.id === connectedUserId &&
4340
+ currentState.own_follows !== undefined) {
4341
+ newState.own_follows = currentState.own_follows.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4841
4342
  }
4842
- if (error) {
4843
- throw error;
4343
+ // Only update if followers array already exists
4344
+ if (currentState.followers !== undefined) {
4345
+ newState.followers = currentState.followers.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4844
4346
  }
4845
4347
  }
4846
- async loadNextPageFollowers(request) {
4847
- await this.loadNextPageFollows('followers', request);
4348
+ return { changed: true, data: newState };
4349
+ };
4350
+ function handleFollowDeleted(eventOrResponse) {
4351
+ const follow = eventOrResponse.follow;
4352
+ if (!shouldUpdateState({
4353
+ stateUpdateQueueId: getStateUpdateQueueId(follow, 'deleted'),
4354
+ stateUpdateQueue: this.stateUpdateQueue,
4355
+ watch: this.currentState.watch,
4356
+ })) {
4357
+ return;
4848
4358
  }
4849
- async loadNextPageFollowing(request) {
4850
- await this.loadNextPageFollows('following', request);
4359
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4360
+ const result = updateStateFollowDeleted(follow, this.currentState, this.fid, connectedUser?.id);
4361
+ {
4362
+ this.state.next(result.data);
4851
4363
  }
4852
- async loadNextPageMembers(request) {
4853
- const currentMembers = this.currentState.members;
4854
- const currentNextCursor = this.currentState.member_pagination?.next;
4855
- const isLoading = this.currentState.member_pagination?.loading_next_page;
4856
- const sort = this.currentState.member_pagination?.sort ?? request.sort;
4857
- let error;
4858
- if (isLoading || !checkHasAnotherPage(currentMembers, currentNextCursor)) {
4859
- return;
4364
+ }
4365
+
4366
+ function handleFollowUpdated(eventOrResponse) {
4367
+ const follow = eventOrResponse.follow;
4368
+ const connectedUserId = this.client.state.getLatestValue().connected_user?.id;
4369
+ const currentFeedId = this.fid;
4370
+ if (!shouldUpdateState({
4371
+ stateUpdateQueueId: getStateUpdateQueueId(follow, 'updated'),
4372
+ stateUpdateQueue: this.stateUpdateQueue,
4373
+ watch: this.currentState.watch,
4374
+ })) {
4375
+ return;
4376
+ }
4377
+ this.state.next((currentState) => {
4378
+ let newState;
4379
+ // this feed followed someone
4380
+ if (follow.source_feed.fid === currentFeedId) {
4381
+ newState ?? (newState = {
4382
+ ...currentState,
4383
+ // Update FeedResponse fields, that has the new follower/following count
4384
+ ...follow.source_feed,
4385
+ });
4386
+ const index = currentState.following?.findIndex((f) => f.target_feed.fid === follow.target_feed.fid) ?? -1;
4387
+ if (index >= 0) {
4388
+ newState.following = [...newState.following];
4389
+ newState.following[index] = follow;
4390
+ }
4860
4391
  }
4861
- try {
4862
- this.state.next((currentState) => ({
4392
+ else if (
4393
+ // someone followed this feed
4394
+ follow.target_feed.fid === currentFeedId) {
4395
+ const source = follow.source_feed;
4396
+ newState ?? (newState = {
4863
4397
  ...currentState,
4864
- member_pagination: {
4865
- ...currentState.member_pagination,
4866
- loading_next_page: true,
4398
+ // Update FeedResponse fields, that has the new follower/following count
4399
+ ...follow.target_feed,
4400
+ });
4401
+ if (source.created_by.id === connectedUserId &&
4402
+ currentState.own_follows) {
4403
+ const index = currentState.own_follows.findIndex((f) => f.source_feed.fid === follow.source_feed.fid);
4404
+ if (index >= 0) {
4405
+ newState.own_follows = [...currentState.own_follows];
4406
+ newState.own_follows[index] = follow;
4407
+ }
4408
+ }
4409
+ const index = currentState.followers?.findIndex((f) => f.source_feed.fid === follow.source_feed.fid) ?? -1;
4410
+ if (index >= 0) {
4411
+ newState.followers = [...newState.followers];
4412
+ newState.followers[index] = follow;
4413
+ }
4414
+ }
4415
+ return newState ?? currentState;
4416
+ });
4417
+ }
4418
+
4419
+ function handleCommentAdded(event) {
4420
+ const { comment } = event;
4421
+ const entityId = comment.parent_id ?? comment.object_id;
4422
+ this.state.next((currentState) => {
4423
+ const entityState = currentState.comments_by_entity_id[entityId];
4424
+ if (typeof entityState?.comments === 'undefined') {
4425
+ return currentState;
4426
+ }
4427
+ const newComments = entityState?.comments ? [...entityState.comments] : [];
4428
+ if (entityState.pagination?.sort === 'last') {
4429
+ newComments.unshift(comment);
4430
+ }
4431
+ else {
4432
+ // 'first' and other sort options
4433
+ newComments.push(comment);
4434
+ }
4435
+ return {
4436
+ ...currentState,
4437
+ comments_by_entity_id: {
4438
+ ...currentState.comments_by_entity_id,
4439
+ [entityId]: {
4440
+ ...currentState.comments_by_entity_id[entityId],
4441
+ comments: newComments,
4867
4442
  },
4868
- }));
4869
- const { next: newNextCursor, members } = await this.client.queryFeedMembers({
4870
- ...request,
4871
- sort,
4872
- feed_id: this.id,
4873
- feed_group_id: this.group,
4874
- next: currentNextCursor,
4443
+ },
4444
+ };
4445
+ });
4446
+ }
4447
+
4448
+ function handleCommentDeleted({ comment }) {
4449
+ const entityId = comment.parent_id ?? comment.object_id;
4450
+ this.state.next((currentState) => {
4451
+ const newCommentsByEntityId = {
4452
+ ...currentState.comments_by_entity_id,
4453
+ [entityId]: {
4454
+ ...currentState.comments_by_entity_id[entityId],
4455
+ },
4456
+ };
4457
+ const index = this.getCommentIndex(comment, currentState);
4458
+ if (newCommentsByEntityId?.[entityId]?.comments?.length && index !== -1) {
4459
+ newCommentsByEntityId[entityId].comments = [
4460
+ ...newCommentsByEntityId[entityId].comments,
4461
+ ];
4462
+ newCommentsByEntityId[entityId]?.comments?.splice(index, 1);
4463
+ }
4464
+ delete newCommentsByEntityId[comment.id];
4465
+ return {
4466
+ ...currentState,
4467
+ comments_by_entity_id: newCommentsByEntityId,
4468
+ };
4469
+ });
4470
+ }
4471
+
4472
+ function handleCommentUpdated(event) {
4473
+ const { comment } = event;
4474
+ const entityId = comment.parent_id ?? comment.object_id;
4475
+ this.state.next((currentState) => {
4476
+ const entityState = currentState.comments_by_entity_id[entityId];
4477
+ if (!entityState?.comments?.length)
4478
+ return currentState;
4479
+ const index = this.getCommentIndex(comment, currentState);
4480
+ if (index === -1)
4481
+ return currentState;
4482
+ const newComments = [...entityState.comments];
4483
+ newComments[index] = comment;
4484
+ return {
4485
+ ...currentState,
4486
+ comments_by_entity_id: {
4487
+ ...currentState.comments_by_entity_id,
4488
+ [entityId]: {
4489
+ ...currentState.comments_by_entity_id[entityId],
4490
+ comments: newComments,
4491
+ },
4492
+ },
4493
+ };
4494
+ });
4495
+ }
4496
+
4497
+ function handleCommentReaction(event) {
4498
+ const { comment, reaction } = event;
4499
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4500
+ this.state.next((currentState) => {
4501
+ const forId = comment.parent_id ?? comment.object_id;
4502
+ const entityState = currentState.comments_by_entity_id[forId];
4503
+ const commentIndex = this.getCommentIndex(comment, currentState);
4504
+ if (commentIndex === -1)
4505
+ return currentState;
4506
+ const newComments = entityState?.comments?.concat([]) ?? [];
4507
+ const commentCopy = { ...comment };
4508
+ delete commentCopy.own_reactions;
4509
+ const newComment = {
4510
+ ...newComments[commentIndex],
4511
+ ...commentCopy,
4512
+ // TODO: FIXME this should be handled by the backend
4513
+ latest_reactions: commentCopy.latest_reactions ?? [],
4514
+ reaction_groups: commentCopy.reaction_groups ?? {},
4515
+ };
4516
+ newComments[commentIndex] = newComment;
4517
+ if (reaction.user.id === connectedUser?.id) {
4518
+ if (event.type === 'feeds.comment.reaction.added') {
4519
+ newComment.own_reactions = newComment.own_reactions.concat(reaction) ?? [reaction];
4520
+ }
4521
+ else if (event.type === 'feeds.comment.reaction.deleted') {
4522
+ newComment.own_reactions = newComment.own_reactions.filter((r) => r.type !== reaction.type);
4523
+ }
4524
+ }
4525
+ return {
4526
+ ...currentState,
4527
+ comments_by_entity_id: {
4528
+ ...currentState.comments_by_entity_id,
4529
+ [forId]: {
4530
+ ...entityState,
4531
+ comments: newComments,
4532
+ },
4533
+ },
4534
+ };
4535
+ });
4536
+ }
4537
+
4538
+ function handleFeedMemberAdded(event) {
4539
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4540
+ this.state.next((currentState) => {
4541
+ let newState;
4542
+ if (typeof currentState.members !== 'undefined') {
4543
+ newState ?? (newState = {
4544
+ ...currentState,
4875
4545
  });
4876
- this.state.next((currentState) => ({
4546
+ newState.members = [event.member, ...currentState.members];
4547
+ }
4548
+ if (connectedUser?.id === event.member.user.id) {
4549
+ newState ?? (newState = {
4877
4550
  ...currentState,
4878
- members: currentState.members
4879
- ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
4880
- : members,
4881
- member_pagination: {
4882
- ...currentState.member_pagination,
4883
- next: newNextCursor,
4884
- // set sort if not defined yet
4885
- sort: currentState.member_pagination?.sort ?? request.sort,
4886
- },
4887
- }));
4551
+ });
4552
+ newState.own_membership = event.member;
4888
4553
  }
4889
- catch (e) {
4890
- error = e;
4554
+ return newState ?? currentState;
4555
+ });
4556
+ }
4557
+
4558
+ function handleFeedMemberUpdated(event) {
4559
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4560
+ this.state.next((currentState) => {
4561
+ const memberIndex = currentState.members?.findIndex((member) => member.user.id === event.member.user.id) ?? -1;
4562
+ let newState;
4563
+ if (memberIndex !== -1) {
4564
+ // if there's an index, there's a member to update
4565
+ const newMembers = [...currentState.members];
4566
+ newMembers[memberIndex] = event.member;
4567
+ newState ?? (newState = {
4568
+ ...currentState,
4569
+ });
4570
+ newState.members = newMembers;
4891
4571
  }
4892
- finally {
4893
- this.state.next((currentState) => ({
4572
+ if (connectedUser?.id === event.member.user.id) {
4573
+ newState ?? (newState = {
4894
4574
  ...currentState,
4895
- member_pagination: {
4896
- ...currentState.member_pagination,
4897
- loading_next_page: false,
4898
- },
4899
- }));
4575
+ });
4576
+ newState.own_membership = event.member;
4900
4577
  }
4901
- if (error) {
4902
- throw error;
4578
+ return newState ?? currentState;
4579
+ });
4580
+ }
4581
+
4582
+ function handleFeedMemberRemoved(event) {
4583
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4584
+ this.state.next((currentState) => {
4585
+ const newState = {
4586
+ ...currentState,
4587
+ members: currentState.members?.filter((member) => member.user.id !== event.user?.id),
4588
+ };
4589
+ if (connectedUser?.id === event.member_id) {
4590
+ delete newState.own_membership;
4903
4591
  }
4592
+ return newState;
4593
+ });
4594
+ }
4595
+
4596
+ const addActivitiesToState = (newActivities, activities, position) => {
4597
+ let result;
4598
+ if (activities === undefined) {
4599
+ activities = [];
4600
+ result = {
4601
+ changed: true,
4602
+ activities,
4603
+ };
4904
4604
  }
4905
- /**
4906
- * Method which queries followers of this feed (feeds which target this feed).
4907
- *
4908
- * _Note: Useful only for feeds with `groupId` of `user` value._
4909
- */
4910
- async queryFollowers(request) {
4911
- const filter = {
4912
- target_feed: this.fid,
4605
+ else {
4606
+ result = {
4607
+ changed: false,
4608
+ activities,
4913
4609
  };
4914
- const response = await this.client.queryFollows({
4915
- filter,
4916
- ...request,
4917
- });
4918
- return response;
4919
4610
  }
4920
- /**
4921
- * Method which queries following of this feed (target feeds of this feed).
4922
- *
4923
- * _Note: Useful only for feeds with `groupId` of `timeline` value._
4924
- */
4925
- async queryFollowing(request) {
4926
- const filter = {
4927
- source_feed: this.fid,
4928
- };
4929
- const response = await this.client.queryFollows({
4930
- filter,
4931
- ...request,
4932
- });
4933
- return response;
4611
+ const newActivitiesDeduplicated = [];
4612
+ newActivities.forEach((newActivityResponse) => {
4613
+ const index = activities.findIndex((a) => a.id === newActivityResponse.id);
4614
+ if (index === -1) {
4615
+ newActivitiesDeduplicated.push(newActivityResponse);
4616
+ }
4617
+ });
4618
+ if (newActivitiesDeduplicated.length > 0) {
4619
+ // TODO: since feed activities are not necessarily ordered by created_at (personalization) we don't order by created_at
4620
+ // Maybe we can add a flag to the JS client to support order by created_at
4621
+ const updatedActivities = [
4622
+ ...(position === 'start' ? newActivitiesDeduplicated : []),
4623
+ ...activities,
4624
+ ...(position === 'end' ? newActivitiesDeduplicated : []),
4625
+ ];
4626
+ result = { changed: true, activities: updatedActivities };
4627
+ }
4628
+ return result;
4629
+ };
4630
+ function handleActivityAdded(event) {
4631
+ const currentActivities = this.currentState.activities;
4632
+ const result = addActivitiesToState([event.activity], currentActivities, 'start');
4633
+ if (result.changed) {
4634
+ this.client.hydratePollCache([event.activity]);
4635
+ this.state.partialNext({ activities: result.activities });
4636
+ }
4637
+ }
4638
+
4639
+ const removeActivityFromState = (activityResponse, activities) => {
4640
+ const index = activities.findIndex((a) => a.id === activityResponse.id);
4641
+ if (index !== -1) {
4642
+ const newActivities = [...activities];
4643
+ newActivities.splice(index, 1);
4644
+ return { changed: true, activities: newActivities };
4645
+ }
4646
+ else {
4647
+ return { changed: false, activities };
4648
+ }
4649
+ };
4650
+ function handleActivityDeleted(event) {
4651
+ const currentActivities = this.currentState.activities;
4652
+ if (currentActivities) {
4653
+ const result = removeActivityFromState(event.activity, currentActivities);
4654
+ if (result.changed) {
4655
+ this.state.partialNext({ activities: result.activities });
4656
+ }
4657
+ }
4658
+ }
4659
+
4660
+ function handleActivityRemovedFromFeed(event) {
4661
+ const currentActivities = this.currentState.activities;
4662
+ if (currentActivities) {
4663
+ const result = removeActivityFromState(event.activity, currentActivities);
4664
+ if (result.changed) {
4665
+ this.state.partialNext({ activities: result.activities });
4666
+ }
4667
+ }
4668
+ }
4669
+
4670
+ const updateActivityInState = (updatedActivityResponse, activities, replaceCompletely = false) => {
4671
+ const index = activities.findIndex((a) => a.id === updatedActivityResponse.id);
4672
+ if (index !== -1) {
4673
+ const newActivities = [...activities];
4674
+ const activity = activities[index];
4675
+ if (replaceCompletely) {
4676
+ newActivities[index] = updatedActivityResponse;
4677
+ }
4678
+ else {
4679
+ newActivities[index] = {
4680
+ ...updatedActivityResponse,
4681
+ own_reactions: activity.own_reactions,
4682
+ own_bookmarks: activity.own_bookmarks,
4683
+ latest_reactions: activity.latest_reactions,
4684
+ reaction_groups: activity.reaction_groups,
4685
+ };
4686
+ }
4687
+ return { changed: true, activities: newActivities };
4688
+ }
4689
+ else {
4690
+ return { changed: false, activities };
4691
+ }
4692
+ };
4693
+ function handleActivityUpdated(event) {
4694
+ const currentActivities = this.currentState.activities;
4695
+ if (currentActivities) {
4696
+ const result = updateActivityInState(event.activity, currentActivities);
4697
+ if (result.changed) {
4698
+ this.client.hydratePollCache([event.activity]);
4699
+ this.state.partialNext({ activities: result.activities });
4700
+ }
4701
+ }
4702
+ }
4703
+
4704
+ const addReactionToActivity = (event, activity, isCurrentUser) => {
4705
+ // Update own_reactions if the reaction is from the current user
4706
+ const ownReactions = [...(activity.own_reactions || [])];
4707
+ if (isCurrentUser) {
4708
+ ownReactions.push(event.reaction);
4709
+ }
4710
+ return {
4711
+ ...activity,
4712
+ own_reactions: ownReactions,
4713
+ latest_reactions: event.activity.latest_reactions,
4714
+ reaction_groups: event.activity.reaction_groups,
4715
+ changed: true,
4716
+ };
4717
+ };
4718
+ const addReactionToActivities = (event, activities, isCurrentUser) => {
4719
+ if (!activities) {
4720
+ return { changed: false, activities: [] };
4934
4721
  }
4935
- async follow(feedOrFid, options) {
4936
- const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
4937
- const response = await this.client.follow({
4938
- ...options,
4939
- source: this.fid,
4940
- target: fid,
4941
- });
4942
- return response;
4722
+ const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
4723
+ if (activityIndex === -1) {
4724
+ return { changed: false, activities };
4943
4725
  }
4944
- async unfollow(feedOrFid) {
4945
- const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
4946
- const response = await this.client.unfollow({
4947
- source: this.fid,
4948
- target: fid,
4949
- });
4950
- return response;
4726
+ const activity = activities[activityIndex];
4727
+ const updatedActivity = addReactionToActivity(event, activity, isCurrentUser);
4728
+ return updateActivityInState(updatedActivity, activities, true);
4729
+ };
4730
+ function handleActivityReactionAdded(event) {
4731
+ const currentActivities = this.currentState.activities;
4732
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4733
+ const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4734
+ const result = addReactionToActivities(event, currentActivities, isCurrentUser);
4735
+ if (result.changed) {
4736
+ this.state.partialNext({ activities: result.activities });
4951
4737
  }
4952
- async getNextPage() {
4953
- const currentState = this.currentState;
4954
- return await this.getOrCreate({
4955
- member_pagination: {
4956
- limit: 0,
4957
- },
4958
- followers_pagination: {
4959
- limit: 0,
4960
- },
4961
- following_pagination: {
4962
- limit: 0,
4963
- },
4964
- next: currentState.next,
4965
- limit: currentState.last_get_or_create_request_config?.limit ?? 20,
4966
- });
4738
+ }
4739
+
4740
+ const removeReactionFromActivity = (event, activity, isCurrentUser) => {
4741
+ // Update own_reactions if the reaction is from the current user
4742
+ const ownReactions = isCurrentUser
4743
+ ? (activity.own_reactions || []).filter((r) => !(r.type === event.reaction.type &&
4744
+ r.user.id === event.reaction.user.id))
4745
+ : activity.own_reactions;
4746
+ return {
4747
+ ...activity,
4748
+ own_reactions: ownReactions,
4749
+ latest_reactions: event.activity.latest_reactions,
4750
+ reaction_groups: event.activity.reaction_groups,
4751
+ changed: true,
4752
+ };
4753
+ };
4754
+ const removeReactionFromActivities = (event, activities, isCurrentUser) => {
4755
+ if (!activities) {
4756
+ return { changed: false, activities: [] };
4967
4757
  }
4968
- addActivity(request) {
4969
- return this.feedsApi.addActivity({
4970
- ...request,
4971
- fids: [this.fid],
4972
- });
4758
+ const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
4759
+ if (activityIndex === -1) {
4760
+ return { changed: false, activities };
4973
4761
  }
4974
- handleWSEvent(event) {
4975
- const eventHandler = this.eventHandlers[event.type];
4976
- // no need to run noop function
4977
- if (eventHandler !== Feed.noop) {
4978
- // @ts-expect-error intersection of handler arguments results to never
4979
- eventHandler?.(event);
4980
- }
4981
- if (typeof eventHandler === 'undefined') {
4982
- console.warn(`Received unknown event type: ${event.type}`, event);
4983
- }
4984
- this.eventDispatcher.dispatch(event);
4762
+ const activity = activities[activityIndex];
4763
+ const updatedActivity = removeReactionFromActivity(event, activity, isCurrentUser);
4764
+ return updateActivityInState(updatedActivity, activities, true);
4765
+ };
4766
+ function handleActivityReactionDeleted(event) {
4767
+ const currentActivities = this.currentState.activities;
4768
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4769
+ const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4770
+ const result = removeReactionFromActivities(event, currentActivities, isCurrentUser);
4771
+ if (result.changed) {
4772
+ this.state.partialNext({ activities: result.activities });
4985
4773
  }
4986
4774
  }
4987
- Feed.noop = () => { };
4988
4775
 
4989
- class ModerationApi {
4990
- constructor(apiClient) {
4991
- this.apiClient = apiClient;
4776
+ const addBookmarkToActivity = (event, activity, isCurrentUser) => {
4777
+ // Update own_bookmarks if the bookmark is from the current user
4778
+ const ownBookmarks = [...(activity.own_bookmarks || [])];
4779
+ if (isCurrentUser) {
4780
+ ownBookmarks.push(event.bookmark);
4992
4781
  }
4993
- async ban(request) {
4994
- const body = {
4995
- target_user_id: request?.target_user_id,
4996
- banned_by_id: request?.banned_by_id,
4997
- channel_cid: request?.channel_cid,
4998
- delete_messages: request?.delete_messages,
4999
- ip_ban: request?.ip_ban,
5000
- reason: request?.reason,
5001
- shadow: request?.shadow,
5002
- timeout: request?.timeout,
5003
- banned_by: request?.banned_by,
5004
- };
5005
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/ban', undefined, undefined, body, 'application/json');
5006
- decoders.BanResponse?.(response.body);
5007
- return { ...response.body, metadata: response.metadata };
4782
+ return {
4783
+ ...activity,
4784
+ own_bookmarks: ownBookmarks,
4785
+ changed: true,
4786
+ };
4787
+ };
4788
+ const addBookmarkToActivities = (event, activities, isCurrentUser) => {
4789
+ if (!activities) {
4790
+ return { changed: false, activities: [] };
5008
4791
  }
5009
- async upsertConfig(request) {
5010
- const body = {
5011
- key: request?.key,
5012
- async: request?.async,
5013
- team: request?.team,
5014
- ai_image_config: request?.ai_image_config,
5015
- ai_text_config: request?.ai_text_config,
5016
- ai_video_config: request?.ai_video_config,
5017
- automod_platform_circumvention_config: request?.automod_platform_circumvention_config,
5018
- automod_semantic_filters_config: request?.automod_semantic_filters_config,
5019
- automod_toxicity_config: request?.automod_toxicity_config,
5020
- aws_rekognition_config: request?.aws_rekognition_config,
5021
- block_list_config: request?.block_list_config,
5022
- bodyguard_config: request?.bodyguard_config,
5023
- google_vision_config: request?.google_vision_config,
5024
- rule_builder_config: request?.rule_builder_config,
5025
- velocity_filter_config: request?.velocity_filter_config,
5026
- video_call_rule_config: request?.video_call_rule_config,
5027
- };
5028
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/config', undefined, undefined, body, 'application/json');
5029
- decoders.UpsertConfigResponse?.(response.body);
5030
- return { ...response.body, metadata: response.metadata };
4792
+ const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4793
+ if (activityIndex === -1) {
4794
+ return { changed: false, activities };
5031
4795
  }
5032
- async deleteConfig(request) {
5033
- const queryParams = {
5034
- team: request?.team,
5035
- };
5036
- const pathParams = {
5037
- key: request?.key,
5038
- };
5039
- const response = await this.apiClient.sendRequest('DELETE', '/api/v2/moderation/config/{key}', pathParams, queryParams);
5040
- decoders.DeleteModerationConfigResponse?.(response.body);
5041
- return { ...response.body, metadata: response.metadata };
4796
+ const activity = activities[activityIndex];
4797
+ const updatedActivity = addBookmarkToActivity(event, activity, isCurrentUser);
4798
+ return updateActivityInState(updatedActivity, activities, true);
4799
+ };
4800
+ function handleBookmarkAdded(event) {
4801
+ const currentActivities = this.currentState.activities;
4802
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4803
+ const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4804
+ const result = addBookmarkToActivities(event, currentActivities, isCurrentUser);
4805
+ if (result.changed) {
4806
+ this.state.partialNext({ activities: result.activities });
5042
4807
  }
5043
- async getConfig(request) {
5044
- const queryParams = {
5045
- team: request?.team,
5046
- };
5047
- const pathParams = {
5048
- key: request?.key,
5049
- };
5050
- const response = await this.apiClient.sendRequest('GET', '/api/v2/moderation/config/{key}', pathParams, queryParams);
5051
- decoders.GetConfigResponse?.(response.body);
5052
- return { ...response.body, metadata: response.metadata };
4808
+ }
4809
+
4810
+ // Helper function to check if two bookmarks are the same
4811
+ // A bookmark is identified by activity_id + folder_id + user_id
4812
+ const isSameBookmark = (bookmark1, bookmark2) => {
4813
+ return (bookmark1.user.id === bookmark2.user.id &&
4814
+ bookmark1.activity.id === bookmark2.activity.id &&
4815
+ bookmark1.folder?.id === bookmark2.folder?.id);
4816
+ };
4817
+ const removeBookmarkFromActivities = (event, activities, isCurrentUser) => {
4818
+ if (!activities) {
4819
+ return { changed: false, activities: [] };
5053
4820
  }
5054
- async queryModerationConfigs(request) {
5055
- const body = {
5056
- limit: request?.limit,
5057
- next: request?.next,
5058
- prev: request?.prev,
5059
- sort: request?.sort,
5060
- filter: request?.filter,
5061
- };
5062
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/configs', undefined, undefined, body, 'application/json');
5063
- decoders.QueryModerationConfigsResponse?.(response.body);
5064
- return { ...response.body, metadata: response.metadata };
4821
+ const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4822
+ if (activityIndex === -1) {
4823
+ return { changed: false, activities };
5065
4824
  }
5066
- async flag(request) {
5067
- const body = {
5068
- entity_id: request?.entity_id,
5069
- entity_type: request?.entity_type,
5070
- entity_creator_id: request?.entity_creator_id,
5071
- reason: request?.reason,
5072
- custom: request?.custom,
5073
- moderation_payload: request?.moderation_payload,
5074
- };
5075
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/flag', undefined, undefined, body, 'application/json');
5076
- decoders.FlagResponse?.(response.body);
5077
- return { ...response.body, metadata: response.metadata };
4825
+ const activity = activities[activityIndex];
4826
+ const updatedActivity = removeBookmarkFromActivity(event, activity, isCurrentUser);
4827
+ return updateActivityInState(updatedActivity, activities, true);
4828
+ };
4829
+ const removeBookmarkFromActivity = (event, activity, isCurrentUser) => {
4830
+ // Update own_bookmarks if the bookmark is from the current user
4831
+ const ownBookmarks = isCurrentUser
4832
+ ? (activity.own_bookmarks || []).filter((bookmark) => !isSameBookmark(bookmark, event.bookmark))
4833
+ : activity.own_bookmarks;
4834
+ return {
4835
+ ...activity,
4836
+ own_bookmarks: ownBookmarks,
4837
+ changed: true,
4838
+ };
4839
+ };
4840
+ function handleBookmarkDeleted(event) {
4841
+ const currentActivities = this.currentState.activities;
4842
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4843
+ const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4844
+ const result = removeBookmarkFromActivities(event, currentActivities, isCurrentUser);
4845
+ if (result.changed) {
4846
+ this.state.partialNext({ activities: result.activities });
5078
4847
  }
5079
- async mute(request) {
5080
- const body = {
5081
- target_ids: request?.target_ids,
5082
- timeout: request?.timeout,
5083
- };
5084
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/mute', undefined, undefined, body, 'application/json');
5085
- decoders.MuteResponse?.(response.body);
5086
- return { ...response.body, metadata: response.metadata };
4848
+ }
4849
+
4850
+ const updateBookmarkInActivity = (event, activity, isCurrentUser) => {
4851
+ // Update own_bookmarks if the bookmark is from the current user
4852
+ let ownBookmarks = activity.own_bookmarks || [];
4853
+ if (isCurrentUser) {
4854
+ const bookmarkIndex = ownBookmarks.findIndex((bookmark) => isSameBookmark(bookmark, event.bookmark));
4855
+ if (bookmarkIndex !== -1) {
4856
+ ownBookmarks = [...ownBookmarks];
4857
+ ownBookmarks[bookmarkIndex] = event.bookmark;
4858
+ }
4859
+ }
4860
+ return {
4861
+ ...activity,
4862
+ own_bookmarks: ownBookmarks,
4863
+ changed: true,
4864
+ };
4865
+ };
4866
+ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4867
+ if (!activities) {
4868
+ return { changed: false, activities: [] };
5087
4869
  }
5088
- async queryReviewQueue(request) {
5089
- const body = {
5090
- limit: request?.limit,
5091
- lock_count: request?.lock_count,
5092
- lock_duration: request?.lock_duration,
5093
- lock_items: request?.lock_items,
5094
- next: request?.next,
5095
- prev: request?.prev,
5096
- stats_only: request?.stats_only,
5097
- sort: request?.sort,
5098
- filter: request?.filter,
5099
- };
5100
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/review_queue', undefined, undefined, body, 'application/json');
5101
- decoders.QueryReviewQueueResponse?.(response.body);
5102
- return { ...response.body, metadata: response.metadata };
4870
+ const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4871
+ if (activityIndex === -1) {
4872
+ return { changed: false, activities };
5103
4873
  }
5104
- async submitAction(request) {
5105
- const body = {
5106
- action_type: request?.action_type,
5107
- item_id: request?.item_id,
5108
- ban: request?.ban,
5109
- custom: request?.custom,
5110
- delete_activity: request?.delete_activity,
5111
- delete_message: request?.delete_message,
5112
- delete_reaction: request?.delete_reaction,
5113
- delete_user: request?.delete_user,
5114
- mark_reviewed: request?.mark_reviewed,
5115
- unban: request?.unban,
5116
- };
5117
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/submit_action', undefined, undefined, body, 'application/json');
5118
- decoders.SubmitActionResponse?.(response.body);
5119
- return { ...response.body, metadata: response.metadata };
4874
+ const activity = activities[activityIndex];
4875
+ const updatedActivity = updateBookmarkInActivity(event, activity, isCurrentUser);
4876
+ return updateActivityInState(updatedActivity, activities, true);
4877
+ };
4878
+ function handleBookmarkUpdated(event) {
4879
+ const currentActivities = this.currentState.activities;
4880
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4881
+ const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4882
+ const result = updateBookmarkInActivities(event, currentActivities, isCurrentUser);
4883
+ if (result.changed) {
4884
+ this.state.partialNext({ activities: result.activities });
5120
4885
  }
5121
4886
  }
5122
4887
 
5123
- class ModerationClient extends ModerationApi {
4888
+ function handleFeedUpdated(event) {
4889
+ this.state.partialNext({ ...event.feed });
5124
4890
  }
5125
4891
 
5126
- const isPollUpdatedEvent = (e) => e.type === 'feeds.poll.updated';
5127
- const isPollClosedEventEvent = (e) => e.type === 'feeds.poll.closed';
5128
- const isPollVoteCastedEvent = (e) => e.type === 'feeds.poll.vote_casted';
5129
- const isPollVoteChangedEvent = (e) => e.type === 'feeds.poll.vote_changed';
5130
- const isPollVoteRemovedEvent = (e) => e.type === 'feeds.poll.vote_removed';
5131
- const isVoteAnswer = (vote) => !!vote?.answer_text;
5132
- class StreamPoll {
5133
- constructor({ client, poll }) {
5134
- this.getInitialStateFromPollResponse = (poll) => {
5135
- const { own_votes, id, ...pollResponseForState } = poll;
5136
- const { ownAnswer, ownVotes } = own_votes?.reduce((acc, voteOrAnswer) => {
5137
- if (isVoteAnswer(voteOrAnswer)) {
5138
- acc.ownAnswer = voteOrAnswer;
5139
- }
5140
- else {
5141
- acc.ownVotes.push(voteOrAnswer);
5142
- }
5143
- return acc;
5144
- }, { ownVotes: [] }) ?? { ownVotes: [] };
5145
- return {
5146
- ...pollResponseForState,
5147
- last_activity_at: new Date(),
5148
- max_voted_option_ids: getMaxVotedOptionIds(pollResponseForState.vote_counts_by_option),
5149
- own_answer: ownAnswer,
5150
- own_votes_by_option_id: getOwnVotesByOptionId(ownVotes),
5151
- };
5152
- };
5153
- this.reinitializeState = (poll) => {
5154
- this.state.partialNext(this.getInitialStateFromPollResponse(poll));
5155
- };
5156
- this.handlePollUpdated = (event) => {
5157
- if (event.poll?.id && event.poll.id !== this.id)
5158
- return;
5159
- if (!isPollUpdatedEvent(event))
5160
- return;
5161
- const { id, ...pollData } = event.poll;
5162
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5163
- this.state.partialNext({
5164
- ...pollData,
5165
- last_activity_at: new Date(event.created_at),
5166
- });
5167
- };
5168
- this.handlePollClosed = (event) => {
5169
- if (event.poll?.id && event.poll.id !== this.id)
5170
- return;
5171
- if (!isPollClosedEventEvent(event))
5172
- return;
5173
- this.state.partialNext({
5174
- is_closed: true,
5175
- last_activity_at: new Date(event.created_at),
5176
- });
5177
- };
5178
- this.handleVoteCasted = (event) => {
5179
- if (event.poll?.id && event.poll.id !== this.id)
5180
- return;
5181
- if (!isPollVoteCastedEvent(event))
5182
- return;
5183
- const currentState = this.data;
5184
- const isOwnVote = event.poll_vote.user_id ===
5185
- this.client.state.getLatestValue().connected_user?.id;
5186
- let latestAnswers = [...currentState.latest_answers];
5187
- let ownAnswer = currentState.own_answer;
5188
- const ownVotesByOptionId = currentState.own_votes_by_option_id;
5189
- let maxVotedOptionIds = currentState.max_voted_option_ids;
5190
- if (isOwnVote) {
5191
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5192
- if (isVoteAnswer(event.poll_vote)) {
5193
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5194
- ownAnswer = event.poll_vote;
5195
- }
5196
- else if (event.poll_vote.option_id) {
5197
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5198
- ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
5199
- }
5200
- }
5201
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5202
- if (isVoteAnswer(event.poll_vote)) {
5203
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5204
- latestAnswers = [event.poll_vote, ...latestAnswers];
5205
- }
5206
- else {
5207
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5208
- }
5209
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5210
- this.state.partialNext({
5211
- answers_count,
5212
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5213
- latest_votes_by_option,
5214
- vote_count,
5215
- vote_counts_by_option,
5216
- latest_answers: latestAnswers,
5217
- last_activity_at: new Date(event.created_at),
5218
- own_answer: ownAnswer,
5219
- own_votes_by_option_id: ownVotesByOptionId,
5220
- max_voted_option_ids: maxVotedOptionIds,
5221
- });
4892
+ function handleNotificationFeedUpdated(event) {
4893
+ console.info('notification feed updated', event);
4894
+ // TODO: handle notification feed updates
4895
+ }
4896
+
4897
+ class Feed extends FeedApi {
4898
+ constructor(client, groupId, id, data, watch = false) {
4899
+ super(client, groupId, id);
4900
+ this.stateUpdateQueue = new Set();
4901
+ this.eventHandlers = {
4902
+ 'feeds.activity.added': handleActivityAdded.bind(this),
4903
+ 'feeds.activity.deleted': handleActivityDeleted.bind(this),
4904
+ 'feeds.activity.reaction.added': handleActivityReactionAdded.bind(this),
4905
+ 'feeds.activity.reaction.deleted': handleActivityReactionDeleted.bind(this),
4906
+ 'feeds.activity.reaction.updated': Feed.noop,
4907
+ 'feeds.activity.removed_from_feed': handleActivityRemovedFromFeed.bind(this),
4908
+ 'feeds.activity.updated': handleActivityUpdated.bind(this),
4909
+ 'feeds.bookmark.added': handleBookmarkAdded.bind(this),
4910
+ 'feeds.bookmark.deleted': handleBookmarkDeleted.bind(this),
4911
+ 'feeds.bookmark.updated': handleBookmarkUpdated.bind(this),
4912
+ 'feeds.bookmark_folder.deleted': Feed.noop,
4913
+ 'feeds.bookmark_folder.updated': Feed.noop,
4914
+ 'feeds.comment.added': handleCommentAdded.bind(this),
4915
+ 'feeds.comment.deleted': handleCommentDeleted.bind(this),
4916
+ 'feeds.comment.updated': handleCommentUpdated.bind(this),
4917
+ 'feeds.feed.created': Feed.noop,
4918
+ 'feeds.feed.deleted': Feed.noop,
4919
+ 'feeds.feed.updated': handleFeedUpdated.bind(this),
4920
+ 'feeds.feed_group.changed': Feed.noop,
4921
+ 'feeds.feed_group.deleted': Feed.noop,
4922
+ 'feeds.follow.created': handleFollowCreated.bind(this),
4923
+ 'feeds.follow.deleted': handleFollowDeleted.bind(this),
4924
+ 'feeds.follow.updated': handleFollowUpdated.bind(this),
4925
+ 'feeds.comment.reaction.added': handleCommentReaction.bind(this),
4926
+ 'feeds.comment.reaction.deleted': handleCommentReaction.bind(this),
4927
+ 'feeds.comment.reaction.updated': Feed.noop,
4928
+ 'feeds.feed_member.added': handleFeedMemberAdded.bind(this),
4929
+ 'feeds.feed_member.removed': handleFeedMemberRemoved.bind(this),
4930
+ 'feeds.feed_member.updated': handleFeedMemberUpdated.bind(this),
4931
+ 'feeds.notification_feed.updated': handleNotificationFeedUpdated.bind(this),
4932
+ // the poll events should be removed from here
4933
+ 'feeds.poll.closed': Feed.noop,
4934
+ 'feeds.poll.deleted': Feed.noop,
4935
+ 'feeds.poll.updated': Feed.noop,
4936
+ 'feeds.poll.vote_casted': Feed.noop,
4937
+ 'feeds.poll.vote_changed': Feed.noop,
4938
+ 'feeds.poll.vote_removed': Feed.noop,
4939
+ 'feeds.activity.pinned': Feed.noop,
4940
+ 'feeds.activity.unpinned': Feed.noop,
4941
+ 'feeds.activity.marked': Feed.noop,
4942
+ 'moderation.custom_action': Feed.noop,
4943
+ 'moderation.flagged': Feed.noop,
4944
+ 'moderation.mark_reviewed': Feed.noop,
4945
+ 'health.check': Feed.noop,
4946
+ 'app.updated': Feed.noop,
4947
+ 'user.banned': Feed.noop,
4948
+ 'user.deactivated': Feed.noop,
4949
+ 'user.muted': Feed.noop,
4950
+ 'user.reactivated': Feed.noop,
4951
+ 'user.updated': Feed.noop,
5222
4952
  };
5223
- this.handleVoteChanged = (event) => {
5224
- // this event is triggered only when event.poll.enforce_unique_vote === true
5225
- if (event.poll?.id && event.poll.id !== this.id)
5226
- return;
5227
- if (!isPollVoteChangedEvent(event))
5228
- return;
5229
- const currentState = this.data;
5230
- const isOwnVote = event.poll_vote.user_id ===
5231
- this.client.state.getLatestValue().connected_user?.id;
5232
- let latestAnswers = [...currentState.latest_answers];
5233
- let ownAnswer = currentState.own_answer;
5234
- let ownVotesByOptionId = currentState.own_votes_by_option_id;
5235
- let maxVotedOptionIds = currentState.max_voted_option_ids;
5236
- if (isOwnVote) {
5237
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5238
- if (isVoteAnswer(event.poll_vote)) {
5239
- latestAnswers = [
5240
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5241
- event.poll_vote,
5242
- ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id),
5243
- ];
5244
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5245
- ownAnswer = event.poll_vote;
5246
- }
5247
- else if (event.poll_vote.option_id) {
5248
- if (event.poll.enforce_unique_vote) {
5249
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5250
- ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
5251
- }
5252
- else {
5253
- ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce((acc, [optionId, vote]) => {
5254
- if (optionId !== event.poll_vote.option_id &&
5255
- vote.id === event.poll_vote.id) {
5256
- return acc;
5257
- }
5258
- acc[optionId] = vote;
5259
- return acc;
5260
- }, {});
5261
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5262
- ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
5263
- }
5264
- if (ownAnswer?.id === event.poll_vote.id) {
5265
- ownAnswer = undefined;
5266
- }
5267
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4953
+ this.eventDispatcher = new EventDispatcher();
4954
+ this.on = this.eventDispatcher.on;
4955
+ this.off = this.eventDispatcher.off;
4956
+ this.state = new StateStore({
4957
+ fid: `${groupId}:${id}`,
4958
+ group_id: groupId,
4959
+ id,
4960
+ ...(data ?? {}),
4961
+ is_loading: false,
4962
+ is_loading_activities: false,
4963
+ comments_by_entity_id: {},
4964
+ watch,
4965
+ });
4966
+ this.client = client;
4967
+ }
4968
+ get fid() {
4969
+ return `${this.group}:${this.id}`;
4970
+ }
4971
+ get currentState() {
4972
+ return this.state.getLatestValue();
4973
+ }
4974
+ async synchronize() {
4975
+ const { last_get_or_create_request_config } = this.state.getLatestValue();
4976
+ if (last_get_or_create_request_config?.watch) {
4977
+ await this.getOrCreate(last_get_or_create_request_config);
4978
+ }
4979
+ }
4980
+ async getOrCreate(request) {
4981
+ if (this.currentState.is_loading_activities) {
4982
+ throw new Error('Only one getOrCreate call is allowed at a time');
4983
+ }
4984
+ this.state.partialNext({
4985
+ is_loading: !request?.next,
4986
+ is_loading_activities: true,
4987
+ });
4988
+ // TODO: pull comments/comment_pagination from activities and comment_sort from request
4989
+ // and pre-populate comments_by_entity_id (once comment_sort and comment_limit are supported)
4990
+ try {
4991
+ const response = await super.getOrCreate(request);
4992
+ if (request?.next) {
4993
+ const { activities: currentActivities = [] } = this.currentState;
4994
+ const result = addActivitiesToState(response.activities, currentActivities, 'end');
4995
+ if (result.changed) {
4996
+ this.state.partialNext({
4997
+ activities: result.activities,
4998
+ next: response.next,
4999
+ prev: response.prev,
5000
+ });
5268
5001
  }
5269
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5270
- }
5271
- else if (isVoteAnswer(event.poll_vote)) {
5272
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5273
- latestAnswers = [event.poll_vote, ...latestAnswers];
5274
5002
  }
5275
5003
  else {
5276
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5004
+ // Empty queue when reinitializing the state
5005
+ this.stateUpdateQueue.clear();
5006
+ const responseCopy = {
5007
+ ...response,
5008
+ ...response.feed,
5009
+ };
5010
+ delete responseCopy.feed;
5011
+ delete responseCopy.metadata;
5012
+ delete responseCopy.duration;
5013
+ // TODO: lazy-load comments from activities when comment_sort and comment_pagination are supported
5014
+ this.state.next((currentState) => {
5015
+ const nextState = {
5016
+ ...currentState,
5017
+ ...responseCopy,
5018
+ };
5019
+ if (!request?.followers_pagination?.limit) {
5020
+ delete nextState.followers;
5021
+ }
5022
+ if (!request?.following_pagination?.limit) {
5023
+ delete nextState.following;
5024
+ }
5025
+ if (response.members.length === 0 && response.feed.member_count > 0) {
5026
+ delete nextState.members;
5027
+ }
5028
+ nextState.last_get_or_create_request_config = request;
5029
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
5030
+ return nextState;
5031
+ });
5277
5032
  }
5278
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5033
+ this.client.hydratePollCache(response.activities);
5034
+ return response;
5035
+ }
5036
+ finally {
5279
5037
  this.state.partialNext({
5280
- answers_count,
5281
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5282
- latest_votes_by_option,
5283
- vote_count,
5284
- vote_counts_by_option,
5285
- latest_answers: latestAnswers,
5286
- last_activity_at: new Date(event.created_at),
5287
- own_answer: ownAnswer,
5288
- own_votes_by_option_id: ownVotesByOptionId,
5289
- max_voted_option_ids: maxVotedOptionIds,
5038
+ is_loading: false,
5039
+ is_loading_activities: false,
5040
+ });
5041
+ }
5042
+ }
5043
+ /**
5044
+ * @internal
5045
+ */
5046
+ handleWatchStopped() {
5047
+ this.state.partialNext({
5048
+ watch: false,
5049
+ });
5050
+ }
5051
+ /**
5052
+ * @internal
5053
+ */
5054
+ handleWatchStarted() {
5055
+ this.state.partialNext({
5056
+ watch: true,
5057
+ });
5058
+ }
5059
+ /**
5060
+ * Returns index of the provided comment object.
5061
+ */
5062
+ getCommentIndex(comment, state) {
5063
+ const { comments_by_entity_id = {} } = state ?? this.currentState;
5064
+ const currentComments = comments_by_entity_id[comment.parent_id ?? comment.object_id]?.comments;
5065
+ if (!currentComments?.length) {
5066
+ return -1;
5067
+ }
5068
+ // @ts-expect-error this will just fail if the comment is not object from state
5069
+ let commentIndex = currentComments.indexOf(comment);
5070
+ // fast lookup failed, try slower approach
5071
+ if (commentIndex === -1) {
5072
+ commentIndex = currentComments.findIndex((comment_) => comment_.id === comment.id);
5073
+ }
5074
+ return commentIndex;
5075
+ }
5076
+ /**
5077
+ * Load child comments of entity (activity or comment) into the state, if the target entity is comment,
5078
+ * `entityParentId` should be provided (`CommentResponse.parent_id ?? CommentResponse.object_id`).
5079
+ */
5080
+ loadCommentsIntoState(data) {
5081
+ // add initial (top level) object for processing
5082
+ const traverseArray = [
5083
+ {
5084
+ entityId: data.entityId,
5085
+ entityParentId: data.entityParentId,
5086
+ comments: data.comments,
5087
+ next: data.next,
5088
+ },
5089
+ ];
5090
+ this.state.next((currentState) => {
5091
+ const newCommentsByEntityId = {
5092
+ ...currentState.comments_by_entity_id,
5093
+ };
5094
+ while (traverseArray.length) {
5095
+ const item = traverseArray.pop();
5096
+ const entityId = item.entityId;
5097
+ // go over entity comments and generate new objects
5098
+ // for further processing if there are any replies
5099
+ item.comments.forEach((comment) => {
5100
+ if (!comment.replies?.length)
5101
+ return;
5102
+ traverseArray.push({
5103
+ entityId: comment.id,
5104
+ entityParentId: entityId,
5105
+ comments: comment.replies,
5106
+ next: comment.meta?.next_cursor,
5107
+ });
5108
+ });
5109
+ // omit replies & meta from the comments (transform ThreadedCommentResponse to CommentResponse)
5110
+ // this is somehow faster than copying the whole
5111
+ // object and deleting the desired properties
5112
+ const newComments = item.comments.map(({ replies: _r, meta: _m, ...restOfTheCommentResponse }) => restOfTheCommentResponse);
5113
+ const existingComments = newCommentsByEntityId[entityId]?.comments;
5114
+ newCommentsByEntityId[entityId] = {
5115
+ ...newCommentsByEntityId[entityId],
5116
+ entity_parent_id: item.entityParentId,
5117
+ pagination: {
5118
+ ...newCommentsByEntityId[entityId]?.pagination,
5119
+ next: item.next,
5120
+ sort: data.sort,
5121
+ },
5122
+ comments: existingComments
5123
+ ? uniqueArrayMerge(existingComments, newComments, (comment) => comment.id)
5124
+ : newComments,
5125
+ };
5126
+ }
5127
+ return {
5128
+ ...currentState,
5129
+ comments_by_entity_id: newCommentsByEntityId,
5130
+ };
5131
+ });
5132
+ }
5133
+ async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
5134
+ let error;
5135
+ try {
5136
+ this.state.next((currentState) => ({
5137
+ ...currentState,
5138
+ comments_by_entity_id: {
5139
+ ...currentState.comments_by_entity_id,
5140
+ [entityId]: {
5141
+ ...currentState.comments_by_entity_id[entityId],
5142
+ pagination: {
5143
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
5144
+ loading_next_page: true,
5145
+ },
5146
+ },
5147
+ },
5148
+ }));
5149
+ const { next, comments } = await base();
5150
+ this.loadCommentsIntoState({
5151
+ entityId,
5152
+ comments,
5153
+ entityParentId,
5154
+ next,
5155
+ sort,
5156
+ });
5157
+ }
5158
+ catch (e) {
5159
+ error = e;
5160
+ }
5161
+ finally {
5162
+ this.state.next((currentState) => ({
5163
+ ...currentState,
5164
+ comments_by_entity_id: {
5165
+ ...currentState.comments_by_entity_id,
5166
+ [entityId]: {
5167
+ ...currentState.comments_by_entity_id[entityId],
5168
+ pagination: {
5169
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
5170
+ loading_next_page: false,
5171
+ },
5172
+ },
5173
+ },
5174
+ }));
5175
+ }
5176
+ if (error) {
5177
+ throw error;
5178
+ }
5179
+ }
5180
+ async loadNextPageActivityComments(activity, request) {
5181
+ const currentEntityState = this.currentState.comments_by_entity_id[activity.id];
5182
+ const currentPagination = currentEntityState?.pagination;
5183
+ const currentNextCursor = currentPagination?.next;
5184
+ const currentSort = currentPagination?.sort;
5185
+ const isLoading = currentPagination?.loading_next_page;
5186
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
5187
+ if (isLoading ||
5188
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
5189
+ return;
5190
+ }
5191
+ await this.loadNextPageComments({
5192
+ entityId: activity.id,
5193
+ base: () => this.client.getComments({
5194
+ ...request,
5195
+ sort,
5196
+ object_id: activity.id,
5197
+ object_type: 'activity',
5198
+ next: currentNextCursor,
5199
+ }),
5200
+ sort,
5201
+ });
5202
+ }
5203
+ async loadNextPageCommentReplies(comment, request) {
5204
+ const currentEntityState = this.currentState.comments_by_entity_id[comment.id];
5205
+ const currentPagination = currentEntityState?.pagination;
5206
+ const currentNextCursor = currentPagination?.next;
5207
+ const currentSort = currentPagination?.sort;
5208
+ const isLoading = currentPagination?.loading_next_page;
5209
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
5210
+ if (isLoading ||
5211
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
5212
+ return;
5213
+ }
5214
+ await this.loadNextPageComments({
5215
+ entityId: comment.id,
5216
+ base: () => this.client.getCommentReplies({
5217
+ ...request,
5218
+ comment_id: comment.id,
5219
+ // use known sort first (prevents broken pagination)
5220
+ sort,
5221
+ next: currentNextCursor,
5222
+ }),
5223
+ entityParentId: comment.parent_id ?? comment.object_id,
5224
+ sort,
5225
+ });
5226
+ }
5227
+ async loadNextPageFollows(type, request) {
5228
+ const paginationKey = `${type}_pagination`;
5229
+ const method = `query${capitalize(type)}`;
5230
+ const currentFollows = this.currentState[type];
5231
+ const currentNextCursor = this.currentState[paginationKey]?.next;
5232
+ const isLoading = this.currentState[paginationKey]?.loading_next_page;
5233
+ const sort = this.currentState[paginationKey]?.sort ?? request.sort;
5234
+ let error;
5235
+ if (isLoading || !checkHasAnotherPage(currentFollows, currentNextCursor)) {
5236
+ return;
5237
+ }
5238
+ try {
5239
+ this.state.next((currentState) => {
5240
+ return {
5241
+ ...currentState,
5242
+ [paginationKey]: {
5243
+ ...currentState[paginationKey],
5244
+ loading_next_page: true,
5245
+ },
5246
+ };
5290
5247
  });
5291
- };
5292
- this.handleVoteRemoved = (event) => {
5293
- if (event.poll?.id && event.poll.id !== this.id)
5294
- return;
5295
- if (!isPollVoteRemovedEvent(event))
5296
- return;
5297
- const currentState = this.data;
5298
- const isOwnVote = event.poll_vote.user_id ===
5299
- this.client.state.getLatestValue().connected_user?.id;
5300
- let latestAnswers = [...currentState.latest_answers];
5301
- let ownAnswer = currentState.own_answer;
5302
- const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
5303
- let maxVotedOptionIds = currentState.max_voted_option_ids;
5304
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5305
- if (isVoteAnswer(event.poll_vote)) {
5306
- latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
5307
- if (isOwnVote) {
5308
- ownAnswer = undefined;
5309
- }
5310
- }
5311
- else {
5312
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5313
- if (isOwnVote && event.poll_vote.option_id) {
5314
- delete ownVotesByOptionId[event.poll_vote.option_id];
5315
- }
5316
- }
5317
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5318
- this.state.partialNext({
5319
- answers_count,
5320
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5321
- latest_votes_by_option,
5322
- vote_count,
5323
- vote_counts_by_option,
5324
- latest_answers: latestAnswers,
5325
- last_activity_at: new Date(event.created_at),
5326
- own_answer: ownAnswer,
5327
- own_votes_by_option_id: ownVotesByOptionId,
5328
- max_voted_option_ids: maxVotedOptionIds,
5248
+ const { next: newNextCursor, follows } = await this[method]({
5249
+ ...request,
5250
+ next: currentNextCursor,
5251
+ sort,
5252
+ });
5253
+ this.state.next((currentState) => {
5254
+ return {
5255
+ ...currentState,
5256
+ [type]: currentState[type] === undefined
5257
+ ? follows
5258
+ : uniqueArrayMerge(currentState[type], follows, (follow) => `${follow.source_feed.fid}-${follow.target_feed.fid}`),
5259
+ [paginationKey]: {
5260
+ ...currentState[paginationKey],
5261
+ next: newNextCursor,
5262
+ sort,
5263
+ },
5264
+ };
5265
+ });
5266
+ }
5267
+ catch (e) {
5268
+ error = e;
5269
+ }
5270
+ finally {
5271
+ this.state.next((currentState) => {
5272
+ return {
5273
+ ...currentState,
5274
+ [paginationKey]: {
5275
+ ...currentState[paginationKey],
5276
+ loading_next_page: false,
5277
+ },
5278
+ };
5279
+ });
5280
+ }
5281
+ if (error) {
5282
+ throw error;
5283
+ }
5284
+ }
5285
+ async loadNextPageFollowers(request) {
5286
+ await this.loadNextPageFollows('followers', request);
5287
+ }
5288
+ async loadNextPageFollowing(request) {
5289
+ await this.loadNextPageFollows('following', request);
5290
+ }
5291
+ async loadNextPageMembers(request) {
5292
+ const currentMembers = this.currentState.members;
5293
+ const currentNextCursor = this.currentState.member_pagination?.next;
5294
+ const isLoading = this.currentState.member_pagination?.loading_next_page;
5295
+ const sort = this.currentState.member_pagination?.sort ?? request.sort;
5296
+ let error;
5297
+ if (isLoading || !checkHasAnotherPage(currentMembers, currentNextCursor)) {
5298
+ return;
5299
+ }
5300
+ try {
5301
+ this.state.next((currentState) => ({
5302
+ ...currentState,
5303
+ member_pagination: {
5304
+ ...currentState.member_pagination,
5305
+ loading_next_page: true,
5306
+ },
5307
+ }));
5308
+ const { next: newNextCursor, members } = await this.client.queryFeedMembers({
5309
+ ...request,
5310
+ sort,
5311
+ feed_id: this.id,
5312
+ feed_group_id: this.group,
5313
+ next: currentNextCursor,
5329
5314
  });
5315
+ this.state.next((currentState) => ({
5316
+ ...currentState,
5317
+ members: currentState.members
5318
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
5319
+ : members,
5320
+ member_pagination: {
5321
+ ...currentState.member_pagination,
5322
+ next: newNextCursor,
5323
+ // set sort if not defined yet
5324
+ sort: currentState.member_pagination?.sort ?? request.sort,
5325
+ },
5326
+ }));
5327
+ }
5328
+ catch (e) {
5329
+ error = e;
5330
+ }
5331
+ finally {
5332
+ this.state.next((currentState) => ({
5333
+ ...currentState,
5334
+ member_pagination: {
5335
+ ...currentState.member_pagination,
5336
+ loading_next_page: false,
5337
+ },
5338
+ }));
5339
+ }
5340
+ if (error) {
5341
+ throw error;
5342
+ }
5343
+ }
5344
+ /**
5345
+ * Method which queries followers of this feed (feeds which target this feed).
5346
+ *
5347
+ * _Note: Useful only for feeds with `groupId` of `user` value._
5348
+ */
5349
+ async queryFollowers(request) {
5350
+ const filter = {
5351
+ target_feed: this.fid,
5330
5352
  };
5331
- this.client = client;
5332
- this.id = poll.id;
5333
- this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
5353
+ const response = await this.client.queryFollows({
5354
+ filter,
5355
+ ...request,
5356
+ });
5357
+ return response;
5334
5358
  }
5335
- get data() {
5336
- return this.state.getLatestValue();
5359
+ /**
5360
+ * Method which queries following of this feed (target feeds of this feed).
5361
+ *
5362
+ * _Note: Useful only for feeds with `groupId` of `timeline` value._
5363
+ */
5364
+ async queryFollowing(request) {
5365
+ const filter = {
5366
+ source_feed: this.fid,
5367
+ };
5368
+ const response = await this.client.queryFollows({
5369
+ filter,
5370
+ ...request,
5371
+ });
5372
+ return response;
5337
5373
  }
5338
- }
5339
- function getMaxVotedOptionIds(voteCountsByOption) {
5340
- let maxVotes = 0;
5341
- let winningOptions = [];
5342
- for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
5343
- if (count > maxVotes) {
5344
- winningOptions = [id];
5345
- maxVotes = count;
5374
+ async follow(feedOrFid, options) {
5375
+ const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
5376
+ const response = await this.client.follow({
5377
+ ...options,
5378
+ source: this.fid,
5379
+ target: fid,
5380
+ });
5381
+ return response;
5382
+ }
5383
+ async unfollow(feedOrFid) {
5384
+ const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
5385
+ const response = await this.client.unfollow({
5386
+ source: this.fid,
5387
+ target: fid,
5388
+ });
5389
+ return response;
5390
+ }
5391
+ async getNextPage() {
5392
+ const currentState = this.currentState;
5393
+ return await this.getOrCreate({
5394
+ member_pagination: {
5395
+ limit: 0,
5396
+ },
5397
+ followers_pagination: {
5398
+ limit: 0,
5399
+ },
5400
+ following_pagination: {
5401
+ limit: 0,
5402
+ },
5403
+ next: currentState.next,
5404
+ limit: currentState.last_get_or_create_request_config?.limit ?? 20,
5405
+ });
5406
+ }
5407
+ addActivity(request) {
5408
+ return this.feedsApi.addActivity({
5409
+ ...request,
5410
+ fids: [this.fid],
5411
+ });
5412
+ }
5413
+ handleWSEvent(event) {
5414
+ const eventHandler = this.eventHandlers[event.type];
5415
+ // no need to run noop function
5416
+ if (eventHandler !== Feed.noop) {
5417
+ // @ts-expect-error intersection of handler arguments results to never
5418
+ eventHandler?.(event);
5346
5419
  }
5347
- else if (count === maxVotes) {
5348
- winningOptions.push(id);
5420
+ if (typeof eventHandler === 'undefined') {
5421
+ console.warn(`Received unknown event type: ${event.type}`, event);
5349
5422
  }
5423
+ this.eventDispatcher.dispatch(event);
5350
5424
  }
5351
- return winningOptions;
5352
- }
5353
- function getOwnVotesByOptionId(ownVotes) {
5354
- return !ownVotes
5355
- ? {}
5356
- : ownVotes.reduce((acc, vote) => {
5357
- if (isVoteAnswer(vote) || !vote.option_id)
5358
- return acc;
5359
- acc[vote.option_id] = vote;
5360
- return acc;
5361
- }, {});
5362
5425
  }
5426
+ Feed.noop = () => { };
5363
5427
 
5364
5428
  class FeedsClient extends FeedsApi {
5365
5429
  constructor(apiKey, options) {
@@ -5619,13 +5683,23 @@ class FeedsClient extends FeedsApi {
5619
5683
  duration: response.duration,
5620
5684
  };
5621
5685
  }
5686
+ async updateFollow(request) {
5687
+ const response = await super.updateFollow(request);
5688
+ [response.follow.source_feed.fid, response.follow.target_feed.fid].forEach((fid) => {
5689
+ const feed = this.activeFeeds[fid];
5690
+ if (feed) {
5691
+ handleFollowUpdated.bind(feed)(response);
5692
+ }
5693
+ });
5694
+ return response;
5695
+ }
5622
5696
  // For follow API endpoints we update the state after HTTP response to allow queryFeeds with watch: false
5623
5697
  async follow(request) {
5624
5698
  const response = await super.follow(request);
5625
5699
  [response.follow.source_feed.fid, response.follow.target_feed.fid].forEach((fid) => {
5626
5700
  const feed = this.activeFeeds[fid];
5627
5701
  if (feed) {
5628
- feed.handleFollowCreated(response.follow);
5702
+ handleFollowCreated.bind(feed)(response);
5629
5703
  }
5630
5704
  });
5631
5705
  return response;
@@ -5635,7 +5709,7 @@ class FeedsClient extends FeedsApi {
5635
5709
  response.follows.forEach((follow) => {
5636
5710
  const feed = this.activeFeeds[follow.source_feed.fid];
5637
5711
  if (feed) {
5638
- feed.handleFollowCreated(follow);
5712
+ handleFollowCreated.bind(feed)({ follow });
5639
5713
  }
5640
5714
  });
5641
5715
  return response;
@@ -5645,10 +5719,7 @@ class FeedsClient extends FeedsApi {
5645
5719
  [request.source, request.target].forEach((fid) => {
5646
5720
  const feed = this.activeFeeds[fid];
5647
5721
  if (feed) {
5648
- feed.handleFollowDeleted({
5649
- source_feed: { fid: request.source },
5650
- target_feed: { fid: request.target },
5651
- });
5722
+ handleFollowDeleted.bind(feed)(response);
5652
5723
  }
5653
5724
  });
5654
5725
  return response;