@stream-io/feeds-client 0.2.11 → 0.2.13

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 (186) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/index.js +1 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/react-bindings.js +56 -46
  5. package/dist/cjs/react-bindings.js.map +1 -1
  6. package/dist/es/index.mjs +2 -2
  7. package/dist/es/index.mjs.map +1 -1
  8. package/dist/es/react-bindings.mjs +56 -46
  9. package/dist/es/react-bindings.mjs.map +1 -1
  10. package/dist/{index-CFv0uza2.js → index-RzB4c4g6.js} +98 -28
  11. package/dist/index-RzB4c4g6.js.map +1 -0
  12. package/dist/{index-DP0C8psw.mjs → index-gvcJhGPH.mjs} +98 -28
  13. package/dist/index-gvcJhGPH.mjs.map +1 -0
  14. package/dist/tsconfig.tsbuildinfo +1 -1
  15. package/dist/types/bindings/react/hooks/feed-state-hooks/useAggregatedActivities.d.ts +9 -4
  16. package/dist/types/bindings/react/hooks/feed-state-hooks/useAggregatedActivities.d.ts.map +1 -1
  17. package/dist/types/bindings/react/hooks/feed-state-hooks/useIsAggregatedActivityRead.d.ts.map +1 -1
  18. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.d.ts +15 -16
  19. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.d.ts.map +1 -1
  20. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnFollows.d.ts +1 -1
  21. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnFollows.d.ts.map +1 -1
  22. package/dist/types/bindings/react/hooks/util/useReactionActions.d.ts +1 -1
  23. package/dist/types/bindings/react/hooks/util/useReactionActions.d.ts.map +1 -1
  24. package/dist/types/bindings/react/wrappers/StreamFeed.d.ts +1 -1
  25. package/dist/types/bindings/react/wrappers/StreamFeed.d.ts.map +1 -1
  26. package/dist/types/bindings/react/wrappers/StreamFeeds.d.ts +1 -1
  27. package/dist/types/bindings/react/wrappers/StreamFeeds.d.ts.map +1 -1
  28. package/dist/types/bindings/react/wrappers/StreamSearch.d.ts +1 -1
  29. package/dist/types/bindings/react/wrappers/StreamSearch.d.ts.map +1 -1
  30. package/dist/types/bindings/react/wrappers/StreamSearchResults.d.ts +1 -1
  31. package/dist/types/bindings/react/wrappers/StreamSearchResults.d.ts.map +1 -1
  32. package/dist/types/common/ApiClient.d.ts +3 -3
  33. package/dist/types/common/ApiClient.d.ts.map +1 -1
  34. package/dist/types/common/EventDispatcher.d.ts +1 -1
  35. package/dist/types/common/EventDispatcher.d.ts.map +1 -1
  36. package/dist/types/common/rate-limit.d.ts +1 -1
  37. package/dist/types/common/rate-limit.d.ts.map +1 -1
  38. package/dist/types/common/real-time/StableWSConnection.d.ts +3 -3
  39. package/dist/types/common/real-time/StableWSConnection.d.ts.map +1 -1
  40. package/dist/types/common/real-time/event-models.d.ts +1 -1
  41. package/dist/types/common/real-time/event-models.d.ts.map +1 -1
  42. package/dist/types/common/search/ActivitySearchSource.d.ts +2 -2
  43. package/dist/types/common/search/ActivitySearchSource.d.ts.map +1 -1
  44. package/dist/types/common/search/FeedSearchSource.d.ts +2 -2
  45. package/dist/types/common/search/FeedSearchSource.d.ts.map +1 -1
  46. package/dist/types/common/search/UserSearchSource.d.ts +2 -2
  47. package/dist/types/common/search/UserSearchSource.d.ts.map +1 -1
  48. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts +3 -3
  49. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts.map +1 -1
  50. package/dist/types/feed/event-handlers/activity/handle-activity-marked.d.ts +3 -3
  51. package/dist/types/feed/event-handlers/activity/handle-activity-marked.d.ts.map +1 -1
  52. package/dist/types/feed/event-handlers/activity/handle-activity-pinned.d.ts +2 -2
  53. package/dist/types/feed/event-handlers/activity/handle-activity-pinned.d.ts.map +1 -1
  54. package/dist/types/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +1 -1
  55. package/dist/types/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts.map +1 -1
  56. package/dist/types/feed/event-handlers/activity/handle-activity-removed-from-feed.d.ts +2 -2
  57. package/dist/types/feed/event-handlers/activity/handle-activity-removed-from-feed.d.ts.map +1 -1
  58. package/dist/types/feed/event-handlers/activity/handle-activity-unpinned.d.ts +2 -2
  59. package/dist/types/feed/event-handlers/activity/handle-activity-unpinned.d.ts.map +1 -1
  60. package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts +3 -3
  61. package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts.map +1 -1
  62. package/dist/types/feed/event-handlers/comment/handle-comment-deleted.d.ts +3 -2
  63. package/dist/types/feed/event-handlers/comment/handle-comment-deleted.d.ts.map +1 -1
  64. package/dist/types/feed/event-handlers/comment/handle-comment-reaction-added.d.ts +3 -2
  65. package/dist/types/feed/event-handlers/comment/handle-comment-reaction-added.d.ts.map +1 -1
  66. package/dist/types/feed/event-handlers/comment/handle-comment-reaction-deleted.d.ts +3 -2
  67. package/dist/types/feed/event-handlers/comment/handle-comment-reaction-deleted.d.ts.map +1 -1
  68. package/dist/types/feed/event-handlers/comment/handle-comment-updated.d.ts +3 -2
  69. package/dist/types/feed/event-handlers/comment/handle-comment-updated.d.ts.map +1 -1
  70. package/dist/types/feed/event-handlers/comment/utils/update-comment-count.d.ts +2 -2
  71. package/dist/types/feed/event-handlers/comment/utils/update-comment-count.d.ts.map +1 -1
  72. package/dist/types/feed/event-handlers/feed/handle-feed-updated.d.ts +2 -2
  73. package/dist/types/feed/event-handlers/feed/handle-feed-updated.d.ts.map +1 -1
  74. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-added.d.ts +2 -2
  75. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-added.d.ts.map +1 -1
  76. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-removed.d.ts +2 -2
  77. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-removed.d.ts.map +1 -1
  78. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-updated.d.ts +2 -2
  79. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-updated.d.ts.map +1 -1
  80. package/dist/types/feed/event-handlers/follow/handle-follow-updated.d.ts +1 -1
  81. package/dist/types/feed/event-handlers/follow/handle-follow-updated.d.ts.map +1 -1
  82. package/dist/types/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +9 -2
  83. package/dist/types/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts.map +1 -1
  84. package/dist/types/feed/event-handlers/watch/handle-watch-started.d.ts +1 -1
  85. package/dist/types/feed/event-handlers/watch/handle-watch-started.d.ts.map +1 -1
  86. package/dist/types/feed/event-handlers/watch/handle-watch-stopped.d.ts +1 -1
  87. package/dist/types/feed/event-handlers/watch/handle-watch-stopped.d.ts.map +1 -1
  88. package/dist/types/feed/feed.d.ts +3 -3
  89. package/dist/types/feed/feed.d.ts.map +1 -1
  90. package/dist/types/feeds-client/feeds-client.d.ts +3 -3
  91. package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
  92. package/dist/types/gen/feeds/FeedApi.d.ts +2 -2
  93. package/dist/types/gen/feeds/FeedApi.d.ts.map +1 -1
  94. package/dist/types/gen/feeds/FeedsApi.d.ts +2 -2
  95. package/dist/types/gen/feeds/FeedsApi.d.ts.map +1 -1
  96. package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts +1 -1
  97. package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts.map +1 -1
  98. package/dist/types/gen/models/index.d.ts +24 -26
  99. package/dist/types/gen/models/index.d.ts.map +1 -1
  100. package/dist/types/gen/moderation/ModerationApi.d.ts +2 -2
  101. package/dist/types/gen/moderation/ModerationApi.d.ts.map +1 -1
  102. package/dist/types/types-internal.d.ts +1 -1
  103. package/dist/types/types-internal.d.ts.map +1 -1
  104. package/dist/types/types.d.ts +4 -4
  105. package/dist/types/types.d.ts.map +1 -1
  106. package/dist/types/utils/event-triggered-by-connected-user.d.ts +2 -2
  107. package/dist/types/utils/event-triggered-by-connected-user.d.ts.map +1 -1
  108. package/dist/types/utils/state-update-queue.d.ts +2 -2
  109. package/dist/types/utils/state-update-queue.d.ts.map +1 -1
  110. package/dist/types/utils/type-assertions.d.ts +3 -4
  111. package/dist/types/utils/type-assertions.d.ts.map +1 -1
  112. package/package.json +1 -1
  113. package/src/bindings/react/hooks/feed-state-hooks/useAggregatedActivities.ts +28 -4
  114. package/src/bindings/react/hooks/feed-state-hooks/useIsAggregatedActivityRead.ts +4 -5
  115. package/src/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.ts +52 -38
  116. package/src/bindings/react/hooks/feed-state-hooks/useOwnFollows.ts +1 -1
  117. package/src/bindings/react/hooks/util/useReactionActions.ts +2 -1
  118. package/src/bindings/react/wrappers/StreamFeed.tsx +1 -1
  119. package/src/bindings/react/wrappers/StreamFeeds.tsx +1 -1
  120. package/src/bindings/react/wrappers/StreamSearch.tsx +1 -1
  121. package/src/bindings/react/wrappers/StreamSearchResults.tsx +1 -1
  122. package/src/common/ApiClient.ts +6 -4
  123. package/src/common/EventDispatcher.ts +1 -1
  124. package/src/common/rate-limit.ts +1 -1
  125. package/src/common/real-time/StableWSConnection.ts +3 -3
  126. package/src/common/real-time/event-models.ts +1 -1
  127. package/src/common/search/ActivitySearchSource.ts +2 -2
  128. package/src/common/search/FeedSearchSource.ts +2 -2
  129. package/src/common/search/UserSearchSource.ts +2 -2
  130. package/src/feed/event-handlers/activity/activity-marked-utils.test.ts +1 -1
  131. package/src/feed/event-handlers/activity/activity-reaction-utils.test.ts +1 -1
  132. package/src/feed/event-handlers/activity/activity-utils.test.ts +1 -1
  133. package/src/feed/event-handlers/activity/handle-activity-added.ts +3 -3
  134. package/src/feed/event-handlers/activity/handle-activity-marked.ts +3 -3
  135. package/src/feed/event-handlers/activity/handle-activity-pinned.test.ts +1 -1
  136. package/src/feed/event-handlers/activity/handle-activity-pinned.ts +3 -3
  137. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +1 -1
  138. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +1 -1
  139. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +1 -1
  140. package/src/feed/event-handlers/activity/handle-activity-removed-from-feed.ts +2 -2
  141. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +2 -2
  142. package/src/feed/event-handlers/activity/handle-activity-unpinned.ts +2 -2
  143. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +2 -2
  144. package/src/feed/event-handlers/activity/handle-activity-updated.ts +3 -3
  145. package/src/feed/event-handlers/bookmark/bookmark-utils.test.ts +1 -1
  146. package/src/feed/event-handlers/comment/handle-comment-added.test.ts +2 -2
  147. package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +2 -2
  148. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +3 -2
  149. package/src/feed/event-handlers/comment/handle-comment-reaction-added.test.ts +2 -2
  150. package/src/feed/event-handlers/comment/handle-comment-reaction-added.ts +3 -2
  151. package/src/feed/event-handlers/comment/handle-comment-reaction-deleted.test.ts +2 -2
  152. package/src/feed/event-handlers/comment/handle-comment-reaction-deleted.ts +3 -2
  153. package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +2 -2
  154. package/src/feed/event-handlers/comment/handle-comment-updated.ts +3 -2
  155. package/src/feed/event-handlers/comment/utils/update-comment-count.test.ts +3 -1
  156. package/src/feed/event-handlers/comment/utils/update-comment-count.ts +2 -2
  157. package/src/feed/event-handlers/feed/handle-feed-updated.ts +2 -2
  158. package/src/feed/event-handlers/feed-member/handle-feed-member-added.ts +2 -2
  159. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +2 -2
  160. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.ts +2 -2
  161. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +1 -1
  162. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +1 -1
  163. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +1 -1
  164. package/src/feed/event-handlers/follow/handle-follow-updated.ts +1 -1
  165. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +310 -12
  166. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +90 -7
  167. package/src/feed/event-handlers/watch/handle-watch-started.ts +1 -1
  168. package/src/feed/event-handlers/watch/handle-watch-stopped.ts +1 -1
  169. package/src/feed/feed.test.ts +1 -1
  170. package/src/feed/feed.ts +24 -4
  171. package/src/feeds-client/event-handlers/user/handle-user-updated.test.ts +1 -1
  172. package/src/feeds-client/feeds-client.ts +5 -4
  173. package/src/gen/feeds/FeedApi.ts +2 -2
  174. package/src/gen/feeds/FeedsApi.ts +2 -2
  175. package/src/gen/model-decoders/decoders.ts +2 -4
  176. package/src/gen/model-decoders/event-decoder-mapping.ts +1 -1
  177. package/src/gen/models/index.ts +34 -33
  178. package/src/gen/moderation/ModerationApi.ts +2 -2
  179. package/src/test-utils/response-generators.ts +2 -2
  180. package/src/types-internal.ts +1 -1
  181. package/src/types.ts +4 -4
  182. package/src/utils/event-triggered-by-connected-user.ts +2 -2
  183. package/src/utils/state-update-queue.ts +2 -2
  184. package/src/utils/type-assertions.ts +7 -11
  185. package/dist/index-CFv0uza2.js.map +0 -1
  186. package/dist/index-DP0C8psw.mjs.map +0 -1
@@ -5,7 +5,7 @@ import * as commentHandlers from '../handle-comment-updated';
5
5
  import * as activityHandlers from '../../activity';
6
6
  import { FeedsClient } from '../../../../feeds-client';
7
7
  import { Feed } from '../../../feed';
8
- import { ActivityResponse, CommentResponse } from '../../../../gen/models';
8
+ import type { ActivityResponse, CommentResponse } from '../../../../gen/models';
9
9
  import {
10
10
  generateCommentResponse,
11
11
  generateFeedResponse,
@@ -14,6 +14,7 @@ import {
14
14
  } from '../../../../test-utils';
15
15
 
16
16
  vi.mock('../../activity', async (importOriginal) => {
17
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
17
18
  const actual = await importOriginal<typeof import('../../activity')>();
18
19
  return {
19
20
  ...actual,
@@ -22,6 +23,7 @@ vi.mock('../../activity', async (importOriginal) => {
22
23
  });
23
24
 
24
25
  vi.mock('../handle-comment-updated', async (importOriginal) => {
26
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
25
27
  const actual = await importOriginal<typeof import('../handle-comment-updated')>();
26
28
  return {
27
29
  ...actual,
@@ -1,5 +1,5 @@
1
- import { ActivityResponse, Feed } from '@self';
2
- import { CommentResponse } from '@self';
1
+ import type { ActivityResponse, Feed } from '@self';
2
+ import type { CommentResponse } from '@self';
3
3
  import { handleCommentUpdated } from '../handle-comment-updated';
4
4
  import { handleActivityUpdated } from '../../activity';
5
5
 
@@ -1,5 +1,5 @@
1
- import { Feed } from '../../../feed';
2
- import { EventPayload, PartializeAllBut } from '../../../types-internal';
1
+ import type { Feed } from '../../../feed';
2
+ import type { EventPayload, PartializeAllBut } from '../../../types-internal';
3
3
 
4
4
  export function handleFeedUpdated(
5
5
  this: Feed,
@@ -1,5 +1,5 @@
1
- import { Feed, FeedState } from '../../../feed';
2
- import { EventPayload } from '../../../types-internal';
1
+ import type { Feed, FeedState } from '../../../feed';
2
+ import type { EventPayload } from '../../../types-internal';
3
3
 
4
4
  export function handleFeedMemberAdded(
5
5
  this: Feed,
@@ -1,5 +1,5 @@
1
- import { Feed, FeedState } from '../../../feed';
2
- import { EventPayload } from '../../../types-internal';
1
+ import type { Feed, FeedState } from '../../../feed';
2
+ import type { EventPayload } from '../../../types-internal';
3
3
 
4
4
  export function handleFeedMemberRemoved(
5
5
  this: Feed,
@@ -1,5 +1,5 @@
1
- import { Feed, FeedState } from '../../../feed';
2
- import { EventPayload } from '../../../types-internal';
1
+ import type { Feed, FeedState } from '../../../feed';
2
+ import type { EventPayload } from '../../../types-internal';
3
3
 
4
4
  export function handleFeedMemberUpdated(
5
5
  this: Feed,
@@ -1,4 +1,4 @@
1
- import {
1
+ import type {
2
2
  FeedResponse,
3
3
  FollowResponse,
4
4
  UserResponse,
@@ -1,4 +1,4 @@
1
- import {
1
+ import type {
2
2
  FollowResponse,
3
3
  FeedResponse,
4
4
  UserResponse,
@@ -8,7 +8,7 @@ import {
8
8
  getHumanId,
9
9
  generateOwnUser,
10
10
  } from '../../../test-utils/response-generators';
11
- import { FollowResponse } from '../../../gen/models';
11
+ import type { FollowResponse } from '../../../gen/models';
12
12
 
13
13
  describe(handleFollowUpdated.name, () => {
14
14
  let feed: Feed;
@@ -1,6 +1,6 @@
1
1
  import type { Feed, FeedState } from '../../feed';
2
2
  import { getStateUpdateQueueId, shouldUpdateState } from '../../../utils';
3
- import { EventPayload, PartializeAllBut } from '../../../types-internal';
3
+ import type { EventPayload, PartializeAllBut } from '../../../types-internal';
4
4
 
5
5
  export function handleFollowUpdated(
6
6
  this: Feed,
@@ -1,10 +1,14 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import {
2
+ import type {
3
3
  NotificationFeedUpdatedEvent,
4
4
  NotificationStatusResponse,
5
5
  AggregatedActivityResponse,
6
6
  } from '../../../gen/models';
7
- import { updateNotificationFeedFromEvent } from './handle-notification-feed-updated';
7
+ import {
8
+ updateNotificationFeedFromEvent,
9
+ addAggregatedActivitiesToState,
10
+ updateNotificationStatus,
11
+ } from './handle-notification-feed-updated';
8
12
 
9
13
  const createMockNotificationFeedUpdatedEvent = (
10
14
  overrides: Partial<NotificationFeedUpdatedEvent> = {},
@@ -30,6 +34,7 @@ const createMockAggregatedActivity = (
30
34
  activity_count: 1,
31
35
  created_at: new Date(),
32
36
  group: 'test-group',
37
+ user_count_truncated: false,
33
38
  score: 1,
34
39
  updated_at: new Date(),
35
40
  user_count: 1,
@@ -42,7 +47,34 @@ describe('notification-feed-utils', () => {
42
47
  it('should return unchanged if event has no notification_status or aggregated_activities', () => {
43
48
  const event = createMockNotificationFeedUpdatedEvent();
44
49
 
45
- const result = updateNotificationFeedFromEvent(event);
50
+ const result = updateNotificationFeedFromEvent(event, [], {
51
+ unread: 0,
52
+ unseen: 0,
53
+ read_activities: [],
54
+ seen_activities: [],
55
+ });
56
+
57
+ expect(result.changed).toBe(false);
58
+ });
59
+
60
+ it(`shouldn't update notification_status when event has notification_status but currentNotificationStatus is undefined`, () => {
61
+ const event = createMockNotificationFeedUpdatedEvent({
62
+ notification_status: createMockNotificationStatus(),
63
+ });
64
+ const result = updateNotificationFeedFromEvent(event, [], undefined);
65
+
66
+ expect(result.changed).toBe(false);
67
+ });
68
+
69
+ it(`shouldn't update aggregated_activities when event has aggregated_activities but currentAggregatedActivities is undefined`, () => {
70
+ const event = createMockNotificationFeedUpdatedEvent({
71
+ aggregated_activities: [createMockAggregatedActivity()],
72
+ });
73
+ const result = updateNotificationFeedFromEvent(
74
+ event,
75
+ undefined,
76
+ createMockNotificationStatus(),
77
+ );
46
78
 
47
79
  expect(result.changed).toBe(false);
48
80
  });
@@ -52,15 +84,23 @@ describe('notification-feed-utils', () => {
52
84
  unread: 5,
53
85
  unseen: 3,
54
86
  read_activities: ['activity1', 'activity2'],
87
+ seen_activities: [],
55
88
  });
56
89
  const event = createMockNotificationFeedUpdatedEvent({
57
90
  notification_status: notificationStatus,
58
91
  });
59
92
 
60
- const result = updateNotificationFeedFromEvent(event);
93
+ const result = updateNotificationFeedFromEvent(event, [], {
94
+ unread: 0,
95
+ unseen: 0,
96
+ read_activities: [],
97
+ seen_activities: [],
98
+ });
61
99
 
62
100
  expect(result.changed).toBe(true);
63
- expect(result.data?.notification_status).toBe(notificationStatus);
101
+ expect(result.data?.notification_status).toStrictEqual(
102
+ notificationStatus,
103
+ );
64
104
  expect(result.data?.aggregated_activities).toBeUndefined();
65
105
  });
66
106
 
@@ -73,10 +113,17 @@ describe('notification-feed-utils', () => {
73
113
  aggregated_activities: aggregatedActivities,
74
114
  });
75
115
 
76
- const result = updateNotificationFeedFromEvent(event);
116
+ const result = updateNotificationFeedFromEvent(event, [], {
117
+ unread: 0,
118
+ unseen: 0,
119
+ read_activities: [],
120
+ seen_activities: [],
121
+ });
77
122
 
78
123
  expect(result.changed).toBe(true);
79
- expect(result.data?.aggregated_activities).toBe(aggregatedActivities);
124
+ expect(result.data?.aggregated_activities).toStrictEqual(
125
+ aggregatedActivities,
126
+ );
80
127
  expect(result.data?.notification_status).toBeUndefined();
81
128
  });
82
129
 
@@ -93,11 +140,23 @@ describe('notification-feed-utils', () => {
93
140
  aggregated_activities: aggregatedActivities,
94
141
  });
95
142
 
96
- const result = updateNotificationFeedFromEvent(event);
143
+ const result = updateNotificationFeedFromEvent(event, [], {
144
+ unread: 0,
145
+ unseen: 0,
146
+ read_activities: [],
147
+ seen_activities: [],
148
+ });
97
149
 
98
150
  expect(result.changed).toBe(true);
99
- expect(result.data?.notification_status).toBe(notificationStatus);
100
- expect(result.data?.aggregated_activities).toBe(aggregatedActivities);
151
+ expect(result.data?.notification_status?.unread).toBe(
152
+ notificationStatus.unread,
153
+ );
154
+ expect(result.data?.notification_status?.unseen).toBe(
155
+ notificationStatus.unseen,
156
+ );
157
+ expect(result.data?.aggregated_activities).toStrictEqual(
158
+ aggregatedActivities,
159
+ );
101
160
  });
102
161
 
103
162
  it('should handle notification_status with all fields', () => {
@@ -105,16 +164,255 @@ describe('notification-feed-utils', () => {
105
164
  unread: 10,
106
165
  unseen: 5,
107
166
  last_seen_at: new Date('2023-01-01'),
167
+ seen_activities: [],
108
168
  read_activities: ['activity1', 'activity2', 'activity3'],
109
169
  });
110
170
  const event = createMockNotificationFeedUpdatedEvent({
111
171
  notification_status: notificationStatus,
112
172
  });
113
173
 
114
- const result = updateNotificationFeedFromEvent(event);
174
+ const result = updateNotificationFeedFromEvent(event, [], {
175
+ unread: 0,
176
+ unseen: 0,
177
+ read_activities: [],
178
+ seen_activities: [],
179
+ });
180
+
181
+ expect(result.changed).toBe(true);
182
+ expect(result.data?.notification_status).toStrictEqual(
183
+ notificationStatus,
184
+ );
185
+ });
186
+ });
187
+
188
+ describe('addAggregatedActivitiesToState', () => {
189
+ it('should add new activities when none exist', () => {
190
+ const newActivities = [
191
+ createMockAggregatedActivity({ group: 'group1' }),
192
+ createMockAggregatedActivity({ group: 'group2' }),
193
+ ];
194
+
195
+ const result = addAggregatedActivitiesToState(
196
+ newActivities,
197
+ undefined,
198
+ 'start',
199
+ );
200
+
201
+ expect(result.changed).toBe(true);
202
+ expect(result.aggregated_activities).toStrictEqual(newActivities);
203
+ });
204
+
205
+ it('should add new activities to existing ones', () => {
206
+ const existingActivities = [
207
+ createMockAggregatedActivity({ group: 'existing1' }),
208
+ ];
209
+ const newActivities = [
210
+ createMockAggregatedActivity({ group: 'new1' }),
211
+ createMockAggregatedActivity({ group: 'new2' }),
212
+ ];
213
+
214
+ const result = addAggregatedActivitiesToState(
215
+ newActivities,
216
+ existingActivities,
217
+ 'start',
218
+ );
219
+
220
+ expect(result.changed).toBe(true);
221
+ expect(result.aggregated_activities).toStrictEqual([
222
+ ...newActivities,
223
+ ...existingActivities,
224
+ ]);
225
+ });
226
+
227
+ it('should add new activities at the end when position is end', () => {
228
+ const existingActivities = [
229
+ createMockAggregatedActivity({ group: 'existing1' }),
230
+ ];
231
+ const newActivities = [createMockAggregatedActivity({ group: 'new1' })];
232
+
233
+ const result = addAggregatedActivitiesToState(
234
+ newActivities,
235
+ existingActivities,
236
+ 'end',
237
+ );
238
+
239
+ expect(result.changed).toBe(true);
240
+ expect(result.aggregated_activities).toStrictEqual([
241
+ ...existingActivities,
242
+ ...newActivities,
243
+ ]);
244
+ });
245
+
246
+ it('should update existing activities with same group (upsert)', () => {
247
+ const baseDate = new Date('2023-01-01');
248
+ const existingActivities = [
249
+ createMockAggregatedActivity({
250
+ group: 'group1',
251
+ activity_count: 1,
252
+ score: 10,
253
+ updated_at: baseDate,
254
+ }),
255
+ createMockAggregatedActivity({
256
+ group: 'group2',
257
+ activity_count: 2,
258
+ score: 20,
259
+ }),
260
+ ];
261
+ const newActivities = [
262
+ createMockAggregatedActivity({
263
+ group: 'group1',
264
+ activity_count: 3,
265
+ score: 30,
266
+ updated_at: new Date('2023-01-02'),
267
+ }),
268
+ createMockAggregatedActivity({
269
+ group: 'group3',
270
+ activity_count: 4,
271
+ score: 40,
272
+ }),
273
+ ];
274
+
275
+ const result = addAggregatedActivitiesToState(
276
+ newActivities,
277
+ existingActivities,
278
+ 'start',
279
+ );
280
+
281
+ expect(result.changed).toBe(true);
282
+ expect(result.aggregated_activities).toHaveLength(3);
283
+
284
+ // Check that group1 was updated
285
+ const updatedGroup1 = result.aggregated_activities.find(
286
+ (a) => a.group === 'group1',
287
+ );
288
+ expect(updatedGroup1?.activity_count).toBe(3);
289
+ expect(updatedGroup1?.score).toBe(30);
290
+ expect(updatedGroup1?.updated_at).toEqual(new Date('2023-01-02'));
291
+
292
+ // Check that group2 remains unchanged
293
+ const unchangedGroup2 = result.aggregated_activities.find(
294
+ (a) => a.group === 'group2',
295
+ );
296
+ expect(unchangedGroup2?.activity_count).toBe(2);
297
+ expect(unchangedGroup2?.score).toBe(20);
298
+
299
+ // Check that group3 was added
300
+ const newGroup3 = result.aggregated_activities.find(
301
+ (a) => a.group === 'group3',
302
+ );
303
+ expect(newGroup3?.activity_count).toBe(4);
304
+ expect(newGroup3?.score).toBe(40);
305
+ });
306
+
307
+ it('should handle mixed new and existing activities', () => {
308
+ const existingActivities = [
309
+ createMockAggregatedActivity({ group: 'existing1' }),
310
+ createMockAggregatedActivity({ group: 'existing2' }),
311
+ ];
312
+ const newActivities = [
313
+ createMockAggregatedActivity({ group: 'existing1', activity_count: 5 }), // Update existing
314
+ createMockAggregatedActivity({ group: 'new1' }), // Add new
315
+ createMockAggregatedActivity({ group: 'existing2', score: 100 }), // Update existing
316
+ createMockAggregatedActivity({ group: 'new2' }), // Add new
317
+ ];
318
+
319
+ const result = addAggregatedActivitiesToState(
320
+ newActivities,
321
+ existingActivities,
322
+ 'start',
323
+ );
115
324
 
116
325
  expect(result.changed).toBe(true);
117
- expect(result.data?.notification_status).toBe(notificationStatus);
326
+ expect(result.aggregated_activities).toHaveLength(4);
327
+
328
+ // Check that existing1 was updated
329
+ const updatedExisting1 = result.aggregated_activities.find(
330
+ (a) => a.group === 'existing1',
331
+ );
332
+ expect(updatedExisting1?.activity_count).toBe(5);
333
+
334
+ // Check that existing2 was updated
335
+ const updatedExisting2 = result.aggregated_activities.find(
336
+ (a) => a.group === 'existing2',
337
+ );
338
+ expect(updatedExisting2?.score).toBe(100);
339
+
340
+ // Check that new activities were added
341
+ expect(
342
+ result.aggregated_activities.find((a) => a.group === 'new1'),
343
+ ).toBeDefined();
344
+ expect(
345
+ result.aggregated_activities.find((a) => a.group === 'new2'),
346
+ ).toBeDefined();
347
+ });
348
+
349
+ it('should preserve order when adding at start', () => {
350
+ const existingActivities = [
351
+ createMockAggregatedActivity({ group: 'existing1' }),
352
+ createMockAggregatedActivity({ group: 'existing2' }),
353
+ ];
354
+ const newActivities = [
355
+ createMockAggregatedActivity({ group: 'new1' }),
356
+ createMockAggregatedActivity({ group: 'new2' }),
357
+ ];
358
+
359
+ const result = addAggregatedActivitiesToState(
360
+ newActivities,
361
+ existingActivities,
362
+ 'start',
363
+ );
364
+
365
+ expect(result.aggregated_activities).toStrictEqual([
366
+ ...newActivities,
367
+ ...existingActivities,
368
+ ]);
369
+ });
370
+
371
+ it('should preserve order when adding at end', () => {
372
+ const existingActivities = [
373
+ createMockAggregatedActivity({ group: 'existing1' }),
374
+ createMockAggregatedActivity({ group: 'existing2' }),
375
+ ];
376
+ const newActivities = [
377
+ createMockAggregatedActivity({ group: 'new1' }),
378
+ createMockAggregatedActivity({ group: 'new2' }),
379
+ ];
380
+
381
+ const result = addAggregatedActivitiesToState(
382
+ newActivities,
383
+ existingActivities,
384
+ 'end',
385
+ );
386
+
387
+ expect(result.aggregated_activities).toStrictEqual([
388
+ ...existingActivities,
389
+ ...newActivities,
390
+ ]);
391
+ });
392
+ });
393
+
394
+ describe('updateNotificationStatus', () => {
395
+ it('should replace old state with new one', () => {
396
+ const newNotificationStatus = createMockNotificationStatus({
397
+ unread: 5,
398
+ unseen: 3,
399
+ read_activities: ['activity1', 'activity2'],
400
+ seen_activities: ['activity3', 'activity4'],
401
+ });
402
+
403
+ const currentNotificationStatus = createMockNotificationStatus({
404
+ unread: 2,
405
+ unseen: 1,
406
+ read_activities: ['activity5', 'activity6'],
407
+ seen_activities: ['activity7', 'activity8'],
408
+ });
409
+
410
+ const result = updateNotificationStatus(
411
+ newNotificationStatus,
412
+ currentNotificationStatus,
413
+ );
414
+
415
+ expect(result.notification_status).toStrictEqual(newNotificationStatus);
118
416
  });
119
417
  });
120
418
  });
@@ -1,13 +1,76 @@
1
1
  import type { Feed } from '../../../feed';
2
- import {
2
+ import type {
3
3
  AggregatedActivityResponse,
4
4
  NotificationFeedUpdatedEvent,
5
5
  NotificationStatusResponse,
6
6
  } from '../../../gen/models';
7
7
  import type { EventPayload, UpdateStateResult } from '../../../types-internal';
8
+ import { uniqueArrayMerge } from '../../../utils';
9
+
10
+ export const addAggregatedActivitiesToState = (
11
+ newAggregatedActivities: AggregatedActivityResponse[],
12
+ aggregatedActivities: AggregatedActivityResponse[] | undefined,
13
+ position: 'start' | 'end',
14
+ ) => {
15
+ let result: UpdateStateResult<{
16
+ aggregated_activities: AggregatedActivityResponse[];
17
+ }>;
18
+ if (newAggregatedActivities.length === 0) {
19
+ result = {
20
+ changed: false,
21
+ aggregated_activities: [],
22
+ };
23
+ } else {
24
+ result = {
25
+ changed: true,
26
+ aggregated_activities: [],
27
+ };
28
+ }
29
+
30
+ result.aggregated_activities =
31
+ position === 'start'
32
+ ? uniqueArrayMerge(
33
+ newAggregatedActivities,
34
+ aggregatedActivities ?? [],
35
+ (a) => a.group,
36
+ )
37
+ : uniqueArrayMerge(
38
+ aggregatedActivities ?? [],
39
+ newAggregatedActivities,
40
+ (a) => a.group,
41
+ );
42
+
43
+ return result;
44
+ };
45
+
46
+ export const updateNotificationStatus = (
47
+ newNotificationStatus?: NotificationStatusResponse,
48
+ currentNotificationStatus?: NotificationStatusResponse,
49
+ ) => {
50
+ if (!newNotificationStatus && !currentNotificationStatus) {
51
+ return {
52
+ changed: false,
53
+ notification_status: undefined,
54
+ };
55
+ } else if (!newNotificationStatus) {
56
+ return {
57
+ changed: false,
58
+ notification_status: currentNotificationStatus,
59
+ };
60
+ } else {
61
+ return {
62
+ changed: true,
63
+ notification_status: {
64
+ ...newNotificationStatus,
65
+ },
66
+ };
67
+ }
68
+ };
8
69
 
9
70
  export const updateNotificationFeedFromEvent = (
10
71
  event: NotificationFeedUpdatedEvent,
72
+ currentAggregatedActivities?: AggregatedActivityResponse[],
73
+ currentNotificationStatus?: NotificationStatusResponse,
11
74
  ): UpdateStateResult<{
12
75
  data?: {
13
76
  notification_status?: NotificationStatusResponse;
@@ -19,15 +82,31 @@ export const updateNotificationFeedFromEvent = (
19
82
  aggregated_activities?: AggregatedActivityResponse[];
20
83
  } = {};
21
84
 
22
- if (event.notification_status) {
23
- updates.notification_status = event.notification_status;
85
+ if (event.notification_status && currentNotificationStatus) {
86
+ const notificationStatusResult = updateNotificationStatus(
87
+ event.notification_status,
88
+ currentNotificationStatus,
89
+ );
90
+
91
+ if (notificationStatusResult.changed) {
92
+ updates.notification_status =
93
+ notificationStatusResult.notification_status;
94
+ }
24
95
  }
25
96
 
26
- if (event.aggregated_activities) {
27
- updates.aggregated_activities = event.aggregated_activities;
97
+ if (event.aggregated_activities && currentAggregatedActivities) {
98
+ const aggregatedActivitiesResult = addAggregatedActivitiesToState(
99
+ event.aggregated_activities,
100
+ currentAggregatedActivities,
101
+ 'start',
102
+ );
103
+
104
+ if (aggregatedActivitiesResult.changed) {
105
+ updates.aggregated_activities =
106
+ aggregatedActivitiesResult.aggregated_activities;
107
+ }
28
108
  }
29
109
 
30
- // Only return changed if we have actual updates
31
110
  if (Object.keys(updates).length > 0) {
32
111
  return {
33
112
  changed: true,
@@ -44,7 +123,11 @@ export function handleNotificationFeedUpdated(
44
123
  this: Feed,
45
124
  event: EventPayload<'feeds.notification_feed.updated'>,
46
125
  ) {
47
- const result = updateNotificationFeedFromEvent(event);
126
+ const result = updateNotificationFeedFromEvent(
127
+ event,
128
+ this.currentState.aggregated_activities,
129
+ this.currentState.notification_status,
130
+ );
48
131
  if (result.changed) {
49
132
  this.state.partialNext({
50
133
  notification_status: result.data?.notification_status,
@@ -1,4 +1,4 @@
1
- import { Feed } from '../../feed';
1
+ import type { Feed } from '../../feed';
2
2
 
3
3
  export function handleWatchStarted(this: Feed) {
4
4
  this.state.partialNext({ watch: true });
@@ -1,4 +1,4 @@
1
- import { Feed } from '../../feed';
1
+ import type { Feed } from '../../feed';
2
2
 
3
3
  export function handleWatchStopped(this: Feed) {
4
4
  this.state.partialNext({ watch: false });
@@ -1,7 +1,7 @@
1
1
  import { beforeEach, describe, expect, it } from 'vitest';
2
2
  import { FeedsClient } from '../feeds-client';
3
3
  import { Feed } from './feed';
4
- import { ActivityResponse } from '../gen/models';
4
+ import type { ActivityResponse } from '../gen/models';
5
5
  import { generateActivityResponse, generateFeedResponse } from '../test-utils';
6
6
 
7
7
  describe('Feed derived state updates', () => {