451 lines
13 KiB
JavaScript
451 lines
13 KiB
JavaScript
// Import necessary modules
|
|
// 导入必要的模块
|
|
import {
|
|
createAvatarSVG
|
|
} from './util.avatar.js';
|
|
import {
|
|
roomsData,
|
|
activeRoomIndex
|
|
} from './room.js';
|
|
import {
|
|
escapeHTML,
|
|
textToHTML
|
|
} from './util.string.js';
|
|
import {
|
|
$,
|
|
$id,
|
|
createElement,
|
|
on,
|
|
addClass,
|
|
removeClass
|
|
} from './util.dom.js';
|
|
import {
|
|
formatFileSize
|
|
} from './util.file.js';
|
|
|
|
// Render the chat area
|
|
// 渲染聊天区域
|
|
export function renderChatArea() {
|
|
const chatArea = $id('chat-area');
|
|
if (!chatArea) return;
|
|
if (activeRoomIndex < 0 || !roomsData[activeRoomIndex]) {
|
|
chatArea.innerHTML = '';
|
|
return
|
|
}
|
|
chatArea.innerHTML = '';
|
|
roomsData[activeRoomIndex].messages.forEach(m => {
|
|
if (m.type === 'me') addMsg(m.text, true, m.msgType || 'text', m.timestamp);
|
|
else if (m.type === 'system') addSystemMsg(m.text, true, m.timestamp);
|
|
else addOtherMsg(m.text, m.userName, m.avatar, true, m.msgType || 'text', m.timestamp)
|
|
})
|
|
}
|
|
|
|
// Add a message to the chat area
|
|
// 添加消息到聊天区域
|
|
export function addMsg(text, isHistory = false, msgType = 'text', timestamp = null) {
|
|
let ts = isHistory ? timestamp : (timestamp || Date.now());
|
|
if (!ts) return;
|
|
if (!isHistory && activeRoomIndex >= 0) {
|
|
roomsData[activeRoomIndex].messages.push({
|
|
type: 'me',
|
|
text,
|
|
msgType,
|
|
timestamp: ts
|
|
})
|
|
}
|
|
const chatArea = $id('chat-area');
|
|
if (!chatArea) return;
|
|
const className = 'bubble me' + (msgType.includes('_private') ? ' private-message' : '');
|
|
const date = new Date(ts);
|
|
const time = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); let contentHtml = '';
|
|
if (msgType === 'image' || msgType === 'image_private') {
|
|
const safeImgSrc = escapeHTML(text).replace(/javascript:/gi, '');
|
|
contentHtml = `<img src="${safeImgSrc}"alt="image"class="bubble-img">`
|
|
} else if (msgType === 'file' || msgType === 'file_private') {
|
|
// Handle file messages
|
|
contentHtml = renderFileMessage(text, true);
|
|
} else {
|
|
contentHtml = textToHTML(text)
|
|
}
|
|
const div = createElement('div', {
|
|
class: className
|
|
}, `<span class="bubble-content">${contentHtml}</span><span class="bubble-meta">${time}</span>`);
|
|
chatArea.appendChild(div);
|
|
chatArea.scrollTop = chatArea.scrollHeight
|
|
}
|
|
|
|
// Add a message from another user to the chat area
|
|
// 添加来自其他用户的消息到聊天区域
|
|
export function addOtherMsg(msg, userName = '', avatar = '', isHistory = false, msgType = 'text', timestamp = null) {
|
|
if (!userName && activeRoomIndex >= 0) {
|
|
const rd = roomsData[activeRoomIndex];
|
|
if (rd && msg && msg.clientId && rd.userMap[msg.clientId]) {
|
|
userName = rd.userMap[msg.clientId].userName || rd.userMap[msg.clientId].username || rd.userMap[msg.clientId].name || 'Anonymous'
|
|
}
|
|
}
|
|
userName = userName || 'Anonymous';
|
|
let ts = isHistory ? timestamp : (timestamp || Date.now());
|
|
if (!isHistory && activeRoomIndex >= 0) {
|
|
roomsData[activeRoomIndex].messages.push({
|
|
type: 'other',
|
|
text: msg,
|
|
userName,
|
|
avatar,
|
|
msgType,
|
|
timestamp: ts
|
|
})
|
|
}
|
|
if (!ts) return;
|
|
const chatArea = $id('chat-area');
|
|
if (!chatArea) return;
|
|
const bubbleWrap = createElement('div', {
|
|
class: 'bubble-other-wrap'
|
|
}); let contentHtml = '';
|
|
if (msgType === 'image' || msgType === 'image_private') {
|
|
const safeImgSrc = escapeHTML(msg).replace(/javascript:/gi, '');
|
|
contentHtml = `<img src="${safeImgSrc}"alt="image"class="bubble-img">`
|
|
} else if (msgType === 'file' || msgType === 'file_private') {
|
|
// Handle file messages
|
|
contentHtml = renderFileMessage(msg, false);
|
|
} else {
|
|
contentHtml = textToHTML(msg)
|
|
}
|
|
const safeUserName = escapeHTML(userName);
|
|
const date = new Date(ts);
|
|
const time = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0');
|
|
let bubbleClasses = 'bubble other';
|
|
if (msgType && msgType.includes('_private')) {
|
|
bubbleClasses += ' private-message'
|
|
}
|
|
bubbleWrap.innerHTML = `<span class="avatar"></span><div class="bubble-other-main"><div class="${bubbleClasses}"><div class="bubble-other-name">${safeUserName}</div><span class="bubble-content">${contentHtml}</span><span class="bubble-meta">${time}</span></div></div>`;
|
|
const svg = createAvatarSVG(userName);
|
|
const avatarEl = $('.avatar', bubbleWrap);
|
|
if (avatarEl) {
|
|
const cleanSvg = svg.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
|
avatarEl.innerHTML = cleanSvg
|
|
}
|
|
chatArea.appendChild(bubbleWrap);
|
|
chatArea.scrollTop = chatArea.scrollHeight
|
|
}
|
|
|
|
// Add a system message to the chat area
|
|
// 添加系统消息到聊天区域
|
|
export function addSystemMsg(text, isHistory = false, timestamp = null) {
|
|
if (!isHistory && activeRoomIndex >= 0) {
|
|
const ts = timestamp || Date.now();
|
|
roomsData[activeRoomIndex].messages.push({
|
|
type: 'system',
|
|
text,
|
|
timestamp: ts
|
|
})
|
|
}
|
|
const chatArea = $id('chat-area');
|
|
if (!chatArea) return;
|
|
const safeText = textToHTML(text);
|
|
const div = createElement('div', {
|
|
class: 'bubble system'
|
|
}, `<span class="bubble-content">${safeText}</span>`);
|
|
chatArea.appendChild(div);
|
|
chatArea.scrollTop = chatArea.scrollHeight
|
|
}
|
|
|
|
// Update the style of the chat input area
|
|
// 更新聊天输入区域的样式
|
|
export function updateChatInputStyle() {
|
|
const rd = roomsData[activeRoomIndex];
|
|
const chatInputArea = $('.chat-input-area');
|
|
const placeholder = $('.input-field-placeholder');
|
|
const inputMessageInput = $('.input-message-input');
|
|
if (!chatInputArea || !placeholder || !inputMessageInput) return;
|
|
if (rd && rd.privateChatTargetId) {
|
|
addClass(chatInputArea, 'private-mode');
|
|
addClass(inputMessageInput, 'private-mode');
|
|
placeholder.textContent = `Private Message to ${escapeHTML(rd.privateChatTargetName)}`
|
|
} else {
|
|
removeClass(chatInputArea, 'private-mode');
|
|
removeClass(inputMessageInput, 'private-mode');
|
|
placeholder.textContent = 'Message'
|
|
}
|
|
const html = inputMessageInput.innerHTML.replace(/<br\s*\/?>(\s*)?/gi, '').replace(/ /g, '').replace(/\u200B/g, '').trim();
|
|
placeholder.style.opacity = (html === '') ? '1' : '0'
|
|
}
|
|
|
|
// Setup image preview functionality
|
|
// 设置图片预览功能
|
|
export function setupImagePreview() {
|
|
on($id('chat-area'), 'click', function(e) {
|
|
const target = e.target;
|
|
if (target.tagName === 'IMG' && target.closest('.bubble-content')) {
|
|
showImageModal(target.src)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Show the image modal
|
|
// 显示图片模态框
|
|
export function showImageModal(src) {
|
|
const modal = createElement('div', {
|
|
class: 'img-modal-bg'
|
|
}, `<div class="img-modal-blur"></div><div class="img-modal-content img-modal-content-overflow"><img src="${src}"class="img-modal-img"/><span class="img-modal-close">×</span></div>`);
|
|
document.body.appendChild(modal);
|
|
on($('.img-modal-close', modal), 'click', () => modal.remove());
|
|
on(modal, 'click', (e) => {
|
|
if (e.target === modal) modal.remove()
|
|
});
|
|
const img = $('img', modal);
|
|
let scale = 1;
|
|
let isDragging = false;
|
|
let lastX = 0,
|
|
lastY = 0;
|
|
let offsetX = 0,
|
|
offsetY = 0;
|
|
img.ondragstart = function(e) {
|
|
e.preventDefault()
|
|
};
|
|
on(img, 'wheel', function(ev) {
|
|
ev.preventDefault();
|
|
const prevScale = scale;
|
|
scale += ev.deltaY < 0 ? 0.1 : -0.1;
|
|
scale = Math.max(0.2, Math.min(5, scale));
|
|
if (scale === 1) {
|
|
offsetX = 0;
|
|
offsetY = 0
|
|
}
|
|
updateTransform()
|
|
});
|
|
|
|
function updateTransform() {
|
|
img.style.transform = `translate(${offsetX}px,${offsetY}px)scale(${scale})`;
|
|
img.style.cursor = scale > 1 ? (isDragging ? 'grabbing' : 'grab') : 'zoom-in'
|
|
}
|
|
on(img, 'mousedown', function(ev) {
|
|
if (scale <= 1) return;
|
|
isDragging = true;
|
|
lastX = ev.clientX;
|
|
lastY = ev.clientY;
|
|
img.style.cursor = 'grabbing';
|
|
document.body.style.userSelect = 'none'
|
|
});
|
|
|
|
function onMouseMove(ev) {
|
|
if (!isDragging) return;
|
|
offsetX += ev.clientX - lastX;
|
|
offsetY += ev.clientY - lastY;
|
|
lastX = ev.clientX;
|
|
lastY = ev.clientY;
|
|
updateTransform()
|
|
}
|
|
|
|
function onMouseUp() {
|
|
if (isDragging) {
|
|
isDragging = false;
|
|
img.style.cursor = 'grab';
|
|
document.body.style.userSelect = ''
|
|
}
|
|
}
|
|
on(window, 'mousemove', onMouseMove);
|
|
on(window, 'mouseup', onMouseUp);
|
|
on(img, 'dblclick', function() {
|
|
scale = 1;
|
|
offsetX = 0;
|
|
offsetY = 0;
|
|
updateTransform()
|
|
});
|
|
const cleanup = () => {
|
|
off(window, 'mousemove', onMouseMove);
|
|
off(window, 'mouseup', onMouseUp);
|
|
document.body.style.userSelect = ''
|
|
};
|
|
on(modal, 'remove', cleanup);
|
|
on($('.img-modal-close', modal), 'click', cleanup);
|
|
updateTransform()
|
|
}
|
|
|
|
// Render file message content
|
|
// 渲染文件消息内容
|
|
function renderFileMessage(fileData, isSender) {
|
|
const {
|
|
fileId,
|
|
fileName,
|
|
originalSize,
|
|
totalVolumes
|
|
} = fileData;
|
|
const safeFileName = escapeHTML(fileName);
|
|
const formattedSize = formatFileSize(originalSize);
|
|
|
|
// Determine file icon based on extension
|
|
// 根据扩展名确定文件图标
|
|
const fileExtension = fileName.split('.').pop().toLowerCase();
|
|
let fileIcon = '📄'; // Default document icon 默认文档图标
|
|
|
|
const iconMap = {
|
|
// Images
|
|
'jpg': '🖼️',
|
|
'jpeg': '🖼️',
|
|
'png': '🖼️',
|
|
'gif': '🖼️',
|
|
'bmp': '🖼️',
|
|
'svg': '🖼️',
|
|
'webp': '🖼️',
|
|
// Videos
|
|
'mp4': '🎬',
|
|
'avi': '🎬',
|
|
'mov': '🎬',
|
|
'mkv': '🎬',
|
|
'wmv': '🎬',
|
|
'flv': '🎬',
|
|
'webm': '🎬',
|
|
// Audio
|
|
'mp3': '🎵',
|
|
'wav': '🎵',
|
|
'flac': '🎵',
|
|
'aac': '🎵',
|
|
'ogg': '🎵',
|
|
'm4a': '🎵',
|
|
// Documents
|
|
'pdf': '📕',
|
|
'doc': '📄',
|
|
'docx': '📄',
|
|
'txt': '📄',
|
|
'rtf': '📄',
|
|
'xls': '📊',
|
|
'xlsx': '📊',
|
|
'csv': '📊',
|
|
'ppt': '📈',
|
|
'pptx': '📈',
|
|
// Archives
|
|
'zip': '🗜️',
|
|
'rar': '🗜️',
|
|
'7z': '🗜️',
|
|
'tar': '🗜️',
|
|
'gz': '🗜️',
|
|
// Code
|
|
'js': '💻',
|
|
'html': '💻',
|
|
'css': '💻',
|
|
'py': '💻',
|
|
'java': '💻',
|
|
'cpp': '💻',
|
|
'c': '💻',
|
|
'php': '💻',
|
|
'rb': '💻',
|
|
'go': '💻',
|
|
'ts': '💻',
|
|
'json': '💻',
|
|
'xml': '💻'
|
|
};
|
|
if (iconMap[fileExtension]) {
|
|
fileIcon = iconMap[fileExtension];
|
|
}
|
|
|
|
// Check actual file transfer status
|
|
const transfer = window.fileTransfers ? window.fileTransfers.get(fileId) : null;
|
|
let statusText = 'Waiting...';
|
|
let progressWidth = '0%';
|
|
let downloadBtnStyle = 'display: none;';
|
|
|
|
if (transfer) {
|
|
if (transfer.status === 'sending') {
|
|
const progress = (transfer.sentVolumes / transfer.totalVolumes) * 100;
|
|
progressWidth = `${progress}%`;
|
|
statusText = `Sending ${transfer.sentVolumes}/${transfer.totalVolumes}`;
|
|
} else if (transfer.status === 'receiving') {
|
|
const progress = (transfer.receivedVolumes.size / transfer.totalVolumes) * 100;
|
|
progressWidth = `${progress}%`;
|
|
statusText = `Receiving ${transfer.receivedVolumes.size}/${transfer.totalVolumes}`;
|
|
} else if (transfer.status === 'completed') {
|
|
progressWidth = '100%';
|
|
statusText = '✓ Completed';
|
|
downloadBtnStyle = isSender ? 'display: none;' : 'display: inline-block;';
|
|
}
|
|
} else if (isSender) {
|
|
statusText = 'Sent';
|
|
progressWidth = '100%';
|
|
}
|
|
|
|
return `
|
|
<div class="file-message" data-file-id="${fileId}">
|
|
<div class="file-info">
|
|
<div class="file-icon">${fileIcon}</div>
|
|
<div class="file-details">
|
|
<div class="file-name" title="${safeFileName}">${safeFileName}</div>
|
|
<div class="file-meta">
|
|
<span class="file-size">${formattedSize}</span>
|
|
<span class="file-volumes">${totalVolumes} volumes</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="file-progress-container">
|
|
<div class="file-progress-bar">
|
|
<div class="file-progress" style="width: ${progressWidth}"></div>
|
|
</div>
|
|
<div class="file-status">${statusText}</div>
|
|
</div>
|
|
<button class="file-download-btn" style="${downloadBtnStyle}" onclick="window.downloadFile('${fileId}')">
|
|
📥 Download
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Automatically adjust the height of the input area
|
|
// 自动调整输入区域的高度
|
|
export function autoGrowInput() {
|
|
const input = $('.input-message-input');
|
|
if (!input) return;
|
|
input.style.height = 'auto';
|
|
input.style.height = input.scrollHeight + 'px'
|
|
}
|
|
|
|
// Handle pasting text as plain text
|
|
// 处理粘贴为纯文本
|
|
function handlePasteAsPlainText(element) {
|
|
if (!element) return;
|
|
on(element, 'paste', function(e) {
|
|
e.preventDefault();
|
|
let text = '';
|
|
if (e.clipboardData || window.clipboardData) {
|
|
text = (e.clipboardData || window.clipboardData).getData('text/plain')
|
|
}
|
|
if (document.queryCommandSupported('insertText')) {
|
|
document.execCommand('insertText', false, text)
|
|
} else {
|
|
const selection = window.getSelection();
|
|
if (selection.rangeCount) {
|
|
const range = selection.getRangeAt(0);
|
|
range.deleteContents();
|
|
const textNode = document.createTextNode(text);
|
|
range.insertNode(textNode);
|
|
range.setStartAfter(textNode);
|
|
range.setEndAfter(textNode);
|
|
selection.removeAllRanges();
|
|
selection.addRange(range)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Setup input placeholder functionality
|
|
// 设置输入框占位符功能
|
|
export function setupInputPlaceholder() {
|
|
const input = $('.input-message-input');
|
|
const placeholder = $('.input-field-placeholder');
|
|
if (!input || !placeholder) return;
|
|
|
|
function checkEmpty() {
|
|
const html = input.innerHTML.replace(/<br\s*\/?>(\s*)?/gi, '').replace(/ /g, '').replace(/\u200B/g, '').trim();
|
|
if (html === '') {
|
|
placeholder.style.opacity = '1'
|
|
} else {
|
|
placeholder.style.opacity = '0'
|
|
}
|
|
autoGrowInput()
|
|
}
|
|
on(input, 'input', checkEmpty);
|
|
on(input, 'blur', checkEmpty);
|
|
on(input, 'focus', checkEmpty);
|
|
handlePasteAsPlainText(input);
|
|
checkEmpty();
|
|
autoGrowInput();
|
|
updateChatInputStyle()
|
|
} |