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.
- package/dist/cjs/index.browser.cjs +843 -146
- package/dist/cjs/index.browser.cjs.map +4 -4
- package/dist/cjs/index.node.cjs +859 -152
- package/dist/cjs/index.node.cjs.map +4 -4
- package/dist/esm/index.js +846 -149
- package/dist/esm/index.js.map +4 -4
- package/dist/types/client.d.ts +52 -19
- package/dist/types/events.d.ts +4 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -2
- package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +2 -2
- package/dist/types/messageComposer/middleware/textComposer/types.d.ts +1 -1
- package/dist/types/pagination/BasePaginator.d.ts +69 -0
- package/dist/types/pagination/ReminderPaginator.d.ts +12 -0
- package/dist/types/pagination/index.d.ts +2 -0
- package/dist/types/reminders/Reminder.d.ts +37 -0
- package/dist/types/reminders/ReminderManager.d.ts +65 -0
- package/dist/types/reminders/ReminderTimer.d.ts +17 -0
- package/dist/types/reminders/index.d.ts +3 -0
- package/dist/types/search/BaseSearchSource.d.ts +87 -0
- package/dist/types/search/ChannelSearchSource.d.ts +17 -0
- package/dist/types/search/MessageSearchSource.d.ts +23 -0
- package/dist/types/search/SearchController.d.ts +44 -0
- package/dist/types/search/UserSearchSource.d.ts +16 -0
- package/dist/types/search/index.d.ts +5 -0
- package/dist/types/store.d.ts +114 -5
- package/dist/types/types.d.ts +48 -3
- package/package.json +4 -4
- package/src/channel.ts +2 -1
- package/src/client.ts +108 -39
- package/src/events.ts +6 -0
- package/src/index.ts +3 -1
- package/src/messageComposer/middleware/textComposer/commands.ts +2 -2
- package/src/messageComposer/middleware/textComposer/mentions.ts +2 -2
- package/src/messageComposer/middleware/textComposer/types.ts +1 -1
- package/src/pagination/BasePaginator.ts +184 -0
- package/src/pagination/ReminderPaginator.ts +38 -0
- package/src/pagination/index.ts +2 -0
- package/src/reminders/Reminder.ts +89 -0
- package/src/reminders/ReminderManager.ts +284 -0
- package/src/reminders/ReminderTimer.ts +86 -0
- package/src/reminders/index.ts +3 -0
- package/src/search/BaseSearchSource.ts +227 -0
- package/src/search/ChannelSearchSource.ts +34 -0
- package/src/search/MessageSearchSource.ts +88 -0
- package/src/search/SearchController.ts +154 -0
- package/src/search/UserSearchSource.ts +35 -0
- package/src/search/index.ts +5 -0
- package/src/store.ts +237 -11
- package/src/token_manager.ts +3 -1
- package/src/types.ts +91 -1
- package/dist/types/search_controller.d.ts +0 -174
- 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
|
+
}
|