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