@stream-io/feeds-client 0.1.8 → 0.1.10

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 (59) hide show
  1. package/@react-bindings/hooks/search-state-hooks/index.ts +3 -0
  2. package/@react-bindings/hooks/util/index.ts +1 -0
  3. package/@react-bindings/index.ts +5 -0
  4. package/CHANGELOG.md +22 -0
  5. package/dist/@react-bindings/contexts/StreamSearchContext.d.ts +12 -0
  6. package/dist/@react-bindings/contexts/StreamSearchResultsContext.d.ts +12 -0
  7. package/dist/@react-bindings/hooks/search-state-hooks/index.d.ts +3 -0
  8. package/dist/@react-bindings/hooks/search-state-hooks/useSearchQuery.d.ts +4 -0
  9. package/dist/@react-bindings/hooks/search-state-hooks/useSearchResult.d.ts +8 -0
  10. package/dist/@react-bindings/hooks/search-state-hooks/useSearchSources.d.ts +4 -0
  11. package/dist/@react-bindings/hooks/util/index.d.ts +1 -0
  12. package/dist/@react-bindings/hooks/util/useBookmarkActions.d.ts +13 -0
  13. package/dist/@react-bindings/hooks/util/useReactionActions.d.ts +1 -1
  14. package/dist/@react-bindings/index.d.ts +5 -0
  15. package/dist/@react-bindings/wrappers/StreamSearch.d.ts +12 -0
  16. package/dist/@react-bindings/wrappers/StreamSearchResults.d.ts +12 -0
  17. package/dist/index-react-bindings.browser.cjs +431 -156
  18. package/dist/index-react-bindings.browser.cjs.map +1 -1
  19. package/dist/index-react-bindings.browser.js +422 -157
  20. package/dist/index-react-bindings.browser.js.map +1 -1
  21. package/dist/index-react-bindings.node.cjs +431 -156
  22. package/dist/index-react-bindings.node.cjs.map +1 -1
  23. package/dist/index-react-bindings.node.js +422 -157
  24. package/dist/index-react-bindings.node.js.map +1 -1
  25. package/dist/index.browser.cjs +346 -264
  26. package/dist/index.browser.cjs.map +1 -1
  27. package/dist/index.browser.js +346 -265
  28. package/dist/index.browser.js.map +1 -1
  29. package/dist/index.node.cjs +346 -264
  30. package/dist/index.node.cjs.map +1 -1
  31. package/dist/index.node.js +346 -265
  32. package/dist/index.node.js.map +1 -1
  33. package/dist/src/Feed.d.ts +40 -9
  34. package/dist/src/FeedsClient.d.ts +8 -1
  35. package/dist/src/common/BaseSearchSource.d.ts +3 -1
  36. package/dist/src/common/FeedSearchSource.d.ts +5 -1
  37. package/dist/src/common/SearchController.d.ts +2 -0
  38. package/dist/src/gen-imports.d.ts +1 -1
  39. package/dist/src/state-updates/follow-utils.d.ts +19 -0
  40. package/dist/src/state-updates/state-update-queue.d.ts +15 -0
  41. package/dist/src/utils.d.ts +1 -0
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/Feed.ts +226 -192
  45. package/src/FeedsClient.ts +75 -3
  46. package/src/common/ActivitySearchSource.ts +5 -15
  47. package/src/common/BaseSearchSource.ts +9 -9
  48. package/src/common/FeedSearchSource.ts +20 -65
  49. package/src/common/SearchController.ts +2 -0
  50. package/src/common/UserSearchSource.ts +9 -61
  51. package/src/gen-imports.ts +1 -1
  52. package/src/state-updates/activity-reaction-utils.test.ts +1 -0
  53. package/src/state-updates/activity-utils.test.ts +1 -0
  54. package/src/state-updates/follow-utils.test.ts +552 -0
  55. package/src/state-updates/follow-utils.ts +126 -0
  56. package/src/state-updates/state-update-queue.test.ts +53 -0
  57. package/src/state-updates/state-update-queue.ts +35 -0
  58. package/src/utils.test.ts +175 -0
  59. package/src/utils.ts +20 -0
@@ -3,12 +3,14 @@ import {
3
3
  ActivityResponse,
4
4
  FeedResponse,
5
5
  FileUploadRequest,
6
+ FollowBatchRequest,
6
7
  ImageUploadRequest,
7
8
  OwnUser,
8
9
  PollResponse,
9
10
  PollVotesResponse,
10
11
  QueryFeedsRequest,
11
12
  QueryPollVotesRequest,
13
+ SingleFollowRequest,
12
14
  UserRequest,
13
15
  WSEvent,
14
16
  } from './gen/models';
@@ -97,6 +99,10 @@ export class FeedsClient extends FeedsApi {
97
99
  activeFeed.synchronize();
98
100
  }
99
101
  }
102
+ } else {
103
+ for (const activeFeed of Object.values(this.activeFeeds)) {
104
+ activeFeed.handleWatchStopped();
105
+ }
100
106
  }
101
107
  break;
102
108
  }
@@ -335,7 +341,7 @@ export class FeedsClient extends FeedsApi {
335
341
  const response = await this.feedsQueryFeeds(request);
336
342
 
337
343
  const feeds = response.feeds.map((f) =>
338
- this.getOrCreateActiveFeed(f.group_id, f.id, f),
344
+ this.getOrCreateActiveFeed(f.group_id, f.id, f, request?.watch),
339
345
  );
340
346
 
341
347
  return {
@@ -357,16 +363,82 @@ export class FeedsClient extends FeedsApi {
357
363
  this.eventDispatcher.dispatch(networkEvent);
358
364
  };
359
365
 
366
+ // For follow API endpoints we update the state after HTTP response to allow queryFeeds with watch: false
367
+ async follow(request: SingleFollowRequest) {
368
+ const response = await super.follow(request);
369
+
370
+ [response.follow.source_feed.fid, response.follow.target_feed.fid].forEach(
371
+ (fid) => {
372
+ const feed = this.activeFeeds[fid];
373
+ if (feed) {
374
+ feed.handleFollowCreated(response.follow);
375
+ }
376
+ },
377
+ );
378
+
379
+ return response;
380
+ }
381
+
382
+ async followBatch(request: FollowBatchRequest) {
383
+ const response = await super.followBatch(request);
384
+
385
+ response.follows.forEach((follow) => {
386
+ const feed = this.activeFeeds[follow.source_feed.fid];
387
+ if (feed) {
388
+ feed.handleFollowCreated(follow);
389
+ }
390
+ });
391
+
392
+ return response;
393
+ }
394
+
395
+ async unfollow(request: SingleFollowRequest) {
396
+ const response = await super.unfollow(request);
397
+
398
+ [request.source, request.target].forEach((fid) => {
399
+ const feed = this.activeFeeds[fid];
400
+ if (feed) {
401
+ feed.handleFollowDeleted({
402
+ source_feed: { fid: request.source },
403
+ target_feed: { fid: request.target },
404
+ });
405
+ }
406
+ });
407
+
408
+ return response;
409
+ }
410
+
411
+ async stopWatchingFeed(request: { feed_group_id: string; feed_id: string }) {
412
+ const connectionId = await this.connectionIdManager.getConnectionId();
413
+ const response = await super.stopWatchingFeed({
414
+ ...request,
415
+ connection_id: connectionId,
416
+ });
417
+
418
+ const feed =
419
+ this.activeFeeds[`${request.feed_group_id}:${request.feed_id}`];
420
+ if (feed) {
421
+ feed.handleWatchStopped();
422
+ }
423
+
424
+ return response;
425
+ }
426
+
360
427
  private readonly getOrCreateActiveFeed = (
361
428
  group: string,
362
429
  id: string,
363
430
  data?: FeedResponse,
431
+ watch?: boolean,
364
432
  ) => {
365
433
  const fid = `${group}:${id}`;
366
434
  if (this.activeFeeds[fid]) {
367
- return this.activeFeeds[fid];
435
+ const feed = this.activeFeeds[fid];
436
+ if (watch && !feed.currentState.watch) {
437
+ feed.handleWatchStarted();
438
+ }
439
+ return feed;
368
440
  } else {
369
- const feed = new Feed(this, group, id, data);
441
+ const feed = new Feed(this, group, id, data, watch);
370
442
  this.activeFeeds[fid] = feed;
371
443
  return feed;
372
444
  }
@@ -7,12 +7,6 @@ import { ActivityResponse } from '../gen/models';
7
7
  export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
8
8
  readonly type = 'activity' as const;
9
9
  private readonly client: FeedsClient;
10
- // messageSearchChannelFilters: ChannelFilters | undefined;
11
- // messageSearchFilters: MessageFilters | undefined;
12
- // messageSearchSort: SearchMessageSort | undefined;
13
- // channelQueryFilters: ChannelFilters | undefined;
14
- // channelQuerySort: ChannelSort | undefined;
15
- // channelQueryOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
16
10
 
17
11
  constructor(client: FeedsClient, options?: SearchSourceOptions) {
18
12
  super(options);
@@ -20,12 +14,15 @@ export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
20
14
  }
21
15
 
22
16
  protected async query(searchQuery: string) {
23
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
17
+ const { connected_user: connectedUser } =
18
+ this.client.state.getLatestValue();
24
19
  if (!connectedUser) return { items: [] };
25
20
 
26
21
  const { activities: items, next } = await this.client.queryActivities({
27
22
  sort: [{ direction: -1, field: 'created_at' }],
28
- filter: { text: { $autocomplete: searchQuery } },
23
+ ...(!this.allowEmptySearchString || searchQuery.length > 0
24
+ ? { filter: { text: { $autocomplete: searchQuery } } }
25
+ : {}),
29
26
  limit: 10,
30
27
  next: this.next ?? undefined,
31
28
  });
@@ -37,10 +34,3 @@ export class ActivitySearchSource extends BaseSearchSource<ActivityResponse> {
37
34
  return items;
38
35
  }
39
36
  }
40
-
41
-
42
- // filter: {
43
- // 'feed.name': { $autocomplete: searchQuery }
44
- // 'feed.description': { $autocomplete: searchQuery }
45
- // 'created_by.name': { $autocomplete: searchQuery }
46
- // },
@@ -1,12 +1,10 @@
1
1
  import { StateStore } from './StateStore';
2
2
  import { debounce, type DebouncedFunc } from './utils';
3
3
 
4
- export type SearchSourceType =
5
- | 'channels'
6
- | 'users'
7
- | 'messages'
8
- | (string & {});
4
+ export type SearchSourceType = 'activity' | 'user' | 'feed' | (string & {});
5
+
9
6
  export type QueryReturnValue<T> = { items: T[]; next?: string | null };
7
+
10
8
  export type DebounceOptions = {
11
9
  debounceMs: number;
12
10
  };
@@ -14,7 +12,6 @@ type DebouncedExecQueryFunction = DebouncedFunc<
14
12
  (searchString?: string) => Promise<void>
15
13
  >;
16
14
 
17
-
18
15
  export interface SearchSource<T = any> {
19
16
  activate(): void;
20
17
 
@@ -46,7 +43,6 @@ export interface SearchSource<T = any> {
46
43
  readonly type: SearchSourceType;
47
44
  }
48
45
 
49
-
50
46
  export type SearchSourceState<T = any> = {
51
47
  hasNext: boolean;
52
48
  isActive: boolean;
@@ -61,24 +57,28 @@ export type SearchSourceOptions = {
61
57
  /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
62
58
  debounceMs?: number;
63
59
  pageSize?: number;
60
+ allowEmptySearchString?: boolean;
64
61
  };
65
62
  const DEFAULT_SEARCH_SOURCE_OPTIONS: Required<SearchSourceOptions> = {
66
63
  debounceMs: 300,
67
64
  pageSize: 10,
65
+ allowEmptySearchString: false,
68
66
  } as const;
69
67
 
70
68
  export abstract class BaseSearchSource<T> implements SearchSource<T> {
71
69
  state: StateStore<SearchSourceState<T>>;
72
70
  protected pageSize: number;
71
+ protected allowEmptySearchString: boolean;
73
72
  abstract readonly type: SearchSourceType;
74
73
  protected searchDebounced!: DebouncedExecQueryFunction;
75
74
 
76
75
  protected constructor(options?: SearchSourceOptions) {
77
- const { debounceMs, pageSize } = {
76
+ const { debounceMs, pageSize, allowEmptySearchString } = {
78
77
  ...DEFAULT_SEARCH_SOURCE_OPTIONS,
79
78
  ...options,
80
79
  };
81
80
  this.pageSize = pageSize;
81
+ this.allowEmptySearchString = allowEmptySearchString;
82
82
  this.state = new StateStore<SearchSourceState<T>>(this.initialState);
83
83
  this.setDebounceOptions({ debounceMs });
84
84
  }
@@ -157,7 +157,7 @@ export abstract class BaseSearchSource<T> implements SearchSource<T> {
157
157
  this.isActive &&
158
158
  !this.isLoading &&
159
159
  (this.hasNext || hasNewSearchQuery) &&
160
- searchString
160
+ (this.allowEmptySearchString || searchString)
161
161
  );
162
162
  };
163
163
 
@@ -4,85 +4,40 @@ import type { SearchSourceOptions } from './BaseSearchSource';
4
4
  import { FeedsClient } from '../FeedsClient';
5
5
  import { Feed } from '../Feed';
6
6
 
7
+ export type FeedSearchSourceOptions = SearchSourceOptions & {
8
+ groupId?: string;
9
+ };
10
+
7
11
  export class FeedSearchSource extends BaseSearchSource<Feed> {
8
12
  readonly type = 'feed' as const;
9
13
  private readonly client: FeedsClient;
10
- // messageSearchChannelFilters: ChannelFilters | undefined;
11
- // messageSearchFilters: MessageFilters | undefined;
12
- // messageSearchSort: SearchMessageSort | undefined;
13
- // channelQueryFilters: ChannelFilters | undefined;
14
- // channelQuerySort: ChannelSort | undefined;
15
- // channelQueryOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
14
+ private readonly feedGroupId?: string | undefined;
16
15
 
17
- constructor(client: FeedsClient, options?: SearchSourceOptions) {
16
+ constructor(client: FeedsClient, options?: FeedSearchSourceOptions) {
18
17
  super(options);
19
18
  this.client = client;
19
+ this.feedGroupId = options?.groupId;
20
20
  }
21
21
 
22
22
  protected async query(searchQuery: string) {
23
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
23
+ const { connected_user: connectedUser } =
24
+ this.client.state.getLatestValue();
24
25
  if (!connectedUser) return { items: [] };
25
26
 
26
- // const channelFilters: ChannelFilters = {
27
- // members: { $in: [this.client.userID] },
28
- // ...this.messageSearchChannelFilters,
29
- // } as ChannelFilters;
30
-
31
- // const messageFilters: MessageFilters = {
32
- // text: searchQuery,
33
- // type: 'regular', // FIXME: type: 'reply' resp. do not filter by type and allow to jump to a message in a thread - missing support
34
- // ...this.messageSearchFilters,
35
- // } as MessageFilters;
36
-
37
- // const sort: SearchMessageSort = {
38
- // created_at: -1,
39
- // ...this.messageSearchSort,
40
- // };
41
-
42
- // const options = {
43
- // limit: this.pageSize,
44
- // next: this.next,
45
- // sort,
46
- // } as SearchOptions;
47
-
48
- // const { next, results } = await this.client.search(
49
- // channelFilters,
50
- // messageFilters,
51
- // options,
52
- // );
53
- // const items = results.map(({ message }) => message);
54
-
55
- // const cids = Array.from(
56
- // items.reduce((acc, message) => {
57
- // if (message.cid && !this.client.activeChannels[message.cid])
58
- // acc.add(message.cid);
59
- // return acc;
60
- // }, new Set<string>()), // keep the cids unique
61
- // );
62
- // const allChannelsLoadedLocally = cids.length === 0;
63
- // if (!allChannelsLoadedLocally) {
64
- // await this.client.queryChannels(
65
- // {
66
- // cid: { $in: cids },
67
- // ...this.channelQueryFilters,
68
- // } as ChannelFilters,
69
- // {
70
- // last_message_at: -1,
71
- // ...this.channelQuerySort,
72
- // },
73
- // this.channelQueryOptions,
74
- // );
75
- // }
76
-
77
27
  const { feeds: items, next } = await this.client.queryFeeds({
78
28
  filter: {
79
- group_id: 'user',
80
- $or: [
81
- { name: { $autocomplete: searchQuery } },
82
- { description: { $autocomplete: searchQuery } },
83
- { 'created_by.name': { $autocomplete: searchQuery } },
84
- ],
29
+ ...(this.feedGroupId ? { group_id: this.feedGroupId } : {}),
30
+ ...(!this.allowEmptySearchString || searchQuery.length > 0
31
+ ? {
32
+ $or: [
33
+ { name: { $autocomplete: searchQuery } },
34
+ { description: { $autocomplete: searchQuery } },
35
+ { 'created_by.name': { $autocomplete: searchQuery } },
36
+ ],
37
+ }
38
+ : {}),
85
39
  },
40
+ next: this.next ?? undefined,
86
41
  });
87
42
 
88
43
  return { items, next };
@@ -23,6 +23,8 @@ export class SearchController {
23
23
  /**
24
24
  * Not intended for direct use by integrators, might be removed without notice resulting in
25
25
  * broken integrations.
26
+ *
27
+ * @internal
26
28
  */
27
29
  _internalState: StateStore<InternalSearchControllerState>;
28
30
  state: StateStore<SearchControllerState>;
@@ -7,12 +7,6 @@ import { UserResponse } from '../gen/models';
7
7
  export class UserSearchSource extends BaseSearchSource<UserResponse> {
8
8
  readonly type = 'user' as const;
9
9
  private readonly client: FeedsClient;
10
- // messageSearchChannelFilters: ChannelFilters | undefined;
11
- // messageSearchFilters: MessageFilters | undefined;
12
- // messageSearchSort: SearchMessageSort | undefined;
13
- // channelQueryFilters: ChannelFilters | undefined;
14
- // channelQuerySort: ChannelSort | undefined;
15
- // channelQueryOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
16
10
 
17
11
  constructor(client: FeedsClient, options?: SearchSourceOptions) {
18
12
  super(options);
@@ -20,66 +14,20 @@ export class UserSearchSource extends BaseSearchSource<UserResponse> {
20
14
  }
21
15
 
22
16
  protected async query(searchQuery: string) {
23
- const { connected_user: connectedUser } = this.client.state.getLatestValue();
17
+ const { connected_user: connectedUser } =
18
+ this.client.state.getLatestValue();
24
19
  if (!connectedUser) return { items: [] };
25
20
 
26
- // const channelFilters: ChannelFilters = {
27
- // members: { $in: [this.client.userID] },
28
- // ...this.messageSearchChannelFilters,
29
- // } as ChannelFilters;
30
-
31
- // const messageFilters: MessageFilters = {
32
- // text: searchQuery,
33
- // type: 'regular', // FIXME: type: 'reply' resp. do not filter by type and allow to jump to a message in a thread - missing support
34
- // ...this.messageSearchFilters,
35
- // } as MessageFilters;
36
-
37
- // const sort: SearchMessageSort = {
38
- // created_at: -1,
39
- // ...this.messageSearchSort,
40
- // };
41
-
42
- // const options = {
43
- // limit: this.pageSize,
44
- // next: this.next,
45
- // sort,
46
- // } as SearchOptions;
47
-
48
- // const { next, results } = await this.client.search(
49
- // channelFilters,
50
- // messageFilters,
51
- // options,
52
- // );
53
- // const items = results.map(({ message }) => message);
54
-
55
- // const cids = Array.from(
56
- // items.reduce((acc, message) => {
57
- // if (message.cid && !this.client.activeChannels[message.cid])
58
- // acc.add(message.cid);
59
- // return acc;
60
- // }, new Set<string>()), // keep the cids unique
61
- // );
62
- // const allChannelsLoadedLocally = cids.length === 0;
63
- // if (!allChannelsLoadedLocally) {
64
- // await this.client.queryChannels(
65
- // {
66
- // cid: { $in: cids },
67
- // ...this.channelQueryFilters,
68
- // } as ChannelFilters,
69
- // {
70
- // last_message_at: -1,
71
- // ...this.channelQuerySort,
72
- // },
73
- // this.channelQueryOptions,
74
- // );
75
- // }
76
-
77
21
  const { users: items } = await this.client.queryUsers({
78
22
  payload: {
79
23
  filter_conditions: {
80
- name: {
81
- $autocomplete: searchQuery,
82
- },
24
+ ...(!this.allowEmptySearchString || searchQuery.length > 0
25
+ ? {
26
+ name: {
27
+ $autocomplete: searchQuery,
28
+ },
29
+ }
30
+ : {}),
83
31
  },
84
32
  },
85
33
  });
@@ -1,3 +1,3 @@
1
1
  export type { ApiClient } from './common/ApiClient';
2
2
  export type { StreamResponse } from './common/types';
3
- export { FeedsApi } from './gen/feeds/FeedsApi';
3
+ export { FeedsClient as FeedsApi } from './FeedsClient';
@@ -35,6 +35,7 @@ const createMockActivity = (id: string): ActivityResponse => ({
35
35
  search_data: {},
36
36
  popularity: 0,
37
37
  score: 0,
38
+ reaction_count: 0,
38
39
  user: {
39
40
  id: 'user1',
40
41
  created_at: new Date(),
@@ -31,6 +31,7 @@ const createMockActivity = (id: string, text?: string): ActivityResponse =>
31
31
  text: text,
32
32
  popularity: 0,
33
33
  score: 0,
34
+ reaction_count: 0,
34
35
  user: {
35
36
  id: 'user1',
36
37
  created_at: new Date(),