From a983210e101448e0c30cf8fe99208fb3c90fcb8f Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 29 Mar 2026 20:32:36 +0200 Subject: [PATCH] style(dm): move timestamp above bubble, improve meta readability Move DM timestamp+status row above the message bubble (consistent with group messages). Increase delivery/SNR meta font size and adjust color for better readability in both light and dark themes. Co-Authored-By: Claude Opus 4.6 --- app/static/css/style.css | 53 ++++++++++++++++------ app/static/css/theme.css | 2 + app/static/js/dm.js | 97 +++++++++++++++++----------------------- 3 files changed, 83 insertions(+), 69 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index f877b6d..1d2b4c9 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -492,7 +492,7 @@ main { font-size: 0.8rem; } - .dm-message { + .dm-message-wrapper { max-width: 85%; } @@ -683,31 +683,58 @@ main { } } -/* DM Message Bubbles */ -.dm-message { +/* DM Message Wrapper - timestamp + bubble */ +.dm-message-wrapper { max-width: 70%; - padding: 0.75rem 1rem; - border-radius: 1rem; - word-wrap: break-word; + display: flex; + flex-direction: column; animation: fadeIn 0.3s ease-in; } -.dm-message.own { +.dm-message-wrapper.own { align-self: flex-end; +} + +.dm-message-wrapper.other { + align-self: flex-start; +} + +/* DM Time Row - above bubble */ +.dm-time-row { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.7rem; + color: var(--text-muted); + margin-bottom: 0.2rem; + padding: 0 0.25rem; +} + +.dm-message-wrapper.own .dm-time-row { + justify-content: flex-end; +} + +/* DM Message Bubbles */ +.dm-message { + padding: 0.75rem 1rem; + border-radius: 1rem; + word-wrap: break-word; +} + +.dm-message.own { background-color: var(--msg-own-bg); border: 1px solid var(--msg-own-border); } .dm-message.other { - align-self: flex-start; background-color: var(--msg-other-bg); border: 1px solid var(--msg-border); } /* DM Message Metadata */ .dm-meta { - font-size: 0.65rem; - color: var(--text-meta); + font-size: 0.75rem; + color: var(--dm-meta-color); margin-top: 0.25rem; } @@ -743,8 +770,8 @@ main { } .dm-delivery-meta { - font-size: 0.65rem; - color: var(--text-meta); + font-size: 0.75rem; + color: var(--dm-meta-color); margin-top: 0.1rem; } @@ -1577,7 +1604,7 @@ main { /* Hidden messages when filtering */ .message-wrapper.filter-hidden, -.dm-message.filter-hidden { +.dm-message-wrapper.filter-hidden { display: none !important; } diff --git a/app/static/css/theme.css b/app/static/css/theme.css index 3f46624..ff1c092 100644 --- a/app/static/css/theme.css +++ b/app/static/css/theme.css @@ -23,6 +23,7 @@ --text-secondary: #495057; --text-muted: #6c757d; --text-meta: #adb5bd; + --dm-meta-color: #8b939b; /* Borders */ --border-color: #dee2e6; @@ -176,6 +177,7 @@ --text-secondary: #94a3b8; --text-muted: #64748b; --text-meta: #475569; + --dm-meta-color: #5c6d82; /* Borders */ --border-color: #334155; diff --git a/app/static/js/dm.js b/app/static/js/dm.js index cd38473..84e7bc8 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -103,7 +103,7 @@ function connectChatSocket() { if (!data.expected_ack) return; // Find message with matching expected_ack in DOM and update status - const msgElements = document.querySelectorAll('#dmMessagesList .dm-message.own'); + const msgElements = document.querySelectorAll('#dmMessagesList .dm-message-wrapper.own'); msgElements.forEach(el => { const statusEl = el.querySelector(`.dm-status[data-ack="${data.expected_ack}"]`); if (statusEl) { @@ -1142,8 +1142,11 @@ function displayMessages(messages) { container.innerHTML = ''; messages.forEach(msg => { - const div = document.createElement('div'); - div.className = `dm-message ${msg.is_own ? 'own' : 'other'}`; + const side = msg.is_own ? 'own' : 'other'; + + // Wrapper: time row + bubble + const wrapper = document.createElement('div'); + wrapper.className = `dm-message-wrapper ${side}`; // Status icon for own messages let statusIcon = ''; @@ -1217,19 +1220,25 @@ function displayMessages(messages) { ` : ''; - div.innerHTML = ` -
- ${formatTime(msg.timestamp)} - ${statusIcon} -
-
${processMessageContent(msg.content)}
+ // Time row above bubble + const timeRow = document.createElement('div'); + timeRow.className = 'dm-time-row'; + timeRow.innerHTML = `${formatTime(msg.timestamp)}${statusIcon}`; + wrapper.appendChild(timeRow); + + // Message bubble + const bubble = document.createElement('div'); + bubble.className = `dm-message ${side}`; + bubble.innerHTML = ` +
${processMessageContent(msg.content)}
${deliveryMeta} ${retryInfo} ${meta} ${resendBtn} `; + wrapper.appendChild(bubble); - container.appendChild(div); + container.appendChild(wrapper); }); // Scroll to bottom @@ -1838,7 +1847,7 @@ function closeDmFilterBar() { function applyDmFilter(query) { currentDmFilterQuery = query.trim(); const container = document.getElementById('dmMessagesList'); - const messages = container.querySelectorAll('.dm-message'); + const messages = container.querySelectorAll('.dm-message-wrapper'); const matchCountEl = document.getElementById('dmFilterMatchCount'); // Remove any existing no-matches message @@ -1886,72 +1895,48 @@ function applyDmFilter(query) { } /** - * Get text content from a DM message - * DM structure: timestamp div, then content div, then meta/actions - * @param {HTMLElement} msgEl - DM message element + * Get text content from a DM message wrapper + * @param {HTMLElement} wrapperEl - DM message wrapper element * @returns {string} - Text content */ -function getDmMessageText(msgEl) { - // The message content is in a div that is not the timestamp row, meta, or actions - const children = msgEl.children; - for (let i = 0; i < children.length; i++) { - const child = children[i]; - // Skip timestamp row (has d-flex class), meta, and actions - if (!child.classList.contains('d-flex') && - !child.classList.contains('dm-meta') && - !child.classList.contains('dm-actions')) { - return child.textContent || ''; - } - } - return ''; +function getDmMessageText(wrapperEl) { + const content = wrapperEl.querySelector('.dm-content'); + return content ? content.textContent || '' : ''; } /** * Highlight matching text in a DM message - * @param {HTMLElement} msgEl - DM message element + * @param {HTMLElement} wrapperEl - DM message wrapper element * @param {number} index - Message index for tracking */ -function highlightDmMessageContent(msgEl, index) { +function highlightDmMessageContent(wrapperEl, index) { const msgId = 'dm_msg_' + index; + const content = wrapperEl.querySelector('.dm-content'); + if (!content) return; - // Find content div (not timestamp, not meta, not actions) - const children = Array.from(msgEl.children); - for (const child of children) { - if (!child.classList.contains('d-flex') && - !child.classList.contains('dm-meta') && - !child.classList.contains('dm-actions')) { - - if (!originalDmMessageContents.has(msgId)) { - originalDmMessageContents.set(msgId, child.innerHTML); - } - - const originalHtml = originalDmMessageContents.get(msgId); - child.innerHTML = FilterUtils.highlightMatches(originalHtml, currentDmFilterQuery); - break; - } + if (!originalDmMessageContents.has(msgId)) { + originalDmMessageContents.set(msgId, content.innerHTML); } + + const originalHtml = originalDmMessageContents.get(msgId); + content.innerHTML = FilterUtils.highlightMatches(originalHtml, currentDmFilterQuery); } /** * Restore original DM message content - * @param {HTMLElement} msgEl - DM message element + * @param {HTMLElement} wrapperEl - DM message wrapper element */ -function restoreDmOriginalContent(msgEl) { +function restoreDmOriginalContent(wrapperEl) { const container = document.getElementById('dmMessagesList'); - const messages = Array.from(container.querySelectorAll('.dm-message')); - const index = messages.indexOf(msgEl); + const wrappers = Array.from(container.querySelectorAll('.dm-message-wrapper')); + const index = wrappers.indexOf(wrapperEl); const msgId = 'dm_msg_' + index; if (!originalDmMessageContents.has(msgId)) return; - const children = Array.from(msgEl.children); - for (const child of children) { - if (!child.classList.contains('d-flex') && - !child.classList.contains('dm-meta') && - !child.classList.contains('dm-actions')) { - child.innerHTML = originalDmMessageContents.get(msgId); - break; - } + const content = wrapperEl.querySelector('.dm-content'); + if (content) { + content.innerHTML = originalDmMessageContents.get(msgId); } }