From 6202ff7077cd57b12d81bb6aeee56f87f94a01b4 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:37:01 +0200 Subject: [PATCH] Addded quote chat message feature --- webui-src/app/chat/chat.js | 48 +++++++++++++- webui-src/app/chat/chat_state.js | 88 ++++++++++++++++++++++++-- webui-src/app/people/people_history.js | 3 +- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 558ca14..0ce91ff 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -1058,10 +1058,16 @@ const ChatRoomJoinView = () => { const Layout = { dismissMenu: () => { + let redraw = false; if (ChatHubState.activeMenu) { ChatHubState.activeMenu = null; - m.redraw(); + redraw = true; } + if (ChatHubState.messageContextMenu && ChatHubState.messageContextMenu.show) { + ChatHubState.messageContextMenu.show = false; + redraw = true; + } + if (redraw) m.redraw(); }, oninit: () => { ChatHubState.activeTab = 'chat'; @@ -1481,7 +1487,45 @@ const Layout = { 'Select a chat room from the left panel to view details or join a conversation.' ), ]), - ]), + ]), + ChatHubState.messageContextMenu.show && m('.chat-msg-context-menu', { + style: `position: fixed; top: ${ChatHubState.messageContextMenu.y}px; left: ${ChatHubState.messageContextMenu.x}px; background: #ffffff; border: 1px solid #cbd5e1; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); padding: 0.35rem 0; z-index: 3000; min-width: 160px;`, + onclick: (e) => e.stopPropagation(), + }, [ + m('.context-menu-item', { + style: 'padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 0.6rem; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const { username, messageText } = ChatHubState.messageContextMenu; + const quoteHeader = `> [${username}]: ${messageText}\n`; + const textarea = document.querySelector('.chat-hub-input-area textarea') || document.querySelector('#msginput'); + if (textarea) { + textarea.value = (textarea.value ? textarea.value.trim() + '\n' : '') + quoteHeader; + textarea.focus(); + } + ChatHubState.messageContextMenu.show = false; + m.redraw(); + }, + }, [ + m('i.fas.fa-quote-right', { style: 'color: #3b82f6;' }), + 'Quote Message' + ]), + m('.context-menu-item', { + style: 'padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 0.6rem; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const { messageText } = ChatHubState.messageContextMenu; + navigator.clipboard.writeText(messageText); + ChatHubState.messageContextMenu.show = false; + m.redraw(); + }, + }, [ + m('i.far.fa-copy', { style: 'color: #64748b;' }), + 'Copy Text' + ]), + ]) ]); }, }; diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 996a156..1e8d226 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -149,7 +149,7 @@ function renderChatMessage(rawText) { .replaceAll('
', '\n') .replace(new RegExp('|<[^>]*>', 'gm'), ''); if (cleanText) { - parts.push(renderTextWithEmoji(cleanText)); + parts.push(renderFormattedMessageText(cleanText)); } } @@ -182,11 +182,61 @@ function renderChatMessage(rawText) { // 3. Normal text message const cleanText = rawText + .replace(/]*>/gi, '\n> ') + .replace(/<\/blockquote>/gi, '\n') .replaceAll('
', '\n') .replaceAll('
', '\n') .replace(new RegExp('|<[^>]*>', 'gm'), ''); - return renderTextWithEmoji(cleanText); + return renderFormattedMessageText(cleanText); +} + +function renderFormattedMessageText(text) { + if (!text) return ''; + const lines = text.split('\n'); + const elements = []; + let currentQuoteLines = []; + + const flushQuote = () => { + if (currentQuoteLines.length > 0) { + const quoteText = currentQuoteLines.join('\n'); + elements.push( + m('blockquote.chat-quote-block', { + style: { + borderLeft: '3px solid #3b82f6', + backgroundColor: '#f8fafc', + color: '#475569', + padding: '0.35rem 0.65rem', + margin: '0.35rem 0', + borderRadius: '0 0.375rem 0.375rem 0', + fontSize: '0.9em', + fontStyle: 'italic', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + } + }, renderTextWithEmoji(quoteText)) + ); + currentQuoteLines = []; + } + }; + + lines.forEach((line, idx) => { + if (line.trim().startsWith('>')) { + const lineContent = line.trim().replace(/^>\s?/, ''); + currentQuoteLines.push(lineContent); + } else { + flushQuote(); + if (line) { + elements.push(renderTextWithEmoji(line)); + } + if (idx < lines.length - 1) { + elements.push(m('br')); + } + } + }); + flushQuote(); + + return elements.length > 0 ? elements : renderTextWithEmoji(text); } /** @@ -381,13 +431,34 @@ const Message = () => { const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; const isRoom = chatType === 3; + const handleContextMenu = (e) => { + e.preventDefault(); + e.stopPropagation(); + const sel = window.getSelection() ? window.getSelection().toString() : ''; + const targetText = sel && sel.trim() ? sel : rawText; + + ChatHubState.messageContextMenu = { + show: true, + x: e.clientX, + y: e.clientY, + messageText: targetText, + username: username, + }; + m.redraw(); + }; + if (isRoom) { const nickColor = getNicknameColor(gxsId, username); return m( '.message.compact', - m('span.datetime', datetime), - m('span.username', { style: { color: nickColor } }, username + ':'), - m('span.messagetext', renderChatMessage(rawText)) + { + oncontextmenu: handleContextMenu, + }, + [ + m('span.datetime', datetime), + m('span.username', { style: { color: nickColor } }, username + ':'), + m('span.messagetext', renderChatMessage(rawText)), + ] ); } @@ -808,6 +879,13 @@ const ChatHubState = { historySearchQuery: '', fullHistoryMessages: [], isHistoryLoading: false, + messageContextMenu: { + show: false, + x: 0, + y: 0, + messageText: '', + username: '', + }, }; module.exports = { diff --git a/webui-src/app/people/people_history.js b/webui-src/app/people/people_history.js index ce43094..a5ba3b0 100644 --- a/webui-src/app/people/people_history.js +++ b/webui-src/app/people/people_history.js @@ -1,11 +1,11 @@ const m = require('mithril'); const rs = require('rswebui'); const peopleState = require('people/people_state'); -const chatState = require('chat/chat_state'); const HistoryBrowserModal = () => { return { oninit: (vnode) => { + const chatState = require('chat/chat_state'); const isRoom = vnode.attrs && vnode.attrs.isRoom; if (isRoom) { chatState.ChatHubState.historySearchQuery = ''; @@ -19,6 +19,7 @@ const HistoryBrowserModal = () => { } }, view: (vnode) => { + const chatState = require('chat/chat_state'); const isRoom = vnode.attrs && vnode.attrs.isRoom; const stateObj = isRoom ? chatState.ChatHubState : peopleState.State;