stream-chat 9.15.0 → 9.17.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.
@@ -1,27 +1,66 @@
1
1
  import { BaseSearchSource } from './BaseSearchSource';
2
+ import type { FilterBuilderOptions } from '../pagination';
3
+ import { FilterBuilder } from '../pagination';
2
4
  import type { Channel } from '../channel';
3
5
  import type { StreamChat } from '../client';
4
6
  import type { ChannelFilters, ChannelOptions, ChannelSort } from '../types';
5
7
  import type { SearchSourceOptions } from './types';
6
8
 
7
- export class ChannelSearchSource extends BaseSearchSource<Channel> {
9
+ type CustomContext = Record<string, unknown>;
10
+
11
+ export type ChannelSearchSourceFilterBuilderContext<
12
+ C extends CustomContext = CustomContext,
13
+ > = { searchQuery?: string } & C;
14
+
15
+ export class ChannelSearchSource<
16
+ TFilterContext extends CustomContext = CustomContext,
17
+ > extends BaseSearchSource<Channel> {
8
18
  readonly type = 'channels';
9
- private client: StreamChat;
19
+ client: StreamChat;
10
20
  filters: ChannelFilters | undefined;
11
21
  sort: ChannelSort | undefined;
12
22
  searchOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
23
+ filterBuilder: FilterBuilder<
24
+ ChannelFilters,
25
+ ChannelSearchSourceFilterBuilderContext<TFilterContext>
26
+ >;
13
27
 
14
- constructor(client: StreamChat, options?: SearchSourceOptions) {
28
+ constructor(
29
+ client: StreamChat,
30
+ options?: SearchSourceOptions,
31
+ filterBuilderOptions: FilterBuilderOptions<
32
+ ChannelFilters,
33
+ ChannelSearchSourceFilterBuilderContext<TFilterContext>
34
+ > = {},
35
+ ) {
15
36
  super(options);
16
37
  this.client = client;
38
+ this.filterBuilder = new FilterBuilder<
39
+ ChannelFilters,
40
+ ChannelSearchSourceFilterBuilderContext<TFilterContext>
41
+ >({
42
+ ...filterBuilderOptions,
43
+ initialFilterConfig: {
44
+ name: {
45
+ enabled: true,
46
+ generate: ({ searchQuery }) =>
47
+ searchQuery ? { name: { $autocomplete: searchQuery } } : null,
48
+ },
49
+ ...filterBuilderOptions.initialFilterConfig,
50
+ },
51
+ });
17
52
  }
18
53
 
19
54
  protected async query(searchQuery: string) {
20
- const filters = {
21
- members: { $in: [this.client.userID] },
22
- name: { $autocomplete: searchQuery },
23
- ...this.filters,
24
- } as ChannelFilters;
55
+ const filters = this.filterBuilder.buildFilters({
56
+ baseFilters: {
57
+ ...(this.client.userID ? { members: { $in: [this.client.userID] } } : {}),
58
+ ...this.filters,
59
+ },
60
+ context: { searchQuery } as Partial<
61
+ ChannelSearchSourceFilterBuilderContext<TFilterContext>
62
+ >,
63
+ });
25
64
  const sort = this.sort ?? {};
26
65
  const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
27
66
  const items = await this.client.queryChannels(filters, sort, options);
@@ -10,46 +10,161 @@ import type {
10
10
  } from '../types';
11
11
  import type { StreamChat } from '../client';
12
12
  import type { SearchSourceOptions } from './types';
13
+ import { FilterBuilder, type FilterBuilderOptions } from '../pagination';
13
14
 
14
- export class MessageSearchSource extends BaseSearchSource<MessageResponse> {
15
+ type CustomContext = Record<string, unknown>;
16
+
17
+ // Built-in contexts per builder
18
+ type BuiltInContexts = {
19
+ messageSearchChannel: { searchQuery?: string };
20
+ messageSearch: { searchQuery?: string };
21
+ channelQuery: { cids?: string[] };
22
+ };
23
+
24
+ // Merge Built-in with user-provided Custom context
25
+ type MergeContext<
26
+ B extends Record<string, unknown>,
27
+ C extends CustomContext | undefined,
28
+ > = B & (C extends object ? C : {});
29
+
30
+ // User can provide custom context for each builder
31
+ type MessageSearchSourceContexts = Partial<{
32
+ messageSearchChannelContext: Record<string, unknown>;
33
+ messageSearchContext: Record<string, unknown>;
34
+ channelQueryContext: Record<string, unknown>;
35
+ }>;
36
+
37
+ export type MessageSearchSourceFilterBuilderOptions<
38
+ TContexts extends MessageSearchSourceContexts = {},
39
+ > = Partial<{
40
+ messageSearchChannel: FilterBuilderOptions<
41
+ ChannelFilters,
42
+ MergeContext<
43
+ BuiltInContexts['messageSearchChannel'],
44
+ TContexts['messageSearchChannelContext']
45
+ >
46
+ >;
47
+ messageSearch: FilterBuilderOptions<
48
+ MessageFilters,
49
+ MergeContext<BuiltInContexts['messageSearch'], TContexts['messageSearchContext']>
50
+ >;
51
+ channelQuery: FilterBuilderOptions<
52
+ ChannelFilters,
53
+ MergeContext<BuiltInContexts['channelQuery'], TContexts['channelQueryContext']>
54
+ >;
55
+ }>;
56
+
57
+ export class MessageSearchSource<
58
+ TContexts extends MessageSearchSourceContexts = {},
59
+ > extends BaseSearchSource<MessageResponse> {
15
60
  readonly type = 'messages';
16
61
  private client: StreamChat;
62
+
17
63
  messageSearchChannelFilters: ChannelFilters | undefined;
18
64
  messageSearchFilters: MessageFilters | undefined;
19
65
  messageSearchSort: SearchMessageSort | undefined;
66
+
20
67
  channelQueryFilters: ChannelFilters | undefined;
21
68
  channelQuerySort: ChannelSort | undefined;
22
69
  channelQueryOptions: Omit<ChannelOptions, 'limit' | 'offset'> | undefined;
23
70
 
24
- constructor(client: StreamChat, options?: SearchSourceOptions) {
71
+ messageSearchChannelFilterBuilder: FilterBuilder<
72
+ ChannelFilters,
73
+ MergeContext<
74
+ BuiltInContexts['messageSearchChannel'],
75
+ TContexts['messageSearchChannelContext']
76
+ >
77
+ >;
78
+ messageSearchFilterBuilder: FilterBuilder<
79
+ MessageFilters,
80
+ MergeContext<BuiltInContexts['messageSearch'], TContexts['messageSearchContext']>
81
+ >;
82
+ channelQueryFilterBuilder: FilterBuilder<
83
+ ChannelFilters,
84
+ MergeContext<BuiltInContexts['channelQuery'], TContexts['channelQueryContext']>
85
+ >;
86
+
87
+ constructor(
88
+ client: StreamChat,
89
+ options?: SearchSourceOptions,
90
+ filterBuilderOptions?: MessageSearchSourceFilterBuilderOptions<TContexts>,
91
+ ) {
25
92
  super(options);
26
93
  this.client = client;
94
+
95
+ this.messageSearchChannelFilterBuilder = new FilterBuilder<
96
+ ChannelFilters,
97
+ MergeContext<
98
+ BuiltInContexts['messageSearchChannel'],
99
+ TContexts['messageSearchChannelContext']
100
+ >
101
+ >(filterBuilderOptions?.messageSearchChannel);
102
+
103
+ this.messageSearchFilterBuilder = new FilterBuilder<
104
+ MessageFilters,
105
+ MergeContext<BuiltInContexts['messageSearch'], TContexts['messageSearchContext']>
106
+ >({
107
+ ...filterBuilderOptions?.messageSearch,
108
+ initialFilterConfig: {
109
+ text: {
110
+ enabled: true,
111
+ generate: ({ searchQuery }) => (searchQuery ? { text: searchQuery } : null),
112
+ },
113
+ ...filterBuilderOptions?.messageSearch?.initialFilterConfig,
114
+ },
115
+ });
116
+
117
+ this.channelQueryFilterBuilder = new FilterBuilder<
118
+ ChannelFilters,
119
+ MergeContext<BuiltInContexts['channelQuery'], TContexts['channelQueryContext']>
120
+ >({
121
+ ...filterBuilderOptions?.channelQuery,
122
+ initialFilterConfig: {
123
+ cid: {
124
+ enabled: true,
125
+ generate: ({ cids }) => (cids ? { cid: { $in: cids } } : null),
126
+ },
127
+ ...filterBuilderOptions?.channelQuery?.initialFilterConfig,
128
+ },
129
+ });
27
130
  }
28
131
 
29
132
  protected async query(searchQuery: string) {
30
- if (!this.client.userID) return { items: [] };
133
+ if (!this.client.userID || !searchQuery || this.next === null) return { items: [] };
31
134
 
32
- const channelFilters: ChannelFilters = {
33
- members: { $in: [this.client.userID] },
34
- ...this.messageSearchChannelFilters,
35
- } as ChannelFilters;
135
+ const channelFilters = this.messageSearchChannelFilterBuilder.buildFilters({
136
+ baseFilters: {
137
+ ...(this.client.userID ? { members: { $in: [this.client.userID] } } : {}),
138
+ ...this.messageSearchChannelFilters,
139
+ },
140
+ context: { searchQuery } as Partial<
141
+ MergeContext<
142
+ BuiltInContexts['messageSearchChannel'],
143
+ TContexts['messageSearchChannelContext']
144
+ >
145
+ >,
146
+ });
36
147
 
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;
148
+ const messageFilters: MessageFilters = this.messageSearchFilterBuilder.buildFilters({
149
+ baseFilters: {
150
+ type: 'regular',
151
+ ...this.messageSearchFilters,
152
+ },
153
+ context: { searchQuery } as Partial<
154
+ MergeContext<BuiltInContexts['messageSearch'], TContexts['messageSearchContext']>
155
+ >,
156
+ });
42
157
 
43
158
  const sort: SearchMessageSort = {
44
159
  created_at: -1,
45
160
  ...this.messageSearchSort,
46
161
  };
47
162
 
48
- const options = {
163
+ const options: SearchOptions = {
49
164
  limit: this.pageSize,
50
165
  next: this.next,
51
166
  sort,
52
- } as SearchOptions;
167
+ };
53
168
 
54
169
  const { next, results } = await this.client.search(
55
170
  channelFilters,
@@ -62,15 +177,18 @@ export class MessageSearchSource extends BaseSearchSource<MessageResponse> {
62
177
  items.reduce((acc, message) => {
63
178
  if (message.cid && !this.client.activeChannels[message.cid]) acc.add(message.cid);
64
179
  return acc;
65
- }, new Set<string>()), // keep the cids unique
180
+ }, new Set<string>()),
66
181
  );
67
- const allChannelsLoadedLocally = cids.length === 0;
68
- if (!allChannelsLoadedLocally) {
182
+
183
+ if (cids.length > 0) {
184
+ const channelQueryFilters = this.channelQueryFilterBuilder.buildFilters({
185
+ baseFilters: this.channelQueryFilters,
186
+ context: { cids } as Partial<
187
+ MergeContext<BuiltInContexts['channelQuery'], TContexts['channelQueryContext']>
188
+ >,
189
+ });
69
190
  await this.client.queryChannels(
70
- {
71
- cid: { $in: cids },
72
- ...this.channelQueryFilters,
73
- } as ChannelFilters,
191
+ channelQueryFilters,
74
192
  {
75
193
  last_message_at: -1,
76
194
  ...this.channelQuerySort,
@@ -1,28 +1,65 @@
1
1
  import { BaseSearchSource } from './BaseSearchSource';
2
+ import { FilterBuilder, type FilterBuilderOptions } from '../pagination';
2
3
  import type { StreamChat } from '../client';
3
4
  import type { UserFilters, UserOptions, UserResponse, UserSort } from '../types';
4
5
  import type { SearchSourceOptions } from './types';
5
6
 
6
- export class UserSearchSource extends BaseSearchSource<UserResponse> {
7
+ type CustomContext = Record<string, unknown>;
8
+
9
+ export type UserSearchSourceFilterBuilderContext<
10
+ C extends CustomContext = CustomContext,
11
+ > = { searchQuery?: string } & C;
12
+
13
+ export class UserSearchSource<
14
+ TFilterContext extends CustomContext = CustomContext,
15
+ > extends BaseSearchSource<UserResponse> {
7
16
  readonly type = 'users';
8
- private client: StreamChat;
17
+ client: StreamChat;
9
18
  filters: UserFilters | undefined;
10
19
  sort: UserSort | undefined;
11
20
  searchOptions: Omit<UserOptions, 'limit' | 'offset'> | undefined;
21
+ filterBuilder: FilterBuilder<
22
+ UserFilters,
23
+ UserSearchSourceFilterBuilderContext<TFilterContext>
24
+ >;
12
25
 
13
- constructor(client: StreamChat, options?: SearchSourceOptions) {
26
+ constructor(
27
+ client: StreamChat,
28
+ options?: SearchSourceOptions,
29
+ filterBuilderOptions: FilterBuilderOptions<
30
+ UserFilters,
31
+ UserSearchSourceFilterBuilderContext<TFilterContext>
32
+ > = {},
33
+ ) {
14
34
  super(options);
15
35
  this.client = client;
36
+ this.filterBuilder = new FilterBuilder<
37
+ UserFilters,
38
+ UserSearchSourceFilterBuilderContext<TFilterContext>
39
+ >({
40
+ initialFilterConfig: {
41
+ $or: {
42
+ enabled: true,
43
+ generate: ({ searchQuery }) =>
44
+ searchQuery
45
+ ? {
46
+ $or: [
47
+ { id: { $autocomplete: searchQuery } },
48
+ { name: { $autocomplete: searchQuery } },
49
+ ],
50
+ }
51
+ : null,
52
+ },
53
+ },
54
+ ...filterBuilderOptions,
55
+ });
16
56
  }
17
57
 
18
58
  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;
59
+ const filters = this.filterBuilder.buildFilters({
60
+ baseFilters: this.filters,
61
+ context: { searchQuery } as UserSearchSourceFilterBuilderContext<TFilterContext>,
62
+ });
26
63
  const sort = { id: 1, ...this.sort } as UserSort;
27
64
  const options = { ...this.searchOptions, limit: this.pageSize, offset: this.offset };
28
65
  const { users } = await this.client.queryUsers(filters, sort, options);
@@ -1,6 +1,6 @@
1
1
  export * from './BaseSearchSource';
2
2
  export * from './SearchController';
3
- export { UserSearchSource } from './UserSearchSource';
4
- export { ChannelSearchSource } from './ChannelSearchSource';
5
- export { MessageSearchSource } from './MessageSearchSource';
3
+ export * from './UserSearchSource';
4
+ export * from './ChannelSearchSource';
5
+ export * from './MessageSearchSource';
6
6
  export * from './types';
package/src/types.ts CHANGED
@@ -287,6 +287,7 @@ export type ChannelResponse = CustomChannelData & {
287
287
  last_message_at?: string;
288
288
  member_count?: number;
289
289
  members?: ChannelMemberResponse[];
290
+ message_count?: number;
290
291
  muted?: boolean;
291
292
  mute_expires_at?: string;
292
293
  own_capabilities?: string[];
@@ -1445,6 +1446,8 @@ export type Event = CustomEventData & {
1445
1446
  ai_message?: string;
1446
1447
  ai_state?: AIState;
1447
1448
  channel?: ChannelResponse;
1449
+ channel_custom?: CustomChannelData;
1450
+ channel_member_count?: number;
1448
1451
  channel_id?: string;
1449
1452
  channel_type?: string;
1450
1453
  cid?: string;
@@ -2365,6 +2368,7 @@ export type ChannelConfigFields = {
2365
2368
  replies?: boolean;
2366
2369
  search?: boolean;
2367
2370
  shared_locations?: boolean;
2371
+ count_messages?: boolean; // Feature flag for message count
2368
2372
  typing_events?: boolean;
2369
2373
  uploads?: boolean;
2370
2374
  url_enrichment?: boolean;
package/src/utils.ts CHANGED
@@ -416,6 +416,81 @@ export const toUpdatedMessagePayload = (
416
416
  };
417
417
  };
418
418
 
419
+ export const toDeletedMessage = ({
420
+ message,
421
+ deletedAt,
422
+ hardDelete = false,
423
+ }: {
424
+ message: LocalMessage | LocalMessageBase;
425
+ deletedAt: LocalMessage['deleted_at'];
426
+ hardDelete: boolean;
427
+ }) => {
428
+ if (hardDelete) {
429
+ /**
430
+ * In case of hard delete, we need to strip down all text, html, attachments and all the custom properties on message
431
+ * The hard-deleted message is kept in the UI until the messages are re-queried
432
+ * FIXME: we are returning an object that does not match LocalMessage | LocalMessageBase
433
+ */
434
+ return {
435
+ attachments: [],
436
+ cid: message.cid,
437
+ created_at: message.created_at,
438
+ deleted_at: deletedAt,
439
+ id: message.id,
440
+ latest_reactions: [],
441
+ mentioned_users: [],
442
+ own_reactions: [],
443
+ parent_id: message.parent_id,
444
+ reply_count: message.reply_count,
445
+ status: message.status,
446
+ thread_participants: message.thread_participants,
447
+ type: 'deleted' as const,
448
+ updated_at: message.updated_at,
449
+ user: message.user,
450
+ };
451
+ } else {
452
+ return {
453
+ ...message,
454
+ attachments: [],
455
+ type: 'deleted',
456
+ deleted_at: deletedAt,
457
+ };
458
+ }
459
+ };
460
+
461
+ export const deleteUserMessages = ({
462
+ messages,
463
+ user,
464
+ hardDelete = false,
465
+ deletedAt,
466
+ }: {
467
+ messages: Array<LocalMessage>;
468
+ user: UserResponse;
469
+ hardDelete: boolean;
470
+ deletedAt: LocalMessage['deleted_at'];
471
+ }) => {
472
+ for (let i = 0; i < messages.length; i++) {
473
+ const message = messages[i];
474
+ if (message.user?.id === user.id) {
475
+ messages[i] =
476
+ message.type === 'deleted'
477
+ ? message
478
+ : (toDeletedMessage({ message, hardDelete, deletedAt }) as LocalMessage);
479
+ }
480
+
481
+ if (message.quoted_message?.user?.id === user.id) {
482
+ messages[i].quoted_message =
483
+ message.quoted_message.type === 'deleted'
484
+ ? message.quoted_message
485
+ : (toDeletedMessage({
486
+ message: messages[i].quoted_message as LocalMessageBase,
487
+ hardDelete,
488
+ deletedAt,
489
+ }) as LocalMessage);
490
+ }
491
+ }
492
+ };
493
+
419
494
  export const findIndexInSortedArray = <T, L>({
420
495
  needle,
421
496
  sortedArray,