From ec49d2de020a4c8eac96cb92915788dcec3eac69 Mon Sep 17 00:00:00 2001 From: yuanfazheng Date: Thu, 27 Aug 2026 21:36:58 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90MiniProgram=E3=80=91update=20Miniprogr?= =?UTF-8?q?am?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MiniProgram/package.json | 10 +- .../roomkit/TUIRoom/assets/icons/FileIcon.svg | 12 + .../TUIRoom/assets/icons/ImageIcon.svg | 14 + .../icons/LogoOfPCInChineseBlackIcon.svg | 58 --- .../icons/LogoOfPCInChineseWhiteIcon.svg | 58 --- .../roomkit/TUIRoom/assets/icons/PlusIcon.svg | 14 + .../components/Chat/ChatEditor/index.vue | 262 ++++++++--- .../Chat/ChatEditor/useChatEditor.ts | 285 +++++++++++- .../components/Chat/EditorTools/index.vue | 11 +- .../components/Chat/MessageList/index.vue | 306 +++++++++++-- .../Chat/MessageList/useMessageListHook.ts | 45 +- .../Chat/MessageTypes/MessageFile.vue | 273 +++++++++++ .../Chat/MessageTypes/MessageImage.vue | 69 +++ .../TUIRoom/components/Chat/index/index.vue | 25 +- .../roomkit/TUIRoom/components/Chat/util.ts | 117 +++++ .../RoomContent/StreamContainer/index.vue | 25 +- .../components/RoomFooter/AudioControl.vue | 14 + .../components/RoomFooter/VideoControl.vue | 14 + .../components/RoomHome/RoomControl/index.vue | 18 +- .../Stream/common/StreamRegion/index.vue | 155 ++++--- .../TUIRoom/components/common/Logo.vue | 13 - .../components/common/base/PopUpH5.vue | 10 +- .../components/common/base/SvgIcon.vue | 5 +- .../src/roomkit/TUIRoom/conference.vue | 28 ++ .../roomkit/TUIRoom/hooks/useLocalPusher.ts | 221 +++++++++ .../roomkit/TUIRoom/hooks/useWxMediaGuard.ts | 285 ++++++++++++ .../src/roomkit/TUIRoom/locales/en-US.ts | 40 ++ .../src/roomkit/TUIRoom/locales/zh-CN.ts | 40 ++ .../TUIRoom/services/function/errorHandler.ts | 47 +- .../services/function/livePusherError.ts | 104 +++++ .../TUIRoom/services/manager/mediaManager.ts | 8 +- .../services/manager/roomActionManager.ts | 236 +++++++--- .../roomkit/TUIRoom/services/roomService.ts | 4 + .../src/roomkit/TUIRoom/services/types.ts | 7 + .../src/roomkit/TUIRoom/stores/chat.ts | 47 +- .../src/roomkit/TUIRoom/stores/room.ts | 12 +- .../src/roomkit/TUIRoom/utils/environment.ts | 39 ++ .../src/roomkit/TUIRoom/utils/serialTask.ts | 15 + .../roomkit/TUIRoom/utils/tuiKeyFeatures.ts | 44 ++ .../src/roomkit/TUIRoom/utils/wxPermission.ts | 427 ++++++++++++++++++ MiniProgram/src/roomkit/pages/home.vue | 5 + MiniProgram/src/roomkit/pages/room.vue | 6 + MiniProgram/src/static/RTCRoomEngine.wasm.br | Bin 284396 -> 284787 bytes MiniProgram/wxmini_dev.bat | 2 +- MiniProgram/wxmini_dev.sh | 4 +- MiniProgram/wxmini_prod.bat | 2 +- MiniProgram/wxmini_prod.sh | 4 +- 47 files changed, 3021 insertions(+), 419 deletions(-) create mode 100644 MiniProgram/src/roomkit/TUIRoom/assets/icons/FileIcon.svg create mode 100644 MiniProgram/src/roomkit/TUIRoom/assets/icons/ImageIcon.svg delete mode 100644 MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseBlackIcon.svg delete mode 100644 MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseWhiteIcon.svg create mode 100644 MiniProgram/src/roomkit/TUIRoom/assets/icons/PlusIcon.svg create mode 100644 MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageFile.vue create mode 100644 MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageImage.vue create mode 100644 MiniProgram/src/roomkit/TUIRoom/hooks/useLocalPusher.ts create mode 100644 MiniProgram/src/roomkit/TUIRoom/hooks/useWxMediaGuard.ts create mode 100644 MiniProgram/src/roomkit/TUIRoom/services/function/livePusherError.ts create mode 100644 MiniProgram/src/roomkit/TUIRoom/utils/serialTask.ts create mode 100644 MiniProgram/src/roomkit/TUIRoom/utils/tuiKeyFeatures.ts create mode 100644 MiniProgram/src/roomkit/TUIRoom/utils/wxPermission.ts diff --git a/MiniProgram/package.json b/MiniProgram/package.json index db1f6a77f..e4ab03002 100644 --- a/MiniProgram/package.json +++ b/MiniProgram/package.json @@ -1,6 +1,6 @@ { "name": "@tencentcloud/tui-roomkit-uniapp", - "version": "3.1.2", + "version": "3.1.4", "bin": { "configureProject": "./configureProject_bin.js" }, @@ -58,10 +58,10 @@ "@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3070720230316001", "@dcloudio/vue-cli-plugin-uni-optimize": "2.0.2-3080720230703001", "@tencentcloud/chat": "^3.5.2", - "@tencentcloud/trtc-component-wx": "^1.0.6", + "@tencentcloud/trtc-component-wx": "2.0.0", "@tencentcloud/tui-core": "latest", - "@tencentcloud/tuiroom-engine-wx": "~3.1.1", - "@tencentcloud/uikit-base-component-uni": "0.3.3", + "@tencentcloud/tuiroom-engine-wx": "~3.1.2", + "@tencentcloud/uikit-base-component-uni": "1.1.0", "@tencentcloud/universal-api": "^2.0.9", "axios": "^0.27.2", "inquirer": "^9.2.11", @@ -86,7 +86,7 @@ "@vue/tsconfig": "^0.1.3", "fs-sync": "^1.0.6", "typescript": "^4.9.4", - "vite": "4.0.4", + "vite": "^4.0.5", "vue-tsc": "^1.0.24" } } diff --git a/MiniProgram/src/roomkit/TUIRoom/assets/icons/FileIcon.svg b/MiniProgram/src/roomkit/TUIRoom/assets/icons/FileIcon.svg new file mode 100644 index 000000000..5e73f98d1 --- /dev/null +++ b/MiniProgram/src/roomkit/TUIRoom/assets/icons/FileIcon.svg @@ -0,0 +1,12 @@ + + + diff --git a/MiniProgram/src/roomkit/TUIRoom/assets/icons/ImageIcon.svg b/MiniProgram/src/roomkit/TUIRoom/assets/icons/ImageIcon.svg new file mode 100644 index 000000000..06c8d4f8a --- /dev/null +++ b/MiniProgram/src/roomkit/TUIRoom/assets/icons/ImageIcon.svg @@ -0,0 +1,14 @@ + + + diff --git a/MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseBlackIcon.svg b/MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseBlackIcon.svg deleted file mode 100644 index f26c63dab..000000000 --- a/MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseBlackIcon.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - diff --git a/MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseWhiteIcon.svg b/MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseWhiteIcon.svg deleted file mode 100644 index 3b26204e6..000000000 --- a/MiniProgram/src/roomkit/TUIRoom/assets/icons/LogoOfPCInChineseWhiteIcon.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - diff --git a/MiniProgram/src/roomkit/TUIRoom/assets/icons/PlusIcon.svg b/MiniProgram/src/roomkit/TUIRoom/assets/icons/PlusIcon.svg new file mode 100644 index 000000000..d96b5a02d --- /dev/null +++ b/MiniProgram/src/roomkit/TUIRoom/assets/icons/PlusIcon.svg @@ -0,0 +1,14 @@ + + + diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/index.vue b/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/index.vue index 5339679bf..e9907aef2 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/index.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/index.vue @@ -1,13 +1,14 @@ @@ -38,98 +90,166 @@ import emoji from '../EditorTools/index.vue'; import useChatEditor from './useChatEditor'; import SvgIcon from '../../common/base/SvgIcon.vue'; import EmojiIcon from '../../../assets/icons/EmojiIcon.svg'; +import PlusIcon from '../../../assets/icons/PlusIcon.svg'; +import ImageIcon from '../../../assets/icons/ImageIcon.svg'; +import FileIcon from '../../../assets/icons/FileIcon.svg'; + const { t, editorInputEle, sendMsg, + hasSendContent, + keyboardHeight, + handleKeyboardHeightChange, isEmojiToolbarVisible, + isActionPanelVisible, + showEmojiPanel, + showActionPanel, isMessageDisabled, sendMessage, + chooseImage, + chooseFile, handleChooseEmoji, - togglePopover, + toggleEmojiPanel, + toggleActionPanel, } = useChatEditor(); diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/useChatEditor.ts b/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/useChatEditor.ts index 46ebdc5c9..573a892f7 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/useChatEditor.ts +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/ChatEditor/useChatEditor.ts @@ -1,14 +1,22 @@ -import { ref, watch } from 'vue'; +import { computed, onBeforeUnmount, ref, watch } from 'vue'; import { storeToRefs } from 'pinia'; import TUIMessage from '../../common/base/Message/index'; +import TUIMessageBox from '../../common/base/MessageBox/index'; import { TencentCloudChat } from '@tencentcloud/tuiroom-engine-wx'; +import TIMUploadPlugin from 'tim-upload-plugin'; import useGetRoomEngine from '../../../hooks/useRoomEngine'; import { useChatStore } from '../../../stores/chat'; import { useRoomStore } from '../../../stores/room'; import { useI18n } from '../../../locales'; import { useBasicStore } from '../../../stores/basic'; -import { decodeSendTextMsg } from '../util'; +import { + decodeSendTextMsg, + formatFilePayload, + formatImageInfoArray, + MAX_FILE_SIZE, + MESSAGE_TYPE, +} from '../util'; export default function useChatEditor() { const roomEngine = useGetRoomEngine(); @@ -18,22 +26,100 @@ export default function useChatEditor() { const roomStore = useRoomStore(); const { roomId } = storeToRefs(basicStore); - const { isMessageDisabled } = storeToRefs(chatStore); + const { isMessageDisabled, keyboardHeight } = storeToRefs(chatStore); const editorInputEle = ref(); const sendMsg = ref(''); const isEmojiToolbarVisible = ref(false); + const isActionPanelVisible = ref(false); + const hasSendContent = computed(() => sendMsg.value.trim().length > 0); + + /** + * A panel and the keyboard occupy the same space at the bottom, so both are + * driven by the keyboard height. That way they are swapped within a single + * render, instead of one of them lagging a frame behind the other. + **/ + const showEmojiPanel = computed( + () => isEmojiToolbarVisible.value && keyboardHeight.value === 0 + ); + const showActionPanel = computed( + () => isActionPanelVisible.value && keyboardHeight.value === 0 + ); + + const closeToolPanels = () => { + isEmojiToolbarVisible.value = false; + isActionPanelVisible.value = false; + }; + + /** + * Pushing the page up would move the sidebar header and the message list off + * screen, so the editor reserves the keyboard height itself instead. + **/ + const handleKeyboardHeightChange = (event: any) => { + const height = event?.detail?.height || 0; + chatStore.setKeyboardHeight(height); + // Typing replaces the panel, which must not come back once the keyboard hides. + if (height > 0) { + closeToolPanels(); + } + }; + + const hideKeyboard = () => { + if (keyboardHeight.value > 0) { + uni.hideKeyboard(); + } + }; + + onBeforeUnmount(() => { + chatStore.setKeyboardHeight(0); + }); + + watch( + [isEmojiToolbarVisible, isActionPanelVisible], + ([isEmojiOpen, isActionOpen]) => { + chatStore.setToolPanelOpen(isEmojiOpen || isActionOpen); + } + ); + + watch( + () => chatStore.isToolPanelOpen, + isOpen => { + if (!isOpen) { + closeToolPanels(); + } + } + ); + watch(isMessageDisabled, value => { if (value) { sendMsg.value = ''; + closeToolPanels(); } }); + const getSenderName = () => + roomStore.localUser.nameCard || + roomStore.localUser.userName || + roomStore.localUser.userId; + + const getTim = () => { + const tim = roomEngine.instance?.getTIM(); + if (!tim) { + throw new Error('tim is null'); + } + /** + * Sending an image or file requires the upload plugin, which the room + * engine does not register on its internal Chat instance. + **/ + tim.registerPlugin({ 'tim-upload-plugin': TIMUploadPlugin }); + return tim; + }; + const sendMessage = async () => { const result = decodeSendTextMsg(sendMsg.value); if (result === '') { return; } sendMsg.value = ''; - isEmojiToolbarVisible.value = false; + closeToolPanels(); try { const tim = roomEngine.instance?.getTIM(); if (!tim) { @@ -49,14 +135,11 @@ export default function useChatEditor() { await tim.sendMessage(message); chatStore.updateMessageList({ ID: Math.random().toString(), - type: 'TIMTextElem', + type: MESSAGE_TYPE.TEXT, payload: { text: result, }, - nick: - roomStore.localUser.nameCard || - roomStore.localUser.userName || - roomStore.localUser.userId, + nick: getSenderName(), from: roomStore.localUser.userId, flow: 'out', sequence: Math.random(), @@ -69,21 +152,197 @@ export default function useChatEditor() { } }; + const sendImageMessage = async (chooseImageResult: Record) => { + const localImageUrl = chooseImageResult?.tempFilePaths?.[0]; + if (!localImageUrl) { + return; + } + /** + * Insert a local message first so the image shows up while uploading. + **/ + const localMessageId = `local_image_${Math.random().toString()}`; + chatStore.updateMessageList({ + ID: localMessageId, + type: MESSAGE_TYPE.IMAGE, + payload: { + imageInfoArray: [{ imageUrl: localImageUrl }], + }, + nick: getSenderName(), + from: roomStore.localUser.userId, + flow: 'out', + sequence: Math.random(), + progress: 0, + }); + try { + const tim = getTim(); + const message = tim.createImageMessage({ + to: roomId.value, + conversationType: TencentCloudChat.TYPES.CONV_GROUP, + payload: { + file: chooseImageResult, + }, + onProgress: (progress: number) => { + chatStore.updateMessageItem(localMessageId, { progress }); + }, + }); + const imResponse = await tim.sendMessage(message); + chatStore.updateMessageItem(localMessageId, { + ID: imResponse.data.message.ID, + payload: { + imageInfoArray: formatImageInfoArray( + imResponse.data.message.payload?.imageInfoArray + ), + }, + progress: 1, + }); + } catch (e) { + chatStore.removeMessage(localMessageId); + TUIMessage({ type: 'error', message: t('Failed to send the image') }); + } + }; + + const chooseImage = () => { + if (isMessageDisabled.value) { + return; + } + closeToolPanels(); + uni.chooseImage({ + count: 1, + sizeType: ['compressed'], + sourceType: ['album', 'camera'], + success: (res: any) => { + sendImageMessage(res); + }, + }); + }; + + const sendFileMessage = async (chooseFileResult: Record) => { + const localFile = chooseFileResult?.tempFiles?.[0]; + if (!localFile?.path) { + return; + } + if (localFile.size > MAX_FILE_SIZE) { + TUIMessage({ + type: 'error', + message: t('The file cannot exceed 100MB'), + }); + return; + } + /** + * Insert a local message first so the file shows up while uploading. + **/ + const localMessageId = `local_file_${Math.random().toString()}`; + chatStore.updateMessageList({ + ID: localMessageId, + type: MESSAGE_TYPE.FILE, + payload: { + fileName: localFile.name, + fileSize: localFile.size, + }, + nick: getSenderName(), + from: roomStore.localUser.userId, + flow: 'out', + sequence: Math.random(), + progress: 0, + }); + try { + const tim = getTim(); + const message = tim.createFileMessage({ + to: roomId.value, + conversationType: TencentCloudChat.TYPES.CONV_GROUP, + payload: { + file: chooseFileResult, + }, + onProgress: (progress: number) => { + chatStore.updateMessageItem(localMessageId, { progress }); + }, + }); + const imResponse = await tim.sendMessage(message); + chatStore.updateMessageItem(localMessageId, { + ID: imResponse.data.message.ID, + payload: formatFilePayload(imResponse.data.message.payload), + progress: 1, + }); + } catch (e) { + chatStore.removeMessage(localMessageId); + TUIMessage({ type: 'error', message: t('Failed to send the file') }); + } + }; + + const openMessageFilePicker = () => { + uni.chooseMessageFile({ + count: 1, + type: 'file', + success: (res: any) => { + sendFileMessage(res); + }, + }); + }; + + /** + * chooseMessageFile jumps straight into the WeChat chat list, which is + * disorienting without warning. Explain where the user is going first. + */ + const chooseFile = () => { + if (isMessageDisabled.value) { + return; + } + closeToolPanels(); + TUIMessageBox({ + title: t('Tips'), + message: t('Files are selected from your WeChat chats'), + confirmButtonText: t('Go (short)'), + cancelButtonText: t('Cancel (short)'), + callback: (res: { confirm?: boolean }) => { + if (res?.confirm) { + openMessageFilePicker(); + } + }, + }); + }; + const handleChooseEmoji = (emojiName: string) => { sendMsg.value += emojiName; - editorInputEle.value.focus(); }; - const togglePopover = () => { - isEmojiToolbarVisible.value = !isEmojiToolbarVisible.value; + + const toggleEmojiPanel = () => { + if (isMessageDisabled.value) { + return; + } + const nextVisible = !isEmojiToolbarVisible.value; + // The panel and the keyboard share the same space at the bottom. + hideKeyboard(); + isActionPanelVisible.value = false; + isEmojiToolbarVisible.value = nextVisible; }; + + const toggleActionPanel = () => { + if (isMessageDisabled.value) { + return; + } + const nextVisible = !isActionPanelVisible.value; + hideKeyboard(); + isEmojiToolbarVisible.value = false; + isActionPanelVisible.value = nextVisible; + }; + return { t, editorInputEle, sendMsg, + hasSendContent, isMessageDisabled, + keyboardHeight, + handleKeyboardHeightChange, sendMessage, + chooseImage, + chooseFile, handleChooseEmoji, isEmojiToolbarVisible, - togglePopover, + isActionPanelVisible, + showEmojiPanel, + showActionPanel, + toggleEmojiPanel, + toggleActionPanel, }; } diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/EditorTools/index.vue b/MiniProgram/src/roomkit/TUIRoom/components/Chat/EditorTools/index.vue index 35c210c5a..97fb3382a 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Chat/EditorTools/index.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/EditorTools/index.vue @@ -23,11 +23,14 @@ const chooseEmoji = (itemName: string) => { diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageList/useMessageListHook.ts b/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageList/useMessageListHook.ts index 990f05a2f..1ca8e7338 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageList/useMessageListHook.ts +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageList/useMessageListHook.ts @@ -8,6 +8,27 @@ import { TencentCloudChat, } from '@tencentcloud/tuiroom-engine-wx'; import useGetRoomEngine from '../../../hooks/useRoomEngine'; +import { formatFilePayload, formatImageInfoArray, MESSAGE_TYPE } from '../util'; + +const SUPPORTED_MESSAGE_TYPES: string[] = [ + MESSAGE_TYPE.TEXT, + MESSAGE_TYPE.IMAGE, + MESSAGE_TYPE.FILE, +]; + +function isSupportedMessage(message: { type: string }) { + return SUPPORTED_MESSAGE_TYPES.includes(message.type); +} + +function getMessagePayload(type: string, payload: Record = {}) { + if (type === MESSAGE_TYPE.IMAGE) { + return { imageInfoArray: formatImageInfoArray(payload.imageInfoArray) }; + } + if (type === MESSAGE_TYPE.FILE) { + return formatFilePayload(payload); + } + return { text: payload.text }; +} export default function useMessageList() { const { t } = useI18n(); @@ -37,9 +58,7 @@ export default function useMessageList() { isCompleted, } = imResponse.data; messageList.value.splice(0, 0, ...historyMessageList); - const currentMessageList = messageList.value.filter( - item => item.type === 'TIMTextElem' - ); + const currentMessageList = messageList.value.filter(isSupportedMessage); chatStore.setMessageListInfo( currentMessageList, isCompleted, @@ -100,22 +119,15 @@ export default function useMessageList() { options.data.forEach((message: any) => { if ( message.conversationID !== currentConversationId || - message.type !== TencentCloudChat.TYPES.MSG_TEXT + !isSupportedMessage(message) ) { return; } - const { - ID, - payload: { text }, - nick: userName, - from: userId, - } = message; + const { ID, payload, nick: userName, from: userId } = message; chatStore.updateMessageList({ ID, - type: 'TIMTextElem', - payload: { - text, - }, + type: message.type, + payload: getMessagePayload(message.type, payload), nick: userName || userId, from: userId, flow: 'in', @@ -127,9 +139,8 @@ export default function useMessageList() { async function setMessageListInfo() { const { currentMessageList, isCompleted, nextReqMessageId } = await getMessageList(); - const filterCurrentMessageList = currentMessageList.filter( - (item: any) => item.type === 'TIMTextElem' - ); + const filterCurrentMessageList = + currentMessageList.filter(isSupportedMessage); chatStore.setMessageListInfo( filterCurrentMessageList, isCompleted, diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageFile.vue b/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageFile.vue new file mode 100644 index 000000000..0bfb00d59 --- /dev/null +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageFile.vue @@ -0,0 +1,273 @@ + + + + + diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageImage.vue b/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageImage.vue new file mode 100644 index 000000000..4c77f71ee --- /dev/null +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/MessageTypes/MessageImage.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/index/index.vue b/MiniProgram/src/roomkit/TUIRoom/components/Chat/index/index.vue index cb2b9b550..1dab74a14 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Chat/index/index.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/index/index.vue @@ -1,17 +1,38 @@ diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Chat/util.ts b/MiniProgram/src/roomkit/TUIRoom/components/Chat/util.ts index 93077eb38..a4fa57610 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Chat/util.ts +++ b/MiniProgram/src/roomkit/TUIRoom/components/Chat/util.ts @@ -9,6 +9,27 @@ * https://cloud.tencent.com/document/product/269/59590 */ +import type { MessageFileInfo, MessageImageInfo } from '../../stores/chat'; + +export const MESSAGE_TYPE = { + TEXT: 'TIMTextElem', + IMAGE: 'TIMImageElem', + FILE: 'TIMFileElem', +}; + +/** + * Chat SDK file messages are capped at 100MB. + **/ +export const MAX_FILE_SIZE = 100 * 1024 * 1024; + +/** + * The Chat SDK returns three sizes in order: origin, large and thumbnail. + **/ +const IMAGE_SIZE_INDEX = { + ORIGIN: 0, + THUMBNAIL: 2, +}; + export const emojiBaseUrl = 'https://web.sdk.qcloud.com/im/assets/emoji-plugin/'; const deprecatedEmojiBaseUrl = 'https://web.sdk.qcloud.com/im/assets/emoji/'; @@ -410,6 +431,102 @@ export function decodeMessageText(payload: string) { return renderDom; } +export function formatImageInfoArray( + imageInfoArray: Record[] = [] +): MessageImageInfo[] { + return imageInfoArray.map(item => ({ + imageUrl: item.imageUrl || item.url || '', + width: item.width, + height: item.height, + })); +} + +/** + * The thumbnail is used for the message bubble and the origin one for preview. + * A local image message only carries a single temporary file path, so both fall back to it. + **/ +export function getImageMessageUrl(imageInfoArray: MessageImageInfo[] = []) { + const getUrl = (imageInfo?: MessageImageInfo) => + imageInfo?.imageUrl || imageInfo?.url || ''; + const origin = imageInfoArray[IMAGE_SIZE_INDEX.ORIGIN]; + const thumbnail = imageInfoArray[IMAGE_SIZE_INDEX.THUMBNAIL] || origin; + return { + thumbnailUrl: getUrl(thumbnail) || getUrl(origin), + originUrl: getUrl(origin), + }; +} + +export function formatFilePayload( + payload: Record = {} +): MessageFileInfo { + return { + fileName: payload.fileName || payload.name || '', + fileSize: payload.fileSize || payload.size || 0, + fileUrl: payload.fileUrl || payload.url || '', + }; +} + +export function formatFileSize(fileSize = 0) { + if (fileSize >= 1024 * 1024) { + return `${(fileSize / (1024 * 1024)).toFixed(2)} MB`; + } + if (fileSize >= 1024) { + return `${(fileSize / 1024).toFixed(2)} KB`; + } + return `${fileSize} B`; +} + +/** + * wx.openDocument only accepts these document types. + **/ +const DOCUMENT_FILE_TYPES = [ + 'doc', + 'docx', + 'xls', + 'xlsx', + 'ppt', + 'pptx', + 'pdf', +]; + +export function getFileExtension(fileName = '') { + const index = fileName.lastIndexOf('.'); + if (index <= 0 || index === fileName.length - 1) { + return ''; + } + return fileName.slice(index + 1).toLowerCase(); +} + +export function isDocumentFile(fileName = '') { + return DOCUMENT_FILE_TYPES.includes(getFileExtension(fileName)); +} + +/** + * wx.saveFileToDisk is only available on WeChat for Windows and Mac. + **/ +export function canSaveFileToDisk() { + try { + const platform = String( + uni.getSystemInfoSync()?.platform || '' + ).toLowerCase(); + return platform === 'windows' || platform === 'mac'; + } catch (e) { + return false; + } +} + +export function getSandboxFilePath(fileName = '') { + const userDataPath = + (uni as any).env?.USER_DATA_PATH || + (globalThis as any).wx?.env?.USER_DATA_PATH || + ''; + const safeName = (fileName || `file_${Date.now()}`).replace( + /[\\/:*?"<>|]/g, + '_' + ); + return userDataPath ? `${userDataPath}/${safeName}` : ''; +} + export function decodeSendTextMsg(payload: string) { /** * Text Message diff --git a/MiniProgram/src/roomkit/TUIRoom/components/RoomContent/StreamContainer/index.vue b/MiniProgram/src/roomkit/TUIRoom/components/RoomContent/StreamContainer/index.vue index a8455fb58..d3cd620ba 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/RoomContent/StreamContainer/index.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/RoomContent/StreamContainer/index.vue @@ -59,6 +59,13 @@ + +
+ getNewStreamInfo(basicStore.userId, TUIVideoStreamType.kCameraStream) ); const setLayout = async (layout: LAYOUT) => { @@ -504,12 +511,24 @@ onUnmounted(() => { .swipe { position: absolute; bottom: 100px; + z-index: 2; display: flex; align-items: center; justify-content: center; width: 100%; } +.harmony-gesture-layer { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + // A fully transparent view can be skipped during hit testing on HarmonyOS. + background-color: rgba(0, 0, 0, 0.01); +} + .swipe-dots { width: 8px; height: 8px; diff --git a/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/AudioControl.vue b/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/AudioControl.vue index b3d11bcd8..70a981f4a 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/AudioControl.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/AudioControl.vue @@ -67,6 +67,7 @@ import TuiButton from '../common/base/Button.vue'; import AudioMediaControl from '../common/AudioMediaControl.vue'; import { useBasicStore } from '../../stores/basic'; import TUIMessageBox from '../common/base/MessageBox/index'; +import { ensureDeviceUsable } from '../../hooks/useWxMediaGuard'; const roomEngine = useRoomEngine(); @@ -124,6 +125,12 @@ async function toggleMuteAudio() { return; } // There is a microphone list and permissions + const isDeviceReady = await ensureDeviceUsable('microphone', t, { + userGesture: true, + }); + if (!isDeviceReady) { + return; + } await roomEngine.instance?.unmuteLocalAudio(); if (!basicStore.isOpenMic) { roomEngine.instance?.openLocalMicrophone(); @@ -153,6 +160,13 @@ async function onRequestReceived(eventInfo: { request: TUIRequest }) { } // Accept the host invitation and turn on the microphone async function handleAccept() { + const isDeviceReady = await ensureDeviceUsable('microphone', t, { + userGesture: true, + }); + if (!isDeviceReady) { + await handleReject(); + return; + } roomStore.setCanControlSelfAudio(true); await roomEngine.instance?.responseRemoteRequest({ requestId: requestOpenMicRequestId.value, diff --git a/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/VideoControl.vue b/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/VideoControl.vue index 71fe4e778..c587755dc 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/VideoControl.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/RoomFooter/VideoControl.vue @@ -70,6 +70,7 @@ import { useBasicStore } from '../../stores/basic'; import TuiButton from '../common/base/Button.vue'; import TUIMessage from '../common/base/Message/index'; import TUIMessageBox from '../common/base/MessageBox/index'; +import { ensureDeviceUsable } from '../../hooks/useWxMediaGuard'; const roomEngine = useGetRoomEngine(); const { deviceManager } = useDeviceManager(); @@ -129,6 +130,12 @@ async function toggleMuteVideo() { }); return; } + const isDeviceReady = await ensureDeviceUsable('camera', t, { + userGesture: true, + }); + if (!isDeviceReady) { + return; + } if (isMobile) { await roomEngine.instance?.openLocalCamera({ isFrontCamera: basicStore.isFrontCamera, @@ -162,6 +169,13 @@ async function onRequestReceived(eventInfo: { request: TUIRequest }) { // Accept the host invitation and turn on the camera async function handleAccept() { + const isDeviceReady = await ensureDeviceUsable('camera', t, { + userGesture: true, + }); + if (!isDeviceReady) { + await handleReject(); + return; + } roomStore.setCanControlSelfVideo(true); await roomEngine.instance?.responseRemoteRequest({ requestId: requestOpenCameraRequestId.value, diff --git a/MiniProgram/src/roomkit/TUIRoom/components/RoomHome/RoomControl/index.vue b/MiniProgram/src/roomkit/TUIRoom/components/RoomHome/RoomControl/index.vue index 54f8ba74b..4d09d08cb 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/RoomHome/RoomControl/index.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/RoomHome/RoomControl/index.vue @@ -2,15 +2,27 @@
- + {{ t('Join Room') }}
- + {{ t('New Room') }}
- + {{ t('Schedule') }}
diff --git a/MiniProgram/src/roomkit/TUIRoom/components/Stream/common/StreamRegion/index.vue b/MiniProgram/src/roomkit/TUIRoom/components/Stream/common/StreamRegion/index.vue index a790a1516..74862e0ae 100644 --- a/MiniProgram/src/roomkit/TUIRoom/components/Stream/common/StreamRegion/index.vue +++ b/MiniProgram/src/roomkit/TUIRoom/components/Stream/common/StreamRegion/index.vue @@ -14,9 +14,11 @@ @scale="handleScaleChange" >