tuikit-atomicx-vue3 4.5.0 → 4.5.2

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 (182) hide show
  1. package/dist/{MessageInput.vue_vue_type_script_setup_true_lang-jPzZ5INK.js → MessageInput.vue_vue_type_script_setup_true_lang-3RVYOdkv.js} +38 -38
  2. package/dist/{PopoverTrigger-L8abAry7.js → PopoverPortal-DV6zFXcf.js} +91 -136
  3. package/dist/PopoverTrigger-DIjW4PKa.js +54 -0
  4. package/dist/{PopperContent-XdhqL8Y2.js → PopperContent-D__dbwpA.js} +6 -6
  5. package/dist/{Teleport-CSEuZbpM.js → Teleport-98QrIYDI.js} +280 -270
  6. package/dist/baseComp/Modal/Modal.js +3 -3
  7. package/dist/components/AudioSettingPanel/index.js +14 -14
  8. package/dist/components/BarrageInput/EmojiPicker/EmojiPicker.js +10 -9
  9. package/dist/components/BarrageInput/TextEditor/CharacterCountExtension.js +1 -1
  10. package/dist/components/BarrageInput/TextEditor/EditorCore.js +16 -15
  11. package/dist/components/ConversationList/ConversationActions/ConversationActions.js +53 -50
  12. package/dist/components/ConversationList/ConversationActions/ConversationActions.vue.d.ts +2 -0
  13. package/dist/components/ConversationList/ConversationCreate/ConversationCreate.js +25 -25
  14. package/dist/components/ConversationList/ConversationList.vue.d.ts +16 -0
  15. package/dist/components/ConversationList/ConversationPreview/ConversationPreview.vue.d.ts +16 -0
  16. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewAbstract.js +62 -40
  17. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewTimestamp.js +28 -26
  18. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewTitle.js +27 -23
  19. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewTitle.vue.d.ts +1 -1
  20. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewUI.js +81 -70
  21. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewUI.vue.d.ts +4 -0
  22. package/dist/components/ConversationList/ConversationPreview/ConversationPreviewUnread.js +33 -31
  23. package/dist/components/ConversationList/ConversationPreview/utils.d.ts +2 -2
  24. package/dist/components/ConversationList/ConversationPreview/utils.js +81 -35
  25. package/dist/components/ConversationList/i18n/en-US.d.ts +11 -1
  26. package/dist/components/ConversationList/i18n/en-US.js +12 -2
  27. package/dist/components/ConversationList/i18n/zh-CN.d.ts +11 -1
  28. package/dist/components/ConversationList/i18n/zh-CN.js +12 -2
  29. package/dist/components/ConversationList/index.d.ts +48 -0
  30. package/dist/components/MessageInput/AttachmentPicker/index.js +10 -9
  31. package/dist/components/MessageInput/EmojiPicker/EmojiPicker.js +17 -16
  32. package/dist/components/MessageInput/MessageInput.js +1 -1
  33. package/dist/components/MessageInput/QuotedMessagePreview/index.js +35 -35
  34. package/dist/components/MessageInput/TextEditor/EditorCore.d.ts +13 -12
  35. package/dist/components/MessageInput/TextEditor/EditorCore.js +54 -89
  36. package/dist/components/MessageInput/TextEditor/extensions/MentionSuggestion.js +229 -0
  37. package/dist/components/MessageInput/TextEditor/extensions/MentionSuggestion.vue.d.ts +15 -0
  38. package/dist/components/MessageInput/TextEditor/extensions/characterCountExtension.js +1 -1
  39. package/dist/components/MessageInput/TextEditor/extensions/emojiExtension.d.ts +1 -0
  40. package/dist/components/MessageInput/TextEditor/extensions/emojiExtension.js +22 -0
  41. package/dist/components/MessageInput/TextEditor/extensions/enterKeyExtension.d.ts +3 -0
  42. package/dist/components/MessageInput/TextEditor/extensions/enterKeyExtension.js +15 -0
  43. package/dist/components/MessageInput/TextEditor/extensions/imageExtension.js +2 -2
  44. package/dist/components/MessageInput/TextEditor/extensions/index.d.ts +8 -0
  45. package/dist/components/MessageInput/TextEditor/extensions/index.js +12 -0
  46. package/dist/components/MessageInput/TextEditor/extensions/mentionExtension.d.ts +5 -0
  47. package/dist/components/MessageInput/TextEditor/extensions/mentionExtension.js +330 -0
  48. package/dist/components/MessageInput/TextEditor/index.js +62 -62
  49. package/dist/components/MessageInput/i18n/en-US.d.ts +3 -0
  50. package/dist/components/MessageInput/i18n/en-US.js +4 -1
  51. package/dist/components/MessageInput/i18n/index.d.ts +6 -0
  52. package/dist/components/MessageInput/i18n/zh-CN.d.ts +3 -0
  53. package/dist/components/MessageInput/i18n/zh-CN.js +4 -1
  54. package/dist/components/MessageInput/index.js +1 -1
  55. package/dist/components/MessageList/Message/ImageMessage/ImageMessage.js +119 -89
  56. package/dist/components/MessageList/Message/ImageMessage/ImagePreview.js +142 -0
  57. package/dist/components/MessageList/Message/ImageMessage/ImagePreview.vue.d.ts +20 -0
  58. package/dist/components/MessageList/Message/Message.vue.d.ts +8 -0
  59. package/dist/components/MessageList/Message/MessageLayout/MessageActionDropdown/MessageActionDropdown.js +3 -3
  60. package/dist/components/MessageList/Message/MessageLayout/MessageLayout.js +69 -58
  61. package/dist/components/MessageList/Message/MessageLayout/MessageLayout.vue.d.ts +8 -0
  62. package/dist/components/MessageList/Message/MessageLayout/MessageMeta/MessageMeta.js +37 -28
  63. package/dist/components/MessageList/Message/index.js +8 -4
  64. package/dist/components/MessageList/MessageList.js +109 -91
  65. package/dist/components/MessageList/MessageList.vue.d.ts +1 -1
  66. package/dist/components/MessageList/index.d.ts +21 -3
  67. package/dist/components/ScheduleRoomPanel/RoomDetail.js +22 -22
  68. package/dist/components/ScheduleRoomPanel/RoomEdit.js +1 -1
  69. package/dist/components/ScheduleRoomPanel/RoomShare.js +4 -4
  70. package/dist/components/ScheduleRoomPanel/ScheduleRoomPanel.js +1 -1
  71. package/dist/components/ScheduleRoomPanel/ScheduledRoomList.js +1 -1
  72. package/dist/components/Search/SearchResults/SearchResultsItem/Message/Message.js +6 -6
  73. package/dist/components/Search/SearchResults/SearchResultsItem/Message/Message.vue.d.ts +1 -1
  74. package/dist/components/UIKitModal/chatErrorModal/chatErrorModal.d.ts +72 -0
  75. package/dist/components/UIKitModal/chatErrorModal/chatErrorModal.js +95 -0
  76. package/dist/components/UIKitModal/chatErrorModal/i18n/en-US/index.d.ts +40 -0
  77. package/dist/components/UIKitModal/chatErrorModal/i18n/en-US/index.js +51 -0
  78. package/dist/components/UIKitModal/chatErrorModal/i18n/index.d.ts +4 -0
  79. package/dist/components/UIKitModal/chatErrorModal/i18n/index.js +6 -0
  80. package/dist/components/UIKitModal/chatErrorModal/i18n/zh-CN/index.d.ts +40 -0
  81. package/dist/components/UIKitModal/chatErrorModal/i18n/zh-CN/index.js +51 -0
  82. package/dist/components/UIKitModal/chatErrorModal/index.d.ts +3 -0
  83. package/dist/components/UIKitModal/chatErrorModal/index.js +11 -0
  84. package/dist/components/VideoSettingPanel/index.js +1 -1
  85. package/dist/hooks/useReadReceipt/useReadReceipt.js +44 -41
  86. package/dist/index-CTthrJb2.js +1461 -0
  87. package/dist/index-DXC5bPY4.js +2174 -0
  88. package/dist/{index-Do-2CngU.js → index-DuAffztD.js} +115 -142
  89. package/dist/{index-7vNB_Vx8.js → index-hHVD-MG2.js} +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.js +150 -149
  92. package/dist/states/GroupSettingState/GroupSettingState.js +109 -102
  93. package/dist/states/LoginState.js +43 -43
  94. package/dist/states/MessageActionState/MessageActionState.js +83 -223
  95. package/dist/states/MessageInputState/MessageInputState.js +111 -82
  96. package/dist/states/MessageInputState/type.d.ts +36 -10
  97. package/dist/states/MessageInputState/utils.d.ts +1 -5
  98. package/dist/states/MessageListState/MessageListState.d.ts +1 -1
  99. package/dist/states/MessageListState/MessageListState.js +26 -23
  100. package/dist/states/RoomParticipantState/index.js +83 -24
  101. package/dist/states/RoomParticipantState/participantEventManager.d.ts +2 -2
  102. package/dist/states/RoomParticipantState/participantEventManager.js +217 -205
  103. package/dist/states/RoomParticipantState/participantManager.d.ts +4 -0
  104. package/dist/states/RoomParticipantState/participantManager.js +159 -127
  105. package/dist/states/RoomState/callManager.d.ts +3 -3
  106. package/dist/states/RoomState/callManager.js +20 -20
  107. package/dist/states/RoomState/common.d.ts +3 -2
  108. package/dist/states/RoomState/common.js +34 -24
  109. package/dist/states/RoomState/roomManager.d.ts +0 -1
  110. package/dist/states/RoomState/roomManager.js +21 -27
  111. package/dist/states/RoomState/scheduleManager.js +2 -2
  112. package/dist/styles/index.css +1 -1
  113. package/dist/{chat/index.d.ts → subEntry/chat/chat.d.ts} +2160 -2087
  114. package/dist/subEntry/chat/chat.js +89 -0
  115. package/dist/subEntry/chat/index.d.ts +11 -0
  116. package/dist/subEntry/chat/index.js +81 -0
  117. package/dist/{chat → subEntry/chat}/server.js +4 -4
  118. package/dist/subEntry/live/index.js +46 -45
  119. package/dist/subEntry/room/index.js +39 -38
  120. package/dist/types/beauty.d.ts +20 -0
  121. package/dist/types/index.js +37 -36
  122. package/dist/types/participant.d.ts +2 -0
  123. package/dist/types/room.d.ts +45 -1
  124. package/dist/types/room.js +4 -3
  125. package/dist/{useId-CtirfF0W.js → useId-B1VwPJLm.js} +1 -1
  126. package/dist/utils/call.js +77 -71
  127. package/dist/{utils-DaB7eSu5.js → utils-BU8IkP_V.js} +1 -1
  128. package/package.json +8 -7
  129. package/src/components/AudioSettingPanel/index.vue +4 -5
  130. package/src/components/ConversationList/ConversationActions/ConversationActions.vue +7 -1
  131. package/src/components/ConversationList/ConversationCreate/ConversationCreate.vue +5 -1
  132. package/src/components/ConversationList/ConversationList.vue +0 -1
  133. package/src/components/ConversationList/ConversationPreview/ConversationPreview.scss +10 -2
  134. package/src/components/ConversationList/ConversationPreview/ConversationPreviewAbstract.vue +32 -1
  135. package/src/components/ConversationList/ConversationPreview/ConversationPreviewTitle.vue +3 -2
  136. package/src/components/ConversationList/ConversationPreview/ConversationPreviewUI.vue +15 -0
  137. package/src/components/ConversationList/ConversationPreview/utils.ts +98 -28
  138. package/src/components/ConversationList/i18n/en-US.ts +11 -1
  139. package/src/components/ConversationList/i18n/zh-CN.ts +11 -1
  140. package/src/components/MessageInput/MessageInput.module.scss +1 -0
  141. package/src/components/MessageInput/QuotedMessagePreview/QuotedMessagePreview.vue +19 -22
  142. package/src/components/MessageInput/TextEditor/Editor.scss +25 -0
  143. package/src/components/MessageInput/TextEditor/EditorCore.ts +79 -99
  144. package/src/components/MessageInput/TextEditor/TextEditor.vue +64 -68
  145. package/src/components/MessageInput/TextEditor/extensions/MentionSuggestion.vue +449 -0
  146. package/src/components/MessageInput/TextEditor/extensions/emojiExtension.ts +22 -0
  147. package/src/components/MessageInput/TextEditor/extensions/enterKeyExtension.ts +22 -0
  148. package/src/components/MessageInput/TextEditor/extensions/index.ts +8 -0
  149. package/src/components/MessageInput/TextEditor/extensions/mentionExtension.ts +87 -0
  150. package/src/components/MessageInput/i18n/en-US.ts +3 -0
  151. package/src/components/MessageInput/i18n/zh-CN.ts +3 -0
  152. package/src/components/MessageList/Message/ImageMessage/ImageMessage.vue +49 -0
  153. package/src/components/MessageList/Message/ImageMessage/ImagePreview.vue +344 -0
  154. package/src/components/MessageList/Message/Message.vue +6 -0
  155. package/src/components/MessageList/Message/MessageLayout/MessageLayout.vue +8 -1
  156. package/src/components/MessageList/Message/MessageLayout/MessageMeta/MessageMeta.vue +12 -3
  157. package/src/components/MessageList/MessageList.vue +50 -14
  158. package/src/components/ScheduleRoomPanel/RoomDetail.vue +1 -0
  159. package/src/components/ScheduleRoomPanel/RoomEdit.vue +2 -1
  160. package/src/components/ScheduleRoomPanel/RoomShare.vue +1 -0
  161. package/src/components/ScheduleRoomPanel/ScheduleRoomPanel.vue +1 -0
  162. package/src/components/ScheduleRoomPanel/ScheduledRoomList.vue +1 -0
  163. package/src/components/Search/SearchResults/SearchResultsItem/Message/Message.vue +30 -31
  164. package/src/components/UIKitModal/chatErrorModal/chatErrorModal.ts +205 -0
  165. package/src/components/UIKitModal/chatErrorModal/i18n/en-US/index.ts +56 -0
  166. package/src/components/UIKitModal/chatErrorModal/i18n/index.ts +4 -0
  167. package/src/components/UIKitModal/chatErrorModal/i18n/zh-CN/index.ts +56 -0
  168. package/src/components/UIKitModal/chatErrorModal/index.ts +16 -0
  169. package/src/components/VideoSettingPanel/index.vue +1 -0
  170. package/src/hooks/useReadReceipt/useReadReceipt.ts +5 -4
  171. package/src/index.ts +1 -1
  172. package/src/{chat/index.ts → subEntry/chat/chat.ts} +25 -18
  173. package/src/subEntry/chat/index.ts +13 -0
  174. package/src/{chat → subEntry/chat}/server.ts +3 -3
  175. package/src/types/beauty.ts +20 -0
  176. package/src/types/participant.ts +3 -0
  177. package/src/types/room.ts +49 -1
  178. package/src/utils/call.ts +8 -0
  179. package/dist/chat/index.js +0 -59
  180. package/dist/index-ZILx4LYk.js +0 -4826
  181. package/dist/states/SearchState.d.ts +0 -314
  182. /package/dist/{chat → subEntry/chat}/server.d.ts +0 -0
@@ -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;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
3
3
  import cs from 'classnames';
4
+ import ImagePreview from './ImagePreview.vue';
4
5
  import type { IMessageModel as MessageModel } from '@tencentcloud/chat-uikit-engine';
5
6
 
6
7
  interface ImageMessageProps {
@@ -15,6 +16,8 @@ interface ImageMessageContent {
15
16
  showName?: string;
16
17
  }
17
18
 
19
+ defineOptions({ inheritAttrs: false });
20
+
18
21
  const MAX_HEIGHT = 320;
19
22
  const MIN_HEIGHT = 50;
20
23
  const DEFAULT_ASPECT_RATIO = 4 / 3;
@@ -58,6 +61,9 @@ const imageRef = ref<HTMLImageElement | null>(null);
58
61
  const containerRef = ref<HTMLDivElement | null>(null);
59
62
  let observerRef: IntersectionObserver | null = null;
60
63
 
64
+ const isPreviewOpen = ref(false);
65
+ let previousBodyOverflow: string | null = null;
66
+
61
67
  const isBlobImage = computed(() => messageContent.value.url.startsWith('blob'));
62
68
 
63
69
  // Utility function to get image dimensions
@@ -180,6 +186,30 @@ const handleImageError = () => {
180
186
  loadingState.value = 'error';
181
187
  };
182
188
 
189
+ function handlePreviewOpen() {
190
+ if (loadingState.value !== 'loaded') return;
191
+ if (!messageContent.value.url) return;
192
+ isPreviewOpen.value = true;
193
+ }
194
+
195
+ function handlePreviewClose() {
196
+ isPreviewOpen.value = false;
197
+ }
198
+
199
+ function lockBodyScroll() {
200
+ if (typeof document === 'undefined') return;
201
+ previousBodyOverflow = document.body.style.overflow;
202
+ document.body.style.overflow = 'hidden';
203
+ }
204
+
205
+ function unlockBodyScroll() {
206
+ if (typeof document === 'undefined') return;
207
+ if (previousBodyOverflow !== null) {
208
+ document.body.style.overflow = previousBodyOverflow;
209
+ previousBodyOverflow = null;
210
+ }
211
+ }
212
+
183
213
  // Watch for message content changes
184
214
  watch(() => messageContent.value.url, () => {
185
215
  loadingState.value = getInitialLoadingState(messageContent.value.url);
@@ -195,6 +225,14 @@ watch(() => messageContent.value.url, () => {
195
225
  fetchBlobDimensions();
196
226
  });
197
227
 
228
+ watch(isPreviewOpen, (open) => {
229
+ if (open) {
230
+ lockBodyScroll();
231
+ } else {
232
+ unlockBodyScroll();
233
+ }
234
+ });
235
+
198
236
  onMounted(() => {
199
237
  initializeLazyLoading();
200
238
  fetchBlobDimensions();
@@ -205,12 +243,14 @@ onUnmounted(() => {
205
243
  if (observerRef) {
206
244
  observerRef.disconnect();
207
245
  }
246
+ unlockBodyScroll();
208
247
  });
209
248
  </script>
210
249
 
211
250
  <template>
212
251
  <div
213
252
  ref="containerRef"
253
+ v-bind="$attrs"
214
254
  :class="cs('image-message', {
215
255
  'image-message--loading': loadingState === 'loading',
216
256
  'image-message--error': loadingState === 'error',
@@ -221,6 +261,9 @@ onUnmounted(() => {
221
261
  height: `${displaySize.height}px`,
222
262
  aspectRatio: `${displaySize.aspectRatio}`,
223
263
  }"
264
+ role="button"
265
+ tabindex="0"
266
+ @click="handlePreviewOpen"
224
267
  >
225
268
  <!-- skeleton -->
226
269
  <div
@@ -249,6 +292,12 @@ onUnmounted(() => {
249
292
  @error="handleImageError"
250
293
  >
251
294
  </div>
295
+ <ImagePreview
296
+ :open="isPreviewOpen"
297
+ :src="messageContent.url"
298
+ :alt="messageContent.showName || 'image message preview'"
299
+ @close="handlePreviewClose"
300
+ />
252
301
  </template>
253
302
 
254
303
  <style lang="scss" scoped>
@@ -0,0 +1,344 @@
1
+ <script lang="ts" setup>
2
+ import { ref, watch, onMounted, onUnmounted } from 'vue';
3
+ import cs from 'classnames';
4
+
5
+ interface ImagePreviewProps {
6
+ open: boolean;
7
+ src: string;
8
+ alt?: string;
9
+ }
10
+
11
+ defineOptions({ inheritAttrs: false });
12
+
13
+ const props = defineProps<ImagePreviewProps>();
14
+ const emit = defineEmits<{
15
+ (e: 'close'): void;
16
+ }>();
17
+
18
+ const scale = ref(1);
19
+ const translate = ref({ x: 0, y: 0 });
20
+ const isDragging = ref(false);
21
+ const lastPointer = ref({ x: 0, y: 0 });
22
+ const isError = ref(false);
23
+
24
+ let pinchStartDistance: number | null = null;
25
+ let pinchStartScale = 1;
26
+
27
+ const MIN_SCALE = 0.5;
28
+ const MAX_SCALE = 3;
29
+
30
+ function clampScale(value: number) {
31
+ return Math.min(MAX_SCALE, Math.max(MIN_SCALE, value));
32
+ }
33
+
34
+ function resetTransform() {
35
+ scale.value = 1;
36
+ translate.value = { x: 0, y: 0 };
37
+ }
38
+
39
+ function handleClose() {
40
+ emit('close');
41
+ resetTransform();
42
+ }
43
+
44
+ function handleImageError() {
45
+ isError.value = true;
46
+ }
47
+
48
+ function handleWheel(event: WheelEvent) {
49
+ event.preventDefault();
50
+ const delta = event.deltaY > 0 ? -0.2 : 0.2;
51
+ const next = clampScale(scale.value + delta);
52
+ scale.value = next;
53
+ }
54
+
55
+ function handleDoubleClick() {
56
+ scale.value = scale.value === 1 ? 2 : 1;
57
+ if (scale.value === 1) {
58
+ translate.value = { x: 0, y: 0 };
59
+ }
60
+ }
61
+
62
+ function handlePointerDown(event: PointerEvent) {
63
+ isDragging.value = true;
64
+ lastPointer.value = { x: event.clientX, y: event.clientY };
65
+ (event.target as HTMLElement)?.setPointerCapture(event.pointerId);
66
+ }
67
+
68
+ function handlePointerMove(event: PointerEvent) {
69
+ if (!isDragging.value) {
70
+ return;
71
+ }
72
+ const dx = event.clientX - lastPointer.value.x;
73
+ const dy = event.clientY - lastPointer.value.y;
74
+ translate.value = {
75
+ x: translate.value.x + dx,
76
+ y: translate.value.y + dy,
77
+ };
78
+ lastPointer.value = { x: event.clientX, y: event.clientY };
79
+ }
80
+
81
+ function handlePointerUp(event: PointerEvent) {
82
+ isDragging.value = false;
83
+ (event.target as HTMLElement)?.releasePointerCapture(event.pointerId);
84
+ }
85
+
86
+ function getTouchDistance(touches: TouchList) {
87
+ if (touches.length < 2) {
88
+ return null;
89
+ }
90
+ const [t1, t2] = [touches[0], touches[1]];
91
+ const dx = t1.clientX - t2.clientX;
92
+ const dy = t1.clientY - t2.clientY;
93
+ return Math.hypot(dx, dy);
94
+ }
95
+
96
+ function handleTouchStart(event: TouchEvent) {
97
+ if (event.touches.length === 2) {
98
+ pinchStartDistance = getTouchDistance(event.touches);
99
+ pinchStartScale = scale.value;
100
+ }
101
+ }
102
+
103
+ function handleTouchMove(event: TouchEvent) {
104
+ if (event.touches.length === 2 && pinchStartDistance) {
105
+ event.preventDefault();
106
+ const currentDistance = getTouchDistance(event.touches);
107
+ if (!currentDistance) {
108
+ return;
109
+ }
110
+ const factor = currentDistance / pinchStartDistance;
111
+ scale.value = clampScale(pinchStartScale * factor);
112
+ }
113
+ }
114
+
115
+ function handleTouchEnd() {
116
+ pinchStartDistance = null;
117
+ }
118
+
119
+ function handleKeydown(event: KeyboardEvent) {
120
+ if (event.key === 'Escape') {
121
+ handleClose();
122
+ }
123
+ }
124
+
125
+ watch(() => props.open, (isOpen) => {
126
+ if (!isOpen) {
127
+ resetTransform();
128
+ isError.value = false;
129
+ }
130
+ });
131
+
132
+ onMounted(() => {
133
+ window.addEventListener('keydown', handleKeydown);
134
+ });
135
+
136
+ onUnmounted(() => {
137
+ window.removeEventListener('keydown', handleKeydown);
138
+ });
139
+ </script>
140
+
141
+ <template>
142
+ <teleport to="body">
143
+ <div
144
+ v-if="open"
145
+ class="image-preview"
146
+ v-bind="$attrs"
147
+ >
148
+ <div class="image-preview__mask" @click="handleClose" />
149
+ <button
150
+ class="image-preview__close"
151
+ type="button"
152
+ aria-label="Close preview"
153
+ @click="handleClose"
154
+ />
155
+ <div class="image-preview__content">
156
+ <div class="image-preview__inner">
157
+ <div
158
+ class="image-preview__viewport"
159
+ >
160
+ <div :class="cs('image-preview__img-wrap', { 'image-preview__img-wrap--error': isError })">
161
+ <img
162
+ v-if="!isError"
163
+ :src="src"
164
+ :alt="alt || 'image preview'"
165
+ :style="{
166
+ transform: `translate3d(${translate.x}px, ${translate.y}px, 0) scale(${scale})`,
167
+ }"
168
+ class="image-preview__img"
169
+ draggable="false"
170
+ @error="handleImageError"
171
+ @wheel.prevent="handleWheel"
172
+ @dblclick.prevent="handleDoubleClick"
173
+ @pointerdown="handlePointerDown"
174
+ @pointermove="handlePointerMove"
175
+ @pointerup="handlePointerUp"
176
+ @pointercancel="handlePointerUp"
177
+ @touchstart.passive="handleTouchStart"
178
+ @touchmove.prevent="handleTouchMove"
179
+ @touchend="handleTouchEnd"
180
+ @touchcancel="handleTouchEnd"
181
+ >
182
+ <div v-else class="image-preview__error">
183
+ <div class="image-preview__error-icon" />
184
+ <span class="image-preview__error-text">Preview failed</span>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </teleport>
192
+ </template>
193
+
194
+ <style scoped lang="scss">
195
+ .image-preview {
196
+ position: fixed;
197
+ inset: 0;
198
+ z-index: 9999;
199
+ display: flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ color: #fff;
203
+ }
204
+
205
+ .image-preview__mask {
206
+ position: absolute;
207
+ inset: 0;
208
+ background: rgba(0, 0, 0, 0.6);
209
+ backdrop-filter: blur(2px);
210
+ z-index: 1;
211
+ }
212
+
213
+ .image-preview__close {
214
+ position: absolute;
215
+ top: 16px;
216
+ right: 16px;
217
+ width: 32px;
218
+ height: 32px;
219
+ border: none;
220
+ border-radius: 50%;
221
+ background: rgba(0, 0, 0, 0.55);
222
+ cursor: pointer;
223
+ z-index: 3;
224
+ }
225
+
226
+ .image-preview__close::before,
227
+ .image-preview__close::after {
228
+ content: "";
229
+ position: absolute;
230
+ top: 15px;
231
+ left: 8px;
232
+ width: 16px;
233
+ height: 2px;
234
+ background: #fff;
235
+ }
236
+
237
+ .image-preview__close::before {
238
+ transform: rotate(45deg);
239
+ }
240
+
241
+ .image-preview__close::after {
242
+ transform: rotate(-45deg);
243
+ }
244
+
245
+ .image-preview__content {
246
+ position: relative;
247
+ width: 100%;
248
+ height: 100%;
249
+ display: flex;
250
+ align-items: center;
251
+ justify-content: center;
252
+ padding: 0;
253
+ box-sizing: border-box;
254
+ z-index: 2;
255
+ pointer-events: none;
256
+ }
257
+
258
+ .image-preview__inner {
259
+ max-width: 100%;
260
+ max-height: 100%;
261
+ width: 100%;
262
+ height: 100%;
263
+ display: flex;
264
+ align-items: center;
265
+ justify-content: center;
266
+ pointer-events: none;
267
+ }
268
+
269
+ .image-preview__viewport {
270
+ position: relative;
271
+ max-width: 100vw;
272
+ max-height: 100vh;
273
+ width: auto;
274
+ height: auto;
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ touch-action: none;
279
+ pointer-events: none;
280
+ overflow: visible;
281
+ }
282
+
283
+ .image-preview__img-wrap {
284
+ position: relative;
285
+ max-width: 100vw;
286
+ max-height: 100vh;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ overflow: visible;
291
+ border-radius: 0;
292
+ background: transparent;
293
+ pointer-events: auto;
294
+ }
295
+
296
+ .image-preview__img {
297
+ max-width: 100vw;
298
+ max-height: 100vh;
299
+ object-fit: contain;
300
+ user-select: none;
301
+ transition: transform 0.1s ease-out;
302
+ pointer-events: auto;
303
+ }
304
+
305
+ .image-preview__img-wrap--error {
306
+ background: rgba(255, 255, 255, 0.06);
307
+ }
308
+
309
+ .image-preview__error {
310
+ display: flex;
311
+ flex-direction: column;
312
+ align-items: center;
313
+ gap: 8px;
314
+ padding: 24px;
315
+ color: #fff;
316
+ font-size: 14px;
317
+ }
318
+
319
+ .image-preview__error-icon {
320
+ width: 48px;
321
+ height: 48px;
322
+ border-radius: 50%;
323
+ background: rgba(255, 255, 255, 0.2);
324
+ position: relative;
325
+ }
326
+
327
+ .image-preview__error-icon::before,
328
+ .image-preview__error-icon::after {
329
+ content: "";
330
+ position: absolute;
331
+ top: 22px;
332
+ left: 14px;
333
+ width: 20px;
334
+ height: 2px;
335
+ background: #fff;
336
+ }
337
+
338
+ .image-preview__error-icon::before { transform: rotate(45deg); }
339
+ .image-preview__error-icon::after { transform: rotate(-45deg); }
340
+
341
+ .image-preview__error-text {
342
+ color: #fff;
343
+ }
344
+ </style>
@@ -5,28 +5,33 @@ import type { MessageModel } from '../../../types/engine';
5
5
 
6
6
  interface MessageProps {
7
7
  message: MessageModel;
8
+ nick?: string;
8
9
  alignment?: 'left' | 'right' | 'two-sided';
9
10
  messageActionList?: MessageAction[] | undefined;
10
11
  isAggregated?: boolean;
11
12
  isFirstInChunk?: boolean;
12
13
  isLastInChunk?: boolean;
13
14
  isHiddenMessageAvatar?: boolean;
15
+ isHiddenMessageNick?: boolean;
14
16
  isHiddenMessageMeta?: boolean;
15
17
  }
16
18
 
17
19
  withDefaults(defineProps<MessageProps>(), {
18
20
  alignment: 'two-sided',
21
+ nick: undefined,
19
22
  messageActionList: undefined,
20
23
  isAggregated: false,
21
24
  isFirstInChunk: undefined,
22
25
  isLastInChunk: undefined,
23
26
  isHiddenMessageAvatar: false,
27
+ isHiddenMessageNick: false,
24
28
  isHiddenMessageMeta: false,
25
29
  });
26
30
  </script>
27
31
 
28
32
  <template>
29
33
  <MessageLayout
34
+ :nick="nick"
30
35
  :message="message"
31
36
  :messageActionList="messageActionList"
32
37
  :alignment="alignment"
@@ -34,6 +39,7 @@ withDefaults(defineProps<MessageProps>(), {
34
39
  :is-first-in-chunk="isFirstInChunk"
35
40
  :is-last-in-chunk="isLastInChunk"
36
41
  :is-hidden-message-avatar="isHiddenMessageAvatar"
42
+ :is-hidden-message-nick="isHiddenMessageNick"
37
43
  :is-hidden-message-meta="isHiddenMessageMeta"
38
44
  />
39
45
  </template>
@@ -25,9 +25,11 @@ import type { MessageModel } from '../../../../types/engine';
25
25
 
26
26
  interface MessageLayoutProps {
27
27
  message: MessageModel;
28
+ nick?: string;
28
29
  isAggregated?: boolean;
29
30
  isHiddenMessageAvatar?: boolean;
30
31
  isHiddenMessageMeta?: boolean;
32
+ isHiddenMessageNick?: boolean;
31
33
  isFirstInChunk?: boolean;
32
34
  isLastInChunk?: boolean;
33
35
  alignment?: 'left' | 'right' | 'two-sided';
@@ -39,9 +41,11 @@ interface MessageLayoutProps {
39
41
  const props = withDefaults(defineProps<MessageLayoutProps>(), {
40
42
  message: () => ({}) as MessageModel,
41
43
  alignment: 'two-sided',
44
+ nick: undefined,
42
45
  isAggregated: false,
43
46
  isHiddenMessageAvatar: false,
44
47
  isHiddenMessageMeta: false,
48
+ isHiddenMessageNick: false,
45
49
  isFirstInChunk: undefined,
46
50
  isLastInChunk: undefined,
47
51
  messageActionList: undefined,
@@ -125,6 +129,9 @@ function handleReadReceiptClose() {
125
129
  :src="message.avatar"
126
130
  />
127
131
  <View :class="cs(wrapperClasses)">
132
+ <View v-if="!isHiddenMessageNick" :class="cs('message-layout__nick')">
133
+ {{ props.nick || message.nameCard || message.nick || message.from }}
134
+ </View>
128
135
  <MessageBubble
129
136
  :class="bubbleClasses"
130
137
  :message="message"
@@ -186,7 +193,7 @@ $message-avatar-gap: 8px;
186
193
 
187
194
  &__nick {
188
195
  font-size: 12px;
189
- min-width: min(70%, 200px);
196
+ max-width: min(70%, 120px);
190
197
  color: var(--text-color-tertiary);
191
198
  @include mixin.text-ellipsis;
192
199
  }
@@ -54,13 +54,22 @@ const readReceiptText = computed(() => {
54
54
  return '';
55
55
  }
56
56
 
57
+ const {
58
+ readCount,
59
+ unreadCount,
60
+ isPeerRead,
61
+ } = props.readReceiptInfo;
62
+
57
63
  if (props.isGroup) {
58
- if (props.readReceiptInfo.unreadCount > 0) {
59
- return `${props.readReceiptInfo.unreadCount} ${t('MessageList.unread')} ·`;
64
+ if (unreadCount === undefined || readCount === undefined) {
65
+ return '';
66
+ }
67
+ if (unreadCount > 0) {
68
+ return `${unreadCount} ${t('MessageList.unread')} ·`;
60
69
  }
61
70
  return `${t('MessageList.all_read')} ·`;
62
71
  }
63
- if (props.readReceiptInfo.isPeerRead) {
72
+ if (isPeerRead) {
64
73
  return `${t('MessageList.read')} ·`;
65
74
  }
66
75
  return `${t('MessageList.unread')} ·`;