stream-chat 8.54.1 → 8.56.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.
@@ -0,0 +1,581 @@
1
+ import type { StreamChat } from './client';
2
+ import type {
3
+ DefaultGenerics,
4
+ ExtendableGenerics,
5
+ Event,
6
+ ChannelOptions,
7
+ ChannelStateOptions,
8
+ ChannelFilters,
9
+ ChannelSort,
10
+ } from './types';
11
+ import { StateStore, ValueOrPatch, isPatch } from './store';
12
+ import { Channel } from './channel';
13
+ import {
14
+ extractSortValue,
15
+ findLastPinnedChannelIndex,
16
+ getAndWatchChannel,
17
+ isChannelArchived,
18
+ isChannelPinned,
19
+ promoteChannel,
20
+ shouldConsiderArchivedChannels,
21
+ shouldConsiderPinnedChannels,
22
+ } from './utils';
23
+
24
+ export type ChannelManagerPagination<SCG extends ExtendableGenerics = DefaultGenerics> = {
25
+ filters: ChannelFilters<SCG>;
26
+ hasNext: boolean;
27
+ isLoading: boolean;
28
+ isLoadingNext: boolean;
29
+ options: ChannelOptions;
30
+ sort: ChannelSort<SCG>;
31
+ };
32
+
33
+ export type ChannelManagerState<SCG extends ExtendableGenerics = DefaultGenerics> = {
34
+ channels: Channel<SCG>[];
35
+ /**
36
+ * This value will become true the first time queryChannels is successfully executed and
37
+ * will remain false otherwise. It's used as a control property regarding whether the list
38
+ * has been initialized yet (i.e a query has already been done at least once) or not. We do
39
+ * this to prevent state.channels from being forced to be nullable.
40
+ */
41
+ initialized: boolean;
42
+ pagination: ChannelManagerPagination<SCG>;
43
+ };
44
+
45
+ export type ChannelSetterParameterType<SCG extends ExtendableGenerics = DefaultGenerics> = ValueOrPatch<
46
+ ChannelManagerState<SCG>['channels']
47
+ >;
48
+ export type ChannelSetterType<SCG extends ExtendableGenerics = DefaultGenerics> = (
49
+ arg: ChannelSetterParameterType<SCG>,
50
+ ) => void;
51
+
52
+ export type GenericEventHandlerType<T extends unknown[]> = (
53
+ ...args: T
54
+ ) => void | (() => void) | ((...args: T) => Promise<void>) | Promise<void>;
55
+ export type EventHandlerType<SCG extends ExtendableGenerics = DefaultGenerics> = GenericEventHandlerType<[Event<SCG>]>;
56
+ export type EventHandlerOverrideType<SCG extends ExtendableGenerics = DefaultGenerics> = GenericEventHandlerType<
57
+ [ChannelSetterType<SCG>, Event<SCG>]
58
+ >;
59
+
60
+ export type ChannelManagerEventTypes =
61
+ | 'notification.added_to_channel'
62
+ | 'notification.message_new'
63
+ | 'notification.removed_from_channel'
64
+ | 'message.new'
65
+ | 'member.updated'
66
+ | 'channel.deleted'
67
+ | 'channel.hidden'
68
+ | 'channel.truncated'
69
+ | 'channel.visible';
70
+
71
+ export type ChannelManagerEventHandlerNames =
72
+ | 'channelDeletedHandler'
73
+ | 'channelHiddenHandler'
74
+ | 'channelTruncatedHandler'
75
+ | 'channelVisibleHandler'
76
+ | 'newMessageHandler'
77
+ | 'memberUpdatedHandler'
78
+ | 'notificationAddedToChannelHandler'
79
+ | 'notificationNewMessageHandler'
80
+ | 'notificationRemovedFromChannelHandler';
81
+
82
+ export type ChannelManagerEventHandlerOverrides<SCG extends ExtendableGenerics = DefaultGenerics> = Partial<
83
+ Record<ChannelManagerEventHandlerNames, EventHandlerOverrideType<SCG>>
84
+ >;
85
+
86
+ export const channelManagerEventToHandlerMapping: {
87
+ [key in ChannelManagerEventTypes]: ChannelManagerEventHandlerNames;
88
+ } = {
89
+ 'channel.deleted': 'channelDeletedHandler',
90
+ 'channel.hidden': 'channelHiddenHandler',
91
+ 'channel.truncated': 'channelTruncatedHandler',
92
+ 'channel.visible': 'channelVisibleHandler',
93
+ 'message.new': 'newMessageHandler',
94
+ 'member.updated': 'memberUpdatedHandler',
95
+ 'notification.added_to_channel': 'notificationAddedToChannelHandler',
96
+ 'notification.message_new': 'notificationNewMessageHandler',
97
+ 'notification.removed_from_channel': 'notificationRemovedFromChannelHandler',
98
+ };
99
+
100
+ export type ChannelManagerOptions = {
101
+ /**
102
+ * Aborts a channels query that is already in progress and runs the new one.
103
+ */
104
+ abortInFlightQuery?: boolean;
105
+ /**
106
+ * Allows channel promotion to be applied where applicable for channels that are
107
+ * currently not part of the channel list within the state. A good example of
108
+ * this would be a channel that is being watched and it receives a new message,
109
+ * but is not part of the list initially.
110
+ */
111
+ allowNotLoadedChannelPromotionForEvent?: {
112
+ 'channel.visible': boolean;
113
+ 'message.new': boolean;
114
+ 'notification.added_to_channel': boolean;
115
+ 'notification.message_new': boolean;
116
+ };
117
+ /**
118
+ * Allows us to lock the order of channels within the list. Any event that would
119
+ * change the order of channels within the list will do nothing.
120
+ */
121
+ lockChannelOrder?: boolean;
122
+ };
123
+
124
+ export const DEFAULT_CHANNEL_MANAGER_OPTIONS = {
125
+ abortInFlightQuery: false,
126
+ allowNotLoadedChannelPromotionForEvent: {
127
+ 'channel.visible': true,
128
+ 'message.new': true,
129
+ 'notification.added_to_channel': true,
130
+ 'notification.message_new': true,
131
+ },
132
+ lockChannelOrder: false,
133
+ };
134
+
135
+ export const DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS = {
136
+ limit: 10,
137
+ offset: 0,
138
+ };
139
+
140
+ /**
141
+ * A class that manages a list of channels and changes it based on configuration and WS events. The
142
+ * list of channels is reactive as well as the pagination and it can be subscribed to for state updates.
143
+ *
144
+ * @internal
145
+ */
146
+ export class ChannelManager<SCG extends ExtendableGenerics = DefaultGenerics> {
147
+ public readonly state: StateStore<ChannelManagerState<SCG>>;
148
+ private client: StreamChat<SCG>;
149
+ private unsubscribeFunctions: Set<() => void> = new Set();
150
+ private eventHandlers: Map<string, EventHandlerType<SCG>> = new Map();
151
+ private eventHandlerOverrides: Map<string, EventHandlerOverrideType<SCG>> = new Map();
152
+ private options: ChannelManagerOptions = {};
153
+ private stateOptions: ChannelStateOptions = {};
154
+
155
+ constructor({
156
+ client,
157
+ eventHandlerOverrides = {},
158
+ options = {},
159
+ }: {
160
+ client: StreamChat<SCG>;
161
+ eventHandlerOverrides?: ChannelManagerEventHandlerOverrides<SCG>;
162
+ options?: ChannelManagerOptions;
163
+ }) {
164
+ this.client = client;
165
+ this.state = new StateStore<ChannelManagerState<SCG>>({
166
+ channels: [],
167
+ pagination: {
168
+ isLoading: false,
169
+ isLoadingNext: false,
170
+ hasNext: false,
171
+ filters: {},
172
+ sort: {},
173
+ options: DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS,
174
+ },
175
+ initialized: false,
176
+ });
177
+ this.setEventHandlerOverrides(eventHandlerOverrides);
178
+ this.setOptions(options);
179
+ this.eventHandlers = new Map(
180
+ Object.entries<EventHandlerType<SCG>>({
181
+ channelDeletedHandler: this.channelDeletedHandler,
182
+ channelHiddenHandler: this.channelHiddenHandler,
183
+ channelVisibleHandler: this.channelVisibleHandler,
184
+ memberUpdatedHandler: this.memberUpdatedHandler,
185
+ newMessageHandler: this.newMessageHandler,
186
+ notificationAddedToChannelHandler: this.notificationAddedToChannelHandler,
187
+ notificationNewMessageHandler: this.notificationNewMessageHandler,
188
+ notificationRemovedFromChannelHandler: this.notificationRemovedFromChannelHandler,
189
+ }),
190
+ );
191
+ }
192
+
193
+ public setChannels = (valueOrFactory: ChannelSetterParameterType<SCG>) => {
194
+ this.state.next((current) => {
195
+ const { channels: currentChannels } = current;
196
+ const newChannels = isPatch(valueOrFactory) ? valueOrFactory(currentChannels) : valueOrFactory;
197
+
198
+ // If the references between the two values are the same, just return the
199
+ // current state; otherwise trigger a state change.
200
+ if (currentChannels === newChannels) {
201
+ return current;
202
+ }
203
+ return { ...current, channels: newChannels };
204
+ });
205
+ };
206
+
207
+ public setEventHandlerOverrides = (eventHandlerOverrides: ChannelManagerEventHandlerOverrides<SCG> = {}) => {
208
+ const truthyEventHandlerOverrides = Object.entries(eventHandlerOverrides).reduce<
209
+ Partial<ChannelManagerEventHandlerOverrides<SCG>>
210
+ >((acc, [key, value]) => {
211
+ if (value) {
212
+ acc[key as keyof ChannelManagerEventHandlerOverrides<SCG>] = value;
213
+ }
214
+ return acc;
215
+ }, {});
216
+ this.eventHandlerOverrides = new Map(Object.entries<EventHandlerOverrideType<SCG>>(truthyEventHandlerOverrides));
217
+ };
218
+
219
+ public setOptions = (options: ChannelManagerOptions = {}) => {
220
+ this.options = { ...DEFAULT_CHANNEL_MANAGER_OPTIONS, ...options };
221
+ };
222
+
223
+ public queryChannels = async (
224
+ filters: ChannelFilters<SCG>,
225
+ sort: ChannelSort<SCG> = [],
226
+ options: ChannelOptions = {},
227
+ stateOptions: ChannelStateOptions = {},
228
+ ) => {
229
+ const { offset, limit } = { ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, ...options };
230
+ const {
231
+ pagination: { isLoading },
232
+ } = this.state.getLatestValue();
233
+
234
+ if (isLoading && !this.options.abortInFlightQuery) {
235
+ return;
236
+ }
237
+
238
+ try {
239
+ this.stateOptions = stateOptions;
240
+ this.state.next((currentState) => ({
241
+ ...currentState,
242
+ pagination: {
243
+ ...currentState.pagination,
244
+ isLoading: true,
245
+ isLoadingNext: false,
246
+ filters,
247
+ sort,
248
+ options,
249
+ },
250
+ }));
251
+
252
+ const channels = await this.client.queryChannels(filters, sort, options, stateOptions);
253
+ const newOffset = offset + (channels?.length ?? 0);
254
+ const newOptions = { ...options, offset: newOffset };
255
+ const { pagination } = this.state.getLatestValue();
256
+
257
+ this.state.partialNext({
258
+ channels,
259
+ pagination: {
260
+ ...pagination,
261
+ hasNext: (channels?.length ?? 0) >= limit,
262
+ isLoading: false,
263
+ options: newOptions,
264
+ },
265
+ initialized: true,
266
+ });
267
+ } catch (error) {
268
+ this.client.logger('error', (error as Error).message);
269
+ this.state.next((currentState) => ({
270
+ ...currentState,
271
+ pagination: { ...currentState.pagination, isLoading: false },
272
+ }));
273
+ throw error;
274
+ }
275
+ };
276
+
277
+ public loadNext = async () => {
278
+ const { pagination, channels, initialized } = this.state.getLatestValue();
279
+ const { filters, sort, options, isLoadingNext, hasNext } = pagination;
280
+
281
+ if (!initialized || isLoadingNext || !hasNext) {
282
+ return;
283
+ }
284
+
285
+ try {
286
+ const { offset, limit } = { ...DEFAULT_CHANNEL_MANAGER_PAGINATION_OPTIONS, ...options };
287
+ this.state.partialNext({
288
+ pagination: { ...pagination, isLoading: false, isLoadingNext: true },
289
+ });
290
+ const nextChannels = await this.client.queryChannels(filters, sort, options, this.stateOptions);
291
+ const newOffset = offset + (nextChannels?.length ?? 0);
292
+ const newOptions = { ...options, offset: newOffset };
293
+
294
+ this.state.partialNext({
295
+ channels: [...(channels || []), ...nextChannels],
296
+ pagination: {
297
+ ...pagination,
298
+ hasNext: (nextChannels?.length ?? 0) >= limit,
299
+ isLoading: false,
300
+ isLoadingNext: false,
301
+ options: newOptions,
302
+ },
303
+ });
304
+ } catch (error) {
305
+ this.client.logger('error', (error as Error).message);
306
+ this.state.next((currentState) => ({
307
+ ...currentState,
308
+ pagination: { ...currentState.pagination, isLoadingNext: false },
309
+ }));
310
+ throw error;
311
+ }
312
+ };
313
+
314
+ private notificationAddedToChannelHandler = async (event: Event<SCG>) => {
315
+ const { id, type, members } = event?.channel ?? {};
316
+ if (!type || !this.options.allowNotLoadedChannelPromotionForEvent?.['notification.added_to_channel']) {
317
+ return;
318
+ }
319
+ const channel = await getAndWatchChannel({
320
+ client: this.client,
321
+ id,
322
+ members: members?.reduce<string[]>((acc, { user, user_id }) => {
323
+ const userId = user_id || user?.id;
324
+ if (userId) {
325
+ acc.push(userId);
326
+ }
327
+ return acc;
328
+ }, []),
329
+ type,
330
+ });
331
+ const { pagination, channels } = this.state.getLatestValue();
332
+ if (!channels) {
333
+ return;
334
+ }
335
+
336
+ const { sort } = pagination ?? {};
337
+
338
+ this.setChannels(
339
+ promoteChannel({
340
+ channels,
341
+ channelToMove: channel,
342
+ sort,
343
+ }),
344
+ );
345
+ };
346
+
347
+ private channelDeletedHandler = (event: Event<SCG>) => {
348
+ const { channels } = this.state.getLatestValue();
349
+ if (!channels) {
350
+ return;
351
+ }
352
+
353
+ const newChannels = [...channels];
354
+ const channelIndex = newChannels.findIndex((channel) => channel.cid === (event.cid || event.channel?.cid));
355
+
356
+ if (channelIndex < 0) {
357
+ return;
358
+ }
359
+
360
+ newChannels.splice(channelIndex, 1);
361
+ this.setChannels(newChannels);
362
+ };
363
+
364
+ private channelHiddenHandler = this.channelDeletedHandler;
365
+
366
+ private newMessageHandler = (event: Event<SCG>) => {
367
+ const { pagination, channels } = this.state.getLatestValue();
368
+ if (!channels) {
369
+ return;
370
+ }
371
+ const { filters, sort } = pagination ?? {};
372
+
373
+ const channelType = event.channel_type;
374
+ const channelId = event.channel_id;
375
+
376
+ if (!channelType || !channelId) {
377
+ return;
378
+ }
379
+
380
+ const targetChannel = this.client.channel(channelType, channelId);
381
+ const targetChannelIndex = channels.indexOf(targetChannel);
382
+ const targetChannelExistsWithinList = targetChannelIndex >= 0;
383
+
384
+ const isTargetChannelPinned = isChannelPinned(targetChannel);
385
+ const isTargetChannelArchived = isChannelArchived(targetChannel);
386
+
387
+ const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
388
+ const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
389
+
390
+ if (
391
+ // filter is defined, target channel is archived and filter option is set to false
392
+ (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
393
+ // filter is defined, target channel isn't archived and filter option is set to true
394
+ (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
395
+ // sort option is defined, target channel is pinned
396
+ (considerPinnedChannels && isTargetChannelPinned) ||
397
+ // list order is locked
398
+ this.options.lockChannelOrder ||
399
+ // target channel is not within the loaded list and loading from cache is disallowed
400
+ (!targetChannelExistsWithinList && !this.options.allowNotLoadedChannelPromotionForEvent?.['message.new'])
401
+ ) {
402
+ return;
403
+ }
404
+
405
+ this.setChannels(
406
+ promoteChannel({
407
+ channels,
408
+ channelToMove: targetChannel,
409
+ channelToMoveIndexWithinChannels: targetChannelIndex,
410
+ sort,
411
+ }),
412
+ );
413
+ };
414
+
415
+ private notificationNewMessageHandler = async (event: Event<SCG>) => {
416
+ const { id, type } = event?.channel ?? {};
417
+
418
+ const { channels, pagination } = this.state.getLatestValue();
419
+ const { filters, sort } = pagination ?? {};
420
+
421
+ if (!channels || !id || !type) {
422
+ return;
423
+ }
424
+
425
+ const channel = await getAndWatchChannel({
426
+ client: this.client,
427
+ id,
428
+ type,
429
+ });
430
+
431
+ const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
432
+ const isTargetChannelArchived = isChannelArchived(channel);
433
+
434
+ if (
435
+ (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
436
+ (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
437
+ !this.options.allowNotLoadedChannelPromotionForEvent?.['notification.message_new']
438
+ ) {
439
+ return;
440
+ }
441
+
442
+ this.setChannels(
443
+ promoteChannel({
444
+ channels,
445
+ channelToMove: channel,
446
+ sort,
447
+ }),
448
+ );
449
+ };
450
+
451
+ private channelVisibleHandler = async (event: Event<SCG>) => {
452
+ const { channels, pagination } = this.state.getLatestValue();
453
+ const { sort, filters } = pagination ?? {};
454
+ const { channel_type: channelType, channel_id: channelId } = event;
455
+
456
+ if (!channels || !channelType || !channelId) {
457
+ return;
458
+ }
459
+
460
+ const channel = await getAndWatchChannel({
461
+ client: this.client,
462
+ id: event.channel_id,
463
+ type: event.channel_type,
464
+ });
465
+
466
+ const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
467
+ const isTargetChannelArchived = isChannelArchived(channel);
468
+
469
+ if (
470
+ (considerArchivedChannels && isTargetChannelArchived && !filters.archived) ||
471
+ (considerArchivedChannels && !isTargetChannelArchived && filters.archived) ||
472
+ !this.options.allowNotLoadedChannelPromotionForEvent?.['channel.visible']
473
+ ) {
474
+ return;
475
+ }
476
+
477
+ this.setChannels(
478
+ promoteChannel({
479
+ channels,
480
+ channelToMove: channel,
481
+ sort,
482
+ }),
483
+ );
484
+ };
485
+
486
+ private notificationRemovedFromChannelHandler = this.channelDeletedHandler;
487
+
488
+ private memberUpdatedHandler = (event: Event<SCG>) => {
489
+ const { pagination, channels } = this.state.getLatestValue();
490
+ const { filters, sort } = pagination;
491
+ if (
492
+ !event.member?.user ||
493
+ event.member.user.id !== this.client.userID ||
494
+ !event.channel_type ||
495
+ !event.channel_id
496
+ ) {
497
+ return;
498
+ }
499
+ const channelType = event.channel_type;
500
+ const channelId = event.channel_id;
501
+
502
+ const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
503
+ const considerArchivedChannels = shouldConsiderArchivedChannels(filters);
504
+ const pinnedAtSort = extractSortValue({ atIndex: 0, sort, targetKey: 'pinned_at' });
505
+
506
+ if (!channels || (!considerPinnedChannels && !considerArchivedChannels) || this.options.lockChannelOrder) {
507
+ return;
508
+ }
509
+
510
+ const targetChannel = this.client.channel(channelType, channelId);
511
+ // assumes that channel instances are not changing
512
+ const targetChannelIndex = channels.indexOf(targetChannel);
513
+ const targetChannelExistsWithinList = targetChannelIndex >= 0;
514
+
515
+ const isTargetChannelPinned = isChannelPinned(targetChannel);
516
+ const isTargetChannelArchived = isChannelArchived(targetChannel);
517
+
518
+ const newChannels = [...channels];
519
+
520
+ if (targetChannelExistsWithinList) {
521
+ newChannels.splice(targetChannelIndex, 1);
522
+ }
523
+
524
+ // handle archiving (remove channel)
525
+ if (
526
+ // When archived filter true, and channel is unarchived
527
+ (considerArchivedChannels && !isTargetChannelArchived && filters?.archived) ||
528
+ // When archived filter false, and channel is archived
529
+ (considerArchivedChannels && isTargetChannelArchived && !filters?.archived)
530
+ ) {
531
+ this.setChannels(newChannels);
532
+ return;
533
+ }
534
+
535
+ // handle pinning
536
+ let lastPinnedChannelIndex: number | null = null;
537
+
538
+ if (pinnedAtSort === 1 || (pinnedAtSort === -1 && !isTargetChannelPinned)) {
539
+ lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
540
+ }
541
+ const newTargetChannelIndex = typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0;
542
+
543
+ // skip state update if the position of the channel does not change
544
+ if (channels[newTargetChannelIndex] === targetChannel) {
545
+ return;
546
+ }
547
+
548
+ newChannels.splice(newTargetChannelIndex, 0, targetChannel);
549
+ this.setChannels(newChannels);
550
+ };
551
+
552
+ private subscriptionOrOverride = (event: Event<SCG>) => {
553
+ const handlerName = channelManagerEventToHandlerMapping[event.type as ChannelManagerEventTypes];
554
+ const defaultEventHandler = this.eventHandlers.get(handlerName);
555
+ const eventHandlerOverride = this.eventHandlerOverrides.get(handlerName);
556
+ if (eventHandlerOverride && typeof eventHandlerOverride === 'function') {
557
+ eventHandlerOverride(this.setChannels, event);
558
+ return;
559
+ }
560
+
561
+ if (defaultEventHandler && typeof defaultEventHandler === 'function') {
562
+ defaultEventHandler(event);
563
+ }
564
+ };
565
+
566
+ public registerSubscriptions = () => {
567
+ if (this.unsubscribeFunctions.size) {
568
+ // Already listening for events and changes
569
+ return;
570
+ }
571
+
572
+ for (const eventType of Object.keys(channelManagerEventToHandlerMapping)) {
573
+ this.unsubscribeFunctions.add(this.client.on(eventType, this.subscriptionOrOverride).unsubscribe);
574
+ }
575
+ };
576
+
577
+ public unregisterSubscriptions = () => {
578
+ this.unsubscribeFunctions.forEach((cleanupFunction) => cleanupFunction());
579
+ this.unsubscribeFunctions.clear();
580
+ };
581
+ }
package/src/client.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  addFileToFormData,
19
19
  axiosParamsSerializer,
20
20
  chatCodes,
21
+ generateChannelTempCid,
21
22
  isFunction,
22
23
  isOnline,
23
24
  isOwnUserBaseProperty,
@@ -126,6 +127,7 @@ import {
126
127
  Mute,
127
128
  MuteUserOptions,
128
129
  MuteUserResponse,
130
+ NewMemberPayload,
129
131
  OGAttachment,
130
132
  OwnUserResponse,
131
133
  PartialMessageUpdate,
@@ -216,6 +218,7 @@ import { Moderation } from './moderation';
216
218
  import { ThreadManager } from './thread_manager';
217
219
  import { DEFAULT_QUERY_CHANNELS_MESSAGE_LIST_PAGE_SIZE } from './constants';
218
220
  import { PollManager } from './poll_manager';
221
+ import { ChannelManager, ChannelManagerEventHandlerOverrides, ChannelManagerOptions } from './channel_manager';
219
222
 
220
223
  function isString(x: unknown): x is string {
221
224
  return typeof x === 'string' || x instanceof String;
@@ -599,6 +602,24 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
599
602
  return Promise.resolve();
600
603
  };
601
604
 
605
+ /**
606
+ * Creates an instance of ChannelManager.
607
+ *
608
+ * @internal
609
+ *
610
+ * @param eventHandlerOverrides - the overrides for event handlers to be used
611
+ * @param options - the options used for the channel manager
612
+ */
613
+ createChannelManager = ({
614
+ eventHandlerOverrides = {},
615
+ options = {},
616
+ }: {
617
+ eventHandlerOverrides?: ChannelManagerEventHandlerOverrides<StreamChatGenerics>;
618
+ options?: ChannelManagerOptions;
619
+ }) => {
620
+ return new ChannelManager({ client: this, eventHandlerOverrides, options });
621
+ };
622
+
602
623
  /**
603
624
  * Creates a new WebSocket connection with the current user. Returns empty promise, if there is an active connection
604
625
  */
@@ -1935,10 +1956,13 @@ export class StreamChat<StreamChatGenerics extends ExtendableGenerics = DefaultG
1935
1956
  getChannelByMembers = (channelType: string, custom: ChannelData<StreamChatGenerics>) => {
1936
1957
  // Check if the channel already exists.
1937
1958
  // Only allow 1 channel object per cid
1938
- const membersStr = [...(custom.members || [])].sort().join(',');
1939
- const tempCid = `${channelType}:!members-${membersStr}`;
1959
+ const memberIds = (custom.members ?? []).map((member: string | NewMemberPayload<StreamChatGenerics>) =>
1960
+ typeof member === 'string' ? member : member.user_id ?? '',
1961
+ );
1962
+ const membersStr = memberIds.sort().join(',');
1963
+ const tempCid = generateChannelTempCid(channelType, memberIds);
1940
1964
 
1941
- if (!membersStr) {
1965
+ if (!tempCid) {
1942
1966
  throw Error('Please specify atleast one member when creating unique conversation');
1943
1967
  }
1944
1968
 
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ export * from './moderation';
11
11
  export * from './permissions';
12
12
  export * from './poll';
13
13
  export * from './poll_manager';
14
+ export * from './search_controller';
14
15
  export * from './segment';
15
16
  export * from './signing';
16
17
  export * from './store';
@@ -19,4 +20,5 @@ export type { ThreadState, ThreadReadState, ThreadRepliesPagination, ThreadUserR
19
20
  export * from './thread_manager';
20
21
  export * from './token_manager';
21
22
  export * from './types';
23
+ export * from './channel_manager';
22
24
  export { isOwnUser, chatCodes, logChatPromiseExecution, formatMessage } from './utils';