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);
}
}