@stream-io/feeds-client 0.3.51 → 1.1.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 (121) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cjs/index.js +2 -1
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/react-bindings.js +5 -19
  5. package/dist/cjs/react-bindings.js.map +1 -1
  6. package/dist/es/index.mjs +15 -14
  7. package/dist/es/index.mjs.map +1 -1
  8. package/dist/es/react-bindings.mjs +5 -19
  9. package/dist/es/react-bindings.mjs.map +1 -1
  10. package/dist/{feeds-client-B4zeBggL.js → feeds-client-C1c6lcS3.js} +705 -214
  11. package/dist/feeds-client-C1c6lcS3.js.map +1 -0
  12. package/dist/{feeds-client-DeAqnd1a.mjs → feeds-client-jtUTE4AC.mjs} +711 -220
  13. package/dist/feeds-client-jtUTE4AC.mjs.map +1 -0
  14. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  15. package/dist/types/activity-with-state-updates/activity-with-state-updates.d.ts.map +1 -1
  16. package/dist/types/bindings/react/hooks/feed-state-hooks/useIsAggregatedActivityRead.d.ts +6 -2
  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/useIsAggregatedActivitySeen.d.ts +6 -2
  19. package/dist/types/bindings/react/hooks/feed-state-hooks/useIsAggregatedActivitySeen.d.ts.map +1 -1
  20. package/dist/types/feed/activity-filter.d.ts +11 -0
  21. package/dist/types/feed/activity-filter.d.ts.map +1 -0
  22. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts +3 -2
  23. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts.map +1 -1
  24. package/dist/types/feed/event-handlers/activity/handle-activity-deleted.d.ts +3 -2
  25. package/dist/types/feed/event-handlers/activity/handle-activity-deleted.d.ts.map +1 -1
  26. package/dist/types/feed/event-handlers/activity/handle-activity-feedback.d.ts +5 -0
  27. package/dist/types/feed/event-handlers/activity/handle-activity-feedback.d.ts.map +1 -1
  28. package/dist/types/feed/event-handlers/activity/handle-activity-pinned.d.ts +3 -2
  29. package/dist/types/feed/event-handlers/activity/handle-activity-pinned.d.ts.map +1 -1
  30. package/dist/types/feed/event-handlers/activity/handle-activity-unpinned.d.ts +3 -2
  31. package/dist/types/feed/event-handlers/activity/handle-activity-unpinned.d.ts.map +1 -1
  32. package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts.map +1 -1
  33. package/dist/types/feed/event-handlers/activity/index.d.ts +3 -1
  34. package/dist/types/feed/event-handlers/activity/index.d.ts.map +1 -1
  35. package/dist/types/feed/event-handlers/activity-updater.d.ts +1 -0
  36. package/dist/types/feed/event-handlers/activity-updater.d.ts.map +1 -1
  37. package/dist/types/feed/event-handlers/add-aggregated-activities-to-state.d.ts +1 -1
  38. package/dist/types/feed/event-handlers/add-aggregated-activities-to-state.d.ts.map +1 -1
  39. package/dist/types/feed/event-handlers/bookmark/handle-bookmark-added.d.ts +6 -5
  40. package/dist/types/feed/event-handlers/bookmark/handle-bookmark-added.d.ts.map +1 -1
  41. package/dist/types/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts +6 -5
  42. package/dist/types/feed/event-handlers/bookmark/handle-bookmark-deleted.d.ts.map +1 -1
  43. package/dist/types/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts +6 -5
  44. package/dist/types/feed/event-handlers/bookmark/handle-bookmark-updated.d.ts.map +1 -1
  45. package/dist/types/feed/event-handlers/comment/handle-comment-updated.d.ts.map +1 -1
  46. package/dist/types/feed/event-handlers/feed/handle-feed-deleted.d.ts +4 -0
  47. package/dist/types/feed/event-handlers/feed/handle-feed-deleted.d.ts.map +1 -0
  48. package/dist/types/feed/event-handlers/feed/handle-feed-updated.d.ts.map +1 -1
  49. package/dist/types/feed/event-handlers/feed/index.d.ts +1 -0
  50. package/dist/types/feed/event-handlers/feed/index.d.ts.map +1 -1
  51. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-added.d.ts +3 -2
  52. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-added.d.ts.map +1 -1
  53. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-removed.d.ts +3 -2
  54. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-removed.d.ts.map +1 -1
  55. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-updated.d.ts +3 -2
  56. package/dist/types/feed/event-handlers/feed-member/handle-feed-member-updated.d.ts.map +1 -1
  57. package/dist/types/feed/event-handlers/follow/handle-follow-created.d.ts +2 -2
  58. package/dist/types/feed/event-handlers/follow/handle-follow-created.d.ts.map +1 -1
  59. package/dist/types/feed/event-handlers/follow/handle-follow-updated.d.ts.map +1 -1
  60. package/dist/types/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts +3 -2
  61. package/dist/types/feed/event-handlers/notification-feed/handle-notification-feed-updated.d.ts.map +1 -1
  62. package/dist/types/feed/feed.d.ts +17 -12
  63. package/dist/types/feed/feed.d.ts.map +1 -1
  64. package/dist/types/feeds-client/apply-new-activity-to-active-feeds.d.ts +4 -0
  65. package/dist/types/feeds-client/apply-new-activity-to-active-feeds.d.ts.map +1 -0
  66. package/dist/types/feeds-client/feeds-client.d.ts +29 -8
  67. package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
  68. package/dist/types/gen/feeds/FeedsApi.d.ts +6 -4
  69. package/dist/types/gen/feeds/FeedsApi.d.ts.map +1 -1
  70. package/dist/types/gen/models/index.d.ts +40 -2
  71. package/dist/types/gen/models/index.d.ts.map +1 -1
  72. package/dist/types/index.d.ts +1 -0
  73. package/dist/types/index.d.ts.map +1 -1
  74. package/dist/types/types.d.ts +15 -0
  75. package/dist/types/types.d.ts.map +1 -1
  76. package/dist/types/utils/state-update-queue.d.ts +11 -2
  77. package/dist/types/utils/state-update-queue.d.ts.map +1 -1
  78. package/dist/types/utils/unique-array-merge.d.ts +1 -1
  79. package/dist/types/utils/unique-array-merge.d.ts.map +1 -1
  80. package/package.json +2 -1
  81. package/src/activity-with-state-updates/activity-with-state-updates.ts +8 -2
  82. package/src/bindings/react/hooks/feed-state-hooks/useIsAggregatedActivityRead.ts +6 -20
  83. package/src/bindings/react/hooks/feed-state-hooks/useIsAggregatedActivitySeen.ts +6 -20
  84. package/src/feed/activity-filter.ts +44 -0
  85. package/src/feed/event-handlers/activity/handle-activity-added.ts +22 -8
  86. package/src/feed/event-handlers/activity/handle-activity-deleted.ts +28 -2
  87. package/src/feed/event-handlers/activity/handle-activity-feedback.ts +17 -7
  88. package/src/feed/event-handlers/activity/handle-activity-pinned.ts +25 -3
  89. package/src/feed/event-handlers/activity/handle-activity-unpinned.ts +25 -2
  90. package/src/feed/event-handlers/activity/handle-activity-updated.ts +5 -1
  91. package/src/feed/event-handlers/activity/index.ts +3 -1
  92. package/src/feed/event-handlers/add-aggregated-activities-to-state.ts +11 -2
  93. package/src/feed/event-handlers/bookmark/handle-bookmark-added.ts +20 -11
  94. package/src/feed/event-handlers/bookmark/handle-bookmark-deleted.ts +21 -11
  95. package/src/feed/event-handlers/bookmark/handle-bookmark-updated.ts +24 -10
  96. package/src/feed/event-handlers/comment/handle-comment-updated.ts +11 -10
  97. package/src/feed/event-handlers/feed/handle-feed-deleted.ts +12 -0
  98. package/src/feed/event-handlers/feed/handle-feed-updated.ts +8 -0
  99. package/src/feed/event-handlers/feed/index.ts +1 -0
  100. package/src/feed/event-handlers/feed-member/handle-feed-member-added.ts +25 -2
  101. package/src/feed/event-handlers/feed-member/handle-feed-member-removed.ts +25 -2
  102. package/src/feed/event-handlers/feed-member/handle-feed-member-updated.ts +25 -2
  103. package/src/feed/event-handlers/follow/handle-follow-created.ts +18 -1
  104. package/src/feed/event-handlers/follow/handle-follow-updated.ts +14 -0
  105. package/src/feed/event-handlers/notification-feed/handle-notification-feed-updated.ts +68 -2
  106. package/src/feed/event-handlers/story-feeds/handle-story-feeds-updated.ts +1 -1
  107. package/src/feed/feed.ts +79 -26
  108. package/src/feeds-client/apply-new-activity-to-active-feeds.ts +9 -0
  109. package/src/feeds-client/feeds-client.ts +301 -28
  110. package/src/gen/feeds/FeedsApi.ts +79 -12
  111. package/src/gen/model-decoders/decoders.ts +7 -0
  112. package/src/gen/models/index.ts +66 -4
  113. package/src/index.ts +1 -0
  114. package/src/types.ts +17 -0
  115. package/src/utils/state-update-queue.ts +42 -28
  116. package/src/utils/unique-array-merge.ts +11 -3
  117. package/dist/feeds-client-B4zeBggL.js.map +0 -1
  118. package/dist/feeds-client-DeAqnd1a.mjs.map +0 -1
  119. package/dist/types/feed/event-handlers/activity/handle-activity-marked.d.ts +0 -12
  120. package/dist/types/feed/event-handlers/activity/handle-activity-marked.d.ts.map +0 -1
  121. package/src/feed/event-handlers/activity/handle-activity-marked.ts +0 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/feeds-client",
3
- "version": "0.3.51",
3
+ "version": "1.1.0",
4
4
  "packageManager": "yarn@3.2.4",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/es/index.mjs",
@@ -50,6 +50,7 @@
50
50
  "CHANGELOG.md"
51
51
  ],
52
52
  "dependencies": {
53
+ "@stream-io/filter": "^1.0.0",
53
54
  "@stream-io/logger": "^2.0.0",
54
55
  "@stream-io/state-store": "^1.1.6",
55
56
  "axios": "^1.7.7"
@@ -42,7 +42,9 @@ export class ActivityWithStateUpdates {
42
42
  constructor(
43
43
  public readonly id: string,
44
44
  private readonly feedsClient: FeedsClient,
45
- { fromResponse }: { fromResponse?: ActivityResponse } = { fromResponse: undefined },
45
+ { fromResponse }: { fromResponse?: ActivityResponse } = {
46
+ fromResponse: undefined,
47
+ },
46
48
  ) {
47
49
  this.state = new StateStore<ActivityState>({
48
50
  activity: undefined,
@@ -104,7 +106,7 @@ export class ActivityWithStateUpdates {
104
106
  });
105
107
 
106
108
  if (this.feed) {
107
- this.feed.activityAddedEventFilter = () => false;
109
+ this.feed.onNewActivity = () => 'ignore';
108
110
  }
109
111
 
110
112
  if (comments) {
@@ -177,6 +179,10 @@ export class ActivityWithStateUpdates {
177
179
  [initialState],
178
180
  [],
179
181
  'start',
182
+ {
183
+ hasOwnFields: initialState.current_feed?.own_capabilities !== undefined,
184
+ backfillOwnFields: false,
185
+ },
180
186
  );
181
187
  this.feed?.state.partialNext({
182
188
  activities,
@@ -1,30 +1,16 @@
1
- import { useMemo } from 'react';
2
-
3
- import { useFeedContext } from '../../contexts/StreamFeedContext';
4
- import { useNotificationStatus } from './useNotificationStatus';
5
1
  import type { Feed } from '../../../../feed';
6
2
  import type { AggregatedActivityResponse } from '../../../../gen/models';
7
3
 
4
+ /**
5
+ * @deprecated use aggregatedActivity.is_read instead
6
+ * @returns
7
+ */
8
8
  export const useIsAggregatedActivityRead = ({
9
- feed: feedFromProps,
9
+ feed: _,
10
10
  aggregatedActivity,
11
11
  }: {
12
12
  feed?: Feed;
13
13
  aggregatedActivity: AggregatedActivityResponse;
14
14
  }) => {
15
- const feedFromContext = useFeedContext();
16
- const feed = feedFromProps ?? feedFromContext;
17
-
18
- const { read_activities: readActivities, last_read_at: lastReadAt } =
19
- useNotificationStatus(feed) ?? {};
20
-
21
- const group = aggregatedActivity.group;
22
-
23
- return useMemo(
24
- () =>
25
- (lastReadAt &&
26
- aggregatedActivity.updated_at.getTime() <= lastReadAt.getTime()) ||
27
- (readActivities ?? []).includes(group),
28
- [lastReadAt, aggregatedActivity.updated_at, readActivities, group],
29
- );
15
+ return aggregatedActivity.is_read;
30
16
  };
@@ -1,30 +1,16 @@
1
- import { useMemo } from 'react';
2
-
3
- import { useFeedContext } from '../../contexts/StreamFeedContext';
4
- import { useNotificationStatus } from './useNotificationStatus';
5
1
  import type { Feed } from '../../../../feed';
6
2
  import type { AggregatedActivityResponse } from '../../../../gen/models';
7
3
 
4
+ /**
5
+ * @deprecated use aggregatedActivity.is_seen instead
6
+ * @returns
7
+ */
8
8
  export const useIsAggregatedActivitySeen = ({
9
- feed: feedFromProps,
9
+ feed: _,
10
10
  aggregatedActivity,
11
11
  }: {
12
12
  feed?: Feed;
13
13
  aggregatedActivity: AggregatedActivityResponse;
14
14
  }) => {
15
- const feedFromContext = useFeedContext();
16
- const feed = feedFromProps ?? feedFromContext;
17
-
18
- const { seen_activities: seenActivities, last_seen_at: lastSeenAt } =
19
- useNotificationStatus(feed) ?? {};
20
-
21
- const group = aggregatedActivity.group;
22
-
23
- return useMemo(
24
- () =>
25
- (lastSeenAt &&
26
- aggregatedActivity.updated_at.getTime() < lastSeenAt.getTime()) ||
27
- (seenActivities ?? []).includes(group),
28
- [lastSeenAt, aggregatedActivity.updated_at, seenActivities, group],
29
- );
15
+ return aggregatedActivity.is_seen;
30
16
  };
@@ -0,0 +1,44 @@
1
+ import { itemMatchesFilter, resolveDotPathValue } from '@stream-io/filter';
2
+ import type { ActivityResponse, GetOrCreateFeedRequest } from '../gen/models';
3
+
4
+ /**
5
+ * Resolvers that map feed filter field names to activity properties.
6
+ * - activity_type: API filter key maps to activity.type
7
+ * - within_bounds: API filter key maps to activity.location (for bounds check)
8
+ */
9
+ const activityResolvers = [
10
+ {
11
+ matchesField: (field: string) => field === 'activity_type',
12
+ resolve: (activity: ActivityResponse) => activity.type,
13
+ },
14
+ {
15
+ matchesField: (field: string) => field === 'within_bounds',
16
+ resolve: (activity: ActivityResponse) => activity.location,
17
+ },
18
+ {
19
+ matchesField: () => true,
20
+ resolve: (item: any, path: any) => resolveDotPathValue(item, path),
21
+ },
22
+ ] as const;
23
+
24
+ /**
25
+ * Returns true if the activity matches the feed's getOrCreate filter (e.g. filter_tags, activity_type, within_bounds).
26
+ * Use this inside onNewActivity to only add activities that match the current feed filter.
27
+ *
28
+ * @param activity - The activity to check
29
+ * @param requestConfig - The last getOrCreate request config (contains the filter). If omitted or filter is empty, returns true.
30
+ * @returns true if the activity matches the filter or there is no filter
31
+ */
32
+ export function activityFilter(
33
+ activity: ActivityResponse,
34
+ requestConfig?: GetOrCreateFeedRequest,
35
+ ): boolean {
36
+ const filter = requestConfig?.filter;
37
+ if (!filter || typeof filter !== 'object') {
38
+ return true;
39
+ }
40
+ return itemMatchesFilter(activity, filter, {
41
+ resolvers: [...activityResolvers],
42
+ arrayEqMode: 'contains',
43
+ });
44
+ }
@@ -7,7 +7,13 @@ export function addActivitiesToState(
7
7
  newActivities: ActivityResponse[],
8
8
  activities: ActivityResponse[] | undefined,
9
9
  position: 'start' | 'end',
10
- { fromWebSocket }: { fromWebSocket: boolean } = { fromWebSocket: false },
10
+ {
11
+ hasOwnFields,
12
+ backfillOwnFields,
13
+ }: { hasOwnFields: boolean; backfillOwnFields: boolean } = {
14
+ hasOwnFields: true,
15
+ backfillOwnFields: true,
16
+ },
11
17
  ) {
12
18
  if (activities === undefined) {
13
19
  return {
@@ -34,7 +40,10 @@ export function addActivitiesToState(
34
40
  ...activities,
35
41
  ...(position === 'end' ? newActivitiesDeduplicated : []),
36
42
  ];
37
- this.newActivitiesAdded(newActivitiesDeduplicated, { fromWebSocket });
43
+ this.activitiesAddedOrUpdated(newActivitiesDeduplicated, {
44
+ hasOwnFields,
45
+ backfillOwnFields,
46
+ });
38
47
 
39
48
  result = { changed: true, activities: updatedActivities };
40
49
  }
@@ -46,17 +55,22 @@ export function handleActivityAdded(
46
55
  this: Feed,
47
56
  event: EventPayload<'feeds.activity.added'>,
48
57
  ) {
49
- if (this.activityAddedEventFilter) {
50
- if (!this.activityAddedEventFilter(event)) {
51
- return;
52
- }
58
+ const currentUser = this.client.state.getLatestValue().connected_user;
59
+ const decision = this.resolveNewActivityDecision(
60
+ event.activity,
61
+ currentUser,
62
+ false,
63
+ );
64
+ if (decision === 'ignore') {
65
+ return;
53
66
  }
67
+ const position = decision === 'add-to-end' ? 'end' : 'start';
54
68
  const currentActivities = this.currentState.activities;
55
69
  const result = addActivitiesToState.bind(this)(
56
70
  [event.activity],
57
71
  currentActivities,
58
- this.currentState.addNewActivitiesTo,
59
- { fromWebSocket: true },
72
+ position,
73
+ { hasOwnFields: false, backfillOwnFields: true },
60
74
  );
61
75
  if (result.changed) {
62
76
  const activity = event.activity;
@@ -3,7 +3,13 @@ import type {
3
3
  ActivityPinResponse,
4
4
  ActivityResponse,
5
5
  } from '../../../gen/models';
6
- import type { EventPayload, UpdateStateResult } from '../../../types-internal';
6
+ import type {
7
+ EventPayload,
8
+ PartializeAllBut,
9
+ UpdateStateResult,
10
+ } from '../../../types-internal';
11
+ import { getStateUpdateQueueId, shouldUpdateState } from '../../../utils';
12
+ import { eventTriggeredByConnectedUser } from '../../../utils/event-triggered-by-connected-user';
7
13
 
8
14
  export function removeActivityFromState(
9
15
  this: Feed,
@@ -45,10 +51,30 @@ export const removePinnedActivityFromState = (
45
51
  }
46
52
  };
47
53
 
54
+ export type ActivityDeletedPayload = PartializeAllBut<
55
+ EventPayload<'feeds.activity.deleted'>,
56
+ 'activity'
57
+ >;
58
+
48
59
  export function handleActivityDeleted(
49
60
  this: Feed,
50
- event: EventPayload<'feeds.activity.deleted'>,
61
+ event: ActivityDeletedPayload,
62
+ fromWs?: boolean,
51
63
  ) {
64
+ if (
65
+ !shouldUpdateState({
66
+ stateUpdateQueueId: getStateUpdateQueueId(event, 'activity-deleted'),
67
+ stateUpdateQueue: this.stateUpdateQueue,
68
+ watch: this.currentState.watch,
69
+ fromWs,
70
+ isTriggeredByConnectedUser: eventTriggeredByConnectedUser.call(
71
+ this,
72
+ event,
73
+ ),
74
+ })
75
+ ) {
76
+ return;
77
+ }
52
78
  const {
53
79
  activities: currentActivities,
54
80
  pinned_activities: currentPinnedActivities,
@@ -2,12 +2,16 @@ import { updateEntityInArray } from '../../../utils';
2
2
  import { isPin } from '../is-activity-pin';
3
3
  import type { Feed } from '../../feed';
4
4
  import type { EventPayload } from '../../../types-internal';
5
- import type { ActivityFeedbackEventPayload, ActivityPinResponse, ActivityResponse } from '../../../gen/models';
5
+ import type {
6
+ ActivityFeedbackEventPayload,
7
+ ActivityPinResponse,
8
+ ActivityResponse,
9
+ } from '../../../gen/models';
6
10
 
7
- const updateActivityFromFeedback = <
11
+ export const updateActivityFromFeedback = <
8
12
  T extends ActivityResponse | ActivityPinResponse,
9
13
  >(
10
- feedback: ActivityFeedbackEventPayload,
14
+ feedback: Pick<ActivityFeedbackEventPayload, 'activity_id' | 'value'>,
11
15
  activities: T[] | undefined,
12
16
  ) => {
13
17
  if (!activities) {
@@ -18,10 +22,16 @@ const updateActivityFromFeedback = <
18
22
  }
19
23
  return updateEntityInArray<T>({
20
24
  entities: activities,
21
- matcher: (e) =>
22
- isPin(e)
23
- ? e.activity.id === feedback.activity_id
24
- : e.id === feedback.activity_id,
25
+ matcher: (e) => {
26
+ const newHidden = feedback.value === 'true';
27
+ if (isPin(e)) {
28
+ return (
29
+ e.activity.id === feedback.activity_id &&
30
+ e.activity.hidden !== newHidden
31
+ );
32
+ }
33
+ return e.id === feedback.activity_id && e.hidden !== newHidden;
34
+ },
25
35
  updater: (e) => {
26
36
  if (isPin(e)) {
27
37
  return {
@@ -1,11 +1,34 @@
1
1
  import type { ActivityPinResponse } from '../../../gen/models';
2
- import type { EventPayload } from '../../../types-internal';
2
+ import type { EventPayload, PartializeAllBut } from '../../../types-internal';
3
3
  import type { Feed } from '../../feed';
4
+ import { getStateUpdateQueueId, shouldUpdateState } from '../../../utils';
5
+ import { eventTriggeredByConnectedUser } from '../../../utils/event-triggered-by-connected-user';
6
+
7
+ export type ActivityPinnedPayload = PartializeAllBut<
8
+ EventPayload<'feeds.activity.pinned'>,
9
+ 'pinned_activity'
10
+ >;
4
11
 
5
12
  export function handleActivityPinned(
6
13
  this: Feed,
7
- event: EventPayload<'feeds.activity.pinned'>,
14
+ event: ActivityPinnedPayload,
15
+ fromWs?: boolean,
8
16
  ) {
17
+ if (
18
+ !shouldUpdateState({
19
+ stateUpdateQueueId: getStateUpdateQueueId(event, 'activity-pinned'),
20
+ stateUpdateQueue: this.stateUpdateQueue,
21
+ watch: this.currentState.watch,
22
+ fromWs,
23
+ isTriggeredByConnectedUser: eventTriggeredByConnectedUser.call(
24
+ this,
25
+ event,
26
+ ),
27
+ })
28
+ ) {
29
+ return;
30
+ }
31
+
9
32
  this.state.next((currentState) => {
10
33
  const newState = {
11
34
  ...currentState,
@@ -17,7 +40,6 @@ export function handleActivityPinned(
17
40
  const pinnedActivity: ActivityPinResponse = {
18
41
  ...event.pinned_activity,
19
42
  user: event.user!,
20
- feed: event.fid,
21
43
  updated_at: new Date(),
22
44
  };
23
45
 
@@ -1,10 +1,33 @@
1
- import type { EventPayload } from '../../../types-internal';
1
+ import type { EventPayload, PartializeAllBut } from '../../../types-internal';
2
2
  import type { Feed, FeedState } from '../../feed';
3
+ import { getStateUpdateQueueId, shouldUpdateState } from '../../../utils';
4
+ import { eventTriggeredByConnectedUser } from '../../../utils/event-triggered-by-connected-user';
5
+
6
+ export type ActivityUnpinnedPayload = PartializeAllBut<
7
+ EventPayload<'feeds.activity.unpinned'>,
8
+ 'pinned_activity'
9
+ >;
3
10
 
4
11
  export function handleActivityUnpinned(
5
12
  this: Feed,
6
- event: EventPayload<'feeds.activity.unpinned'>,
13
+ event: ActivityUnpinnedPayload,
14
+ fromWs?: boolean,
7
15
  ) {
16
+ if (
17
+ !shouldUpdateState({
18
+ stateUpdateQueueId: getStateUpdateQueueId(event, 'activity-unpinned'),
19
+ stateUpdateQueue: this.stateUpdateQueue,
20
+ watch: this.currentState.watch,
21
+ fromWs,
22
+ isTriggeredByConnectedUser: eventTriggeredByConnectedUser.call(
23
+ this,
24
+ event,
25
+ ),
26
+ })
27
+ ) {
28
+ return;
29
+ }
30
+
8
31
  this.state.next((currentState) => {
9
32
  let newState: FeedState | undefined;
10
33
 
@@ -90,7 +90,11 @@ export function handleActivityUpdated(
90
90
  ];
91
91
 
92
92
  if (result1?.changed || result2.changed) {
93
- this.client.hydratePollCache([payload.activity]);
93
+ this.activitiesAddedOrUpdated([payload.activity], {
94
+ hasOwnFields:
95
+ payload.activity.current_feed?.own_capabilities !== undefined,
96
+ backfillOwnFields: false,
97
+ });
94
98
 
95
99
  this.state.partialNext({
96
100
  activities: result1?.changed ? result1.entities : currentActivities,
@@ -5,4 +5,6 @@ export * from './handle-activity-updated';
5
5
  export * from './handle-activity-reaction-added';
6
6
  export * from './handle-activity-reaction-deleted';
7
7
  export * from './handle-activity-reaction-updated';
8
- export * from './handle-activity-marked';
8
+ export * from './handle-activity-pinned';
9
+ export * from './handle-activity-unpinned';
10
+ export * from './handle-activity-feedback';
@@ -6,7 +6,7 @@ import { updateActivity } from './activity-updater';
6
6
  export const addAggregatedActivitiesToState = (
7
7
  newAggregatedActivities: AggregatedActivityResponse[],
8
8
  aggregatedActivities: AggregatedActivityResponse[] | undefined,
9
- position: 'start' | 'end' | 'replace',
9
+ position: 'start' | 'end' | 'replace-then-end' | 'replace-then-start',
10
10
  ) => {
11
11
  let result: UpdateStateResult<{
12
12
  aggregated_activities: AggregatedActivityResponse[];
@@ -58,11 +58,20 @@ export const addAggregatedActivitiesToState = (
58
58
  (a) => a.group,
59
59
  );
60
60
  break;
61
- case 'replace':
61
+ case 'replace-then-end':
62
62
  result.aggregated_activities = replaceUniqueArrayMerge(
63
63
  aggregatedActivities ?? [],
64
64
  newAggregatedActivities,
65
65
  (a) => a.group,
66
+ 'end',
67
+ );
68
+ break;
69
+ case 'replace-then-start':
70
+ result.aggregated_activities = replaceUniqueArrayMerge(
71
+ aggregatedActivities ?? [],
72
+ newAggregatedActivities,
73
+ (a) => a.group,
74
+ 'start',
66
75
  );
67
76
  break;
68
77
  }
@@ -2,10 +2,15 @@ import type { Feed } from '../../../feed';
2
2
  import type {
3
3
  ActivityPinResponse,
4
4
  ActivityResponse,
5
- BookmarkAddedEvent,
6
5
  } from '../../../gen/models';
7
- import type { EventPayload } from '../../../types-internal';
6
+ import type { EventPayload, PartializeAllBut } from '../../../types-internal';
8
7
  import { updateEntityInArray } from '../../../utils';
8
+ import { isSameBookmark } from './handle-bookmark-deleted';
9
+
10
+ export type BookmarkAddedPayload = PartializeAllBut<
11
+ EventPayload<'feeds.bookmark.added'>,
12
+ 'bookmark'
13
+ >;
9
14
 
10
15
  const sharedUpdateActivity = ({
11
16
  currentActivity,
@@ -13,7 +18,7 @@ const sharedUpdateActivity = ({
13
18
  eventBelongsToCurrentUser,
14
19
  }: {
15
20
  currentActivity: ActivityResponse;
16
- event: BookmarkAddedEvent;
21
+ event: BookmarkAddedPayload;
17
22
  eventBelongsToCurrentUser: boolean;
18
23
  }): ActivityResponse => {
19
24
  let newOwnBookmarks = currentActivity.own_bookmarks;
@@ -30,13 +35,16 @@ const sharedUpdateActivity = ({
30
35
  };
31
36
 
32
37
  export const addBookmarkToActivities = (
33
- event: BookmarkAddedEvent,
38
+ event: BookmarkAddedPayload,
34
39
  activities: ActivityResponse[] | undefined,
35
40
  eventBelongsToCurrentUser: boolean,
36
41
  ) =>
37
42
  updateEntityInArray({
38
43
  entities: activities,
39
- matcher: (activity) => activity.id === event.bookmark.activity.id,
44
+ matcher: (activity) =>
45
+ activity.id === event.bookmark.activity.id &&
46
+ (!eventBelongsToCurrentUser ||
47
+ !activity.own_bookmarks.some((b) => isSameBookmark(b, event.bookmark))),
40
48
  updater: (matchedActivity) =>
41
49
  sharedUpdateActivity({
42
50
  currentActivity: matchedActivity,
@@ -46,14 +54,18 @@ export const addBookmarkToActivities = (
46
54
  });
47
55
 
48
56
  export const addBookmarkToPinnedActivities = (
49
- event: BookmarkAddedEvent,
57
+ event: BookmarkAddedPayload,
50
58
  pinnedActivities: ActivityPinResponse[] | undefined,
51
59
  eventBelongsToCurrentUser: boolean,
52
60
  ) =>
53
61
  updateEntityInArray({
54
62
  entities: pinnedActivities,
55
63
  matcher: (pinnedActivity) =>
56
- pinnedActivity.activity.id === event.bookmark.activity.id,
64
+ pinnedActivity.activity.id === event.bookmark.activity.id &&
65
+ (!eventBelongsToCurrentUser ||
66
+ !pinnedActivity.activity.own_bookmarks.some((b) =>
67
+ isSameBookmark(b, event.bookmark),
68
+ )),
57
69
  updater: (matchedPinnedActivity) => {
58
70
  const newActivity = sharedUpdateActivity({
59
71
  currentActivity: matchedPinnedActivity.activity,
@@ -72,10 +84,7 @@ export const addBookmarkToPinnedActivities = (
72
84
  },
73
85
  });
74
86
 
75
- export function handleBookmarkAdded(
76
- this: Feed,
77
- event: EventPayload<'feeds.bookmark.added'>,
78
- ) {
87
+ export function handleBookmarkAdded(this: Feed, event: BookmarkAddedPayload) {
79
88
  const {
80
89
  activities: currentActivities,
81
90
  pinned_activities: currentPinnedActivities,
@@ -2,14 +2,16 @@ import type { Feed } from '../../../feed';
2
2
  import type {
3
3
  ActivityPinResponse,
4
4
  ActivityResponse,
5
- BookmarkDeletedEvent,
6
5
  BookmarkResponse,
7
6
  } from '../../../gen/models';
8
- import type { EventPayload } from '../../../types-internal';
7
+ import type { EventPayload, PartializeAllBut } from '../../../types-internal';
9
8
  import { updateEntityInArray } from '../../../utils';
10
9
 
11
- // Helper function to check if two bookmarks are the same
12
- // A bookmark is identified by activity_id + folder_id + user_id
10
+ export type BookmarkDeletedPayload = PartializeAllBut<
11
+ EventPayload<'feeds.bookmark.deleted'>,
12
+ 'bookmark'
13
+ >;
14
+
13
15
  export const isSameBookmark = (
14
16
  bookmark1: BookmarkResponse,
15
17
  bookmark2: BookmarkResponse,
@@ -17,7 +19,8 @@ export const isSameBookmark = (
17
19
  return (
18
20
  bookmark1.user.id === bookmark2.user.id &&
19
21
  bookmark1.activity.id === bookmark2.activity.id &&
20
- bookmark1.folder?.id === bookmark2.folder?.id
22
+ bookmark1.folder?.id === bookmark2.folder?.id &&
23
+ bookmark1.updated_at.getTime() === bookmark2.updated_at.getTime()
21
24
  );
22
25
  };
23
26
 
@@ -27,7 +30,7 @@ const sharedUpdateActivity = ({
27
30
  eventBelongsToCurrentUser,
28
31
  }: {
29
32
  currentActivity: ActivityResponse;
30
- event: BookmarkDeletedEvent;
33
+ event: BookmarkDeletedPayload;
31
34
  eventBelongsToCurrentUser: boolean;
32
35
  }): ActivityResponse => {
33
36
  let newOwnBookmarks = currentActivity.own_bookmarks;
@@ -46,13 +49,16 @@ const sharedUpdateActivity = ({
46
49
  };
47
50
 
48
51
  export const removeBookmarkFromActivities = (
49
- event: BookmarkDeletedEvent,
52
+ event: BookmarkDeletedPayload,
50
53
  activities: ActivityResponse[] | undefined,
51
54
  eventBelongsToCurrentUser: boolean,
52
55
  ) =>
53
56
  updateEntityInArray({
54
57
  entities: activities,
55
- matcher: (activity) => activity.id === event.bookmark.activity.id,
58
+ matcher: (activity) =>
59
+ activity.id === event.bookmark.activity.id &&
60
+ (!eventBelongsToCurrentUser ||
61
+ activity.own_bookmarks.some((b) => isSameBookmark(b, event.bookmark))),
56
62
  updater: (matchedActivity) =>
57
63
  sharedUpdateActivity({
58
64
  currentActivity: matchedActivity,
@@ -62,14 +68,18 @@ export const removeBookmarkFromActivities = (
62
68
  });
63
69
 
64
70
  export const removeBookmarkFromPinnedActivities = (
65
- event: BookmarkDeletedEvent,
71
+ event: BookmarkDeletedPayload,
66
72
  pinnedActivities: ActivityPinResponse[] | undefined,
67
73
  eventBelongsToCurrentUser: boolean,
68
74
  ) =>
69
75
  updateEntityInArray({
70
76
  entities: pinnedActivities,
71
77
  matcher: (pinnedActivity) =>
72
- pinnedActivity.activity.id === event.bookmark.activity.id,
78
+ pinnedActivity.activity.id === event.bookmark.activity.id &&
79
+ (!eventBelongsToCurrentUser ||
80
+ pinnedActivity.activity.own_bookmarks.some((b) =>
81
+ isSameBookmark(b, event.bookmark),
82
+ )),
73
83
  updater: (matchedPinnedActivity) => {
74
84
  const newActivity = sharedUpdateActivity({
75
85
  currentActivity: matchedPinnedActivity.activity,
@@ -90,7 +100,7 @@ export const removeBookmarkFromPinnedActivities = (
90
100
 
91
101
  export function handleBookmarkDeleted(
92
102
  this: Feed,
93
- event: EventPayload<'feeds.bookmark.deleted'>,
103
+ event: BookmarkDeletedPayload,
94
104
  ) {
95
105
  const {
96
106
  activities: currentActivities,