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