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.
- package/dist/cjs/index.browser.js +1457 -1178
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +1458 -1180
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +1457 -1178
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/client.d.ts +4 -2
- 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 +19 -4
- package/dist/types/utils.d.ts +4 -2
- package/package.json +1 -1
- package/src/client.ts +0 -8
- package/src/connection.ts +5 -4
- package/src/messageComposer/CustomDataManager.ts +8 -0
- package/src/messageComposer/LocationComposer.ts +8 -0
- package/src/messageComposer/MessageComposerEffectHandlers.ts +89 -0
- package/src/messageComposer/attachmentManager.ts +54 -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 +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
|
|
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
|
-
|
|
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,
|