stream-chat 9.44.2 → 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.
- 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
|
@@ -3,14 +3,31 @@ import {
|
|
|
3
3
|
getTriggerCharWithToken,
|
|
4
4
|
insertItemWithTrigger,
|
|
5
5
|
} from './textMiddlewareUtils';
|
|
6
|
+
import { getMentionedUsersInText } from './commandUtils';
|
|
7
|
+
import {
|
|
8
|
+
userResponsesToMentionEntities,
|
|
9
|
+
userSuggestionToMentionEntity,
|
|
10
|
+
userSuggestionToUserResponse,
|
|
11
|
+
} from './mentionUtils';
|
|
6
12
|
import { BaseSearchSource, type SearchSourceOptions } from '../../../search';
|
|
7
13
|
import { mergeWith } from '../../../utils/mergeWith';
|
|
8
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
ChannelMentionSuggestion,
|
|
16
|
+
HereMentionSuggestion,
|
|
17
|
+
MentionEntity,
|
|
18
|
+
MentionSuggestion,
|
|
19
|
+
RoleMentionSuggestion,
|
|
20
|
+
TextComposerMiddlewareOptions,
|
|
21
|
+
UserGroupMentionSuggestion,
|
|
22
|
+
UserSuggestion,
|
|
23
|
+
} from './types';
|
|
9
24
|
import type { StreamChat } from '../../../client';
|
|
10
25
|
import type {
|
|
11
26
|
MemberFilters,
|
|
12
27
|
MemberSort,
|
|
28
|
+
SearchUserGroupsOptions,
|
|
13
29
|
UserFilters,
|
|
30
|
+
UserGroupResponse,
|
|
14
31
|
UserOptions,
|
|
15
32
|
UserResponse,
|
|
16
33
|
UserSort,
|
|
@@ -75,17 +92,204 @@ export const calculateLevenshtein = (query: string, name: string) => {
|
|
|
75
92
|
};
|
|
76
93
|
|
|
77
94
|
export type MentionsSearchSourceOptions = SearchSourceOptions & {
|
|
95
|
+
allowedMentionTypes?: Partial<Record<MentionType, boolean>>;
|
|
78
96
|
mentionAllAppUsers?: boolean;
|
|
97
|
+
suggestionFactoryMappers?: MentionSuggestionFactoryMapperOverrides;
|
|
79
98
|
textComposerText?: string;
|
|
99
|
+
trigger?: string;
|
|
80
100
|
// todo: document that if you want transliteration, you need to provide the function, e.g. import {default: transliterate} from '@sindresorhus/transliterate';
|
|
81
101
|
// this is now replacing a parameter useMentionsTransliteration
|
|
82
102
|
transliterate?: (text: string) => string;
|
|
83
103
|
};
|
|
84
104
|
|
|
85
|
-
|
|
105
|
+
type MentionType = MentionSuggestion['mentionType'];
|
|
106
|
+
type MentionSuggestionFactoryInputByType = {
|
|
107
|
+
channel: 'channel';
|
|
108
|
+
here: 'here';
|
|
109
|
+
role: string;
|
|
110
|
+
user: UserResponse;
|
|
111
|
+
user_group: UserGroupResponse;
|
|
112
|
+
};
|
|
113
|
+
type MentionSuggestionByType = {
|
|
114
|
+
channel: ChannelMentionSuggestion;
|
|
115
|
+
here: HereMentionSuggestion;
|
|
116
|
+
role: RoleMentionSuggestion;
|
|
117
|
+
user: UserSuggestion;
|
|
118
|
+
user_group: UserGroupMentionSuggestion;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type MentionSuggestionFactoryMapperContext = {
|
|
122
|
+
searchToken: string;
|
|
123
|
+
source: MentionsSearchSource;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export type MentionSuggestionFactoryMapper<
|
|
127
|
+
TMentionType extends MentionType = MentionType,
|
|
128
|
+
> = (
|
|
129
|
+
value: MentionSuggestionFactoryInputByType[TMentionType],
|
|
130
|
+
context: MentionSuggestionFactoryMapperContext,
|
|
131
|
+
) => MentionSuggestionByType[TMentionType];
|
|
132
|
+
|
|
133
|
+
export type MentionSuggestionFactoryMapperOverrides = {
|
|
134
|
+
[TMentionType in MentionType]?: MentionSuggestionFactoryMapper<TMentionType>;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const DEFAULT_ALLOWED_MENTION_TYPES: Record<MentionType, boolean> = {
|
|
138
|
+
channel: true,
|
|
139
|
+
here: true,
|
|
140
|
+
role: true,
|
|
141
|
+
user: true,
|
|
142
|
+
user_group: true,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
type UserGroupSearchCursor = Pick<SearchUserGroupsOptions, 'id_gt' | 'name_gt'>;
|
|
146
|
+
type UserPaginationState = {
|
|
147
|
+
itemCount: number;
|
|
148
|
+
nextOffset?: number;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const decodeUserGroupCursor = <TCursor extends object>(cursor?: string | null) => {
|
|
152
|
+
if (!cursor) return undefined;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(cursor) as TCursor;
|
|
156
|
+
} catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const upsertUserResponse = (users: UserResponse[], user: UserResponse) => {
|
|
162
|
+
const existingIndex = users.findIndex((currentUser) => currentUser.id === user.id);
|
|
163
|
+
if (existingIndex === -1) return users.concat(user);
|
|
164
|
+
|
|
165
|
+
const nextUsers = [...users];
|
|
166
|
+
nextUsers.splice(existingIndex, 1, user);
|
|
167
|
+
return nextUsers;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const upsertMentionEntity = (mentions: MentionEntity[], entity: MentionEntity) => {
|
|
171
|
+
const existingIndex = mentions.findIndex(
|
|
172
|
+
(currentEntity) =>
|
|
173
|
+
currentEntity.id === entity.id && currentEntity.mentionType === entity.mentionType,
|
|
174
|
+
);
|
|
175
|
+
if (existingIndex === -1) return mentions.concat(entity);
|
|
176
|
+
|
|
177
|
+
const nextMentions = [...mentions];
|
|
178
|
+
nextMentions.splice(existingIndex, 1, entity);
|
|
179
|
+
return nextMentions;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const mentionSuggestionToEntity = (suggestion: MentionSuggestion): MentionEntity => {
|
|
183
|
+
if (suggestion.mentionType === 'user') {
|
|
184
|
+
return userSuggestionToMentionEntity(suggestion);
|
|
185
|
+
} else if (suggestion.mentionType === 'channel') {
|
|
186
|
+
return {
|
|
187
|
+
id: 'channel',
|
|
188
|
+
mentionType: 'channel',
|
|
189
|
+
name: 'channel',
|
|
190
|
+
};
|
|
191
|
+
} else if (suggestion.mentionType === 'here') {
|
|
192
|
+
return {
|
|
193
|
+
id: 'here',
|
|
194
|
+
mentionType: 'here',
|
|
195
|
+
name: 'here',
|
|
196
|
+
};
|
|
197
|
+
} else if (suggestion.mentionType === 'role') {
|
|
198
|
+
return {
|
|
199
|
+
id: suggestion.id,
|
|
200
|
+
mentionType: 'role',
|
|
201
|
+
name: suggestion.name,
|
|
202
|
+
};
|
|
203
|
+
} else if (suggestion.mentionType === 'user_group') {
|
|
204
|
+
return {
|
|
205
|
+
id: suggestion.id,
|
|
206
|
+
mentionType: 'user_group',
|
|
207
|
+
name: suggestion.name,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
throw new Error(`Unsupported mention suggestion type: ${JSON.stringify(suggestion)}`);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const mentionSuggestionToInsertText = (suggestion: MentionSuggestion) =>
|
|
215
|
+
`@${suggestion.name || suggestion.id} `;
|
|
216
|
+
|
|
217
|
+
const DEFAULT_SUGGESTION_FACTORY_MAPPERS: {
|
|
218
|
+
[TMentionType in MentionType]: MentionSuggestionFactoryMapper<TMentionType>;
|
|
219
|
+
} = {
|
|
220
|
+
channel: (value, { searchToken }) => {
|
|
221
|
+
const name = String(value);
|
|
222
|
+
return {
|
|
223
|
+
id: name,
|
|
224
|
+
mentionType: 'channel',
|
|
225
|
+
name: 'channel',
|
|
226
|
+
...getTokenizedSuggestionDisplayName({
|
|
227
|
+
displayName: name,
|
|
228
|
+
searchToken,
|
|
229
|
+
}),
|
|
230
|
+
} satisfies ChannelMentionSuggestion;
|
|
231
|
+
},
|
|
232
|
+
here: (value, { searchToken }) => {
|
|
233
|
+
const name = String(value);
|
|
234
|
+
return {
|
|
235
|
+
id: name,
|
|
236
|
+
mentionType: 'here',
|
|
237
|
+
name: 'here',
|
|
238
|
+
...getTokenizedSuggestionDisplayName({
|
|
239
|
+
displayName: name,
|
|
240
|
+
searchToken,
|
|
241
|
+
}),
|
|
242
|
+
} satisfies HereMentionSuggestion;
|
|
243
|
+
},
|
|
244
|
+
role: (value, { searchToken }) => {
|
|
245
|
+
const role = String(value);
|
|
246
|
+
return {
|
|
247
|
+
id: role,
|
|
248
|
+
mentionType: 'role',
|
|
249
|
+
name: role,
|
|
250
|
+
...getTokenizedSuggestionDisplayName({
|
|
251
|
+
displayName: role,
|
|
252
|
+
searchToken,
|
|
253
|
+
}),
|
|
254
|
+
} satisfies RoleMentionSuggestion;
|
|
255
|
+
},
|
|
256
|
+
user: (value, { searchToken }) => {
|
|
257
|
+
const user = value as UserResponse;
|
|
258
|
+
return {
|
|
259
|
+
...user,
|
|
260
|
+
mentionType: 'user',
|
|
261
|
+
...getTokenizedSuggestionDisplayName({
|
|
262
|
+
displayName: user.name || user.id,
|
|
263
|
+
searchToken,
|
|
264
|
+
}),
|
|
265
|
+
} satisfies UserSuggestion;
|
|
266
|
+
},
|
|
267
|
+
user_group: (value, { searchToken }) => {
|
|
268
|
+
const userGroup = value as UserGroupResponse;
|
|
269
|
+
return {
|
|
270
|
+
description: userGroup.description,
|
|
271
|
+
id: userGroup.id,
|
|
272
|
+
/*
|
|
273
|
+
Currently, all members of the group are always returned. Groups are limited to 100 members.
|
|
274
|
+
The memberCount == len(members) will always be true unless we add pagination here in the future
|
|
275
|
+
*/
|
|
276
|
+
memberCount: userGroup.members?.length,
|
|
277
|
+
mentionType: 'user_group',
|
|
278
|
+
name: userGroup.name,
|
|
279
|
+
...getTokenizedSuggestionDisplayName({
|
|
280
|
+
displayName: userGroup.name || userGroup.id,
|
|
281
|
+
searchToken,
|
|
282
|
+
}),
|
|
283
|
+
} satisfies UserGroupMentionSuggestion;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export class MentionsSearchSource extends BaseSearchSource<MentionSuggestion> {
|
|
86
288
|
readonly type = 'mentions';
|
|
87
289
|
protected client: StreamChat;
|
|
88
290
|
protected channel: Channel;
|
|
291
|
+
protected latestUserPaginationState?: UserPaginationState;
|
|
292
|
+
protected userGroupCursor?: string;
|
|
89
293
|
userFilters: UserFilters | undefined;
|
|
90
294
|
memberFilters: MemberFilters | undefined;
|
|
91
295
|
userSort: UserSort | undefined;
|
|
@@ -94,12 +298,28 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
94
298
|
config: MentionsSearchSourceOptions;
|
|
95
299
|
|
|
96
300
|
constructor(channel: Channel, options?: MentionsSearchSourceOptions) {
|
|
97
|
-
const {
|
|
98
|
-
|
|
301
|
+
const {
|
|
302
|
+
allowedMentionTypes,
|
|
303
|
+
mentionAllAppUsers,
|
|
304
|
+
suggestionFactoryMappers,
|
|
305
|
+
textComposerText,
|
|
306
|
+
transliterate,
|
|
307
|
+
trigger,
|
|
308
|
+
...restOptions
|
|
309
|
+
} = options || {};
|
|
99
310
|
super(restOptions);
|
|
100
311
|
this.client = channel.getClient();
|
|
101
312
|
this.channel = channel;
|
|
102
|
-
this.config = {
|
|
313
|
+
this.config = {
|
|
314
|
+
allowedMentionTypes: {
|
|
315
|
+
...DEFAULT_ALLOWED_MENTION_TYPES,
|
|
316
|
+
...allowedMentionTypes,
|
|
317
|
+
},
|
|
318
|
+
mentionAllAppUsers,
|
|
319
|
+
suggestionFactoryMappers,
|
|
320
|
+
textComposerText,
|
|
321
|
+
trigger,
|
|
322
|
+
};
|
|
103
323
|
|
|
104
324
|
if (transliterate) {
|
|
105
325
|
this.transliterate = transliterate;
|
|
@@ -111,15 +331,91 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
111
331
|
return countLoadedMembers < MAX_CHANNEL_MEMBER_COUNT_IN_CHANNEL_QUERY;
|
|
112
332
|
}
|
|
113
333
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
334
|
+
normalizeSearchValue = (value?: string) =>
|
|
335
|
+
this.transliterate(removeDiacritics(value)).toLowerCase();
|
|
336
|
+
|
|
337
|
+
matchesSearchQuery = (value: string | undefined, searchQuery: string) => {
|
|
338
|
+
if (!searchQuery) return true;
|
|
339
|
+
return this.normalizeSearchValue(value).includes(
|
|
340
|
+
this.normalizeSearchValue(searchQuery),
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
matchesPrefixSearchQuery = (value: string | undefined, searchQuery: string) => {
|
|
345
|
+
if (!searchQuery) return true;
|
|
346
|
+
|
|
347
|
+
return this.normalizeSearchValue(value).startsWith(
|
|
348
|
+
this.normalizeSearchValue(searchQuery),
|
|
349
|
+
);
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
matchesUserNameSearchQuery = (value: string | undefined, searchQuery: string) => {
|
|
353
|
+
if (!searchQuery) return true;
|
|
354
|
+
|
|
355
|
+
const normalizedValueWords = this.normalizeSearchValue(value)
|
|
356
|
+
.split(/\s+/)
|
|
357
|
+
.filter(Boolean);
|
|
358
|
+
const normalizedQueryWords = this.normalizeSearchValue(searchQuery)
|
|
359
|
+
.split(/\s+/)
|
|
360
|
+
.filter(Boolean);
|
|
361
|
+
|
|
362
|
+
if (!normalizedValueWords.length || !normalizedQueryWords.length) return false;
|
|
363
|
+
|
|
364
|
+
const fullMatchWords = normalizedQueryWords.slice(0, -1);
|
|
365
|
+
const finalQueryWord = normalizedQueryWords[normalizedQueryWords.length - 1];
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
fullMatchWords.every((queryWord) => normalizedValueWords.includes(queryWord)) &&
|
|
369
|
+
normalizedValueWords.some((valueWord) => valueWord.startsWith(finalQueryWord))
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
isMentionTypeAllowed = (mentionType: MentionType) =>
|
|
374
|
+
this.config.allowedMentionTypes?.[mentionType] ?? true;
|
|
375
|
+
|
|
376
|
+
protected mapMentionSuggestion = <TMentionType extends MentionType>(
|
|
377
|
+
mentionType: TMentionType,
|
|
378
|
+
value: MentionSuggestionFactoryInputByType[TMentionType],
|
|
379
|
+
searchToken = this.searchQuery,
|
|
380
|
+
) => {
|
|
381
|
+
const mapper =
|
|
382
|
+
this.config.suggestionFactoryMappers?.[mentionType] ??
|
|
383
|
+
DEFAULT_SUGGESTION_FACTORY_MAPPERS[mentionType];
|
|
384
|
+
|
|
385
|
+
return mapper(value, {
|
|
386
|
+
searchToken,
|
|
387
|
+
source: this,
|
|
388
|
+
}) as MentionSuggestionByType[TMentionType];
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
getChannelTeam = () => this.channel.data?.team;
|
|
392
|
+
|
|
393
|
+
toUserSuggestion = (
|
|
394
|
+
user: UserResponse,
|
|
395
|
+
searchToken = this.searchQuery,
|
|
396
|
+
): UserSuggestion => this.mapMentionSuggestion('user', user, searchToken);
|
|
397
|
+
|
|
398
|
+
toChannelMentionSuggestion = (
|
|
399
|
+
searchToken = this.searchQuery,
|
|
400
|
+
): ChannelMentionSuggestion =>
|
|
401
|
+
this.mapMentionSuggestion('channel', 'channel', searchToken);
|
|
402
|
+
|
|
403
|
+
toHereMentionSuggestion = (searchToken = this.searchQuery): HereMentionSuggestion =>
|
|
404
|
+
this.mapMentionSuggestion('here', 'here', searchToken);
|
|
405
|
+
|
|
406
|
+
toRoleMentionSuggestion = (
|
|
407
|
+
role: string,
|
|
408
|
+
searchToken = this.searchQuery,
|
|
409
|
+
): RoleMentionSuggestion => this.mapMentionSuggestion('role', role, searchToken);
|
|
410
|
+
|
|
411
|
+
toUserGroupMentionSuggestion = (
|
|
412
|
+
userGroup: UserGroupResponse,
|
|
413
|
+
searchToken = this.searchQuery,
|
|
414
|
+
): UserGroupMentionSuggestion =>
|
|
415
|
+
this.mapMentionSuggestion('user_group', userGroup, searchToken);
|
|
121
416
|
|
|
122
417
|
getStateBeforeFirstQuery(newSearchString: string) {
|
|
418
|
+
this.userGroupCursor = undefined;
|
|
123
419
|
const newState = super.getStateBeforeFirstQuery(newSearchString);
|
|
124
420
|
const { items } = this.state.getLatestValue();
|
|
125
421
|
return {
|
|
@@ -133,6 +429,18 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
133
429
|
return this.isActive && !this.isLoading && (hasNewSearchQuery || this.hasNext);
|
|
134
430
|
};
|
|
135
431
|
|
|
432
|
+
protected updatePaginationStateFromQuery() {
|
|
433
|
+
const userPaginationState = this.latestUserPaginationState ?? { itemCount: 0 };
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
hasNext:
|
|
437
|
+
typeof userPaginationState.nextOffset !== 'undefined' ||
|
|
438
|
+
typeof this.userGroupCursor !== 'undefined',
|
|
439
|
+
next: undefined,
|
|
440
|
+
offset: (this.offset ?? 0) + userPaginationState.itemCount,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
136
444
|
transliterate = (text: string) => text;
|
|
137
445
|
|
|
138
446
|
getMembersAndWatchers = () => {
|
|
@@ -153,6 +461,25 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
153
461
|
return Object.values(uniqueUsers);
|
|
154
462
|
};
|
|
155
463
|
|
|
464
|
+
getBuiltinMentionSuggestions = (searchQuery: string): MentionSuggestion[] =>
|
|
465
|
+
[
|
|
466
|
+
...(this.isMentionTypeAllowed('channel')
|
|
467
|
+
? [this.toChannelMentionSuggestion(searchQuery)]
|
|
468
|
+
: []),
|
|
469
|
+
...(this.isMentionTypeAllowed('here')
|
|
470
|
+
? [this.toHereMentionSuggestion(searchQuery)]
|
|
471
|
+
: []),
|
|
472
|
+
].filter(({ name }) => this.matchesPrefixSearchQuery(name, searchQuery));
|
|
473
|
+
|
|
474
|
+
getRoleMentionSuggestions = async (query: string): Promise<RoleMentionSuggestion[]> => {
|
|
475
|
+
if (!this.isMentionTypeAllowed('role')) return [];
|
|
476
|
+
if (!query) return [];
|
|
477
|
+
const { roles } = await this.client.searchRoles({ query });
|
|
478
|
+
return [...(roles?.map((role) => role.name) ?? [])]
|
|
479
|
+
.sort((left, right) => left.localeCompare(right))
|
|
480
|
+
.map((role) => this.toRoleMentionSuggestion(role, query));
|
|
481
|
+
};
|
|
482
|
+
|
|
156
483
|
searchMembersLocally = (searchQuery: string) => {
|
|
157
484
|
const { textComposerText } = this.config;
|
|
158
485
|
if (!textComposerText) return [];
|
|
@@ -163,22 +490,16 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
163
490
|
if (!searchQuery) return true;
|
|
164
491
|
|
|
165
492
|
const updatedId = this.transliterate(removeDiacritics(user.id)).toLowerCase();
|
|
166
|
-
const updatedName = this.transliterate(removeDiacritics(user.name)).toLowerCase();
|
|
167
493
|
const updatedQuery = this.transliterate(
|
|
168
494
|
removeDiacritics(searchQuery),
|
|
169
495
|
).toLowerCase();
|
|
170
496
|
|
|
171
497
|
const maxDistance = 3;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
updatedName.includes(updatedQuery) ||
|
|
178
|
-
(levenshtein <= maxDistance && lastDigits)
|
|
179
|
-
) {
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
498
|
+
const trigger = this.config.trigger ?? '@';
|
|
499
|
+
const lastDigits = textComposerText.slice(-(maxDistance + 1)).includes(trigger);
|
|
500
|
+
|
|
501
|
+
if (this.matchesUserNameSearchQuery(user.name, updatedQuery)) {
|
|
502
|
+
return true;
|
|
182
503
|
}
|
|
183
504
|
|
|
184
505
|
const levenshtein = calculateLevenshtein(updatedQuery, updatedId);
|
|
@@ -204,7 +525,7 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
204
525
|
});
|
|
205
526
|
};
|
|
206
527
|
|
|
207
|
-
prepareQueryUsersParams = (searchQuery: string) => ({
|
|
528
|
+
prepareQueryUsersParams = (searchQuery: string, offset = 0) => ({
|
|
208
529
|
filters: {
|
|
209
530
|
$or: [
|
|
210
531
|
{ id: { $autocomplete: searchQuery } },
|
|
@@ -213,10 +534,10 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
213
534
|
...this.userFilters,
|
|
214
535
|
} as UserFilters,
|
|
215
536
|
sort: this.userSort ?? ([{ name: 1 }, { id: 1 }] as UserSort), // todo: document the change - the sort is overridden, not merged
|
|
216
|
-
options: { ...this.searchOptions, limit: this.pageSize, offset
|
|
537
|
+
options: { ...this.searchOptions, limit: this.pageSize, offset },
|
|
217
538
|
});
|
|
218
539
|
|
|
219
|
-
prepareQueryMembersParams = (searchQuery: string) => {
|
|
540
|
+
prepareQueryMembersParams = (searchQuery: string, offset = 0) => {
|
|
220
541
|
// QueryMembers failed with error: \"sort must contain at maximum 1 item\"
|
|
221
542
|
const maxSortParamsCount = 1;
|
|
222
543
|
let sort: MemberSort = [{ user_id: 1 }];
|
|
@@ -232,42 +553,160 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
232
553
|
filters:
|
|
233
554
|
this.memberFilters ?? ({ name: { $autocomplete: searchQuery } } as MemberFilters), // autocomplete possible only for name
|
|
234
555
|
sort,
|
|
235
|
-
options: { ...this.searchOptions, limit: this.pageSize, offset
|
|
556
|
+
options: { ...this.searchOptions, limit: this.pageSize, offset },
|
|
236
557
|
};
|
|
237
558
|
};
|
|
238
559
|
|
|
239
|
-
queryUsers = async (searchQuery: string) => {
|
|
240
|
-
const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery);
|
|
560
|
+
queryUsers = async (searchQuery: string, offset = 0) => {
|
|
561
|
+
const { filters, sort, options } = this.prepareQueryUsersParams(searchQuery, offset);
|
|
241
562
|
const { users } = await this.client.queryUsers(filters, sort, options);
|
|
242
563
|
return users;
|
|
243
564
|
};
|
|
244
565
|
|
|
245
|
-
queryMembers = async (searchQuery: string) => {
|
|
246
|
-
const { filters, sort, options } = this.prepareQueryMembersParams(
|
|
566
|
+
queryMembers = async (searchQuery: string, offset = 0) => {
|
|
567
|
+
const { filters, sort, options } = this.prepareQueryMembersParams(
|
|
568
|
+
searchQuery,
|
|
569
|
+
offset,
|
|
570
|
+
);
|
|
247
571
|
const response = await this.channel.queryMembers(filters, sort, options);
|
|
248
572
|
|
|
249
573
|
return response.members.map((member) => member.user) as UserResponse[];
|
|
250
574
|
};
|
|
251
575
|
|
|
252
|
-
async
|
|
576
|
+
getUserSuggestionsPage = async (searchQuery: string, userOffset = 0) => {
|
|
577
|
+
if (!this.isMentionTypeAllowed('user')) {
|
|
578
|
+
return {
|
|
579
|
+
items: [],
|
|
580
|
+
nextOffset: undefined,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
253
584
|
let users: UserResponse[];
|
|
254
585
|
const shouldSearchLocally =
|
|
255
586
|
this.allMembersLoadedWithInitialChannelQuery || !searchQuery;
|
|
256
587
|
|
|
257
588
|
if (this.config.mentionAllAppUsers) {
|
|
258
|
-
users = await this.queryUsers(searchQuery);
|
|
589
|
+
users = await this.queryUsers(searchQuery, userOffset);
|
|
259
590
|
} else if (shouldSearchLocally) {
|
|
260
|
-
|
|
591
|
+
const localUsers = this.searchMembersLocally(searchQuery);
|
|
592
|
+
const items = localUsers
|
|
593
|
+
.slice(userOffset, userOffset + this.pageSize)
|
|
594
|
+
.map((user) => this.toUserSuggestion(user, searchQuery));
|
|
595
|
+
return {
|
|
596
|
+
items,
|
|
597
|
+
nextOffset:
|
|
598
|
+
localUsers.length > userOffset + this.pageSize
|
|
599
|
+
? userOffset + items.length
|
|
600
|
+
: undefined,
|
|
601
|
+
};
|
|
261
602
|
} else {
|
|
262
|
-
users = await this.queryMembers(searchQuery);
|
|
603
|
+
users = await this.queryMembers(searchQuery, userOffset);
|
|
263
604
|
}
|
|
264
605
|
|
|
606
|
+
const items = users.map((user) => this.toUserSuggestion(user, searchQuery));
|
|
265
607
|
return {
|
|
266
|
-
items
|
|
608
|
+
items,
|
|
609
|
+
nextOffset: users.length === this.pageSize ? userOffset + users.length : undefined,
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
buildUserGroupSearchCursor = (items: UserGroupResponse[]) => {
|
|
614
|
+
if (items.length < this.pageSize) return undefined;
|
|
615
|
+
|
|
616
|
+
const lastItem = items[items.length - 1];
|
|
617
|
+
if (!lastItem?.name) return undefined;
|
|
618
|
+
|
|
619
|
+
return JSON.stringify({
|
|
620
|
+
id_gt: lastItem.id,
|
|
621
|
+
name_gt: lastItem.name,
|
|
622
|
+
} satisfies UserGroupSearchCursor);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
getUserGroupSuggestionsPage = async (searchQuery: string, cursor?: string) => {
|
|
626
|
+
if (!this.isMentionTypeAllowed('user_group')) {
|
|
627
|
+
return {
|
|
628
|
+
items: [],
|
|
629
|
+
next: undefined,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (!searchQuery) {
|
|
634
|
+
return {
|
|
635
|
+
items: [],
|
|
636
|
+
next: undefined,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const teamId = this.getChannelTeam();
|
|
641
|
+
const userGroupCursor = decodeUserGroupCursor<UserGroupSearchCursor>(cursor);
|
|
642
|
+
const options: SearchUserGroupsOptions = {
|
|
643
|
+
query: searchQuery,
|
|
644
|
+
limit: this.pageSize,
|
|
645
|
+
...(teamId ? { team_id: teamId } : {}),
|
|
646
|
+
...(userGroupCursor?.id_gt ? { id_gt: userGroupCursor.id_gt } : {}),
|
|
647
|
+
...(userGroupCursor?.name_gt ? { name_gt: userGroupCursor.name_gt } : {}),
|
|
648
|
+
};
|
|
649
|
+
const { user_groups } = await this.client.searchUserGroups(options);
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
items: user_groups.map((userGroup) =>
|
|
653
|
+
this.toUserGroupMentionSuggestion(userGroup, searchQuery),
|
|
654
|
+
),
|
|
655
|
+
next: this.buildUserGroupSearchCursor(user_groups),
|
|
656
|
+
};
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
async query(searchQuery: string) {
|
|
660
|
+
const userOffset = this.offset ?? 0;
|
|
661
|
+
const isFirstPage = userOffset === 0 && typeof this.userGroupCursor === 'undefined';
|
|
662
|
+
const previousUserPaginationState = this.latestUserPaginationState;
|
|
663
|
+
const previousUserGroupCursor = this.userGroupCursor;
|
|
664
|
+
const [userResultsState, userGroupResultsState, roleSuggestionsState] =
|
|
665
|
+
await Promise.allSettled([
|
|
666
|
+
this.getUserSuggestionsPage(searchQuery, userOffset),
|
|
667
|
+
this.getUserGroupSuggestionsPage(searchQuery, previousUserGroupCursor),
|
|
668
|
+
isFirstPage
|
|
669
|
+
? this.getRoleMentionSuggestions(searchQuery)
|
|
670
|
+
: Promise.resolve([] as RoleMentionSuggestion[]),
|
|
671
|
+
]);
|
|
672
|
+
|
|
673
|
+
const userResults =
|
|
674
|
+
userResultsState.status === 'fulfilled'
|
|
675
|
+
? userResultsState.value
|
|
676
|
+
: {
|
|
677
|
+
items: [],
|
|
678
|
+
nextOffset: isFirstPage ? undefined : previousUserPaginationState?.nextOffset,
|
|
679
|
+
};
|
|
680
|
+
const userGroupResults =
|
|
681
|
+
userGroupResultsState.status === 'fulfilled'
|
|
682
|
+
? userGroupResultsState.value
|
|
683
|
+
: {
|
|
684
|
+
items: [],
|
|
685
|
+
next: isFirstPage ? undefined : previousUserGroupCursor,
|
|
686
|
+
};
|
|
687
|
+
const roleSuggestions =
|
|
688
|
+
roleSuggestionsState.status === 'fulfilled' ? roleSuggestionsState.value : [];
|
|
689
|
+
const items = [
|
|
690
|
+
...(isFirstPage ? this.getBuiltinMentionSuggestions(searchQuery) : []),
|
|
691
|
+
...roleSuggestions,
|
|
692
|
+
...userGroupResults.items,
|
|
693
|
+
...userResults.items,
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
this.latestUserPaginationState = {
|
|
697
|
+
itemCount: userResults.items.length,
|
|
698
|
+
nextOffset: userResults.nextOffset,
|
|
699
|
+
};
|
|
700
|
+
this.userGroupCursor = userGroupResults.next;
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
items,
|
|
267
704
|
};
|
|
268
705
|
}
|
|
269
706
|
|
|
270
|
-
filterMutes
|
|
707
|
+
filterMutes(data: UserSuggestion[]): UserSuggestion[];
|
|
708
|
+
filterMutes(data: MentionSuggestion[]): MentionSuggestion[];
|
|
709
|
+
filterMutes(data: MentionSuggestion[]) {
|
|
271
710
|
const { textComposerText } = this.config;
|
|
272
711
|
if (!textComposerText) return [];
|
|
273
712
|
|
|
@@ -278,28 +717,32 @@ export class MentionsSearchSource extends BaseSearchSource<UserSuggestion> {
|
|
|
278
717
|
if (!mutedUsers.length) return data;
|
|
279
718
|
|
|
280
719
|
if (textComposerText.includes('/unmute')) {
|
|
281
|
-
return data.filter(
|
|
282
|
-
|
|
720
|
+
return data.filter(
|
|
721
|
+
(suggestion) =>
|
|
722
|
+
suggestion.mentionType === 'user' &&
|
|
723
|
+
mutedUsers.some((mute) => mute.target.id === suggestion.id),
|
|
283
724
|
);
|
|
284
725
|
}
|
|
285
|
-
return data.filter(
|
|
286
|
-
|
|
726
|
+
return data.filter(
|
|
727
|
+
(suggestion) =>
|
|
728
|
+
suggestion.mentionType !== 'user' ||
|
|
729
|
+
mutedUsers.every((mute) => mute.target.id !== suggestion.id),
|
|
287
730
|
);
|
|
288
|
-
}
|
|
731
|
+
}
|
|
289
732
|
|
|
290
|
-
filterQueryResults(items:
|
|
733
|
+
filterQueryResults(items: MentionSuggestion[]) {
|
|
291
734
|
return this.filterMutes(items);
|
|
292
735
|
}
|
|
736
|
+
|
|
737
|
+
resetState() {
|
|
738
|
+
this.latestUserPaginationState = undefined;
|
|
739
|
+
this.userGroupCursor = undefined;
|
|
740
|
+
super.resetState();
|
|
741
|
+
}
|
|
293
742
|
}
|
|
294
743
|
|
|
295
744
|
const DEFAULT_OPTIONS: TextComposerMiddlewareOptions = { minChars: 1, trigger: '@' };
|
|
296
745
|
|
|
297
|
-
const userSuggestionToUserResponse = (suggestion: UserSuggestion): UserResponse => {
|
|
298
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
299
|
-
const { tokenizedDisplayName, ...userResponse } = suggestion;
|
|
300
|
-
return userResponse;
|
|
301
|
-
};
|
|
302
|
-
|
|
303
746
|
/**
|
|
304
747
|
* TextComposer middleware for mentions
|
|
305
748
|
* Usage:
|
|
@@ -320,7 +763,7 @@ const userSuggestionToUserResponse = (suggestion: UserSuggestion): UserResponse
|
|
|
320
763
|
*/
|
|
321
764
|
|
|
322
765
|
export type MentionsMiddleware = Middleware<
|
|
323
|
-
TextComposerMiddlewareExecutorState<
|
|
766
|
+
TextComposerMiddlewareExecutorState<MentionSuggestion>,
|
|
324
767
|
'onChange' | 'onSuggestionItemSelect'
|
|
325
768
|
>;
|
|
326
769
|
|
|
@@ -336,18 +779,52 @@ export const createMentionsMiddleware = (
|
|
|
336
779
|
searchSource = options.searchSource;
|
|
337
780
|
searchSource.resetState();
|
|
338
781
|
} else {
|
|
339
|
-
searchSource = new MentionsSearchSource(channel);
|
|
782
|
+
searchSource = new MentionsSearchSource(channel, { trigger: finalOptions.trigger });
|
|
340
783
|
}
|
|
341
784
|
searchSource.activate();
|
|
785
|
+
// Tracks the cursor position of the most recently inserted mention so the
|
|
786
|
+
// VERY NEXT change (typically a controlled value echo on some platforms)
|
|
787
|
+
// can suppress the dropdown even when the text shape heuristic in `onChange`
|
|
788
|
+
// would otherwise let it reopen (which is wrong). Consumed on the first
|
|
789
|
+
// `onChange` after it's set, so any user driven event that triggers it would
|
|
790
|
+
// clear it.
|
|
791
|
+
let lastInsertedMentionEndOffset: number | undefined;
|
|
342
792
|
return {
|
|
343
793
|
id: 'stream-io/text-composer/mentions-middleware',
|
|
344
794
|
handlers: {
|
|
345
795
|
onChange: ({ state, next, complete, forward }) => {
|
|
346
796
|
if (!state.selection) return forward();
|
|
797
|
+
const cursorJustInsertedAMention =
|
|
798
|
+
lastInsertedMentionEndOffset !== undefined &&
|
|
799
|
+
state.selection.end === lastInsertedMentionEndOffset;
|
|
800
|
+
lastInsertedMentionEndOffset = undefined;
|
|
801
|
+
// Only prune stale mentions during normal text editing. Entering command mode
|
|
802
|
+
// clears text/mentions through the `command.activate` effect, which first
|
|
803
|
+
// snapshots the previous TextComposer state so it can be restored on
|
|
804
|
+
// `clearCommand()`. Custom middleware is allowed to remove that effect,
|
|
805
|
+
// though, and in that opt-out case we must not silently drop mentions
|
|
806
|
+
// here just because the user typed a raw command like `/ban`.
|
|
807
|
+
const currentMentions =
|
|
808
|
+
state.command || state.text.trimStart().startsWith('/')
|
|
809
|
+
? state.mentionedUsers
|
|
810
|
+
: getMentionedUsersInText(state.text, state.mentionedUsers);
|
|
811
|
+
const mentionedUsersChanged =
|
|
812
|
+
currentMentions.length !== state.mentionedUsers.length ||
|
|
813
|
+
currentMentions.some(
|
|
814
|
+
(user, index) => user.id !== state.mentionedUsers[index]?.id,
|
|
815
|
+
);
|
|
816
|
+
const stateWithMentions = mentionedUsersChanged
|
|
817
|
+
? { ...state, mentionedUsers: currentMentions }
|
|
818
|
+
: state;
|
|
819
|
+
|
|
820
|
+
const textBeforeCursor = stateWithMentions.text.slice(
|
|
821
|
+
0,
|
|
822
|
+
stateWithMentions.selection.end,
|
|
823
|
+
);
|
|
347
824
|
|
|
348
825
|
const triggerWithToken = getTriggerCharWithToken({
|
|
349
826
|
trigger: finalOptions.trigger,
|
|
350
|
-
text:
|
|
827
|
+
text: textBeforeCursor,
|
|
351
828
|
});
|
|
352
829
|
|
|
353
830
|
const newSearchTriggered =
|
|
@@ -357,22 +834,59 @@ export const createMentionsMiddleware = (
|
|
|
357
834
|
searchSource.resetStateAndActivate();
|
|
358
835
|
}
|
|
359
836
|
|
|
837
|
+
// The trigger detection regex above also accepts `@<token><trailing-space>`
|
|
838
|
+
// as "active" so users can keep refining a partial mention they typed by
|
|
839
|
+
// hand. That falsely reopens the dropdown when the cursor sits at the
|
|
840
|
+
// trailing space boundary of a mention the user has already committed
|
|
841
|
+
// (post suggestion select or manual cursor placement back into that slot).
|
|
842
|
+
//
|
|
843
|
+
// Discriminate that case by checking, for each entity in
|
|
844
|
+
// `state.mentions`, whether the text immediately before the cursor
|
|
845
|
+
// ends with the entity's actual inserted textual form (`@<name|id> `)
|
|
846
|
+
// AND that form appears exactly once in the prefix. This:
|
|
847
|
+
// - avoids false positives when an entity's `id` happens to match a
|
|
848
|
+
// different `@<token>` the user just typed (e.g. user "John Doe"
|
|
849
|
+
// whose id is "john" and the user types a fresh `@ivan ` later);
|
|
850
|
+
// - avoids false positives when the user is refining a brand new
|
|
851
|
+
// mention whose query equals an already committed mention name
|
|
852
|
+
// (text has two `@<name> ` occurrences; only one is committed, the
|
|
853
|
+
// cursor is most likely on the new one being typed).
|
|
854
|
+
const triggerMatchesCommittedMention =
|
|
855
|
+
!!triggerWithToken &&
|
|
856
|
+
/\s$/.test(textBeforeCursor) &&
|
|
857
|
+
(cursorJustInsertedAMention ||
|
|
858
|
+
(stateWithMentions.mentions ?? []).some((entity) => {
|
|
859
|
+
const insertedToken = entity.name ?? entity.id;
|
|
860
|
+
if (!insertedToken) return false;
|
|
861
|
+
const insertedForm = `@${insertedToken} `;
|
|
862
|
+
if (!textBeforeCursor.endsWith(insertedForm)) return false;
|
|
863
|
+
const escapedInsertedForm = insertedForm.replace(
|
|
864
|
+
/[.*+?^${}()|[\]\\]/g,
|
|
865
|
+
'\\$&',
|
|
866
|
+
);
|
|
867
|
+
const occurrences = textBeforeCursor.match(
|
|
868
|
+
new RegExp(escapedInsertedForm, 'g'),
|
|
869
|
+
);
|
|
870
|
+
return (occurrences?.length ?? 0) === 1;
|
|
871
|
+
}));
|
|
872
|
+
|
|
360
873
|
const triggerWasRemoved =
|
|
361
874
|
!triggerWithToken || triggerWithToken.length < finalOptions.minChars;
|
|
362
875
|
|
|
363
|
-
if (triggerWasRemoved) {
|
|
364
|
-
const hasStaleSuggestions =
|
|
365
|
-
|
|
876
|
+
if (triggerWasRemoved || triggerMatchesCommittedMention) {
|
|
877
|
+
const hasStaleSuggestions =
|
|
878
|
+
stateWithMentions.suggestions?.trigger === finalOptions.trigger;
|
|
879
|
+
const newState = { ...stateWithMentions };
|
|
366
880
|
if (hasStaleSuggestions) {
|
|
367
881
|
delete newState.suggestions;
|
|
368
882
|
}
|
|
369
883
|
return next(newState);
|
|
370
884
|
}
|
|
371
885
|
|
|
372
|
-
searchSource.config.textComposerText =
|
|
886
|
+
searchSource.config.textComposerText = stateWithMentions.text;
|
|
373
887
|
|
|
374
888
|
return complete({
|
|
375
|
-
...
|
|
889
|
+
...stateWithMentions,
|
|
376
890
|
suggestions: {
|
|
377
891
|
query: triggerWithToken.slice(1),
|
|
378
892
|
searchSource,
|
|
@@ -386,17 +900,33 @@ export const createMentionsMiddleware = (
|
|
|
386
900
|
return forward();
|
|
387
901
|
|
|
388
902
|
searchSource.resetStateAndActivate();
|
|
903
|
+
const mentionEntity = mentionSuggestionToEntity(selectedSuggestion);
|
|
904
|
+
const mentions = upsertMentionEntity(
|
|
905
|
+
state.mentions ?? userResponsesToMentionEntities(state.mentionedUsers),
|
|
906
|
+
mentionEntity,
|
|
907
|
+
);
|
|
908
|
+
const insertResult = insertItemWithTrigger({
|
|
909
|
+
insertText: mentionSuggestionToInsertText(selectedSuggestion),
|
|
910
|
+
selection: state.selection,
|
|
911
|
+
text: state.text,
|
|
912
|
+
trigger: finalOptions.trigger,
|
|
913
|
+
});
|
|
914
|
+
// Hand off the just inserted cursor position to the next `onChange`
|
|
915
|
+
// so it can suppress the dropdown even when the text shape heuristic
|
|
916
|
+
// doesn't catch a reselection of the same entity (multiple
|
|
917
|
+
// occurrences of `@<name> ` in the text for exammple).
|
|
918
|
+
lastInsertedMentionEndOffset = insertResult.selection.end;
|
|
389
919
|
return complete({
|
|
390
920
|
...state,
|
|
391
|
-
...
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
921
|
+
...insertResult,
|
|
922
|
+
mentionedUsers:
|
|
923
|
+
selectedSuggestion.mentionType === 'user'
|
|
924
|
+
? upsertUserResponse(
|
|
925
|
+
state.mentionedUsers,
|
|
926
|
+
userSuggestionToUserResponse(selectedSuggestion),
|
|
927
|
+
)
|
|
928
|
+
: state.mentionedUsers,
|
|
929
|
+
mentions,
|
|
400
930
|
suggestions: undefined,
|
|
401
931
|
});
|
|
402
932
|
},
|