Skip to content

Commit a922e17

Browse files
committed
Group consecutive same-author messages in PiP DMs and threads
Back-to-back messages from one person were rendering as separate blocks (each with its own name/time header) in the DM pop-out and in threads, instead of grouping the way the main channel does. PiP DMs: the live-append path rebuilt the "previous message" from the DOM dataset but only carried user_id and time. The grouping check also compares username (and persona), so it never matched and every message stayed ungrouped. Rebuild the previous message with the username/persona/break fields the dataset already stores. Threads: the thread panel had no grouping at all. Add a compact reply variant (same 5-minute, same-author, no-reply rule as the main channel) that drops the avatar and author header and shows a hover timestamp in the gutter. The display name is stashed on the row so reply/quote still resolve the author.
1 parent fa9b42f commit a922e17

2 files changed

Lines changed: 84 additions & 16 deletions

File tree

public/css/style.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7979,6 +7979,28 @@ html:not([data-toolbaricons="emoji"]):not([data-toolbaricons="color"]) .tb-icon-
79797979
border-radius: var(--radius-sm);
79807980
transition: background var(--transition), box-shadow var(--transition);
79817981
}
7982+
/* Compact thread reply — grouped under the previous reply from the same
7983+
author. Avatar/author header are dropped; a timestamp shows in the avatar
7984+
gutter on hover, like the main channel's compact rows. */
7985+
.thread-message.thread-compact {
7986+
margin-bottom: 2px;
7987+
}
7988+
.thread-msg-compact-spacer {
7989+
background: transparent !important;
7990+
display: flex;
7991+
align-items: center;
7992+
justify-content: center;
7993+
}
7994+
.thread-compact-time {
7995+
font-size: 10px;
7996+
color: var(--text-muted);
7997+
opacity: 0;
7998+
transition: opacity var(--transition);
7999+
white-space: nowrap;
8000+
}
8001+
.thread-message.thread-compact:hover .thread-compact-time {
8002+
opacity: 1;
8003+
}
79828004
.thread-message:hover {
79838005
background: rgba(255,255,255,0.02);
79848006
box-shadow: var(--msg-glow);

public/js/modules/app-utilities.js

Lines changed: 62 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2243,7 +2243,7 @@ _setThreadParentHeader(meta = {}) {
22432243
},
22442244

22452245
_setThreadReply(msgEl, msgId) {
2246-
const author = msgEl.querySelector('.thread-msg-author')?.textContent || 'someone';
2246+
const author = msgEl.querySelector('.thread-msg-author')?.textContent || msgEl.dataset.username || 'someone';
22472247
const rawContent = msgEl.dataset.rawContent || msgEl.querySelector('.thread-msg-content')?.textContent || '';
22482248
const preview = rawContent.length > 70 ? rawContent.substring(0, 70) + '…' : rawContent;
22492249
this._threadReplyingTo = { id: msgId, username: author, content: rawContent };
@@ -2266,7 +2266,7 @@ _clearThreadReply() {
22662266

22672267
_quoteThreadMessage(msgEl) {
22682268
const rawContent = msgEl.dataset.rawContent || msgEl.querySelector('.thread-msg-content')?.textContent || '';
2269-
const author = msgEl.querySelector('.thread-msg-author')?.textContent || 'someone';
2269+
const author = msgEl.querySelector('.thread-msg-author')?.textContent || msgEl.dataset.username || 'someone';
22702270
const quotedLines = rawContent.split('\n').map(l => `> ${l}`).join('\n');
22712271
const quoteText = `> @${author} wrote:\n${quotedLines}\n`;
22722272

@@ -2588,8 +2588,16 @@ _appendDMPiPMessage(msg) {
25882588
let prevMsg = null;
25892589
const lastEl = list.lastElementChild;
25902590
if (lastEl && lastEl.dataset && lastEl.dataset.userId && lastEl.dataset.msgId) {
2591+
// Rebuild enough of the previous message for `_createMessageEl`'s grouping
2592+
// check. It compares username and persona too, so a prev that only carried
2593+
// user_id + time never matched and every message rendered ungrouped. (the
2594+
// dataset already stores these from when the element was created.)
25912595
prevMsg = {
25922596
user_id: parseInt(lastEl.dataset.userId, 10),
2597+
username: lastEl.dataset.username || null,
2598+
persona_id: lastEl.dataset.personaId ? parseInt(lastEl.dataset.personaId, 10) : null,
2599+
persona_username: lastEl.dataset.personaUsername || null,
2600+
break_chain: lastEl.dataset.breakChain === '1' ? 1 : 0,
25932601
created_at: lastEl.dataset.time
25942602
};
25952603
}
@@ -2918,29 +2926,67 @@ _appendThreadMessage(msg) {
29182926
? `<div class="thread-msg-more"><button class="thread-msg-more-btn" type="button" aria-label="More actions">${iMore}</button><div class="thread-msg-overflow">${threadOverflowToolbarBtns}</div></div>`
29192927
: '';
29202928

2929+
// Group consecutive replies from the same author (within 5 min, no reply
2930+
// banner) into compact rows, the same way the main channel does — drop the
2931+
// avatar and author header, keep the content and the hover toolbar. The
2932+
// thread's parent message lives in a separate preview element, not in this
2933+
// container, so we only ever group reply-against-reply.
2934+
let threadCompact = false;
2935+
const prevEl = container.lastElementChild;
2936+
if (prevEl && prevEl.classList?.contains('thread-message') && !msg.reply_to) {
2937+
const samePerson = parseInt(prevEl.dataset.userId, 10) === msg.user_id
2938+
&& (prevEl.dataset.personaId || '') === (msg.persona_id ? String(msg.persona_id) : '');
2939+
const prevTime = prevEl.dataset.time ? new Date(prevEl.dataset.time).getTime() : 0;
2940+
const within = prevTime && (new Date(msg.created_at).getTime() - prevTime) < 5 * 60 * 1000;
2941+
threadCompact = samePerson && within;
2942+
}
2943+
29212944
const el = document.createElement('div');
2922-
el.className = 'thread-message';
2945+
el.className = 'thread-message' + (threadCompact ? ' thread-compact' : '');
29232946
el.dataset.msgId = msg.id;
29242947
el.dataset.rawContent = msg.content;
2925-
el.innerHTML = `
2926-
<div class="thread-msg-row">
2927-
${avatarHtml}
2928-
<div class="thread-msg-body">
2929-
<div class="thread-msg-header">
2930-
<span class="thread-msg-author" style="color:${color}">${this._escapeHtml(displayName)}</span>
2931-
<span class="thread-msg-time">${this._formatTime(msg.created_at)}</span>
2932-
<span class="thread-msg-header-spacer"></span>
2948+
el.dataset.userId = msg.user_id;
2949+
el.dataset.time = msg.created_at;
2950+
// Stash the display name so reply/quote still resolve the author on compact
2951+
// rows, which don't render a `.thread-msg-author` element.
2952+
el.dataset.username = displayName;
2953+
if (msg.persona_id) el.dataset.personaId = String(msg.persona_id);
2954+
if (threadCompact) {
2955+
const shortTime = new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2956+
el.innerHTML = `
2957+
<div class="thread-msg-row">
2958+
<div class="thread-msg-avatar thread-msg-compact-spacer"><span class="thread-compact-time">${this._escapeHtml(shortTime)}</span></div>
2959+
<div class="thread-msg-body">
29332960
<div class="thread-msg-toolbar">
29342961
<div class="msg-toolbar-group">${threadCoreToolbarBtns}</div>
29352962
${threadOverflowHtml}
29362963
</div>
2964+
<div class="thread-msg-content">${this._formatContent(msg.content)}</div>
2965+
${reactionsHtml}
29372966
</div>
2938-
${replyHtml}
2939-
<div class="thread-msg-content">${this._formatContent(msg.content)}</div>
2940-
${reactionsHtml}
29412967
</div>
2942-
</div>
2943-
`;
2968+
`;
2969+
} else {
2970+
el.innerHTML = `
2971+
<div class="thread-msg-row">
2972+
${avatarHtml}
2973+
<div class="thread-msg-body">
2974+
<div class="thread-msg-header">
2975+
<span class="thread-msg-author" style="color:${color}">${this._escapeHtml(displayName)}</span>
2976+
<span class="thread-msg-time">${this._formatTime(msg.created_at)}</span>
2977+
<span class="thread-msg-header-spacer"></span>
2978+
<div class="thread-msg-toolbar">
2979+
<div class="msg-toolbar-group">${threadCoreToolbarBtns}</div>
2980+
${threadOverflowHtml}
2981+
</div>
2982+
</div>
2983+
${replyHtml}
2984+
<div class="thread-msg-content">${this._formatContent(msg.content)}</div>
2985+
${reactionsHtml}
2986+
</div>
2987+
</div>
2988+
`;
2989+
}
29442990
container.appendChild(el);
29452991
try { this._decryptE2EImages?.(el); } catch {}
29462992
try { this._decryptE2EFiles?.(el); } catch {}

0 commit comments

Comments
 (0)