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