stream-chat 9.44.1 → 9.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/cjs/index.browser.js +3546 -2681
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +3555 -2681
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +3546 -2681
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/channel_manager.d.ts +5 -2
  8. package/dist/types/channel_state.d.ts +1 -1
  9. package/dist/types/client.d.ts +112 -5
  10. package/dist/types/constants.d.ts +1 -0
  11. package/dist/types/messageComposer/LocationComposer.d.ts +1 -1
  12. package/dist/types/messageComposer/configuration/commands.configuration.d.ts +6 -0
  13. package/dist/types/messageComposer/configuration/configuration.d.ts +1 -2
  14. package/dist/types/messageComposer/configuration/index.d.ts +4 -0
  15. package/dist/types/messageComposer/configuration/types.d.ts +21 -0
  16. package/dist/types/messageComposer/fileUtils.d.ts +1 -1
  17. package/dist/types/messageComposer/messageComposer.d.ts +6 -4
  18. package/dist/types/messageComposer/middleware/messageComposer/compositionValidation.d.ts +2 -1
  19. package/dist/types/messageComposer/middleware/messageComposer/textComposer.d.ts +1 -1
  20. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +10 -1
  21. package/dist/types/messageComposer/middleware/textComposer/mentionUtils.d.ts +8 -0
  22. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +77 -15
  23. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +51 -2
  24. package/dist/types/messageComposer/pollComposer.d.ts +2 -2
  25. package/dist/types/messageComposer/textComposer.d.ts +17 -3
  26. package/dist/types/pagination/UserGroupPaginator.d.ts +21 -0
  27. package/dist/types/pagination/index.d.ts +1 -0
  28. package/dist/types/types.d.ts +132 -2
  29. package/dist/types/utils.d.ts +2 -0
  30. package/package.json +38 -31
  31. package/src/channel_manager.ts +88 -13
  32. package/src/client.ts +217 -12
  33. package/src/constants.ts +1 -0
  34. package/src/messageComposer/MessageComposerEffectHandlers.ts +1 -0
  35. package/src/messageComposer/configuration/commands.configuration.ts +55 -0
  36. package/src/messageComposer/configuration/configuration.ts +3 -1
  37. package/src/messageComposer/configuration/index.ts +4 -0
  38. package/src/messageComposer/configuration/types.ts +27 -0
  39. package/src/messageComposer/messageComposer.ts +73 -22
  40. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +23 -15
  41. package/src/messageComposer/middleware/messageComposer/textComposer.ts +151 -31
  42. package/src/messageComposer/middleware/textComposer/commandUtils.ts +68 -1
  43. package/src/messageComposer/middleware/textComposer/commands.ts +6 -2
  44. package/src/messageComposer/middleware/textComposer/mentionUtils.ts +33 -0
  45. package/src/messageComposer/middleware/textComposer/mentions.ts +596 -66
  46. package/src/messageComposer/middleware/textComposer/types.ts +70 -2
  47. package/src/messageComposer/textComposer.ts +154 -10
  48. package/src/pagination/UserGroupPaginator.ts +93 -0
  49. package/src/pagination/index.ts +1 -0
  50. package/src/permissions.ts +1 -0
  51. package/src/types.ts +161 -2
  52. package/src/utils.ts +1 -0
@@ -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 { getRawCommandName, notifyCommandDisabled } from '../textComposer/commandUtils';
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 commandSearchSource = new CommandSearchSource(composer.channel);
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
- commandSearchSource,
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
- import type { MessageComposer } from '../../messageComposer';
8
- import type { MiddlewareHandlerParams } from '../../../middleware';
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 { mentionedUsers, text } = composer.textComposer;
22
- // Instead of checking if a user is still mentioned every time the text changes,
23
- // just filter out non-mentioned users before submit, which is cheaper
24
- // and allows users to easily undo any accidental deletion
25
- const mentioned_users = Array.from(
26
- new Set(
27
- mentionedUsers.filter(
28
- ({ id, name }) => text.includes(`@${id}`) || text.includes(`@${name}`),
29
- ),
30
- ),
31
- );
32
-
33
- // prevent introducing text and mentioned_users array into the payload sent to the server
34
- if (!text && mentioned_users.length === 0) return forward();
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 { mentionedUsers, text: inputText } = composer.textComposer;
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
- mentioned_users: mentioned_users?.map((u) => u.id),
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 { getCompleteCommandInString, notifyCommandDisabled } from './commandUtils';
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?.query(commandName).items[0];
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));