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'), '');
- 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;