@stream-io/feeds-client 0.3.6 → 0.3.8

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 (62) hide show
  1. package/CHANGELOG.md +14 -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 +76 -14
  5. package/dist/cjs/react-bindings.js.map +1 -1
  6. package/dist/es/index.mjs +3 -2
  7. package/dist/es/index.mjs.map +1 -1
  8. package/dist/es/react-bindings.mjs +76 -14
  9. package/dist/es/react-bindings.mjs.map +1 -1
  10. package/dist/{feeds-client-47vliZx_.js → feeds-client-CwioZBvA.js} +355 -52
  11. package/dist/feeds-client-CwioZBvA.js.map +1 -0
  12. package/dist/{feeds-client-Cd2LsXp-.mjs → feeds-client-DVbsjKUf.mjs} +355 -52
  13. package/dist/feeds-client-DVbsjKUf.mjs.map +1 -0
  14. package/dist/types/activity-with-state-updates/activity-with-state-updates.d.ts +49 -0
  15. package/dist/types/activity-with-state-updates/activity-with-state-updates.d.ts.map +1 -0
  16. package/dist/types/activity-with-state-updates/get-feed.d.ts +3 -0
  17. package/dist/types/activity-with-state-updates/get-feed.d.ts.map +1 -0
  18. package/dist/types/bindings/react/hooks/feed-state-hooks/index.d.ts +1 -0
  19. package/dist/types/bindings/react/hooks/feed-state-hooks/index.d.ts.map +1 -1
  20. package/dist/types/bindings/react/hooks/feed-state-hooks/useActivityComments.d.ts +32 -0
  21. package/dist/types/bindings/react/hooks/feed-state-hooks/useActivityComments.d.ts.map +1 -0
  22. package/dist/types/bindings/react/hooks/feed-state-hooks/useComments.d.ts +4 -0
  23. package/dist/types/bindings/react/hooks/feed-state-hooks/useComments.d.ts.map +1 -1
  24. package/dist/types/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.d.ts.map +1 -1
  25. package/dist/types/bindings/react/hooks/useCreateFeedsClient.d.ts.map +1 -1
  26. package/dist/types/common/real-time/event-models.d.ts +1 -0
  27. package/dist/types/common/real-time/event-models.d.ts.map +1 -1
  28. package/dist/types/feed/event-handlers/activity-updater.d.ts +1 -0
  29. package/dist/types/feed/event-handlers/activity-updater.d.ts.map +1 -1
  30. package/dist/types/feed/feed.d.ts +1 -1
  31. package/dist/types/feed/feed.d.ts.map +1 -1
  32. package/dist/types/feeds-client/active-activity.d.ts +8 -0
  33. package/dist/types/feeds-client/active-activity.d.ts.map +1 -0
  34. package/dist/types/feeds-client/feeds-client.d.ts +36 -3
  35. package/dist/types/feeds-client/feeds-client.d.ts.map +1 -1
  36. package/dist/types/gen/feeds/FeedsApi.d.ts +9 -1
  37. package/dist/types/gen/feeds/FeedsApi.d.ts.map +1 -1
  38. package/dist/types/gen/models/index.d.ts +52 -0
  39. package/dist/types/gen/models/index.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +1 -0
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/types.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/activity-with-state-updates/activity-with-state-updates.ts +190 -0
  45. package/src/activity-with-state-updates/get-feed.ts +5 -0
  46. package/src/bindings/react/hooks/feed-state-hooks/index.ts +1 -0
  47. package/src/bindings/react/hooks/feed-state-hooks/useActivityComments.ts +113 -0
  48. package/src/bindings/react/hooks/feed-state-hooks/useComments.ts +4 -0
  49. package/src/bindings/react/hooks/feed-state-hooks/useOwnCapabilities.ts +12 -9
  50. package/src/bindings/react/hooks/useCreateFeedsClient.ts +0 -6
  51. package/src/common/real-time/event-models.ts +5 -1
  52. package/src/feed/feed.ts +16 -6
  53. package/src/feeds-client/active-activity.ts +42 -0
  54. package/src/feeds-client/feeds-client.ts +162 -53
  55. package/src/gen/feeds/FeedsApi.ts +86 -0
  56. package/src/gen/model-decoders/decoders.ts +41 -0
  57. package/src/gen/models/index.ts +84 -0
  58. package/src/index.ts +1 -0
  59. package/src/test-utils/response-generators.ts +1 -0
  60. package/src/types.ts +8 -10
  61. package/dist/feeds-client-47vliZx_.js.map +0 -1
  62. package/dist/feeds-client-Cd2LsXp-.mjs.map +0 -1
@@ -0,0 +1,190 @@
1
+ import { StateStore } from '@stream-io/state-store';
2
+ import type { Feed, FeedState } from '../feed';
3
+ import type { FeedsClient } from '../feeds-client';
4
+ import type { ActivityResponse } from '../gen/models';
5
+ import {
6
+ connectActivityToFeed,
7
+ disconnectActivityFromFeed,
8
+ isAnyFeedWatched,
9
+ } from '../feeds-client/active-activity';
10
+ import type { GetCommentsRequest } from '@self';
11
+ import { deepEqual } from '../utils/deep-equal';
12
+
13
+ type GetActivityConfig = {
14
+ comments?: Partial<
15
+ Omit<GetCommentsRequest, 'object_id' | 'object_type' | 'next'>
16
+ >;
17
+ };
18
+
19
+ export type ActivityState = { activity?: ActivityResponse } & Pick<
20
+ FeedState,
21
+ 'comments_by_entity_id'
22
+ > & {
23
+ /**
24
+ * True when state is being fetched from API
25
+ */
26
+ is_loading: boolean;
27
+ /**
28
+ * @internal
29
+ */
30
+ last_get_request_config?: GetActivityConfig;
31
+ };
32
+
33
+ export class ActivityWithStateUpdates {
34
+ readonly state: StateStore<ActivityState>;
35
+ protected feed: Feed | undefined;
36
+ private unsubscribeFromFeedState?: () => void;
37
+ private inProgressGet?: {
38
+ request?: GetActivityConfig;
39
+ promise: Promise<ActivityResponse>;
40
+ };
41
+
42
+ constructor(
43
+ public readonly id: string,
44
+ private readonly feedsClient: FeedsClient,
45
+ ) {
46
+ this.state = new StateStore<ActivityState>({
47
+ activity: undefined,
48
+ comments_by_entity_id: {},
49
+ is_loading: false,
50
+ });
51
+ }
52
+
53
+ get currentState() {
54
+ return this.state.getLatestValue();
55
+ }
56
+
57
+ get feeds() {
58
+ return this.currentState.activity?.feeds ?? [];
59
+ }
60
+
61
+ /**
62
+ * Fetch activity and load it into state
63
+ * @param watch - Whether to watch the feed the activity belongs to for real-time updates
64
+ * @param feed - The feed to watch. Use only if the activity belongs to multiple feeds and you want to specify the feed explicitly.
65
+ * @param feedSelector - A function to select the feed from the activity response. Use only if the activity belongs to multiple feeds and you want to specify the feed explicitly.
66
+ */
67
+ async get(request: GetActivityConfig = {}) {
68
+ if (this.inProgressGet && deepEqual(this.inProgressGet.request, request)) {
69
+ return this.inProgressGet.promise;
70
+ }
71
+
72
+ const { comments } = request;
73
+
74
+ this.state.partialNext({
75
+ is_loading: true,
76
+ last_get_request_config: request,
77
+ });
78
+
79
+ const getActivityRequest = this.feedsClient
80
+ .getActivity({
81
+ id: this.id,
82
+ })
83
+ .then((response) => response.activity);
84
+ this.inProgressGet = { request, promise: getActivityRequest };
85
+ const activityResponse = await getActivityRequest;
86
+
87
+ this.feedsClient.hydratePollCache([activityResponse]);
88
+
89
+ this.setFeed({
90
+ // We set feed to first containing feed
91
+ // But in WS event handler we match events by any of the containing feeds, so as long as any of the containing feeds are watched, we'll do a state update
92
+ // This is a bit hacky, proper solution would be to refactor all activity event handlers and detach them from feed instance
93
+ fid: activityResponse.feeds[0],
94
+ initialState: activityResponse,
95
+ });
96
+
97
+ if (this.feed) {
98
+ this.feed.activityAddedEventFilter = () => false;
99
+ }
100
+
101
+ if (comments) {
102
+ await this.loadNextPageActivityComments(comments);
103
+ }
104
+
105
+ this.subscribeToFeedState();
106
+
107
+ this.inProgressGet = undefined;
108
+
109
+ return activityResponse;
110
+ }
111
+
112
+ loadNextPageActivityComments(
113
+ request?: Partial<
114
+ Omit<GetCommentsRequest, 'object_id' | 'object_type' | 'next'>
115
+ >,
116
+ ) {
117
+ const activity = this.feed?.currentState.activities?.[0];
118
+ if (!activity || !this.feed) {
119
+ throw new Error('Initialize activity first');
120
+ }
121
+ if (!this.currentState.last_get_request_config?.comments) {
122
+ this.state.partialNext({
123
+ last_get_request_config: {
124
+ ...this.currentState.last_get_request_config,
125
+ comments: request,
126
+ },
127
+ });
128
+ }
129
+ return this.feed.loadNextPageActivityComments(activity, request);
130
+ }
131
+
132
+ loadNextPageCommentReplies(
133
+ ...params: Parameters<Feed['loadNextPageCommentReplies']>
134
+ ) {
135
+ if (!this.feed) {
136
+ throw new Error('Initialize activity first');
137
+ }
138
+ return this.feed.loadNextPageCommentReplies(...params);
139
+ }
140
+
141
+ dispose() {
142
+ this.unsubscribeFromFeedState?.();
143
+ disconnectActivityFromFeed.call(this.feedsClient, this.id);
144
+ }
145
+
146
+ /**
147
+ * @internal
148
+ */
149
+ async synchronize() {
150
+ const allFids = this.currentState.activity?.feeds ?? [];
151
+ if (!isAnyFeedWatched.call(this.feedsClient, allFids)) {
152
+ return;
153
+ }
154
+ this.inProgressGet = undefined;
155
+ return this.get(this.currentState.last_get_request_config);
156
+ }
157
+
158
+ private setFeed({
159
+ fid,
160
+ initialState,
161
+ }: {
162
+ fid: string;
163
+ initialState: ActivityResponse;
164
+ }) {
165
+ this.feed = connectActivityToFeed.call(this.feedsClient, { fid });
166
+
167
+ this.feed.state.partialNext({
168
+ activities: [initialState],
169
+ });
170
+ }
171
+
172
+ private subscribeToFeedState() {
173
+ this.unsubscribeFromFeedState?.();
174
+ this.unsubscribeFromFeedState = this.feed?.state.subscribeWithSelector(
175
+ (state) => ({
176
+ activity: state.activities?.find((activity) => activity.id === this.id),
177
+ comments_by_entity_id: state.comments_by_entity_id,
178
+ }),
179
+ (state) => {
180
+ if (state.activity) {
181
+ this.state.partialNext({
182
+ activity: state.activity,
183
+ comments_by_entity_id: state.comments_by_entity_id,
184
+ is_loading: false,
185
+ });
186
+ }
187
+ },
188
+ );
189
+ }
190
+ }
@@ -0,0 +1,5 @@
1
+ import type { ActivityWithStateUpdates } from './activity-with-state-updates';
2
+
3
+ export function getFeed(this: ActivityWithStateUpdates) {
4
+ return this.feed;
5
+ }
@@ -9,3 +9,4 @@ export * from './useNotificationStatus';
9
9
  export * from './useAggregatedActivities';
10
10
  export * from './useIsAggregatedActivityRead';
11
11
  export * from './useIsAggregatedActivitySeen';
12
+ export * from './useActivityComments';
@@ -0,0 +1,113 @@
1
+ import { useCallback, useMemo } from 'react';
2
+ import {
3
+ type ActivityResponse,
4
+ type CommentResponse,
5
+ type Feed,
6
+ type FeedState,
7
+ checkHasAnotherPage,
8
+ type ActivityWithStateUpdates,
9
+ type ActivityState,
10
+ type StateStore,
11
+ } from '@self';
12
+ import { useFeedContext } from '../../contexts/StreamFeedContext';
13
+ import { useStateStore } from '@stream-io/state-store/react-bindings';
14
+
15
+ const canLoadComments = (
16
+ feedOrActivity: Feed | ActivityResponse | ActivityWithStateUpdates,
17
+ ): feedOrActivity is ActivityWithStateUpdates | Feed => {
18
+ return (
19
+ 'loadNextPageCommentReplies' in feedOrActivity &&
20
+ 'loadNextPageActivityComments' in feedOrActivity
21
+ );
22
+ };
23
+
24
+ type UseCommentsReturnType<T extends ActivityResponse | CommentResponse> = {
25
+ comments: NonNullable<
26
+ FeedState['comments_by_entity_id'][T['id']]
27
+ >['comments'];
28
+ comments_pagination: NonNullable<
29
+ FeedState['comments_by_entity_id'][T['id']]
30
+ >['pagination'];
31
+ has_next_page: boolean;
32
+ is_loading_next_page: boolean;
33
+ loadNextPage: (
34
+ request?: T extends CommentResponse
35
+ ? Parameters<Feed['loadNextPageCommentReplies']>[1]
36
+ : Parameters<Feed['loadNextPageActivityComments']>[1],
37
+ ) => Promise<void>;
38
+ };
39
+
40
+ export function useActivityComments({
41
+ feed: feedFromProps,
42
+ parentComment,
43
+ activity,
44
+ }: {
45
+ feed?: Feed;
46
+ parentComment?: CommentResponse;
47
+ activity?: ActivityResponse | ActivityWithStateUpdates;
48
+ }) {
49
+ const feedFromContext = useFeedContext();
50
+ const feed = feedFromProps ?? feedFromContext;
51
+ const feedOrActivity = feed ?? activity;
52
+
53
+ if (!feedOrActivity) {
54
+ throw new Error('Feed or activity is required');
55
+ }
56
+
57
+ if (!canLoadComments(feedOrActivity)) {
58
+ throw new Error('Feed or activity does not support loading comments');
59
+ }
60
+
61
+ if (!(activity || parentComment)) {
62
+ throw new Error('Activity or parent comment is required');
63
+ }
64
+
65
+ const entityId = parentComment?.id ?? activity?.id ?? '';
66
+ const selector = useCallback(
67
+ (state: FeedState | ActivityState) => ({
68
+ comments: state.comments_by_entity_id?.[entityId]?.comments,
69
+ comments_pagination: state.comments_by_entity_id?.[entityId]?.pagination,
70
+ }),
71
+ [entityId],
72
+ );
73
+
74
+ const data = useStateStore(
75
+ feedOrActivity.state as StateStore<FeedState | ActivityState>,
76
+ selector,
77
+ );
78
+
79
+ const loadNextPage = useCallback<
80
+ UseCommentsReturnType<ActivityResponse | CommentResponse>['loadNextPage']
81
+ >(
82
+ (request) => {
83
+ if (parentComment) {
84
+ return feedOrActivity.loadNextPageCommentReplies(
85
+ parentComment,
86
+ request,
87
+ );
88
+ } else {
89
+ if (activity && canLoadComments(activity)) {
90
+ return activity.loadNextPageActivityComments(request);
91
+ } else if (feed) {
92
+ return feed.loadNextPageActivityComments(activity?.id ?? '', request);
93
+ } else {
94
+ throw new Error('Activity or feed is required');
95
+ }
96
+ }
97
+ },
98
+ [feedOrActivity, feed, parentComment, activity],
99
+ );
100
+
101
+ return useMemo(() => {
102
+ return {
103
+ ...data,
104
+ has_next_page: checkHasAnotherPage(
105
+ data.comments,
106
+ data.comments_pagination?.next,
107
+ ),
108
+ is_loading_next_page:
109
+ data?.comments_pagination?.loading_next_page ?? false,
110
+ loadNextPage,
111
+ };
112
+ }, [data, loadNextPage]);
113
+ }
@@ -27,6 +27,10 @@ type UseCommentsReturnType<T extends ActivityResponse | CommentResponse> = {
27
27
  ) => Promise<void>;
28
28
  };
29
29
 
30
+ /**
31
+ * @deprecated Use `useActivityComments` instead.
32
+ * @param
33
+ */
30
34
  export function useComments<T extends CommentParent>(_: {
31
35
  feed: Feed;
32
36
  parent: T;
@@ -12,16 +12,19 @@ export const useOwnCapabilities = (feedFromProps?: Feed) => {
12
12
  const feed = feedFromProps ?? feedFromContext;
13
13
  const fid = feed?.feed;
14
14
 
15
- const selector = useCallback((currentState: FeedsClientState) => {
16
- if (!fid) {
17
- return { feedOwnCapabilities: stableEmptyArray };
18
- }
15
+ const selector = useCallback(
16
+ (currentState: FeedsClientState) => {
17
+ if (!fid) {
18
+ return { feedOwnCapabilities: stableEmptyArray };
19
+ }
19
20
 
20
- return {
21
- feedOwnCapabilities:
22
- currentState.own_capabilities_by_fid[fid] ?? stableEmptyArray,
23
- };
24
- }, [fid]);
21
+ return {
22
+ feedOwnCapabilities:
23
+ currentState.own_capabilities_by_fid[fid] ?? stableEmptyArray,
24
+ };
25
+ },
26
+ [fid],
27
+ );
25
28
 
26
29
  const { feedOwnCapabilities = stableEmptyArray } =
27
30
  useStateStore(client?.state, selector) ?? {};
@@ -43,7 +43,6 @@ export const useCreateFeedsClient = ({
43
43
  .connectUser(cachedUserData, tokenOrProvider)
44
44
  .then(() => {
45
45
  setError(null);
46
- console.log('Successfully connected user: ', cachedUserData.id);
47
46
  })
48
47
  .catch((err) => {
49
48
  setError(err);
@@ -60,11 +59,6 @@ export const useCreateFeedsClient = ({
60
59
  })
61
60
  .catch((err) => {
62
61
  setError(err);
63
- })
64
- .then(() => {
65
- console.log(
66
- `Connection for user "${cachedUserData.id}" has been closed`,
67
- );
68
62
  });
69
63
  };
70
64
  }, [apiKey, cachedUserData, cachedOptions, tokenOrProvider]);
@@ -43,7 +43,11 @@ export enum UnhandledErrorType {
43
43
  FetchingOwnCapabilitiesOnNewActivity = 'fetching-own-capabilities-on-new-activity',
44
44
  }
45
45
 
46
- export type SyncFailure = { feed: string; reason: unknown };
46
+ export type SyncFailure = {
47
+ feed: string;
48
+ reason: unknown;
49
+ activity_id?: string;
50
+ };
47
51
 
48
52
  export type UnhandledErrorEvent = {
49
53
  type: 'errors.unhandled';
package/src/feed/feed.ts CHANGED
@@ -59,7 +59,12 @@ import type {
59
59
  LoadingStates,
60
60
  PagerResponseWithLoadingStates,
61
61
  } from '../types';
62
- import { checkHasAnotherPage, Constants, uniqueArrayMerge } from '../utils';
62
+ import {
63
+ checkHasAnotherPage,
64
+ Constants,
65
+ feedsLoggerSystem,
66
+ uniqueArrayMerge,
67
+ } from '../utils';
63
68
  import { handleActivityFeedback } from './event-handlers/activity/handle-activity-feedback';
64
69
  import { deepEqual } from '../utils/deep-equal';
65
70
 
@@ -595,13 +600,15 @@ export class Feed extends FeedApi {
595
600
  }
596
601
 
597
602
  public async loadNextPageActivityComments(
598
- activity: ActivityResponse,
603
+ activity: ActivityResponse | string,
599
604
  request?: Partial<
600
605
  Omit<GetCommentsRequest, 'object_id' | 'object_type' | 'next'>
601
606
  >,
602
607
  ) {
603
608
  const currentEntityState =
604
- this.currentState.comments_by_entity_id[activity.id];
609
+ this.currentState.comments_by_entity_id[
610
+ typeof activity === 'string' ? activity : activity.id
611
+ ];
605
612
  const currentPagination = currentEntityState?.pagination;
606
613
  const currentNextCursor = currentPagination?.next;
607
614
  const currentSort = currentPagination?.sort;
@@ -617,13 +624,14 @@ export class Feed extends FeedApi {
617
624
  return;
618
625
  }
619
626
 
627
+ const entityId = typeof activity === 'string' ? activity : activity.id;
620
628
  await this.loadNextPageComments({
621
- entityId: activity.id,
629
+ entityId: entityId,
622
630
  base: () =>
623
631
  this.client.getComments({
624
632
  ...request,
625
633
  sort,
626
- object_id: activity.id,
634
+ object_id: entityId,
627
635
  object_type: 'activity',
628
636
  next: currentNextCursor,
629
637
  }),
@@ -929,7 +937,9 @@ export class Feed extends FeedApi {
929
937
  }
930
938
 
931
939
  if (typeof eventHandler === 'undefined') {
932
- console.warn(`Received unknown event type: ${event.type}`, event);
940
+ feedsLoggerSystem
941
+ .getLogger('event-dispatcher')
942
+ .warn(`Received unknown feed event, type: ${event.type}`, event);
933
943
  }
934
944
 
935
945
  this.eventDispatcher.dispatch(event);
@@ -0,0 +1,42 @@
1
+ import { Feed } from '../feed';
2
+ import type { FeedsClient } from './feeds-client';
3
+
4
+ export function connectActivityToFeed(
5
+ this: FeedsClient,
6
+ {
7
+ fid,
8
+ }: {
9
+ fid: string;
10
+ },
11
+ ) {
12
+ const [group, id] = fid.split(':');
13
+ const activeFeed = this.activeFeeds[fid];
14
+
15
+ const feed = new Feed(
16
+ this,
17
+ group,
18
+ id,
19
+ undefined,
20
+ activeFeed?.currentState.watch,
21
+ );
22
+
23
+ return feed;
24
+ }
25
+
26
+ export function isAnyFeedWatched(this: FeedsClient, fids: string[]) {
27
+ for (const fid of fids) {
28
+ const feed = this.activeFeeds[fid];
29
+ if (feed && feed.currentState.last_get_or_create_request_config?.watch) {
30
+ return true;
31
+ }
32
+ }
33
+
34
+ return false;
35
+ }
36
+
37
+ export function disconnectActivityFromFeed(this: FeedsClient, id: string) {
38
+ const activeFeed = this.activeActivities[id];
39
+ if (activeFeed) {
40
+ delete this.activeActivities[id];
41
+ }
42
+ }