diff --git a/WebApp/client/public/onebyone/chatmessage.js b/WebApp/client/public/onebyone/chatmessage.js new file mode 100644 index 0000000..22123ff --- /dev/null +++ b/WebApp/client/public/onebyone/chatmessage.js @@ -0,0 +1,251 @@ +/** + * 消息模块 + * 处理聊天消息的发送、接收和显示 + */ +import { showNotification, generateId } from './utils.js'; +import store from './store.js'; +import { mockMessages } from './models.js'; +// 消息相关的状态管理方法 +let messageState = { + messages: [...mockMessages], + unreadCount: 0, + isSidebarOpen: false +}; + +let listeners = []; + +/** + * 订阅状态变化 + * @param {Function} callback - 回调函数 + * @returns {Function} 取消订阅的函数 + */ +export function subscribe(callback) { + listeners.push(callback); + return () => { + listeners = listeners.filter(cb => cb !== callback); + }; +} + +/** + * 通知所有监听器 + * @param {Object} changes - 变化对象 + */ +function notify(changes) { + listeners.forEach(cb => cb(messageState, changes)); +} + +/** + * 添加消息 + * @param {Object} message - 消息对象 + */ +export function addMessage (message) { + messageState.messages.push(message); + + // 如果侧边栏关闭且不是自己发的,增加未读 + if (!messageState.isSidebarOpen && !message.isSelf) { + messageState.unreadCount++; + notify({ type: 'SIDEBAR_TOGGLE', unreadCount: messageState.unreadCount }); + } + + notify({ type: 'NEW_MESSAGE', message, unreadCount: messageState.unreadCount }); +} + +/** + * 发送聊天消息 + * @param {Object} message - 消息对象 + * @param {Object} renderstreaming - WebRTC连接管理实例 + */ +export function sendChatMessage(message) { + if (store.getRenderStreaming()) { + store.getRenderStreaming().sendMessage({ + type: 'chat-message', + message: message, + }); + } +} + +/** + * 处理接收到的聊天消息 + * @param {Object} data - 消息数据 + */ +export function handleChatMessage(data) { + console.log('处理聊天:', data); + addMessage(data); + + const isImage = data.content && data.content.startsWith('data:image/'); + const messageType = isImage ? 'file' : 'text'; + + // 显示通知 + if (!message.isSelf) { + const content = isImage ? '[图片]' : message.content; + showNotification(`${message.senderName}: ${content.substring(0, 20)}${content.length > 20 ? '...' : ''}`); + } +} + +/** + * 切换侧边栏 + * @returns {boolean} 切换后的状态 + */ +export function toggleSidebar() { + messageState.isSidebarOpen = !messageState.isSidebarOpen; + if (messageState.isSidebarOpen) { + messageState.unreadCount = 0; + } + notify({ type: 'SIDEBAR_TOGGLE', isOpen: messageState.isSidebarOpen, unreadCount: messageState.unreadCount }); + return messageState.isSidebarOpen; +} + +/** + * 获取消息状态 + * @returns {Object} 消息状态 + */ +export function getMessageState() { + return messageState; +} + +/** + * 发送消息 + */ +export function sendMessage() { + const chatInput = document.getElementById('chatInput'); + const content = chatInput.value.trim(); + + if (content) { + const state = store.getState(); + const message = { + id: generateId(), + senderId: state.session.localUser.id, + senderName: state.session.localUser.name, + senderAvatar: state.session.localUser.avatar, + content: content, + type: 'text', + timestamp: new Date().toISOString(), + isSelf: true + }; + + addMessage(message); + + const newMessage = { + id: generateId(), + senderId: state.session.remoteUser.id, + senderName: state.session.remoteUser.name, + senderAvatar: state.session.remoteUser.avatar, + content: content, + type: 'text', + timestamp: new Date().toISOString(), + isSelf: false + }; + chatInput.value = ''; + // 发送消息到服务器 + sendChatMessage(newMessage); + + //wsManager.send('chat-message', message); + } +} + +/** + * 处理聊天输入回车 + * @param {KeyboardEvent} event - 键盘事件 + */ +export function handleChatSubmit(event) { + if (event.key === 'Enter') { + sendMessage(); + } +} + +/** + * 打开图片选择器 + */ +export function openImagePicker() { + document.getElementById('imageInput').click(); +} + +/** + * 处理图片上传 + * @param {Event} event - 事件对象 + */ +export function handleImageUpload(event) { + const file = event.target.files[0]; + if (file) { + // 检查文件类型 + if (!file.type.startsWith('image/')) { + showNotification('请选择图片文件', 3000); + return; + } + + // 检查文件大小(限制为5MB) + if (file.size > 5 * 1024 * 1024) { + showNotification('图片文件不能超过5MB', 3000); + return; + } + + // 读取图片文件 + const reader = new FileReader(); + reader.onload = function (e) { + const imageUrl = e.target.result; + sendImageMessage(imageUrl, file.name); + }; + reader.readAsDataURL(file); + + // 重置文件输入 + event.target.value = ''; + } +} + +/** + * 发送图片消息 + * @param {string} imageUrl - 图片URL + * @param {string} fileName - 文件名 + */ +export function sendImageMessage(imageUrl, fileName) { + const state = store.getState(); + const newMessage = { + id: generateId(), + senderId: state.session.localUser.id, + senderName: state.session.localUser.name, + senderAvatar: state.session.localUser.avatar, + content: imageUrl, + fileName: fileName, + type: 'file', + timestamp: new Date().toISOString(), + isSelf: true + }; + + (newMessage); + + // 发送消息到服务器 + // wsManager.send('send-message', newMessage); +} + +/** + * 绑定消息相关的DOM事件 + */ +export function bindMessageEvents() { + // 发送消息 + window.sendMessage = sendMessage; + + // 处理聊天输入回车 + window.handleChatSubmit = handleChatSubmit; + + // 打开图片选择器 + window.openImagePicker = openImagePicker; + + // 处理图片上传 + window.handleImageUpload = handleImageUpload; +} + +// 导出所有函数 +export default { + sendMessage, + handleChatSubmit, + openImagePicker, + handleImageUpload, + sendImageMessage, + bindMessageEvents, + addMessage, + sendChatMessage, + handleChatMessage, + toggleSidebar, + getMessageState, + subscribe +}; diff --git a/WebApp/client/public/onebyone/main.js b/WebApp/client/public/onebyone/main.js index e75dd24..8638119 100644 --- a/WebApp/client/public/onebyone/main.js +++ b/WebApp/client/public/onebyone/main.js @@ -5,118 +5,18 @@ import store from './store.js'; import UIRenderer from './renderer.js'; import apiClient from './api.js'; -// import wsManager from './websocket.js'; -import { mockCallSession } from './models.js'; -import { showNotification, generateId } from './utils.js'; +import { showNotification } from './utils.js'; +import chatMessage from './chatmessage.js'; // 全局变量 -let renderer = null; let connectionId = ""; -/** - * 初始化应用 - */ -// function initApp() { -// // 初始化渲染器 -// renderer = new UIRenderer(store); - -// // 初始化WebSocket连接 -// wsManager.connect(); - -// // 绑定WebSocket事件 -// bindWebSocketEvents(); - -// // 绑定DOM事件 -// bindDomEvents(); - -// // 初始化WebRTC (如果需要) -// // initWebRTC(); - - -// console.log('App initialized'); -// } - -/** - * 绑定WebSocket事件 - */ -function bindWebSocketEvents() { - // wsManager.on('connect', () => { - // console.log('WebSocket connected'); - // showNotification('已连接到服务器'); - // }); - - // wsManager.on('disconnect', () => { - // console.log('WebSocket disconnected'); - // showNotification('与服务器的连接已断开', 5000); - // }); - - // wsManager.on('message-received', (data) => { - // console.log('Message received:', data); - // store.addMessage(data.message); - // }); - - // wsManager.on('user-joined', (data) => { - // console.log('User joined:', data); - // showNotification(`${data.userId} 加入了通话`); - // }); - - // wsManager.on('user-left', (data) => { - // console.log('User left:', data); - // showNotification(`${data.userId} 离开了通话`); - // }); - - // wsManager.on('media-state-changed', (data) => { - // console.log('Media state changed:', data); - // // 更新远端媒体状态 - // if (data.userId !== store.getLocalUser().id) { - // store.updateRemoteMedia(data); - // } - // }); - - // wsManager.on('network-quality', (data) => { - // console.log('Network quality changed:', data); - // // 更新网络质量 - // const state = store.getState(); - // if (data.userId === state.session.remoteUser.id) { - // state.session.remoteUser.networkQuality = data.quality; - // store.notify({ type: 'NETWORK_CHANGE', quality: data.quality }); - // } - // }); - - // wsManager.on('call-ended', (data) => { - // console.log('Call ended:', data); - // store.endCall(); - // showNotification('通话已结束', 3000); - // }); - - // wsManager.on('call-request', (data) => { - // console.log('Call request received:', data); - // // 显示通话请求弹窗 - // if (window.showCallRequest) { - // const caller = { - // name: mockCallSession.remoteUser.name, - // avatar: mockCallSession.remoteUser.avatar - // }; - // window.showCallRequest(caller); - // connectionId = data.connectionId; - // } - // }); - // //处理发送消息响应 - // wsManager.on('chat-message', (data) => { - // console.log('chat-message:', data); - // // 显示消息 - // store.addMessage({ - // data: data.message, - // }); - // }); -} - /** * 绑定DOM事件 */ function bindDomEvents() { // 切换侧边栏 window.toggleSidebar = function () { - store.toggleSidebar(); + chatMessage.toggleSidebar(); }; // 切换麦克风 @@ -209,107 +109,8 @@ function bindDomEvents() { store.setUp(connectionId); }; - // 发送消息 - window.sendMessage = function () { - const chatInput = document.getElementById('chatInput'); - const content = chatInput.value.trim(); - - if (content) { - const state = store.getState(); - const message = { - id: state.id, - senderId: state.session.localUser.id, - senderName: state.session.localUser.name, - senderAvatar: state.session.localUser.avatar, - content: content, - type: 'text', - timestamp: new Date().toISOString(), - isSelf: true - }; - - store.addMessage(message); - - - const newMessage = { - id: state.id, - senderId: state.session.remoteUser.id, - senderName: state.session.remoteUser.name, - senderAvatar: state.session.remoteUser.avatar, - content: content, - type: 'text', - timestamp: new Date().toISOString(), - isSelf: false - - }; - chatInput.value = ''; - // 发送消息到服务器 - store.sendChatMessage(newMessage); - - //wsManager.send('chat-message', message); - } - }; - - // 处理聊天输入回车 - window.handleChatSubmit = function (event) { - if (event.key === 'Enter') { - window.sendMessage(); - } - }; - - // 打开图片选择器 - window.openImagePicker = function () { - document.getElementById('imageInput').click(); - }; - - // 处理图片上传 - window.handleImageUpload = function (event) { - const file = event.target.files[0]; - if (file) { - // 检查文件类型 - if (!file.type.startsWith('image/')) { - showNotification('请选择图片文件', 3000); - return; - } - - // 检查文件大小(限制为5MB) - if (file.size > 5 * 1024 * 1024) { - showNotification('图片文件不能超过5MB', 3000); - return; - } - - // 读取图片文件 - const reader = new FileReader(); - reader.onload = function (e) { - const imageUrl = e.target.result; - sendImageMessage(imageUrl, file.name); - }; - reader.readAsDataURL(file); - - // 重置文件输入 - event.target.value = ''; - } - }; - - // 发送图片消息 - function sendImageMessage(imageUrl, fileName) { - const state = store.getState(); - const newMessage = { - id: generateId(), - senderId: state.session.localUser.id, - senderName: state.session.localUser.name, - senderAvatar: state.session.localUser.avatar, - content: imageUrl, - fileName: fileName, - type: 'file', - timestamp: new Date().toISOString(), - isSelf: true - }; - - store.addMessage(newMessage); - - // 发送消息到服务器 - // wsManager.send('send-message', newMessage); - } + // 绑定消息相关事件 + chatMessage.bindMessageEvents(); // 键盘快捷键 document.addEventListener('keydown', (event) => { @@ -367,9 +168,6 @@ window.addEventListener('DOMContentLoaded', async () => { // 绑定DOM事件 bindDomEvents(); - // 绑定WebSocket事件 - bindWebSocketEvents(); - console.log('Video call app initialized successfully'); } catch (error) { console.error('Error initializing app:', error); @@ -378,4 +176,4 @@ window.addEventListener('DOMContentLoaded', async () => { }); // 导出全局变量 -export { store, renderer, apiClient }; +export { store, apiClient }; diff --git a/WebApp/client/public/onebyone/models.js b/WebApp/client/public/onebyone/models.js index 62a2dfd..18e6b3f 100644 --- a/WebApp/client/public/onebyone/models.js +++ b/WebApp/client/public/onebyone/models.js @@ -106,26 +106,6 @@ const mockMessages = [ type: "system", timestamp: "2024-01-15T14:30:00.000Z", isSelf: false - }, - { - id: "msg-002", - senderId: "user-remote-002", - senderName: mockCallSession.remoteUser.name, - senderAvatar: mockCallSession.remoteUser.avatar, - content: "嗨,能听到我说话吗?", - type: "text", - timestamp: "2024-01-15T14:32:15.000Z", - isSelf: false - }, - { - id: "msg-003", - senderId: "user-local-001", - senderName: mockCallSession.localUser.name, - senderAvatar: mockCallSession.localUser.avatar, - content: "很清楚!你的画面也很清晰 👍", - type: "text", - timestamp: "2024-01-15T14:32:45.000Z", - isSelf: true } ]; diff --git a/WebApp/client/public/onebyone/renderer.js b/WebApp/client/public/onebyone/renderer.js index 05f05d8..7a8e7c5 100644 --- a/WebApp/client/public/onebyone/renderer.js +++ b/WebApp/client/public/onebyone/renderer.js @@ -4,6 +4,7 @@ */ import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js'; import { mockCallSession } from './models.js'; +import chatMessage from './chatmessage.js'; class UIRenderer { constructor(stateManager) { this.stateManager = stateManager; @@ -56,6 +57,8 @@ class UIRenderer { // 订阅状态变化 this.unsubscribe = stateManager.subscribe(this.render.bind(this)); + // 订阅消息状态变化 + this.messageUnsubscribe = chatMessage.subscribe(this.renderMessageState.bind(this)); // 初始化渲染 this.render(this.stateManager.getState(), { type: 'INIT' }); @@ -71,6 +74,25 @@ class UIRenderer { }); } + // 渲染消息状态变化 + renderMessageState(messageState, changes) { + switch (changes.type) { + case 'NEW_MESSAGE': + this.renderChatMessages(messageState.messages); + this.renderUnreadCount(changes.unreadCount); + break; + case 'SIDEBAR_TOGGLE': + this.renderSidebar(changes.isOpen); + // 当侧边栏打开时,重置未读消息计数 + if (changes.isOpen) { + this.renderUnreadCount(0); + } else { + this.renderUnreadCount(changes.unreadCount); + } + break; + } + } + // 绑定事件监听器 bindEventListeners() { // 事件监听器 @@ -85,7 +107,7 @@ class UIRenderer { this.renderRemoteVideo(state.session.remoteUser); this.renderLocalVideo(state.session.localUser, state.localStream); this.renderControlButtons(state.session.localUser.mediaState); - this.renderChatMessages(state.messages); + this.renderChatMessages(chatMessage.getMessageState().messages); this.renderUserList(state.session.localUser, state.session.remoteUser); // 初始化时检查远程流状态,显示或隐藏占位背景 @@ -121,19 +143,6 @@ class UIRenderer { this.renderRemoteVideo(state.session.remoteUser); this.renderUserList(state.session.localUser, state.session.remoteUser); break; - case 'NEW_MESSAGE': - this.renderChatMessages(state.messages); - this.renderUnreadCount(changes.unreadCount); - break; - case 'SIDEBAR_TOGGLE': - this.renderSidebar(changes.isOpen); - // 当侧边栏打开时,重置未读消息计数 - if (changes.isOpen) { - this.renderUnreadCount(0); - } else{ - this.renderUnreadCount(changes.unreadCount); - } - break; case 'NETWORK_CHANGE': this.renderNetworkStatus(changes.quality); break; @@ -645,6 +654,9 @@ class UIRenderer { if (this.unsubscribe) { this.unsubscribe(); } + if (this.messageUnsubscribe) { + this.messageUnsubscribe(); + } } } diff --git a/WebApp/client/public/onebyone/store.js b/WebApp/client/public/onebyone/store.js index 001c5bc..d161382 100644 --- a/WebApp/client/public/onebyone/store.js +++ b/WebApp/client/public/onebyone/store.js @@ -2,18 +2,16 @@ * 状态管理 * 使用简单的 Observable 模式,可替换为 Redux/Vuex/Pinia */ -import { mockCallSession, mockMessages } from './models.js'; +import { mockCallSession } from './models.js'; import { Signaling, WebSocketSignaling } from "../../module/signaling.js";// 信令管理 import { RenderStreaming } from "../../module/renderstreaming.js"; // WebRTC连接管理 import { getServerConfig, getRTCConfiguration } from "../js/config.js";//服务器配置和RTC配置 import { showNotification, generateId } from './utils.js'; // 导入通知函数 +import chatMessage from './chatmessage.js'; // 默认视频流尺寸 const defaultStreamWidth = 1280; const defaultStreamHeight = 720; - - - class CallStateManager { constructor() { const renderstreaming = null; // WebRTC连接管理实例 @@ -26,9 +24,6 @@ class CallStateManager { ...mockCallSession, status: 'idle' // 初始状态为空闲 }, - messages: [...mockMessages], - isSidebarOpen: false, - unreadCount: 0, localStream: null, // MediaStream 对象 remoteStream: null // MediaStream 对象 }; @@ -372,7 +367,7 @@ class CallStateManager { // 处理聊天 // 添加到列表并更新UI - this.handleChatMessage(data.message); + chatMessage.handleChatMessage(data.message); } else if (data.type === 'on-message') { } @@ -457,34 +452,19 @@ class CallStateManager { this.notify({ type: 'REMOTE_MEDIA_CHANGE', mediaState }); } - // 添加 + // 添加消息 addMessage(message) { - this.state.messages.push(message); - - // 如果侧边栏关闭且不是自己发的,增加未读 - if (!this.state.isSidebarOpen && !message.isSelf) { - this.state.unreadCount++; - this.notify({ type: 'SIDEBAR_TOGGLE', unreadCount: this.state.unreadCount }); - } - - this.notify({ type: 'NEW_MESSAGE', message, unreadCount: this.state.unreadCount }); + chatMessage.addMessage(message); } + + // 发送聊天消息 sendChatMessage(message) { - if (this.renderstreaming) { - - this.renderstreaming.sendMessage({ - type: 'chat-message', - message: message, - }); - } + chatMessage.sendChatMessage(message, this.renderstreaming); } + // 切换侧边栏 toggleSidebar() { - this.state.isSidebarOpen = !this.state.isSidebarOpen; - if (this.state.isSidebarOpen) { - this.state.unreadCount = 0; - } - this.notify({ type: 'SIDEBAR_TOGGLE', isOpen: this.state.isSidebarOpen }); + chatMessage.toggleSidebar(); } // 结束通话 @@ -812,18 +792,8 @@ class CallStateManager { } } - // 处理接收到的聊天 - handleChatMessage(data) { - console.log('处理聊天:', data); - store.addMessage(data); - const isImage = data.message && data.message.startsWith('data:image/'); - const messageType = isImage ? 'file' : 'text'; - // 显示通知 - showNotification(`${senderName}: ${data.message.substring(0, 20)}${data.message.length > 20 ? '...' : ''}`); - } - // 发送媒体状态到服务器 emitMediaStateChange() { const payload = { @@ -939,8 +909,10 @@ class CallStateManager { getState() { return this.state; } getLocalUser() { return this.state.session.localUser; } getRemoteUser() { return this.state.session.remoteUser; } - getMessages() { return this.state.messages; } + getConnectionId() { return this.connectionId; } + + getRenderStreaming() { return this.renderstreaming; } } // 创建单例实例