stream-chat 9.42.3 → 9.43.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 (39) hide show
  1. package/dist/cjs/index.browser.js +1451 -1169
  2. package/dist/cjs/index.browser.js.map +3 -3
  3. package/dist/cjs/index.node.js +1452 -1171
  4. package/dist/cjs/index.node.js.map +3 -3
  5. package/dist/esm/index.mjs +1451 -1169
  6. package/dist/esm/index.mjs.map +3 -3
  7. package/dist/types/messageComposer/CustomDataManager.d.ts +3 -0
  8. package/dist/types/messageComposer/LocationComposer.d.ts +3 -0
  9. package/dist/types/messageComposer/MessageComposerEffectHandlers.d.ts +17 -0
  10. package/dist/types/messageComposer/attachmentManager.d.ts +7 -0
  11. package/dist/types/messageComposer/linkPreviewsManager.d.ts +4 -1
  12. package/dist/types/messageComposer/messageComposer.d.ts +39 -1
  13. package/dist/types/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.d.ts +2 -1
  14. package/dist/types/messageComposer/middleware/textComposer/commandEffects.d.ts +5 -0
  15. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +7 -0
  16. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -0
  17. package/dist/types/messageComposer/middleware/textComposer/index.d.ts +1 -0
  18. package/dist/types/messageComposer/middleware/textComposer/textMiddlewareUtils.d.ts +0 -34
  19. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +13 -0
  20. package/dist/types/messageComposer/pollComposer.d.ts +3 -0
  21. package/dist/types/messageComposer/textComposer.d.ts +5 -1
  22. package/package.json +1 -1
  23. package/src/messageComposer/CustomDataManager.ts +8 -0
  24. package/src/messageComposer/LocationComposer.ts +8 -0
  25. package/src/messageComposer/MessageComposerEffectHandlers.ts +87 -0
  26. package/src/messageComposer/attachmentManager.ts +55 -0
  27. package/src/messageComposer/linkPreviewsManager.ts +12 -3
  28. package/src/messageComposer/messageComposer.ts +107 -0
  29. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +58 -18
  30. package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +7 -1
  31. package/src/messageComposer/middleware/textComposer/commandEffects.ts +51 -0
  32. package/src/messageComposer/middleware/textComposer/commandStringExtraction.ts +1 -4
  33. package/src/messageComposer/middleware/textComposer/commandUtils.ts +48 -0
  34. package/src/messageComposer/middleware/textComposer/commands.ts +15 -7
  35. package/src/messageComposer/middleware/textComposer/index.ts +1 -0
  36. package/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +3 -46
  37. package/src/messageComposer/middleware/textComposer/types.ts +20 -0
  38. package/src/messageComposer/pollComposer.ts +8 -0
  39. package/src/messageComposer/textComposer.ts +54 -6
@@ -2,6 +2,7 @@ import { AttachmentManager } from './attachmentManager';
2
2
  import { CustomDataManager } from './CustomDataManager';
3
3
  import { LinkPreviewsManager } from './linkPreviewsManager';
4
4
  import { LocationComposer } from './LocationComposer';
5
+ import { MessageComposerEffectHandlers } from './MessageComposerEffectHandlers';
5
6
  import { PollComposer } from './pollComposer';
6
7
  import { TextComposer } from './textComposer';
7
8
  import { DEFAULT_COMPOSER_CONFIG } from './configuration';
@@ -18,6 +19,7 @@ import { Channel } from '../channel';
18
19
  import { Thread } from '../thread';
19
20
  import type {
20
21
  ChannelAPIResponse,
22
+ CommandResponse,
21
23
  DraftMessage,
22
24
  DraftResponse,
23
25
  EventTypes,
@@ -29,6 +31,17 @@ import type {
29
31
  import { WithSubscriptions } from '../utils/WithSubscriptions';
30
32
  import type { StreamChat } from '../client';
31
33
  import type { MessageComposerConfig } from './configuration/types';
34
+ import type {
35
+ CommandSuggestionDisabledReason,
36
+ TextComposerCommandActivationEffect,
37
+ TextComposerCommandClearEffect,
38
+ } from './middleware/textComposer/types';
39
+ import type { AttachmentManagerSnapshot } from './attachmentManager';
40
+ import type { CustomDataManagerSnapshot } from './CustomDataManager';
41
+ import type { LinkPreviewsManagerSnapshot } from './linkPreviewsManager';
42
+ import type { LocationComposerSnapshot } from './LocationComposer';
43
+ import type { PollComposerSnapshot } from './pollComposer';
44
+ import type { TextComposerSnapshot } from './textComposer';
32
45
  import type { DeepPartial } from '../types.utility';
33
46
  import type { MergeWithCustomizer } from '../utils/mergeWith/mergeWithCore';
34
47
 
@@ -40,6 +53,31 @@ export type EditingAuditState = {
40
53
  lastChange: LastComposerChange;
41
54
  };
42
55
 
56
+ export type BuiltInMessageComposerEffect =
57
+ | TextComposerCommandActivationEffect
58
+ | TextComposerCommandClearEffect;
59
+
60
+ export type CustomMessageComposerEffect = {
61
+ type: string & {};
62
+ } & Record<string, unknown>;
63
+
64
+ export type MessageComposerEffect =
65
+ | BuiltInMessageComposerEffect
66
+ | CustomMessageComposerEffect;
67
+
68
+ export type MessageComposerEffectHandler<
69
+ T extends { type: string } = MessageComposerEffect,
70
+ > = (effect: T, composer: MessageComposer) => void;
71
+
72
+ export type MessageComposerSnapshot = {
73
+ attachmentManager: AttachmentManagerSnapshot;
74
+ customDataManager: CustomDataManagerSnapshot;
75
+ linkPreviewsManager: LinkPreviewsManagerSnapshot;
76
+ locationComposer: LocationComposerSnapshot;
77
+ pollComposer: PollComposerSnapshot;
78
+ textComposer: TextComposerSnapshot;
79
+ };
80
+
43
81
  export type LocalMessageWithLegacyThreadId = LocalMessage & { legacyThreadId?: string };
44
82
  export type CompositionContext = Channel | Thread | LocalMessageWithLegacyThreadId;
45
83
 
@@ -142,6 +180,8 @@ export class MessageComposer extends WithSubscriptions {
142
180
  pollComposer: PollComposer;
143
181
  locationComposer: LocationComposer;
144
182
  customDataManager: CustomDataManager;
183
+ private snapshots: MessageComposerSnapshot[] = [];
184
+ private effectHandlers: MessageComposerEffectHandlers;
145
185
  // todo: mediaRecorder: MediaRecorderController;
146
186
 
147
187
  constructor({
@@ -219,6 +259,7 @@ export class MessageComposer extends WithSubscriptions {
219
259
  this.draftCompositionMiddlewareExecutor = new MessageDraftComposerMiddlewareExecutor({
220
260
  composer: this,
221
261
  });
262
+ this.effectHandlers = new MessageComposerEffectHandlers({ composer: this });
222
263
  }
223
264
 
224
265
  static evaluateContextType(compositionContext: CompositionContext) {
@@ -259,6 +300,9 @@ export class MessageComposer extends WithSubscriptions {
259
300
 
260
301
  setEditedMessage = (editedMessage: LocalMessage | null | undefined) => {
261
302
  this.state.partialNext({ editedMessage: editedMessage ?? null });
303
+ if (editedMessage) {
304
+ this.textComposer.clearCommand();
305
+ }
262
306
  };
263
307
 
264
308
  get contextType() {
@@ -316,6 +360,24 @@ export class MessageComposer extends WithSubscriptions {
316
360
  return this.state.getLatestValue().quotedMessage;
317
361
  }
318
362
 
363
+ getCommandDisabledReason = (
364
+ command: CommandResponse,
365
+ ): CommandSuggestionDisabledReason | undefined => {
366
+ if (this.editedMessage) return 'editing';
367
+
368
+ if (
369
+ this.quotedMessage &&
370
+ (command.set === 'moderation_set' || command.name === 'moderation_set')
371
+ ) {
372
+ return 'quoted_message';
373
+ }
374
+
375
+ return undefined;
376
+ };
377
+
378
+ isCommandDisabled = (command: CommandResponse) =>
379
+ !!this.getCommandDisabledReason(command);
380
+
319
381
  get pollId() {
320
382
  return this.state.getLatestValue().pollId;
321
383
  }
@@ -374,6 +436,7 @@ export class MessageComposer extends WithSubscriptions {
374
436
  initState = ({
375
437
  composition,
376
438
  }: { composition?: DraftResponse | MessageResponse | LocalMessage } = {}) => {
439
+ this.clearSnapshots();
377
440
  this.editingAuditState.partialNext(this.initEditingAuditState(composition));
378
441
 
379
442
  const message: LocalMessage | DraftMessage | undefined =
@@ -414,6 +477,46 @@ export class MessageComposer extends WithSubscriptions {
414
477
  composition?: DraftResponse | MessageResponse | LocalMessage,
415
478
  ) => initEditingAuditState(composition);
416
479
 
480
+ clearSnapshots = () => {
481
+ this.snapshots = [];
482
+ };
483
+
484
+ getSnapshot = (): MessageComposerSnapshot => ({
485
+ attachmentManager: this.attachmentManager.getSnapshot(),
486
+ customDataManager: this.customDataManager.getSnapshot(),
487
+ linkPreviewsManager: this.linkPreviewsManager.getSnapshot(),
488
+ locationComposer: this.locationComposer.getSnapshot(),
489
+ pollComposer: this.pollComposer.getSnapshot(),
490
+ textComposer: this.textComposer.getSnapshot(),
491
+ });
492
+
493
+ restoreSnapshot = (snapshot: MessageComposerSnapshot) => {
494
+ this.attachmentManager.restoreSnapshot(snapshot.attachmentManager);
495
+ this.linkPreviewsManager.restoreSnapshot(snapshot.linkPreviewsManager);
496
+ this.locationComposer.restoreSnapshot(snapshot.locationComposer);
497
+ this.pollComposer.restoreSnapshot(snapshot.pollComposer);
498
+ this.customDataManager.restoreSnapshot(snapshot.customDataManager);
499
+ this.textComposer.restoreSnapshot(snapshot.textComposer);
500
+ };
501
+
502
+ captureSnapshot = (snapshot = this.getSnapshot()) => {
503
+ if (this.snapshots.length) return;
504
+ this.snapshots.push(snapshot);
505
+ };
506
+
507
+ popSnapshot = () => this.snapshots.pop();
508
+
509
+ registerEffectHandler = <T extends { type: string }>(
510
+ type: T['type'],
511
+ handler: MessageComposerEffectHandler<T>,
512
+ ): void => {
513
+ this.effectHandlers.registerEffectHandler(type, handler);
514
+ };
515
+
516
+ applyEffects = <T extends { type: string }>(effects: T[] = []) => {
517
+ this.effectHandlers.applyEffects(effects);
518
+ };
519
+
417
520
  private logStateUpdateTimestamp() {
418
521
  this.editingAuditState.partialNext({
419
522
  lastChange: { ...this.lastChange, stateUpdate: new Date().getTime() },
@@ -671,6 +774,10 @@ export class MessageComposer extends WithSubscriptions {
671
774
 
672
775
  setQuotedMessage = (quotedMessage: LocalMessage | null) => {
673
776
  this.state.partialNext({ quotedMessage });
777
+ const activeCommand = this.textComposer.command;
778
+ if (quotedMessage && activeCommand && this.isCommandDisabled(activeCommand)) {
779
+ this.textComposer.clearCommand();
780
+ }
674
781
  };
675
782
 
676
783
  toggleShowReplyInChannel = () => {
@@ -1,4 +1,7 @@
1
1
  import { textIsEmpty } from '../../textComposer';
2
+ import type { CommandResponse } from '../../../types';
3
+ import { CommandSearchSource } from '../textComposer/commands';
4
+ import { getRawCommandName, notifyCommandDisabled } from '../textComposer/commandUtils';
2
5
  import type {
3
6
  MessageComposerMiddlewareState,
4
7
  MessageCompositionMiddleware,
@@ -8,30 +11,67 @@ import type {
8
11
  import type { MessageComposer } from '../../messageComposer';
9
12
  import type { MiddlewareHandlerParams } from '../../../middleware';
10
13
 
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
+ const getDisabledRawCommand = (
27
+ composer: MessageComposer,
28
+ searchSource: CommandSearchSource,
29
+ text?: string,
30
+ ): CommandResponse | undefined => {
31
+ const rawCommand = getCommandByName(searchSource, getRawCommandName(text));
32
+ if (rawCommand && composer.isCommandDisabled(rawCommand)) {
33
+ return rawCommand;
34
+ }
35
+ };
36
+
11
37
  export const createCompositionValidationMiddleware = (
12
38
  composer: MessageComposer,
13
- ): MessageCompositionMiddleware => ({
14
- id: 'stream-io/message-composer-middleware/data-validation',
15
- handlers: {
16
- compose: async ({
17
- state,
18
- discard,
19
- forward,
20
- }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
21
- const { maxLengthOnSend } = composer.config.text ?? {};
22
- const inputText = state.message.text ?? '';
39
+ ): MessageCompositionMiddleware => {
40
+ const commandSearchSource = new CommandSearchSource(composer.channel);
23
41
 
24
- const hasExceededMaxLength =
25
- typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend;
42
+ return {
43
+ id: 'stream-io/message-composer-middleware/data-validation',
44
+ handlers: {
45
+ compose: async ({
46
+ state,
47
+ discard,
48
+ forward,
49
+ }: MiddlewareHandlerParams<MessageComposerMiddlewareState>) => {
50
+ const { maxLengthOnSend } = composer.config.text ?? {};
51
+ const inputText = state.message.text ?? '';
26
52
 
27
- if (composer.compositionIsEmpty || hasExceededMaxLength) {
28
- return await discard();
29
- }
53
+ const disabledRawCommand = getDisabledRawCommand(
54
+ composer,
55
+ commandSearchSource,
56
+ inputText,
57
+ );
58
+ if (disabledRawCommand) {
59
+ notifyCommandDisabled(composer, disabledRawCommand);
60
+ return await discard();
61
+ }
30
62
 
31
- return await forward();
63
+ const hasExceededMaxLength =
64
+ typeof maxLengthOnSend === 'number' && inputText.length > maxLengthOnSend;
65
+
66
+ if (composer.compositionIsEmpty || hasExceededMaxLength) {
67
+ return await discard();
68
+ }
69
+
70
+ return await forward();
71
+ },
32
72
  },
33
- },
34
- });
73
+ };
74
+ };
35
75
 
36
76
  export const createDraftCompositionValidationMiddleware = (
37
77
  composer: MessageComposer,
@@ -1,4 +1,5 @@
1
1
  import { createCommandsMiddleware } from './commands';
2
+ import { createCommandEffectsMiddleware } from './commandEffects';
2
3
  import { createMentionsMiddleware } from './mentions';
3
4
  import { createTextComposerPreValidationMiddleware } from './validation';
4
5
  import { MiddlewareExecutor } from '../../../middleware';
@@ -10,6 +11,7 @@ import type {
10
11
  import type {
11
12
  Suggestion,
12
13
  Suggestions,
14
+ TextComposerEffect,
13
15
  TextComposerMiddlewareExecutorOptions,
14
16
  TextComposerState,
15
17
  } from './types';
@@ -19,6 +21,7 @@ export type TextComposerMiddlewareExecutorState<T extends Suggestion = Suggestio
19
21
  change?: {
20
22
  selectedSuggestion?: T;
21
23
  };
24
+ effects?: TextComposerEffect[];
22
25
  };
23
26
 
24
27
  export type TextComposerHandlerNames = 'onChange' | 'onSuggestionItemSelect';
@@ -43,7 +46,10 @@ export class TextComposerMiddlewareExecutor<
43
46
  this.use([
44
47
  createTextComposerPreValidationMiddleware(composer) as TextComposerMiddleware<T>,
45
48
  createMentionsMiddleware(composer.channel) as TextComposerMiddleware<T>,
46
- createCommandsMiddleware(composer.channel) as TextComposerMiddleware<T>,
49
+ createCommandsMiddleware(composer.channel, {
50
+ composer,
51
+ }) as TextComposerMiddleware<T>,
52
+ createCommandEffectsMiddleware() as TextComposerMiddleware<T>,
47
53
  ]);
48
54
  }
49
55
 
@@ -0,0 +1,51 @@
1
+ import type { Middleware } from '../../../middleware';
2
+ import type { CommandResponse } from '../../../types';
3
+ import type {
4
+ CommandSuggestion,
5
+ TextComposerCommandActivationEffect,
6
+ TextComposerCommandActivationStateToRestore,
7
+ } from './types';
8
+ import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
9
+
10
+ export type CommandEffectsMiddleware = Middleware<
11
+ TextComposerMiddlewareExecutorState<CommandSuggestion>,
12
+ 'onChange' | 'onSuggestionItemSelect'
13
+ >;
14
+
15
+ const emptyCommandStateToRestore: TextComposerCommandActivationStateToRestore = {
16
+ selection: { start: 0, end: 0 },
17
+ text: '',
18
+ };
19
+
20
+ const createCommandActivationEffect = (
21
+ command: CommandResponse,
22
+ ): TextComposerCommandActivationEffect => ({
23
+ command,
24
+ stateToRestore: emptyCommandStateToRestore,
25
+ type: 'command.activate',
26
+ });
27
+
28
+ const isCommandResponse = (suggestion: unknown): suggestion is CommandSuggestion =>
29
+ typeof (suggestion as CommandSuggestion | undefined)?.name === 'string';
30
+
31
+ export const createCommandEffectsMiddleware = (): CommandEffectsMiddleware => ({
32
+ handlers: {
33
+ onChange: ({ forward }) => forward(),
34
+ onSuggestionItemSelect: ({ state, next, forward }) => {
35
+ const { selectedSuggestion } = state.change ?? {};
36
+ if (
37
+ !isCommandResponse(selectedSuggestion) ||
38
+ !state.command ||
39
+ state.command.name !== selectedSuggestion.name
40
+ ) {
41
+ return forward();
42
+ }
43
+
44
+ return next({
45
+ ...state,
46
+ effects: [...(state.effects ?? []), createCommandActivationEffect(state.command)],
47
+ });
48
+ },
49
+ },
50
+ id: 'stream-io/text-composer/command-effects-middleware',
51
+ });
@@ -1,16 +1,13 @@
1
1
  import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
2
2
  import type { CommandSuggestion } from './types';
3
3
  import type { Middleware } from '../../../middleware';
4
- import { escapeRegExp } from './textMiddlewareUtils';
4
+ import { stripCommandFromText } from './commandUtils';
5
5
 
6
6
  export type CommandStringExtractionMiddleware = Middleware<
7
7
  TextComposerMiddlewareExecutorState<CommandSuggestion>,
8
8
  'onChange' | 'onSuggestionItemSelect'
9
9
  >;
10
10
 
11
- const stripCommandFromText = (text: string, commandName: string) =>
12
- text.replace(new RegExp(`^${escapeRegExp(`/${commandName}`)}\\s*`), '');
13
-
14
11
  export const createCommandStringExtractionMiddleware =
15
12
  (): CommandStringExtractionMiddleware => ({
16
13
  handlers: {
@@ -0,0 +1,48 @@
1
+ import type { MessageComposer } from '../../messageComposer';
2
+ import type { CommandResponse } from '../../../types';
3
+
4
+ export function escapeCommandRegExp(text: string) {
5
+ return text.replace(/[-[\]{}()*+?.,/\\^$|#]/g, '\\$&');
6
+ }
7
+
8
+ export const getRawCommandName = (text?: string) =>
9
+ text?.match(/^\/(\S+)(?:\s.*)?$/)?.[1];
10
+
11
+ export const getCompleteCommandInString = (text: string) => {
12
+ // starts with "/" followed by 1+ non-whitespace chars followed by 1+ white-space chars
13
+ // the command name is extracted into a separate group
14
+ const match = text.match(/^\/(\S+)\s+.*/);
15
+ const commandName = match && match[1];
16
+ return commandName;
17
+ };
18
+
19
+ export const stripCommandFromText = (text: string, commandName: string) =>
20
+ text.replace(new RegExp(`^${escapeCommandRegExp(`/${commandName}`)}\\s*`), '');
21
+
22
+ export const notifyCommandDisabled = (
23
+ composer: MessageComposer,
24
+ command: CommandResponse,
25
+ ) => {
26
+ const disabledReason = composer.getCommandDisabledReason(command);
27
+ if (!disabledReason) return;
28
+
29
+ composer.client.notifications.addWarning({
30
+ message:
31
+ disabledReason === 'editing'
32
+ ? 'Command not available while editing'
33
+ : 'Command not available while replying',
34
+ origin: {
35
+ emitter: 'MessageComposer',
36
+ context: { command, composer },
37
+ },
38
+ options: {
39
+ type: 'validation:command:disabled',
40
+ metadata: {
41
+ command: command.name,
42
+ reason: disabledReason,
43
+ },
44
+ },
45
+ });
46
+
47
+ return true;
48
+ };
@@ -4,12 +4,10 @@ import type { SearchSourceOptions } from '../../../search';
4
4
  import { BaseSearchSourceSync } from '../../../search';
5
5
  import type { CommandResponse } from '../../../types';
6
6
  import { mergeWith } from '../../../utils/mergeWith';
7
+ import type { MessageComposer } from '../../messageComposer';
7
8
  import type { CommandSuggestion, TextComposerMiddlewareOptions } from './types';
8
- import {
9
- getCompleteCommandInString,
10
- getTriggerCharWithToken,
11
- insertItemWithTrigger,
12
- } from './textMiddlewareUtils';
9
+ import { getCompleteCommandInString, notifyCommandDisabled } from './commandUtils';
10
+ import { getTriggerCharWithToken, insertItemWithTrigger } from './textMiddlewareUtils';
13
11
  import type { TextComposerMiddlewareExecutorState } from './TextComposerMiddlewareExecutor';
14
12
 
15
13
  export class CommandSearchSource extends BaseSearchSourceSync<CommandSuggestion> {
@@ -70,7 +68,10 @@ export class CommandSearchSource extends BaseSearchSourceSync<CommandSuggestion>
70
68
  });
71
69
 
72
70
  return {
73
- items: selectedCommands.map((c) => ({ ...c, id: c.name })),
71
+ items: selectedCommands.map((command) => ({
72
+ ...command,
73
+ id: command.name,
74
+ })),
74
75
  next: null,
75
76
  };
76
77
  }
@@ -103,6 +104,7 @@ export type CommandsMiddleware = Middleware<
103
104
  export const createCommandsMiddleware = (
104
105
  channel: Channel,
105
106
  options?: Partial<TextComposerMiddlewareOptions> & {
107
+ composer?: MessageComposer;
106
108
  searchSource?: CommandSearchSource;
107
109
  },
108
110
  ): CommandsMiddleware => {
@@ -123,7 +125,8 @@ export const createCommandsMiddleware = (
123
125
  const commandName = getCompleteCommandInString(finalText);
124
126
  if (commandName) {
125
127
  const command = searchSource?.query(commandName).items[0];
126
- if (command) {
128
+ const composer = options?.composer;
129
+ if (command && !composer?.isCommandDisabled(command)) {
127
130
  return next({
128
131
  ...state,
129
132
  command,
@@ -173,6 +176,11 @@ export const createCommandsMiddleware = (
173
176
  if (!selectedSuggestion || state.suggestions?.trigger !== finalOptions.trigger)
174
177
  return forward();
175
178
 
179
+ const composer = options?.composer;
180
+ if (composer && notifyCommandDisabled(composer, selectedSuggestion)) {
181
+ return forward();
182
+ }
183
+
176
184
  searchSource.resetStateAndActivate();
177
185
  return next({
178
186
  ...state,
@@ -1,5 +1,6 @@
1
1
  export * from './activeCommandGuard';
2
2
  export * from './commands';
3
+ export * from './commandEffects';
3
4
  export * from './commandStringExtraction';
4
5
  export * from './mentions';
5
6
  export * from './validation';
@@ -1,4 +1,5 @@
1
1
  import type { TextSelection } from './types';
2
+ import { escapeCommandRegExp } from './commandUtils';
2
3
 
3
4
  /**
4
5
  * For commands, we want to match all patterns except:
@@ -16,7 +17,7 @@ export const getTriggerCharWithToken = ({
16
17
  isCommand?: boolean;
17
18
  acceptTrailingSpaces?: boolean;
18
19
  }) => {
19
- const escapedTrigger = escapeRegExp(trigger);
20
+ const escapedTrigger = escapeCommandRegExp(trigger);
20
21
  const triggerNorWhitespace = `[^\\s${escapedTrigger}]*`;
21
22
 
22
23
  const match = text.match(
@@ -33,14 +34,6 @@ export const getTriggerCharWithToken = ({
33
34
  return match && match[match.length - 1].trim();
34
35
  };
35
36
 
36
- export const getCompleteCommandInString = (text: string) => {
37
- // starts with "/" followed by 1+ non-whitespace chars followed by 1+ white-space chars
38
- // the comand name is extracted into a separate group
39
- const match = text.match(/^\/(\S+)\s+.*/);
40
- const commandName = match && match[1];
41
- return commandName;
42
- };
43
-
44
37
  export const insertItemWithTrigger = ({
45
38
  insertText,
46
39
  selection,
@@ -93,42 +86,6 @@ export const replaceWordWithEntity = async ({
93
86
  return textBeforeWord + newWord + spaces + textAfterCaret;
94
87
  };
95
88
 
96
- /**
97
- * Escapes a string for use in a regular expression
98
- * @param text - The string to escape
99
- * @returns The escaped string
100
- * What does this regex do?
101
-
102
- The regex escapes special regex characters by adding a backslash before them. Here's what it matches:
103
- - dash
104
- [ ] square brackets
105
- { } curly braces
106
- ( ) parentheses
107
- * asterisk
108
- + plus
109
- ? question mark
110
- . period
111
- , comma
112
- / forward slash
113
- \ backslash
114
- ^ caret
115
- $ dollar sign
116
- | pipe
117
- # hash
118
-
119
- The \\$& replacement adds a backslash before any matched character.
120
- This is needed when you want to use these characters literally
121
- in a regex pattern instead of their special regex meanings.
122
- For example:
123
- escapeRegExp("hello.world") // Returns: "hello\.world"
124
- escapeRegExp("[test]") // Returns: "\[test\]"
125
-
126
- This is commonly used when building dynamic regex patterns from user input to prevent special characters from being interpreted as regex syntax.
127
- */
128
- export function escapeRegExp(text: string) {
129
- return text.replace(/[-[\]{}()*+?.,/\\^$|#]/g, '\\$&');
130
- }
131
-
132
89
  export type TokenizationPayload = {
133
90
  tokenizedDisplayName: { token: string; parts: string[] };
134
91
  };
@@ -144,7 +101,7 @@ export const getTokenizedSuggestionDisplayName = ({
144
101
  token: searchToken,
145
102
  parts: searchToken
146
103
  ? displayName
147
- .split(new RegExp(`(${escapeRegExp(searchToken)})`, 'gi'))
104
+ .split(new RegExp(`(${escapeCommandRegExp(searchToken)})`, 'gi'))
148
105
  .filter(Boolean)
149
106
  : [displayName],
150
107
  },
@@ -1,4 +1,5 @@
1
1
  import type { MessageComposer } from '../../messageComposer';
2
+ import type { MessageComposerEffect } from '../../messageComposer';
2
3
  import type { CommandResponse, UserResponse } from '../../../types';
3
4
  import type { TokenizationPayload } from './textMiddlewareUtils';
4
5
  import type { SearchSource, SearchSourceSync } from '../../../search';
@@ -12,11 +13,30 @@ export type BaseSuggestion = {
12
13
  id: string;
13
14
  };
14
15
 
16
+ export type CommandSuggestionDisabledReason = 'editing' | 'quoted_message';
17
+
15
18
  export type CommandSuggestion = BaseSuggestion & CommandResponse;
16
19
  export type UserSuggestion = BaseSuggestion & UserResponse & TokenizationPayload;
17
20
  export type CustomValidSuggestion = BaseSuggestion & CustomTextComposerSuggestion;
18
21
  export type Suggestion = CommandSuggestion | UserSuggestion | CustomValidSuggestion;
19
22
 
23
+ export type TextComposerStateSnapshot = TextComposerState;
24
+
25
+ export type TextComposerCommandActivationStateToRestore =
26
+ Partial<TextComposerStateSnapshot>;
27
+
28
+ export type TextComposerCommandActivationEffect = {
29
+ command: CommandResponse;
30
+ stateToRestore?: TextComposerCommandActivationStateToRestore;
31
+ type: 'command.activate';
32
+ };
33
+
34
+ export type TextComposerCommandClearEffect = {
35
+ type: 'command.clear';
36
+ };
37
+
38
+ export type TextComposerEffect = MessageComposerEffect;
39
+
20
40
  export type TextComposerMiddlewareOptions = {
21
41
  minChars: number;
22
42
  trigger: string;
@@ -13,6 +13,8 @@ import type {
13
13
  UpdateFieldsData,
14
14
  } from './middleware/pollComposer';
15
15
 
16
+ export type PollComposerSnapshot = PollComposerState;
17
+
16
18
  export type PollComposerOptions = {
17
19
  composer: MessageComposer;
18
20
  };
@@ -107,6 +109,12 @@ export class PollComposer {
107
109
  this.state.next(this.initialState);
108
110
  };
109
111
 
112
+ getSnapshot = (): PollComposerSnapshot => this.state.getLatestValue();
113
+
114
+ restoreSnapshot = (snapshot: PollComposerSnapshot) => {
115
+ this.state.next(snapshot);
116
+ };
117
+
110
118
  /**
111
119
  * Updates specified fields and generates relevant errors
112
120
  * @param data