@stream-io/feeds-client 0.2.0 → 0.2.2

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 (83) hide show
  1. package/@react-bindings/hooks/feed-state-hooks/index.ts +4 -0
  2. package/CHANGELOG.md +16 -0
  3. package/dist/@react-bindings/hooks/feed-state-hooks/index.d.ts +4 -0
  4. package/dist/@react-bindings/hooks/feed-state-hooks/useAggregatedActivities.d.ts +11 -0
  5. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivityRead.d.ts +6 -0
  6. package/dist/@react-bindings/hooks/feed-state-hooks/useIsAggregatedActivitySeen.d.ts +6 -0
  7. package/dist/@react-bindings/hooks/feed-state-hooks/useNotificationStatus.d.ts +13 -0
  8. package/dist/@react-bindings/wrappers/StreamFeed.d.ts +1 -1
  9. package/dist/index-react-bindings.browser.cjs +505 -222
  10. package/dist/index-react-bindings.browser.cjs.map +1 -1
  11. package/dist/index-react-bindings.browser.js +502 -223
  12. package/dist/index-react-bindings.browser.js.map +1 -1
  13. package/dist/index-react-bindings.node.cjs +505 -222
  14. package/dist/index-react-bindings.node.cjs.map +1 -1
  15. package/dist/index-react-bindings.node.js +502 -223
  16. package/dist/index-react-bindings.node.js.map +1 -1
  17. package/dist/index.browser.cjs +440 -205
  18. package/dist/index.browser.cjs.map +1 -1
  19. package/dist/index.browser.js +440 -206
  20. package/dist/index.browser.js.map +1 -1
  21. package/dist/index.node.cjs +440 -205
  22. package/dist/index.node.cjs.map +1 -1
  23. package/dist/index.node.js +440 -206
  24. package/dist/index.node.js.map +1 -1
  25. package/dist/src/feed/event-handlers/activity/handle-activity-deleted.d.ts +12 -3
  26. package/dist/src/feed/event-handlers/activity/handle-activity-marked.d.ts +11 -0
  27. package/dist/src/feed/event-handlers/activity/handle-activity-pinned.d.ts +3 -0
  28. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-added.d.ts +10 -6
  29. package/dist/src/feed/event-handlers/activity/handle-activity-reaction-deleted.d.ts +10 -6
  30. package/dist/src/feed/event-handlers/activity/handle-activity-unpinned.d.ts +3 -0
  31. package/dist/src/feed/event-handlers/activity/handle-activity-updated.d.ts +7 -3
  32. package/dist/src/feed/event-handlers/activity/index.d.ts +1 -0
  33. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +10 -6
  34. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +10 -6
  35. package/dist/src/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +10 -6
  36. package/dist/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +8 -1
  37. package/dist/src/feed/feed.d.ts +2 -2
  38. package/dist/src/gen/models/index.d.ts +36 -1
  39. package/dist/src/test-utils/response-generators.d.ts +66 -1
  40. package/dist/src/utils/index.d.ts +1 -0
  41. package/dist/src/utils/update-entity-in-array.d.ts +27 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/feed/event-handlers/activity/activity-marked-utils.test.ts +208 -0
  45. package/src/feed/event-handlers/activity/activity-reaction-utils.test.ts +108 -96
  46. package/src/feed/event-handlers/activity/activity-utils.test.ts +84 -122
  47. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +43 -10
  48. package/src/feed/event-handlers/activity/handle-activity-marked.ts +68 -0
  49. package/src/feed/event-handlers/activity/handle-activity-pinned.test.ts +60 -0
  50. package/src/feed/event-handlers/activity/handle-activity-pinned.ts +30 -0
  51. package/src/feed/event-handlers/activity/handle-activity-reaction-added.test.ts +157 -0
  52. package/src/feed/event-handlers/activity/handle-activity-reaction-added.ts +82 -40
  53. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.test.ts +200 -0
  54. package/src/feed/event-handlers/activity/handle-activity-reaction-deleted.ts +89 -51
  55. package/src/feed/event-handlers/activity/handle-activity-unpinned.test.ts +95 -0
  56. package/src/feed/event-handlers/activity/handle-activity-unpinned.ts +30 -0
  57. package/src/feed/event-handlers/activity/handle-activity-updated.test.ts +115 -0
  58. package/src/feed/event-handlers/activity/handle-activity-updated.ts +73 -35
  59. package/src/feed/event-handlers/activity/index.ts +2 -1
  60. package/src/feed/event-handlers/bookmark/bookmark-utils.test.ts +121 -109
  61. package/src/feed/event-handlers/bookmark/handle-bookmark-added.test.ts +178 -0
  62. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +82 -39
  63. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.test.ts +188 -0
  64. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +86 -48
  65. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.test.ts +196 -0
  66. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +83 -44
  67. package/src/feed/event-handlers/comment/handle-comment-added.test.ts +147 -0
  68. package/src/feed/event-handlers/comment/handle-comment-deleted.test.ts +133 -0
  69. package/src/feed/event-handlers/comment/handle-comment-deleted.ts +24 -10
  70. package/src/feed/event-handlers/comment/handle-comment-reaction.test.ts +315 -0
  71. package/src/feed/event-handlers/comment/handle-comment-updated.test.ts +131 -0
  72. package/src/feed/event-handlers/follow/handle-follow-created.test.ts +7 -7
  73. package/src/feed/event-handlers/follow/handle-follow-deleted.test.ts +2 -2
  74. package/src/feed/event-handlers/follow/handle-follow-updated.test.ts +1 -1
  75. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.test.ts +120 -0
  76. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +47 -3
  77. package/src/feed/feed.ts +4 -2
  78. package/src/gen/model-decoders/decoders.ts +14 -1
  79. package/src/gen/models/index.ts +73 -2
  80. package/src/gen/moderation/ModerationApi.ts +1 -0
  81. package/src/test-utils/response-generators.ts +383 -0
  82. package/src/utils/index.ts +1 -0
  83. package/src/utils/update-entity-in-array.ts +51 -0
@@ -1,67 +1,109 @@
1
1
  import type { Feed } from '../../../feed';
2
2
  import type {
3
+ ActivityPinResponse,
3
4
  ActivityReactionAddedEvent,
4
5
  ActivityResponse,
5
6
  } from '../../../gen/models';
6
- import type { EventPayload, UpdateStateResult } from '../../../types-internal';
7
+ import type { EventPayload } from '../../../types-internal';
8
+ import { updateEntityInArray } from '../../../utils';
7
9
 
8
- import { updateActivityInState } from './handle-activity-updated';
10
+ // shared function to update the activity with the new reaction
11
+ const sharedUpdateActivity = ({
12
+ currentActivity,
13
+ event,
14
+ eventBelongsToCurrentUser,
15
+ }: {
16
+ currentActivity: ActivityResponse;
17
+ event: ActivityReactionAddedEvent;
18
+ eventBelongsToCurrentUser: boolean;
19
+ }) => {
20
+ let newOwnReactions = currentActivity.own_reactions;
9
21
 
10
- export const addReactionToActivity = (
11
- event: ActivityReactionAddedEvent,
12
- activity: ActivityResponse,
13
- isCurrentUser: boolean,
14
- ): UpdateStateResult<ActivityResponse> => {
15
- // Update own_reactions if the reaction is from the current user
16
- const ownReactions = [...(activity.own_reactions || [])];
17
- if (isCurrentUser) {
18
- ownReactions.push(event.reaction);
22
+ if (eventBelongsToCurrentUser) {
23
+ newOwnReactions = [...currentActivity.own_reactions, event.reaction];
19
24
  }
20
25
 
21
26
  return {
22
- ...activity,
23
- own_reactions: ownReactions,
24
- latest_reactions: event.activity.latest_reactions,
25
- reaction_groups: event.activity.reaction_groups,
26
- changed: true,
27
+ ...event.activity,
28
+ own_reactions: newOwnReactions,
29
+ own_bookmarks: currentActivity.own_bookmarks,
27
30
  };
28
31
  };
29
32
 
30
33
  export const addReactionToActivities = (
31
34
  event: ActivityReactionAddedEvent,
32
35
  activities: ActivityResponse[] | undefined,
33
- isCurrentUser: boolean,
34
- ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
35
- if (!activities) {
36
- return { changed: false, activities: [] };
37
- }
36
+ eventBelongsToCurrentUser: boolean,
37
+ ) =>
38
+ updateEntityInArray({
39
+ entities: activities,
40
+ matcher: (activity) => activity.id === event.activity.id,
41
+ updater: (matchedActivity) =>
42
+ sharedUpdateActivity({
43
+ currentActivity: matchedActivity,
44
+ event,
45
+ eventBelongsToCurrentUser,
46
+ }),
47
+ });
38
48
 
39
- const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
40
- if (activityIndex === -1) {
41
- return { changed: false, activities };
42
- }
49
+ export const addReactionToPinnedActivities = (
50
+ event: ActivityReactionAddedEvent,
51
+ pinnedActivities: ActivityPinResponse[] | undefined,
52
+ eventBelongsToCurrentUser: boolean,
53
+ ) =>
54
+ updateEntityInArray({
55
+ entities: pinnedActivities,
56
+ matcher: (pinnedActivity) =>
57
+ pinnedActivity.activity.id === event.activity.id,
58
+ updater: (matchedPinnedActivity) => {
59
+ const newActivity = sharedUpdateActivity({
60
+ currentActivity: matchedPinnedActivity.activity,
61
+ event,
62
+ eventBelongsToCurrentUser,
63
+ });
43
64
 
44
- const activity = activities[activityIndex];
45
- const updatedActivity = addReactionToActivity(event, activity, isCurrentUser);
46
- return updateActivityInState(updatedActivity, activities, true);
47
- };
65
+ // this should never happen, but just in case
66
+ if (newActivity === matchedPinnedActivity.activity) {
67
+ return matchedPinnedActivity;
68
+ }
69
+
70
+ return {
71
+ ...matchedPinnedActivity,
72
+ activity: newActivity,
73
+ };
74
+ },
75
+ });
48
76
 
49
77
  export function handleActivityReactionAdded(
50
78
  this: Feed,
51
79
  event: EventPayload<'feeds.activity.reaction.added'>,
52
80
  ) {
53
- const currentActivities = this.currentState.activities;
81
+ const {
82
+ activities: currentActivities,
83
+ pinned_activities: currentPinnedActivities,
84
+ } = this.currentState;
54
85
  const connectedUser = this.client.state.getLatestValue().connected_user;
55
- const isCurrentUser = Boolean(
56
- connectedUser && event.reaction.user.id === connectedUser.id,
57
- );
86
+ const eventBelongsToCurrentUser =
87
+ typeof connectedUser !== 'undefined' &&
88
+ event.reaction.user.id === connectedUser.id;
89
+
90
+ const [result1, result2] = [
91
+ addReactionToActivities(
92
+ event,
93
+ currentActivities,
94
+ eventBelongsToCurrentUser,
95
+ ),
96
+ addReactionToPinnedActivities(
97
+ event,
98
+ currentPinnedActivities,
99
+ eventBelongsToCurrentUser,
100
+ ),
101
+ ];
58
102
 
59
- const result = addReactionToActivities(
60
- event,
61
- currentActivities,
62
- isCurrentUser,
63
- );
64
- if (result.changed) {
65
- this.state.partialNext({ activities: result.activities });
103
+ if (result1.changed || result2.changed) {
104
+ this.state.partialNext({
105
+ activities: result1.entities,
106
+ pinned_activities: result2.entities,
107
+ });
66
108
  }
67
109
  }
@@ -0,0 +1,200 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { Feed } from '../../../feed';
3
+ import { FeedsClient } from '../../../feeds-client';
4
+ import { handleActivityReactionDeleted } from './handle-activity-reaction-deleted';
5
+ import {
6
+ generateActivityPinResponse,
7
+ generateActivityResponse,
8
+ generateFeedReactionResponse,
9
+ generateFeedResponse,
10
+ generateOwnUser,
11
+ getHumanId,
12
+ generateActivityReactionDeletedEvent,
13
+ } from '../../../test-utils/response-generators';
14
+
15
+ describe(handleActivityReactionDeleted.name, () => {
16
+ let feed: Feed;
17
+ let client: FeedsClient;
18
+ let currentUserId: string;
19
+
20
+ beforeEach(() => {
21
+ client = new FeedsClient('mock-api-key');
22
+ currentUserId = getHumanId();
23
+ client.state.partialNext({
24
+ connected_user: generateOwnUser({ id: currentUserId }),
25
+ });
26
+ const feedResponse = generateFeedResponse({
27
+ id: 'main',
28
+ group_id: 'user',
29
+ created_by: { id: currentUserId },
30
+ });
31
+ feed = new Feed(
32
+ client,
33
+ feedResponse.group_id,
34
+ feedResponse.id,
35
+ feedResponse,
36
+ );
37
+ });
38
+
39
+ it('removes a reaction from the correct activity for current user & updates activities with event.activity', () => {
40
+ const event = generateActivityReactionDeletedEvent({
41
+ activity: {
42
+ reaction_count: 0,
43
+ },
44
+ reaction: {
45
+ type: 'like',
46
+ user: { id: currentUserId },
47
+ },
48
+ user: { id: currentUserId },
49
+ });
50
+
51
+ const activity = generateActivityResponse({
52
+ reaction_count: 1,
53
+ own_reactions: [
54
+ generateFeedReactionResponse({
55
+ type: 'like',
56
+ user: { id: currentUserId },
57
+ activity_id: event.activity.id,
58
+ }),
59
+ ],
60
+ id: event.activity.id,
61
+ });
62
+ const activityPin = generateActivityPinResponse({
63
+ activity: { ...activity },
64
+ });
65
+ feed.state.partialNext({
66
+ activities: [activity],
67
+ pinned_activities: [activityPin],
68
+ });
69
+
70
+ const stateBefore = feed.currentState;
71
+ expect(stateBefore.activities![0].own_reactions).toHaveLength(1);
72
+ expect(
73
+ stateBefore.pinned_activities![0].activity.own_reactions,
74
+ ).toHaveLength(1);
75
+ expect(stateBefore.activities![0].reaction_count).toBe(1);
76
+ expect(stateBefore.pinned_activities![0].activity.reaction_count).toBe(
77
+ 1,
78
+ );
79
+
80
+ handleActivityReactionDeleted.call(feed, event);
81
+
82
+ const stateAfter = feed.currentState;
83
+ expect(stateAfter.activities![0].own_reactions).toHaveLength(0);
84
+ expect(
85
+ stateAfter.pinned_activities![0].activity.own_reactions,
86
+ ).toHaveLength(0);
87
+ expect(stateAfter.activities![0].reaction_count).toBe(0);
88
+ expect(stateAfter.pinned_activities![0].activity.reaction_count).toBe(0);
89
+ expect(stateAfter.activities![0].own_bookmarks).toBe(
90
+ stateBefore.activities![0].own_bookmarks,
91
+ );
92
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toBe(
93
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
94
+ );
95
+ });
96
+
97
+ it('does not remove from own_reactions if reaction is from another user but still updates activity', () => {
98
+ const event = generateActivityReactionDeletedEvent({
99
+ activity: {
100
+ reaction_count: 0,
101
+ },
102
+ reaction: {
103
+ type: 'like',
104
+ user: { id: 'other-user-id' },
105
+ },
106
+ user: { id: 'other-user-id' },
107
+ });
108
+
109
+ const activity = generateActivityResponse({
110
+ reaction_count: 1,
111
+ own_reactions: [
112
+ generateFeedReactionResponse({
113
+ type: 'like',
114
+ user: { id: currentUserId },
115
+ activity_id: event.activity.id,
116
+ }),
117
+ ],
118
+ id: event.activity.id,
119
+ });
120
+ const activityPin = generateActivityPinResponse({
121
+ activity: { ...activity },
122
+ });
123
+ feed.state.partialNext({
124
+ activities: [activity],
125
+ pinned_activities: [activityPin],
126
+ });
127
+
128
+ const stateBefore = feed.currentState;
129
+ expect(stateBefore.activities![0].own_reactions).toHaveLength(1);
130
+ expect(
131
+ stateBefore.pinned_activities![0].activity.own_reactions,
132
+ ).toHaveLength(1);
133
+ expect(stateBefore.activities![0].reaction_count).toBe(1);
134
+ expect(stateBefore.pinned_activities![0].activity.reaction_count).toBe(
135
+ 1,
136
+ );
137
+
138
+ handleActivityReactionDeleted.call(feed, event);
139
+
140
+ const stateAfter = feed.currentState;
141
+ expect(stateAfter.activities![0].own_reactions).toHaveLength(1);
142
+ expect(stateAfter.activities![0].own_reactions).toBe(
143
+ stateBefore.activities![0].own_reactions,
144
+ );
145
+ expect(stateAfter.pinned_activities![0].activity.own_reactions).toBe(
146
+ stateBefore.pinned_activities![0].activity.own_reactions,
147
+ );
148
+ expect(stateAfter.activities![0].own_bookmarks).toBe(
149
+ stateBefore.activities![0].own_bookmarks,
150
+ );
151
+ expect(stateAfter.pinned_activities![0].activity.own_bookmarks).toBe(
152
+ stateBefore.pinned_activities![0].activity.own_bookmarks,
153
+ );
154
+ expect(
155
+ stateAfter.pinned_activities![0].activity.own_reactions,
156
+ ).toHaveLength(1);
157
+ expect(stateAfter.activities![0].reaction_count).toBe(0);
158
+ expect(stateAfter.pinned_activities![0].activity.reaction_count).toBe(0);
159
+ });
160
+
161
+ it('does nothing if activity is not found', () => {
162
+ const event = generateActivityReactionDeletedEvent({
163
+ activity: {
164
+ reaction_count: 0,
165
+ },
166
+ reaction: {
167
+ type: 'like',
168
+ user: { id: currentUserId },
169
+ },
170
+ user: { id: currentUserId },
171
+ });
172
+
173
+ const activity = generateActivityResponse({
174
+ reaction_count: 1,
175
+ own_reactions: [
176
+ generateFeedReactionResponse({
177
+ type: 'like',
178
+ user: { id: currentUserId },
179
+ activity_id: 'activity1',
180
+ }),
181
+ ],
182
+ id: 'activity1',
183
+ });
184
+ const activityPin = generateActivityPinResponse({
185
+ activity: { ...activity },
186
+ });
187
+ feed.state.partialNext({
188
+ activities: [activity],
189
+ pinned_activities: [activityPin],
190
+ });
191
+
192
+ const stateBefore = feed.currentState;
193
+
194
+ handleActivityReactionDeleted.call(feed, event);
195
+
196
+ const stateAfter = feed.currentState;
197
+
198
+ expect(stateAfter).toBe(stateBefore);
199
+ });
200
+ });
@@ -1,75 +1,113 @@
1
1
  import type { Feed } from '../../../feed';
2
- import type {
2
+ import {
3
+ ActivityPinResponse,
3
4
  ActivityReactionDeletedEvent,
4
5
  ActivityResponse,
5
6
  } from '../../../gen/models';
6
- import type { EventPayload, UpdateStateResult } from '../../../types-internal';
7
+ import type { EventPayload } from '../../../types-internal';
8
+ import { updateEntityInArray } from '../../../utils';
7
9
 
8
- import { updateActivityInState } from './handle-activity-updated';
9
- export const removeReactionFromActivity = (
10
- event: ActivityReactionDeletedEvent,
11
- activity: ActivityResponse,
12
- isCurrentUser: boolean,
13
- ): UpdateStateResult<ActivityResponse> => {
14
- // Update own_reactions if the reaction is from the current user
15
- const ownReactions = isCurrentUser
16
- ? (activity.own_reactions || []).filter(
17
- (r) =>
18
- !(
19
- r.type === event.reaction.type &&
20
- r.user.id === event.reaction.user.id
21
- ),
22
- )
23
- : activity.own_reactions;
10
+ const sharedUpdateActivity = ({
11
+ currentActivity,
12
+ event,
13
+ eventBelongsToCurrentUser,
14
+ }: {
15
+ currentActivity: ActivityResponse;
16
+ event: ActivityReactionDeletedEvent;
17
+ eventBelongsToCurrentUser: boolean;
18
+ }) => {
19
+ let newOwnReactions = currentActivity.own_reactions;
20
+
21
+ if (eventBelongsToCurrentUser) {
22
+ newOwnReactions = currentActivity.own_reactions.filter(
23
+ (reaction) =>
24
+ !(
25
+ reaction.type === event.reaction.type &&
26
+ reaction.user.id === event.reaction.user.id
27
+ ),
28
+ );
29
+ }
24
30
 
25
31
  return {
26
- ...activity,
27
- own_reactions: ownReactions,
28
- latest_reactions: event.activity.latest_reactions,
29
- reaction_groups: event.activity.reaction_groups,
30
- changed: true,
32
+ ...event.activity,
33
+ own_reactions: newOwnReactions,
34
+ own_bookmarks: currentActivity.own_bookmarks,
31
35
  };
32
36
  };
33
37
 
34
38
  export const removeReactionFromActivities = (
35
39
  event: ActivityReactionDeletedEvent,
36
40
  activities: ActivityResponse[] | undefined,
37
- isCurrentUser: boolean,
38
- ): UpdateStateResult<{ activities: ActivityResponse[] }> => {
39
- if (!activities) {
40
- return { changed: false, activities: [] };
41
- }
41
+ eventBelongsToCurrentUser: boolean,
42
+ ) =>
43
+ updateEntityInArray({
44
+ entities: activities,
45
+ matcher: (activity) => activity.id === event.activity.id,
46
+ updater: (matchedActivity) =>
47
+ sharedUpdateActivity({
48
+ currentActivity: matchedActivity,
49
+ event,
50
+ eventBelongsToCurrentUser,
51
+ }),
52
+ });
42
53
 
43
- const activityIndex = activities.findIndex((a) => a.id === event.activity.id);
44
- if (activityIndex === -1) {
45
- return { changed: false, activities };
46
- }
54
+ export const removeReactionFromPinnedActivities = (
55
+ event: ActivityReactionDeletedEvent,
56
+ activities: ActivityPinResponse[] | undefined,
57
+ eventBelongsToCurrentUser: boolean,
58
+ ) =>
59
+ updateEntityInArray({
60
+ entities: activities,
61
+ matcher: (pinnedActivity) =>
62
+ pinnedActivity.activity.id === event.activity.id,
63
+ updater: (matchedPinnedActivity) => {
64
+ const newActivity = sharedUpdateActivity({
65
+ currentActivity: matchedPinnedActivity.activity,
66
+ event,
67
+ eventBelongsToCurrentUser,
68
+ });
47
69
 
48
- const activity = activities[activityIndex];
49
- const updatedActivity = removeReactionFromActivity(
50
- event,
51
- activity,
52
- isCurrentUser,
53
- );
54
- return updateActivityInState(updatedActivity, activities, true);
55
- };
70
+ if (newActivity === matchedPinnedActivity.activity) {
71
+ return matchedPinnedActivity;
72
+ }
73
+
74
+ return {
75
+ ...matchedPinnedActivity,
76
+ activity: newActivity,
77
+ };
78
+ },
79
+ });
56
80
 
57
81
  export function handleActivityReactionDeleted(
58
82
  this: Feed,
59
83
  event: EventPayload<'feeds.activity.reaction.deleted'>,
60
84
  ) {
61
- const currentActivities = this.currentState.activities;
85
+ const {
86
+ activities: currentActivities,
87
+ pinned_activities: currentPinnedActivities,
88
+ } = this.currentState;
62
89
  const connectedUser = this.client.state.getLatestValue().connected_user;
63
- const isCurrentUser = Boolean(
64
- connectedUser && event.reaction.user.id === connectedUser.id,
65
- );
90
+ const eventBelongsToCurrentUser =
91
+ typeof connectedUser !== 'undefined' &&
92
+ event.reaction.user.id === connectedUser.id;
93
+
94
+ const [result1, result2] = [
95
+ removeReactionFromActivities(
96
+ event,
97
+ currentActivities,
98
+ eventBelongsToCurrentUser,
99
+ ),
100
+ removeReactionFromPinnedActivities(
101
+ event,
102
+ currentPinnedActivities,
103
+ eventBelongsToCurrentUser,
104
+ ),
105
+ ];
66
106
 
67
- const result = removeReactionFromActivities(
68
- event,
69
- currentActivities,
70
- isCurrentUser,
71
- );
72
- if (result.changed) {
73
- this.state.partialNext({ activities: result.activities });
107
+ if (result1.changed || result2.changed) {
108
+ this.state.partialNext({
109
+ activities: result1.entities,
110
+ pinned_activities: result2.entities,
111
+ });
74
112
  }
75
113
  }
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+
3
+ import { Feed } from '../../../feed';
4
+ import { FeedsClient } from '../../../feeds-client';
5
+ import { handleActivityUnpinned } from './handle-activity-unpinned';
6
+ import {
7
+ generateActivityPinResponse,
8
+ generateFeedResponse,
9
+ } from '../../../test-utils/response-generators';
10
+ import { ActivityPinResponse } from '../../../gen/models';
11
+ import { EventPayload } from '../../../types-internal';
12
+
13
+ // Helper to construct the event payload for 'feeds.activity.unpinned'
14
+ function makeUnpinnedEvent(
15
+ pinnedActivity: ActivityPinResponse,
16
+ ): EventPayload<'feeds.activity.unpinned'> {
17
+ return {
18
+ type: 'feeds.activity.unpinned',
19
+ created_at: pinnedActivity.created_at,
20
+ fid: pinnedActivity.feed,
21
+ custom: {},
22
+ pinned_activity: {
23
+ created_at: pinnedActivity.created_at,
24
+ duration: '0',
25
+ feed: pinnedActivity.feed,
26
+ user_id: pinnedActivity.user.id,
27
+ activity: pinnedActivity.activity,
28
+ },
29
+ user: pinnedActivity.user,
30
+ };
31
+ }
32
+
33
+ describe(handleActivityUnpinned.name, () => {
34
+ let feed: Feed;
35
+ let client: FeedsClient;
36
+ let pinnedActivity: ActivityPinResponse;
37
+ let otherPinnedActivity: ActivityPinResponse;
38
+
39
+ beforeEach(() => {
40
+ client = new FeedsClient('mock-api-key');
41
+ const feedResponse = generateFeedResponse({ id: 'main', group_id: 'user' });
42
+ feed = new Feed(
43
+ client,
44
+ feedResponse.group_id,
45
+ feedResponse.id,
46
+ feedResponse,
47
+ );
48
+ pinnedActivity = generateActivityPinResponse();
49
+ otherPinnedActivity = generateActivityPinResponse();
50
+ feed.state.next((currentState) => ({
51
+ ...currentState,
52
+ pinned_activities: [pinnedActivity, otherPinnedActivity],
53
+ }));
54
+ });
55
+
56
+ it('removes the correct activity from pinned_activities', () => {
57
+ const event = makeUnpinnedEvent(pinnedActivity);
58
+ handleActivityUnpinned.call(feed, event);
59
+ const { pinned_activities } = feed.currentState;
60
+ expect(pinned_activities).toHaveLength(1);
61
+ expect(pinned_activities![0]).toBe(otherPinnedActivity);
62
+ });
63
+
64
+ it('does nothing if the activity is not found', () => {
65
+ const unrelatedActivity = generateActivityPinResponse();
66
+ const event = makeUnpinnedEvent(unrelatedActivity);
67
+ const stateBefore = feed.currentState;
68
+ handleActivityUnpinned.call(feed, event);
69
+ const stateAfter = feed.currentState;
70
+ expect(stateAfter.pinned_activities).toBe(stateBefore.pinned_activities);
71
+ });
72
+
73
+ it('does nothing if pinned_activities is empty', () => {
74
+ feed.state.next((currentState) => ({
75
+ ...currentState,
76
+ pinned_activities: [],
77
+ }));
78
+ const event = makeUnpinnedEvent(pinnedActivity);
79
+ const stateBefore = feed.currentState;
80
+ handleActivityUnpinned.call(feed, event);
81
+ const stateAfter = feed.currentState;
82
+ expect(stateAfter).toBe(stateBefore);
83
+ });
84
+
85
+ it('does nothing if pinned_activities is undefined', () => {
86
+ feed.state.next((currentState) => ({
87
+ ...currentState,
88
+ pinned_activities: undefined,
89
+ }));
90
+ const event = makeUnpinnedEvent(pinnedActivity);
91
+ handleActivityUnpinned.call(feed, event);
92
+ const stateAfter = feed.currentState;
93
+ expect(stateAfter.pinned_activities).toBeUndefined();
94
+ });
95
+ });
@@ -0,0 +1,30 @@
1
+ import { EventPayload } from '../../../types-internal';
2
+ import { Feed, FeedState } from '../../feed';
3
+
4
+ export function handleActivityUnpinned(
5
+ this: Feed,
6
+ event: EventPayload<'feeds.activity.unpinned'>,
7
+ ) {
8
+ this.state.next((currentState) => {
9
+ let newState: FeedState | undefined;
10
+
11
+ const index =
12
+ currentState.pinned_activities?.findIndex(
13
+ (pinnedActivity) =>
14
+ pinnedActivity.activity.id === event.pinned_activity.activity.id,
15
+ ) ?? -1;
16
+
17
+ if (index >= 0) {
18
+ newState ??= {
19
+ ...currentState,
20
+ };
21
+
22
+ const newPinnedActivities = [...currentState.pinned_activities!];
23
+ newPinnedActivities.splice(index, 1);
24
+
25
+ newState.pinned_activities = newPinnedActivities;
26
+ }
27
+
28
+ return newState ?? currentState;
29
+ });
30
+ }