stream-chat 9.42.2 → 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.
- package/dist/cjs/index.browser.js +1451 -1169
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +1452 -1171
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +1451 -1169
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/messageComposer/CustomDataManager.d.ts +3 -0
- package/dist/types/messageComposer/LocationComposer.d.ts +3 -0
- package/dist/types/messageComposer/MessageComposerEffectHandlers.d.ts +17 -0
- package/dist/types/messageComposer/attachmentManager.d.ts +7 -0
- package/dist/types/messageComposer/linkPreviewsManager.d.ts +4 -1
- package/dist/types/messageComposer/messageComposer.d.ts +39 -1
- package/dist/types/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.d.ts +2 -1
- package/dist/types/messageComposer/middleware/textComposer/commandEffects.d.ts +5 -0
- package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +7 -0
- package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +2 -0
- package/dist/types/messageComposer/middleware/textComposer/index.d.ts +1 -0
- package/dist/types/messageComposer/middleware/textComposer/textMiddlewareUtils.d.ts +0 -34
- package/dist/types/messageComposer/middleware/textComposer/types.d.ts +13 -0
- package/dist/types/messageComposer/pollComposer.d.ts +3 -0
- package/dist/types/messageComposer/textComposer.d.ts +5 -1
- package/dist/types/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/messageComposer/CustomDataManager.ts +8 -0
- package/src/messageComposer/LocationComposer.ts +8 -0
- package/src/messageComposer/MessageComposerEffectHandlers.ts +87 -0
- package/src/messageComposer/attachmentManager.ts +55 -0
- package/src/messageComposer/linkPreviewsManager.ts +12 -3
- package/src/messageComposer/messageComposer.ts +107 -0
- package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +58 -18
- package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +7 -1
- package/src/messageComposer/middleware/textComposer/commandEffects.ts +51 -0
- package/src/messageComposer/middleware/textComposer/commandStringExtraction.ts +1 -4
- package/src/messageComposer/middleware/textComposer/commandUtils.ts +48 -0
- package/src/messageComposer/middleware/textComposer/commands.ts +15 -7
- package/src/messageComposer/middleware/textComposer/index.ts +1 -0
- package/src/messageComposer/middleware/textComposer/textMiddlewareUtils.ts +3 -46
- package/src/messageComposer/middleware/textComposer/types.ts +20 -0
- package/src/messageComposer/pollComposer.ts +8 -0
- package/src/messageComposer/textComposer.ts +54 -6
- package/src/types.ts +1 -7
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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,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 =
|
|
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(`(${
|
|
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
|