index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <template>
  2. <div
  3. v-if="!isAllActionItemInvalid && !messageItem.hasRiskContent"
  4. ref="messageToolDom"
  5. :class="['dialog-item', !isPC ? 'dialog-item-h5' : 'dialog-item-web']"
  6. >
  7. <slot
  8. v-if="featureConfig.EmojiReaction"
  9. name="TUIEmojiPlugin"
  10. />
  11. <div
  12. class="dialog-item-list"
  13. :class="!isPC ? 'dialog-item-list-h5' : 'dialog-item-list-web'"
  14. >
  15. <template v-for="(item, index) in actionItems">
  16. <div
  17. v-if="item.renderCondition()"
  18. :key="item.key"
  19. class="list-item"
  20. @click="getFunction(index)"
  21. @mousedown="beforeCopy(item.key)"
  22. >
  23. <Icon
  24. :file="item.iconUrl"
  25. :size="'15px'"
  26. />
  27. <span class="list-item-text">{{ item.text }}</span>
  28. </div>
  29. </template>
  30. </div>
  31. </div>
  32. </template>
  33. <script lang="ts" setup>
  34. import TUIChatEngine, {
  35. TUIStore,
  36. StoreName,
  37. TUITranslateService,
  38. IMessageModel,
  39. } from '@tencentcloud/chat-uikit-engine';
  40. import { TUIGlobal } from '@tencentcloud/universal-api';
  41. import { ref, watchEffect, computed, onMounted, onUnmounted } from '../../../../adapter-vue';
  42. import Icon from '../../../common/Icon.vue';
  43. import { Toast, TOAST_TYPE } from '../../../common/Toast/index';
  44. import delIcon from '../../../../assets/icon/msg-del.svg';
  45. import copyIcon from '../../../../assets/icon/msg-copy.svg';
  46. import quoteIcon from '../../../../assets/icon/msg-quote.svg';
  47. import revokeIcon from '../../../../assets/icon/msg-revoke.svg';
  48. import forwardIcon from '../../../../assets/icon/msg-forward.svg';
  49. import translateIcon from '../../../../assets/icon/translate.svg';
  50. import multipleSelectIcon from '../../../../assets/icon/multiple-select.svg';
  51. import convertText from '../../../../assets/icon/convertText_zh.svg';
  52. import { enableSampleTaskStatus } from '../../../../utils/enableSampleTaskStatus';
  53. import { transformTextWithKeysToEmojiNames } from '../../emoji-config';
  54. import { isH5, isPC, isUniFrameWork } from '../../../../utils/env';
  55. import { ITranslateInfo, IConvertInfo } from '../../../../interface';
  56. import TUIChatConfig from '../../config';
  57. // uni-app conditional compilation will not run the following code
  58. // #ifndef APP || APP-PLUS || MP || H5
  59. import CopyManager from '../../utils/copy';
  60. // #endif
  61. interface IProps {
  62. messageItem: IMessageModel;
  63. isMultipleSelectMode: boolean;
  64. }
  65. interface IEmits {
  66. (key: 'toggleMultipleSelectMode'): void;
  67. }
  68. const emits = defineEmits<IEmits>();
  69. const props = withDefaults(defineProps<IProps>(), {
  70. isMultipleSelectMode: false,
  71. messageItem: () => ({}) as IMessageModel,
  72. });
  73. const featureConfig = TUIChatConfig.getFeatureConfig();
  74. const TYPES = TUIChatEngine.TYPES;
  75. const actionItems = ref([
  76. {
  77. key: 'open',
  78. text: TUITranslateService.t('TUIChat.打开'),
  79. iconUrl: copyIcon,
  80. renderCondition() {
  81. if (!featureConfig.DownloadFile || !message.value) return false;
  82. return isPC && (message.value?.type === TYPES.MSG_FILE
  83. || message.value.type === TYPES.MSG_VIDEO
  84. || message.value.type === TYPES.MSG_IMAGE);
  85. },
  86. clickEvent: openMessage,
  87. },
  88. {
  89. key: 'copy',
  90. text: TUITranslateService.t('TUIChat.复制'),
  91. iconUrl: copyIcon,
  92. renderCondition() {
  93. if (!featureConfig.CopyMessage || !message.value) return false;
  94. return message.value.type === TYPES.MSG_TEXT;
  95. },
  96. clickEvent: copyMessage,
  97. },
  98. {
  99. key: 'revoke',
  100. text: TUITranslateService.t('TUIChat.撤回'),
  101. iconUrl: revokeIcon,
  102. renderCondition() {
  103. if (!featureConfig.RevokeMessage || !message.value) return false;
  104. return message.value.flow === 'out' && message.value.status === 'success';
  105. },
  106. clickEvent: revokeMessage,
  107. },
  108. {
  109. key: 'delete',
  110. text: TUITranslateService.t('TUIChat.删除'),
  111. iconUrl: delIcon,
  112. renderCondition() {
  113. if (!featureConfig.DeleteMessage || !message.value) return false;
  114. return message.value.status === 'success';
  115. },
  116. clickEvent: deleteMessage,
  117. },
  118. {
  119. key: 'forward',
  120. text: TUITranslateService.t('TUIChat.转发'),
  121. iconUrl: forwardIcon,
  122. renderCondition() {
  123. if (!featureConfig.ForwardMessage || !message.value) return false;
  124. return message.value.status === 'success';
  125. },
  126. clickEvent: forwardSingleMessage,
  127. },
  128. {
  129. key: 'quote',
  130. text: TUITranslateService.t('TUIChat.引用'),
  131. iconUrl: quoteIcon,
  132. renderCondition() {
  133. if (!featureConfig.QuoteMessage || !message.value) return false;
  134. const _message = TUIStore.getMessageModel(message.value.ID);
  135. return message.value.status === 'success' && !_message.getSignalingInfo();
  136. },
  137. clickEvent: quoteMessage,
  138. },
  139. {
  140. key: 'translate',
  141. text: TUITranslateService.t('TUIChat.翻译'),
  142. visible: false,
  143. iconUrl: translateIcon,
  144. renderCondition() {
  145. if (!featureConfig.TranslateMessage || !message.value) return false;
  146. return message.value.status === 'success' && message.value.type === TYPES.MSG_TEXT;
  147. },
  148. clickEvent: translateMessage,
  149. },
  150. {
  151. key: 'convert',
  152. text: TUITranslateService.t('TUIChat.转文字'),
  153. visible: false,
  154. iconUrl: convertText,
  155. renderCondition() {
  156. if (!featureConfig.VoiceToText || !message.value) return false;
  157. return message.value.status === 'success' && message.value.type === TYPES.MSG_AUDIO;
  158. },
  159. clickEvent: convertVoiceToText,
  160. },
  161. {
  162. key: 'multi-select',
  163. text: TUITranslateService.t('TUIChat.多选'),
  164. iconUrl: multipleSelectIcon,
  165. renderCondition() {
  166. if (!featureConfig.MultiSelection || !message.value) return false;
  167. return message.value.status === 'success';
  168. },
  169. clickEvent: multipleSelectMessage,
  170. },
  171. ]);
  172. const message = ref<IMessageModel>();
  173. const messageToolDom = ref<HTMLElement>();
  174. onMounted(() => {
  175. TUIStore.watch(StoreName.CHAT, {
  176. translateTextInfo: onMessageTranslationInfoUpdated,
  177. voiceToTextInfo: onMessageConvertInfoUpdated,
  178. });
  179. });
  180. onUnmounted(() => {
  181. TUIStore.unwatch(StoreName.CHAT, {
  182. translateTextInfo: onMessageTranslationInfoUpdated,
  183. voiceToTextInfo: onMessageConvertInfoUpdated,
  184. });
  185. });
  186. watchEffect(() => {
  187. message.value = TUIStore.getMessageModel(props.messageItem.ID);
  188. });
  189. const isAllActionItemInvalid = computed(() => {
  190. for (let i = 0; i < actionItems.value.length; ++i) {
  191. if (actionItems.value[i].renderCondition()) {
  192. return false;
  193. }
  194. }
  195. return true;
  196. });
  197. function getFunction(index: number) {
  198. // Compatible with Vue2 and WeChat Mini Program syntax, dynamic binding is not allowed.
  199. actionItems.value[index].clickEvent();
  200. }
  201. function openMessage() {
  202. let url = '';
  203. switch (message.value?.type) {
  204. case TUIChatEngine.TYPES.MSG_FILE:
  205. url = message.value.payload.fileUrl;
  206. break;
  207. case TUIChatEngine.TYPES.MSG_VIDEO:
  208. url = message.value.payload.remoteVideoUrl;
  209. break;
  210. case TUIChatEngine.TYPES.MSG_IMAGE:
  211. url = message.value.payload.imageInfoArray[0].url;
  212. break;
  213. }
  214. window?.open(url, '_blank');
  215. }
  216. function revokeMessage() {
  217. if (!message.value) return;
  218. const messageModel = TUIStore.getMessageModel(message.value.ID);
  219. messageModel
  220. .revokeMessage()
  221. .then(() => {
  222. enableSampleTaskStatus('revokeMessage');
  223. })
  224. .catch((error: any) => {
  225. if (error.code === 20016) {
  226. const message = TUITranslateService.t('TUIChat.已过撤回时限');
  227. Toast({
  228. message,
  229. type: TOAST_TYPE.ERROR,
  230. });
  231. }
  232. });
  233. }
  234. function deleteMessage() {
  235. if (!message.value) return;
  236. const messageModel = TUIStore.getMessageModel(message.value.ID);
  237. messageModel.deleteMessage();
  238. }
  239. async function copyMessage() {
  240. if (isUniFrameWork) {
  241. TUIGlobal?.setClipboardData({
  242. data: transformTextWithKeysToEmojiNames(message.value?.payload?.text),
  243. });
  244. } else {
  245. // uni-app conditional compilation will not run the following code
  246. // #ifndef APP || APP-PLUS || MP || H5
  247. CopyManager.copySelection(message.value?.payload?.text);
  248. // #endif
  249. }
  250. }
  251. function beforeCopy(key: string) {
  252. // only pc support copy selection or copy full message text
  253. // uni-app and h5 only support copy full message text
  254. if (key !== 'copy' || isH5) {
  255. return;
  256. }
  257. // uni-app conditional compilation will not run the following code
  258. // #ifndef APP || APP-PLUS || MP || H5
  259. CopyManager.saveCurrentSelection();
  260. // #endif
  261. }
  262. function forwardSingleMessage() {
  263. if (!message.value) return;
  264. TUIStore.update(StoreName.CUSTOM, 'singleForwardMessageID', message.value.ID);
  265. }
  266. function quoteMessage() {
  267. if (!message.value) return;
  268. message.value.quoteMessage();
  269. }
  270. function translateMessage() {
  271. const enable = TUIStore.getData(StoreName.APP, 'enabledTranslationPlugin');
  272. if (!enable) {
  273. Toast({
  274. message: TUITranslateService.t('TUIChat.请开通翻译功能'),
  275. type: TOAST_TYPE.WARNING,
  276. });
  277. return;
  278. }
  279. if (!message.value) return;
  280. const index = actionItems.value.findIndex(item => item.key === 'translate');
  281. TUIStore.update(StoreName.CHAT, 'translateTextInfo', {
  282. conversationID: message.value.conversationID,
  283. messageID: message.value.ID,
  284. visible: !actionItems.value[index].visible,
  285. });
  286. }
  287. function convertVoiceToText() {
  288. const enable = TUIStore.getData(StoreName.APP, 'enabledVoiceToText');
  289. if (!enable) {
  290. Toast({
  291. message: TUITranslateService.t('TUIChat.请开通语音转文字功能'),
  292. });
  293. return;
  294. }
  295. if (!message.value) return;
  296. const index = actionItems.value.findIndex(item => item.key === 'convert');
  297. TUIStore.update(StoreName.CHAT, 'voiceToTextInfo', {
  298. conversationID: message.value.conversationID,
  299. messageID: message.value.ID,
  300. visible: !actionItems.value[index].visible,
  301. });
  302. }
  303. function multipleSelectMessage() {
  304. emits('toggleMultipleSelectMode');
  305. }
  306. function onMessageTranslationInfoUpdated(info: Map<string, ITranslateInfo[]>) {
  307. if (info === undefined) return;
  308. const translationInfoList = info.get(props.messageItem.conversationID) || [];
  309. const idx = actionItems.value.findIndex(item => item.key === 'translate');
  310. for (let i = 0; i < translationInfoList.length; ++i) {
  311. const { messageID, visible } = translationInfoList[i];
  312. if (messageID === props.messageItem.ID) {
  313. actionItems.value[idx].text = TUITranslateService.t(visible ? 'TUIChat.隐藏' : 'TUIChat.翻译');
  314. actionItems.value[idx].visible = !!visible;
  315. return;
  316. }
  317. }
  318. actionItems.value[idx].text = TUITranslateService.t('TUIChat.翻译');
  319. }
  320. function onMessageConvertInfoUpdated(info: Map<string, IConvertInfo[]>) {
  321. if (info === undefined) return;
  322. const convertInfoList = info.get(props.messageItem.conversationID) || [];
  323. const idx = actionItems.value.findIndex(item => item.key === 'convert');
  324. for (let i = 0; i < convertInfoList.length; ++i) {
  325. const { messageID, visible } = convertInfoList[i];
  326. if (messageID === props.messageItem.ID) {
  327. actionItems.value[idx].text = TUITranslateService.t(visible ? 'TUIChat.隐藏' : 'TUIChat.转文字');
  328. actionItems.value[idx].visible = !!visible;
  329. return;
  330. }
  331. }
  332. actionItems.value[idx].text = TUITranslateService.t('TUIChat.转文字');
  333. }
  334. defineExpose({
  335. messageToolDom,
  336. });
  337. </script>
  338. <style lang="scss" scoped>
  339. @import "../../../../assets/styles/common";
  340. .dialog-item-web {
  341. background: #fff;
  342. border-radius: 8px;
  343. border: 1px solid #e0e0e0;
  344. padding: 12px 0;
  345. .dialog-item-list {
  346. display: flex;
  347. align-items: baseline;
  348. white-space: nowrap;
  349. flex-wrap: wrap;
  350. max-width: 280px;
  351. .list-item {
  352. padding: 4px 12px;
  353. display: flex;
  354. flex-direction: row;
  355. align-items: center;
  356. .list-item-text {
  357. padding-left: 4px;
  358. font-size: 12px;
  359. line-height: 17px;
  360. }
  361. }
  362. }
  363. }
  364. .dialog-item-h5 {
  365. @extend .dialog-item-web;
  366. padding: 0;
  367. .dialog-item-list {
  368. margin: 10px;
  369. white-space: nowrap;
  370. flex-wrap: wrap;
  371. max-width: 280px;
  372. .list-item {
  373. padding: 0 8px;
  374. display: flex;
  375. flex-direction: column;
  376. align-items: center;
  377. color: #4f4f4f;
  378. .list-item-text {
  379. padding-left: 0;
  380. }
  381. }
  382. }
  383. }
  384. </style>