tuikit-atomicx-vue3 4.5.0 → 4.5.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.
Files changed (109) hide show
  1. package/dist/{PopoverTrigger-L8abAry7.js → PopoverPortal-DghpKKm8.js} +91 -136
  2. package/dist/PopoverTrigger-BajjNkGO.js +54 -0
  3. package/dist/{PopperContent-XdhqL8Y2.js → PopperContent-SLoFuK7k.js} +6 -6
  4. package/dist/{Teleport-CSEuZbpM.js → Teleport-DFrneqLM.js} +4 -4
  5. package/dist/baseComp/Modal/Modal.js +3 -3
  6. package/dist/components/BarrageInput/EmojiPicker/EmojiPicker.js +10 -9
  7. package/dist/components/BarrageInput/TextEditor/CharacterCountExtension.js +1 -1
  8. package/dist/components/BarrageInput/TextEditor/EditorCore.js +16 -15
  9. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewAbstract.js +62 -40
  10. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewTimestamp.js +29 -27
  11. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewTitle.js +27 -23
  12. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewTitle.vue.d.ts +1 -1
  13. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewUI.js +67 -65
  14. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewUnread.js +32 -30
  15. package/dist/components/ConversationList/ConversationPreview/utils.d.ts +2 -2
  16. package/dist/components/ConversationList/ConversationPreview/utils.js +81 -35
  17. package/dist/components/ConversationList/i18n/en-US.d.ts +10 -1
  18. package/dist/components/ConversationList/i18n/en-US.js +11 -2
  19. package/dist/components/ConversationList/i18n/zh-CN.d.ts +10 -1
  20. package/dist/components/ConversationList/i18n/zh-CN.js +11 -2
  21. package/dist/components/LiveScenePanel/index.js +10 -9
  22. package/dist/components/MessageInput/AttachmentPicker/index.js +10 -9
  23. package/dist/components/MessageInput/EmojiPicker/EmojiPicker.js +17 -16
  24. package/dist/components/MessageInput/QuotedMessagePreview/index.js +35 -35
  25. package/dist/components/MessageInput/TextEditor/EditorCore.d.ts +13 -12
  26. package/dist/components/MessageInput/TextEditor/EditorCore.js +54 -89
  27. package/dist/components/MessageInput/TextEditor/extensions/MentionSuggestion.js +229 -0
  28. package/dist/components/MessageInput/TextEditor/extensions/MentionSuggestion.vue.d.ts +15 -0
  29. package/dist/components/MessageInput/TextEditor/extensions/characterCountExtension.js +1 -1
  30. package/dist/components/MessageInput/TextEditor/extensions/emojiExtension.d.ts +1 -0
  31. package/dist/components/MessageInput/TextEditor/extensions/emojiExtension.js +22 -0
  32. package/dist/components/MessageInput/TextEditor/extensions/enterKeyExtension.d.ts +3 -0
  33. package/dist/components/MessageInput/TextEditor/extensions/enterKeyExtension.js +15 -0
  34. package/dist/components/MessageInput/TextEditor/extensions/imageExtension.js +2 -2
  35. package/dist/components/MessageInput/TextEditor/extensions/index.d.ts +8 -0
  36. package/dist/components/MessageInput/TextEditor/extensions/index.js +12 -0
  37. package/dist/components/MessageInput/TextEditor/extensions/mentionExtension.d.ts +5 -0
  38. package/dist/components/MessageInput/TextEditor/extensions/mentionExtension.js +330 -0
  39. package/dist/components/MessageInput/TextEditor/index.js +62 -62
  40. package/dist/components/MessageInput/i18n/en-US.d.ts +3 -0
  41. package/dist/components/MessageInput/i18n/en-US.js +4 -1
  42. package/dist/components/MessageInput/i18n/index.d.ts +6 -0
  43. package/dist/components/MessageInput/i18n/zh-CN.d.ts +3 -0
  44. package/dist/components/MessageInput/i18n/zh-CN.js +4 -1
  45. package/dist/components/MessageList/Message/ImageMessage/ImageMessage.js +119 -89
  46. package/dist/components/MessageList/Message/ImageMessage/ImagePreview.js +142 -0
  47. package/dist/components/MessageList/Message/ImageMessage/ImagePreview.vue.d.ts +20 -0
  48. package/dist/components/MessageList/Message/Message.vue.d.ts +8 -0
  49. package/dist/components/MessageList/Message/MessageLayout/MessageActionDropdown/MessageActionDropdown.js +3 -3
  50. package/dist/components/MessageList/Message/MessageLayout/MessageLayout.js +69 -58
  51. package/dist/components/MessageList/Message/MessageLayout/MessageLayout.vue.d.ts +8 -0
  52. package/dist/components/MessageList/Message/index.js +8 -4
  53. package/dist/components/MessageList/MessageList.js +98 -90
  54. package/dist/components/MessageList/MessageList.vue.d.ts +1 -1
  55. package/dist/components/MessageList/index.d.ts +21 -3
  56. package/dist/components/Search/SearchResults/SearchResultsItem/Message/Message.js +6 -6
  57. package/dist/components/Search/SearchResults/SearchResultsItem/Message/Message.vue.d.ts +1 -1
  58. package/dist/index-BvFYOUyz.js +2936 -0
  59. package/dist/{index-Do-2CngU.js → index-C8Jw_xE4.js} +1621 -1731
  60. package/dist/{index-7vNB_Vx8.js → index-CiYL_XsE.js} +1 -1
  61. package/dist/index-CzCDLp99.js +2174 -0
  62. package/dist/index.d.ts +1 -1
  63. package/dist/index.js +103 -103
  64. package/dist/states/MessageActionState/MessageActionState.js +83 -223
  65. package/dist/states/MessageInputState/MessageInputState.js +115 -83
  66. package/dist/states/MessageInputState/type.d.ts +36 -10
  67. package/dist/states/MessageInputState/utils.d.ts +1 -5
  68. package/dist/states/MessageListState/MessageListState.d.ts +1 -1
  69. package/dist/styles/index.css +1 -1
  70. package/dist/{chat/index.d.ts → subEntry/chat/chat.d.ts} +2112 -2087
  71. package/dist/subEntry/chat/chat.js +89 -0
  72. package/dist/subEntry/chat/index.d.ts +11 -0
  73. package/dist/subEntry/chat/index.js +81 -0
  74. package/dist/{chat → subEntry/chat}/server.js +4 -4
  75. package/dist/{useId-CtirfF0W.js → useId-D5WE76CM.js} +1 -1
  76. package/dist/{utils-DaB7eSu5.js → utils-CttDpxqz.js} +1 -1
  77. package/package.json +8 -6
  78. package/src/components/ConversationList/ConversationPreview/ConversationPreview.scss +8 -0
  79. package/src/components/ConversationList/ConversationPreview/ConversationPreviewAbstract.vue +32 -1
  80. package/src/components/ConversationList/ConversationPreview/ConversationPreviewTitle.vue +3 -2
  81. package/src/components/ConversationList/ConversationPreview/utils.ts +98 -28
  82. package/src/components/ConversationList/i18n/en-US.ts +10 -1
  83. package/src/components/ConversationList/i18n/zh-CN.ts +10 -1
  84. package/src/components/LiveScenePanel/index.vue +1 -0
  85. package/src/components/MessageInput/QuotedMessagePreview/QuotedMessagePreview.vue +19 -22
  86. package/src/components/MessageInput/TextEditor/Editor.scss +25 -0
  87. package/src/components/MessageInput/TextEditor/EditorCore.ts +79 -99
  88. package/src/components/MessageInput/TextEditor/TextEditor.vue +64 -68
  89. package/src/components/MessageInput/TextEditor/extensions/MentionSuggestion.vue +449 -0
  90. package/src/components/MessageInput/TextEditor/extensions/emojiExtension.ts +22 -0
  91. package/src/components/MessageInput/TextEditor/extensions/enterKeyExtension.ts +22 -0
  92. package/src/components/MessageInput/TextEditor/extensions/index.ts +8 -0
  93. package/src/components/MessageInput/TextEditor/extensions/mentionExtension.ts +87 -0
  94. package/src/components/MessageInput/i18n/en-US.ts +3 -0
  95. package/src/components/MessageInput/i18n/zh-CN.ts +3 -0
  96. package/src/components/MessageList/Message/ImageMessage/ImageMessage.vue +49 -0
  97. package/src/components/MessageList/Message/ImageMessage/ImagePreview.vue +344 -0
  98. package/src/components/MessageList/Message/Message.vue +6 -0
  99. package/src/components/MessageList/Message/MessageLayout/MessageLayout.vue +8 -1
  100. package/src/components/MessageList/MessageList.vue +36 -14
  101. package/src/components/Search/SearchResults/SearchResultsItem/Message/Message.vue +30 -31
  102. package/src/index.ts +1 -1
  103. package/src/{chat/index.ts → subEntry/chat/chat.ts} +25 -18
  104. package/src/subEntry/chat/index.ts +13 -0
  105. package/src/{chat → subEntry/chat}/server.ts +3 -3
  106. package/dist/chat/index.js +0 -59
  107. package/dist/index-ZILx4LYk.js +0 -4826
  108. package/dist/states/SearchState.d.ts +0 -314
  109. /package/dist/{chat → subEntry/chat}/server.d.ts +0 -0
@@ -0,0 +1,449 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed, watch, onBeforeUnmount, onMounted } from 'vue';
3
+ import {
4
+ PopoverRoot,
5
+ PopoverContent,
6
+ PopoverAnchor,
7
+ PopoverPortal,
8
+ } from 'reka-ui';
9
+ import { throttle } from '../../../../utils/lodash';
10
+ import { TUIChatEngine } from '@tencentcloud/chat-uikit-engine';
11
+ import { useUIKit } from '@tencentcloud/uikit-base-component-vue3';
12
+ import { Avatar } from '../../../Avatar';
13
+ import { useGroupSettingState } from '../../../../states/GroupSettingState';
14
+ import type { SuggestionProps, SuggestionKeyDownProps } from '@tiptap/suggestion';
15
+ import type { GroupMember } from '../../../../states/GroupSettingState';
16
+
17
+ const props = defineProps<SuggestionProps>();
18
+
19
+ const { t } = useUIKit();
20
+
21
+ const selectedIndex = ref(0);
22
+ const isOpen = ref(true);
23
+ const listboxRef = ref<HTMLDivElement | null>(null);
24
+ const sentinelRef = ref<HTMLDivElement | null>(null);
25
+ const isLoading = ref(false);
26
+ const resizeKey = ref(0); // Trigger virtualElement recalculation on window resize
27
+
28
+ const { allMembers, currentUserID, groupID, memberCount, getGroupMemberList } = useGroupSettingState();
29
+
30
+ const availableMembers = computed<Partial<GroupMember>[]>(() => {
31
+ if (!allMembers.value || !currentUserID.value || !memberCount.value) {
32
+ return [];
33
+ }
34
+
35
+ const excludeSelfMembers: Partial<GroupMember>[] = allMembers.value.filter(member => member.userID !== currentUserID.value);
36
+
37
+ if (memberCount.value && memberCount.value > 2) {
38
+ excludeSelfMembers.unshift({
39
+ userID: TUIChatEngine.TYPES.MSG_AT_ALL,
40
+ nick: t('MessageInput.at_all_members') || 'all',
41
+ avatar: '/at_all_members.png',
42
+ });
43
+ }
44
+
45
+ return excludeSelfMembers;
46
+ });
47
+
48
+ const hasMore = computed(() => (allMembers.value?.length || 0) < (memberCount.value || 0));
49
+
50
+ const filteredItems = computed(() => {
51
+ const normalizedQuery = props.query.toLowerCase().trim();
52
+
53
+ if (!normalizedQuery) {
54
+ return availableMembers.value;
55
+ }
56
+
57
+ return availableMembers.value.filter(item =>
58
+ item.nick!.toLowerCase().includes(normalizedQuery),
59
+ );
60
+ });
61
+
62
+ const selectedMember = computed(() => filteredItems.value[selectedIndex.value] || null);
63
+
64
+ const showSentinel = computed(() => hasMore.value || isLoading.value || filteredItems.value.length === 0);
65
+
66
+ const loadMore = async () => {
67
+ if (isLoading.value || !hasMore.value || !groupID.value) {
68
+ return;
69
+ }
70
+
71
+ isLoading.value = true;
72
+
73
+ try {
74
+ await getGroupMemberList({
75
+ count: 100,
76
+ offset: allMembers.value?.length || 0,
77
+ });
78
+ } catch (error) {
79
+ // eslint-disable-next-line no-console
80
+ console.error('[MentionSuggestion] Failed to load more members:', error);
81
+ } finally {
82
+ isLoading.value = false;
83
+ }
84
+ };
85
+
86
+ // Handle side effects: Open state and Auto-load
87
+ watch(
88
+ [() => props.query, filteredItems, hasMore],
89
+ ([query, items, more]) => {
90
+ const normalizedQuery = query.toLowerCase().trim();
91
+
92
+ // 1. Control Open State
93
+ if (!normalizedQuery) {
94
+ isOpen.value = true;
95
+ } else if (items.length === 0 && !more) {
96
+ isOpen.value = false;
97
+ } else {
98
+ isOpen.value = true;
99
+ }
100
+
101
+ // 2. Auto load if items are few
102
+ if (items.length < 10 && more && !isLoading.value) {
103
+ loadMore();
104
+ }
105
+ },
106
+ { immediate: true }
107
+ );
108
+
109
+ const virtualElement = computed(() => {
110
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
111
+ resizeKey.value; // Dependency to trigger recalculation on window resize
112
+
113
+ const rect = props.clientRect?.();
114
+ if (!rect) {
115
+ return null;
116
+ }
117
+
118
+ return {
119
+ getBoundingClientRect: () => ({
120
+ width: 0,
121
+ height: rect.height,
122
+ x: rect.left,
123
+ y: rect.top,
124
+ top: rect.top,
125
+ left: rect.left,
126
+ right: rect.left,
127
+ bottom: rect.top + rect.height,
128
+ }),
129
+ };
130
+ });
131
+
132
+ // Reset selection when filtered items change (but not when loading more)
133
+ watch(filteredItems, (newItems, oldItems) => {
134
+ // Only reset if it's a filter change (length decreased or query changed)
135
+ // Don't reset if it's loading more (length increased)
136
+ if (!oldItems || newItems.length < oldItems.length || newItems.length === 0) {
137
+ selectedIndex.value = 0;
138
+ } else if (newItems.length > oldItems.length) {
139
+ // If loading more, keep the current selection valid
140
+ // Ensure selectedIndex is still within bounds
141
+ if (selectedIndex.value >= newItems.length) {
142
+ selectedIndex.value = newItems.length - 1;
143
+ }
144
+ }
145
+ }, { immediate: true });
146
+
147
+ let observer: IntersectionObserver | null = null;
148
+
149
+ const setupObserver = () => {
150
+ if (observer) {
151
+ observer.disconnect();
152
+ observer = null;
153
+ }
154
+
155
+ if (!listboxRef.value || !sentinelRef.value) {
156
+ return;
157
+ }
158
+
159
+ observer = new IntersectionObserver(
160
+ (entries) => {
161
+ entries.forEach((entry) => {
162
+ if (entry.isIntersecting) {
163
+ loadMore();
164
+ }
165
+ });
166
+ },
167
+ {
168
+ root: listboxRef.value,
169
+ rootMargin: '0px',
170
+ threshold: 0.1,
171
+ },
172
+ );
173
+
174
+ observer.observe(sentinelRef.value);
175
+ };
176
+
177
+ // Watch for sentinel element and setup observer
178
+ // Added filteredItems and isLoading to dependencies to ensure robust observer setup during search/load
179
+ watch([listboxRef, sentinelRef, showSentinel], () => {
180
+ if (showSentinel.value && listboxRef.value && sentinelRef.value) {
181
+ setTimeout(setupObserver, 0);
182
+ }
183
+ }, { flush: 'post' });
184
+
185
+ onMounted(() => {
186
+ window.addEventListener('resize', throttle(handleWindowResize, 30));
187
+ });
188
+
189
+ onBeforeUnmount(() => {
190
+ if (observer) {
191
+ observer.disconnect();
192
+ observer = null;
193
+ }
194
+ window.removeEventListener('resize', handleWindowResize);
195
+ });
196
+
197
+ const handleWindowResize = () => {
198
+ resizeKey.value += 1;
199
+ };
200
+
201
+ const handleSelectMember = (index: number) => {
202
+ const member = filteredItems.value[index];
203
+ if (member) {
204
+ props.command({ id: member.userID, label: member.nick, mentionSuggestionChar: '@' });
205
+ }
206
+ };
207
+
208
+ const scrollToSelected = () => {
209
+ if (!listboxRef.value) {
210
+ return;
211
+ }
212
+
213
+ const selectedItem = listboxRef.value.children[selectedIndex.value] as HTMLElement;
214
+ if (!selectedItem) {
215
+ return;
216
+ }
217
+
218
+ // 1. Ensure the selected member is visible
219
+ selectedItem.scrollIntoView({ block: 'nearest' });
220
+
221
+ // 2. If it's the last item and there's more data, ensure the loading indicator (sentinel) is also visible
222
+ // This provides visual feedback that more items are being loaded
223
+ if (selectedIndex.value === filteredItems.value.length - 1 && hasMore.value && sentinelRef.value) {
224
+ sentinelRef.value.scrollIntoView({ block: 'nearest' });
225
+ }
226
+ };
227
+
228
+ const onKeyDown = (keyDownProps: SuggestionKeyDownProps): boolean => {
229
+ const { event } = keyDownProps;
230
+
231
+ if (event.key === 'ArrowDown') {
232
+ event.preventDefault();
233
+ const nextIndex = Math.min(selectedIndex.value + 1, filteredItems.value.length - 1);
234
+ selectedIndex.value = nextIndex;
235
+ scrollToSelected();
236
+
237
+ // If reached the last item and has more data to load, trigger load
238
+ if (nextIndex === filteredItems.value.length - 1 && hasMore.value && !isLoading.value) {
239
+ loadMore();
240
+ }
241
+
242
+ return true;
243
+ }
244
+
245
+ if (event.key === 'ArrowUp') {
246
+ event.preventDefault();
247
+ selectedIndex.value = Math.max(selectedIndex.value - 1, 0);
248
+ scrollToSelected();
249
+ return true;
250
+ }
251
+
252
+ if (event.key === 'Enter') {
253
+ event.preventDefault();
254
+ if (selectedMember.value) {
255
+ handleSelectMember(selectedIndex.value);
256
+ return true;
257
+ }
258
+ }
259
+
260
+ if (event.key === 'Escape') {
261
+ return true;
262
+ }
263
+
264
+ return false;
265
+ };
266
+
267
+ const handleOpenAutoFocus = (event: Event) => {
268
+ event.preventDefault();
269
+ };
270
+
271
+ defineExpose({
272
+ onKeyDown,
273
+ });
274
+ </script>
275
+
276
+ <template>
277
+ <PopoverRoot v-model:open="isOpen" :modal="false">
278
+ <!-- Virtual anchor for cursor positioning -->
279
+ <PopoverAnchor v-if="virtualElement" :reference="virtualElement" />
280
+
281
+ <PopoverPortal>
282
+ <PopoverContent
283
+ class="mention-suggestion__content"
284
+ side="top"
285
+ align="start"
286
+ :side-offset="8"
287
+ :collision-padding="8"
288
+ :avoid-collisions="false"
289
+ @open-auto-focus="handleOpenAutoFocus"
290
+ >
291
+ <!-- Member list with manual keyboard navigation -->
292
+ <div ref="listboxRef" class="mention-suggestion__listbox">
293
+ <!-- Member items -->
294
+ <div
295
+ v-for="(item, index) in filteredItems"
296
+ :key="item.userID"
297
+ class="mention-suggestion__item"
298
+ :class="{ 'mention-suggestion__item--selected': index === selectedIndex }"
299
+ @click="handleSelectMember(index)"
300
+ @mouseenter="selectedIndex = index"
301
+ >
302
+ <Avatar
303
+ v-if="item.avatar !== '/at_all_members.png'"
304
+ class="mention-suggestion__avatar"
305
+ :src="item.avatar"
306
+ />
307
+ <span class="mention-suggestion__name">{{ item.nick || item.userID }}</span>
308
+ </div>
309
+
310
+ <!-- Sentinel element for loading more / showing states -->
311
+ <div
312
+ v-if="showSentinel"
313
+ ref="sentinelRef"
314
+ class="mention-suggestion__sentinel"
315
+ >
316
+ <!-- Loading state -->
317
+ <div v-if="hasMore" class="mention-suggestion__loading">
318
+ <div class="mention-suggestion__spinner" />
319
+ <span>{{ t('MessageInput.loading') }}</span>
320
+ </div>
321
+
322
+ <!-- No results after search -->
323
+ <div
324
+ v-else-if="filteredItems.length === 0 && !hasMore"
325
+ class="mention-suggestion__empty"
326
+ >
327
+ {{ t('MessageInput.no_matching_members') }}
328
+ </div>
329
+ </div>
330
+ </div>
331
+ </PopoverContent>
332
+ </PopoverPortal>
333
+ </PopoverRoot>
334
+ </template>
335
+
336
+ <style scoped lang="scss">
337
+ @use '../../../../styles/mixins/index.scss' as mixins;
338
+
339
+ :deep(.mention-suggestion__content) {
340
+ width: 160px;
341
+ background: var(--dropdown-color-default);
342
+ border-radius: 8px;
343
+ box-shadow: 0 4px 12px var(--shadow-color);
344
+ overflow: hidden;
345
+ z-index: 9999;
346
+ }
347
+
348
+ :deep(.mention-suggestion__listbox) {
349
+ max-height: 200px;
350
+ overflow-y: auto;
351
+ overflow-x: hidden;
352
+ padding: 4px 0;
353
+
354
+ @include mixins.scrollbar-default();
355
+ }
356
+
357
+ :deep(.mention-suggestion__item) {
358
+ display: flex;
359
+ align-items: center;
360
+ height: 40px;
361
+ width: 100%;
362
+ box-sizing: border-box;
363
+ padding: 8px 12px;
364
+ cursor: pointer;
365
+ transition: background-color 0.15s;
366
+ outline: none;
367
+ }
368
+
369
+ :deep(.mention-suggestion__item--selected) {
370
+ background-color: var(--dropdown-color-active);
371
+ }
372
+
373
+ :deep(.mention-suggestion__item:hover) {
374
+ background-color: var(--dropdown-color-hover);
375
+ }
376
+
377
+ :deep(.mention-suggestion__sentinel) {
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ }
382
+
383
+ :deep(.mention-suggestion__loading) {
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 8px;
387
+ padding: 4px;
388
+ color: var(--text-color-primary);
389
+ font-size: 13px;
390
+ width: 100%;
391
+ justify-content: center;
392
+ }
393
+
394
+ :deep(.mention-suggestion__spinner) {
395
+ width: 16px;
396
+ height: 16px;
397
+ border: 2px solid rgba(0, 122, 255, 0.2);
398
+ border-top-color: rgba(0, 122, 255, 0.8);
399
+ border-radius: 50%;
400
+ animation: spin 0.6s linear infinite;
401
+ }
402
+
403
+ @keyframes spin {
404
+ to {
405
+ transform: rotate(360deg);
406
+ }
407
+ }
408
+
409
+ :deep(.mention-suggestion__load-trigger) {
410
+ height: 20px;
411
+ width: 100%;
412
+ background: transparent;
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: center;
416
+ }
417
+
418
+ :deep(.mention-suggestion__load-hint) {
419
+ font-size: 12px;
420
+ color: var(--text-color-primary);
421
+ padding: 6px 0;
422
+ user-select: none;
423
+ }
424
+
425
+ :deep(.mention-suggestion__empty) {
426
+ padding: 4px 0px;
427
+ text-align: center;
428
+ color: var(--text-color-primary);
429
+ font-size: 14px;
430
+ }
431
+
432
+ :deep(.mention-suggestion__avatar) {
433
+ width: 20px;
434
+ height: 20px;
435
+ border-radius: 50%;
436
+ margin-right: 12px;
437
+ flex-shrink: 0;
438
+ object-fit: cover;
439
+ }
440
+
441
+ :deep(.mention-suggestion__name) {
442
+ font-size: 14px;
443
+ color: var(--text-color-primary);
444
+ font-weight: 500;
445
+ white-space: nowrap;
446
+ overflow: hidden;
447
+ text-overflow: ellipsis;
448
+ }
449
+ </style>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Emoji Extension - Display inline emojis as images
3
+ */
4
+ import Image from '@tiptap/extension-image';
5
+ import { MessageContentType } from '../../../../states/MessageInputState';
6
+
7
+ export function createEmojiExtension() {
8
+ return Image.extend({
9
+ name: MessageContentType.EMOJI,
10
+ inline: true,
11
+ group: 'inline',
12
+ draggable: true,
13
+ addOptions() {
14
+ return {
15
+ ...this.parent?.(),
16
+ HTMLAttributes: {
17
+ class: 'message-emoji',
18
+ },
19
+ };
20
+ },
21
+ });
22
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Enter Key Extension - Handle Enter key for sending message and Mod-Enter for line break
3
+ */
4
+ import { Extension } from '@tiptap/vue-3';
5
+
6
+ export function createEnterKeyExtension(onEnter?: () => void) {
7
+ return Extension.create({
8
+ name: 'enterKey',
9
+ addKeyboardShortcuts() {
10
+ return {
11
+ 'Enter': () => {
12
+ onEnter?.();
13
+ return true;
14
+ },
15
+ 'Mod-Enter': ({ editor }) => {
16
+ editor.commands.setHardBreak();
17
+ return true;
18
+ },
19
+ };
20
+ },
21
+ });
22
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Extensions Index - Centralized export for all Tiptap extensions
3
+ */
4
+ export { CharacterCount } from './characterCountExtension';
5
+ export { createEmojiExtension } from './emojiExtension';
6
+ export { createEnterKeyExtension } from './enterKeyExtension';
7
+ export { createImageExtension } from './imageExtension';
8
+ export { createMentionExtension } from './mentionExtension';
@@ -0,0 +1,87 @@
1
+ /**
2
+ * TipTap Mention Extension Configuration
3
+ * @description Enables @ mention functionality in the editor
4
+ * Data management is handled by MentionSuggestion component via GroupSettingState
5
+ */
6
+ import Mention from '@tiptap/extension-mention';
7
+ import { VueRenderer } from '@tiptap/vue-3';
8
+ import { useConversationListState } from '../../../../states/ConversationListState';
9
+ import { useGroupSettingState, GroupType } from '../../../../states/GroupSettingState';
10
+ import { ConversationType } from '../../../../types/engine';
11
+ import MentionSuggestion from './MentionSuggestion.vue';
12
+ import type { SuggestionKeyDownProps, SuggestionProps } from '@tiptap/suggestion';
13
+
14
+ interface MentionComponentRef {
15
+ onKeyDown: (props: SuggestionKeyDownProps) => boolean;
16
+ }
17
+
18
+ /**
19
+ * Create Mention extension for @ member functionality
20
+ * @returns Configured Mention extension
21
+ */
22
+ export function createMentionExtension() {
23
+ return Mention.configure({
24
+ deleteTriggerWithBackspace: true,
25
+ HTMLAttributes: {
26
+ class: 'uikit-message-input-mention-tag',
27
+ },
28
+ suggestion: {
29
+ char: '@',
30
+ // Items are now managed inside the component via GroupSettingState
31
+ items: () => [],
32
+ render: () => {
33
+ let component: VueRenderer | null = null;
34
+ let element: HTMLElement | null = null;
35
+
36
+ const destroy = () => {
37
+ if (element?.parentNode) {
38
+ element.parentNode.removeChild(element);
39
+ }
40
+ component?.destroy();
41
+ component = null;
42
+ element = null;
43
+ };
44
+
45
+ return {
46
+ onStart: (props: SuggestionProps) => {
47
+ const { activeConversation } = useConversationListState();
48
+ const { memberCount, groupType } = useGroupSettingState();
49
+ if (
50
+ activeConversation.value?.type === ConversationType.GROUP
51
+ && memberCount.value
52
+ && memberCount.value > 1
53
+ && groupType.value !== GroupType.AVCHATROOM
54
+ ) {
55
+ component = new VueRenderer(MentionSuggestion, {
56
+ props,
57
+ editor: props.editor,
58
+ });
59
+
60
+ if (component.element) {
61
+ element = component.element as HTMLElement;
62
+ document.body.appendChild(element);
63
+ }
64
+ }
65
+ },
66
+
67
+ onUpdate(props: SuggestionProps) {
68
+ component?.updateProps(props);
69
+ },
70
+
71
+ onKeyDown(props: SuggestionKeyDownProps) {
72
+ if (props.event.key === 'Escape') {
73
+ destroy();
74
+ return true;
75
+ }
76
+
77
+ return (component?.ref as unknown as MentionComponentRef)?.onKeyDown?.(props) || false;
78
+ },
79
+
80
+ onExit() {
81
+ destroy();
82
+ },
83
+ };
84
+ },
85
+ },
86
+ });
87
+ }
@@ -12,6 +12,9 @@ const MessageInput = {
12
12
  cancel: 'Cancel',
13
13
  confirm: 'Confirm',
14
14
  initiate_call: 'Initiate call',
15
+ loading: 'Loading...',
16
+ no_matching_members: 'No matching members',
17
+ at_all_members: 'All members',
15
18
  };
16
19
 
17
20
  export default MessageInput;
@@ -12,6 +12,9 @@ const MessageInput = {
12
12
  cancel: '取消',
13
13
  confirm: '确认',
14
14
  initiate_call: '发起通话',
15
+ loading: '加载中...',
16
+ no_matching_members: '暂无匹配成员',
17
+ at_all_members: '所有人',
15
18
  };
16
19
 
17
20
  export default MessageInput;