@stream-io/feeds-client 0.3.51 → 1.0.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 (44) hide show
  1. package/CHANGELOG.md +12 -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 +1 -1
  5. package/dist/es/index.mjs +15 -14
  6. package/dist/es/react-bindings.mjs +1 -1
  7. package/dist/{feeds-client-DeAqnd1a.mjs → feeds-client-B03y08Kq.mjs} +184 -67
  8. package/dist/feeds-client-B03y08Kq.mjs.map +1 -0
  9. package/dist/{feeds-client-B4zeBggL.js → feeds-client-tw63OGrd.js} +178 -61
  10. package/dist/feeds-client-tw63OGrd.js.map +1 -0
  11. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  12. package/dist/types/activity-with-state-updates/activity-with-state-updates.d.ts.map +1 -1
  13. package/dist/types/feed/activity-filter.d.ts +11 -0
  14. package/dist/types/feed/activity-filter.d.ts.map +1 -0
  15. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts +3 -2
  16. package/dist/types/feed/event-handlers/activity/handle-activity-added.d.ts.map +1 -1
  17. package/dist/types/feed/event-handlers/activity/handle-activity-updated.d.ts.map +1 -1
  18. package/dist/types/feed/event-handlers/comment/handle-comment-updated.d.ts.map +1 -1
  19. package/dist/types/feed/event-handlers/follow/handle-follow-created.d.ts +2 -2
  20. package/dist/types/feed/event-handlers/follow/handle-follow-created.d.ts.map +1 -1
  21. package/dist/types/feed/feed.d.ts +17 -12
  22. package/dist/types/feed/feed.d.ts.map +1 -1
  23. package/dist/types/feeds-client/apply-new-activity-to-active-feeds.d.ts +4 -0
  24. package/dist/types/feeds-client/apply-new-activity-to-active-feeds.d.ts.map +1 -0
  25. package/dist/types/feeds-client/feeds-client.d.ts +6 -8
  26. package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
  27. package/dist/types/index.d.ts +1 -0
  28. package/dist/types/index.d.ts.map +1 -1
  29. package/dist/types/types.d.ts +15 -0
  30. package/dist/types/types.d.ts.map +1 -1
  31. package/package.json +2 -1
  32. package/src/activity-with-state-updates/activity-with-state-updates.ts +8 -2
  33. package/src/feed/activity-filter.ts +44 -0
  34. package/src/feed/event-handlers/activity/handle-activity-added.ts +22 -8
  35. package/src/feed/event-handlers/activity/handle-activity-updated.ts +5 -1
  36. package/src/feed/event-handlers/comment/handle-comment-updated.ts +11 -10
  37. package/src/feed/event-handlers/follow/handle-follow-created.ts +18 -1
  38. package/src/feed/feed.ts +72 -21
  39. package/src/feeds-client/apply-new-activity-to-active-feeds.ts +9 -0
  40. package/src/feeds-client/feeds-client.ts +46 -28
  41. package/src/index.ts +1 -0
  42. package/src/types.ts +17 -0
  43. package/dist/feeds-client-B4zeBggL.js.map +0 -1
  44. package/dist/feeds-client-DeAqnd1a.mjs.map +0 -1
package/src/feed/feed.ts CHANGED
@@ -13,7 +13,6 @@ import type {
13
13
  ThreadedCommentResponse,
14
14
  FollowRequest,
15
15
  QueryCommentsRequest,
16
- ActivityAddedEvent,
17
16
  EnrichmentOptions,
18
17
  } from '../gen/models';
19
18
  import type { StreamResponse } from '../gen-imports';
@@ -55,11 +54,15 @@ import {
55
54
  import { capitalize } from '../common/utils';
56
55
  import type {
57
56
  ActivityIdOrCommentId,
57
+ ConnectedUser,
58
58
  GetCommentsRepliesRequest,
59
59
  GetCommentsRequest,
60
60
  LoadingStates,
61
+ OnNewActivityCallback,
62
+ OnNewActivityResult,
61
63
  PagerResponseWithLoadingStates,
62
64
  } from '../types';
65
+ import { activityFilter } from './activity-filter';
63
66
  import {
64
67
  checkHasAnotherPage,
65
68
  Constants,
@@ -143,12 +146,6 @@ export type FeedState = Omit<
143
146
  * `true` if the feed is receiving real-time updates via WebSocket
144
147
  */
145
148
  watch: boolean;
146
-
147
- /**
148
- * When a new activity is received from a WebSocket event by default it's added to the start of the list. You can change this to `end` to add it to the end of the list.
149
- * Useful for story feeds.
150
- */
151
- addNewActivitiesTo: 'start' | 'end';
152
149
  };
153
150
 
154
151
  type EventHandlerByEventType = {
@@ -237,8 +234,7 @@ export class Feed extends FeedApi {
237
234
  id: string,
238
235
  data?: FeedResponse,
239
236
  watch = false,
240
- addNewActivitiesTo: 'start' | 'end' = 'start',
241
- public activityAddedEventFilter?: (event: ActivityAddedEvent) => boolean,
237
+ public onNewActivity?: OnNewActivityCallback,
242
238
  ) {
243
239
  super(client, groupId, id);
244
240
  this.state = new StateStore<FeedState>({
@@ -250,7 +246,6 @@ export class Feed extends FeedApi {
250
246
  is_loading_activities: false,
251
247
  comments_by_entity_id: {},
252
248
  watch,
253
- addNewActivitiesTo,
254
249
  });
255
250
  this.client = client;
256
251
 
@@ -278,10 +273,6 @@ export class Feed extends FeedApi {
278
273
  return this.state.getLatestValue();
279
274
  }
280
275
 
281
- set addNewActivitiesTo(value: 'start' | 'end') {
282
- this.state.partialNext({ addNewActivitiesTo: value });
283
- }
284
-
285
276
  hasActivity(activityId: string) {
286
277
  return this.indexedActivityIds.has(activityId);
287
278
  }
@@ -292,6 +283,31 @@ export class Feed extends FeedApi {
292
283
  );
293
284
  }
294
285
 
286
+ /**
287
+ * Resolves how to handle a new activity (WS or HTTP): ignore, add-to-start, or add-to-end.
288
+ * Uses onNewActivity if set; else default (current user + filter match) adds to start.
289
+ */
290
+ protected resolveNewActivityDecision(
291
+ activity: ActivityResponse,
292
+ currentUser: ConnectedUser | undefined,
293
+ _fromHttp: boolean,
294
+ ): OnNewActivityResult {
295
+ if (this.onNewActivity) {
296
+ return this.onNewActivity({ activity, currentUser });
297
+ }
298
+ if (!currentUser) return 'ignore';
299
+ if (activity.user?.id !== currentUser.id) return 'ignore';
300
+ if (
301
+ !activityFilter(
302
+ activity,
303
+ this.currentState.last_get_or_create_request_config,
304
+ )
305
+ ) {
306
+ return 'ignore';
307
+ }
308
+ return 'add-to-start';
309
+ }
310
+
295
311
  async synchronize() {
296
312
  const { last_get_or_create_request_config } = this.state.getLatestValue();
297
313
  if (last_get_or_create_request_config?.watch) {
@@ -346,6 +362,7 @@ export class Feed extends FeedApi {
346
362
  response.activities,
347
363
  currentActivities,
348
364
  'end',
365
+ { hasOwnFields: true, backfillOwnFields: false },
349
366
  );
350
367
 
351
368
  const aggregatedActivitiesResult = addAggregatedActivitiesToState(
@@ -416,7 +433,7 @@ export class Feed extends FeedApi {
416
433
  });
417
434
  }
418
435
 
419
- this.newActivitiesAdded(response.activities);
436
+ this.activitiesAddedOrUpdated(response.activities);
420
437
 
421
438
  return response;
422
439
  } finally {
@@ -939,12 +956,44 @@ export class Feed extends FeedApi {
939
956
  });
940
957
  }
941
958
 
959
+ /**
960
+ * Applies a new activity to this feed's state (decision + add to activities).
961
+ * Used when the activity was added via this feed's addActivity or via client.addActivity.
962
+ */
963
+ protected addActivityFromHTTPResponse(activity: ActivityResponse): void {
964
+ const currentUser = this.client.state.getLatestValue().connected_user;
965
+ const decision = this.resolveNewActivityDecision(
966
+ activity,
967
+ currentUser,
968
+ true,
969
+ );
970
+ if (decision !== 'ignore') {
971
+ const position = decision === 'add-to-end' ? 'end' : 'start';
972
+ const currentActivities = this.currentState.activities;
973
+ const result = addActivitiesToState.bind(this)(
974
+ [activity],
975
+ currentActivities,
976
+ position,
977
+ {
978
+ hasOwnFields: activity.current_feed?.own_capabilities !== undefined,
979
+ backfillOwnFields: false,
980
+ },
981
+ );
982
+ if (result.changed) {
983
+ this.client.hydratePollCache([activity]);
984
+ this.state.partialNext({ activities: result.activities });
985
+ }
986
+ }
987
+ }
988
+
942
989
  async addActivity(request: Omit<ActivityRequest, 'feeds'>) {
943
990
  const response = await this.client.addActivity({
944
991
  ...request,
945
992
  feeds: [this.feed],
946
993
  });
947
994
 
995
+ this.addActivityFromHTTPResponse(response.activity);
996
+
948
997
  return response;
949
998
  }
950
999
 
@@ -983,11 +1032,12 @@ export class Feed extends FeedApi {
983
1032
  this.eventDispatcher.dispatch(event);
984
1033
  }
985
1034
 
986
- protected newActivitiesAdded(
1035
+ protected activitiesAddedOrUpdated(
987
1036
  activities: ActivityResponse[],
988
1037
  options: {
989
- fromWebSocket: boolean;
990
- } = { fromWebSocket: false },
1038
+ hasOwnFields: boolean;
1039
+ backfillOwnFields: boolean;
1040
+ } = { hasOwnFields: true, backfillOwnFields: true },
991
1041
  ) {
992
1042
  this.client.hydratePollCache(activities);
993
1043
  this.getOrCreateFeeds(activities, options);
@@ -996,7 +1046,8 @@ export class Feed extends FeedApi {
996
1046
  private getOrCreateFeeds(
997
1047
  activities: ActivityResponse[],
998
1048
  options: {
999
- fromWebSocket: boolean;
1049
+ hasOwnFields: boolean;
1050
+ backfillOwnFields: boolean;
1000
1051
  },
1001
1052
  ) {
1002
1053
  const enrichmentOptions =
@@ -1018,7 +1069,7 @@ export class Feed extends FeedApi {
1018
1069
  const fieldsToUpdate: Array<
1019
1070
  'own_capabilities' | 'own_follows' | 'own_followings' | 'own_membership'
1020
1071
  > = [];
1021
- if (!options.fromWebSocket) {
1072
+ if (options.hasOwnFields) {
1022
1073
  fieldsToUpdate.push('own_membership');
1023
1074
  if (!enrichmentOptions?.skip_own_capabilities) {
1024
1075
  fieldsToUpdate.push('own_capabilities');
@@ -1038,7 +1089,7 @@ export class Feed extends FeedApi {
1038
1089
  fieldsToUpdate,
1039
1090
  });
1040
1091
  });
1041
- if (options.fromWebSocket) {
1092
+ if (!options.hasOwnFields && options.backfillOwnFields) {
1042
1093
  const uninitializedFeeds = newFeeds.filter((feedResponse) => {
1043
1094
  const feed = this.client.feed(feedResponse.group_id, feedResponse.id);
1044
1095
  // own_capabilities can only be undefined if we haven't fetched it yet
@@ -0,0 +1,9 @@
1
+ import type { Feed } from '../feed/feed';
2
+ import type { ActivityResponse } from '../gen/models';
3
+
4
+ export function applyNewActivityToActiveFeeds(
5
+ this: Feed,
6
+ activity: ActivityResponse,
7
+ ): void {
8
+ return this.addActivityFromHTTPResponse(activity);
9
+ }
@@ -1,7 +1,8 @@
1
1
  import { FeedsApi } from '../gen/feeds/FeedsApi';
2
2
  import type {
3
- ActivityAddedEvent,
4
3
  ActivityResponse,
4
+ AddActivityRequest,
5
+ AddActivityResponse,
5
6
  AddCommentReactionRequest,
6
7
  AddCommentReactionResponse,
7
8
  AddCommentRequest,
@@ -41,6 +42,7 @@ import type {
41
42
  import type {
42
43
  ConnectedUser,
43
44
  FeedsEvent,
45
+ OnNewActivityCallback,
44
46
  StreamFile,
45
47
  TokenOrProvider,
46
48
  } from '../types';
@@ -82,6 +84,7 @@ import {
82
84
  handleWatchStarted,
83
85
  handleWatchStopped,
84
86
  } from '../feed';
87
+ import { applyNewActivityToActiveFeeds } from './apply-new-activity-to-active-feeds';
85
88
  import { handleUserUpdated } from './event-handlers';
86
89
  import {
87
90
  type SyncFailure,
@@ -614,6 +617,19 @@ export class FeedsClient extends FeedsApi {
614
617
  return response;
615
618
  };
616
619
 
620
+ async addActivity(
621
+ request: AddActivityRequest,
622
+ ): Promise<StreamResponse<AddActivityResponse>> {
623
+ const response = await super.addActivity(request);
624
+ request.feeds.forEach((fid) => {
625
+ const feed = this.activeFeeds[fid];
626
+ if (feed) {
627
+ applyNewActivityToActiveFeeds.call(feed, response.activity);
628
+ }
629
+ });
630
+ return response;
631
+ }
632
+
617
633
  addActivityReaction = async (
618
634
  request: AddReactionRequest & {
619
635
  activity_id: string;
@@ -737,16 +753,14 @@ export class FeedsClient extends FeedsApi {
737
753
  * @param groupId for example `user`, `notification` or id of a custom feed group
738
754
  * @param id
739
755
  * @param options
740
- * @param options.addNewActivitiesTo - when a new activity is received from a WebSocket event by default it's added to the start of the list. You can change this to `end` to add it to the end of the list. Useful for story feeds.
741
- * @param options.activityAddedEventFilter - a callback that is called when a new activity is received from a WebSocket event. You can use this to prevent the activity from being added to the feed. Useful for feed filtering, or if you don't want new activities to be added to the feed.
756
+ * @param options.onNewActivity - callback to control how new activities (WS or addActivity response) are added: 'add-to-start', 'add-to-end', or 'ignore'.
742
757
  * @returns
743
758
  */
744
759
  feed = (
745
760
  groupId: string,
746
761
  id: string,
747
762
  options?: {
748
- addNewActivitiesTo?: 'start' | 'end';
749
- activityAddedEventFilter?: (event: ActivityAddedEvent) => boolean;
763
+ onNewActivity?: OnNewActivityCallback;
750
764
  },
751
765
  ) => {
752
766
  return this.getOrCreateActiveFeed({
@@ -787,12 +801,14 @@ export class FeedsClient extends FeedsApi {
787
801
  id: feedResponse.id,
788
802
  data: feedResponse,
789
803
  watch: request?.watch,
790
- fieldsToUpdate: [
791
- 'own_capabilities',
792
- 'own_follows',
793
- 'own_membership',
794
- 'own_followings',
795
- ],
804
+ fieldsToUpdate: request?.enrich_own_fields
805
+ ? [
806
+ 'own_capabilities',
807
+ 'own_follows',
808
+ 'own_membership',
809
+ 'own_followings',
810
+ ]
811
+ : [],
796
812
  }),
797
813
  );
798
814
 
@@ -843,7 +859,7 @@ export class FeedsClient extends FeedsApi {
843
859
  // For follow API endpoints we update the state after HTTP response to allow queryFeeds with watch: false
844
860
  async follow(request: FollowRequest) {
845
861
  const response = await super.follow(request);
846
- this.updateStateFromFollows([response.follow]);
862
+ this.updateStateFromFollows([response.follow], !!request.enrich_own_fields);
847
863
 
848
864
  return response;
849
865
  }
@@ -855,7 +871,7 @@ export class FeedsClient extends FeedsApi {
855
871
  */
856
872
  async followBatch(request: FollowBatchRequest) {
857
873
  const response = await super.followBatch(request);
858
- this.updateStateFromFollows(response.follows);
874
+ this.updateStateFromFollows(response.follows, !!request.enrich_own_fields);
859
875
 
860
876
  return response;
861
877
  }
@@ -863,12 +879,16 @@ export class FeedsClient extends FeedsApi {
863
879
  async getOrCreateFollows(request: FollowBatchRequest) {
864
880
  const response = await super.getOrCreateFollows(request);
865
881
 
866
- this.updateStateFromFollows(response.created);
882
+ this.updateStateFromFollows(response.created, !!request.enrich_own_fields);
867
883
 
868
884
  return response;
869
885
  }
870
886
 
871
- async unfollow(request: { source: string; target: string, enrich_own_fields?: boolean }) {
887
+ async unfollow(request: {
888
+ source: string;
889
+ target: string;
890
+ enrich_own_fields?: boolean;
891
+ }) {
872
892
  const response = await super.unfollow(request);
873
893
  this.updateStateFromUnfollows([response.follow]);
874
894
 
@@ -951,8 +971,7 @@ export class FeedsClient extends FeedsApi {
951
971
  data?: FeedResponse;
952
972
  watch?: boolean;
953
973
  options?: {
954
- addNewActivitiesTo?: 'start' | 'end';
955
- activityAddedEventFilter?: (event: ActivityAddedEvent) => boolean;
974
+ onNewActivity?: OnNewActivityCallback;
956
975
  };
957
976
  fieldsToUpdate: Array<
958
977
  'own_capabilities' | 'own_follows' | 'own_followings' | 'own_membership'
@@ -969,20 +988,14 @@ export class FeedsClient extends FeedsApi {
969
988
  id,
970
989
  data,
971
990
  watch,
972
- options?.addNewActivitiesTo,
973
- options?.activityAddedEventFilter,
991
+ options?.onNewActivity,
974
992
  );
975
993
  }
976
994
 
977
995
  const feed = this.activeFeeds[fid];
978
996
 
979
- if (!isCreated && options) {
980
- if (options?.addNewActivitiesTo) {
981
- feed.addNewActivitiesTo = options.addNewActivitiesTo;
982
- }
983
- if (options?.activityAddedEventFilter) {
984
- feed.activityAddedEventFilter = options.activityAddedEventFilter;
985
- }
997
+ if (!isCreated && options?.onNewActivity !== undefined) {
998
+ feed.onNewActivity = options.onNewActivity;
986
999
  }
987
1000
 
988
1001
  if (!feed.currentState.watch) {
@@ -1097,13 +1110,18 @@ export class FeedsClient extends FeedsApi {
1097
1110
  ];
1098
1111
  }
1099
1112
 
1100
- private updateStateFromFollows(follows: FollowResponse[]) {
1113
+ private updateStateFromFollows(
1114
+ follows: FollowResponse[],
1115
+ hasOwnFields: boolean,
1116
+ ) {
1101
1117
  follows.forEach((follow) => {
1102
1118
  const feeds = [
1103
1119
  ...this.findAllActiveFeedsByFid(follow.source_feed.feed),
1104
1120
  ...this.findAllActiveFeedsByFid(follow.target_feed.feed),
1105
1121
  ];
1106
- feeds.forEach((f) => handleFollowCreated.bind(f)({ follow }, false));
1122
+ feeds.forEach((f) =>
1123
+ handleFollowCreated.bind(f)({ follow }, false, hasOwnFields),
1124
+ );
1107
1125
  });
1108
1126
  }
1109
1127
 
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from '@stream-io/state-store';
2
2
  export * from './feeds-client/feeds-client';
3
3
  export * from './feed/feed';
4
+ export { activityFilter } from './feed/activity-filter';
4
5
  export * from './gen/models';
5
6
  export * from './types';
6
7
  export * from './common/types';
package/src/types.ts CHANGED
@@ -36,3 +36,20 @@ export type StreamFile = File | { name: string; uri: string; type: string };
36
36
  export type CommentParent = ActivityResponse | CommentResponse;
37
37
 
38
38
  export type ConnectedUser = OwnUserResponse & { name?: string; image?: string };
39
+
40
+ /**
41
+ * Result of the onNewActivity callback: whether to add a new activity to the feed and where.
42
+ * - 'add-to-start': prepend to the activities list (e.g. new posts at top)
43
+ * - 'add-to-end': append to the activities list (e.g. stories)
44
+ * - 'ignore': do not add to the feed
45
+ */
46
+ export type OnNewActivityResult = 'add-to-start' | 'add-to-end' | 'ignore';
47
+
48
+ /**
49
+ * Callback invoked when a new activity is received (WebSocket event or addActivity HTTP response).
50
+ * Return how the feed should handle it.
51
+ */
52
+ export type OnNewActivityCallback = (params: {
53
+ activity: ActivityResponse;
54
+ currentUser: ConnectedUser | undefined;
55
+ }) => OnNewActivityResult;