@stream-io/feeds-client 0.1.10 → 0.2.0

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 (168) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/@react-bindings/contexts/StreamFeedContext.d.ts +1 -1
  3. package/dist/@react-bindings/contexts/StreamFeedsContext.d.ts +1 -1
  4. package/dist/@react-bindings/hooks/feed-state-hooks/useComments.d.ts +1 -1
  5. package/dist/@react-bindings/hooks/feed-state-hooks/useFeedActivities.d.ts +1 -1
  6. package/dist/@react-bindings/hooks/feed-state-hooks/useFeedMetadata.d.ts +1 -1
  7. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowers.d.ts +1 -1
  8. package/dist/@react-bindings/hooks/feed-state-hooks/useFollowing.d.ts +1 -1
  9. package/dist/@react-bindings/hooks/feed-state-hooks/useOwnCapabilities.d.ts +1 -1
  10. package/dist/@react-bindings/hooks/feed-state-hooks/useOwnFollows.d.ts +1 -1
  11. package/dist/@react-bindings/hooks/useCreateFeedsClient.d.ts +1 -1
  12. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +1 -1
  13. package/dist/index-react-bindings.browser.cjs +1720 -1601
  14. package/dist/index-react-bindings.browser.cjs.map +1 -1
  15. package/dist/index-react-bindings.browser.js +1720 -1601
  16. package/dist/index-react-bindings.browser.js.map +1 -1
  17. package/dist/index-react-bindings.node.cjs +1720 -1601
  18. package/dist/index-react-bindings.node.cjs.map +1 -1
  19. package/dist/index-react-bindings.node.js +1720 -1601
  20. package/dist/index-react-bindings.node.js.map +1 -1
  21. package/dist/index.browser.cjs +1724 -1602
  22. package/dist/index.browser.cjs.map +1 -1
  23. package/dist/index.browser.js +1722 -1603
  24. package/dist/index.browser.js.map +1 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.node.cjs +1724 -1602
  27. package/dist/index.node.cjs.map +1 -1
  28. package/dist/index.node.js +1722 -1603
  29. package/dist/index.node.js.map +1 -1
  30. package/dist/src/common/ActivitySearchSource.d.ts +1 -1
  31. package/dist/src/common/FeedSearchSource.d.ts +2 -2
  32. package/dist/src/common/Poll.d.ts +1 -1
  33. package/dist/src/common/UserSearchSource.d.ts +1 -1
  34. package/dist/src/common/real-time/StableWSConnection.d.ts +3 -3
  35. package/dist/src/feed/event-handlers/activity/handle-activity-added.d.ts +7 -0
  36. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +8 -0
  37. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +8 -0
  38. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +8 -0
  39. package/dist/src/feed/event-handlers/activity/handle-activity-removed-from-feed.d.ts +3 -0
  40. package/dist/src/feed/event-handlers/activity/handle-activity-updated.d.ts +8 -0
  41. package/dist/src/feed/event-handlers/activity/index.d.ts +6 -0
  42. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +8 -0
  43. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +9 -0
  44. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +8 -0
  45. package/dist/src/feed/event-handlers/bookmark/index.d.ts +3 -0
  46. package/dist/src/feed/event-handlers/comment/handle-comment-added.d.ts +3 -0
  47. package/dist/src/feed/event-handlers/comment/handle-comment-deleted.d.ts +3 -0
  48. package/dist/src/feed/event-handlers/comment/handle-comment-reaction.d.ts +3 -0
  49. package/dist/src/feed/event-handlers/comment/handle-comment-updated.d.ts +3 -0
  50. package/dist/src/feed/event-handlers/comment/index.d.ts +4 -0
  51. package/dist/src/feed/event-handlers/feed/handle-feed-updated.d.ts +3 -0
  52. package/dist/src/feed/event-handlers/feed/index.d.ts +1 -0
  53. package/dist/src/feed/event-handlers/feed-member/handle-feed-member-added.d.ts +3 -0
  54. package/dist/src/feed/event-handlers/feed-member/handle-feed-member-removed.d.ts +3 -0
  55. package/dist/src/feed/event-handlers/feed-member/handle-feed-member-updated.d.ts +3 -0
  56. package/dist/src/feed/event-handlers/feed-member/index.d.ts +3 -0
  57. package/dist/src/feed/event-handlers/follow/handle-follow-created.d.ts +7 -0
  58. package/dist/src/feed/event-handlers/follow/handle-follow-deleted.d.ts +7 -0
  59. package/dist/src/feed/event-handlers/follow/handle-follow-updated.d.ts +3 -0
  60. package/dist/src/feed/event-handlers/follow/index.d.ts +3 -0
  61. package/dist/src/feed/event-handlers/index.d.ts +8 -0
  62. package/dist/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +3 -0
  63. package/dist/src/feed/event-handlers/notification-feed/index.d.ts +1 -0
  64. package/dist/src/feed/event-handlers/watch/handle-watch-started.d.ts +2 -0
  65. package/dist/src/feed/event-handlers/watch/handle-watch-stopped.d.ts +2 -0
  66. package/dist/src/feed/event-handlers/watch/index.d.ts +2 -0
  67. package/dist/src/{Feed.d.ts → feed/feed.d.ts} +16 -43
  68. package/dist/src/feed/index.d.ts +2 -0
  69. package/dist/src/feeds-client/event-handlers/index.d.ts +1 -0
  70. package/dist/src/feeds-client/event-handlers/user/handle-user-updated.d.ts +3 -0
  71. package/dist/src/feeds-client/feeds-client.d.ts +76 -0
  72. package/dist/src/feeds-client/index.d.ts +2 -0
  73. package/dist/src/gen/feeds/FeedsApi.d.ts +27 -23
  74. package/dist/src/gen/models/index.d.ts +168 -23
  75. package/dist/src/gen-imports.d.ts +1 -1
  76. package/dist/src/test-utils/index.d.ts +1 -0
  77. package/dist/src/test-utils/response-generators.d.ts +9 -0
  78. package/dist/src/types-internal.d.ts +7 -0
  79. package/dist/src/types.d.ts +1 -1
  80. package/dist/src/utils/check-has-another-page.d.ts +1 -0
  81. package/dist/src/utils/constants.d.ts +3 -0
  82. package/dist/src/utils/index.d.ts +5 -0
  83. package/dist/src/utils/state-update-queue.d.ts +6 -0
  84. package/dist/src/utils/type-assertions.d.ts +7 -0
  85. package/dist/src/utils/unique-array-merge.d.ts +1 -0
  86. package/dist/tsconfig.tsbuildinfo +1 -1
  87. package/index.ts +2 -2
  88. package/package.json +3 -2
  89. package/src/common/ActivitySearchSource.ts +1 -1
  90. package/src/common/FeedSearchSource.ts +2 -2
  91. package/src/common/Poll.ts +1 -1
  92. package/src/common/UserSearchSource.ts +1 -1
  93. package/src/{state-updates → feed/event-handlers/activity}/activity-reaction-utils.test.ts +12 -2
  94. package/src/{state-updates → feed/event-handlers/activity}/activity-utils.test.ts +3 -2
  95. package/src/{state-updates/activity-utils.ts → feed/event-handlers/activity/handle-activity-added.ts} +16 -36
  96. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +30 -0
  97. package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +67 -0
  98. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +75 -0
  99. package/src/feed/event-handlers/activity/handle-activity-removed-from-feed.ts +16 -0
  100. package/src/feed/event-handlers/activity/handle-activity-updated.ts +47 -0
  101. package/src/feed/event-handlers/activity/index.ts +6 -0
  102. package/src/{state-updates → feed/event-handlers/bookmark}/bookmark-utils.test.ts +2 -2
  103. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +63 -0
  104. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +84 -0
  105. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +76 -0
  106. package/src/feed/event-handlers/bookmark/index.ts +3 -0
  107. package/src/feed/event-handlers/comment/handle-comment-added.ts +38 -0
  108. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +35 -0
  109. package/src/feed/event-handlers/comment/handle-comment-reaction.ts +61 -0
  110. package/src/feed/event-handlers/comment/handle-comment-updated.ts +35 -0
  111. package/src/feed/event-handlers/comment/index.ts +4 -0
  112. package/src/feed/event-handlers/feed/handle-feed-updated.ts +9 -0
  113. package/src/feed/event-handlers/feed/index.ts +1 -0
  114. package/src/feed/event-handlers/feed-member/handle-feed-member-added.ts +31 -0
  115. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +24 -0
  116. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.ts +40 -0
  117. package/src/feed/event-handlers/feed-member/index.ts +3 -0
  118. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +250 -0
  119. package/src/feed/event-handlers/follow/handle-follow-created.ts +90 -0
  120. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +268 -0
  121. package/src/feed/event-handlers/follow/handle-follow-deleted.ts +95 -0
  122. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +174 -0
  123. package/src/feed/event-handlers/follow/handle-follow-updated.ts +85 -0
  124. package/src/feed/event-handlers/follow/index.ts +3 -0
  125. package/src/feed/event-handlers/index.ts +8 -0
  126. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +10 -0
  127. package/src/feed/event-handlers/notification-feed/index.ts +1 -0
  128. package/src/feed/event-handlers/watch/handle-watch-started.ts +5 -0
  129. package/src/feed/event-handlers/watch/handle-watch-stopped.ts +5 -0
  130. package/src/feed/event-handlers/watch/index.ts +2 -0
  131. package/src/{Feed.ts → feed/feed.ts} +87 -516
  132. package/src/feed/index.ts +2 -0
  133. package/src/feeds-client/event-handlers/index.ts +1 -0
  134. package/src/feeds-client/event-handlers/user/handle-user-updated.test.ts +53 -0
  135. package/src/feeds-client/event-handlers/user/handle-user-updated.ts +28 -0
  136. package/src/{FeedsClient.ts → feeds-client/feeds-client.ts} +63 -36
  137. package/src/feeds-client/index.ts +2 -0
  138. package/src/gen/feeds/FeedsApi.ts +164 -138
  139. package/src/gen/model-decoders/decoders.ts +22 -0
  140. package/src/gen/models/index.ts +288 -29
  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 +101 -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/FeedsClient.d.ts +0 -75
  155. package/dist/src/state-updates/activity-reaction-utils.d.ts +0 -10
  156. package/dist/src/state-updates/activity-utils.d.ts +0 -13
  157. package/dist/src/state-updates/bookmark-utils.d.ts +0 -14
  158. package/dist/src/state-updates/follow-utils.d.ts +0 -19
  159. package/dist/src/state-updates/state-update-queue.d.ts +0 -15
  160. package/dist/src/utils.d.ts +0 -10
  161. package/src/state-updates/activity-reaction-utils.ts +0 -107
  162. package/src/state-updates/bookmark-utils.ts +0 -167
  163. package/src/state-updates/follow-utils.test.ts +0 -552
  164. package/src/state-updates/follow-utils.ts +0 -126
  165. package/src/state-updates/state-update-queue.ts +0 -35
  166. package/src/utils.ts +0 -48
  167. /package/dist/src/{ModerationClient.d.ts → moderation-client.d.ts} +0 -0
  168. /package/src/{ModerationClient.ts → moderation-client.ts} +0 -0
@@ -905,6 +905,7 @@ decoders.FeedResponse = (input) => {
905
905
  updated_at: { type: 'DatetimeType', isSingle: true },
906
906
  created_by: { type: 'UserResponse', isSingle: true },
907
907
  deleted_at: { type: 'DatetimeType', isSingle: true },
908
+ own_follows: { type: 'FollowResponse', isSingle: false },
908
909
  };
909
910
  return decode(typeMappings, input);
910
911
  };
@@ -1123,6 +1124,13 @@ decoders.ModerationCustomActionEvent = (input) => {
1123
1124
  };
1124
1125
  return decode(typeMappings, input);
1125
1126
  };
1127
+ decoders.ModerationFlagResponse = (input) => {
1128
+ const typeMappings = {
1129
+ review_queue_item: { type: 'ReviewQueueItemResponse', isSingle: true },
1130
+ user: { type: 'UserResponse', isSingle: true },
1131
+ };
1132
+ return decode(typeMappings, input);
1133
+ };
1126
1134
  decoders.ModerationFlaggedEvent = (input) => {
1127
1135
  const typeMappings = {
1128
1136
  created_at: { type: 'DatetimeType', isSingle: true },
@@ -1159,6 +1167,7 @@ decoders.NotificationFeedUpdatedEvent = (input) => {
1159
1167
  };
1160
1168
  decoders.NotificationStatusResponse = (input) => {
1161
1169
  const typeMappings = {
1170
+ last_read_at: { type: 'DatetimeType', isSingle: true },
1162
1171
  last_seen_at: { type: 'DatetimeType', isSingle: true },
1163
1172
  };
1164
1173
  return decode(typeMappings, input);
@@ -1426,6 +1435,7 @@ decoders.ReviewQueueItemResponse = (input) => {
1426
1435
  updated_at: { type: 'DatetimeType', isSingle: true },
1427
1436
  actions: { type: 'ActionLogResponse', isSingle: false },
1428
1437
  bans: { type: 'Ban', isSingle: false },
1438
+ flags: { type: 'ModerationFlagResponse', isSingle: false },
1429
1439
  completed_at: { type: 'DatetimeType', isSingle: true },
1430
1440
  reviewed_at: { type: 'DatetimeType', isSingle: true },
1431
1441
  assigned_to: { type: 'UserResponse', isSingle: true },
@@ -1502,6 +1512,12 @@ decoders.ThreadedCommentResponse = (input) => {
1502
1512
  };
1503
1513
  return decode(typeMappings, input);
1504
1514
  };
1515
+ decoders.UnfollowResponse = (input) => {
1516
+ const typeMappings = {
1517
+ follow: { type: 'FollowResponse', isSingle: true },
1518
+ };
1519
+ return decode(typeMappings, input);
1520
+ };
1505
1521
  decoders.UnpinActivityResponse = (input) => {
1506
1522
  const typeMappings = {
1507
1523
  activity: { type: 'ActivityResponse', isSingle: true },
@@ -1742,7 +1758,7 @@ class FeedsApi {
1742
1758
  async addActivity(request) {
1743
1759
  const body = {
1744
1760
  type: request?.type,
1745
- fids: request?.fids,
1761
+ feeds: request?.feeds,
1746
1762
  expires_at: request?.expires_at,
1747
1763
  id: request?.id,
1748
1764
  parent_id: request?.parent_id,
@@ -1772,7 +1788,7 @@ class FeedsApi {
1772
1788
  }
1773
1789
  async deleteActivities(request) {
1774
1790
  const body = {
1775
- activity_ids: request?.activity_ids,
1791
+ ids: request?.ids,
1776
1792
  hard_delete: request?.hard_delete,
1777
1793
  };
1778
1794
  const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/activities/delete', undefined, undefined, body, 'application/json');
@@ -1791,56 +1807,6 @@ class FeedsApi {
1791
1807
  decoders.QueryActivitiesResponse?.(response.body);
1792
1808
  return { ...response.body, metadata: response.metadata };
1793
1809
  }
1794
- async deleteActivity(request) {
1795
- const queryParams = {
1796
- hard_delete: request?.hard_delete,
1797
- };
1798
- const pathParams = {
1799
- activity_id: request?.activity_id,
1800
- };
1801
- const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/activities/{activity_id}', pathParams, queryParams);
1802
- decoders.DeleteActivityResponse?.(response.body);
1803
- return { ...response.body, metadata: response.metadata };
1804
- }
1805
- async getActivity(request) {
1806
- const pathParams = {
1807
- activity_id: request?.activity_id,
1808
- };
1809
- const response = await this.apiClient.sendRequest('GET', '/api/v2/feeds/activities/{activity_id}', pathParams, undefined);
1810
- decoders.GetActivityResponse?.(response.body);
1811
- return { ...response.body, metadata: response.metadata };
1812
- }
1813
- async updateActivityPartial(request) {
1814
- const pathParams = {
1815
- activity_id: request?.activity_id,
1816
- };
1817
- const body = {
1818
- unset: request?.unset,
1819
- set: request?.set,
1820
- };
1821
- const response = await this.apiClient.sendRequest('PATCH', '/api/v2/feeds/activities/{activity_id}', pathParams, undefined, body, 'application/json');
1822
- decoders.UpdateActivityPartialResponse?.(response.body);
1823
- return { ...response.body, metadata: response.metadata };
1824
- }
1825
- async updateActivity(request) {
1826
- const pathParams = {
1827
- activity_id: request?.activity_id,
1828
- };
1829
- const body = {
1830
- expires_at: request?.expires_at,
1831
- poll_id: request?.poll_id,
1832
- text: request?.text,
1833
- visibility: request?.visibility,
1834
- attachments: request?.attachments,
1835
- filter_tags: request?.filter_tags,
1836
- interest_tags: request?.interest_tags,
1837
- custom: request?.custom,
1838
- location: request?.location,
1839
- };
1840
- const response = await this.apiClient.sendRequest('PUT', '/api/v2/feeds/activities/{activity_id}', pathParams, undefined, body, 'application/json');
1841
- decoders.UpdateActivityResponse?.(response.body);
1842
- return { ...response.body, metadata: response.metadata };
1843
- }
1844
1810
  async deleteBookmark(request) {
1845
1811
  const queryParams = {
1846
1812
  folder_id: request?.folder_id,
@@ -1879,6 +1845,21 @@ class FeedsApi {
1879
1845
  decoders.AddBookmarkResponse?.(response.body);
1880
1846
  return { ...response.body, metadata: response.metadata };
1881
1847
  }
1848
+ async activityFeedback(request) {
1849
+ const pathParams = {
1850
+ activity_id: request?.activity_id,
1851
+ };
1852
+ const body = {
1853
+ hide: request?.hide,
1854
+ mute_user: request?.mute_user,
1855
+ reason: request?.reason,
1856
+ report: request?.report,
1857
+ show_less: request?.show_less,
1858
+ };
1859
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/activities/{activity_id}/feedback', pathParams, undefined, body, 'application/json');
1860
+ decoders.ActivityFeedbackResponse?.(response.body);
1861
+ return { ...response.body, metadata: response.metadata };
1862
+ }
1882
1863
  async castPollVote(request) {
1883
1864
  const pathParams = {
1884
1865
  activity_id: request?.activity_id,
@@ -1941,6 +1922,56 @@ class FeedsApi {
1941
1922
  decoders.DeleteActivityReactionResponse?.(response.body);
1942
1923
  return { ...response.body, metadata: response.metadata };
1943
1924
  }
1925
+ async deleteActivity(request) {
1926
+ const queryParams = {
1927
+ hard_delete: request?.hard_delete,
1928
+ };
1929
+ const pathParams = {
1930
+ id: request?.id,
1931
+ };
1932
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/activities/{id}', pathParams, queryParams);
1933
+ decoders.DeleteActivityResponse?.(response.body);
1934
+ return { ...response.body, metadata: response.metadata };
1935
+ }
1936
+ async getActivity(request) {
1937
+ const pathParams = {
1938
+ id: request?.id,
1939
+ };
1940
+ const response = await this.apiClient.sendRequest('GET', '/api/v2/feeds/activities/{id}', pathParams, undefined);
1941
+ decoders.GetActivityResponse?.(response.body);
1942
+ return { ...response.body, metadata: response.metadata };
1943
+ }
1944
+ async updateActivityPartial(request) {
1945
+ const pathParams = {
1946
+ id: request?.id,
1947
+ };
1948
+ const body = {
1949
+ unset: request?.unset,
1950
+ set: request?.set,
1951
+ };
1952
+ const response = await this.apiClient.sendRequest('PATCH', '/api/v2/feeds/activities/{id}', pathParams, undefined, body, 'application/json');
1953
+ decoders.UpdateActivityPartialResponse?.(response.body);
1954
+ return { ...response.body, metadata: response.metadata };
1955
+ }
1956
+ async updateActivity(request) {
1957
+ const pathParams = {
1958
+ id: request?.id,
1959
+ };
1960
+ const body = {
1961
+ expires_at: request?.expires_at,
1962
+ poll_id: request?.poll_id,
1963
+ text: request?.text,
1964
+ visibility: request?.visibility,
1965
+ attachments: request?.attachments,
1966
+ filter_tags: request?.filter_tags,
1967
+ interest_tags: request?.interest_tags,
1968
+ custom: request?.custom,
1969
+ location: request?.location,
1970
+ };
1971
+ const response = await this.apiClient.sendRequest('PUT', '/api/v2/feeds/activities/{id}', pathParams, undefined, body, 'application/json');
1972
+ decoders.UpdateActivityResponse?.(response.body);
1973
+ return { ...response.body, metadata: response.metadata };
1974
+ }
1944
1975
  async queryBookmarkFolders(request) {
1945
1976
  const body = {
1946
1977
  limit: request?.limit,
@@ -2036,49 +2067,52 @@ class FeedsApi {
2036
2067
  return { ...response.body, metadata: response.metadata };
2037
2068
  }
2038
2069
  async deleteComment(request) {
2070
+ const queryParams = {
2071
+ hard_delete: request?.hard_delete,
2072
+ };
2039
2073
  const pathParams = {
2040
- comment_id: request?.comment_id,
2074
+ id: request?.id,
2041
2075
  };
2042
- const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/comments/{comment_id}', pathParams, undefined);
2076
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/comments/{id}', pathParams, queryParams);
2043
2077
  decoders.DeleteCommentResponse?.(response.body);
2044
2078
  return { ...response.body, metadata: response.metadata };
2045
2079
  }
2046
2080
  async getComment(request) {
2047
2081
  const pathParams = {
2048
- comment_id: request?.comment_id,
2082
+ id: request?.id,
2049
2083
  };
2050
- const response = await this.apiClient.sendRequest('GET', '/api/v2/feeds/comments/{comment_id}', pathParams, undefined);
2084
+ const response = await this.apiClient.sendRequest('GET', '/api/v2/feeds/comments/{id}', pathParams, undefined);
2051
2085
  decoders.GetCommentResponse?.(response.body);
2052
2086
  return { ...response.body, metadata: response.metadata };
2053
2087
  }
2054
2088
  async updateComment(request) {
2055
2089
  const pathParams = {
2056
- comment_id: request?.comment_id,
2090
+ id: request?.id,
2057
2091
  };
2058
2092
  const body = {
2059
2093
  comment: request?.comment,
2060
2094
  custom: request?.custom,
2061
2095
  };
2062
- const response = await this.apiClient.sendRequest('PATCH', '/api/v2/feeds/comments/{comment_id}', pathParams, undefined, body, 'application/json');
2096
+ const response = await this.apiClient.sendRequest('PATCH', '/api/v2/feeds/comments/{id}', pathParams, undefined, body, 'application/json');
2063
2097
  decoders.UpdateCommentResponse?.(response.body);
2064
2098
  return { ...response.body, metadata: response.metadata };
2065
2099
  }
2066
2100
  async addCommentReaction(request) {
2067
2101
  const pathParams = {
2068
- comment_id: request?.comment_id,
2102
+ id: request?.id,
2069
2103
  };
2070
2104
  const body = {
2071
2105
  type: request?.type,
2072
2106
  create_notification_activity: request?.create_notification_activity,
2073
2107
  custom: request?.custom,
2074
2108
  };
2075
- const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/comments/{comment_id}/reactions', pathParams, undefined, body, 'application/json');
2109
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/comments/{id}/reactions', pathParams, undefined, body, 'application/json');
2076
2110
  decoders.AddCommentReactionResponse?.(response.body);
2077
2111
  return { ...response.body, metadata: response.metadata };
2078
2112
  }
2079
2113
  async queryCommentReactions(request) {
2080
2114
  const pathParams = {
2081
- comment_id: request?.comment_id,
2115
+ id: request?.id,
2082
2116
  };
2083
2117
  const body = {
2084
2118
  limit: request?.limit,
@@ -2087,16 +2121,16 @@ class FeedsApi {
2087
2121
  sort: request?.sort,
2088
2122
  filter: request?.filter,
2089
2123
  };
2090
- const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/comments/{comment_id}/reactions/query', pathParams, undefined, body, 'application/json');
2124
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/comments/{id}/reactions/query', pathParams, undefined, body, 'application/json');
2091
2125
  decoders.QueryCommentReactionsResponse?.(response.body);
2092
2126
  return { ...response.body, metadata: response.metadata };
2093
2127
  }
2094
2128
  async deleteCommentReaction(request) {
2095
2129
  const pathParams = {
2096
- comment_id: request?.comment_id,
2130
+ id: request?.id,
2097
2131
  type: request?.type,
2098
2132
  };
2099
- const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/comments/{comment_id}/reactions/{type}', pathParams, undefined);
2133
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/feeds/comments/{id}/reactions/{type}', pathParams, undefined);
2100
2134
  decoders.DeleteCommentReactionResponse?.(response.body);
2101
2135
  return { ...response.body, metadata: response.metadata };
2102
2136
  }
@@ -2110,9 +2144,9 @@ class FeedsApi {
2110
2144
  next: request?.next,
2111
2145
  };
2112
2146
  const pathParams = {
2113
- comment_id: request?.comment_id,
2147
+ id: request?.id,
2114
2148
  };
2115
- const response = await this.apiClient.sendRequest('GET', '/api/v2/feeds/comments/{comment_id}/replies', pathParams, queryParams);
2149
+ const response = await this.apiClient.sendRequest('GET', '/api/v2/feeds/comments/{id}/replies', pathParams, queryParams);
2116
2150
  decoders.GetCommentRepliesResponse?.(response.body);
2117
2151
  return { ...response.body, metadata: response.metadata };
2118
2152
  }
@@ -2161,7 +2195,6 @@ class FeedsApi {
2161
2195
  feed_id: request?.feed_id,
2162
2196
  };
2163
2197
  const body = {
2164
- created_by_id: request?.created_by_id,
2165
2198
  custom: request?.custom,
2166
2199
  };
2167
2200
  const response = await this.apiClient.sendRequest('PUT', '/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}', pathParams, undefined, body, 'application/json');
@@ -2177,6 +2210,7 @@ class FeedsApi {
2177
2210
  mark_all_read: request?.mark_all_read,
2178
2211
  mark_all_seen: request?.mark_all_seen,
2179
2212
  mark_read: request?.mark_read,
2213
+ mark_seen: request?.mark_seen,
2180
2214
  mark_watched: request?.mark_watched,
2181
2215
  };
2182
2216
  const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}/activities/mark/batch', pathParams, undefined, body, 'application/json');
@@ -2287,7 +2321,7 @@ class FeedsApi {
2287
2321
  decoders.CreateFeedsBatchResponse?.(response.body);
2288
2322
  return { ...response.body, metadata: response.metadata };
2289
2323
  }
2290
- async feedsQueryFeeds(request) {
2324
+ async _queryFeeds(request) {
2291
2325
  const queryParams = {
2292
2326
  connection_id: request?.connection_id,
2293
2327
  };
@@ -2330,8 +2364,8 @@ class FeedsApi {
2330
2364
  }
2331
2365
  async acceptFollow(request) {
2332
2366
  const body = {
2333
- source_fid: request?.source_fid,
2334
- target_fid: request?.target_fid,
2367
+ source: request?.source,
2368
+ target: request?.target,
2335
2369
  follower_role: request?.follower_role,
2336
2370
  };
2337
2371
  const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/follows/accept', undefined, undefined, body, 'application/json');
@@ -2360,8 +2394,8 @@ class FeedsApi {
2360
2394
  }
2361
2395
  async rejectFollow(request) {
2362
2396
  const body = {
2363
- source_fid: request?.source_fid,
2364
- target_fid: request?.target_fid,
2397
+ source: request?.source,
2398
+ target: request?.target,
2365
2399
  };
2366
2400
  const response = await this.apiClient.sendRequest('POST', '/api/v2/feeds/follows/reject', undefined, undefined, body, 'application/json');
2367
2401
  decoders.RejectFollowResponse?.(response.body);
@@ -3741,1624 +3775,1692 @@ const decodeWSEvent = (data) => {
3741
3775
  }
3742
3776
  };
3743
3777
 
3744
- class FeedApi {
3745
- constructor(feedsApi, group, id) {
3746
- this.feedsApi = feedsApi;
3747
- this.group = group;
3748
- this.id = id;
3778
+ class ModerationApi {
3779
+ constructor(apiClient) {
3780
+ this.apiClient = apiClient;
3749
3781
  }
3750
- delete(request) {
3751
- return this.feedsApi.deleteFeed({
3752
- feed_id: this.id,
3753
- feed_group_id: this.group,
3754
- ...request,
3755
- });
3782
+ async ban(request) {
3783
+ const body = {
3784
+ target_user_id: request?.target_user_id,
3785
+ banned_by_id: request?.banned_by_id,
3786
+ channel_cid: request?.channel_cid,
3787
+ delete_messages: request?.delete_messages,
3788
+ ip_ban: request?.ip_ban,
3789
+ reason: request?.reason,
3790
+ shadow: request?.shadow,
3791
+ timeout: request?.timeout,
3792
+ banned_by: request?.banned_by,
3793
+ };
3794
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/ban', undefined, undefined, body, 'application/json');
3795
+ decoders.BanResponse?.(response.body);
3796
+ return { ...response.body, metadata: response.metadata };
3756
3797
  }
3757
- getOrCreate(request) {
3758
- return this.feedsApi.getOrCreateFeed({
3759
- feed_id: this.id,
3760
- feed_group_id: this.group,
3761
- ...request,
3762
- });
3798
+ async upsertConfig(request) {
3799
+ const body = {
3800
+ key: request?.key,
3801
+ async: request?.async,
3802
+ team: request?.team,
3803
+ ai_image_config: request?.ai_image_config,
3804
+ ai_text_config: request?.ai_text_config,
3805
+ ai_video_config: request?.ai_video_config,
3806
+ automod_platform_circumvention_config: request?.automod_platform_circumvention_config,
3807
+ automod_semantic_filters_config: request?.automod_semantic_filters_config,
3808
+ automod_toxicity_config: request?.automod_toxicity_config,
3809
+ aws_rekognition_config: request?.aws_rekognition_config,
3810
+ block_list_config: request?.block_list_config,
3811
+ bodyguard_config: request?.bodyguard_config,
3812
+ google_vision_config: request?.google_vision_config,
3813
+ rule_builder_config: request?.rule_builder_config,
3814
+ velocity_filter_config: request?.velocity_filter_config,
3815
+ video_call_rule_config: request?.video_call_rule_config,
3816
+ };
3817
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/config', undefined, undefined, body, 'application/json');
3818
+ decoders.UpsertConfigResponse?.(response.body);
3819
+ return { ...response.body, metadata: response.metadata };
3763
3820
  }
3764
- update(request) {
3765
- return this.feedsApi.updateFeed({
3766
- feed_id: this.id,
3767
- feed_group_id: this.group,
3768
- ...request,
3769
- });
3821
+ async deleteConfig(request) {
3822
+ const queryParams = {
3823
+ team: request?.team,
3824
+ };
3825
+ const pathParams = {
3826
+ key: request?.key,
3827
+ };
3828
+ const response = await this.apiClient.sendRequest('DELETE', '/api/v2/moderation/config/{key}', pathParams, queryParams);
3829
+ decoders.DeleteModerationConfigResponse?.(response.body);
3830
+ return { ...response.body, metadata: response.metadata };
3770
3831
  }
3771
- markActivity(request) {
3772
- return this.feedsApi.markActivity({
3773
- feed_id: this.id,
3774
- feed_group_id: this.group,
3775
- ...request,
3776
- });
3832
+ async getConfig(request) {
3833
+ const queryParams = {
3834
+ team: request?.team,
3835
+ };
3836
+ const pathParams = {
3837
+ key: request?.key,
3838
+ };
3839
+ const response = await this.apiClient.sendRequest('GET', '/api/v2/moderation/config/{key}', pathParams, queryParams);
3840
+ decoders.GetConfigResponse?.(response.body);
3841
+ return { ...response.body, metadata: response.metadata };
3777
3842
  }
3778
- unpinActivity(request) {
3779
- return this.feedsApi.unpinActivity({
3780
- feed_id: this.id,
3781
- feed_group_id: this.group,
3782
- ...request,
3783
- });
3843
+ async queryModerationConfigs(request) {
3844
+ const body = {
3845
+ limit: request?.limit,
3846
+ next: request?.next,
3847
+ prev: request?.prev,
3848
+ sort: request?.sort,
3849
+ filter: request?.filter,
3850
+ };
3851
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/configs', undefined, undefined, body, 'application/json');
3852
+ decoders.QueryModerationConfigsResponse?.(response.body);
3853
+ return { ...response.body, metadata: response.metadata };
3784
3854
  }
3785
- pinActivity(request) {
3786
- return this.feedsApi.pinActivity({
3787
- feed_id: this.id,
3788
- feed_group_id: this.group,
3789
- ...request,
3790
- });
3855
+ async flag(request) {
3856
+ const body = {
3857
+ entity_id: request?.entity_id,
3858
+ entity_type: request?.entity_type,
3859
+ entity_creator_id: request?.entity_creator_id,
3860
+ reason: request?.reason,
3861
+ custom: request?.custom,
3862
+ moderation_payload: request?.moderation_payload,
3863
+ };
3864
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/flag', undefined, undefined, body, 'application/json');
3865
+ decoders.FlagResponse?.(response.body);
3866
+ return { ...response.body, metadata: response.metadata };
3791
3867
  }
3792
- updateFeedMembers(request) {
3793
- return this.feedsApi.updateFeedMembers({
3794
- feed_id: this.id,
3795
- feed_group_id: this.group,
3796
- ...request,
3797
- });
3798
- }
3799
- acceptFeedMemberInvite(request) {
3800
- return this.feedsApi.acceptFeedMemberInvite({
3801
- feed_id: this.id,
3802
- feed_group_id: this.group,
3803
- ...request,
3804
- });
3805
- }
3806
- queryFeedMembers(request) {
3807
- return this.feedsApi.queryFeedMembers({
3808
- feed_id: this.id,
3809
- feed_group_id: this.group,
3810
- ...request,
3811
- });
3812
- }
3813
- rejectFeedMemberInvite(request) {
3814
- return this.feedsApi.rejectFeedMemberInvite({
3815
- feed_id: this.id,
3816
- feed_group_id: this.group,
3817
- ...request,
3818
- });
3819
- }
3820
- stopWatching(request) {
3821
- return this.feedsApi.stopWatchingFeed({
3822
- feed_id: this.id,
3823
- feed_group_id: this.group,
3824
- ...request,
3825
- });
3826
- }
3827
- }
3828
-
3829
- const addActivitiesToState = (newActivities, activities, position) => {
3830
- let result;
3831
- if (activities === undefined) {
3832
- activities = [];
3833
- result = {
3834
- changed: true,
3835
- activities,
3868
+ async mute(request) {
3869
+ const body = {
3870
+ target_ids: request?.target_ids,
3871
+ timeout: request?.timeout,
3836
3872
  };
3873
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/mute', undefined, undefined, body, 'application/json');
3874
+ decoders.MuteResponse?.(response.body);
3875
+ return { ...response.body, metadata: response.metadata };
3837
3876
  }
3838
- else {
3839
- result = {
3840
- changed: false,
3841
- activities,
3877
+ async queryReviewQueue(request) {
3878
+ const body = {
3879
+ limit: request?.limit,
3880
+ lock_count: request?.lock_count,
3881
+ lock_duration: request?.lock_duration,
3882
+ lock_items: request?.lock_items,
3883
+ next: request?.next,
3884
+ prev: request?.prev,
3885
+ stats_only: request?.stats_only,
3886
+ sort: request?.sort,
3887
+ filter: request?.filter,
3842
3888
  };
3889
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/review_queue', undefined, undefined, body, 'application/json');
3890
+ decoders.QueryReviewQueueResponse?.(response.body);
3891
+ return { ...response.body, metadata: response.metadata };
3843
3892
  }
3844
- const newActivitiesDeduplicated = [];
3845
- newActivities.forEach((newActivityResponse) => {
3846
- const index = activities.findIndex((a) => a.id === newActivityResponse.id);
3847
- if (index === -1) {
3848
- newActivitiesDeduplicated.push(newActivityResponse);
3849
- }
3850
- });
3851
- if (newActivitiesDeduplicated.length > 0) {
3852
- // TODO: since feed activities are not necessarily ordered by created_at (personalization) we don't order by created_at
3853
- // Maybe we can add a flag to the JS client to support order by created_at
3854
- const updatedActivities = [
3855
- ...(position === 'start' ? newActivitiesDeduplicated : []),
3856
- ...activities,
3857
- ...(position === 'end' ? newActivitiesDeduplicated : []),
3858
- ];
3859
- result = { changed: true, activities: updatedActivities };
3860
- }
3861
- return result;
3862
- };
3863
- const updateActivityInState = (updatedActivityResponse, activities) => {
3864
- const index = activities.findIndex((a) => a.id === updatedActivityResponse.id);
3865
- if (index !== -1) {
3866
- const newActivities = [...activities];
3867
- const activity = activities[index];
3868
- newActivities[index] = {
3869
- ...updatedActivityResponse,
3870
- own_reactions: activity.own_reactions,
3871
- own_bookmarks: activity.own_bookmarks,
3872
- latest_reactions: activity.latest_reactions,
3873
- reaction_groups: activity.reaction_groups,
3893
+ async submitAction(request) {
3894
+ const body = {
3895
+ action_type: request?.action_type,
3896
+ item_id: request?.item_id,
3897
+ ban: request?.ban,
3898
+ custom: request?.custom,
3899
+ delete_activity: request?.delete_activity,
3900
+ delete_message: request?.delete_message,
3901
+ delete_reaction: request?.delete_reaction,
3902
+ delete_user: request?.delete_user,
3903
+ mark_reviewed: request?.mark_reviewed,
3904
+ unban: request?.unban,
3874
3905
  };
3875
- return { changed: true, activities: newActivities };
3876
- }
3877
- else {
3878
- return { changed: false, activities };
3879
- }
3880
- };
3881
- const removeActivityFromState = (activityResponse, activities) => {
3882
- const index = activities.findIndex((a) => a.id === activityResponse.id);
3883
- if (index !== -1) {
3884
- const newActivities = [...activities];
3885
- newActivities.splice(index, 1);
3886
- return { changed: true, activities: newActivities };
3887
- }
3888
- else {
3889
- return { changed: false, activities };
3906
+ const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/submit_action', undefined, undefined, body, 'application/json');
3907
+ decoders.SubmitActionResponse?.(response.body);
3908
+ return { ...response.body, metadata: response.metadata };
3890
3909
  }
3891
- };
3910
+ }
3892
3911
 
3893
- const updateActivityInActivities$1 = (updatedActivity, activities) => {
3894
- const index = activities.findIndex((a) => a.id === updatedActivity.id);
3895
- if (index !== -1) {
3896
- const newActivities = [...activities];
3897
- newActivities[index] = updatedActivity;
3898
- return { changed: true, activities: newActivities };
3899
- }
3900
- else {
3901
- return { changed: false, activities };
3902
- }
3903
- };
3904
- const addReactionToActivity = (event, activity, isCurrentUser) => {
3905
- // Update own_reactions if the reaction is from the current user
3906
- const ownReactions = [...(activity.own_reactions || [])];
3907
- if (isCurrentUser) {
3908
- ownReactions.push(event.reaction);
3909
- }
3910
- return {
3911
- ...activity,
3912
- own_reactions: ownReactions,
3913
- latest_reactions: event.activity.latest_reactions,
3914
- reaction_groups: event.activity.reaction_groups,
3915
- changed: true,
3916
- };
3917
- };
3918
- const removeReactionFromActivity = (event, activity, isCurrentUser) => {
3919
- // Update own_reactions if the reaction is from the current user
3920
- const ownReactions = isCurrentUser
3921
- ? (activity.own_reactions || []).filter((r) => !(r.type === event.reaction.type &&
3922
- r.user.id === event.reaction.user.id))
3923
- : activity.own_reactions;
3924
- return {
3925
- ...activity,
3926
- own_reactions: ownReactions,
3927
- latest_reactions: event.activity.latest_reactions,
3928
- reaction_groups: event.activity.reaction_groups,
3929
- changed: true,
3930
- };
3931
- };
3932
- const addReactionToActivities = (event, activities, isCurrentUser) => {
3933
- if (!activities) {
3934
- return { changed: false, activities: [] };
3935
- }
3936
- const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
3937
- if (activityIndex === -1) {
3938
- return { changed: false, activities };
3939
- }
3940
- const activity = activities[activityIndex];
3941
- const updatedActivity = addReactionToActivity(event, activity, isCurrentUser);
3942
- return updateActivityInActivities$1(updatedActivity, activities);
3943
- };
3944
- const removeReactionFromActivities = (event, activities, isCurrentUser) => {
3945
- if (!activities) {
3946
- return { changed: false, activities: [] };
3947
- }
3948
- const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
3949
- if (activityIndex === -1) {
3950
- return { changed: false, activities };
3951
- }
3952
- const activity = activities[activityIndex];
3953
- const updatedActivity = removeReactionFromActivity(event, activity, isCurrentUser);
3954
- return updateActivityInActivities$1(updatedActivity, activities);
3955
- };
3912
+ class ModerationClient extends ModerationApi {
3913
+ }
3956
3914
 
3957
- // Helper function to check if two bookmarks are the same
3958
- // A bookmark is identified by activity_id + folder_id + user_id
3959
- const isSameBookmark = (bookmark1, bookmark2) => {
3960
- return (bookmark1.user.id === bookmark2.user.id &&
3961
- bookmark1.activity.id === bookmark2.activity.id &&
3962
- bookmark1.folder?.id === bookmark2.folder?.id);
3963
- };
3964
- const updateActivityInActivities = (updatedActivity, activities) => {
3965
- const index = activities.findIndex((a) => a.id === updatedActivity.id);
3966
- if (index !== -1) {
3967
- const newActivities = [...activities];
3968
- newActivities[index] = updatedActivity;
3969
- return { changed: true, activities: newActivities };
3970
- }
3971
- else {
3972
- return { changed: false, activities };
3973
- }
3974
- };
3975
- const addBookmarkToActivity = (event, activity, isCurrentUser) => {
3976
- // Update own_bookmarks if the bookmark is from the current user
3977
- const ownBookmarks = [...(activity.own_bookmarks || [])];
3978
- if (isCurrentUser) {
3979
- ownBookmarks.push(event.bookmark);
3980
- }
3981
- return {
3982
- ...activity,
3983
- own_bookmarks: ownBookmarks,
3984
- changed: true,
3985
- };
3986
- };
3987
- const removeBookmarkFromActivity = (event, activity, isCurrentUser) => {
3988
- // Update own_bookmarks if the bookmark is from the current user
3989
- const ownBookmarks = isCurrentUser
3990
- ? (activity.own_bookmarks || []).filter((bookmark) => !isSameBookmark(bookmark, event.bookmark))
3991
- : activity.own_bookmarks;
3992
- return {
3993
- ...activity,
3994
- own_bookmarks: ownBookmarks,
3995
- changed: true,
3996
- };
3997
- };
3998
- const updateBookmarkInActivity = (event, activity, isCurrentUser) => {
3999
- // Update own_bookmarks if the bookmark is from the current user
4000
- let ownBookmarks = activity.own_bookmarks || [];
4001
- if (isCurrentUser) {
4002
- const bookmarkIndex = ownBookmarks.findIndex((bookmark) => isSameBookmark(bookmark, event.bookmark));
4003
- if (bookmarkIndex !== -1) {
4004
- ownBookmarks = [...ownBookmarks];
4005
- ownBookmarks[bookmarkIndex] = event.bookmark;
4006
- }
4007
- }
4008
- return {
4009
- ...activity,
4010
- own_bookmarks: ownBookmarks,
4011
- changed: true,
4012
- };
4013
- };
4014
- const addBookmarkToActivities = (event, activities, isCurrentUser) => {
4015
- if (!activities) {
4016
- return { changed: false, activities: [] };
4017
- }
4018
- const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4019
- if (activityIndex === -1) {
4020
- return { changed: false, activities };
4021
- }
4022
- const activity = activities[activityIndex];
4023
- const updatedActivity = addBookmarkToActivity(event, activity, isCurrentUser);
4024
- return updateActivityInActivities(updatedActivity, activities);
4025
- };
4026
- const removeBookmarkFromActivities = (event, activities, isCurrentUser) => {
4027
- if (!activities) {
4028
- return { changed: false, activities: [] };
4029
- }
4030
- const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4031
- if (activityIndex === -1) {
4032
- return { changed: false, activities };
4033
- }
4034
- const activity = activities[activityIndex];
4035
- const updatedActivity = removeBookmarkFromActivity(event, activity, isCurrentUser);
4036
- return updateActivityInActivities(updatedActivity, activities);
4037
- };
4038
- const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4039
- if (!activities) {
4040
- return { changed: false, activities: [] };
4041
- }
4042
- const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4043
- if (activityIndex === -1) {
4044
- return { changed: false, activities };
4045
- }
4046
- const activity = activities[activityIndex];
4047
- const updatedActivity = updateBookmarkInActivity(event, activity, isCurrentUser);
4048
- return updateActivityInActivities(updatedActivity, activities);
4049
- };
4050
-
4051
- const isFeedResponse = (follow) => {
4052
- return 'created_by' in follow;
4053
- };
4054
- const handleFollowCreated = (follow, currentState, currentFeedId, connectedUserId) => {
4055
- // filter non-accepted follows (the way getOrCreate does by default)
4056
- if (follow.status !== 'accepted') {
4057
- return { changed: false, data: currentState };
4058
- }
4059
- let newState = { ...currentState };
4060
- // this feed followed someone
4061
- if (follow.source_feed.fid === currentFeedId) {
4062
- newState = {
4063
- ...newState,
4064
- // Update FeedResponse fields, that has the new follower/following count
4065
- ...follow.source_feed,
3915
+ const isPollUpdatedEvent = (e) => e.type === 'feeds.poll.updated';
3916
+ const isPollClosedEventEvent = (e) => e.type === 'feeds.poll.closed';
3917
+ const isPollVoteCastedEvent = (e) => e.type === 'feeds.poll.vote_casted';
3918
+ const isPollVoteChangedEvent = (e) => e.type === 'feeds.poll.vote_changed';
3919
+ const isPollVoteRemovedEvent = (e) => e.type === 'feeds.poll.vote_removed';
3920
+ const isVoteAnswer = (vote) => !!vote?.answer_text;
3921
+ class StreamPoll {
3922
+ constructor({ client, poll }) {
3923
+ this.getInitialStateFromPollResponse = (poll) => {
3924
+ const { own_votes, id, ...pollResponseForState } = poll;
3925
+ const { ownAnswer, ownVotes } = own_votes?.reduce((acc, voteOrAnswer) => {
3926
+ if (isVoteAnswer(voteOrAnswer)) {
3927
+ acc.ownAnswer = voteOrAnswer;
3928
+ }
3929
+ else {
3930
+ acc.ownVotes.push(voteOrAnswer);
3931
+ }
3932
+ return acc;
3933
+ }, { ownVotes: [] }) ?? { ownVotes: [] };
3934
+ return {
3935
+ ...pollResponseForState,
3936
+ last_activity_at: new Date(),
3937
+ max_voted_option_ids: getMaxVotedOptionIds(pollResponseForState.vote_counts_by_option),
3938
+ own_answer: ownAnswer,
3939
+ own_votes_by_option_id: getOwnVotesByOptionId(ownVotes),
3940
+ };
4066
3941
  };
4067
- // Only update if following array already exists
4068
- if (currentState.following !== undefined) {
4069
- newState.following = [follow, ...currentState.following];
4070
- }
4071
- }
4072
- else if (
4073
- // someone followed this feed
4074
- follow.target_feed.fid === currentFeedId) {
4075
- const source = follow.source_feed;
4076
- newState = {
4077
- ...newState,
4078
- // Update FeedResponse fields, that has the new follower/following count
4079
- ...follow.target_feed,
3942
+ this.reinitializeState = (poll) => {
3943
+ this.state.partialNext(this.getInitialStateFromPollResponse(poll));
4080
3944
  };
4081
- if (source.created_by.id === connectedUserId) {
4082
- newState.own_follows = currentState.own_follows
4083
- ? currentState.own_follows.concat(follow)
4084
- : [follow];
4085
- }
4086
- // Only update if followers array already exists
4087
- if (currentState.followers !== undefined) {
4088
- newState.followers = [follow, ...currentState.followers];
4089
- }
4090
- }
4091
- return { changed: true, data: newState };
4092
- };
4093
- const handleFollowDeleted = (follow, currentState, currentFeedId, connectedUserId) => {
4094
- let newState = { ...currentState };
4095
- // this feed unfollowed someone
4096
- if (follow.source_feed.fid === currentFeedId) {
4097
- newState = {
4098
- ...newState,
4099
- // Update FeedResponse fields, that has the new follower/following count
4100
- ...follow.source_feed,
3945
+ this.handlePollUpdated = (event) => {
3946
+ if (event.poll?.id && event.poll.id !== this.id)
3947
+ return;
3948
+ if (!isPollUpdatedEvent(event))
3949
+ return;
3950
+ const { id, ...pollData } = event.poll;
3951
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3952
+ this.state.partialNext({
3953
+ ...pollData,
3954
+ last_activity_at: new Date(event.created_at),
3955
+ });
4101
3956
  };
4102
- // Only update if following array already exists
4103
- if (currentState.following !== undefined) {
4104
- newState.following = currentState.following.filter((followItem) => followItem.target_feed.fid !== follow.target_feed.fid);
4105
- }
4106
- }
4107
- else if (
4108
- // someone unfollowed this feed
4109
- follow.target_feed.fid === currentFeedId) {
4110
- const source = follow.source_feed;
4111
- newState = {
4112
- ...newState,
4113
- // Update FeedResponse fields, that has the new follower/following count
4114
- ...follow.target_feed,
3957
+ this.handlePollClosed = (event) => {
3958
+ if (event.poll?.id && event.poll.id !== this.id)
3959
+ return;
3960
+ if (!isPollClosedEventEvent(event))
3961
+ return;
3962
+ this.state.partialNext({
3963
+ is_closed: true,
3964
+ last_activity_at: new Date(event.created_at),
3965
+ });
4115
3966
  };
4116
- if (isFeedResponse(source) &&
4117
- source.created_by.id === connectedUserId &&
4118
- currentState.own_follows !== undefined) {
4119
- newState.own_follows = currentState.own_follows.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4120
- }
4121
- // Only update if followers array already exists
4122
- if (currentState.followers !== undefined) {
4123
- newState.followers = currentState.followers.filter((followItem) => followItem.source_feed.fid !== follow.source_feed.fid);
4124
- }
4125
- }
4126
- return { changed: true, data: newState };
4127
- };
4128
- const handleFollowUpdated = (currentState) => {
4129
- // For now, we'll treat follow updates as no-ops since the current implementation does
4130
- // This can be enhanced later if needed
4131
- return { changed: false, data: currentState };
4132
- };
4133
-
4134
- const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4135
- typeof cursor === 'string';
4136
- const isCommentResponse = (entity) => {
4137
- return typeof entity?.object_id === 'string';
4138
- };
4139
- const Constants = {
4140
- DEFAULT_COMMENT_PAGINATION: 'first',
4141
- };
4142
- const uniqueArrayMerge = (existingArray, arrayToMerge, getKey) => {
4143
- const existing = new Set();
4144
- existingArray.forEach((value) => {
4145
- const key = getKey(value);
4146
- existing.add(key);
4147
- });
4148
- const filteredArrayToMerge = arrayToMerge.filter((value) => {
4149
- const key = getKey(value);
4150
- return !existing.has(key);
4151
- });
4152
- return existingArray.concat(filteredArrayToMerge);
4153
- };
4154
-
4155
- const shouldUpdateState = ({ stateUpdateId, stateUpdateQueue, watch, }) => {
4156
- if (!watch) {
4157
- return true;
4158
- }
4159
- if (watch && stateUpdateQueue.has(stateUpdateId)) {
4160
- stateUpdateQueue.delete(stateUpdateId);
4161
- return false;
4162
- }
4163
- stateUpdateQueue.add(stateUpdateId);
4164
- return true;
4165
- };
4166
- const getStateUpdateQueueIdForFollow = (follow) => {
4167
- return `follow${follow.source_feed.fid}-${follow.target_feed.fid}`;
4168
- };
4169
- const getStateUpdateQueueIdForUnfollow = (follow) => {
4170
- return `unfollow${follow.source_feed.fid}-${follow.target_feed.fid}`;
4171
- };
4172
-
4173
- class Feed extends FeedApi {
4174
- constructor(client, groupId, id, data, watch = false) {
4175
- super(client, groupId, id);
4176
- this.stateUpdateQueue = new Set();
4177
- this.eventHandlers = {
4178
- 'feeds.activity.added': (event) => {
4179
- const currentActivities = this.currentState.activities;
4180
- const result = addActivitiesToState([event.activity], currentActivities, 'start');
4181
- if (result.changed) {
4182
- this.client.hydratePollCache([event.activity]);
4183
- this.state.partialNext({ activities: result.activities });
4184
- }
4185
- },
4186
- 'feeds.activity.deleted': (event) => {
4187
- const currentActivities = this.currentState.activities;
4188
- if (currentActivities) {
4189
- const result = removeActivityFromState(event.activity, currentActivities);
4190
- if (result.changed) {
4191
- this.state.partialNext({ activities: result.activities });
4192
- }
3967
+ this.handleVoteCasted = (event) => {
3968
+ if (event.poll?.id && event.poll.id !== this.id)
3969
+ return;
3970
+ if (!isPollVoteCastedEvent(event))
3971
+ return;
3972
+ const currentState = this.data;
3973
+ const isOwnVote = event.poll_vote.user_id ===
3974
+ this.client.state.getLatestValue().connected_user?.id;
3975
+ let latestAnswers = [...currentState.latest_answers];
3976
+ let ownAnswer = currentState.own_answer;
3977
+ const ownVotesByOptionId = currentState.own_votes_by_option_id;
3978
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
3979
+ if (isOwnVote) {
3980
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3981
+ if (isVoteAnswer(event.poll_vote)) {
3982
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3983
+ ownAnswer = event.poll_vote;
4193
3984
  }
4194
- },
4195
- 'feeds.activity.reaction.added': (event) => {
4196
- const currentActivities = this.currentState.activities;
4197
- const connectedUser = this.client.state.getLatestValue().connected_user;
4198
- const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4199
- const result = addReactionToActivities(event, currentActivities, isCurrentUser);
4200
- if (result.changed) {
4201
- this.state.partialNext({ activities: result.activities });
3985
+ else if (event.poll_vote.option_id) {
3986
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3987
+ ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
4202
3988
  }
4203
- },
4204
- 'feeds.activity.reaction.deleted': (event) => {
4205
- const currentActivities = this.currentState.activities;
4206
- const connectedUser = this.client.state.getLatestValue().connected_user;
4207
- const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4208
- const result = removeReactionFromActivities(event, currentActivities, isCurrentUser);
4209
- if (result.changed) {
4210
- this.state.partialNext({ activities: result.activities });
3989
+ }
3990
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3991
+ if (isVoteAnswer(event.poll_vote)) {
3992
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
3993
+ latestAnswers = [event.poll_vote, ...latestAnswers];
3994
+ }
3995
+ else {
3996
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
3997
+ }
3998
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
3999
+ this.state.partialNext({
4000
+ answers_count,
4001
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4002
+ latest_votes_by_option,
4003
+ vote_count,
4004
+ vote_counts_by_option,
4005
+ latest_answers: latestAnswers,
4006
+ last_activity_at: new Date(event.created_at),
4007
+ own_answer: ownAnswer,
4008
+ own_votes_by_option_id: ownVotesByOptionId,
4009
+ max_voted_option_ids: maxVotedOptionIds,
4010
+ });
4011
+ };
4012
+ this.handleVoteChanged = (event) => {
4013
+ // this event is triggered only when event.poll.enforce_unique_vote === true
4014
+ if (event.poll?.id && event.poll.id !== this.id)
4015
+ return;
4016
+ if (!isPollVoteChangedEvent(event))
4017
+ return;
4018
+ const currentState = this.data;
4019
+ const isOwnVote = event.poll_vote.user_id ===
4020
+ this.client.state.getLatestValue().connected_user?.id;
4021
+ let latestAnswers = [...currentState.latest_answers];
4022
+ let ownAnswer = currentState.own_answer;
4023
+ let ownVotesByOptionId = currentState.own_votes_by_option_id;
4024
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
4025
+ if (isOwnVote) {
4026
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4027
+ if (isVoteAnswer(event.poll_vote)) {
4028
+ latestAnswers = [
4029
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4030
+ event.poll_vote,
4031
+ ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id),
4032
+ ];
4033
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4034
+ ownAnswer = event.poll_vote;
4211
4035
  }
4212
- },
4213
- 'feeds.activity.reaction.updated': Feed.noop,
4214
- 'feeds.activity.removed_from_feed': (event) => {
4215
- const currentActivities = this.currentState.activities;
4216
- if (currentActivities) {
4217
- const result = removeActivityFromState(event.activity, currentActivities);
4218
- if (result.changed) {
4219
- this.state.partialNext({ activities: result.activities });
4036
+ else if (event.poll_vote.option_id) {
4037
+ if (event.poll.enforce_unique_vote) {
4038
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4039
+ ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
4220
4040
  }
4221
- }
4222
- },
4223
- 'feeds.activity.updated': (event) => {
4224
- const currentActivities = this.currentState.activities;
4225
- if (currentActivities) {
4226
- const result = updateActivityInState(event.activity, currentActivities);
4227
- if (result.changed) {
4228
- this.client.hydratePollCache([event.activity]);
4229
- this.state.partialNext({ activities: result.activities });
4041
+ else {
4042
+ ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce((acc, [optionId, vote]) => {
4043
+ if (optionId !== event.poll_vote.option_id &&
4044
+ vote.id === event.poll_vote.id) {
4045
+ return acc;
4046
+ }
4047
+ acc[optionId] = vote;
4048
+ return acc;
4049
+ }, {});
4050
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4051
+ ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
4052
+ }
4053
+ if (ownAnswer?.id === event.poll_vote.id) {
4054
+ ownAnswer = undefined;
4230
4055
  }
4056
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4231
4057
  }
4232
- },
4233
- 'feeds.bookmark.added': this.handleBookmarkAdded.bind(this),
4234
- 'feeds.bookmark.deleted': this.handleBookmarkDeleted.bind(this),
4235
- 'feeds.bookmark.updated': this.handleBookmarkUpdated.bind(this),
4236
- 'feeds.bookmark_folder.deleted': Feed.noop,
4237
- 'feeds.bookmark_folder.updated': Feed.noop,
4238
- 'feeds.comment.added': (event) => {
4239
- const { comment } = event;
4240
- const forId = comment.parent_id ?? comment.object_id;
4241
- this.state.next((currentState) => {
4242
- const entityState = currentState.comments_by_entity_id[forId];
4243
- const newComments = entityState?.comments?.concat([]) ?? [];
4244
- if (entityState?.pagination?.sort === 'last' &&
4245
- !checkHasAnotherPage(entityState.comments, entityState?.pagination.next)) {
4246
- newComments.unshift(comment);
4247
- }
4248
- else if (entityState?.pagination?.sort === 'first') {
4249
- newComments.push(comment);
4250
- }
4251
- else {
4252
- // no other sorting option is supported yet
4253
- return currentState;
4254
- }
4255
- return {
4256
- ...currentState,
4257
- comments_by_entity_id: {
4258
- ...currentState.comments_by_entity_id,
4259
- [forId]: {
4260
- ...currentState.comments_by_entity_id[forId],
4261
- comments: newComments,
4262
- },
4263
- },
4264
- };
4265
- });
4266
- },
4267
- 'feeds.comment.deleted': ({ comment }) => {
4268
- const forId = comment.parent_id ?? comment.object_id;
4269
- this.state.next((currentState) => {
4270
- const newCommentsByEntityId = {
4271
- ...currentState.comments_by_entity_id,
4272
- [forId]: {
4273
- ...currentState.comments_by_entity_id[forId],
4274
- },
4275
- };
4276
- const index = this.getCommentIndex(comment, currentState);
4277
- if (newCommentsByEntityId?.[forId]?.comments?.length && index !== -1) {
4278
- newCommentsByEntityId[forId].comments = [
4279
- ...newCommentsByEntityId[forId].comments,
4280
- ];
4281
- newCommentsByEntityId[forId]?.comments?.splice(index, 1);
4282
- }
4283
- delete newCommentsByEntityId[comment.id];
4284
- return {
4285
- ...currentState,
4286
- comments_by_entity_id: newCommentsByEntityId,
4287
- };
4288
- });
4289
- },
4290
- 'feeds.comment.updated': (event) => {
4291
- const { comment } = event;
4292
- const forId = comment.parent_id ?? comment.object_id;
4293
- this.state.next((currentState) => {
4294
- const entityState = currentState.comments_by_entity_id[forId];
4295
- if (!entityState?.comments?.length)
4296
- return currentState;
4297
- const index = this.getCommentIndex(comment, currentState);
4298
- if (index === -1)
4299
- return currentState;
4300
- const newComments = [...entityState.comments];
4301
- newComments[index] = comment;
4302
- return {
4303
- ...currentState,
4304
- comments_by_entity_id: {
4305
- ...currentState.comments_by_entity_id,
4306
- [forId]: {
4307
- ...currentState.comments_by_entity_id[forId],
4308
- comments: newComments,
4309
- },
4310
- },
4311
- };
4312
- });
4313
- },
4314
- 'feeds.feed.created': Feed.noop,
4315
- 'feeds.feed.deleted': Feed.noop,
4316
- 'feeds.feed.updated': (event) => {
4317
- this.state.partialNext({ ...event.feed });
4318
- },
4319
- 'feeds.feed_group.changed': Feed.noop,
4320
- 'feeds.feed_group.deleted': Feed.noop,
4321
- 'feeds.follow.created': (event) => {
4322
- this.handleFollowCreated(event.follow);
4323
- },
4324
- 'feeds.follow.deleted': (event) => {
4325
- this.handleFollowDeleted(event.follow);
4326
- },
4327
- 'feeds.follow.updated': (_event) => {
4328
- handleFollowUpdated(this.currentState);
4329
- },
4330
- 'feeds.comment.reaction.added': this.handleCommentReactionEvent.bind(this),
4331
- 'feeds.comment.reaction.deleted': this.handleCommentReactionEvent.bind(this),
4332
- 'feeds.comment.reaction.updated': Feed.noop,
4333
- 'feeds.feed_member.added': (event) => {
4334
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4335
- this.state.next((currentState) => {
4336
- let newState;
4337
- if (typeof currentState.members !== 'undefined') {
4338
- newState ?? (newState = {
4339
- ...currentState,
4340
- });
4341
- newState.members = [event.member, ...currentState.members];
4342
- }
4343
- if (connectedUser?.id === event.member.user.id) {
4344
- newState ?? (newState = {
4345
- ...currentState,
4346
- });
4347
- newState.own_membership = event.member;
4348
- }
4349
- return newState ?? currentState;
4350
- });
4351
- },
4352
- 'feeds.feed_member.removed': (event) => {
4353
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4354
- this.state.next((currentState) => {
4355
- const newState = {
4356
- ...currentState,
4357
- members: currentState.members?.filter((member) => member.user.id !== event.user?.id),
4358
- };
4359
- if (connectedUser?.id === event.member_id) {
4360
- delete newState.own_membership;
4361
- }
4362
- return newState;
4363
- });
4364
- },
4365
- 'feeds.feed_member.updated': (event) => {
4366
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4367
- this.state.next((currentState) => {
4368
- const memberIndex = currentState.members?.findIndex((member) => member.user.id === event.member.user.id) ?? -1;
4369
- let newState;
4370
- if (memberIndex !== -1) {
4371
- // if there's an index, there's a member to update
4372
- const newMembers = [...currentState.members];
4373
- newMembers[memberIndex] = event.member;
4374
- newState ?? (newState = {
4375
- ...currentState,
4376
- });
4377
- newState.members = newMembers;
4378
- }
4379
- if (connectedUser?.id === event.member.user.id) {
4380
- newState ?? (newState = {
4381
- ...currentState,
4382
- });
4383
- newState.own_membership = event.member;
4384
- }
4385
- return newState ?? currentState;
4386
- });
4387
- },
4388
- 'feeds.notification_feed.updated': (event) => {
4389
- console.info('notification feed updated', event);
4390
- // TODO: handle notification feed updates
4391
- },
4392
- // the poll events should be removed from here
4393
- 'feeds.poll.closed': Feed.noop,
4394
- 'feeds.poll.deleted': Feed.noop,
4395
- 'feeds.poll.updated': Feed.noop,
4396
- 'feeds.poll.vote_casted': Feed.noop,
4397
- 'feeds.poll.vote_changed': Feed.noop,
4398
- 'feeds.poll.vote_removed': Feed.noop,
4399
- 'feeds.activity.pinned': Feed.noop,
4400
- 'feeds.activity.unpinned': Feed.noop,
4401
- 'feeds.activity.marked': Feed.noop,
4402
- 'moderation.custom_action': Feed.noop,
4403
- 'moderation.flagged': Feed.noop,
4404
- 'moderation.mark_reviewed': Feed.noop,
4405
- 'health.check': Feed.noop,
4406
- 'app.updated': Feed.noop,
4407
- 'user.banned': Feed.noop,
4408
- 'user.deactivated': Feed.noop,
4409
- 'user.muted': Feed.noop,
4410
- 'user.reactivated': Feed.noop,
4411
- 'user.updated': Feed.noop,
4412
- };
4413
- this.eventDispatcher = new EventDispatcher();
4414
- this.on = this.eventDispatcher.on;
4415
- this.off = this.eventDispatcher.off;
4416
- this.state = new StateStore({
4417
- fid: `${groupId}:${id}`,
4418
- group_id: groupId,
4419
- id,
4420
- ...(data ?? {}),
4421
- is_loading: false,
4422
- is_loading_activities: false,
4423
- comments_by_entity_id: {},
4424
- watch,
4425
- });
4426
- this.client = client;
4427
- }
4428
- get fid() {
4429
- return `${this.group}:${this.id}`;
4430
- }
4431
- get currentState() {
4432
- return this.state.getLatestValue();
4433
- }
4434
- handleCommentReactionEvent(event) {
4435
- const { comment, reaction } = event;
4436
- const connectedUser = this.client.state.getLatestValue().connected_user;
4437
- this.state.next((currentState) => {
4438
- const forId = comment.parent_id ?? comment.object_id;
4439
- const entityState = currentState.comments_by_entity_id[forId];
4440
- const commentIndex = this.getCommentIndex(comment, currentState);
4441
- if (commentIndex === -1)
4442
- return currentState;
4443
- const newComments = entityState?.comments?.concat([]) ?? [];
4444
- const commentCopy = { ...comment };
4445
- delete commentCopy.own_reactions;
4446
- const newComment = {
4447
- ...newComments[commentIndex],
4448
- ...commentCopy,
4449
- // TODO: FIXME this should be handled by the backend
4450
- latest_reactions: commentCopy.latest_reactions ?? [],
4451
- reaction_groups: commentCopy.reaction_groups ?? {},
4452
- };
4453
- newComments[commentIndex] = newComment;
4454
- if (reaction.user.id === connectedUser?.id) {
4455
- if (event.type === 'feeds.comment.reaction.added') {
4456
- newComment.own_reactions = newComment.own_reactions.concat(reaction) ?? [reaction];
4457
- }
4458
- else if (event.type === 'feeds.comment.reaction.deleted') {
4459
- newComment.own_reactions = newComment.own_reactions.filter((r) => r.type !== reaction.type);
4460
- }
4058
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4461
4059
  }
4462
- return {
4463
- ...currentState,
4464
- comments_by_entity_id: {
4465
- ...currentState.comments_by_entity_id,
4466
- [forId]: {
4467
- ...entityState,
4468
- comments: newComments,
4469
- },
4470
- },
4471
- };
4472
- });
4473
- }
4474
- async synchronize() {
4475
- const { last_get_or_create_request_config } = this.state.getLatestValue();
4476
- if (last_get_or_create_request_config?.watch) {
4477
- await this.getOrCreate(last_get_or_create_request_config);
4478
- }
4479
- }
4480
- async getOrCreate(request) {
4481
- if (this.currentState.is_loading_activities) {
4482
- throw new Error('Only one getOrCreate call is allowed at a time');
4483
- }
4484
- this.state.partialNext({
4485
- is_loading: !request?.next,
4486
- is_loading_activities: true,
4487
- });
4488
- // TODO: pull comments/comment_pagination from activities and comment_sort from request
4489
- // and pre-populate comments_by_entity_id (once comment_sort and comment_limit are supported)
4490
- try {
4491
- const response = await super.getOrCreate(request);
4492
- if (request?.next) {
4493
- const { activities: currentActivities = [] } = this.currentState;
4494
- const result = addActivitiesToState(response.activities, currentActivities, 'end');
4495
- if (result.changed) {
4496
- this.state.partialNext({
4497
- activities: result.activities,
4498
- next: response.next,
4499
- prev: response.prev,
4500
- });
4060
+ else if (isVoteAnswer(event.poll_vote)) {
4061
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4062
+ latestAnswers = [event.poll_vote, ...latestAnswers];
4063
+ }
4064
+ else {
4065
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4066
+ }
4067
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
4068
+ this.state.partialNext({
4069
+ answers_count,
4070
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4071
+ latest_votes_by_option,
4072
+ vote_count,
4073
+ vote_counts_by_option,
4074
+ latest_answers: latestAnswers,
4075
+ last_activity_at: new Date(event.created_at),
4076
+ own_answer: ownAnswer,
4077
+ own_votes_by_option_id: ownVotesByOptionId,
4078
+ max_voted_option_ids: maxVotedOptionIds,
4079
+ });
4080
+ };
4081
+ this.handleVoteRemoved = (event) => {
4082
+ if (event.poll?.id && event.poll.id !== this.id)
4083
+ return;
4084
+ if (!isPollVoteRemovedEvent(event))
4085
+ return;
4086
+ const currentState = this.data;
4087
+ const isOwnVote = event.poll_vote.user_id ===
4088
+ this.client.state.getLatestValue().connected_user?.id;
4089
+ let latestAnswers = [...currentState.latest_answers];
4090
+ let ownAnswer = currentState.own_answer;
4091
+ const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
4092
+ let maxVotedOptionIds = currentState.max_voted_option_ids;
4093
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4094
+ if (isVoteAnswer(event.poll_vote)) {
4095
+ latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
4096
+ if (isOwnVote) {
4097
+ ownAnswer = undefined;
4501
4098
  }
4502
4099
  }
4503
4100
  else {
4504
- // Empty queue when reinitializing the state
4505
- this.stateUpdateQueue.clear();
4506
- const responseCopy = {
4507
- ...response,
4508
- ...response.feed,
4509
- };
4510
- delete responseCopy.feed;
4511
- delete responseCopy.metadata;
4512
- delete responseCopy.duration;
4513
- this.state.next((currentState) => {
4514
- const nextState = {
4515
- ...currentState,
4516
- ...responseCopy,
4517
- };
4518
- if (!request?.followers_pagination?.limit) {
4519
- delete nextState.followers;
4520
- }
4521
- if (!request?.following_pagination?.limit) {
4522
- delete nextState.following;
4523
- }
4524
- if (response.members.length === 0 && response.feed.member_count > 0) {
4525
- delete nextState.members;
4526
- }
4527
- nextState.last_get_or_create_request_config = request;
4528
- nextState.watch = request?.watch ? request.watch : currentState.watch;
4529
- return nextState;
4530
- });
4101
+ maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
4102
+ if (isOwnVote && event.poll_vote.option_id) {
4103
+ delete ownVotesByOptionId[event.poll_vote.option_id];
4104
+ }
4531
4105
  }
4532
- this.client.hydratePollCache(response.activities);
4533
- return response;
4534
- }
4535
- finally {
4106
+ const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
4536
4107
  this.state.partialNext({
4537
- is_loading: false,
4538
- is_loading_activities: false,
4108
+ answers_count,
4109
+ // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
4110
+ latest_votes_by_option,
4111
+ vote_count,
4112
+ vote_counts_by_option,
4113
+ latest_answers: latestAnswers,
4114
+ last_activity_at: new Date(event.created_at),
4115
+ own_answer: ownAnswer,
4116
+ own_votes_by_option_id: ownVotesByOptionId,
4117
+ max_voted_option_ids: maxVotedOptionIds,
4539
4118
  });
4540
- }
4119
+ };
4120
+ this.client = client;
4121
+ this.id = poll.id;
4122
+ this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
4541
4123
  }
4542
- /**
4543
- * @internal
4544
- */
4545
- handleFollowCreated(follow) {
4546
- if (!shouldUpdateState({
4547
- stateUpdateId: getStateUpdateQueueIdForFollow(follow),
4548
- stateUpdateQueue: this.stateUpdateQueue,
4549
- watch: this.currentState.watch,
4550
- })) {
4551
- return;
4552
- }
4553
- const connectedUser = this.client.state.getLatestValue().connected_user;
4554
- const result = handleFollowCreated(follow, this.currentState, this.fid, connectedUser?.id);
4555
- if (result.changed) {
4556
- this.state.next(result.data);
4557
- }
4124
+ get data() {
4125
+ return this.state.getLatestValue();
4558
4126
  }
4559
- /**
4560
- * @internal
4561
- */
4562
- handleFollowDeleted(follow) {
4563
- if (!shouldUpdateState({
4564
- stateUpdateId: getStateUpdateQueueIdForUnfollow(follow),
4565
- stateUpdateQueue: this.stateUpdateQueue,
4566
- watch: this.currentState.watch,
4567
- })) {
4568
- return;
4127
+ }
4128
+ function getMaxVotedOptionIds(voteCountsByOption) {
4129
+ let maxVotes = 0;
4130
+ let winningOptions = [];
4131
+ for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
4132
+ if (count > maxVotes) {
4133
+ winningOptions = [id];
4134
+ maxVotes = count;
4569
4135
  }
4570
- const connectedUser = this.client.state.getLatestValue().connected_user;
4571
- const result = handleFollowDeleted(follow, this.currentState, this.fid, connectedUser?.id);
4572
- {
4573
- this.state.next(result.data);
4136
+ else if (count === maxVotes) {
4137
+ winningOptions.push(id);
4574
4138
  }
4575
4139
  }
4576
- /**
4577
- * @internal
4578
- */
4579
- handleWatchStopped() {
4580
- this.state.partialNext({
4581
- watch: false,
4582
- });
4583
- }
4584
- /**
4585
- * @internal
4586
- */
4587
- handleWatchStarted() {
4588
- this.state.partialNext({
4589
- watch: true,
4140
+ return winningOptions;
4141
+ }
4142
+ function getOwnVotesByOptionId(ownVotes) {
4143
+ return !ownVotes
4144
+ ? {}
4145
+ : ownVotes.reduce((acc, vote) => {
4146
+ if (isVoteAnswer(vote) || !vote.option_id)
4147
+ return acc;
4148
+ acc[vote.option_id] = vote;
4149
+ return acc;
4150
+ }, {});
4151
+ }
4152
+
4153
+ class FeedApi {
4154
+ constructor(feedsApi, group, id) {
4155
+ this.feedsApi = feedsApi;
4156
+ this.group = group;
4157
+ this.id = id;
4158
+ }
4159
+ delete(request) {
4160
+ return this.feedsApi.deleteFeed({
4161
+ feed_id: this.id,
4162
+ feed_group_id: this.group,
4163
+ ...request,
4590
4164
  });
4591
4165
  }
4592
- handleBookmarkAdded(event) {
4593
- const currentActivities = this.currentState.activities;
4594
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4595
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4596
- const result = addBookmarkToActivities(event, currentActivities, isCurrentUser);
4597
- if (result.changed) {
4598
- this.state.partialNext({ activities: result.activities });
4599
- }
4166
+ getOrCreate(request) {
4167
+ return this.feedsApi.getOrCreateFeed({
4168
+ feed_id: this.id,
4169
+ feed_group_id: this.group,
4170
+ ...request,
4171
+ });
4600
4172
  }
4601
- handleBookmarkDeleted(event) {
4602
- const currentActivities = this.currentState.activities;
4603
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4604
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4605
- const result = removeBookmarkFromActivities(event, currentActivities, isCurrentUser);
4606
- if (result.changed) {
4607
- this.state.partialNext({ activities: result.activities });
4608
- }
4173
+ update(request) {
4174
+ return this.feedsApi.updateFeed({
4175
+ feed_id: this.id,
4176
+ feed_group_id: this.group,
4177
+ ...request,
4178
+ });
4609
4179
  }
4610
- handleBookmarkUpdated(event) {
4611
- const currentActivities = this.currentState.activities;
4612
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
4613
- const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4614
- const result = updateBookmarkInActivities(event, currentActivities, isCurrentUser);
4615
- if (result.changed) {
4616
- this.state.partialNext({ activities: result.activities });
4617
- }
4180
+ markActivity(request) {
4181
+ return this.feedsApi.markActivity({
4182
+ feed_id: this.id,
4183
+ feed_group_id: this.group,
4184
+ ...request,
4185
+ });
4618
4186
  }
4619
- /**
4620
- * Returns index of the provided comment object.
4621
- */
4622
- getCommentIndex(comment, state) {
4623
- const { comments_by_entity_id = {} } = state ?? this.currentState;
4624
- const currentComments = comments_by_entity_id[comment.parent_id ?? comment.object_id]?.comments;
4625
- if (!currentComments?.length) {
4626
- return -1;
4627
- }
4628
- // @ts-expect-error this will just fail if the comment is not object from state
4629
- let commentIndex = currentComments.indexOf(comment);
4630
- // fast lookup failed, try slower approach
4631
- if (commentIndex === -1) {
4632
- commentIndex = currentComments.findIndex((comment_) => comment_.id === comment.id);
4633
- }
4634
- return commentIndex;
4187
+ unpinActivity(request) {
4188
+ return this.feedsApi.unpinActivity({
4189
+ feed_id: this.id,
4190
+ feed_group_id: this.group,
4191
+ ...request,
4192
+ });
4635
4193
  }
4636
- /**
4637
- * Load child comments of entity (activity or comment) into the state, if the target entity is comment,
4638
- * `entityParentId` should be provided (`CommentResponse.parent_id ?? CommentResponse.object_id`).
4639
- */
4640
- loadCommentsIntoState(data) {
4641
- // add initial (top level) object for processing
4642
- const traverseArray = [
4643
- {
4644
- entityId: data.entityId,
4645
- entityParentId: data.entityParentId,
4646
- comments: data.comments,
4647
- next: data.next,
4648
- },
4649
- ];
4650
- this.state.next((currentState) => {
4651
- const newCommentsByEntityId = {
4652
- ...currentState.comments_by_entity_id,
4653
- };
4654
- while (traverseArray.length) {
4655
- const item = traverseArray.pop();
4656
- const entityId = item.entityId;
4657
- // go over entity comments and generate new objects
4658
- // for further processing if there are any replies
4659
- item.comments.forEach((comment) => {
4660
- if (!comment.replies?.length)
4661
- return;
4662
- traverseArray.push({
4663
- entityId: comment.id,
4664
- entityParentId: entityId,
4665
- comments: comment.replies,
4666
- next: comment.meta?.next_cursor,
4667
- });
4668
- });
4669
- // omit replies & meta from the comments (transform ThreadedCommentResponse to CommentResponse)
4670
- // this is somehow faster than copying the whole
4671
- // object and deleting the desired properties
4672
- const newComments = item.comments.map(({ replies: _r, meta: _m, ...restOfTheCommentResponse }) => restOfTheCommentResponse);
4673
- newCommentsByEntityId[entityId] = {
4674
- ...newCommentsByEntityId[entityId],
4675
- entity_parent_id: item.entityParentId,
4676
- pagination: {
4677
- ...newCommentsByEntityId[entityId]?.pagination,
4678
- next: item.next,
4679
- sort: data.sort,
4680
- },
4681
- comments: newCommentsByEntityId[entityId]?.comments
4682
- ? newCommentsByEntityId[entityId].comments?.concat(newComments)
4683
- : newComments,
4684
- };
4685
- }
4686
- return {
4687
- ...currentState,
4688
- comments_by_entity_id: newCommentsByEntityId,
4689
- };
4194
+ pinActivity(request) {
4195
+ return this.feedsApi.pinActivity({
4196
+ feed_id: this.id,
4197
+ feed_group_id: this.group,
4198
+ ...request,
4690
4199
  });
4691
4200
  }
4692
- async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
4693
- let error;
4694
- try {
4695
- this.state.next((currentState) => ({
4696
- ...currentState,
4697
- comments_by_entity_id: {
4698
- ...currentState.comments_by_entity_id,
4699
- [entityId]: {
4700
- ...currentState.comments_by_entity_id[entityId],
4701
- pagination: {
4702
- ...currentState.comments_by_entity_id[entityId]?.pagination,
4703
- loading_next_page: true,
4704
- },
4705
- },
4706
- },
4707
- }));
4708
- const { next, comments } = await base();
4709
- this.loadCommentsIntoState({
4710
- entityId,
4711
- comments,
4712
- entityParentId,
4713
- next,
4714
- sort,
4715
- });
4716
- }
4717
- catch (e) {
4718
- error = e;
4719
- }
4720
- finally {
4721
- this.state.next((currentState) => ({
4722
- ...currentState,
4723
- comments_by_entity_id: {
4724
- ...currentState.comments_by_entity_id,
4725
- [entityId]: {
4726
- ...currentState.comments_by_entity_id[entityId],
4727
- pagination: {
4728
- ...currentState.comments_by_entity_id[entityId]?.pagination,
4729
- loading_next_page: false,
4730
- },
4731
- },
4732
- },
4733
- }));
4734
- }
4735
- if (error) {
4736
- throw error;
4737
- }
4201
+ updateFeedMembers(request) {
4202
+ return this.feedsApi.updateFeedMembers({
4203
+ feed_id: this.id,
4204
+ feed_group_id: this.group,
4205
+ ...request,
4206
+ });
4738
4207
  }
4739
- async loadNextPageActivityComments(activity, request) {
4740
- const currentEntityState = this.currentState.comments_by_entity_id[activity.id];
4741
- const currentPagination = currentEntityState?.pagination;
4742
- const currentNextCursor = currentPagination?.next;
4743
- const currentSort = currentPagination?.sort;
4744
- const isLoading = currentPagination?.loading_next_page;
4745
- const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4746
- if (isLoading ||
4747
- !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4748
- return;
4749
- }
4750
- await this.loadNextPageComments({
4751
- entityId: activity.id,
4752
- base: () => this.client.getComments({
4753
- ...request,
4754
- sort,
4755
- object_id: activity.id,
4756
- object_type: 'activity',
4757
- next: currentNextCursor,
4758
- }),
4759
- sort,
4208
+ acceptFeedMemberInvite(request) {
4209
+ return this.feedsApi.acceptFeedMemberInvite({
4210
+ feed_id: this.id,
4211
+ feed_group_id: this.group,
4212
+ ...request,
4760
4213
  });
4761
4214
  }
4762
- async loadNextPageCommentReplies(comment, request) {
4763
- const currentEntityState = this.currentState.comments_by_entity_id[comment.id];
4764
- const currentPagination = currentEntityState?.pagination;
4765
- const currentNextCursor = currentPagination?.next;
4766
- const currentSort = currentPagination?.sort;
4767
- const isLoading = currentPagination?.loading_next_page;
4768
- const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
4769
- if (isLoading ||
4770
- !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
4771
- return;
4772
- }
4773
- await this.loadNextPageComments({
4774
- entityId: comment.id,
4775
- base: () => this.client.getCommentReplies({
4776
- ...request,
4777
- comment_id: comment.id,
4778
- // use known sort first (prevents broken pagination)
4779
- sort: currentSort ??
4780
- request?.sort ??
4781
- Constants.DEFAULT_COMMENT_PAGINATION,
4782
- next: currentNextCursor,
4783
- }),
4784
- entityParentId: comment.parent_id ?? comment.object_id,
4785
- sort,
4215
+ queryFeedMembers(request) {
4216
+ return this.feedsApi.queryFeedMembers({
4217
+ feed_id: this.id,
4218
+ feed_group_id: this.group,
4219
+ ...request,
4786
4220
  });
4787
4221
  }
4788
- async loadNextPageFollows(type, request) {
4789
- const paginationKey = `${type}_pagination`;
4790
- const method = `query${capitalize(type)}`;
4791
- const currentFollows = this.currentState[type];
4792
- const currentNextCursor = this.currentState[paginationKey]?.next;
4793
- const isLoading = this.currentState[paginationKey]?.loading_next_page;
4794
- const sort = this.currentState[paginationKey]?.sort ?? request.sort;
4795
- let error;
4796
- if (isLoading || !checkHasAnotherPage(currentFollows, currentNextCursor)) {
4797
- return;
4798
- }
4799
- try {
4800
- this.state.next((currentState) => {
4801
- return {
4802
- ...currentState,
4803
- [paginationKey]: {
4804
- ...currentState[paginationKey],
4805
- loading_next_page: true,
4806
- },
4807
- };
4808
- });
4809
- const { next: newNextCursor, follows } = await this[method]({
4810
- ...request,
4811
- next: currentNextCursor,
4812
- sort,
4813
- });
4814
- this.state.next((currentState) => {
4815
- return {
4816
- ...currentState,
4817
- [type]: currentState[type] === undefined
4818
- ? follows
4819
- : uniqueArrayMerge(currentState[type], follows, (follow) => `${follow.source_feed.fid}-${follow.target_feed.fid}`),
4820
- [paginationKey]: {
4821
- ...currentState[paginationKey],
4822
- next: newNextCursor,
4823
- sort,
4824
- },
4825
- };
4826
- });
4222
+ rejectFeedMemberInvite(request) {
4223
+ return this.feedsApi.rejectFeedMemberInvite({
4224
+ feed_id: this.id,
4225
+ feed_group_id: this.group,
4226
+ ...request,
4227
+ });
4228
+ }
4229
+ stopWatching(request) {
4230
+ return this.feedsApi.stopWatchingFeed({
4231
+ feed_id: this.id,
4232
+ feed_group_id: this.group,
4233
+ ...request,
4234
+ });
4235
+ }
4236
+ }
4237
+
4238
+ const checkHasAnotherPage = (v, cursor) => (typeof v === 'undefined' && typeof cursor === 'undefined') ||
4239
+ typeof cursor === 'string';
4240
+
4241
+ const uniqueArrayMerge = (existingArray, arrayToMerge, getKey) => {
4242
+ const existing = new Set();
4243
+ existingArray.forEach((value) => {
4244
+ const key = getKey(value);
4245
+ existing.add(key);
4246
+ });
4247
+ const filteredArrayToMerge = arrayToMerge.filter((value) => {
4248
+ const key = getKey(value);
4249
+ return !existing.has(key);
4250
+ });
4251
+ return existingArray.concat(filteredArrayToMerge);
4252
+ };
4253
+
4254
+ const Constants = {
4255
+ DEFAULT_COMMENT_PAGINATION: 'first',
4256
+ };
4257
+
4258
+ const isFollowResponse = (data) => {
4259
+ return 'source_feed' in data && 'target_feed' in data;
4260
+ };
4261
+ const isCommentResponse = (entity) => {
4262
+ return typeof entity?.object_id === 'string';
4263
+ };
4264
+
4265
+ const shouldUpdateState = ({ stateUpdateQueueId, stateUpdateQueue, watch, }) => {
4266
+ if (!watch) {
4267
+ return true;
4268
+ }
4269
+ if (watch && stateUpdateQueue.has(stateUpdateQueueId)) {
4270
+ stateUpdateQueue.delete(stateUpdateQueueId);
4271
+ return false;
4272
+ }
4273
+ stateUpdateQueue.add(stateUpdateQueueId);
4274
+ return true;
4275
+ };
4276
+ function getStateUpdateQueueId(data, prefix) {
4277
+ if (isFollowResponse(data)) {
4278
+ const toJoin = [data.source_feed.feed, data.target_feed.feed];
4279
+ if (prefix) {
4280
+ toJoin.unshift(prefix);
4827
4281
  }
4828
- catch (e) {
4829
- error = e;
4282
+ return toJoin.join('-');
4283
+ }
4284
+ // else if (isMemberResponse(data)) {
4285
+ // }
4286
+ throw new Error(`Cannot create state update queueId for data: ${JSON.stringify(data)}`);
4287
+ }
4288
+
4289
+ const updateStateFollowCreated = (follow, currentState, currentFeedId, connectedUserId) => {
4290
+ // filter non-accepted follows (the way getOrCreate does by default)
4291
+ if (follow.status !== 'accepted') {
4292
+ return { changed: false, data: currentState };
4293
+ }
4294
+ let newState = { ...currentState };
4295
+ // this feed followed someone
4296
+ if (follow.source_feed.feed === currentFeedId) {
4297
+ newState = {
4298
+ ...newState,
4299
+ // Update FeedResponse fields, that has the new follower/following count
4300
+ ...follow.source_feed,
4301
+ };
4302
+ // Only update if following array already exists
4303
+ if (currentState.following !== undefined) {
4304
+ newState.following = [follow, ...currentState.following];
4830
4305
  }
4831
- finally {
4832
- this.state.next((currentState) => {
4833
- return {
4834
- ...currentState,
4835
- [paginationKey]: {
4836
- ...currentState[paginationKey],
4837
- loading_next_page: false,
4838
- },
4839
- };
4840
- });
4306
+ }
4307
+ else if (
4308
+ // someone followed this feed
4309
+ follow.target_feed.feed === currentFeedId) {
4310
+ const source = follow.source_feed;
4311
+ newState = {
4312
+ ...newState,
4313
+ // Update FeedResponse fields, that has the new follower/following count
4314
+ ...follow.target_feed,
4315
+ };
4316
+ if (source.created_by.id === connectedUserId) {
4317
+ newState.own_follows = currentState.own_follows
4318
+ ? currentState.own_follows.concat(follow)
4319
+ : [follow];
4841
4320
  }
4842
- if (error) {
4843
- throw error;
4321
+ // Only update if followers array already exists
4322
+ if (currentState.followers !== undefined) {
4323
+ newState.followers = [follow, ...currentState.followers];
4844
4324
  }
4845
4325
  }
4846
- async loadNextPageFollowers(request) {
4847
- await this.loadNextPageFollows('followers', request);
4326
+ return { changed: true, data: newState };
4327
+ };
4328
+ function handleFollowCreated(eventOrResponse) {
4329
+ const follow = eventOrResponse.follow;
4330
+ if (!shouldUpdateState({
4331
+ stateUpdateQueueId: getStateUpdateQueueId(follow, 'created'),
4332
+ stateUpdateQueue: this.stateUpdateQueue,
4333
+ watch: this.currentState.watch,
4334
+ })) {
4335
+ return;
4848
4336
  }
4849
- async loadNextPageFollowing(request) {
4850
- await this.loadNextPageFollows('following', request);
4337
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4338
+ const result = updateStateFollowCreated(follow, this.currentState, this.feed, connectedUser?.id);
4339
+ if (result.changed) {
4340
+ this.state.next(result.data);
4851
4341
  }
4852
- async loadNextPageMembers(request) {
4853
- const currentMembers = this.currentState.members;
4854
- const currentNextCursor = this.currentState.member_pagination?.next;
4855
- const isLoading = this.currentState.member_pagination?.loading_next_page;
4856
- const sort = this.currentState.member_pagination?.sort ?? request.sort;
4857
- let error;
4858
- if (isLoading || !checkHasAnotherPage(currentMembers, currentNextCursor)) {
4859
- return;
4342
+ }
4343
+
4344
+ const updateStateFollowDeleted = (follow, currentState, currentFeedId, connectedUserId) => {
4345
+ let newState = { ...currentState };
4346
+ // this feed unfollowed someone
4347
+ if (follow.source_feed.feed === currentFeedId) {
4348
+ newState = {
4349
+ ...newState,
4350
+ // Update FeedResponse fields, that has the new follower/following count
4351
+ ...follow.source_feed,
4352
+ };
4353
+ // Only update if following array already exists
4354
+ if (currentState.following !== undefined) {
4355
+ newState.following = currentState.following.filter((followItem) => followItem.target_feed.feed !== follow.target_feed.feed);
4860
4356
  }
4861
- try {
4862
- this.state.next((currentState) => ({
4357
+ }
4358
+ else if (
4359
+ // someone unfollowed this feed
4360
+ follow.target_feed.feed === currentFeedId) {
4361
+ const source = follow.source_feed;
4362
+ newState = {
4363
+ ...newState,
4364
+ // Update FeedResponse fields, that has the new follower/following count
4365
+ ...follow.target_feed,
4366
+ };
4367
+ if (source.created_by.id === connectedUserId &&
4368
+ currentState.own_follows !== undefined) {
4369
+ newState.own_follows = currentState.own_follows.filter((followItem) => followItem.source_feed.feed !== follow.source_feed.feed);
4370
+ }
4371
+ // Only update if followers array already exists
4372
+ if (currentState.followers !== undefined) {
4373
+ newState.followers = currentState.followers.filter((followItem) => followItem.source_feed.feed !== follow.source_feed.feed);
4374
+ }
4375
+ }
4376
+ return { changed: true, data: newState };
4377
+ };
4378
+ function handleFollowDeleted(eventOrResponse) {
4379
+ const follow = eventOrResponse.follow;
4380
+ if (!shouldUpdateState({
4381
+ stateUpdateQueueId: getStateUpdateQueueId(follow, 'deleted'),
4382
+ stateUpdateQueue: this.stateUpdateQueue,
4383
+ watch: this.currentState.watch,
4384
+ })) {
4385
+ return;
4386
+ }
4387
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4388
+ const result = updateStateFollowDeleted(follow, this.currentState, this.feed, connectedUser?.id);
4389
+ {
4390
+ this.state.next(result.data);
4391
+ }
4392
+ }
4393
+
4394
+ function handleFollowUpdated(eventOrResponse) {
4395
+ const follow = eventOrResponse.follow;
4396
+ const connectedUserId = this.client.state.getLatestValue().connected_user?.id;
4397
+ const currentFeedId = this.feed;
4398
+ if (!shouldUpdateState({
4399
+ stateUpdateQueueId: getStateUpdateQueueId(follow, 'updated'),
4400
+ stateUpdateQueue: this.stateUpdateQueue,
4401
+ watch: this.currentState.watch,
4402
+ })) {
4403
+ return;
4404
+ }
4405
+ this.state.next((currentState) => {
4406
+ let newState;
4407
+ // this feed followed someone
4408
+ if (follow.source_feed.feed === currentFeedId) {
4409
+ newState ?? (newState = {
4863
4410
  ...currentState,
4864
- member_pagination: {
4865
- ...currentState.member_pagination,
4866
- loading_next_page: true,
4867
- },
4868
- }));
4869
- const { next: newNextCursor, members } = await this.client.queryFeedMembers({
4870
- ...request,
4871
- sort,
4872
- feed_id: this.id,
4873
- feed_group_id: this.group,
4874
- next: currentNextCursor,
4411
+ // Update FeedResponse fields, that has the new follower/following count
4412
+ ...follow.source_feed,
4875
4413
  });
4876
- this.state.next((currentState) => ({
4414
+ const index = currentState.following?.findIndex((f) => f.target_feed.feed === follow.target_feed.feed) ?? -1;
4415
+ if (index >= 0) {
4416
+ newState.following = [...newState.following];
4417
+ newState.following[index] = follow;
4418
+ }
4419
+ }
4420
+ else if (
4421
+ // someone followed this feed
4422
+ follow.target_feed.feed === currentFeedId) {
4423
+ const source = follow.source_feed;
4424
+ newState ?? (newState = {
4877
4425
  ...currentState,
4878
- members: currentState.members
4879
- ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
4880
- : members,
4881
- member_pagination: {
4882
- ...currentState.member_pagination,
4883
- next: newNextCursor,
4884
- // set sort if not defined yet
4885
- sort: currentState.member_pagination?.sort ?? request.sort,
4886
- },
4887
- }));
4426
+ // Update FeedResponse fields, that has the new follower/following count
4427
+ ...follow.target_feed,
4428
+ });
4429
+ if (source.created_by.id === connectedUserId &&
4430
+ currentState.own_follows) {
4431
+ const index = currentState.own_follows.findIndex((f) => f.source_feed.feed === follow.source_feed.feed);
4432
+ if (index >= 0) {
4433
+ newState.own_follows = [...currentState.own_follows];
4434
+ newState.own_follows[index] = follow;
4435
+ }
4436
+ }
4437
+ const index = currentState.followers?.findIndex((f) => f.source_feed.feed === follow.source_feed.feed) ?? -1;
4438
+ if (index >= 0) {
4439
+ newState.followers = [...newState.followers];
4440
+ newState.followers[index] = follow;
4441
+ }
4888
4442
  }
4889
- catch (e) {
4890
- error = e;
4443
+ return newState ?? currentState;
4444
+ });
4445
+ }
4446
+
4447
+ function handleCommentAdded(event) {
4448
+ const { comment } = event;
4449
+ const entityId = comment.parent_id ?? comment.object_id;
4450
+ this.state.next((currentState) => {
4451
+ const entityState = currentState.comments_by_entity_id[entityId];
4452
+ if (typeof entityState?.comments === 'undefined') {
4453
+ return currentState;
4891
4454
  }
4892
- finally {
4893
- this.state.next((currentState) => ({
4894
- ...currentState,
4895
- member_pagination: {
4896
- ...currentState.member_pagination,
4897
- loading_next_page: false,
4898
- },
4899
- }));
4455
+ const newComments = entityState?.comments ? [...entityState.comments] : [];
4456
+ if (entityState.pagination?.sort === 'last') {
4457
+ newComments.unshift(comment);
4900
4458
  }
4901
- if (error) {
4902
- throw error;
4459
+ else {
4460
+ // 'first' and other sort options
4461
+ newComments.push(comment);
4903
4462
  }
4904
- }
4905
- /**
4906
- * Method which queries followers of this feed (feeds which target this feed).
4907
- *
4908
- * _Note: Useful only for feeds with `groupId` of `user` value._
4909
- */
4910
- async queryFollowers(request) {
4911
- const filter = {
4912
- target_feed: this.fid,
4913
- };
4914
- const response = await this.client.queryFollows({
4915
- filter,
4916
- ...request,
4917
- });
4918
- return response;
4919
- }
4920
- /**
4921
- * Method which queries following of this feed (target feeds of this feed).
4922
- *
4923
- * _Note: Useful only for feeds with `groupId` of `timeline` value._
4924
- */
4925
- async queryFollowing(request) {
4926
- const filter = {
4927
- source_feed: this.fid,
4463
+ return {
4464
+ ...currentState,
4465
+ comments_by_entity_id: {
4466
+ ...currentState.comments_by_entity_id,
4467
+ [entityId]: {
4468
+ ...currentState.comments_by_entity_id[entityId],
4469
+ comments: newComments,
4470
+ },
4471
+ },
4928
4472
  };
4929
- const response = await this.client.queryFollows({
4930
- filter,
4931
- ...request,
4932
- });
4933
- return response;
4934
- }
4935
- async follow(feedOrFid, options) {
4936
- const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
4937
- const response = await this.client.follow({
4938
- ...options,
4939
- source: this.fid,
4940
- target: fid,
4941
- });
4942
- return response;
4943
- }
4944
- async unfollow(feedOrFid) {
4945
- const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.fid;
4946
- const response = await this.client.unfollow({
4947
- source: this.fid,
4948
- target: fid,
4949
- });
4950
- return response;
4951
- }
4952
- async getNextPage() {
4953
- const currentState = this.currentState;
4954
- return await this.getOrCreate({
4955
- member_pagination: {
4956
- limit: 0,
4473
+ });
4474
+ }
4475
+
4476
+ function handleCommentDeleted({ comment }) {
4477
+ const entityId = comment.parent_id ?? comment.object_id;
4478
+ this.state.next((currentState) => {
4479
+ const newCommentsByEntityId = {
4480
+ ...currentState.comments_by_entity_id,
4481
+ [entityId]: {
4482
+ ...currentState.comments_by_entity_id[entityId],
4957
4483
  },
4958
- followers_pagination: {
4959
- limit: 0,
4484
+ };
4485
+ const index = this.getCommentIndex(comment, currentState);
4486
+ if (newCommentsByEntityId?.[entityId]?.comments?.length && index !== -1) {
4487
+ newCommentsByEntityId[entityId].comments = [
4488
+ ...newCommentsByEntityId[entityId].comments,
4489
+ ];
4490
+ newCommentsByEntityId[entityId]?.comments?.splice(index, 1);
4491
+ }
4492
+ delete newCommentsByEntityId[comment.id];
4493
+ return {
4494
+ ...currentState,
4495
+ comments_by_entity_id: newCommentsByEntityId,
4496
+ };
4497
+ });
4498
+ }
4499
+
4500
+ function handleCommentUpdated(event) {
4501
+ const { comment } = event;
4502
+ const entityId = comment.parent_id ?? comment.object_id;
4503
+ this.state.next((currentState) => {
4504
+ const entityState = currentState.comments_by_entity_id[entityId];
4505
+ if (!entityState?.comments?.length)
4506
+ return currentState;
4507
+ const index = this.getCommentIndex(comment, currentState);
4508
+ if (index === -1)
4509
+ return currentState;
4510
+ const newComments = [...entityState.comments];
4511
+ newComments[index] = comment;
4512
+ return {
4513
+ ...currentState,
4514
+ comments_by_entity_id: {
4515
+ ...currentState.comments_by_entity_id,
4516
+ [entityId]: {
4517
+ ...currentState.comments_by_entity_id[entityId],
4518
+ comments: newComments,
4519
+ },
4960
4520
  },
4961
- following_pagination: {
4962
- limit: 0,
4521
+ };
4522
+ });
4523
+ }
4524
+
4525
+ function handleCommentReaction(event) {
4526
+ const { comment, reaction } = event;
4527
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4528
+ this.state.next((currentState) => {
4529
+ const forId = comment.parent_id ?? comment.object_id;
4530
+ const entityState = currentState.comments_by_entity_id[forId];
4531
+ const commentIndex = this.getCommentIndex(comment, currentState);
4532
+ if (commentIndex === -1)
4533
+ return currentState;
4534
+ const newComments = entityState?.comments?.concat([]) ?? [];
4535
+ const commentCopy = { ...comment };
4536
+ delete commentCopy.own_reactions;
4537
+ const newComment = {
4538
+ ...newComments[commentIndex],
4539
+ ...commentCopy,
4540
+ // TODO: FIXME this should be handled by the backend
4541
+ latest_reactions: commentCopy.latest_reactions ?? [],
4542
+ reaction_groups: commentCopy.reaction_groups ?? {},
4543
+ };
4544
+ newComments[commentIndex] = newComment;
4545
+ if (reaction.user.id === connectedUser?.id) {
4546
+ if (event.type === 'feeds.comment.reaction.added') {
4547
+ newComment.own_reactions = newComment.own_reactions.concat(reaction) ?? [reaction];
4548
+ }
4549
+ else if (event.type === 'feeds.comment.reaction.deleted') {
4550
+ newComment.own_reactions = newComment.own_reactions.filter((r) => r.type !== reaction.type);
4551
+ }
4552
+ }
4553
+ return {
4554
+ ...currentState,
4555
+ comments_by_entity_id: {
4556
+ ...currentState.comments_by_entity_id,
4557
+ [forId]: {
4558
+ ...entityState,
4559
+ comments: newComments,
4560
+ },
4963
4561
  },
4964
- next: currentState.next,
4965
- limit: currentState.last_get_or_create_request_config?.limit ?? 20,
4966
- });
4967
- }
4968
- addActivity(request) {
4969
- return this.feedsApi.addActivity({
4970
- ...request,
4971
- fids: [this.fid],
4972
- });
4973
- }
4974
- handleWSEvent(event) {
4975
- const eventHandler = this.eventHandlers[event.type];
4976
- // no need to run noop function
4977
- if (eventHandler !== Feed.noop) {
4978
- // @ts-expect-error intersection of handler arguments results to never
4979
- eventHandler?.(event);
4562
+ };
4563
+ });
4564
+ }
4565
+
4566
+ function handleFeedMemberAdded(event) {
4567
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4568
+ this.state.next((currentState) => {
4569
+ let newState;
4570
+ if (typeof currentState.members !== 'undefined') {
4571
+ newState ?? (newState = {
4572
+ ...currentState,
4573
+ });
4574
+ newState.members = [event.member, ...currentState.members];
4980
4575
  }
4981
- if (typeof eventHandler === 'undefined') {
4982
- console.warn(`Received unknown event type: ${event.type}`, event);
4576
+ if (connectedUser?.id === event.member.user.id) {
4577
+ newState ?? (newState = {
4578
+ ...currentState,
4579
+ });
4580
+ newState.own_membership = event.member;
4983
4581
  }
4984
- this.eventDispatcher.dispatch(event);
4985
- }
4582
+ return newState ?? currentState;
4583
+ });
4986
4584
  }
4987
- Feed.noop = () => { };
4988
4585
 
4989
- class ModerationApi {
4990
- constructor(apiClient) {
4991
- this.apiClient = apiClient;
4992
- }
4993
- async ban(request) {
4994
- const body = {
4995
- target_user_id: request?.target_user_id,
4996
- banned_by_id: request?.banned_by_id,
4997
- channel_cid: request?.channel_cid,
4998
- delete_messages: request?.delete_messages,
4999
- ip_ban: request?.ip_ban,
5000
- reason: request?.reason,
5001
- shadow: request?.shadow,
5002
- timeout: request?.timeout,
5003
- banned_by: request?.banned_by,
5004
- };
5005
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/ban', undefined, undefined, body, 'application/json');
5006
- decoders.BanResponse?.(response.body);
5007
- return { ...response.body, metadata: response.metadata };
5008
- }
5009
- async upsertConfig(request) {
5010
- const body = {
5011
- key: request?.key,
5012
- async: request?.async,
5013
- team: request?.team,
5014
- ai_image_config: request?.ai_image_config,
5015
- ai_text_config: request?.ai_text_config,
5016
- ai_video_config: request?.ai_video_config,
5017
- automod_platform_circumvention_config: request?.automod_platform_circumvention_config,
5018
- automod_semantic_filters_config: request?.automod_semantic_filters_config,
5019
- automod_toxicity_config: request?.automod_toxicity_config,
5020
- aws_rekognition_config: request?.aws_rekognition_config,
5021
- block_list_config: request?.block_list_config,
5022
- bodyguard_config: request?.bodyguard_config,
5023
- google_vision_config: request?.google_vision_config,
5024
- rule_builder_config: request?.rule_builder_config,
5025
- velocity_filter_config: request?.velocity_filter_config,
5026
- video_call_rule_config: request?.video_call_rule_config,
4586
+ function handleFeedMemberUpdated(event) {
4587
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4588
+ this.state.next((currentState) => {
4589
+ const memberIndex = currentState.members?.findIndex((member) => member.user.id === event.member.user.id) ?? -1;
4590
+ let newState;
4591
+ if (memberIndex !== -1) {
4592
+ // if there's an index, there's a member to update
4593
+ const newMembers = [...currentState.members];
4594
+ newMembers[memberIndex] = event.member;
4595
+ newState ?? (newState = {
4596
+ ...currentState,
4597
+ });
4598
+ newState.members = newMembers;
4599
+ }
4600
+ if (connectedUser?.id === event.member.user.id) {
4601
+ newState ?? (newState = {
4602
+ ...currentState,
4603
+ });
4604
+ newState.own_membership = event.member;
4605
+ }
4606
+ return newState ?? currentState;
4607
+ });
4608
+ }
4609
+
4610
+ function handleFeedMemberRemoved(event) {
4611
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4612
+ this.state.next((currentState) => {
4613
+ const newState = {
4614
+ ...currentState,
4615
+ members: currentState.members?.filter((member) => member.user.id !== event.user?.id),
4616
+ };
4617
+ if (connectedUser?.id === event.member_id) {
4618
+ delete newState.own_membership;
4619
+ }
4620
+ return newState;
4621
+ });
4622
+ }
4623
+
4624
+ const addActivitiesToState = (newActivities, activities, position) => {
4625
+ let result;
4626
+ if (activities === undefined) {
4627
+ activities = [];
4628
+ result = {
4629
+ changed: true,
4630
+ activities,
5027
4631
  };
5028
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/config', undefined, undefined, body, 'application/json');
5029
- decoders.UpsertConfigResponse?.(response.body);
5030
- return { ...response.body, metadata: response.metadata };
5031
4632
  }
5032
- async deleteConfig(request) {
5033
- const queryParams = {
5034
- team: request?.team,
5035
- };
5036
- const pathParams = {
5037
- key: request?.key,
4633
+ else {
4634
+ result = {
4635
+ changed: false,
4636
+ activities,
5038
4637
  };
5039
- const response = await this.apiClient.sendRequest('DELETE', '/api/v2/moderation/config/{key}', pathParams, queryParams);
5040
- decoders.DeleteModerationConfigResponse?.(response.body);
5041
- return { ...response.body, metadata: response.metadata };
5042
4638
  }
5043
- async getConfig(request) {
5044
- const queryParams = {
5045
- team: request?.team,
5046
- };
5047
- const pathParams = {
5048
- key: request?.key,
5049
- };
5050
- const response = await this.apiClient.sendRequest('GET', '/api/v2/moderation/config/{key}', pathParams, queryParams);
5051
- decoders.GetConfigResponse?.(response.body);
5052
- return { ...response.body, metadata: response.metadata };
4639
+ const newActivitiesDeduplicated = [];
4640
+ newActivities.forEach((newActivityResponse) => {
4641
+ const index = activities.findIndex((a) => a.id === newActivityResponse.id);
4642
+ if (index === -1) {
4643
+ newActivitiesDeduplicated.push(newActivityResponse);
4644
+ }
4645
+ });
4646
+ if (newActivitiesDeduplicated.length > 0) {
4647
+ // TODO: since feed activities are not necessarily ordered by created_at (personalization) we don't order by created_at
4648
+ // Maybe we can add a flag to the JS client to support order by created_at
4649
+ const updatedActivities = [
4650
+ ...(position === 'start' ? newActivitiesDeduplicated : []),
4651
+ ...activities,
4652
+ ...(position === 'end' ? newActivitiesDeduplicated : []),
4653
+ ];
4654
+ result = { changed: true, activities: updatedActivities };
5053
4655
  }
5054
- async queryModerationConfigs(request) {
5055
- const body = {
5056
- limit: request?.limit,
5057
- next: request?.next,
5058
- prev: request?.prev,
5059
- sort: request?.sort,
5060
- filter: request?.filter,
5061
- };
5062
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/configs', undefined, undefined, body, 'application/json');
5063
- decoders.QueryModerationConfigsResponse?.(response.body);
5064
- return { ...response.body, metadata: response.metadata };
4656
+ return result;
4657
+ };
4658
+ function handleActivityAdded(event) {
4659
+ const currentActivities = this.currentState.activities;
4660
+ const result = addActivitiesToState([event.activity], currentActivities, 'start');
4661
+ if (result.changed) {
4662
+ this.client.hydratePollCache([event.activity]);
4663
+ this.state.partialNext({ activities: result.activities });
5065
4664
  }
5066
- async flag(request) {
5067
- const body = {
5068
- entity_id: request?.entity_id,
5069
- entity_type: request?.entity_type,
5070
- entity_creator_id: request?.entity_creator_id,
5071
- reason: request?.reason,
5072
- custom: request?.custom,
5073
- moderation_payload: request?.moderation_payload,
5074
- };
5075
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/flag', undefined, undefined, body, 'application/json');
5076
- decoders.FlagResponse?.(response.body);
5077
- return { ...response.body, metadata: response.metadata };
4665
+ }
4666
+
4667
+ const removeActivityFromState = (activityResponse, activities) => {
4668
+ const index = activities.findIndex((a) => a.id === activityResponse.id);
4669
+ if (index !== -1) {
4670
+ const newActivities = [...activities];
4671
+ newActivities.splice(index, 1);
4672
+ return { changed: true, activities: newActivities };
5078
4673
  }
5079
- async mute(request) {
5080
- const body = {
5081
- target_ids: request?.target_ids,
5082
- timeout: request?.timeout,
5083
- };
5084
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/mute', undefined, undefined, body, 'application/json');
5085
- decoders.MuteResponse?.(response.body);
5086
- return { ...response.body, metadata: response.metadata };
4674
+ else {
4675
+ return { changed: false, activities };
5087
4676
  }
5088
- async queryReviewQueue(request) {
5089
- const body = {
5090
- limit: request?.limit,
5091
- lock_count: request?.lock_count,
5092
- lock_duration: request?.lock_duration,
5093
- lock_items: request?.lock_items,
5094
- next: request?.next,
5095
- prev: request?.prev,
5096
- stats_only: request?.stats_only,
5097
- sort: request?.sort,
5098
- filter: request?.filter,
5099
- };
5100
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/review_queue', undefined, undefined, body, 'application/json');
5101
- decoders.QueryReviewQueueResponse?.(response.body);
5102
- return { ...response.body, metadata: response.metadata };
4677
+ };
4678
+ function handleActivityDeleted(event) {
4679
+ const currentActivities = this.currentState.activities;
4680
+ if (currentActivities) {
4681
+ const result = removeActivityFromState(event.activity, currentActivities);
4682
+ if (result.changed) {
4683
+ this.state.partialNext({ activities: result.activities });
4684
+ }
5103
4685
  }
5104
- async submitAction(request) {
5105
- const body = {
5106
- action_type: request?.action_type,
5107
- item_id: request?.item_id,
5108
- ban: request?.ban,
5109
- custom: request?.custom,
5110
- delete_activity: request?.delete_activity,
5111
- delete_message: request?.delete_message,
5112
- delete_reaction: request?.delete_reaction,
5113
- delete_user: request?.delete_user,
5114
- mark_reviewed: request?.mark_reviewed,
5115
- unban: request?.unban,
5116
- };
5117
- const response = await this.apiClient.sendRequest('POST', '/api/v2/moderation/submit_action', undefined, undefined, body, 'application/json');
5118
- decoders.SubmitActionResponse?.(response.body);
5119
- return { ...response.body, metadata: response.metadata };
4686
+ }
4687
+
4688
+ function handleActivityRemovedFromFeed(event) {
4689
+ const currentActivities = this.currentState.activities;
4690
+ if (currentActivities) {
4691
+ const result = removeActivityFromState(event.activity, currentActivities);
4692
+ if (result.changed) {
4693
+ this.state.partialNext({ activities: result.activities });
4694
+ }
5120
4695
  }
5121
4696
  }
5122
4697
 
5123
- class ModerationClient extends ModerationApi {
4698
+ const updateActivityInState = (updatedActivityResponse, activities, replaceCompletely = false) => {
4699
+ const index = activities.findIndex((a) => a.id === updatedActivityResponse.id);
4700
+ if (index !== -1) {
4701
+ const newActivities = [...activities];
4702
+ const activity = activities[index];
4703
+ if (replaceCompletely) {
4704
+ newActivities[index] = updatedActivityResponse;
4705
+ }
4706
+ else {
4707
+ newActivities[index] = {
4708
+ ...updatedActivityResponse,
4709
+ own_reactions: activity.own_reactions,
4710
+ own_bookmarks: activity.own_bookmarks,
4711
+ latest_reactions: activity.latest_reactions,
4712
+ reaction_groups: activity.reaction_groups,
4713
+ };
4714
+ }
4715
+ return { changed: true, activities: newActivities };
4716
+ }
4717
+ else {
4718
+ return { changed: false, activities };
4719
+ }
4720
+ };
4721
+ function handleActivityUpdated(event) {
4722
+ const currentActivities = this.currentState.activities;
4723
+ if (currentActivities) {
4724
+ const result = updateActivityInState(event.activity, currentActivities);
4725
+ if (result.changed) {
4726
+ this.client.hydratePollCache([event.activity]);
4727
+ this.state.partialNext({ activities: result.activities });
4728
+ }
4729
+ }
5124
4730
  }
5125
4731
 
5126
- const isPollUpdatedEvent = (e) => e.type === 'feeds.poll.updated';
5127
- const isPollClosedEventEvent = (e) => e.type === 'feeds.poll.closed';
5128
- const isPollVoteCastedEvent = (e) => e.type === 'feeds.poll.vote_casted';
5129
- const isPollVoteChangedEvent = (e) => e.type === 'feeds.poll.vote_changed';
5130
- const isPollVoteRemovedEvent = (e) => e.type === 'feeds.poll.vote_removed';
5131
- const isVoteAnswer = (vote) => !!vote?.answer_text;
5132
- class StreamPoll {
5133
- constructor({ client, poll }) {
5134
- this.getInitialStateFromPollResponse = (poll) => {
5135
- const { own_votes, id, ...pollResponseForState } = poll;
5136
- const { ownAnswer, ownVotes } = own_votes?.reduce((acc, voteOrAnswer) => {
5137
- if (isVoteAnswer(voteOrAnswer)) {
5138
- acc.ownAnswer = voteOrAnswer;
5139
- }
5140
- else {
5141
- acc.ownVotes.push(voteOrAnswer);
5142
- }
5143
- return acc;
5144
- }, { ownVotes: [] }) ?? { ownVotes: [] };
5145
- return {
5146
- ...pollResponseForState,
5147
- last_activity_at: new Date(),
5148
- max_voted_option_ids: getMaxVotedOptionIds(pollResponseForState.vote_counts_by_option),
5149
- own_answer: ownAnswer,
5150
- own_votes_by_option_id: getOwnVotesByOptionId(ownVotes),
5151
- };
5152
- };
5153
- this.reinitializeState = (poll) => {
5154
- this.state.partialNext(this.getInitialStateFromPollResponse(poll));
5155
- };
5156
- this.handlePollUpdated = (event) => {
5157
- if (event.poll?.id && event.poll.id !== this.id)
5158
- return;
5159
- if (!isPollUpdatedEvent(event))
5160
- return;
5161
- const { id, ...pollData } = event.poll;
5162
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5163
- this.state.partialNext({
5164
- ...pollData,
5165
- last_activity_at: new Date(event.created_at),
5166
- });
5167
- };
5168
- this.handlePollClosed = (event) => {
5169
- if (event.poll?.id && event.poll.id !== this.id)
5170
- return;
5171
- if (!isPollClosedEventEvent(event))
5172
- return;
5173
- this.state.partialNext({
5174
- is_closed: true,
5175
- last_activity_at: new Date(event.created_at),
5176
- });
5177
- };
5178
- this.handleVoteCasted = (event) => {
5179
- if (event.poll?.id && event.poll.id !== this.id)
5180
- return;
5181
- if (!isPollVoteCastedEvent(event))
5182
- return;
5183
- const currentState = this.data;
5184
- const isOwnVote = event.poll_vote.user_id ===
5185
- this.client.state.getLatestValue().connected_user?.id;
5186
- let latestAnswers = [...currentState.latest_answers];
5187
- let ownAnswer = currentState.own_answer;
5188
- const ownVotesByOptionId = currentState.own_votes_by_option_id;
5189
- let maxVotedOptionIds = currentState.max_voted_option_ids;
5190
- if (isOwnVote) {
5191
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5192
- if (isVoteAnswer(event.poll_vote)) {
5193
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5194
- ownAnswer = event.poll_vote;
5195
- }
5196
- else if (event.poll_vote.option_id) {
5197
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5198
- ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
5199
- }
5200
- }
5201
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5202
- if (isVoteAnswer(event.poll_vote)) {
5203
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5204
- latestAnswers = [event.poll_vote, ...latestAnswers];
5205
- }
5206
- else {
5207
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5208
- }
5209
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5210
- this.state.partialNext({
5211
- answers_count,
5212
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5213
- latest_votes_by_option,
5214
- vote_count,
5215
- vote_counts_by_option,
5216
- latest_answers: latestAnswers,
5217
- last_activity_at: new Date(event.created_at),
5218
- own_answer: ownAnswer,
5219
- own_votes_by_option_id: ownVotesByOptionId,
5220
- max_voted_option_ids: maxVotedOptionIds,
5221
- });
4732
+ const addReactionToActivity = (event, activity, isCurrentUser) => {
4733
+ // Update own_reactions if the reaction is from the current user
4734
+ const ownReactions = [...(activity.own_reactions || [])];
4735
+ if (isCurrentUser) {
4736
+ ownReactions.push(event.reaction);
4737
+ }
4738
+ return {
4739
+ ...activity,
4740
+ own_reactions: ownReactions,
4741
+ latest_reactions: event.activity.latest_reactions,
4742
+ reaction_groups: event.activity.reaction_groups,
4743
+ changed: true,
4744
+ };
4745
+ };
4746
+ const addReactionToActivities = (event, activities, isCurrentUser) => {
4747
+ if (!activities) {
4748
+ return { changed: false, activities: [] };
4749
+ }
4750
+ const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
4751
+ if (activityIndex === -1) {
4752
+ return { changed: false, activities };
4753
+ }
4754
+ const activity = activities[activityIndex];
4755
+ const updatedActivity = addReactionToActivity(event, activity, isCurrentUser);
4756
+ return updateActivityInState(updatedActivity, activities, true);
4757
+ };
4758
+ function handleActivityReactionAdded(event) {
4759
+ const currentActivities = this.currentState.activities;
4760
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4761
+ const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4762
+ const result = addReactionToActivities(event, currentActivities, isCurrentUser);
4763
+ if (result.changed) {
4764
+ this.state.partialNext({ activities: result.activities });
4765
+ }
4766
+ }
4767
+
4768
+ const removeReactionFromActivity = (event, activity, isCurrentUser) => {
4769
+ // Update own_reactions if the reaction is from the current user
4770
+ const ownReactions = isCurrentUser
4771
+ ? (activity.own_reactions || []).filter((r) => !(r.type === event.reaction.type &&
4772
+ r.user.id === event.reaction.user.id))
4773
+ : activity.own_reactions;
4774
+ return {
4775
+ ...activity,
4776
+ own_reactions: ownReactions,
4777
+ latest_reactions: event.activity.latest_reactions,
4778
+ reaction_groups: event.activity.reaction_groups,
4779
+ changed: true,
4780
+ };
4781
+ };
4782
+ const removeReactionFromActivities = (event, activities, isCurrentUser) => {
4783
+ if (!activities) {
4784
+ return { changed: false, activities: [] };
4785
+ }
4786
+ const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
4787
+ if (activityIndex === -1) {
4788
+ return { changed: false, activities };
4789
+ }
4790
+ const activity = activities[activityIndex];
4791
+ const updatedActivity = removeReactionFromActivity(event, activity, isCurrentUser);
4792
+ return updateActivityInState(updatedActivity, activities, true);
4793
+ };
4794
+ function handleActivityReactionDeleted(event) {
4795
+ const currentActivities = this.currentState.activities;
4796
+ const connectedUser = this.client.state.getLatestValue().connected_user;
4797
+ const isCurrentUser = Boolean(connectedUser && event.reaction.user.id === connectedUser.id);
4798
+ const result = removeReactionFromActivities(event, currentActivities, isCurrentUser);
4799
+ if (result.changed) {
4800
+ this.state.partialNext({ activities: result.activities });
4801
+ }
4802
+ }
4803
+
4804
+ const addBookmarkToActivity = (event, activity, isCurrentUser) => {
4805
+ // Update own_bookmarks if the bookmark is from the current user
4806
+ const ownBookmarks = [...(activity.own_bookmarks || [])];
4807
+ if (isCurrentUser) {
4808
+ ownBookmarks.push(event.bookmark);
4809
+ }
4810
+ return {
4811
+ ...activity,
4812
+ own_bookmarks: ownBookmarks,
4813
+ changed: true,
4814
+ };
4815
+ };
4816
+ const addBookmarkToActivities = (event, activities, isCurrentUser) => {
4817
+ if (!activities) {
4818
+ return { changed: false, activities: [] };
4819
+ }
4820
+ const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4821
+ if (activityIndex === -1) {
4822
+ return { changed: false, activities };
4823
+ }
4824
+ const activity = activities[activityIndex];
4825
+ const updatedActivity = addBookmarkToActivity(event, activity, isCurrentUser);
4826
+ return updateActivityInState(updatedActivity, activities, true);
4827
+ };
4828
+ function handleBookmarkAdded(event) {
4829
+ const currentActivities = this.currentState.activities;
4830
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4831
+ const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4832
+ const result = addBookmarkToActivities(event, currentActivities, isCurrentUser);
4833
+ if (result.changed) {
4834
+ this.state.partialNext({ activities: result.activities });
4835
+ }
4836
+ }
4837
+
4838
+ // Helper function to check if two bookmarks are the same
4839
+ // A bookmark is identified by activity_id + folder_id + user_id
4840
+ const isSameBookmark = (bookmark1, bookmark2) => {
4841
+ return (bookmark1.user.id === bookmark2.user.id &&
4842
+ bookmark1.activity.id === bookmark2.activity.id &&
4843
+ bookmark1.folder?.id === bookmark2.folder?.id);
4844
+ };
4845
+ const removeBookmarkFromActivities = (event, activities, isCurrentUser) => {
4846
+ if (!activities) {
4847
+ return { changed: false, activities: [] };
4848
+ }
4849
+ const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4850
+ if (activityIndex === -1) {
4851
+ return { changed: false, activities };
4852
+ }
4853
+ const activity = activities[activityIndex];
4854
+ const updatedActivity = removeBookmarkFromActivity(event, activity, isCurrentUser);
4855
+ return updateActivityInState(updatedActivity, activities, true);
4856
+ };
4857
+ const removeBookmarkFromActivity = (event, activity, isCurrentUser) => {
4858
+ // Update own_bookmarks if the bookmark is from the current user
4859
+ const ownBookmarks = isCurrentUser
4860
+ ? (activity.own_bookmarks || []).filter((bookmark) => !isSameBookmark(bookmark, event.bookmark))
4861
+ : activity.own_bookmarks;
4862
+ return {
4863
+ ...activity,
4864
+ own_bookmarks: ownBookmarks,
4865
+ changed: true,
4866
+ };
4867
+ };
4868
+ function handleBookmarkDeleted(event) {
4869
+ const currentActivities = this.currentState.activities;
4870
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4871
+ const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4872
+ const result = removeBookmarkFromActivities(event, currentActivities, isCurrentUser);
4873
+ if (result.changed) {
4874
+ this.state.partialNext({ activities: result.activities });
4875
+ }
4876
+ }
4877
+
4878
+ const updateBookmarkInActivity = (event, activity, isCurrentUser) => {
4879
+ // Update own_bookmarks if the bookmark is from the current user
4880
+ let ownBookmarks = activity.own_bookmarks || [];
4881
+ if (isCurrentUser) {
4882
+ const bookmarkIndex = ownBookmarks.findIndex((bookmark) => isSameBookmark(bookmark, event.bookmark));
4883
+ if (bookmarkIndex !== -1) {
4884
+ ownBookmarks = [...ownBookmarks];
4885
+ ownBookmarks[bookmarkIndex] = event.bookmark;
4886
+ }
4887
+ }
4888
+ return {
4889
+ ...activity,
4890
+ own_bookmarks: ownBookmarks,
4891
+ changed: true,
4892
+ };
4893
+ };
4894
+ const updateBookmarkInActivities = (event, activities, isCurrentUser) => {
4895
+ if (!activities) {
4896
+ return { changed: false, activities: [] };
4897
+ }
4898
+ const activityIndex = activities.findIndex((a) => a.id === event.bookmark.activity.id);
4899
+ if (activityIndex === -1) {
4900
+ return { changed: false, activities };
4901
+ }
4902
+ const activity = activities[activityIndex];
4903
+ const updatedActivity = updateBookmarkInActivity(event, activity, isCurrentUser);
4904
+ return updateActivityInState(updatedActivity, activities, true);
4905
+ };
4906
+ function handleBookmarkUpdated(event) {
4907
+ const currentActivities = this.currentState.activities;
4908
+ const { connected_user: connectedUser } = this.client.state.getLatestValue();
4909
+ const isCurrentUser = event.bookmark.user.id === connectedUser?.id;
4910
+ const result = updateBookmarkInActivities(event, currentActivities, isCurrentUser);
4911
+ if (result.changed) {
4912
+ this.state.partialNext({ activities: result.activities });
4913
+ }
4914
+ }
4915
+
4916
+ function handleFeedUpdated(event) {
4917
+ this.state.partialNext({ ...event.feed });
4918
+ }
4919
+
4920
+ function handleNotificationFeedUpdated(event) {
4921
+ console.info('notification feed updated', event);
4922
+ // TODO: handle notification feed updates
4923
+ }
4924
+
4925
+ function handleWatchStarted() {
4926
+ this.state.partialNext({ watch: true });
4927
+ }
4928
+
4929
+ function handleWatchStopped() {
4930
+ this.state.partialNext({ watch: false });
4931
+ }
4932
+
4933
+ class Feed extends FeedApi {
4934
+ constructor(client, groupId, id, data, watch = false) {
4935
+ super(client, groupId, id);
4936
+ this.stateUpdateQueue = new Set();
4937
+ this.eventHandlers = {
4938
+ 'feeds.activity.added': handleActivityAdded.bind(this),
4939
+ 'feeds.activity.deleted': handleActivityDeleted.bind(this),
4940
+ 'feeds.activity.reaction.added': handleActivityReactionAdded.bind(this),
4941
+ 'feeds.activity.reaction.deleted': handleActivityReactionDeleted.bind(this),
4942
+ 'feeds.activity.reaction.updated': Feed.noop,
4943
+ 'feeds.activity.removed_from_feed': handleActivityRemovedFromFeed.bind(this),
4944
+ 'feeds.activity.updated': handleActivityUpdated.bind(this),
4945
+ 'feeds.bookmark.added': handleBookmarkAdded.bind(this),
4946
+ 'feeds.bookmark.deleted': handleBookmarkDeleted.bind(this),
4947
+ 'feeds.bookmark.updated': handleBookmarkUpdated.bind(this),
4948
+ 'feeds.bookmark_folder.deleted': Feed.noop,
4949
+ 'feeds.bookmark_folder.updated': Feed.noop,
4950
+ 'feeds.comment.added': handleCommentAdded.bind(this),
4951
+ 'feeds.comment.deleted': handleCommentDeleted.bind(this),
4952
+ 'feeds.comment.updated': handleCommentUpdated.bind(this),
4953
+ 'feeds.feed.created': Feed.noop,
4954
+ 'feeds.feed.deleted': Feed.noop,
4955
+ 'feeds.feed.updated': handleFeedUpdated.bind(this),
4956
+ 'feeds.feed_group.changed': Feed.noop,
4957
+ 'feeds.feed_group.deleted': Feed.noop,
4958
+ 'feeds.follow.created': handleFollowCreated.bind(this),
4959
+ 'feeds.follow.deleted': handleFollowDeleted.bind(this),
4960
+ 'feeds.follow.updated': handleFollowUpdated.bind(this),
4961
+ 'feeds.comment.reaction.added': handleCommentReaction.bind(this),
4962
+ 'feeds.comment.reaction.deleted': handleCommentReaction.bind(this),
4963
+ 'feeds.comment.reaction.updated': Feed.noop,
4964
+ 'feeds.feed_member.added': handleFeedMemberAdded.bind(this),
4965
+ 'feeds.feed_member.removed': handleFeedMemberRemoved.bind(this),
4966
+ 'feeds.feed_member.updated': handleFeedMemberUpdated.bind(this),
4967
+ 'feeds.notification_feed.updated': handleNotificationFeedUpdated.bind(this),
4968
+ // the poll events should be removed from here
4969
+ 'feeds.poll.closed': Feed.noop,
4970
+ 'feeds.poll.deleted': Feed.noop,
4971
+ 'feeds.poll.updated': Feed.noop,
4972
+ 'feeds.poll.vote_casted': Feed.noop,
4973
+ 'feeds.poll.vote_changed': Feed.noop,
4974
+ 'feeds.poll.vote_removed': Feed.noop,
4975
+ 'feeds.activity.pinned': Feed.noop,
4976
+ 'feeds.activity.unpinned': Feed.noop,
4977
+ 'feeds.activity.marked': Feed.noop,
4978
+ 'moderation.custom_action': Feed.noop,
4979
+ 'moderation.flagged': Feed.noop,
4980
+ 'moderation.mark_reviewed': Feed.noop,
4981
+ 'health.check': Feed.noop,
4982
+ 'app.updated': Feed.noop,
4983
+ 'user.banned': Feed.noop,
4984
+ 'user.deactivated': Feed.noop,
4985
+ 'user.muted': Feed.noop,
4986
+ 'user.reactivated': Feed.noop,
4987
+ 'user.updated': Feed.noop,
5222
4988
  };
5223
- this.handleVoteChanged = (event) => {
5224
- // this event is triggered only when event.poll.enforce_unique_vote === true
5225
- if (event.poll?.id && event.poll.id !== this.id)
5226
- return;
5227
- if (!isPollVoteChangedEvent(event))
5228
- return;
5229
- const currentState = this.data;
5230
- const isOwnVote = event.poll_vote.user_id ===
5231
- this.client.state.getLatestValue().connected_user?.id;
5232
- let latestAnswers = [...currentState.latest_answers];
5233
- let ownAnswer = currentState.own_answer;
5234
- let ownVotesByOptionId = currentState.own_votes_by_option_id;
5235
- let maxVotedOptionIds = currentState.max_voted_option_ids;
5236
- if (isOwnVote) {
5237
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5238
- if (isVoteAnswer(event.poll_vote)) {
5239
- latestAnswers = [
5240
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5241
- event.poll_vote,
5242
- ...latestAnswers.filter((answer) => answer.id !== event.poll_vote.id),
5243
- ];
5244
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5245
- ownAnswer = event.poll_vote;
4989
+ this.eventDispatcher = new EventDispatcher();
4990
+ this.on = this.eventDispatcher.on;
4991
+ this.off = this.eventDispatcher.off;
4992
+ this.state = new StateStore({
4993
+ feed: `${groupId}:${id}`,
4994
+ group_id: groupId,
4995
+ id,
4996
+ ...(data ?? {}),
4997
+ is_loading: false,
4998
+ is_loading_activities: false,
4999
+ comments_by_entity_id: {},
5000
+ watch,
5001
+ });
5002
+ this.client = client;
5003
+ }
5004
+ get feed() {
5005
+ return `${this.group}:${this.id}`;
5006
+ }
5007
+ get currentState() {
5008
+ return this.state.getLatestValue();
5009
+ }
5010
+ async synchronize() {
5011
+ const { last_get_or_create_request_config } = this.state.getLatestValue();
5012
+ if (last_get_or_create_request_config?.watch) {
5013
+ await this.getOrCreate(last_get_or_create_request_config);
5014
+ }
5015
+ }
5016
+ async getOrCreate(request) {
5017
+ if (this.currentState.is_loading_activities) {
5018
+ throw new Error('Only one getOrCreate call is allowed at a time');
5019
+ }
5020
+ this.state.partialNext({
5021
+ is_loading: !request?.next,
5022
+ is_loading_activities: true,
5023
+ });
5024
+ // TODO: pull comments/comment_pagination from activities and comment_sort from request
5025
+ // and pre-populate comments_by_entity_id (once comment_sort and comment_limit are supported)
5026
+ try {
5027
+ const response = await super.getOrCreate(request);
5028
+ if (request?.next) {
5029
+ const { activities: currentActivities = [] } = this.currentState;
5030
+ const result = addActivitiesToState(response.activities, currentActivities, 'end');
5031
+ if (result.changed) {
5032
+ this.state.partialNext({
5033
+ activities: result.activities,
5034
+ next: response.next,
5035
+ prev: response.prev,
5036
+ });
5246
5037
  }
5247
- else if (event.poll_vote.option_id) {
5248
- if (event.poll.enforce_unique_vote) {
5249
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5250
- ownVotesByOptionId = { [event.poll_vote.option_id]: event.poll_vote };
5038
+ }
5039
+ else {
5040
+ // Empty queue when reinitializing the state
5041
+ this.stateUpdateQueue.clear();
5042
+ const responseCopy = {
5043
+ ...response,
5044
+ ...response.feed,
5045
+ };
5046
+ delete responseCopy.feed;
5047
+ delete responseCopy.metadata;
5048
+ delete responseCopy.duration;
5049
+ // TODO: lazy-load comments from activities when comment_sort and comment_pagination are supported
5050
+ this.state.next((currentState) => {
5051
+ const nextState = {
5052
+ ...currentState,
5053
+ ...responseCopy,
5054
+ };
5055
+ if (!request?.followers_pagination?.limit) {
5056
+ delete nextState.followers;
5251
5057
  }
5252
- else {
5253
- ownVotesByOptionId = Object.entries(ownVotesByOptionId).reduce((acc, [optionId, vote]) => {
5254
- if (optionId !== event.poll_vote.option_id &&
5255
- vote.id === event.poll_vote.id) {
5256
- return acc;
5257
- }
5258
- acc[optionId] = vote;
5259
- return acc;
5260
- }, {});
5261
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5262
- ownVotesByOptionId[event.poll_vote.option_id] = event.poll_vote;
5058
+ if (!request?.following_pagination?.limit) {
5059
+ delete nextState.following;
5263
5060
  }
5264
- if (ownAnswer?.id === event.poll_vote.id) {
5265
- ownAnswer = undefined;
5061
+ if (response.members.length === 0 && response.feed.member_count > 0) {
5062
+ delete nextState.members;
5266
5063
  }
5267
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5268
- }
5269
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5270
- }
5271
- else if (isVoteAnswer(event.poll_vote)) {
5272
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5273
- latestAnswers = [event.poll_vote, ...latestAnswers];
5274
- }
5275
- else {
5276
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5064
+ nextState.last_get_or_create_request_config = request;
5065
+ nextState.watch = request?.watch ? request.watch : currentState.watch;
5066
+ return nextState;
5067
+ });
5277
5068
  }
5278
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5069
+ this.client.hydratePollCache(response.activities);
5070
+ return response;
5071
+ }
5072
+ finally {
5279
5073
  this.state.partialNext({
5280
- answers_count,
5281
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5282
- latest_votes_by_option,
5283
- vote_count,
5284
- vote_counts_by_option,
5285
- latest_answers: latestAnswers,
5286
- last_activity_at: new Date(event.created_at),
5287
- own_answer: ownAnswer,
5288
- own_votes_by_option_id: ownVotesByOptionId,
5289
- max_voted_option_ids: maxVotedOptionIds,
5074
+ is_loading: false,
5075
+ is_loading_activities: false,
5076
+ });
5077
+ }
5078
+ }
5079
+ /**
5080
+ * Returns index of the provided comment object.
5081
+ */
5082
+ getCommentIndex(comment, state) {
5083
+ const { comments_by_entity_id = {} } = state ?? this.currentState;
5084
+ const currentComments = comments_by_entity_id[comment.parent_id ?? comment.object_id]?.comments;
5085
+ if (!currentComments?.length) {
5086
+ return -1;
5087
+ }
5088
+ // @ts-expect-error this will just fail if the comment is not object from state
5089
+ let commentIndex = currentComments.indexOf(comment);
5090
+ // fast lookup failed, try slower approach
5091
+ if (commentIndex === -1) {
5092
+ commentIndex = currentComments.findIndex((comment_) => comment_.id === comment.id);
5093
+ }
5094
+ return commentIndex;
5095
+ }
5096
+ /**
5097
+ * Load child comments of entity (activity or comment) into the state, if the target entity is comment,
5098
+ * `entityParentId` should be provided (`CommentResponse.parent_id ?? CommentResponse.object_id`).
5099
+ */
5100
+ loadCommentsIntoState(data) {
5101
+ // add initial (top level) object for processing
5102
+ const traverseArray = [
5103
+ {
5104
+ entityId: data.entityId,
5105
+ entityParentId: data.entityParentId,
5106
+ comments: data.comments,
5107
+ next: data.next,
5108
+ },
5109
+ ];
5110
+ this.state.next((currentState) => {
5111
+ const newCommentsByEntityId = {
5112
+ ...currentState.comments_by_entity_id,
5113
+ };
5114
+ while (traverseArray.length) {
5115
+ const item = traverseArray.pop();
5116
+ const entityId = item.entityId;
5117
+ // go over entity comments and generate new objects
5118
+ // for further processing if there are any replies
5119
+ item.comments.forEach((comment) => {
5120
+ if (!comment.replies?.length)
5121
+ return;
5122
+ traverseArray.push({
5123
+ entityId: comment.id,
5124
+ entityParentId: entityId,
5125
+ comments: comment.replies,
5126
+ next: comment.meta?.next_cursor,
5127
+ });
5128
+ });
5129
+ // omit replies & meta from the comments (transform ThreadedCommentResponse to CommentResponse)
5130
+ // this is somehow faster than copying the whole
5131
+ // object and deleting the desired properties
5132
+ const newComments = item.comments.map(({ replies: _r, meta: _m, ...restOfTheCommentResponse }) => restOfTheCommentResponse);
5133
+ const existingComments = newCommentsByEntityId[entityId]?.comments;
5134
+ newCommentsByEntityId[entityId] = {
5135
+ ...newCommentsByEntityId[entityId],
5136
+ entity_parent_id: item.entityParentId,
5137
+ pagination: {
5138
+ ...newCommentsByEntityId[entityId]?.pagination,
5139
+ next: item.next,
5140
+ sort: data.sort,
5141
+ },
5142
+ comments: existingComments
5143
+ ? uniqueArrayMerge(existingComments, newComments, (comment) => comment.id)
5144
+ : newComments,
5145
+ };
5146
+ }
5147
+ return {
5148
+ ...currentState,
5149
+ comments_by_entity_id: newCommentsByEntityId,
5150
+ };
5151
+ });
5152
+ }
5153
+ async loadNextPageComments({ entityId, base, sort, entityParentId, }) {
5154
+ let error;
5155
+ try {
5156
+ this.state.next((currentState) => ({
5157
+ ...currentState,
5158
+ comments_by_entity_id: {
5159
+ ...currentState.comments_by_entity_id,
5160
+ [entityId]: {
5161
+ ...currentState.comments_by_entity_id[entityId],
5162
+ pagination: {
5163
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
5164
+ loading_next_page: true,
5165
+ },
5166
+ },
5167
+ },
5168
+ }));
5169
+ const { next, comments } = await base();
5170
+ this.loadCommentsIntoState({
5171
+ entityId,
5172
+ comments,
5173
+ entityParentId,
5174
+ next,
5175
+ sort,
5176
+ });
5177
+ }
5178
+ catch (e) {
5179
+ error = e;
5180
+ }
5181
+ finally {
5182
+ this.state.next((currentState) => ({
5183
+ ...currentState,
5184
+ comments_by_entity_id: {
5185
+ ...currentState.comments_by_entity_id,
5186
+ [entityId]: {
5187
+ ...currentState.comments_by_entity_id[entityId],
5188
+ pagination: {
5189
+ ...currentState.comments_by_entity_id[entityId]?.pagination,
5190
+ loading_next_page: false,
5191
+ },
5192
+ },
5193
+ },
5194
+ }));
5195
+ }
5196
+ if (error) {
5197
+ throw error;
5198
+ }
5199
+ }
5200
+ async loadNextPageActivityComments(activity, request) {
5201
+ const currentEntityState = this.currentState.comments_by_entity_id[activity.id];
5202
+ const currentPagination = currentEntityState?.pagination;
5203
+ const currentNextCursor = currentPagination?.next;
5204
+ const currentSort = currentPagination?.sort;
5205
+ const isLoading = currentPagination?.loading_next_page;
5206
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
5207
+ if (isLoading ||
5208
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
5209
+ return;
5210
+ }
5211
+ await this.loadNextPageComments({
5212
+ entityId: activity.id,
5213
+ base: () => this.client.getComments({
5214
+ ...request,
5215
+ sort,
5216
+ object_id: activity.id,
5217
+ object_type: 'activity',
5218
+ next: currentNextCursor,
5219
+ }),
5220
+ sort,
5221
+ });
5222
+ }
5223
+ async loadNextPageCommentReplies(comment, request) {
5224
+ const currentEntityState = this.currentState.comments_by_entity_id[comment.id];
5225
+ const currentPagination = currentEntityState?.pagination;
5226
+ const currentNextCursor = currentPagination?.next;
5227
+ const currentSort = currentPagination?.sort;
5228
+ const isLoading = currentPagination?.loading_next_page;
5229
+ const sort = currentSort ?? request?.sort ?? Constants.DEFAULT_COMMENT_PAGINATION;
5230
+ if (isLoading ||
5231
+ !checkHasAnotherPage(currentEntityState?.comments, currentNextCursor)) {
5232
+ return;
5233
+ }
5234
+ await this.loadNextPageComments({
5235
+ entityId: comment.id,
5236
+ base: () => this.client.getCommentReplies({
5237
+ ...request,
5238
+ id: comment.id,
5239
+ // use known sort first (prevents broken pagination)
5240
+ sort,
5241
+ next: currentNextCursor,
5242
+ }),
5243
+ entityParentId: comment.parent_id ?? comment.object_id,
5244
+ sort,
5245
+ });
5246
+ }
5247
+ async loadNextPageFollows(type, request) {
5248
+ const paginationKey = `${type}_pagination`;
5249
+ const method = `query${capitalize(type)}`;
5250
+ const currentFollows = this.currentState[type];
5251
+ const currentNextCursor = this.currentState[paginationKey]?.next;
5252
+ const isLoading = this.currentState[paginationKey]?.loading_next_page;
5253
+ const sort = this.currentState[paginationKey]?.sort ?? request.sort;
5254
+ let error;
5255
+ if (isLoading || !checkHasAnotherPage(currentFollows, currentNextCursor)) {
5256
+ return;
5257
+ }
5258
+ try {
5259
+ this.state.next((currentState) => {
5260
+ return {
5261
+ ...currentState,
5262
+ [paginationKey]: {
5263
+ ...currentState[paginationKey],
5264
+ loading_next_page: true,
5265
+ },
5266
+ };
5290
5267
  });
5291
- };
5292
- this.handleVoteRemoved = (event) => {
5293
- if (event.poll?.id && event.poll.id !== this.id)
5294
- return;
5295
- if (!isPollVoteRemovedEvent(event))
5296
- return;
5297
- const currentState = this.data;
5298
- const isOwnVote = event.poll_vote.user_id ===
5299
- this.client.state.getLatestValue().connected_user?.id;
5300
- let latestAnswers = [...currentState.latest_answers];
5301
- let ownAnswer = currentState.own_answer;
5302
- const ownVotesByOptionId = { ...currentState.own_votes_by_option_id };
5303
- let maxVotedOptionIds = currentState.max_voted_option_ids;
5304
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5305
- if (isVoteAnswer(event.poll_vote)) {
5306
- latestAnswers = latestAnswers.filter((answer) => answer.id !== event.poll_vote.id);
5307
- if (isOwnVote) {
5308
- ownAnswer = undefined;
5309
- }
5310
- }
5311
- else {
5312
- maxVotedOptionIds = getMaxVotedOptionIds(event.poll.vote_counts_by_option);
5313
- if (isOwnVote && event.poll_vote.option_id) {
5314
- delete ownVotesByOptionId[event.poll_vote.option_id];
5315
- }
5316
- }
5317
- const { answers_count, latest_votes_by_option, vote_count, vote_counts_by_option, } = event.poll;
5318
- this.state.partialNext({
5319
- answers_count,
5320
- // @ts-expect-error Incompatibility between PollResponseData and Poll due to teams_role, remove when OpenAPI spec is fixed
5321
- latest_votes_by_option,
5322
- vote_count,
5323
- vote_counts_by_option,
5324
- latest_answers: latestAnswers,
5325
- last_activity_at: new Date(event.created_at),
5326
- own_answer: ownAnswer,
5327
- own_votes_by_option_id: ownVotesByOptionId,
5328
- max_voted_option_ids: maxVotedOptionIds,
5268
+ const { next: newNextCursor, follows } = await this[method]({
5269
+ ...request,
5270
+ next: currentNextCursor,
5271
+ sort,
5272
+ });
5273
+ this.state.next((currentState) => {
5274
+ return {
5275
+ ...currentState,
5276
+ [type]: currentState[type] === undefined
5277
+ ? follows
5278
+ : uniqueArrayMerge(currentState[type], follows, (follow) => `${follow.source_feed.feed}-${follow.target_feed.feed}`),
5279
+ [paginationKey]: {
5280
+ ...currentState[paginationKey],
5281
+ next: newNextCursor,
5282
+ sort,
5283
+ },
5284
+ };
5285
+ });
5286
+ }
5287
+ catch (e) {
5288
+ error = e;
5289
+ }
5290
+ finally {
5291
+ this.state.next((currentState) => {
5292
+ return {
5293
+ ...currentState,
5294
+ [paginationKey]: {
5295
+ ...currentState[paginationKey],
5296
+ loading_next_page: false,
5297
+ },
5298
+ };
5299
+ });
5300
+ }
5301
+ if (error) {
5302
+ throw error;
5303
+ }
5304
+ }
5305
+ async loadNextPageFollowers(request) {
5306
+ await this.loadNextPageFollows('followers', request);
5307
+ }
5308
+ async loadNextPageFollowing(request) {
5309
+ await this.loadNextPageFollows('following', request);
5310
+ }
5311
+ async loadNextPageMembers(request) {
5312
+ const currentMembers = this.currentState.members;
5313
+ const currentNextCursor = this.currentState.member_pagination?.next;
5314
+ const isLoading = this.currentState.member_pagination?.loading_next_page;
5315
+ const sort = this.currentState.member_pagination?.sort ?? request.sort;
5316
+ let error;
5317
+ if (isLoading || !checkHasAnotherPage(currentMembers, currentNextCursor)) {
5318
+ return;
5319
+ }
5320
+ try {
5321
+ this.state.next((currentState) => ({
5322
+ ...currentState,
5323
+ member_pagination: {
5324
+ ...currentState.member_pagination,
5325
+ loading_next_page: true,
5326
+ },
5327
+ }));
5328
+ const { next: newNextCursor, members } = await this.client.queryFeedMembers({
5329
+ ...request,
5330
+ sort,
5331
+ feed_id: this.id,
5332
+ feed_group_id: this.group,
5333
+ next: currentNextCursor,
5329
5334
  });
5335
+ this.state.next((currentState) => ({
5336
+ ...currentState,
5337
+ members: currentState.members
5338
+ ? uniqueArrayMerge(currentState.members, members, ({ user }) => user.id)
5339
+ : members,
5340
+ member_pagination: {
5341
+ ...currentState.member_pagination,
5342
+ next: newNextCursor,
5343
+ // set sort if not defined yet
5344
+ sort: currentState.member_pagination?.sort ?? request.sort,
5345
+ },
5346
+ }));
5347
+ }
5348
+ catch (e) {
5349
+ error = e;
5350
+ }
5351
+ finally {
5352
+ this.state.next((currentState) => ({
5353
+ ...currentState,
5354
+ member_pagination: {
5355
+ ...currentState.member_pagination,
5356
+ loading_next_page: false,
5357
+ },
5358
+ }));
5359
+ }
5360
+ if (error) {
5361
+ throw error;
5362
+ }
5363
+ }
5364
+ /**
5365
+ * Method which queries followers of this feed (feeds which target this feed).
5366
+ *
5367
+ * _Note: Useful only for feeds with `groupId` of `user` value._
5368
+ */
5369
+ async queryFollowers(request) {
5370
+ const filter = {
5371
+ target_feed: this.feed,
5330
5372
  };
5331
- this.client = client;
5332
- this.id = poll.id;
5333
- this.state = new StateStore(this.getInitialStateFromPollResponse(poll));
5373
+ const response = await this.client.queryFollows({
5374
+ filter,
5375
+ ...request,
5376
+ });
5377
+ return response;
5334
5378
  }
5335
- get data() {
5336
- return this.state.getLatestValue();
5379
+ /**
5380
+ * Method which queries following of this feed (target feeds of this feed).
5381
+ *
5382
+ * _Note: Useful only for feeds with `groupId` of `timeline` value._
5383
+ */
5384
+ async queryFollowing(request) {
5385
+ const filter = {
5386
+ source_feed: this.feed,
5387
+ };
5388
+ const response = await this.client.queryFollows({
5389
+ filter,
5390
+ ...request,
5391
+ });
5392
+ return response;
5337
5393
  }
5338
- }
5339
- function getMaxVotedOptionIds(voteCountsByOption) {
5340
- let maxVotes = 0;
5341
- let winningOptions = [];
5342
- for (const [id, count] of Object.entries(voteCountsByOption ?? {})) {
5343
- if (count > maxVotes) {
5344
- winningOptions = [id];
5345
- maxVotes = count;
5394
+ async follow(feedOrFid, options) {
5395
+ const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.feed;
5396
+ const response = await this.client.follow({
5397
+ ...options,
5398
+ source: this.feed,
5399
+ target: fid,
5400
+ });
5401
+ return response;
5402
+ }
5403
+ async unfollow(feedOrFid) {
5404
+ const fid = typeof feedOrFid === 'string' ? feedOrFid : feedOrFid.feed;
5405
+ const response = await this.client.unfollow({
5406
+ source: this.feed,
5407
+ target: fid,
5408
+ });
5409
+ return response;
5410
+ }
5411
+ async getNextPage() {
5412
+ const currentState = this.currentState;
5413
+ return await this.getOrCreate({
5414
+ member_pagination: {
5415
+ limit: 0,
5416
+ },
5417
+ followers_pagination: {
5418
+ limit: 0,
5419
+ },
5420
+ following_pagination: {
5421
+ limit: 0,
5422
+ },
5423
+ next: currentState.next,
5424
+ limit: currentState.last_get_or_create_request_config?.limit ?? 20,
5425
+ });
5426
+ }
5427
+ addActivity(request) {
5428
+ return this.feedsApi.addActivity({
5429
+ ...request,
5430
+ feeds: [this.feed],
5431
+ });
5432
+ }
5433
+ handleWSEvent(event) {
5434
+ const eventHandler = this.eventHandlers[event.type];
5435
+ // no need to run noop function
5436
+ if (eventHandler !== Feed.noop) {
5437
+ // @ts-expect-error intersection of handler arguments results to never
5438
+ eventHandler?.(event);
5346
5439
  }
5347
- else if (count === maxVotes) {
5348
- winningOptions.push(id);
5440
+ if (typeof eventHandler === 'undefined') {
5441
+ console.warn(`Received unknown event type: ${event.type}`, event);
5349
5442
  }
5443
+ this.eventDispatcher.dispatch(event);
5350
5444
  }
5351
- return winningOptions;
5352
5445
  }
5353
- function getOwnVotesByOptionId(ownVotes) {
5354
- return !ownVotes
5355
- ? {}
5356
- : ownVotes.reduce((acc, vote) => {
5357
- if (isVoteAnswer(vote) || !vote.option_id)
5358
- return acc;
5359
- acc[vote.option_id] = vote;
5360
- return acc;
5361
- }, {});
5446
+ Feed.noop = () => { };
5447
+
5448
+ function handleUserUpdated(event) {
5449
+ this.state.next((currentState) => {
5450
+ let newState;
5451
+ const { connected_user } = currentState;
5452
+ if (connected_user && connected_user.id === event.user.id) {
5453
+ newState ?? (newState = {
5454
+ ...currentState,
5455
+ });
5456
+ newState.connected_user = {
5457
+ ...connected_user,
5458
+ ...event.user,
5459
+ };
5460
+ }
5461
+ // TODO: update other users in user map (if/once applicable)
5462
+ return newState ?? currentState;
5463
+ });
5362
5464
  }
5363
5465
 
5364
5466
  class FeedsClient extends FeedsApi {
@@ -5468,7 +5570,7 @@ class FeedsClient extends FeedsApi {
5468
5570
  if (this.activeFeeds[fid]) {
5469
5571
  const feed = this.activeFeeds[fid];
5470
5572
  if (watch && !feed.currentState.watch) {
5471
- feed.handleWatchStarted();
5573
+ handleWatchStarted.bind(feed)();
5472
5574
  }
5473
5575
  return feed;
5474
5576
  }
@@ -5504,7 +5606,7 @@ class FeedsClient extends FeedsApi {
5504
5606
  }
5505
5607
  else {
5506
5608
  for (const activeFeed of Object.values(this.activeFeeds)) {
5507
- activeFeed.handleWatchStopped();
5609
+ handleWatchStopped.bind(activeFeed)();
5508
5610
  }
5509
5611
  }
5510
5612
  break;
@@ -5584,6 +5686,10 @@ class FeedsClient extends FeedsApi {
5584
5686
  feeds.forEach((f) => f.handleWSEvent(event));
5585
5687
  break;
5586
5688
  }
5689
+ case 'user.updated': {
5690
+ handleUserUpdated.call(this, event);
5691
+ break;
5692
+ }
5587
5693
  default: {
5588
5694
  feed?.handleWSEvent(event);
5589
5695
  }
@@ -5609,7 +5715,7 @@ class FeedsClient extends FeedsApi {
5609
5715
  }
5610
5716
  }
5611
5717
  async queryFeeds(request) {
5612
- const response = await this.feedsQueryFeeds(request);
5718
+ const response = await this._queryFeeds(request);
5613
5719
  const feeds = response.feeds.map((f) => this.getOrCreateActiveFeed(f.group_id, f.id, f, request?.watch));
5614
5720
  return {
5615
5721
  feeds,
@@ -5619,13 +5725,29 @@ class FeedsClient extends FeedsApi {
5619
5725
  duration: response.duration,
5620
5726
  };
5621
5727
  }
5728
+ async updateFollow(request) {
5729
+ const response = await super.updateFollow(request);
5730
+ [
5731
+ response.follow.source_feed.feed,
5732
+ response.follow.target_feed.feed,
5733
+ ].forEach((fid) => {
5734
+ const feed = this.activeFeeds[fid];
5735
+ if (feed) {
5736
+ handleFollowUpdated.bind(feed)(response);
5737
+ }
5738
+ });
5739
+ return response;
5740
+ }
5622
5741
  // For follow API endpoints we update the state after HTTP response to allow queryFeeds with watch: false
5623
5742
  async follow(request) {
5624
5743
  const response = await super.follow(request);
5625
- [response.follow.source_feed.fid, response.follow.target_feed.fid].forEach((fid) => {
5744
+ [
5745
+ response.follow.source_feed.feed,
5746
+ response.follow.target_feed.feed,
5747
+ ].forEach((fid) => {
5626
5748
  const feed = this.activeFeeds[fid];
5627
5749
  if (feed) {
5628
- feed.handleFollowCreated(response.follow);
5750
+ handleFollowCreated.bind(feed)(response);
5629
5751
  }
5630
5752
  });
5631
5753
  return response;
@@ -5633,9 +5755,9 @@ class FeedsClient extends FeedsApi {
5633
5755
  async followBatch(request) {
5634
5756
  const response = await super.followBatch(request);
5635
5757
  response.follows.forEach((follow) => {
5636
- const feed = this.activeFeeds[follow.source_feed.fid];
5758
+ const feed = this.activeFeeds[follow.source_feed.feed];
5637
5759
  if (feed) {
5638
- feed.handleFollowCreated(follow);
5760
+ handleFollowCreated.bind(feed)({ follow });
5639
5761
  }
5640
5762
  });
5641
5763
  return response;
@@ -5645,10 +5767,7 @@ class FeedsClient extends FeedsApi {
5645
5767
  [request.source, request.target].forEach((fid) => {
5646
5768
  const feed = this.activeFeeds[fid];
5647
5769
  if (feed) {
5648
- feed.handleFollowDeleted({
5649
- source_feed: { fid: request.source },
5650
- target_feed: { fid: request.target },
5651
- });
5770
+ handleFollowDeleted.bind(feed)(response);
5652
5771
  }
5653
5772
  });
5654
5773
  return response;
@@ -5661,7 +5780,7 @@ class FeedsClient extends FeedsApi {
5661
5780
  });
5662
5781
  const feed = this.activeFeeds[`${request.feed_group_id}:${request.feed_id}`];
5663
5782
  if (feed) {
5664
- feed.handleWatchStopped();
5783
+ handleWatchStopped.bind(feed)();
5665
5784
  }
5666
5785
  return response;
5667
5786
  }
@@ -6055,12 +6174,12 @@ const useReactionActions = ({ entity, type, }) => {
6055
6174
  const hasOwnReaction = useMemo(() => !!entity.own_reactions?.find((r) => r.type === type), [entity.own_reactions, type]);
6056
6175
  const addReaction = useStableCallback(async () => {
6057
6176
  await (isComment
6058
- ? client?.addCommentReaction({ comment_id: entity.id, type })
6177
+ ? client?.addCommentReaction({ id: entity.id, type })
6059
6178
  : client?.addReaction({ activity_id: entity.id, type }));
6060
6179
  });
6061
6180
  const removeReaction = useStableCallback(async () => {
6062
6181
  await (isComment
6063
- ? client?.deleteCommentReaction({ comment_id: entity.id, type })
6182
+ ? client?.deleteCommentReaction({ id: entity.id, type })
6064
6183
  : client?.deleteActivityReaction({
6065
6184
  activity_id: entity.id,
6066
6185
  type,