stream-chat 9.44.2 → 9.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/cjs/index.browser.js +3460 -2659
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +3469 -2659
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +3460 -2659
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/channel_state.d.ts +1 -1
  8. package/dist/types/client.d.ts +81 -3
  9. package/dist/types/constants.d.ts +1 -0
  10. package/dist/types/messageComposer/LocationComposer.d.ts +1 -1
  11. package/dist/types/messageComposer/configuration/commands.configuration.d.ts +6 -0
  12. package/dist/types/messageComposer/configuration/configuration.d.ts +1 -2
  13. package/dist/types/messageComposer/configuration/index.d.ts +4 -0
  14. package/dist/types/messageComposer/configuration/types.d.ts +21 -0
  15. package/dist/types/messageComposer/fileUtils.d.ts +1 -1
  16. package/dist/types/messageComposer/messageComposer.d.ts +6 -4
  17. package/dist/types/messageComposer/middleware/messageComposer/compositionValidation.d.ts +2 -1
  18. package/dist/types/messageComposer/middleware/messageComposer/textComposer.d.ts +1 -1
  19. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +10 -1
  20. package/dist/types/messageComposer/middleware/textComposer/mentionUtils.d.ts +8 -0
  21. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +77 -15
  22. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +51 -2
  23. package/dist/types/messageComposer/pollComposer.d.ts +2 -2
  24. package/dist/types/messageComposer/textComposer.d.ts +17 -3
  25. package/dist/types/pagination/UserGroupPaginator.d.ts +21 -0
  26. package/dist/types/pagination/index.d.ts +1 -0
  27. package/dist/types/types.d.ts +123 -2
  28. package/dist/types/utils.d.ts +2 -0
  29. package/package.json +38 -31
  30. package/src/client.ts +143 -2
  31. package/src/constants.ts +1 -0
  32. package/src/messageComposer/MessageComposerEffectHandlers.ts +1 -0
  33. package/src/messageComposer/configuration/commands.configuration.ts +55 -0
  34. package/src/messageComposer/configuration/configuration.ts +3 -1
  35. package/src/messageComposer/configuration/index.ts +4 -0
  36. package/src/messageComposer/configuration/types.ts +27 -0
  37. package/src/messageComposer/messageComposer.ts +73 -22
  38. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +23 -15
  39. package/src/messageComposer/middleware/messageComposer/textComposer.ts +151 -31
  40. package/src/messageComposer/middleware/textComposer/commandUtils.ts +68 -1
  41. package/src/messageComposer/middleware/textComposer/commands.ts +6 -2
  42. package/src/messageComposer/middleware/textComposer/mentionUtils.ts +33 -0
  43. package/src/messageComposer/middleware/textComposer/mentions.ts +596 -66
  44. package/src/messageComposer/middleware/textComposer/types.ts +70 -2
  45. package/src/messageComposer/textComposer.ts +154 -10
  46. package/src/pagination/UserGroupPaginator.ts +93 -0
  47. package/src/pagination/index.ts +1 -0
  48. package/src/permissions.ts +1 -0
  49. package/src/types.ts +152 -2
  50. package/src/utils.ts +1 -0
@@ -1,6 +1,8 @@
1
1
  import type { LinkPreview } from '../linkPreviewsManager';
2
2
  import type { FileUploadFilter } from '../attachmentManager';
3
+ import type { MessageComposer } from '../messageComposer';
3
4
  import type { FileLike, FileReference } from '../types';
5
+ import type { CommandResponse, UserResponse } from '../../types';
4
6
 
5
7
  export type MinimumUploadRequestResult = { file: string; thumb_url?: string } & Partial<
6
8
  Record<string, unknown>
@@ -38,6 +40,29 @@ export type TextComposerConfig = {
38
40
  maxLengthOnSend?: number;
39
41
  };
40
42
 
43
+ export type CommandSendability = {
44
+ command: CommandResponse;
45
+ ready: boolean;
46
+ reason?: string & {};
47
+ metadata?: Record<string, unknown>;
48
+ };
49
+
50
+ export type CommandSendValidationContext = {
51
+ command: CommandResponse;
52
+ composer: MessageComposer;
53
+ commandArgsText: string;
54
+ mentionedUsersInText: UserResponse[];
55
+ rawText: string;
56
+ };
57
+
58
+ export type CommandSendValidator = (
59
+ context: CommandSendValidationContext,
60
+ ) => CommandSendability | undefined;
61
+
62
+ export type CommandsConfig = {
63
+ sendValidator: CommandSendValidator;
64
+ };
65
+
41
66
  export type AttachmentManagerConfig = {
42
67
  // todo: document removal of noFiles prop showing how to achieve the same with custom fileUploadFilter function
43
68
  /**
@@ -86,6 +111,8 @@ export type LocationComposerConfig = {
86
111
  export type MessageComposerConfig = {
87
112
  /** If true, enables creating drafts on the server */
88
113
  drafts: DraftsConfiguration;
114
+ /** Configuration for command sendability validation */
115
+ commands: CommandsConfig;
89
116
  /** Configuration for the attachment manager */
90
117
  attachments: AttachmentManagerConfig;
91
118
  /** Configuration for the link previews manager */
@@ -5,7 +5,7 @@ import { LocationComposer } from './LocationComposer';
5
5
  import { MessageComposerEffectHandlers } from './MessageComposerEffectHandlers';
6
6
  import { PollComposer } from './pollComposer';
7
7
  import { TextComposer } from './textComposer';
8
- import { DEFAULT_COMPOSER_CONFIG } from './configuration';
8
+ import { applyCommandValidatorOverride, DEFAULT_COMPOSER_CONFIG } from './configuration';
9
9
  import type { MessageComposerMiddlewareValue } from './middleware';
10
10
  import {
11
11
  MessageComposerMiddlewareExecutor,
@@ -30,7 +30,7 @@ import type {
30
30
  } from '../types';
31
31
  import { WithSubscriptions } from '../utils/WithSubscriptions';
32
32
  import type { StreamChat } from '../client';
33
- import type { MessageComposerConfig } from './configuration/types';
33
+ import type { CommandSendability, MessageComposerConfig } from './configuration/types';
34
34
  import type {
35
35
  CommandSuggestionDisabledReason,
36
36
  TextComposerCommandActivationEffect,
@@ -44,6 +44,10 @@ import type { PollComposerSnapshot } from './pollComposer';
44
44
  import type { TextComposerSnapshot } from './textComposer';
45
45
  import type { DeepPartial } from '../types.utility';
46
46
  import type { MergeWithCustomizer } from '../utils/mergeWith/mergeWithCore';
47
+ import {
48
+ getMentionedUsersInText,
49
+ stripCommandFromText,
50
+ } from './middleware/textComposer/commandUtils';
47
51
 
48
52
  type UnregisterSubscriptions = Unsubscribe;
49
53
 
@@ -208,7 +212,16 @@ export class MessageComposer extends WithSubscriptions {
208
212
  );
209
213
  }
210
214
 
211
- const mergeChannelConfigCustomizer: MergeWithCustomizer<
215
+ /**
216
+ * Customizes config merges for the composer constructor.
217
+ *
218
+ * It catches two scalar override cases that should not use the default deep merge:
219
+ * - client-disabled `enabled` flags stay disabled even if the channel config tries to re-enable them
220
+ * - scalar channel-config values replace client defaults for matching config keys
221
+ *
222
+ * All other values fall back to the normal `mergeWith` behavior.
223
+ */
224
+ const mergeMessageComposerConfigCustomizer: MergeWithCustomizer<
212
225
  DeepPartial<MessageComposerConfig>
213
226
  > = (originalVal, channelConfigVal, key) =>
214
227
  typeof originalVal === 'object'
@@ -223,14 +236,17 @@ export class MessageComposer extends WithSubscriptions {
223
236
  : originalVal;
224
237
 
225
238
  this.configState = new StateStore<MessageComposerConfig>(
226
- mergeWith(
227
- mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
228
- {
229
- location: {
230
- enabled: this.channel.getConfig()?.shared_locations,
239
+ applyCommandValidatorOverride(
240
+ mergeWith(
241
+ mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
242
+ {
243
+ location: {
244
+ enabled: this.channel.getConfig()?.shared_locations,
245
+ },
231
246
  },
232
- },
233
- mergeChannelConfigCustomizer,
247
+ mergeMessageComposerConfigCustomizer,
248
+ ),
249
+ config,
234
250
  ),
235
251
  );
236
252
 
@@ -360,6 +376,14 @@ export class MessageComposer extends WithSubscriptions {
360
376
  return this.state.getLatestValue().quotedMessage;
361
377
  }
362
378
 
379
+ get pollId() {
380
+ return this.state.getLatestValue().pollId;
381
+ }
382
+
383
+ get showReplyInChannel() {
384
+ return this.state.getLatestValue().showReplyInChannel;
385
+ }
386
+
363
387
  getCommandDisabledReason = (
364
388
  command: CommandResponse,
365
389
  ): CommandSuggestionDisabledReason | undefined => {
@@ -378,21 +402,46 @@ export class MessageComposer extends WithSubscriptions {
378
402
  isCommandDisabled = (command: CommandResponse) =>
379
403
  !!this.getCommandDisabledReason(command);
380
404
 
381
- get pollId() {
382
- return this.state.getLatestValue().pollId;
383
- }
405
+ validateCommandSendability = (
406
+ command: CommandResponse,
407
+ text = this.textComposer.text,
408
+ ): CommandSendability => {
409
+ const currentMentionedUsers = this.textComposer.mentionedUsers;
410
+ const mentionedUsersInText = getMentionedUsersInText(text, currentMentionedUsers);
411
+
412
+ const validationContext = {
413
+ command,
414
+ commandArgsText: command.name
415
+ ? stripCommandFromText(text, command.name).trim()
416
+ : text.trim(),
417
+ composer: this,
418
+ mentionedUsersInText,
419
+ rawText: text,
420
+ };
384
421
 
385
- get showReplyInChannel() {
386
- return this.state.getLatestValue().showReplyInChannel;
422
+ const result = this.config.commands.sendValidator(validationContext);
423
+ if (result && !result.ready) {
424
+ return result;
425
+ }
426
+
427
+ return { command, ready: true };
428
+ };
429
+
430
+ get isCommandSendable() {
431
+ const currentCommand = this.textComposer.command;
432
+ return !currentCommand || this.validateCommandSendability(currentCommand).ready;
387
433
  }
388
434
 
389
435
  get hasSendableData() {
390
- return !!(
391
- (!this.attachmentManager.uploadsInProgressCount &&
392
- (!this.textComposer.textIsEmpty ||
393
- this.attachmentManager.successfulUploadsCount > 0)) ||
394
- this.pollId ||
395
- !!this.locationComposer.validLocation
436
+ return (
437
+ this.isCommandSendable &&
438
+ !!(
439
+ (!this.attachmentManager.uploadsInProgressCount &&
440
+ (!this.textComposer.textIsEmpty ||
441
+ this.attachmentManager.successfulUploadsCount > 0)) ||
442
+ this.pollId ||
443
+ !!this.locationComposer.validLocation
444
+ )
396
445
  );
397
446
  }
398
447
 
@@ -426,7 +475,9 @@ export class MessageComposer extends WithSubscriptions {
426
475
  }
427
476
 
428
477
  updateConfig(config: DeepPartial<MessageComposerConfig>) {
429
- this.configState.partialNext(mergeWith(this.config, config));
478
+ this.configState.partialNext(
479
+ applyCommandValidatorOverride(mergeWith(this.config, config), config),
480
+ );
430
481
  }
431
482
 
432
483
  refreshId = () => {
@@ -1,7 +1,12 @@
1
1
  import { textIsEmpty } from '../../textComposer';
2
2
  import type { CommandResponse } from '../../../types';
3
3
  import { CommandSearchSource } from '../textComposer/commands';
4
- import { getRawCommandName, notifyCommandDisabled } from '../textComposer/commandUtils';
4
+ import {
5
+ getCommandByName,
6
+ getRawCommandName,
7
+ notifyCommandDisabled,
8
+ notifyCommandNotReady,
9
+ } from '../textComposer/commandUtils';
5
10
  import type {
6
11
  MessageComposerMiddlewareState,
7
12
  MessageCompositionMiddleware,
@@ -11,18 +16,6 @@ import type {
11
16
  import type { MessageComposer } from '../../messageComposer';
12
17
  import type { MiddlewareHandlerParams } from '../../../middleware';
13
18
 
14
- const getCommandByName = (
15
- searchSource: CommandSearchSource,
16
- commandName?: string,
17
- ): CommandResponse | undefined => {
18
- if (!commandName) return;
19
-
20
- const normalizedCommandName = commandName.toLowerCase();
21
- return searchSource
22
- .query(normalizedCommandName)
23
- .items.find((command) => command.name?.toLowerCase() === normalizedCommandName);
24
- };
25
-
26
19
  const getDisabledRawCommand = (
27
20
  composer: MessageComposer,
28
21
  searchSource: CommandSearchSource,
@@ -36,8 +29,10 @@ const getDisabledRawCommand = (
36
29
 
37
30
  export const createCompositionValidationMiddleware = (
38
31
  composer: MessageComposer,
32
+ commandSearchSource?: CommandSearchSource,
39
33
  ): MessageCompositionMiddleware => {
40
- const commandSearchSource = new CommandSearchSource(composer.channel);
34
+ const effectiveCommandSearchSource =
35
+ commandSearchSource ?? new CommandSearchSource(composer.channel);
41
36
 
42
37
  return {
43
38
  id: 'stream-io/message-composer-middleware/data-validation',
@@ -52,7 +47,7 @@ export const createCompositionValidationMiddleware = (
52
47
 
53
48
  const disabledRawCommand = getDisabledRawCommand(
54
49
  composer,
55
- commandSearchSource,
50
+ effectiveCommandSearchSource,
56
51
  inputText,
57
52
  );
58
53
  if (disabledRawCommand) {
@@ -60,6 +55,19 @@ export const createCompositionValidationMiddleware = (
60
55
  return await discard();
61
56
  }
62
57
 
58
+ const currentCommand =
59
+ composer.textComposer.command ??
60
+ getCommandByName(effectiveCommandSearchSource, getRawCommandName(inputText));
61
+ if (
62
+ currentCommand &&
63
+ notifyCommandNotReady({
64
+ composer,
65
+ sendability: composer.validateCommandSendability(currentCommand, inputText),
66
+ })
67
+ ) {
68
+ return await discard();
69
+ }
70
+
63
71
  const hasExceededMaxLength =
64
72
  typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend;
65
73
 
@@ -1,11 +1,114 @@
1
+ import type { MiddlewareHandlerParams } from '../../../middleware';
2
+ import type { DraftMessage, LocalMessage, UserResponse } from '../../../types';
3
+ import type { MessageComposer } from '../../messageComposer';
4
+ import { mentionEntityToUserResponse } from '../textComposer/mentionUtils';
5
+ import type { MentionEntity } from '../textComposer/types';
1
6
  import type {
2
7
  MessageComposerMiddlewareState,
3
8
  MessageCompositionMiddleware,
4
9
  MessageDraftComposerMiddlewareValueState,
5
10
  MessageDraftCompositionMiddleware,
6
11
  } from './types';
7
- import type { MessageComposer } from '../../messageComposer';
8
- import type { MiddlewareHandlerParams } from '../../../middleware';
12
+
13
+ type MentionPayloadBase = Pick<
14
+ LocalMessage,
15
+ 'mentioned_channel' | 'mentioned_group_ids' | 'mentioned_here' | 'mentioned_roles'
16
+ >;
17
+
18
+ type MentionCompositionMetadata = Omit<
19
+ Required<MentionPayloadBase>,
20
+ 'mentioned_channel' | 'mentioned_here'
21
+ > & {
22
+ mentioned_channel: boolean;
23
+ mentioned_here: boolean;
24
+ mentioned_users: UserResponse[];
25
+ };
26
+
27
+ type BuildMentionCompositionMetadataParams = {
28
+ mentions: MentionEntity[];
29
+ text: string;
30
+ };
31
+
32
+ type DraftMentionPayload = Pick<
33
+ DraftMessage,
34
+ | 'mentioned_channel'
35
+ | 'mentioned_group_ids'
36
+ | 'mentioned_here'
37
+ | 'mentioned_roles'
38
+ | 'mentioned_users'
39
+ >;
40
+
41
+ const textIncludesMentionToken = (text: string, token: string) =>
42
+ text.includes(`@${token}`);
43
+ const isDefined = <TValue>(value: TValue | undefined): value is TValue =>
44
+ value !== undefined;
45
+
46
+ const getMentionEntityTextCandidates = (entity: MentionEntity) => {
47
+ if (entity.mentionType === 'channel') return ['channel'];
48
+ if (entity.mentionType === 'here') return ['here'];
49
+ if (entity.mentionType === 'user') return [entity.id, entity.name].filter(isDefined);
50
+ if (entity.mentionType === 'role') return [entity.name, entity.id].filter(isDefined);
51
+ if (entity.mentionType === 'user_group') {
52
+ return entity.name ? [entity.name, entity.id].filter(isDefined) : [];
53
+ }
54
+
55
+ return [];
56
+ };
57
+
58
+ const isMentionEntityPresentInText = (entity: MentionEntity, text: string) => {
59
+ const textCandidates = getMentionEntityTextCandidates(entity);
60
+ if (!textCandidates.length) return true;
61
+
62
+ return textCandidates.some((candidate) => textIncludesMentionToken(text, candidate));
63
+ };
64
+
65
+ const dedupeBy = <TItem, TKey extends string>(
66
+ items: TItem[],
67
+ getKey: (item: TItem) => TKey,
68
+ ) => {
69
+ const uniqueItems = new Map<TKey, TItem>();
70
+
71
+ items.forEach((item) => {
72
+ uniqueItems.set(getKey(item), item);
73
+ });
74
+
75
+ return [...uniqueItems.values()];
76
+ };
77
+
78
+ const buildMentionCompositionMetadata = ({
79
+ mentions,
80
+ text,
81
+ }: BuildMentionCompositionMetadataParams): MentionCompositionMetadata => {
82
+ const presentMentions = dedupeBy(
83
+ mentions.filter((entity) => isMentionEntityPresentInText(entity, text)),
84
+ (entity) => `${entity.mentionType}:${entity.id}`,
85
+ );
86
+
87
+ return presentMentions.reduce<MentionCompositionMetadata>(
88
+ (acc, entity) => {
89
+ if (entity.mentionType === 'user') {
90
+ acc.mentioned_users.push(mentionEntityToUserResponse(entity));
91
+ } else if (entity.mentionType === 'channel') {
92
+ acc.mentioned_channel = true;
93
+ } else if (entity.mentionType === 'here') {
94
+ acc.mentioned_here = true;
95
+ } else if (entity.mentionType === 'role') {
96
+ acc.mentioned_roles.push(entity.id);
97
+ } else if (entity.mentionType === 'user_group') {
98
+ acc.mentioned_group_ids.push(entity.id);
99
+ }
100
+
101
+ return acc;
102
+ },
103
+ {
104
+ mentioned_channel: false,
105
+ mentioned_group_ids: [],
106
+ mentioned_here: false,
107
+ mentioned_roles: [],
108
+ mentioned_users: [],
109
+ },
110
+ );
111
+ };
9
112
 
10
113
  export const createTextComposerCompositionMiddleware = (
11
114
  composer: MessageComposer,
@@ -18,30 +121,44 @@ export const createTextComposerCompositionMiddleware = (
18
121
  forward,
19
122
  }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
20
123
  if (!composer.textComposer) return forward();
21
- const { mentionedUsers, text } = composer.textComposer;
22
- // Instead of checking if a user is still mentioned every time the text changes,
23
- // just filter out non-mentioned users before submit, which is cheaper
24
- // and allows users to easily undo any accidental deletion
25
- const mentioned_users = Array.from(
26
- new Set(
27
- mentionedUsers.filter(
28
- ({ id, name }) => text.includes(`@${id}`) || text.includes(`@${name}`),
29
- ),
30
- ),
31
- );
32
-
33
- // prevent introducing text and mentioned_users array into the payload sent to the server
34
- if (!text && mentioned_users.length === 0) return forward();
124
+ const { mentions, text } = composer.textComposer;
125
+ const {
126
+ mentioned_channel,
127
+ mentioned_group_ids,
128
+ mentioned_here,
129
+ mentioned_roles,
130
+ mentioned_users,
131
+ } = buildMentionCompositionMetadata({ mentions, text });
132
+
133
+ // prevent introducing text and mention metadata into the payload sent to the server
134
+ if (
135
+ !text &&
136
+ !mentioned_channel &&
137
+ !mentioned_here &&
138
+ mentioned_group_ids.length === 0 &&
139
+ mentioned_roles.length === 0 &&
140
+ mentioned_users.length === 0
141
+ ) {
142
+ return forward();
143
+ }
35
144
 
36
145
  return next({
37
146
  ...state,
38
147
  localMessage: {
39
148
  ...state.localMessage,
149
+ mentioned_channel,
150
+ mentioned_group_ids,
151
+ mentioned_here,
152
+ mentioned_roles,
40
153
  mentioned_users,
41
154
  text,
42
155
  },
43
156
  message: {
44
157
  ...state.message,
158
+ mentioned_channel,
159
+ mentioned_group_ids,
160
+ mentioned_here,
161
+ mentioned_roles,
45
162
  mentioned_users: mentioned_users.map((u) => u.id),
46
163
  text,
47
164
  },
@@ -62,31 +179,34 @@ export const createDraftTextComposerCompositionMiddleware = (
62
179
  }: MiddlewareHandlerParams<MessageDraftComposerMiddlewareValueState>) => {
63
180
  if (!composer.textComposer) return forward();
64
181
  const { maxLengthOnSend } = composer.config.text ?? {};
65
- const { mentionedUsers, text: inputText } = composer.textComposer;
66
- // Instead of checking if a user is still mentioned every time the text changes,
67
- // just filter out non-mentioned users before submit, which is cheaper
68
- // and allows users to easily undo any accidental deletion
69
- const mentioned_users = mentionedUsers.length
70
- ? Array.from(
71
- new Set(
72
- mentionedUsers.filter(
73
- ({ id, name }) =>
74
- inputText.includes(`@${id}`) || inputText.includes(`@${name}`),
75
- ),
76
- ),
77
- )
78
- : undefined;
182
+ const { mentions, text: inputText } = composer.textComposer;
79
183
 
80
184
  const text =
81
185
  typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend
82
186
  ? inputText.slice(0, maxLengthOnSend)
83
187
  : inputText;
188
+ const {
189
+ mentioned_channel,
190
+ mentioned_group_ids,
191
+ mentioned_here,
192
+ mentioned_roles,
193
+ mentioned_users,
194
+ } = buildMentionCompositionMetadata({ mentions, text });
195
+ const draftMentionPayload: DraftMentionPayload = {
196
+ ...(mentioned_channel ? { mentioned_channel: true } : {}),
197
+ ...(mentioned_group_ids.length ? { mentioned_group_ids } : {}),
198
+ ...(mentioned_here ? { mentioned_here: true } : {}),
199
+ ...(mentioned_roles.length ? { mentioned_roles } : {}),
200
+ ...(mentioned_users.length
201
+ ? { mentioned_users: mentioned_users.map((u) => u.id) }
202
+ : {}),
203
+ };
84
204
 
85
205
  return next({
86
206
  ...state,
87
207
  draft: {
88
208
  ...state.draft,
89
- mentioned_users: mentioned_users?.map((u) => u.id),
209
+ ...draftMentionPayload,
90
210
  text,
91
211
  },
92
212
  });
@@ -1,5 +1,7 @@
1
1
  import type { MessageComposer } from '../../messageComposer';
2
- import type { CommandResponse } from '../../../types';
2
+ import type { CommandResponse, UserResponse } from '../../../types';
3
+ import type { CommandSendability } from '../../configuration';
4
+ import type { CommandSearchSource } from './commands';
3
5
 
4
6
  export function escapeCommandRegExp(text: string) {
5
7
  return text.replace(/[-[\]{}()*+?.,/\\^$|#]/g, '\\$&');
@@ -19,6 +21,43 @@ export const getCompleteCommandInString = (text: string) => {
19
21
  export const stripCommandFromText = (text: string, commandName: string) =>
20
22
  text.replace(new RegExp(`^${escapeCommandRegExp(`/${commandName}`)}\\s*`), '');
21
23
 
24
+ export const stripMentionTokens = (
25
+ text: string,
26
+ mentionedUsersInText: UserResponse[],
27
+ trigger = '@',
28
+ ) =>
29
+ mentionedUsersInText.reduce((value, user) => {
30
+ let next = value.replace(`${trigger}${user.id}`, '');
31
+
32
+ if (user.name) {
33
+ next = next.replace(`${trigger}${user.name}`, '');
34
+ }
35
+
36
+ return next.trim();
37
+ }, text.trim());
38
+
39
+ export const getMentionedUsersInText = (text: string, mentionedUsers: UserResponse[]) =>
40
+ Array.from(
41
+ new Set(
42
+ mentionedUsers.filter(
43
+ ({ id, name }) =>
44
+ text.includes(`@${id}`) || (!!name && text.includes(`@${name}`)),
45
+ ),
46
+ ),
47
+ );
48
+
49
+ export const getCommandByName = (
50
+ searchSource: CommandSearchSource,
51
+ commandName?: string,
52
+ ): CommandResponse | undefined => {
53
+ if (!commandName) return;
54
+
55
+ const normalizedCommandName = commandName.toLowerCase();
56
+ return searchSource
57
+ .query(normalizedCommandName)
58
+ .items.find((command) => command.name?.toLowerCase() === normalizedCommandName);
59
+ };
60
+
22
61
  export const notifyCommandDisabled = (
23
62
  composer: MessageComposer,
24
63
  command: CommandResponse,
@@ -46,3 +85,31 @@ export const notifyCommandDisabled = (
46
85
 
47
86
  return true;
48
87
  };
88
+
89
+ export const notifyCommandNotReady = ({
90
+ composer,
91
+ sendability,
92
+ }: {
93
+ composer: MessageComposer;
94
+ sendability: CommandSendability;
95
+ }) => {
96
+ if (sendability.ready) return;
97
+
98
+ composer.client.notifications.addWarning({
99
+ message: 'Command not ready to be sent',
100
+ origin: {
101
+ emitter: 'MessageComposer',
102
+ context: { command: sendability.command, composer },
103
+ },
104
+ options: {
105
+ type: 'validation:command:not-ready',
106
+ metadata: {
107
+ command: sendability.command.name,
108
+ ...(sendability.reason ? { reason: sendability.reason } : {}),
109
+ ...(sendability.metadata ?? {}),
110
+ },
111
+ },
112
+ });
113
+
114
+ return true;
115
+ };
@@ -6,7 +6,11 @@ import type { CommandResponse } from '../../../types';
6
6
  import { mergeWith } from '../../../utils/mergeWith';
7
7
  import type { MessageComposer } from '../../messageComposer';
8
8
  import type { CommandSuggestion, TextComposerMiddlewareOptions } from './types';
9
- import { getCompleteCommandInString, notifyCommandDisabled } from './commandUtils';
9
+ import {
10
+ getCommandByName,
11
+ getCompleteCommandInString,
12
+ notifyCommandDisabled,
13
+ } from './commandUtils';
10
14
  import { getTriggerCharWithToken, insertItemWithTrigger } from './textMiddlewareUtils';
11
15
  import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
12
16
 
@@ -124,7 +128,7 @@ export const createCommandsMiddleware = (
124
128
  const finalText = state.text.slice(0, state.selection.end);
125
129
  const commandName = getCompleteCommandInString(finalText);
126
130
  if (commandName) {
127
- const command = searchSource?.query(commandName).items[0];
131
+ const command = getCommandByName(searchSource, commandName);
128
132
  const composer = options?.composer;
129
133
  if (command && !composer?.isCommandDisabled(command)) {
130
134
  return next({
@@ -0,0 +1,33 @@
1
+ import type { UserResponse } from '../../../types';
2
+ import type { MentionEntity, UserMentionEntity, UserSuggestion } from './types';
3
+
4
+ export const isUserMentionEntity = (entity: MentionEntity): entity is UserMentionEntity =>
5
+ entity.mentionType === 'user';
6
+
7
+ export const userResponseToMentionEntity = (user: UserResponse): UserMentionEntity => ({
8
+ ...user,
9
+ mentionType: 'user',
10
+ });
11
+
12
+ export const userResponsesToMentionEntities = (users: UserResponse[]) =>
13
+ users.map(userResponseToMentionEntity);
14
+
15
+ export const mentionEntityToUserResponse = (entity: UserMentionEntity): UserResponse => {
16
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
+ const { mentionType, ...user } = entity;
18
+ return user;
19
+ };
20
+
21
+ export const userSuggestionToUserResponse = (
22
+ suggestion: UserSuggestion,
23
+ ): UserResponse => {
24
+ const { mentionType, tokenizedDisplayName, ...userResponse } = suggestion;
25
+ void mentionType;
26
+ void tokenizedDisplayName;
27
+ return userResponse;
28
+ };
29
+
30
+ export const userSuggestionToMentionEntity = (
31
+ suggestion: UserSuggestion,
32
+ ): UserMentionEntity =>
33
+ userResponseToMentionEntity(userSuggestionToUserResponse(suggestion));