stream-chat 9.42.3 → 9.43.1

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 (45) hide show
  1. package/dist/cjs/index.browser.js +1457 -1178
  2. package/dist/cjs/index.browser.js.map +3 -3
  3. package/dist/cjs/index.node.js +1458 -1180
  4. package/dist/cjs/index.node.js.map +3 -3
  5. package/dist/esm/index.mjs +1457 -1178
  6. package/dist/esm/index.mjs.map +3 -3
  7. package/dist/types/client.d.ts +4 -2
  8. package/dist/types/messageComposer/CustomDataManager.d.ts +3 -0
  9. package/dist/types/messageComposer/LocationComposer.d.ts +3 -0
  10. package/dist/types/messageComposer/MessageComposerEffectHandlers.d.ts +17 -0
  11. package/dist/types/messageComposer/attachmentManager.d.ts +7 -0
  12. package/dist/types/messageComposer/linkPreviewsManager.d.ts +4 -1
  13. package/dist/types/messageComposer/messageComposer.d.ts +39 -1
  14. package/dist/types/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.d.ts +2 -1
  15. package/dist/types/messageComposer/middleware/textComposer/commandEffects.d.ts +5 -0
  16. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +7 -0
  17. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -0
  18. package/dist/types/messageComposer/middleware/textComposer/index.d.ts +1 -0
  19. package/dist/types/messageComposer/middleware/textComposer/textMiddlewareUtils.d.ts +0 -34
  20. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +13 -0
  21. package/dist/types/messageComposer/pollComposer.d.ts +3 -0
  22. package/dist/types/messageComposer/textComposer.d.ts +5 -1
  23. package/dist/types/types.d.ts +19 -4
  24. package/dist/types/utils.d.ts +4 -2
  25. package/package.json +1 -1
  26. package/src/client.ts +0 -8
  27. package/src/connection.ts +5 -4
  28. package/src/messageComposer/CustomDataManager.ts +8 -0
  29. package/src/messageComposer/LocationComposer.ts +8 -0
  30. package/src/messageComposer/MessageComposerEffectHandlers.ts +89 -0
  31. package/src/messageComposer/attachmentManager.ts +54 -0
  32. package/src/messageComposer/linkPreviewsManager.ts +12 -3
  33. package/src/messageComposer/messageComposer.ts +107 -0
  34. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +58 -18
  35. package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +7 -1
  36. package/src/messageComposer/middleware/textComposer/commandEffects.ts +51 -0
  37. package/src/messageComposer/middleware/textComposer/commandStringExtraction.ts +1 -4
  38. package/src/messageComposer/middleware/textComposer/commandUtils.ts +48 -0
  39. package/src/messageComposer/middleware/textComposer/commands.ts +15 -7
  40. package/src/messageComposer/middleware/textComposer/index.ts +1 -0
  41. package/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +3 -46
  42. package/src/messageComposer/middleware/textComposer/types.ts +20 -0
  43. package/src/messageComposer/pollComposer.ts +8 -0
  44. package/src/messageComposer/textComposer.ts +54 -6
  45. package/src/types.ts +27 -4
@@ -0,0 +1,89 @@
1
+ import type { MessageComposer, MessageComposerEffectHandler } from './messageComposer';
2
+ import type {
3
+ TextComposerCommandActivationEffect,
4
+ TextComposerCommandClearEffect,
5
+ } from './middleware/textComposer/types';
6
+
7
+ type RegisteredMessageComposerEffectHandler = (
8
+ effect: { type: string },
9
+ composer: MessageComposer,
10
+ ) => void;
11
+
12
+ export type MessageComposerEffectHandlersOptions = {
13
+ composer: MessageComposer;
14
+ };
15
+
16
+ const applyCommandActivationEffect: MessageComposerEffectHandler<
17
+ TextComposerCommandActivationEffect
18
+ > = (effect, composer) => {
19
+ const snapshot = composer.getSnapshot();
20
+ if (effect.stateToRestore) {
21
+ snapshot.textComposer = {
22
+ ...snapshot.textComposer,
23
+ ...effect.stateToRestore,
24
+ command: null,
25
+ };
26
+ }
27
+ composer.captureSnapshot(snapshot);
28
+
29
+ // we manually clear because we want the command to still persist
30
+ composer.textComposer.state.next({
31
+ command: effect.command,
32
+ mentionedUsers: [],
33
+ suggestions: undefined,
34
+ selection: { start: 0, end: 0 },
35
+ text: '',
36
+ });
37
+ const attachmentsToCancel = composer.attachmentManager.attachments;
38
+ composer.attachmentManager.initState();
39
+ composer.attachmentManager.cancelAttachmentUploads(attachmentsToCancel);
40
+ composer.linkPreviewsManager.initState();
41
+ composer.locationComposer.initState();
42
+ composer.pollComposer.initState();
43
+ composer.customDataManager.initState();
44
+ };
45
+
46
+ const applyCommandClearEffect: MessageComposerEffectHandler<
47
+ TextComposerCommandClearEffect
48
+ > = (_, composer) => {
49
+ const snapshot = composer.popSnapshot();
50
+
51
+ if (!snapshot) return;
52
+
53
+ composer.restoreSnapshot(snapshot);
54
+ };
55
+
56
+ export class MessageComposerEffectHandlers {
57
+ private handlers = new Map<string, RegisteredMessageComposerEffectHandler>();
58
+
59
+ constructor(private options: MessageComposerEffectHandlersOptions) {
60
+ this.registerDefaultHandlers();
61
+ }
62
+
63
+ private registerDefaultHandlers = () => {
64
+ this.registerEffectHandler<TextComposerCommandActivationEffect>(
65
+ 'command.activate',
66
+ applyCommandActivationEffect,
67
+ );
68
+ this.registerEffectHandler<TextComposerCommandClearEffect>(
69
+ 'command.clear',
70
+ applyCommandClearEffect,
71
+ );
72
+ };
73
+
74
+ registerEffectHandler = <T extends { type: string }>(
75
+ type: T['type'],
76
+ handler: MessageComposerEffectHandler<T>,
77
+ ): void => {
78
+ this.handlers.set(type, handler as RegisteredMessageComposerEffectHandler);
79
+ };
80
+
81
+ applyEffects = <T extends { type: string }>(effects: T[] = []) => {
82
+ effects.forEach((effect) => this.applyEffect(effect));
83
+ };
84
+
85
+ private applyEffect = (effect: { type: string }) => {
86
+ const handler = this.handlers.get(effect.type);
87
+ handler?.(effect, this.options.composer);
88
+ };
89
+ }
@@ -41,6 +41,8 @@ export type AttachmentManagerState = {
41
41
  attachments: LocalAttachment[];
42
42
  };
43
43
 
44
+ export type AttachmentManagerSnapshot = AttachmentManagerState;
45
+
44
46
  export type AttachmentManagerOptions = {
45
47
  composer: MessageComposer;
46
48
  message?: DraftMessage | LocalMessage;
@@ -208,10 +210,62 @@ export class AttachmentManager {
208
210
  );
209
211
  }
210
212
 
213
+ cancelAttachmentUploads = (attachments: LocalAttachment[] = this.attachments) => {
214
+ for (const { localMetadata } of attachments) {
215
+ this.client.uploadManager.deleteUploadRecord(localMetadata.id);
216
+ }
217
+ };
218
+
219
+ private normalizeSnapshotAttachment = (attachment: LocalAttachment) => {
220
+ if (attachment.localMetadata.uploadState !== 'uploading') return attachment;
221
+
222
+ this.client.uploadManager.deleteUploadRecord(attachment.localMetadata.id);
223
+
224
+ return {
225
+ ...attachment,
226
+ localMetadata: {
227
+ ...attachment.localMetadata,
228
+ uploadProgress: undefined,
229
+ uploadState: 'failed',
230
+ },
231
+ } as LocalAttachment;
232
+ };
233
+
211
234
  initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => {
212
235
  this.state.next(initState({ message }));
213
236
  };
214
237
 
238
+ getSnapshot = (): AttachmentManagerSnapshot => {
239
+ const state = this.state.getLatestValue();
240
+ let hasUpdates = false;
241
+ const attachments = state.attachments.map(this.normalizeSnapshotAttachment);
242
+
243
+ for (let i = 0; i < attachments.length; i++) {
244
+ if (attachments[i] !== state.attachments[i]) {
245
+ hasUpdates = true;
246
+ break;
247
+ }
248
+ }
249
+
250
+ return hasUpdates ? { ...state, attachments } : state;
251
+ };
252
+
253
+ restoreSnapshot = (snapshot: AttachmentManagerSnapshot) => {
254
+ this.cancelAttachmentUploads(this.attachments);
255
+ this.state.next(snapshot);
256
+ };
257
+
258
+ setAttachments = (attachments: LocalAttachment[]) => {
259
+ this.state.partialNext({ attachments });
260
+ };
261
+
262
+ clearAttachments = () => {
263
+ if (!this.attachments.length) return;
264
+ this.removeAttachments(
265
+ this.attachments.map((attachment) => attachment.localMetadata.id),
266
+ );
267
+ };
268
+
215
269
  getAttachmentIndex = (localId: string) => {
216
270
  const attachmentsById = this.attachmentsById;
217
271
 
@@ -11,7 +11,7 @@ export type LinkPreview = OGAttachment & {
11
11
  };
12
12
 
13
13
  export interface ILinkPreviewsManager {
14
- /** Function cancels all the scheduled or in-progress URL enrichment queries and resets the state. */
14
+ /** Function cancels all scheduled or in-progress URL enrichment queries. */
15
15
  cancelURLEnrichment: () => void;
16
16
  /** Function that triggers the search for URLs and their enrichment. */
17
17
  findAndEnrichUrls?: DebouncedFunc<(text: string) => void>;
@@ -38,6 +38,8 @@ export type LinkPreviewsManagerState = {
38
38
  previews: LinkPreviewMap;
39
39
  };
40
40
 
41
+ export type LinkPreviewsManagerSnapshot = LinkPreviewsManagerState;
42
+
41
43
  export type LinkPreviewsManagerOptions = {
42
44
  composer: MessageComposer;
43
45
  message?: DraftMessage | LocalMessage;
@@ -186,9 +188,16 @@ export class LinkPreviewsManager implements ILinkPreviewsManager {
186
188
  }
187
189
 
188
190
  initState = ({ message }: { message?: DraftMessage | LocalMessage } = {}) => {
191
+ this.cancelURLEnrichment();
189
192
  this.state.next(initState({ message: this.enabled ? message : undefined }));
190
193
  };
191
194
 
195
+ getSnapshot = (): LinkPreviewsManagerSnapshot => this.state.getLatestValue();
196
+
197
+ restoreSnapshot = (snapshot: LinkPreviewsManagerSnapshot) => {
198
+ this.state.next(snapshot);
199
+ };
200
+
192
201
  private _findAndEnrichUrls = async (text: string) => {
193
202
  if (!this.enabled) return;
194
203
  const urls = this.config.findURLFn(text);
@@ -254,8 +263,8 @@ export class LinkPreviewsManager implements ILinkPreviewsManager {
254
263
  };
255
264
 
256
265
  cancelURLEnrichment = () => {
257
- this.findAndEnrichUrls.cancel();
258
- this.findAndEnrichUrls.flush();
266
+ this.findAndEnrichUrls.cancel?.();
267
+ this.findAndEnrichUrls.flush?.();
259
268
  };
260
269
 
261
270
  /**
@@ -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,