index.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. <template>
  2. <div
  3. v-if="hasQuoteContent"
  4. :class="{
  5. 'reference-content': true,
  6. 'reverse': message.flow === 'out',
  7. }"
  8. @click="scrollToOriginalMessage"
  9. >
  10. <div
  11. v-if="isMessageRevoked"
  12. class="revoked-text"
  13. >
  14. {{ TUITranslateService.t('TUIChat.引用内容已撤回') }}
  15. </div>
  16. <div
  17. v-else
  18. class="max-double-line"
  19. >
  20. {{ messageQuoteContent.messageSender }}: {{ transformTextWithKeysToEmojiNames(messageQuoteText) }}
  21. </div>
  22. </div>
  23. </template>
  24. <script lang="ts" setup>
  25. import { computed, ref, onMounted } from '../../../../../adapter-vue';
  26. import {
  27. TUIStore,
  28. StoreName,
  29. IMessageModel,
  30. TUITranslateService,
  31. } from '@tencentcloud/chat-uikit-engine';
  32. import { getBoundingClientRect, getScrollInfo } from '@tencentcloud/universal-api';
  33. import { isUniFrameWork } from '../../../../../utils/env';
  34. import { Toast, TOAST_TYPE } from '../../../../../components/common/Toast/index';
  35. import { ICloudCustomData, IQuoteContent, MessageQuoteTypeEnum } from './interface.ts';
  36. import { transformTextWithKeysToEmojiNames } from '../../../emoji-config';
  37. export interface IProps {
  38. message: IMessageModel;
  39. }
  40. export interface IEmits {
  41. (e: 'scrollTo', scrollHeight: number): void;
  42. (e: 'blinkMessage', messageID: string | undefined): void;
  43. }
  44. const emits = defineEmits<IEmits>();
  45. const props = withDefaults(defineProps<IProps>(), {
  46. message: () => ({} as IMessageModel),
  47. });
  48. let selfAddValue = 0;
  49. const messageQuoteText = ref<string>('');
  50. const hasQuoteContent = ref(false);
  51. const messageQuoteContent = ref<IQuoteContent>({} as IQuoteContent);
  52. const isMessageRevoked = computed<boolean>(() => {
  53. try {
  54. const cloudCustomData: ICloudCustomData = JSON.parse(props.message?.cloudCustomData || '{}');
  55. const quotedMessageModel = TUIStore.getMessageModel(cloudCustomData.messageReply.messageID);
  56. return quotedMessageModel?.isRevoked;
  57. } catch (error) {
  58. return true;
  59. }
  60. });
  61. onMounted(() => {
  62. try {
  63. const cloudCustomData: ICloudCustomData = JSON.parse(props.message?.cloudCustomData || '{}');
  64. hasQuoteContent.value = Boolean(cloudCustomData.messageReply);
  65. if (hasQuoteContent.value) {
  66. messageQuoteContent.value = cloudCustomData.messageReply;
  67. messageQuoteText.value = performQuoteContent(messageQuoteContent.value);
  68. }
  69. } catch (error) {
  70. hasQuoteContent.value = false;
  71. }
  72. });
  73. function performQuoteContent(params: IQuoteContent) {
  74. let messageKey: string = '';
  75. let quoteContent: string = '';
  76. switch (params.messageType) {
  77. case MessageQuoteTypeEnum.TYPE_TEXT:
  78. messageKey = '[文本]';
  79. break;
  80. case MessageQuoteTypeEnum.TYPE_CUSTOM:
  81. messageKey = '[自定义消息]';
  82. break;
  83. case MessageQuoteTypeEnum.TYPE_IMAGE:
  84. messageKey = '[图片]';
  85. break;
  86. case MessageQuoteTypeEnum.TYPE_SOUND:
  87. messageKey = '[音频]';
  88. break;
  89. case MessageQuoteTypeEnum.TYPE_VIDEO:
  90. messageKey = '[视频]';
  91. break;
  92. case MessageQuoteTypeEnum.TYPE_FILE:
  93. messageKey = '[文件]';
  94. break;
  95. case MessageQuoteTypeEnum.TYPE_LOCATION:
  96. messageKey = '[地理位置]';
  97. break;
  98. case MessageQuoteTypeEnum.TYPE_FACE:
  99. messageKey = '[动画表情]';
  100. break;
  101. case MessageQuoteTypeEnum.TYPE_GROUP_TIPS:
  102. messageKey = '[群提示]';
  103. break;
  104. case MessageQuoteTypeEnum.TYPE_MERGER:
  105. messageKey = '[聊天记录]';
  106. break;
  107. default:
  108. messageKey = '[消息]';
  109. break;
  110. }
  111. if (
  112. [
  113. MessageQuoteTypeEnum.TYPE_TEXT,
  114. MessageQuoteTypeEnum.TYPE_MERGER,
  115. ].includes(params.messageType)
  116. ) {
  117. quoteContent = params.messageAbstract;
  118. }
  119. return quoteContent ? quoteContent : TUITranslateService.t(`TUIChat.${messageKey}`);
  120. }
  121. async function scrollToOriginalMessage() {
  122. if (isMessageRevoked.value) {
  123. return;
  124. }
  125. const originMessageID = messageQuoteContent.value?.messageID;
  126. const currentMessageList = TUIStore.getData(StoreName.CHAT, 'messageList');
  127. const isOriginalMessageInScreen = currentMessageList.some(msg => msg.ID === originMessageID);
  128. if (originMessageID && isOriginalMessageInScreen) {
  129. try {
  130. const scrollViewRect = await getBoundingClientRect('#messageScrollList', 'messageList');
  131. const originalMessageRect = await getBoundingClientRect('#tui-' + originMessageID, 'messageList');
  132. const { scrollTop } = await getScrollInfo('#messageScrollList', 'messageList');
  133. const finalScrollTop = originalMessageRect.top + scrollTop - scrollViewRect.top - (selfAddValue++ % 2);
  134. const isNeedScroll = originalMessageRect.top < scrollViewRect.top;
  135. if (!isUniFrameWork && window) {
  136. const scrollView = document.getElementById('messageScrollList');
  137. if (isNeedScroll && scrollView) {
  138. scrollView.scrollTop = finalScrollTop;
  139. }
  140. } else if (isUniFrameWork && isNeedScroll) {
  141. emits('scrollTo', finalScrollTop);
  142. }
  143. emits('blinkMessage', originMessageID);
  144. } catch (error) {
  145. console.error(error);
  146. }
  147. } else {
  148. Toast({
  149. message: TUITranslateService.t('TUIChat.无法定位到原消息'),
  150. type: TOAST_TYPE.WARNING,
  151. });
  152. }
  153. }
  154. </script>
  155. <style lang="scss" scoped>
  156. .reference-content {
  157. max-width: 272px;
  158. margin-top: 4px;
  159. margin-left: 44px;
  160. padding: 12px;
  161. font-size: 12px;
  162. color: #666;
  163. word-wrap: break-word;
  164. word-break: break-all;
  165. background-color: #fbfbfb;
  166. border-radius: 8px;
  167. line-height: 16.8px;
  168. cursor: pointer;
  169. -webkit-tap-highlight-color: transparent;
  170. }
  171. .reverse.reference-content {
  172. margin-right: 44px;
  173. margin-left: auto;
  174. }
  175. .revoked-text {
  176. color: #999;
  177. }
  178. .max-double-line {
  179. word-break: break-all;
  180. overflow: hidden;
  181. display: -webkit-box;
  182. max-height: 33px;
  183. -webkit-line-clamp: 2;
  184. -webkit-box-orient: vertical;
  185. }
  186. </style>