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