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