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
package/src/client.ts CHANGED
@@ -42,6 +42,8 @@ import {
42
42
 
43
43
  import type {
44
44
  ActiveLiveLocationsAPIResponse,
45
+ AddUserGroupMembersOptions,
46
+ AddUserGroupMembersResponse,
45
47
  APIErrorResponse,
46
48
  APIResponse,
47
49
  AppSettings,
@@ -86,12 +88,16 @@ import type {
86
88
  CreatePollOptionAPIResponse,
87
89
  CreatePredefinedFilterOptions,
88
90
  CreateReminderOptions,
91
+ CreateRoleAPIResponse,
92
+ CreateUserGroupOptions,
93
+ CreateUserGroupResponse,
89
94
  CustomPermissionOptions,
90
95
  DeactivateUsersOptions,
91
96
  DeleteChannelsResponse,
92
97
  DeleteCommandResponse,
93
98
  DeleteMessageOptions,
94
99
  DeleteRetentionPolicyResponse,
100
+ DeleteUserGroupOptions,
95
101
  DeleteUserOptions,
96
102
  Device,
97
103
  DeviceIdentifier,
@@ -134,12 +140,15 @@ import type {
134
140
  GetThreadOptions,
135
141
  GetUnreadCountAPIResponse,
136
142
  GetUnreadCountBatchAPIResponse,
143
+ GetUserGroupOptions,
144
+ GetUserGroupResponse,
137
145
  ListChannelResponse,
138
146
  ListCommandsResponse,
139
147
  ListImportsPaginationOptions,
140
148
  ListImportsResponse,
141
149
  ListPredefinedFiltersOptions,
142
150
  ListPredefinedFiltersResponse,
151
+ ListRolesAPIResponse,
143
152
  LocalMessage,
144
153
  Logger,
145
154
  MarkChannelsReadOptions,
@@ -197,6 +206,8 @@ import type {
197
206
  QueryTeamUsageStatsResponse,
198
207
  QueryThreadsAPIResponse,
199
208
  QueryThreadsOptions,
209
+ QueryUserGroupsOptions,
210
+ QueryUserGroupsResponse,
200
211
  QueryVotesFilters,
201
212
  QueryVotesOptions,
202
213
  ReactionFilters,
@@ -205,6 +216,8 @@ import type {
205
216
  ReactivateUserOptions,
206
217
  ReactivateUsersOptions,
207
218
  ReminderAPIResponse,
219
+ RemoveUserGroupMembersOptions,
220
+ RemoveUserGroupMembersResponse,
208
221
  ReviewFlagReportOptions,
209
222
  ReviewFlagReportResponse,
210
223
  SdkIdentifier,
@@ -212,6 +225,10 @@ import type {
212
225
  SearchMessageSortBase,
213
226
  SearchOptions,
214
227
  SearchPayload,
228
+ SearchRolesAPIResponse,
229
+ SearchRolesOptions,
230
+ SearchUserGroupsOptions,
231
+ SearchUserGroupsResponse,
215
232
  SegmentData,
216
233
  SegmentResponse,
217
234
  SegmentTargetsResponse,
@@ -245,6 +262,8 @@ import type {
245
262
  UpdatePredefinedFilterOptions,
246
263
  UpdateReminderOptions,
247
264
  UpdateSegmentData,
265
+ UpdateUserGroupOptions,
266
+ UpdateUserGroupResponse,
248
267
  UpdateUsersAPIResponse,
249
268
  UpsertPushPreferencesResponse,
250
269
  UserCustomEvent,
@@ -281,6 +300,13 @@ function isString(x: unknown): x is string {
281
300
 
282
301
  type MessageComposerTearDownFunction = () => void;
283
302
 
303
+ export type QueryChannelsResponseWithChannels = Omit<
304
+ QueryChannelsAPIResponse,
305
+ 'channels'
306
+ > & {
307
+ channels: Channel[];
308
+ };
309
+
284
310
  type MessageComposerSetupFunction = ({
285
311
  composer,
286
312
  }: {
@@ -1827,6 +1853,120 @@ export class StreamChat {
1827
1853
  return data;
1828
1854
  }
1829
1855
 
1856
+ /**
1857
+ * queryUserGroups - List user groups with cursor-based pagination.
1858
+ *
1859
+ * @param {QueryUserGroupsOptions} options The query options
1860
+ *
1861
+ * @return {Promise<QueryUserGroupsResponse>} User Group Query Response
1862
+ */
1863
+ async queryUserGroups(options: QueryUserGroupsOptions = {}) {
1864
+ return await this.get<QueryUserGroupsResponse>(this.baseURL + '/usergroups', options);
1865
+ }
1866
+
1867
+ /**
1868
+ * createUserGroup - Create a user group
1869
+ *
1870
+ * @param {CreateUserGroupOptions} options The create options
1871
+ *
1872
+ * @return {Promise<CreateUserGroupResponse>} User Group Create Response
1873
+ */
1874
+ async createUserGroup(options: CreateUserGroupOptions) {
1875
+ return await this.post<CreateUserGroupResponse>(
1876
+ this.baseURL + '/usergroups',
1877
+ options,
1878
+ );
1879
+ }
1880
+
1881
+ /**
1882
+ * getUserGroup - Get a user group by ID
1883
+ *
1884
+ * @param {string} id The user group ID
1885
+ * @param {GetUserGroupOptions} options Optional query options
1886
+ *
1887
+ * @return {Promise<GetUserGroupResponse>} User Group Get Response
1888
+ */
1889
+ async getUserGroup(id: string, options: GetUserGroupOptions = {}) {
1890
+ return await this.get<GetUserGroupResponse>(
1891
+ `${this.baseURL}/usergroups/${encodeURIComponent(id)}`,
1892
+ options,
1893
+ );
1894
+ }
1895
+
1896
+ /**
1897
+ * searchUserGroups - Search user groups by prefix for autocomplete
1898
+ *
1899
+ * @param {SearchUserGroupsOptions} options The search options
1900
+ *
1901
+ * @return {Promise<SearchUserGroupsResponse>} User Group Search Response
1902
+ */
1903
+ async searchUserGroups(options: SearchUserGroupsOptions) {
1904
+ return await this.get<SearchUserGroupsResponse>(
1905
+ this.baseURL + '/usergroups/search',
1906
+ options,
1907
+ );
1908
+ }
1909
+
1910
+ /**
1911
+ * updateUserGroup - Update a user group by ID
1912
+ *
1913
+ * @param {string} id The user group ID
1914
+ * @param {UpdateUserGroupOptions} options The update options
1915
+ *
1916
+ * @return {Promise<UpdateUserGroupResponse>} User Group Update Response
1917
+ */
1918
+ async updateUserGroup(id: string, options: UpdateUserGroupOptions) {
1919
+ return await this.put<UpdateUserGroupResponse>(
1920
+ `${this.baseURL}/usergroups/${encodeURIComponent(id)}`,
1921
+ options,
1922
+ );
1923
+ }
1924
+
1925
+ /**
1926
+ * deleteUserGroup - Delete a user group by ID
1927
+ *
1928
+ * @param {string} id The user group ID
1929
+ * @param {DeleteUserGroupOptions} options Optional query options
1930
+ *
1931
+ * @return {Promise<APIResponse>} User Group Delete Response
1932
+ */
1933
+ async deleteUserGroup(id: string, options: DeleteUserGroupOptions = {}) {
1934
+ return await this.delete<APIResponse>(
1935
+ `${this.baseURL}/usergroups/${encodeURIComponent(id)}`,
1936
+ options,
1937
+ );
1938
+ }
1939
+
1940
+ /**
1941
+ * addUserGroupMembers - Add members to a user group
1942
+ *
1943
+ * @param {string} id The user group ID
1944
+ * @param {AddUserGroupMembersOptions} options The add-members options
1945
+ *
1946
+ * @return {Promise<AddUserGroupMembersResponse>} User Group Add Members Response
1947
+ */
1948
+ async addUserGroupMembers(id: string, options: AddUserGroupMembersOptions) {
1949
+ return await this.post<AddUserGroupMembersResponse>(
1950
+ `${this.baseURL}/usergroups/${encodeURIComponent(id)}/members`,
1951
+ options,
1952
+ );
1953
+ }
1954
+
1955
+ /**
1956
+ * removeUserGroupMembers - Remove members from a user group
1957
+ *
1958
+ * @param {string} id The user group ID
1959
+ * @param {RemoveUserGroupMembersOptions} options The remove-members options
1960
+ *
1961
+ * @return {Promise<RemoveUserGroupMembersResponse>} User Group Remove Members Response
1962
+ */
1963
+ async removeUserGroupMembers(id: string, options: RemoveUserGroupMembersOptions) {
1964
+ return await this.post<RemoveUserGroupMembersResponse>(
1965
+ `${this.baseURL}/usergroups/${encodeURIComponent(id)}/members/delete`,
1966
+ options,
1967
+ );
1968
+ }
1969
+
1830
1970
  /**
1831
1971
  * queryBannedUsers - Query user bans
1832
1972
  *
@@ -1888,20 +2028,26 @@ export class StreamChat {
1888
2028
  }
1889
2029
 
1890
2030
  /**
1891
- * queryChannelsRequest - Queries channels and returns the raw response
2031
+ * queryChannelsRequestWithResponse - Queries channels and returns the full API response
2032
+ * including top-level metadata such as `predefined_filter`.
2033
+ *
2034
+ * This exists as a compatibility bridge, as changing `queryChannelsRequest()` to return
2035
+ * `QueryChannelsAPIResponse` would be a breaking change because it currently returns
2036
+ * only the channel list. In the next major release, the request/response APIs should
2037
+ * be consolidated so callers can access the full response through the primary API.
1892
2038
  *
1893
2039
  * @param {ChannelFilters} filterConditions object MongoDB style filters. Can be empty object when using predefined_filter in options.
1894
2040
  * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
1895
2041
  * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
1896
2042
  * @param {ChannelOptions} [options] Options object. Can include predefined_filter, filter_values, and sort_values for using predefined filters.
1897
2043
  *
1898
- * @return {Promise<Array<ChannelAPIResponse>>} search channels response
2044
+ * @return {Promise<QueryChannelsAPIResponse>} full search channels response
1899
2045
  */
1900
- async queryChannelsRequest(
2046
+ async queryChannelsRequestWithResponse(
1901
2047
  filterConditions: ChannelFilters,
1902
2048
  sort: ChannelSort = [],
1903
2049
  options: ChannelOptions = {},
1904
- ) {
2050
+ ): Promise<QueryChannelsAPIResponse> {
1905
2051
  const defaultOptions: ChannelOptions = {
1906
2052
  state: true,
1907
2053
  watch: true,
@@ -1934,9 +2080,33 @@ export class StreamChat {
1934
2080
  ...restOptions,
1935
2081
  };
1936
2082
 
1937
- const data = await this.post<QueryChannelsAPIResponse>(
1938
- this.baseURL + '/channels',
1939
- payload,
2083
+ return await this.post<QueryChannelsAPIResponse>(this.baseURL + '/channels', payload);
2084
+ }
2085
+
2086
+ /**
2087
+ * queryChannelsRequest - Queries channels and returns the raw channel response list.
2088
+ *
2089
+ * This preserves the historical return shape for backwards compatibility. Use
2090
+ * `queryChannelsRequestWithResponse()` when response level metadata such as
2091
+ * `predefined_filter` is needed. In the next major release these APIs should be
2092
+ * consolidated into a single full-response API.
2093
+ *
2094
+ * @param {ChannelFilters} filterConditions object MongoDB style filters. Can be empty object when using predefined_filter in options.
2095
+ * @param {ChannelSort} [sort] Sort options, for instance {created_at: -1}.
2096
+ * When using multiple fields, make sure you use array of objects to guarantee field order, for instance [{last_updated: -1}, {created_at: 1}]
2097
+ * @param {ChannelOptions} [options] Options object. Can include predefined_filter, filter_values, and sort_values for using predefined filters.
2098
+ *
2099
+ * @return {Promise<Array<ChannelAPIResponse>>} search channels response
2100
+ */
2101
+ async queryChannelsRequest(
2102
+ filterConditions: ChannelFilters,
2103
+ sort: ChannelSort = [],
2104
+ options: ChannelOptions = {},
2105
+ ) {
2106
+ const data = await this.queryChannelsRequestWithResponse(
2107
+ filterConditions,
2108
+ sort,
2109
+ options,
1940
2110
  );
1941
2111
 
1942
2112
  // FIXME: In the next major release, return the full QueryChannelsAPIResponse
@@ -1955,16 +2125,34 @@ export class StreamChat {
1955
2125
  * @param {ChannelStateOptions} [stateOptions] State options object. These options will only be used for state management and won't be sent in the request.
1956
2126
  * - stateOptions.skipInitialization - Skips the initialization of the state for the channels matching the ids in the list.
1957
2127
  * - stateOptions.skipHydration - Skips returning the channels as instances of the Channel class and rather returns the raw query response.
2128
+ * - stateOptions.withResponse - Returns the full query response with hydrated channels. This is a compatibility bridge for internal callers that need response-level metadata while the default return value remains `Channel[]`.
1958
2129
  *
1959
2130
  * @return {Promise<Array<Channel>>} search channels response
1960
2131
  */
2132
+ async queryChannels(
2133
+ filterConditions: ChannelFilters,
2134
+ sort: ChannelSort,
2135
+ options: ChannelOptions,
2136
+ stateOptions: ChannelStateOptions & { withResponse: true },
2137
+ ): Promise<QueryChannelsResponseWithChannels>;
2138
+ async queryChannels(
2139
+ filterConditions?: ChannelFilters,
2140
+ sort?: ChannelSort,
2141
+ options?: ChannelOptions,
2142
+ stateOptions?: ChannelStateOptions,
2143
+ ): Promise<Channel[]>;
1961
2144
  async queryChannels(
1962
2145
  filterConditions: ChannelFilters,
1963
2146
  sort: ChannelSort = [],
1964
2147
  options: ChannelOptions = {},
1965
2148
  stateOptions: ChannelStateOptions = {},
1966
- ) {
1967
- const channels = await this.queryChannelsRequest(filterConditions, sort, options);
2149
+ ): Promise<Channel[] | QueryChannelsResponseWithChannels> {
2150
+ const queryChannelsResponse = await this.queryChannelsRequestWithResponse(
2151
+ filterConditions,
2152
+ sort,
2153
+ options,
2154
+ );
2155
+ const channels = queryChannelsResponse.channels;
1968
2156
 
1969
2157
  this.dispatchEvent({
1970
2158
  type: 'channels.queried',
@@ -1980,7 +2168,16 @@ export class StreamChat {
1980
2168
  });
1981
2169
  }
1982
2170
 
1983
- return this.hydrateActiveChannels(channels, stateOptions, options);
2171
+ const hydratedChannels = this.hydrateActiveChannels(channels, stateOptions, options);
2172
+
2173
+ if (stateOptions.withResponse) {
2174
+ return {
2175
+ ...queryChannelsResponse,
2176
+ channels: hydratedChannels,
2177
+ };
2178
+ }
2179
+
2180
+ return hydratedChannels;
1984
2181
  }
1985
2182
 
1986
2183
  /**
@@ -3758,7 +3955,7 @@ export class StreamChat {
3758
3955
  * @returns {Promise<APIResponse>}
3759
3956
  */
3760
3957
  createRole(name: string) {
3761
- return this.post<APIResponse>(`${this.baseURL}/roles`, { name });
3958
+ return this.post<CreateRoleAPIResponse>(`${this.baseURL}/roles`, { name });
3762
3959
  }
3763
3960
 
3764
3961
  /** listRoles - returns the list of all roles for this application
@@ -3766,7 +3963,15 @@ export class StreamChat {
3766
3963
  * @returns {Promise<APIResponse>}
3767
3964
  */
3768
3965
  listRoles() {
3769
- return this.get<APIResponse>(`${this.baseURL}/roles`);
3966
+ return this.get<ListRolesAPIResponse>(`${this.baseURL}/roles`);
3967
+ }
3968
+
3969
+ /** listRoles - returns the list of all roles for this application
3970
+ *
3971
+ * @returns {Promise<APIResponse>}
3972
+ */
3973
+ searchRoles(options: SearchRolesOptions) {
3974
+ return this.get<SearchRolesAPIResponse>(`${this.baseURL}/roles/search`, options);
3770
3975
  }
3771
3976
 
3772
3977
  /** deleteRole - deletes a custom role
package/src/constants.ts CHANGED
@@ -15,6 +15,7 @@ export const RESERVED_UPDATED_MESSAGE_FIELDS = Object.freeze({
15
15
  updated_at: true,
16
16
  command: true,
17
17
  // Back-end enriches these fields
18
+ mentioned_groups: true,
18
19
  mentioned_users: true,
19
20
  quoted_message: true,
20
21
  // Client-specific fields
@@ -30,6 +30,7 @@ const applyCommandActivationEffect: MessageComposerEffectHandler<
30
30
  composer.textComposer.state.next({
31
31
  command: effect.command,
32
32
  mentionedUsers: [],
33
+ mentions: [],
33
34
  suggestions: undefined,
34
35
  selection: { start: 0, end: 0 },
35
36
  text: '',
@@ -0,0 +1,55 @@
1
+ import type {
2
+ CommandsConfig,
3
+ CommandSendValidator,
4
+ MessageComposerConfig,
5
+ } from './types';
6
+ import type { DeepPartial } from '../../types.utility';
7
+ import { stripMentionTokens } from '../middleware';
8
+
9
+ export const MENTION_ONLY_COMMANDS = new Set(['mute', 'unmute', 'unban']);
10
+ export const defaultCommandSendabilityValidator: CommandSendValidator = ({
11
+ command,
12
+ commandArgsText,
13
+ mentionedUsersInText,
14
+ }) => {
15
+ if (command.name !== 'ban' && !MENTION_ONLY_COMMANDS.has(command.name ?? '')) return;
16
+
17
+ if (mentionedUsersInText.length === 0) {
18
+ return { command, ready: false, reason: 'missing-mention' };
19
+ }
20
+
21
+ if (command.name !== 'ban') {
22
+ return { command, ready: true };
23
+ }
24
+
25
+ const banReason = stripMentionTokens(commandArgsText, mentionedUsersInText);
26
+
27
+ if (!banReason.length) {
28
+ return { command, ready: false, reason: 'missing-ban-reason' };
29
+ }
30
+
31
+ return { command, ready: true };
32
+ };
33
+ export const DEFAULT_COMMANDS_CONFIG: CommandsConfig = {
34
+ sendValidator: defaultCommandSendabilityValidator,
35
+ };
36
+ export const applyCommandValidatorOverride = (
37
+ targetConfig: MessageComposerConfig,
38
+ sourceConfig?: DeepPartial<MessageComposerConfig>,
39
+ ) => {
40
+ const overrideValidator = sourceConfig?.commands?.sendValidator as
41
+ | CommandSendValidator
42
+ | undefined;
43
+
44
+ if (typeof overrideValidator === 'undefined') {
45
+ return targetConfig;
46
+ }
47
+
48
+ return {
49
+ ...targetConfig,
50
+ commands: {
51
+ ...targetConfig.commands,
52
+ sendValidator: overrideValidator,
53
+ },
54
+ };
55
+ };
@@ -5,9 +5,10 @@ import type {
5
5
  LinkPreviewsManagerConfig,
6
6
  LocationComposerConfig,
7
7
  MessageComposerConfig,
8
+ TextComposerConfig,
8
9
  } from './types';
9
- import type { TextComposerConfig } from './types';
10
10
  import { generateUUIDv4 } from '../../utils';
11
+ import { DEFAULT_COMMANDS_CONFIG } from './commands.configuration';
11
12
 
12
13
  export const DEFAULT_LINK_PREVIEW_MANAGER_CONFIG: LinkPreviewsManagerConfig = {
13
14
  debounceURLEnrichmentMs: 1500,
@@ -46,6 +47,7 @@ export const DEFAULT_LOCATION_COMPOSER_CONFIG: LocationComposerConfig = {
46
47
 
47
48
  export const DEFAULT_COMPOSER_CONFIG: MessageComposerConfig = {
48
49
  attachments: DEFAULT_ATTACHMENT_MANAGER_CONFIG,
50
+ commands: DEFAULT_COMMANDS_CONFIG,
49
51
  drafts: { enabled: false },
50
52
  linkPreviews: DEFAULT_LINK_PREVIEW_MANAGER_CONFIG,
51
53
  location: DEFAULT_LOCATION_COMPOSER_CONFIG,
@@ -1,2 +1,6 @@
1
1
  export * from './configuration';
2
2
  export * from './types';
3
+ export { applyCommandValidatorOverride } from './commands.configuration';
4
+ export { DEFAULT_COMMANDS_CONFIG } from './commands.configuration';
5
+ export { defaultCommandSendabilityValidator } from './commands.configuration';
6
+ export { MENTION_ONLY_COMMANDS } from './commands.configuration';
@@ -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
- const mergeChannelConfigCustomizer: MergeWithCustomizer<
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
- mergeWith(
227
- mergeWith(DEFAULT_COMPOSER_CONFIG, config ?? {}),
228
- {
229
- location: {
230
- enabled: this.channel.getConfig()?.shared_locations,
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
- mergeChannelConfigCustomizer,
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
- get pollId() {
382
- return this.state.getLatestValue().pollId;
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
- get showReplyInChannel() {
386
- return this.state.getLatestValue().showReplyInChannel;
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
- (!this.attachmentManager.uploadsInProgressCount &&
392
- (!this.textComposer.textIsEmpty ||
393
- this.attachmentManager.successfulUploadsCount > 0)) ||
394
- this.pollId ||
395
- !!this.locationComposer.validLocation
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(mergeWith(this.config, config));
478
+ this.configState.partialNext(
479
+ applyCommandValidatorOverride(mergeWith(this.config, config), config),
480
+ );
430
481
  }
431
482
 
432
483
  refreshId = () => {