Files
nodecrypt/web/chat.js
T
2025-05-24 16:18:47 +08:00

286 lines
8.9 KiB
JavaScript

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';
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)
})
}
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 {
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
}
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 {
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
}
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
}
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(/&nbsp;/g, '').replace(/\u200B/g, '').trim();
placeholder.style.opacity = (html === '') ? '1' : '0'
}
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)
}
})
}
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">&times;</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()
}
export function autoGrowInput() {
const input = $('.input-message-input');
if (!input) return;
input.style.height = 'auto';
input.style.height = input.scrollHeight + 'px'
}
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)
}
}
})
}
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(/&nbsp;/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()
}