stream-chat 9.50.2 → 9.51.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.
- package/dist/cjs/index.browser.js +252 -94
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +252 -94
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +252 -94
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/channel.d.ts +3 -2
- package/dist/types/client.d.ts +13 -9
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +15 -9
- package/dist/types/offline-support/offline_support_api.d.ts +7 -1
- package/dist/types/offline-support/offline_sync_manager.d.ts +19 -1
- package/dist/types/search/BaseSearchSource.d.ts +42 -24
- package/dist/types/search/ChannelMemberSearchSource.d.ts +9 -4
- package/dist/types/search/ChannelSearchSource.d.ts +9 -3
- package/dist/types/search/MessageSearchSource.d.ts +14 -3
- package/dist/types/search/UserSearchSource.d.ts +9 -3
- package/dist/types/search/types.d.ts +14 -1
- package/dist/types/types.d.ts +20 -1
- package/dist/types/utils.d.ts +1 -1
- package/package.json +7 -7
- package/src/channel.ts +4 -0
- package/src/client.ts +54 -12
- package/src/messageComposer/middleware/textComposer/mentions.ts +75 -23
- package/src/offline-support/offline_support_api.ts +17 -2
- package/src/offline-support/offline_sync_manager.ts +73 -14
- package/src/search/BaseSearchSource.ts +123 -39
- package/src/search/ChannelMemberSearchSource.ts +22 -11
- package/src/search/ChannelSearchSource.ts +16 -5
- package/src/search/MessageSearchSource.ts +36 -4
- package/src/search/UserSearchSource.ts +16 -5
- package/src/search/types.ts +14 -1
- package/src/types.ts +21 -1
- package/src/utils.ts +6 -3
|
@@ -9,10 +9,24 @@ import type {
|
|
|
9
9
|
import type { APIError } from '../errors';
|
|
10
10
|
import { isAPIError, isErrorRetryable } from '../errors';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Passed to a search source's `query()`. Carries a cancellation handle and makes no
|
|
14
|
+
* claim that the query performs a request: a source may resolve from local data and
|
|
15
|
+
* ignore the signal. Where a source does issue requests, forward this straight through
|
|
16
|
+
* as the request options.
|
|
17
|
+
*/
|
|
18
|
+
export type SearchQueryOptions = {
|
|
19
|
+
/** Aborted once a newer query supersedes this one. */
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
};
|
|
22
|
+
|
|
12
23
|
export type DebounceOptions = {
|
|
13
|
-
|
|
24
|
+
/** Applies to both short and long queries unless overridden by the options below. */
|
|
25
|
+
debounceMs?: number;
|
|
26
|
+
shortQueryDebounceMs?: number;
|
|
27
|
+
longQueryDebounceMs?: number;
|
|
28
|
+
shortQueryMaxLength?: number;
|
|
14
29
|
};
|
|
15
|
-
type DebouncedExecQueryFunction = DebouncedFunc<(searchString?: string) => Promise<void>>;
|
|
16
30
|
|
|
17
31
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
32
|
interface ISearchSource<T = any> {
|
|
@@ -54,18 +68,44 @@ export interface SearchSourceSync<T = any> extends ISearchSource<T> {
|
|
|
54
68
|
search(text?: string): void;
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
// Debounce defaults are resolved by resolveDebounceOptions, not here.
|
|
72
|
+
const DEFAULT_SEARCH_SOURCE_OPTIONS: Required<
|
|
73
|
+
Omit<SearchSourceOptions, keyof DebounceOptions>
|
|
74
|
+
> = {
|
|
59
75
|
pageSize: 10,
|
|
60
76
|
allowEmptySearchString: false,
|
|
61
77
|
resetOnNewSearchQuery: true,
|
|
62
78
|
} as const;
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
/**
|
|
81
|
+
* An explicit `debounceMs` applies to both buckets, so integrators who already tuned it
|
|
82
|
+
* keep exactly their configured interval. Only when it is absent do the 500/300 defaults
|
|
83
|
+
* kick in.
|
|
84
|
+
*/
|
|
85
|
+
const resolveDebounceOptions = ({
|
|
86
|
+
debounceMs,
|
|
87
|
+
shortQueryDebounceMs,
|
|
88
|
+
longQueryDebounceMs,
|
|
89
|
+
shortQueryMaxLength,
|
|
90
|
+
}: DebounceOptions) => ({
|
|
91
|
+
shortQueryDebounceMs: shortQueryDebounceMs ?? debounceMs ?? 500,
|
|
92
|
+
longQueryDebounceMs: longQueryDebounceMs ?? debounceMs ?? 300,
|
|
93
|
+
shortQueryMaxLength: shortQueryMaxLength ?? 2,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
abstract class BaseSearchSourceBase<
|
|
97
|
+
T,
|
|
98
|
+
R extends void | Promise<void>,
|
|
99
|
+
> implements ISearchSource<T> {
|
|
65
100
|
state: StateStore<SearchSourceState<T>>;
|
|
66
101
|
pageSize: number;
|
|
67
102
|
protected allowEmptySearchString: boolean;
|
|
68
103
|
protected resetOnNewSearchQuery: boolean;
|
|
104
|
+
// assigned by setDebounceOptions, which the constructor always calls
|
|
105
|
+
protected shortQueryDebounceMs!: number;
|
|
106
|
+
protected longQueryDebounceMs!: number;
|
|
107
|
+
protected shortQueryMaxLength!: number;
|
|
108
|
+
protected searchDebounced!: DebouncedFunc<(searchString?: string) => R>;
|
|
69
109
|
abstract readonly type: SearchSourceType;
|
|
70
110
|
|
|
71
111
|
protected constructor(options?: SearchSourceOptions) {
|
|
@@ -77,8 +117,44 @@ abstract class BaseSearchSourceBase<T> implements ISearchSource<T> {
|
|
|
77
117
|
this.allowEmptySearchString = allowEmptySearchString;
|
|
78
118
|
this.resetOnNewSearchQuery = resetOnNewSearchQuery;
|
|
79
119
|
this.state = new StateStore<SearchSourceState<T>>(this.initialState);
|
|
120
|
+
// Field initializers run before this body, so setDebounceOptions is already
|
|
121
|
+
// assigned; it only captures getDebounceMs lazily.
|
|
122
|
+
this.setDebounceOptions(options ?? {});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
abstract executeQuery(newSearchString?: string): R;
|
|
126
|
+
|
|
127
|
+
setDebounceOptions = (options: DebounceOptions = {}) => {
|
|
128
|
+
const resolved = resolveDebounceOptions(options);
|
|
129
|
+
this.shortQueryDebounceMs = resolved.shortQueryDebounceMs;
|
|
130
|
+
this.longQueryDebounceMs = resolved.longQueryDebounceMs;
|
|
131
|
+
this.shortQueryMaxLength = resolved.shortQueryMaxLength;
|
|
132
|
+
this.searchDebounced = debounce(this.executeQuery.bind(this), (searchString) =>
|
|
133
|
+
this.getDebounceMs(searchString),
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Short queries match too much and are slow server-side, so they get a longer debounce.
|
|
139
|
+
* Resolved per call, which is what lets the interval switch as the user keeps typing.
|
|
140
|
+
*/
|
|
141
|
+
protected getDebounceMs = (searchString?: string) => {
|
|
142
|
+
const { length } = searchString ?? this.searchQuery;
|
|
143
|
+
return length <= this.shortQueryMaxLength
|
|
144
|
+
? this.shortQueryDebounceMs
|
|
145
|
+
: this.longQueryDebounceMs;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A new search query preempts an in-flight one; pagination waits for it to finish
|
|
150
|
+
* and needs a next page to fetch.
|
|
151
|
+
*/
|
|
152
|
+
protected canDispatchQuery(hasNewSearchQuery: boolean) {
|
|
153
|
+
return hasNewSearchQuery || (!this.isLoading && this.hasNext);
|
|
80
154
|
}
|
|
81
155
|
|
|
156
|
+
search = (searchQuery?: string) => this.searchDebounced(searchQuery);
|
|
157
|
+
|
|
82
158
|
get lastQueryError() {
|
|
83
159
|
return this.state.getLatestValue().lastQueryError;
|
|
84
160
|
}
|
|
@@ -99,7 +175,7 @@ abstract class BaseSearchSourceBase<T> implements ISearchSource<T> {
|
|
|
99
175
|
return this.state.getLatestValue().isLoading;
|
|
100
176
|
}
|
|
101
177
|
|
|
102
|
-
get initialState() {
|
|
178
|
+
get initialState(): SearchSourceState<T> {
|
|
103
179
|
return {
|
|
104
180
|
hasNext: true,
|
|
105
181
|
isActive: false,
|
|
@@ -143,8 +219,7 @@ abstract class BaseSearchSourceBase<T> implements ISearchSource<T> {
|
|
|
143
219
|
const searchString = newSearchString ?? this.searchQuery;
|
|
144
220
|
return !!(
|
|
145
221
|
this.isActive &&
|
|
146
|
-
|
|
147
|
-
(this.hasNext || hasNewSearchQuery) &&
|
|
222
|
+
this.canDispatchQuery(hasNewSearchQuery) &&
|
|
148
223
|
(this.allowEmptySearchString || searchString)
|
|
149
224
|
);
|
|
150
225
|
};
|
|
@@ -217,34 +292,48 @@ abstract class BaseSearchSourceBase<T> implements ISearchSource<T> {
|
|
|
217
292
|
}
|
|
218
293
|
|
|
219
294
|
export abstract class BaseSearchSource<T>
|
|
220
|
-
extends BaseSearchSourceBase<T
|
|
295
|
+
extends BaseSearchSourceBase<T, Promise<void>>
|
|
221
296
|
implements SearchSource<T>
|
|
222
297
|
{
|
|
223
|
-
|
|
298
|
+
/** Aborts the in-flight request, if any, once a newer query is dispatched. */
|
|
299
|
+
protected queryAbortController: AbortController | null = null;
|
|
224
300
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
protected abstract query(searchQuery: string): Promise<QueryReturnValue<T>>;
|
|
301
|
+
protected abstract query(
|
|
302
|
+
searchQuery: string,
|
|
303
|
+
options?: SearchQueryOptions,
|
|
304
|
+
): Promise<QueryReturnValue<T>>;
|
|
232
305
|
|
|
233
306
|
protected abstract filterQueryResults(items: T[]): T[] | Promise<T[]>;
|
|
234
307
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
308
|
+
/** Aborts the in-flight request, if any. Returns true when one was aborted. */
|
|
309
|
+
protected abortInFlightQuery() {
|
|
310
|
+
if (!this.queryAbortController) return false;
|
|
311
|
+
this.queryAbortController.abort();
|
|
312
|
+
this.queryAbortController = null;
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
resetState() {
|
|
317
|
+
// otherwise an in-flight response would repopulate the state we just cleared
|
|
318
|
+
this.abortInFlightQuery();
|
|
319
|
+
super.resetState();
|
|
320
|
+
}
|
|
238
321
|
|
|
239
322
|
async executeQuery(newSearchString?: string) {
|
|
323
|
+
// Checked before anything is dispatched, so that the abort below always has a
|
|
324
|
+
// successor that will clear isLoading.
|
|
240
325
|
if (!this.canExecuteQuery(newSearchString)) return;
|
|
241
326
|
|
|
327
|
+
// cancel the previous request before dispatching the new one
|
|
328
|
+
this.abortInFlightQuery();
|
|
329
|
+
const { signal } = (this.queryAbortController = new AbortController());
|
|
330
|
+
|
|
242
331
|
const { hasNewSearchQuery, searchString } =
|
|
243
332
|
this.prepareStateForQuery(newSearchString);
|
|
244
333
|
|
|
245
334
|
let stateUpdate: Partial<SearchSourceState<T>> = {};
|
|
246
335
|
try {
|
|
247
|
-
const results = await this.query(searchString);
|
|
336
|
+
const results = await this.query(searchString, { signal });
|
|
248
337
|
if (!results) return;
|
|
249
338
|
|
|
250
339
|
const { items } = results;
|
|
@@ -256,37 +345,34 @@ export abstract class BaseSearchSource<T>
|
|
|
256
345
|
stateUpdate.hasNext = false;
|
|
257
346
|
}
|
|
258
347
|
} finally {
|
|
259
|
-
|
|
348
|
+
// A newer query owns the state now - publishing here would clobber it with a
|
|
349
|
+
// stale result (and with the abort error).
|
|
350
|
+
if (!signal.aborted) {
|
|
351
|
+
this.state.next(this.getStateAfterQuery(stateUpdate, hasNewSearchQuery));
|
|
352
|
+
}
|
|
260
353
|
}
|
|
261
354
|
}
|
|
262
355
|
|
|
263
|
-
search = (searchQuery?: string) => this.searchDebounced(searchQuery);
|
|
264
|
-
|
|
265
356
|
cancelScheduledQuery() {
|
|
266
357
|
this.searchDebounced.cancel();
|
|
358
|
+
// Nothing will dispatch a successor query, so release the loading state the
|
|
359
|
+
// aborted query owned - its own finally block skips the state write.
|
|
360
|
+
if (this.abortInFlightQuery() && this.isLoading) {
|
|
361
|
+
this.state.partialNext({ isLoading: false });
|
|
362
|
+
}
|
|
267
363
|
}
|
|
268
364
|
}
|
|
269
365
|
|
|
366
|
+
// Queries are resolved locally, so there is nothing to cancel here - only the
|
|
367
|
+
// dynamic debounce applies.
|
|
270
368
|
export abstract class BaseSearchSourceSync<T>
|
|
271
|
-
extends BaseSearchSourceBase<T>
|
|
369
|
+
extends BaseSearchSourceBase<T, void>
|
|
272
370
|
implements SearchSourceSync<T>
|
|
273
371
|
{
|
|
274
|
-
protected searchDebounced!: DebouncedExecQueryFunction;
|
|
275
|
-
|
|
276
|
-
constructor(options?: SearchSourceOptions) {
|
|
277
|
-
const { debounceMs } = { ...DEFAULT_SEARCH_SOURCE_OPTIONS, ...options };
|
|
278
|
-
super(options);
|
|
279
|
-
this.setDebounceOptions({ debounceMs });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
372
|
protected abstract query(searchQuery: string): QueryReturnValue<T>;
|
|
283
373
|
|
|
284
374
|
protected abstract filterQueryResults(items: T[]): T[];
|
|
285
375
|
|
|
286
|
-
setDebounceOptions = ({ debounceMs }: DebounceOptions) => {
|
|
287
|
-
this.searchDebounced = debounce(this.executeQuery.bind(this), debounceMs);
|
|
288
|
-
};
|
|
289
|
-
|
|
290
376
|
executeQuery(newSearchString?: string) {
|
|
291
377
|
if (!this.canExecuteQuery(newSearchString)) return;
|
|
292
378
|
|
|
@@ -311,8 +397,6 @@ export abstract class BaseSearchSourceSync<T>
|
|
|
311
397
|
}
|
|
312
398
|
}
|
|
313
399
|
|
|
314
|
-
search = (searchQuery?: string) => this.searchDebounced(searchQuery);
|
|
315
|
-
|
|
316
400
|
cancelScheduledQuery() {
|
|
317
401
|
this.searchDebounced.cancel();
|
|
318
402
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import { FilterBuilder, type FilterBuilderOptions } from '../pagination';
|
|
3
3
|
import type { Channel } from '../channel';
|
|
4
4
|
import type {
|
|
@@ -15,6 +15,13 @@ export type ChannelMemberSearchSourceFilterBuilderContext<
|
|
|
15
15
|
C extends CustomContext = CustomContext,
|
|
16
16
|
> = { searchQuery?: string } & C;
|
|
17
17
|
|
|
18
|
+
export type ChannelMemberSearchSourceOptions = SearchSourceOptions & {
|
|
19
|
+
/** Static base filters merged under the dynamically generated ones. */
|
|
20
|
+
filters?: MemberFilters;
|
|
21
|
+
sort?: MemberSort;
|
|
22
|
+
searchOptions?: Omit<QueryMembersOptions, 'limit' | 'offset'>;
|
|
23
|
+
};
|
|
24
|
+
|
|
18
25
|
export class ChannelMemberSearchSource<
|
|
19
26
|
TFilterContext extends CustomContext = CustomContext,
|
|
20
27
|
> extends BaseSearchSource<ChannelMemberResponse> {
|
|
@@ -30,14 +37,19 @@ export class ChannelMemberSearchSource<
|
|
|
30
37
|
|
|
31
38
|
constructor(
|
|
32
39
|
channel: Channel,
|
|
33
|
-
options?:
|
|
40
|
+
options?: ChannelMemberSearchSourceOptions,
|
|
34
41
|
filterBuilderOptions: FilterBuilderOptions<
|
|
35
42
|
MemberFilters,
|
|
36
43
|
ChannelMemberSearchSourceFilterBuilderContext<TFilterContext>
|
|
37
44
|
> = {},
|
|
38
45
|
) {
|
|
39
|
-
|
|
46
|
+
const { filters, sort, searchOptions, ...restOptions } = options || {};
|
|
47
|
+
// members are listed with an empty query, so the initial load must be allowed
|
|
48
|
+
super({ ...restOptions, allowEmptySearchString: true });
|
|
40
49
|
this.channel = channel;
|
|
50
|
+
this.filters = filters;
|
|
51
|
+
this.sort = sort;
|
|
52
|
+
this.searchOptions = searchOptions;
|
|
41
53
|
this.filterBuilder = new FilterBuilder<
|
|
42
54
|
MemberFilters,
|
|
43
55
|
ChannelMemberSearchSourceFilterBuilderContext<TFilterContext>
|
|
@@ -60,13 +72,7 @@ export class ChannelMemberSearchSource<
|
|
|
60
72
|
});
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
|
|
64
|
-
const hasNewSearchQuery = typeof newSearchString !== 'undefined';
|
|
65
|
-
|
|
66
|
-
return this.isActive && !this.isLoading && (this.hasNext || hasNewSearchQuery);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
protected async query(searchQuery: string) {
|
|
75
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
70
76
|
const filters = this.filterBuilder.buildFilters({
|
|
71
77
|
baseFilters: this.filters,
|
|
72
78
|
context: {
|
|
@@ -75,7 +81,12 @@ export class ChannelMemberSearchSource<
|
|
|
75
81
|
});
|
|
76
82
|
const sort = this.sort ?? [];
|
|
77
83
|
const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
|
|
78
|
-
const { members } = await this.channel.queryMembers(
|
|
84
|
+
const { members } = await this.channel.queryMembers(
|
|
85
|
+
filters ?? {},
|
|
86
|
+
sort,
|
|
87
|
+
options,
|
|
88
|
+
queryOptions,
|
|
89
|
+
);
|
|
79
90
|
return { items: members };
|
|
80
91
|
}
|
|
81
92
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import type { FilterBuilderOptions } from '../pagination';
|
|
3
3
|
import { FilterBuilder } from '../pagination';
|
|
4
4
|
import type { Channel } from '../channel';
|
|
@@ -12,6 +12,13 @@ export type ChannelSearchSourceFilterBuilderContext<
|
|
|
12
12
|
C extends CustomContext = CustomContext,
|
|
13
13
|
> = { searchQuery?: string } & C;
|
|
14
14
|
|
|
15
|
+
export type ChannelSearchSourceOptions = SearchSourceOptions & {
|
|
16
|
+
/** Static base filters merged under the dynamically generated ones. */
|
|
17
|
+
filters?: ChannelFilters;
|
|
18
|
+
sort?: ChannelSort;
|
|
19
|
+
searchOptions?: Omit<ChannelOptions, 'limit' | 'offset'>;
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
export class ChannelSearchSource<
|
|
16
23
|
TFilterContext extends CustomContext = CustomContext,
|
|
17
24
|
> extends BaseSearchSource<Channel> {
|
|
@@ -27,14 +34,18 @@ export class ChannelSearchSource<
|
|
|
27
34
|
|
|
28
35
|
constructor(
|
|
29
36
|
client: StreamChat,
|
|
30
|
-
options?:
|
|
37
|
+
options?: ChannelSearchSourceOptions,
|
|
31
38
|
filterBuilderOptions: FilterBuilderOptions<
|
|
32
39
|
ChannelFilters,
|
|
33
40
|
ChannelSearchSourceFilterBuilderContext<TFilterContext>
|
|
34
41
|
> = {},
|
|
35
42
|
) {
|
|
36
|
-
|
|
43
|
+
const { filters, sort, searchOptions, ...restOptions } = options || {};
|
|
44
|
+
super(restOptions);
|
|
37
45
|
this.client = client;
|
|
46
|
+
this.filters = filters;
|
|
47
|
+
this.sort = sort;
|
|
48
|
+
this.searchOptions = searchOptions;
|
|
38
49
|
this.filterBuilder = new FilterBuilder<
|
|
39
50
|
ChannelFilters,
|
|
40
51
|
ChannelSearchSourceFilterBuilderContext<TFilterContext>
|
|
@@ -51,7 +62,7 @@ export class ChannelSearchSource<
|
|
|
51
62
|
});
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
protected async query(searchQuery: string) {
|
|
65
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
55
66
|
const filters = this.filterBuilder.buildFilters({
|
|
56
67
|
baseFilters: {
|
|
57
68
|
...(this.client.userID ? { members: { $in: [this.client.userID] } } : {}),
|
|
@@ -63,7 +74,7 @@ export class ChannelSearchSource<
|
|
|
63
74
|
});
|
|
64
75
|
const sort = this.sort ?? {};
|
|
65
76
|
const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
|
|
66
|
-
const items = await this.client.queryChannels(filters, sort, options);
|
|
77
|
+
const items = await this.client.queryChannels(filters, sort, options, queryOptions);
|
|
67
78
|
return { items };
|
|
68
79
|
}
|
|
69
80
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import type {
|
|
3
3
|
ChannelFilters,
|
|
4
4
|
ChannelOptions,
|
|
@@ -54,6 +54,18 @@ export type MessageSearchSourceFilterBuilderOptions<
|
|
|
54
54
|
>;
|
|
55
55
|
}>;
|
|
56
56
|
|
|
57
|
+
export type MessageSearchSourceOptions = SearchSourceOptions & {
|
|
58
|
+
/** Static base filters for the channel scope of the message search. */
|
|
59
|
+
messageSearchChannelFilters?: ChannelFilters;
|
|
60
|
+
/** Static base filters for the message search itself. */
|
|
61
|
+
messageSearchFilters?: MessageFilters;
|
|
62
|
+
messageSearchSort?: SearchMessageSort;
|
|
63
|
+
/** Static base filters for the follow-up query that hydrates unknown channels. */
|
|
64
|
+
channelQueryFilters?: ChannelFilters;
|
|
65
|
+
channelQuerySort?: ChannelSort;
|
|
66
|
+
channelQueryOptions?: Omit<ChannelOptions, 'limit' | 'offset'>;
|
|
67
|
+
};
|
|
68
|
+
|
|
57
69
|
export class MessageSearchSource<
|
|
58
70
|
TContexts extends MessageSearchSourceContexts = {},
|
|
59
71
|
> extends BaseSearchSource<MessageResponse> {
|
|
@@ -86,11 +98,26 @@ export class MessageSearchSource<
|
|
|
86
98
|
|
|
87
99
|
constructor(
|
|
88
100
|
client: StreamChat,
|
|
89
|
-
options?:
|
|
101
|
+
options?: MessageSearchSourceOptions,
|
|
90
102
|
filterBuilderOptions?: MessageSearchSourceFilterBuilderOptions<TContexts>,
|
|
91
103
|
) {
|
|
92
|
-
|
|
104
|
+
const {
|
|
105
|
+
messageSearchChannelFilters,
|
|
106
|
+
messageSearchFilters,
|
|
107
|
+
messageSearchSort,
|
|
108
|
+
channelQueryFilters,
|
|
109
|
+
channelQuerySort,
|
|
110
|
+
channelQueryOptions,
|
|
111
|
+
...restOptions
|
|
112
|
+
} = options || {};
|
|
113
|
+
super(restOptions);
|
|
93
114
|
this.client = client;
|
|
115
|
+
this.messageSearchChannelFilters = messageSearchChannelFilters;
|
|
116
|
+
this.messageSearchFilters = messageSearchFilters;
|
|
117
|
+
this.messageSearchSort = messageSearchSort;
|
|
118
|
+
this.channelQueryFilters = channelQueryFilters;
|
|
119
|
+
this.channelQuerySort = channelQuerySort;
|
|
120
|
+
this.channelQueryOptions = channelQueryOptions;
|
|
94
121
|
|
|
95
122
|
this.messageSearchChannelFilterBuilder = new FilterBuilder<
|
|
96
123
|
ChannelFilters,
|
|
@@ -129,7 +156,7 @@ export class MessageSearchSource<
|
|
|
129
156
|
});
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
protected async query(searchQuery: string) {
|
|
159
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
133
160
|
if (!this.client.userID || this.next === null) return { items: [] };
|
|
134
161
|
|
|
135
162
|
const channelFilters = this.messageSearchChannelFilterBuilder.buildFilters({
|
|
@@ -170,9 +197,13 @@ export class MessageSearchSource<
|
|
|
170
197
|
channelFilters,
|
|
171
198
|
messageFilters,
|
|
172
199
|
options,
|
|
200
|
+
queryOptions,
|
|
173
201
|
);
|
|
174
202
|
const items = results.map(({ message }) => message);
|
|
175
203
|
|
|
204
|
+
// a newer query already replaced this one - skip the cid scan and the hydration request
|
|
205
|
+
if (queryOptions.signal?.aborted) return { items, next };
|
|
206
|
+
|
|
176
207
|
const cids = Array.from(
|
|
177
208
|
items.reduce((acc, message) => {
|
|
178
209
|
if (message.cid && !this.client.activeChannels[message.cid]) acc.add(message.cid);
|
|
@@ -194,6 +225,7 @@ export class MessageSearchSource<
|
|
|
194
225
|
...this.channelQuerySort,
|
|
195
226
|
},
|
|
196
227
|
this.channelQueryOptions,
|
|
228
|
+
queryOptions,
|
|
197
229
|
);
|
|
198
230
|
}
|
|
199
231
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseSearchSource } from './BaseSearchSource';
|
|
1
|
+
import { BaseSearchSource, type SearchQueryOptions } from './BaseSearchSource';
|
|
2
2
|
import { FilterBuilder, type FilterBuilderOptions } from '../pagination';
|
|
3
3
|
import type { StreamChat } from '../client';
|
|
4
4
|
import type { UserFilters, UserOptions, UserResponse, UserSort } from '../types';
|
|
@@ -10,6 +10,13 @@ export type UserSearchSourceFilterBuilderContext<
|
|
|
10
10
|
C extends CustomContext = CustomContext,
|
|
11
11
|
> = { searchQuery?: string } & C;
|
|
12
12
|
|
|
13
|
+
export type UserSearchSourceOptions = SearchSourceOptions & {
|
|
14
|
+
/** Static base filters merged under the dynamically generated ones. */
|
|
15
|
+
filters?: UserFilters;
|
|
16
|
+
sort?: UserSort;
|
|
17
|
+
searchOptions?: Omit<UserOptions, 'limit' | 'offset'>;
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
export class UserSearchSource<
|
|
14
21
|
TFilterContext extends CustomContext = CustomContext,
|
|
15
22
|
> extends BaseSearchSource<UserResponse> {
|
|
@@ -25,14 +32,18 @@ export class UserSearchSource<
|
|
|
25
32
|
|
|
26
33
|
constructor(
|
|
27
34
|
client: StreamChat,
|
|
28
|
-
options?:
|
|
35
|
+
options?: UserSearchSourceOptions,
|
|
29
36
|
filterBuilderOptions: FilterBuilderOptions<
|
|
30
37
|
UserFilters,
|
|
31
38
|
UserSearchSourceFilterBuilderContext<TFilterContext>
|
|
32
39
|
> = {},
|
|
33
40
|
) {
|
|
34
|
-
|
|
41
|
+
const { filters, sort, searchOptions, ...restOptions } = options || {};
|
|
42
|
+
super(restOptions);
|
|
35
43
|
this.client = client;
|
|
44
|
+
this.filters = filters;
|
|
45
|
+
this.sort = sort;
|
|
46
|
+
this.searchOptions = searchOptions;
|
|
36
47
|
this.filterBuilder = new FilterBuilder<
|
|
37
48
|
UserFilters,
|
|
38
49
|
UserSearchSourceFilterBuilderContext<TFilterContext>
|
|
@@ -55,7 +66,7 @@ export class UserSearchSource<
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
protected async query(searchQuery: string) {
|
|
69
|
+
protected async query(searchQuery: string, queryOptions: SearchQueryOptions = {}) {
|
|
59
70
|
const filters = this.filterBuilder.buildFilters({
|
|
60
71
|
baseFilters: this.filters,
|
|
61
72
|
context: { searchQuery } as UserSearchSourceFilterBuilderContext<TFilterContext>,
|
|
@@ -68,7 +79,7 @@ export class UserSearchSource<
|
|
|
68
79
|
sort = { id: 1, ...this.sort };
|
|
69
80
|
}
|
|
70
81
|
const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
|
|
71
|
-
const { users } = await this.client.queryUsers(filters, sort, options);
|
|
82
|
+
const { users } = await this.client.queryUsers(filters, sort, options, queryOptions);
|
|
72
83
|
return { items: users };
|
|
73
84
|
}
|
|
74
85
|
|
package/src/search/types.ts
CHANGED
|
@@ -11,8 +11,21 @@ export type SearchSourceState<T = any> = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export type SearchSourceOptions = {
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Legacy single debounce interval. When set, it applies to both short and long queries,
|
|
16
|
+
* unless overridden by `shortQueryDebounceMs` / `longQueryDebounceMs`.
|
|
17
|
+
*/
|
|
15
18
|
debounceMs?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Debounce interval for queries no longer than `shortQueryMaxLength`. Such queries have
|
|
21
|
+
* low selectivity and are expensive server-side, so they are debounced harder.
|
|
22
|
+
* Defaults to 500ms.
|
|
23
|
+
*/
|
|
24
|
+
shortQueryDebounceMs?: number;
|
|
25
|
+
/** Debounce interval for queries longer than `shortQueryMaxLength`. Defaults to 300ms. */
|
|
26
|
+
longQueryDebounceMs?: number;
|
|
27
|
+
/** Query length (inclusive) that still counts as short. Defaults to 2. */
|
|
28
|
+
shortQueryMaxLength?: number;
|
|
16
29
|
pageSize?: number;
|
|
17
30
|
/** When true, the source can execute queries with an empty search string. Defaults to false. */
|
|
18
31
|
allowEmptySearchString?: boolean;
|
package/src/types.ts
CHANGED
|
@@ -167,6 +167,7 @@ export type AppSettingsAPIResponse = APIResponse & {
|
|
|
167
167
|
moderation_dashboard_preferences?: Record<string, unknown> | null;
|
|
168
168
|
moderation_audio_call_moderation_enabled?: boolean;
|
|
169
169
|
moderation_enabled?: boolean;
|
|
170
|
+
moderation_flood_rules_enabled?: boolean;
|
|
170
171
|
moderation_llm_configurability_enabled?: boolean;
|
|
171
172
|
moderation_multitenant_blocklist_enabled?: boolean;
|
|
172
173
|
moderation_video_call_moderation_enabled?: boolean;
|
|
@@ -319,6 +320,8 @@ export type QueryFutureChannelBansOptions = {
|
|
|
319
320
|
|
|
320
321
|
export type BlockListResponse = BlockList & {
|
|
321
322
|
created_at?: string;
|
|
323
|
+
id?: string;
|
|
324
|
+
owner_user_id?: string;
|
|
322
325
|
type?: string;
|
|
323
326
|
updated_at?: string;
|
|
324
327
|
};
|
|
@@ -1045,6 +1048,8 @@ export type UserResponse = CustomUserData & {
|
|
|
1045
1048
|
anon?: boolean;
|
|
1046
1049
|
banned?: boolean;
|
|
1047
1050
|
blocked_user_ids?: string[];
|
|
1051
|
+
/** Set on the user of a read event, marking when they last read the event's channel. */
|
|
1052
|
+
channel_last_read_at?: string;
|
|
1048
1053
|
created_at?: string;
|
|
1049
1054
|
deactivated_at?: string;
|
|
1050
1055
|
deleted_at?: string;
|
|
@@ -1184,7 +1189,13 @@ export type ChannelQueryOptions = {
|
|
|
1184
1189
|
watchers?: PaginationOptions;
|
|
1185
1190
|
};
|
|
1186
1191
|
|
|
1187
|
-
|
|
1192
|
+
/**
|
|
1193
|
+
* Composed with RequestOptions because `queryChannels` carries the abort signal in this
|
|
1194
|
+
* bag; neither the state flags nor the signal are serialized into the request. Declaring
|
|
1195
|
+
* the composition keeps passing a RequestOptions here intentional rather than incidental
|
|
1196
|
+
* structural compatibility.
|
|
1197
|
+
*/
|
|
1198
|
+
export type ChannelStateOptions = RequestOptions & {
|
|
1188
1199
|
offlineMode?: boolean;
|
|
1189
1200
|
skipInitialization?: string[];
|
|
1190
1201
|
skipHydration?: boolean;
|
|
@@ -1568,6 +1579,12 @@ export type SearchOptions = {
|
|
|
1568
1579
|
sort?: SearchMessageSort;
|
|
1569
1580
|
};
|
|
1570
1581
|
|
|
1582
|
+
/** Per-request options that are never part of the serialized request payload. */
|
|
1583
|
+
export type RequestOptions = {
|
|
1584
|
+
/** Aborts the request. See AbortController. */
|
|
1585
|
+
signal?: AbortSignal;
|
|
1586
|
+
};
|
|
1587
|
+
|
|
1571
1588
|
export type StreamChatOptions = AxiosRequestConfig & {
|
|
1572
1589
|
/**
|
|
1573
1590
|
* Used to disable warnings that are triggered by using connectUser or connectAnonymousUser server-side.
|
|
@@ -2588,6 +2605,7 @@ export type BlockList = {
|
|
|
2588
2605
|
is_confusable_folding_enabled?: boolean;
|
|
2589
2606
|
is_leet_check_enabled?: boolean;
|
|
2590
2607
|
is_plural_check_enabled?: boolean;
|
|
2608
|
+
is_substring_matching_enabled?: boolean;
|
|
2591
2609
|
};
|
|
2592
2610
|
|
|
2593
2611
|
export type ChannelConfig = ChannelConfigFields &
|
|
@@ -3098,6 +3116,8 @@ export type PermissionAPIObject = {
|
|
|
3098
3116
|
level?: string;
|
|
3099
3117
|
name?: string;
|
|
3100
3118
|
owner?: boolean;
|
|
3119
|
+
/** Resource type that defines ownership for this permission's action, e.g. 'Channel' for CreateMessage. */
|
|
3120
|
+
owner_resource?: string;
|
|
3101
3121
|
same_team?: boolean;
|
|
3102
3122
|
tags?: string[];
|
|
3103
3123
|
};
|
package/src/utils.ts
CHANGED
|
@@ -723,11 +723,13 @@ export interface DebouncedFunc<T extends (...args: any[]) => any> {
|
|
|
723
723
|
flush(): ReturnType<T> | undefined;
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
-
// works exactly the same as lodash.debounce
|
|
726
|
+
// works exactly the same as lodash.debounce, except that the timeout can also be
|
|
727
|
+
// a function of the call arguments, resolved on every call (e.g. to debounce short,
|
|
728
|
+
// low-selectivity search queries harder than long ones)
|
|
727
729
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
728
730
|
export const debounce = <T extends (...args: any[]) => any>(
|
|
729
731
|
fn: T,
|
|
730
|
-
timeout = 0,
|
|
732
|
+
timeout: number | ((...args: Parameters<T>) => number) = 0,
|
|
731
733
|
{ leading = false, trailing = true }: { leading?: boolean; trailing?: boolean } = {},
|
|
732
734
|
): DebouncedFunc<T> => {
|
|
733
735
|
let runningTimeout: null | NodeJS.Timeout = null;
|
|
@@ -750,7 +752,8 @@ export const debounce = <T extends (...args: any[]) => any>(
|
|
|
750
752
|
runningTimeout = null;
|
|
751
753
|
};
|
|
752
754
|
|
|
753
|
-
|
|
755
|
+
const delay = typeof timeout === 'function' ? timeout(...args) : timeout;
|
|
756
|
+
runningTimeout = setTimeout(timeoutHandler, delay);
|
|
754
757
|
return lastResult;
|
|
755
758
|
};
|
|
756
759
|
|