stream-chat 9.3.0 → 9.5.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 (53) hide show
  1. package/dist/cjs/index.browser.cjs +843 -146
  2. package/dist/cjs/index.browser.cjs.map +4 -4
  3. package/dist/cjs/index.node.cjs +859 -152
  4. package/dist/cjs/index.node.cjs.map +4 -4
  5. package/dist/esm/index.js +846 -149
  6. package/dist/esm/index.js.map +4 -4
  7. package/dist/types/client.d.ts +52 -19
  8. package/dist/types/events.d.ts +4 -0
  9. package/dist/types/index.d.ts +3 -1
  10. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -2
  11. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +2 -2
  12. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +1 -1
  13. package/dist/types/pagination/BasePaginator.d.ts +69 -0
  14. package/dist/types/pagination/ReminderPaginator.d.ts +12 -0
  15. package/dist/types/pagination/index.d.ts +2 -0
  16. package/dist/types/reminders/Reminder.d.ts +37 -0
  17. package/dist/types/reminders/ReminderManager.d.ts +65 -0
  18. package/dist/types/reminders/ReminderTimer.d.ts +17 -0
  19. package/dist/types/reminders/index.d.ts +3 -0
  20. package/dist/types/search/BaseSearchSource.d.ts +87 -0
  21. package/dist/types/search/ChannelSearchSource.d.ts +17 -0
  22. package/dist/types/search/MessageSearchSource.d.ts +23 -0
  23. package/dist/types/search/SearchController.d.ts +44 -0
  24. package/dist/types/search/UserSearchSource.d.ts +16 -0
  25. package/dist/types/search/index.d.ts +5 -0
  26. package/dist/types/store.d.ts +114 -5
  27. package/dist/types/types.d.ts +48 -3
  28. package/package.json +4 -4
  29. package/src/channel.ts +2 -1
  30. package/src/client.ts +108 -39
  31. package/src/events.ts +6 -0
  32. package/src/index.ts +3 -1
  33. package/src/messageComposer/middleware/textComposer/commands.ts +2 -2
  34. package/src/messageComposer/middleware/textComposer/mentions.ts +2 -2
  35. package/src/messageComposer/middleware/textComposer/types.ts +1 -1
  36. package/src/pagination/BasePaginator.ts +184 -0
  37. package/src/pagination/ReminderPaginator.ts +38 -0
  38. package/src/pagination/index.ts +2 -0
  39. package/src/reminders/Reminder.ts +89 -0
  40. package/src/reminders/ReminderManager.ts +284 -0
  41. package/src/reminders/ReminderTimer.ts +86 -0
  42. package/src/reminders/index.ts +3 -0
  43. package/src/search/BaseSearchSource.ts +227 -0
  44. package/src/search/ChannelSearchSource.ts +34 -0
  45. package/src/search/MessageSearchSource.ts +88 -0
  46. package/src/search/SearchController.ts +154 -0
  47. package/src/search/UserSearchSource.ts +35 -0
  48. package/src/search/index.ts +5 -0
  49. package/src/store.ts +237 -11
  50. package/src/token_manager.ts +3 -1
  51. package/src/types.ts +91 -1
  52. package/dist/types/search_controller.d.ts +0 -174
  53. package/src/search_controller.ts +0 -523
@@ -0,0 +1,227 @@
1
+ import { StateStore } from '../store';
2
+ import { debounce, type DebouncedFunc } from '../utils';
3
+
4
+ export type SearchSourceType = 'channels' | 'users' | 'messages' | (string & {});
5
+ export type QueryReturnValue<T> = { items: T[]; next?: string | null };
6
+ export type DebounceOptions = {
7
+ debounceMs: number;
8
+ };
9
+ type DebouncedExecQueryFunction = DebouncedFunc<(searchString?: string) => Promise<void>>;
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ export interface SearchSource<T = any> {
13
+ activate(): void;
14
+
15
+ cancelScheduledQuery(): void;
16
+
17
+ canExecuteQuery(newSearchString?: string): boolean;
18
+
19
+ deactivate(): void;
20
+
21
+ readonly hasNext: boolean;
22
+ readonly hasResults: boolean;
23
+ readonly initialState: SearchSourceState<T>;
24
+ readonly isActive: boolean;
25
+ readonly isLoading: boolean;
26
+ readonly items: T[] | undefined;
27
+ readonly lastQueryError: Error | undefined;
28
+ readonly next: string | undefined | null;
29
+ readonly offset: number | undefined;
30
+
31
+ resetState(): void;
32
+
33
+ search(text?: string): Promise<void> | undefined;
34
+
35
+ readonly searchQuery: string;
36
+
37
+ setDebounceOptions(options: DebounceOptions): void;
38
+
39
+ readonly state: StateStore<SearchSourceState<T>>;
40
+ readonly type: SearchSourceType;
41
+ }
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ export type SearchSourceState<T = any> = {
45
+ hasNext: boolean;
46
+ isActive: boolean;
47
+ isLoading: boolean;
48
+ items: T[] | undefined;
49
+ searchQuery: string;
50
+ lastQueryError?: Error;
51
+ next?: string | null;
52
+ offset?: number;
53
+ };
54
+ export type SearchSourceOptions = {
55
+ /** The number of milliseconds to debounce the search query. The default interval is 300ms. */
56
+ debounceMs?: number;
57
+ pageSize?: number;
58
+ };
59
+ const DEFAULT_SEARCH_SOURCE_OPTIONS: Required<SearchSourceOptions> = {
60
+ debounceMs: 300,
61
+ pageSize: 10,
62
+ } as const;
63
+
64
+ export abstract class BaseSearchSource<T> implements SearchSource<T> {
65
+ state: StateStore<SearchSourceState<T>>;
66
+ protected pageSize: number;
67
+ abstract readonly type: SearchSourceType;
68
+ protected searchDebounced!: DebouncedExecQueryFunction;
69
+
70
+ protected constructor(options?: SearchSourceOptions) {
71
+ const { debounceMs, pageSize } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
72
+ this.pageSize = pageSize;
73
+ this.state = new StateStore<SearchSourceState<T>>(this.initialState);
74
+ this.setDebounceOptions({ debounceMs });
75
+ }
76
+
77
+ get lastQueryError() {
78
+ return this.state.getLatestValue().lastQueryError;
79
+ }
80
+
81
+ get hasNext() {
82
+ return this.state.getLatestValue().hasNext;
83
+ }
84
+
85
+ get hasResults() {
86
+ return Array.isArray(this.state.getLatestValue().items);
87
+ }
88
+
89
+ get isActive() {
90
+ return this.state.getLatestValue().isActive;
91
+ }
92
+
93
+ get isLoading() {
94
+ return this.state.getLatestValue().isLoading;
95
+ }
96
+
97
+ get initialState() {
98
+ return {
99
+ hasNext: true,
100
+ isActive: false,
101
+ isLoading: false,
102
+ items: undefined,
103
+ lastQueryError: undefined,
104
+ next: undefined,
105
+ offset: 0,
106
+ searchQuery: '',
107
+ };
108
+ }
109
+
110
+ get items() {
111
+ return this.state.getLatestValue().items;
112
+ }
113
+
114
+ get next() {
115
+ return this.state.getLatestValue().next;
116
+ }
117
+
118
+ get offset() {
119
+ return this.state.getLatestValue().offset;
120
+ }
121
+
122
+ get searchQuery() {
123
+ return this.state.getLatestValue().searchQuery;
124
+ }
125
+
126
+ protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
127
+
128
+ protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
129
+
130
+ setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
131
+ this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
132
+ };
133
+
134
+ activate = () => {
135
+ if (this.isActive) return;
136
+ this.state.partialNext({ isActive: true });
137
+ };
138
+
139
+ deactivate = () => {
140
+ if (!this.isActive) return;
141
+ this.state.partialNext({ isActive: false });
142
+ };
143
+
144
+ canExecuteQuery = (newSearchString?: string) => {
145
+ const hasNewSearchQuery = typeof newSearchString !== 'undefined';
146
+ const searchString = newSearchString ?? this.searchQuery;
147
+ return !!(
148
+ this.isActive &&
149
+ !this.isLoading &&
150
+ (this.hasNext || hasNewSearchQuery) &&
151
+ searchString
152
+ );
153
+ };
154
+
155
+ protected getStateBeforeFirstQuery(newSearchString: string): SearchSourceState<T> {
156
+ return {
157
+ ...this.initialState,
158
+ isActive: this.isActive,
159
+ isLoading: true,
160
+ searchQuery: newSearchString,
161
+ };
162
+ }
163
+
164
+ protected getStateAfterQuery(
165
+ stateUpdate: Partial<SearchSourceState<T>>,
166
+ isFirstPage: boolean,
167
+ ): SearchSourceState<T> {
168
+ const current = this.state.getLatestValue();
169
+ return {
170
+ ...current,
171
+ lastQueryError: undefined, // reset lastQueryError that can be overridden by the stateUpdate
172
+ ...stateUpdate,
173
+ isLoading: false,
174
+ items: isFirstPage
175
+ ? stateUpdate.items
176
+ : [...(this.items ?? []), ...(stateUpdate.items || [])],
177
+ };
178
+ }
179
+
180
+ async executeQuery(newSearchString?: string) {
181
+ if (!this.canExecuteQuery(newSearchString)) return;
182
+ const hasNewSearchQuery = typeof newSearchString !== 'undefined';
183
+ const searchString = newSearchString ?? this.searchQuery;
184
+
185
+ if (hasNewSearchQuery) {
186
+ this.state.next(this.getStateBeforeFirstQuery(newSearchString ?? ''));
187
+ } else {
188
+ this.state.partialNext({ isLoading: true });
189
+ }
190
+
191
+ const stateUpdate: Partial<SearchSourceState<T>> = {};
192
+ try {
193
+ const results = await this.query(searchString);
194
+ if (!results) return;
195
+ const { items, next } = results;
196
+
197
+ if (next || next === null) {
198
+ stateUpdate.next = next;
199
+ stateUpdate.hasNext = !!next;
200
+ } else {
201
+ stateUpdate.offset = (this.offset ?? 0) + items.length;
202
+ stateUpdate.hasNext = items.length === this.pageSize;
203
+ }
204
+
205
+ stateUpdate.items = await this.filterQueryResults(items);
206
+ } catch (e) {
207
+ stateUpdate.lastQueryError = e as Error;
208
+ } finally {
209
+ this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
210
+ }
211
+ }
212
+
213
+ search = (searchQuery?: string) => this.searchDebounced(searchQuery);
214
+
215
+ cancelScheduledQuery() {
216
+ this.searchDebounced.cancel();
217
+ }
218
+
219
+ resetState() {
220
+ this.state.next(this.initialState);
221
+ }
222
+
223
+ resetStateAndActivate() {
224
+ this.resetState();
225
+ this.activate();
226
+ }
227
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseSearchSource } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './BaseSearchSource';
3
+ import type { Channel } from '../channel';
4
+ import type { StreamChat } from '../client';
5
+ import type { ChannelFilters, ChannelOptions, ChannelSort } from '../types';
6
+
7
+ export class ChannelSearchSource extends BaseSearchSource<Channel> {
8
+ readonly type = 'channels';
9
+ private client: StreamChat;
10
+ filters: ChannelFilters | undefined;
11
+ sort: ChannelSort | undefined;
12
+ searchOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
13
+
14
+ constructor(client: StreamChat, options?: SearchSourceOptions) {
15
+ super(options);
16
+ this.client = client;
17
+ }
18
+
19
+ protected async query(searchQuery: string) {
20
+ const filters = {
21
+ members: { $in: [this.client.userID] },
22
+ name: { $autocomplete: searchQuery },
23
+ ...this.filters,
24
+ } as ChannelFilters;
25
+ const sort = this.sort ?? {};
26
+ const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
27
+ const items = await this.client.queryChannels(filters, sort, options);
28
+ return { items };
29
+ }
30
+
31
+ protected filterQueryResults(items: Channel[]) {
32
+ return items;
33
+ }
34
+ }
@@ -0,0 +1,88 @@
1
+ import { BaseSearchSource } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './BaseSearchSource';
3
+ import type {
4
+ ChannelFilters,
5
+ ChannelOptions,
6
+ ChannelSort,
7
+ MessageFilters,
8
+ MessageResponse,
9
+ SearchMessageSort,
10
+ SearchOptions,
11
+ } from '../types';
12
+ import type { StreamChat } from '../client';
13
+
14
+ export class MessageSearchSource extends BaseSearchSource<MessageResponse> {
15
+ readonly type = 'messages';
16
+ private client: StreamChat;
17
+ messageSearchChannelFilters: ChannelFilters | undefined;
18
+ messageSearchFilters: MessageFilters | undefined;
19
+ messageSearchSort: SearchMessageSort | undefined;
20
+ channelQueryFilters: ChannelFilters | undefined;
21
+ channelQuerySort: ChannelSort | undefined;
22
+ channelQueryOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
23
+
24
+ constructor(client: StreamChat, options?: SearchSourceOptions) {
25
+ super(options);
26
+ this.client = client;
27
+ }
28
+
29
+ protected async query(searchQuery: string) {
30
+ if (!this.client.userID) return { items: [] };
31
+
32
+ const channelFilters: ChannelFilters = {
33
+ members: { $in: [this.client.userID] },
34
+ ...this.messageSearchChannelFilters,
35
+ } as ChannelFilters;
36
+
37
+ const messageFilters: MessageFilters = {
38
+ text: searchQuery,
39
+ type: 'regular', // FIXME: type: 'reply' resp. do not filter by type and allow to jump to a message in a thread - missing support
40
+ ...this.messageSearchFilters,
41
+ } as MessageFilters;
42
+
43
+ const sort: SearchMessageSort = {
44
+ created_at: -1,
45
+ ...this.messageSearchSort,
46
+ };
47
+
48
+ const options = {
49
+ limit: this.pageSize,
50
+ next: this.next,
51
+ sort,
52
+ } as SearchOptions;
53
+
54
+ const { next, results } = await this.client.search(
55
+ channelFilters,
56
+ messageFilters,
57
+ options,
58
+ );
59
+ const items = results.map(({ message }) => message);
60
+
61
+ const cids = Array.from(
62
+ items.reduce((acc, message) => {
63
+ if (message.cid && !this.client.activeChannels[message.cid]) acc.add(message.cid);
64
+ return acc;
65
+ }, new Set<string>()), // keep the cids unique
66
+ );
67
+ const allChannelsLoadedLocally = cids.length === 0;
68
+ if (!allChannelsLoadedLocally) {
69
+ await this.client.queryChannels(
70
+ {
71
+ cid: { $in: cids },
72
+ ...this.channelQueryFilters,
73
+ } as ChannelFilters,
74
+ {
75
+ last_message_at: -1,
76
+ ...this.channelQuerySort,
77
+ },
78
+ this.channelQueryOptions,
79
+ );
80
+ }
81
+
82
+ return { items, next };
83
+ }
84
+
85
+ protected filterQueryResults(items: MessageResponse[]) {
86
+ return items;
87
+ }
88
+ }
@@ -0,0 +1,154 @@
1
+ import { StateStore } from '../store';
2
+ import type { MessageResponse } from '../types';
3
+ import type { SearchSource } from './BaseSearchSource';
4
+
5
+ export type SearchControllerState = {
6
+ isActive: boolean;
7
+ searchQuery: string;
8
+ sources: SearchSource[];
9
+ };
10
+
11
+ export type InternalSearchControllerState = {
12
+ // FIXME: focusedMessage should live in a MessageListController class that does not exist yet.
13
+ // This state prop should be then removed
14
+ focusedMessage?: MessageResponse;
15
+ };
16
+
17
+ export type SearchControllerConfig = {
18
+ // The controller will make sure there is always exactly one active source. Enabled by default.
19
+ keepSingleActiveSource: boolean;
20
+ };
21
+
22
+ export type SearchControllerOptions = {
23
+ config?: Partial<SearchControllerConfig>;
24
+ sources?: SearchSource[];
25
+ };
26
+
27
+ export class SearchController {
28
+ /**
29
+ * Not intended for direct use by integrators, might be removed without notice resulting in
30
+ * broken integrations.
31
+ */
32
+ _internalState: StateStore<InternalSearchControllerState>;
33
+ state: StateStore<SearchControllerState>;
34
+ config: SearchControllerConfig;
35
+
36
+ constructor({ config, sources }: SearchControllerOptions = {}) {
37
+ this.state = new StateStore<SearchControllerState>({
38
+ isActive: false,
39
+ searchQuery: '',
40
+ sources: sources ?? [],
41
+ });
42
+ this._internalState = new StateStore<InternalSearchControllerState>({});
43
+ this.config = { keepSingleActiveSource: true, ...config };
44
+ }
45
+ get hasNext() {
46
+ return this.sources.some((source) => source.hasNext);
47
+ }
48
+
49
+ get sources() {
50
+ return this.state.getLatestValue().sources;
51
+ }
52
+
53
+ get activeSources() {
54
+ return this.state.getLatestValue().sources.filter((s) => s.isActive);
55
+ }
56
+
57
+ get isActive() {
58
+ return this.state.getLatestValue().isActive;
59
+ }
60
+
61
+ get searchQuery() {
62
+ return this.state.getLatestValue().searchQuery;
63
+ }
64
+
65
+ get searchSourceTypes(): Array<SearchSource['type']> {
66
+ return this.sources.map((s) => s.type);
67
+ }
68
+
69
+ addSource = (source: SearchSource) => {
70
+ this.state.partialNext({
71
+ sources: [...this.sources, source],
72
+ });
73
+ };
74
+
75
+ getSource = (sourceType: SearchSource['type']) =>
76
+ this.sources.find((s) => s.type === sourceType);
77
+
78
+ removeSource = (sourceType: SearchSource['type']) => {
79
+ const newSources = this.sources.filter((s) => s.type !== sourceType);
80
+ if (newSources.length === this.sources.length) return;
81
+ this.state.partialNext({ sources: newSources });
82
+ };
83
+
84
+ activateSource = (sourceType: SearchSource['type']) => {
85
+ const source = this.getSource(sourceType);
86
+ if (!source || source.isActive) return;
87
+ if (this.config.keepSingleActiveSource) {
88
+ this.sources.forEach((s) => {
89
+ if (s.type !== sourceType) {
90
+ s.deactivate();
91
+ }
92
+ });
93
+ }
94
+ source.activate();
95
+ this.state.partialNext({ sources: [...this.sources] });
96
+ };
97
+
98
+ deactivateSource = (sourceType: SearchSource['type']) => {
99
+ const source = this.getSource(sourceType);
100
+ if (!source?.isActive) return;
101
+ if (this.activeSources.length === 1) return;
102
+ source.deactivate();
103
+ this.state.partialNext({ sources: [...this.sources] });
104
+ };
105
+
106
+ activate = () => {
107
+ if (!this.activeSources.length) {
108
+ const sourcesToActivate = this.config.keepSingleActiveSource
109
+ ? this.sources.slice(0, 1)
110
+ : this.sources;
111
+ sourcesToActivate.forEach((s) => s.activate());
112
+ }
113
+ if (this.isActive) return;
114
+ this.state.partialNext({ isActive: true });
115
+ };
116
+
117
+ search = async (searchQuery?: string) => {
118
+ const searchedSources = this.activeSources;
119
+ this.state.partialNext({
120
+ searchQuery,
121
+ });
122
+ await Promise.all(searchedSources.map((source) => source.search(searchQuery)));
123
+ };
124
+
125
+ cancelSearchQueries = () => {
126
+ this.activeSources.forEach((s) => s.cancelScheduledQuery());
127
+ };
128
+
129
+ clear = () => {
130
+ this.cancelSearchQueries();
131
+ this.sources.forEach((source) =>
132
+ source.state.next({ ...source.initialState, isActive: source.isActive }),
133
+ );
134
+ this.state.next((current) => ({
135
+ ...current,
136
+ isActive: true,
137
+ queriesInProgress: [],
138
+ searchQuery: '',
139
+ }));
140
+ };
141
+
142
+ exit = () => {
143
+ this.cancelSearchQueries();
144
+ this.sources.forEach((source) =>
145
+ source.state.next({ ...source.initialState, isActive: source.isActive }),
146
+ );
147
+ this.state.next((current) => ({
148
+ ...current,
149
+ isActive: false,
150
+ queriesInProgress: [],
151
+ searchQuery: '',
152
+ }));
153
+ };
154
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseSearchSource } from './BaseSearchSource';
2
+ import type { SearchSourceOptions } from './BaseSearchSource';
3
+ import type { StreamChat } from '../client';
4
+ import type { UserFilters, UserOptions, UserResponse, UserSort } from '../types';
5
+
6
+ export class UserSearchSource extends BaseSearchSource<UserResponse> {
7
+ readonly type = 'users';
8
+ private client: StreamChat;
9
+ filters: UserFilters | undefined;
10
+ sort: UserSort | undefined;
11
+ searchOptions: Omit<UserOptions, 'limit' | 'offset'> | undefined;
12
+
13
+ constructor(client: StreamChat, options?: SearchSourceOptions) {
14
+ super(options);
15
+ this.client = client;
16
+ }
17
+
18
+ protected async query(searchQuery: string) {
19
+ const filters = {
20
+ $or: [
21
+ { id: { $autocomplete: searchQuery } },
22
+ { name: { $autocomplete: searchQuery } },
23
+ ],
24
+ ...this.filters,
25
+ } as UserFilters;
26
+ const sort = { id: 1, ...this.sort } as UserSort;
27
+ const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
28
+ const { users } = await this.client.queryUsers(filters, sort, options);
29
+ return { items: users };
30
+ }
31
+
32
+ protected filterQueryResults(items: UserResponse[]) {
33
+ return items.filter((u) => u.id !== this.client.user?.id);
34
+ }
35
+ }
@@ -0,0 +1,5 @@
1
+ export * from './BaseSearchSource';
2
+ export * from './SearchController';
3
+ export { UserSearchSource } from './UserSearchSource';
4
+ export { ChannelSearchSource } from './ChannelSearchSource';
5
+ export { MessageSearchSource } from './MessageSearchSource';