Redesign message list to Mattermost-style: left-aligned, always named

Replaced the Slack/iMessage-style layout (your own messages right-aligned
in an accent-colored bubble, no name shown since you obviously know you
sent it) with a flat, uniformly left-aligned chat log matching Mattermost:
every message shows its sender's avatar and name, dropped only for
consecutive messages from the same sender in a row (grouping logic already
existed for other people's messages; now applies uniformly regardless of
who sent it). No more per-message "mine" background color, since real
Mattermost doesn't have one either -- messages are distinguished only by
the username label, with a hover-revealed row highlight and the existing
edit affordance (still author-only) taking the place of the old bubble.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 11:48:59 -06:00
co-authored by Claude Sonnet 5
parent f43cf61ddd
commit 559adf9b7e
2 changed files with 75 additions and 71 deletions
+43 -44
View File
@@ -4,98 +4,97 @@
padding: var(--sp-4); padding: var(--sp-4);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px;
} }
.message-row { .message-row {
position: relative;
display: flex; display: flex;
gap: var(--sp-2); gap: var(--sp-2);
justify-content: flex-start; padding: 1px var(--sp-2);
align-items: flex-end; border-radius: 6px;
} }
.message-row-mine { .message-row-start {
justify-content: flex-end; margin-top: 10px;
}
.message-row:hover {
background: var(--ds-surface);
} }
.message-avatar-slot { .message-avatar-slot {
width: 28px; width: 36px;
flex: none; flex: none;
} }
.message-bubble-wrap { .message-content {
display: flex; flex: 1;
flex-direction: column; min-width: 0;
align-items: flex-start;
max-width: 65%;
} }
.message-row-mine .message-bubble-wrap { .message-header {
align-items: flex-end; display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 2px;
} }
.message-author { .message-author {
font-size: 0.76rem; font-size: 0.86rem;
color: var(--ds-muted); font-weight: 700;
margin-bottom: 3px; color: var(--ds-text);
font-weight: 600;
} }
.message-bubble { .message-time {
position: relative; font-size: 0.68rem;
background: var(--ds-surface-2); color: var(--ds-muted);
color: var(--ds-text); font-family: var(--mono);
padding: 8px 12px; }
border-radius: 10px;
.message-text {
font-size: 0.88rem; font-size: 0.88rem;
line-height: 1.45; line-height: 1.45;
color: var(--ds-text);
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
} }
.message-bubble-mine { .message-edited {
background: var(--ds-accent-2); font-style: italic;
color: var(--ds-muted);
font-size: 0.78rem;
} }
.message-edit-link { .message-edit-link {
display: none; display: none;
position: absolute; position: absolute;
top: -18px; top: 2px;
right: 0; right: var(--sp-2);
background: transparent; background: var(--ds-surface-2);
border: none; border: 1px solid var(--ds-border);
color: var(--ds-muted); color: var(--ds-muted);
font-size: 0.68rem; font-size: 0.68rem;
cursor: pointer; cursor: pointer;
padding: 2px 4px; padding: 2px 8px;
border-radius: 6px;
} }
.message-edit-link:hover { .message-edit-link:hover {
color: var(--ds-text); color: var(--ds-text);
border-color: var(--ds-accent);
} }
.message-bubble:hover .message-edit-link { .message-row:hover .message-edit-link {
display: inline; display: inline;
} }
.message-edit-input { .message-edit-input {
background: var(--ds-surface-2); background: var(--ds-surface-2);
border: 1px solid var(--ds-accent); border: 1px solid var(--ds-accent);
border-radius: 10px; border-radius: 6px;
padding: 8px 12px; padding: 6px 10px;
font-size: 0.88rem; font-size: 0.88rem;
color: var(--ds-text); color: var(--ds-text);
font-family: var(--sans); font-family: var(--sans);
width: 100%; width: 100%;
} }
.message-time {
font-size: 0.68rem;
color: var(--ds-muted);
margin-top: 3px;
font-family: var(--mono);
}
.message-edited {
font-style: italic;
}
+21 -16
View File
@@ -37,21 +37,29 @@ export function MessageList({ messages, members, onEdit }: MessageListProps) {
{messages.map((msg, i) => { {messages.map((msg, i) => {
const mine = msg.user_id === user?.id const mine = msg.user_id === user?.id
const prev = messages[i - 1] const prev = messages[i - 1]
const showAvatar = !mine && (!prev || prev.user_id !== msg.user_id) // Mattermost-style grouping: every message shows who sent it, but
const showName = showAvatar // consecutive messages from the same sender only repeat the
// avatar/name/timestamp header on the first one in the run --
// applies uniformly, including to your own messages.
const isGroupStart = !prev || prev.user_id !== msg.user_id
const editing = editingId === msg.id const editing = editingId === msg.id
return ( return (
<div key={msg.id} className={`message-row${mine ? ' message-row-mine' : ''}`}> <div key={msg.id} className={`message-row${isGroupStart ? ' message-row-start' : ''}`}>
{!mine && (
<div className="message-avatar-slot"> <div className="message-avatar-slot">
{showAvatar && ( {isGroupStart && (
<UserAvatar username={msg.username} colorIndex={senderColorIndex(msg.username, members)} /> <UserAvatar username={msg.username} colorIndex={senderColorIndex(msg.username, members)} />
)} )}
</div> </div>
<div className="message-content">
{isGroupStart && (
<div className="message-header">
<span className="message-author">{msg.username}</span>
<span className="message-time">
{new Date(msg.created_at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
</span>
</div>
)} )}
<div className="message-bubble-wrap">
{showName && <div className="message-author">{msg.username}</div>}
{editing ? ( {editing ? (
<input <input
autoFocus autoFocus
@@ -65,9 +73,13 @@ export function MessageList({ messages, members, onEdit }: MessageListProps) {
onBlur={() => commitEdit(msg.id)} onBlur={() => commitEdit(msg.id)}
/> />
) : ( ) : (
<div className={`message-bubble${mine ? ' message-bubble-mine' : ''}`}> <div className="message-text">
{msg.content} {msg.content}
{mine && ( {msg.edited_at && <span className="message-edited"> (edited)</span>}
</div>
)}
</div>
{mine && !editing && (
<button <button
type="button" type="button"
className="message-edit-link" className="message-edit-link"
@@ -78,13 +90,6 @@ export function MessageList({ messages, members, onEdit }: MessageListProps) {
</button> </button>
)} )}
</div> </div>
)}
<div className="message-time">
{new Date(msg.created_at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
{msg.edited_at && <span className="message-edited"> (edited)</span>}
</div>
</div>
</div>
) )
})} })}
<div ref={bottomRef} /> <div ref={bottomRef} />