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);
display: flex;
flex-direction: column;
gap: 14px;
}
.message-row {
position: relative;
display: flex;
gap: var(--sp-2);
justify-content: flex-start;
align-items: flex-end;
padding: 1px var(--sp-2);
border-radius: 6px;
}
.message-row-mine {
justify-content: flex-end;
.message-row-start {
margin-top: 10px;
}
.message-row:hover {
background: var(--ds-surface);
}
.message-avatar-slot {
width: 28px;
width: 36px;
flex: none;
}
.message-bubble-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 65%;
.message-content {
flex: 1;
min-width: 0;
}
.message-row-mine .message-bubble-wrap {
align-items: flex-end;
.message-header {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 2px;
}
.message-author {
font-size: 0.76rem;
color: var(--ds-muted);
margin-bottom: 3px;
font-weight: 600;
font-size: 0.86rem;
font-weight: 700;
color: var(--ds-text);
}
.message-bubble {
position: relative;
background: var(--ds-surface-2);
color: var(--ds-text);
padding: 8px 12px;
border-radius: 10px;
.message-time {
font-size: 0.68rem;
color: var(--ds-muted);
font-family: var(--mono);
}
.message-text {
font-size: 0.88rem;
line-height: 1.45;
color: var(--ds-text);
white-space: pre-wrap;
word-break: break-word;
}
.message-bubble-mine {
background: var(--ds-accent-2);
.message-edited {
font-style: italic;
color: var(--ds-muted);
font-size: 0.78rem;
}
.message-edit-link {
display: none;
position: absolute;
top: -18px;
right: 0;
background: transparent;
border: none;
top: 2px;
right: var(--sp-2);
background: var(--ds-surface-2);
border: 1px solid var(--ds-border);
color: var(--ds-muted);
font-size: 0.68rem;
cursor: pointer;
padding: 2px 4px;
padding: 2px 8px;
border-radius: 6px;
}
.message-edit-link:hover {
color: var(--ds-text);
border-color: var(--ds-accent);
}
.message-bubble:hover .message-edit-link {
.message-row:hover .message-edit-link {
display: inline;
}
.message-edit-input {
background: var(--ds-surface-2);
border: 1px solid var(--ds-accent);
border-radius: 10px;
padding: 8px 12px;
border-radius: 6px;
padding: 6px 10px;
font-size: 0.88rem;
color: var(--ds-text);
font-family: var(--sans);
width: 100%;
}
.message-time {
font-size: 0.68rem;
color: var(--ds-muted);
margin-top: 3px;
font-family: var(--mono);
}
.message-edited {
font-style: italic;
}
+32 -27
View File
@@ -37,21 +37,29 @@ export function MessageList({ messages, members, onEdit }: MessageListProps) {
{messages.map((msg, i) => {
const mine = msg.user_id === user?.id
const prev = messages[i - 1]
const showAvatar = !mine && (!prev || prev.user_id !== msg.user_id)
const showName = showAvatar
// Mattermost-style grouping: every message shows who sent it, but
// 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
return (
<div key={msg.id} className={`message-row${mine ? ' message-row-mine' : ''}`}>
{!mine && (
<div className="message-avatar-slot">
{showAvatar && (
<UserAvatar username={msg.username} colorIndex={senderColorIndex(msg.username, members)} />
)}
</div>
)}
<div className="message-bubble-wrap">
{showName && <div className="message-author">{msg.username}</div>}
<div key={msg.id} className={`message-row${isGroupStart ? ' message-row-start' : ''}`}>
<div className="message-avatar-slot">
{isGroupStart && (
<UserAvatar username={msg.username} colorIndex={senderColorIndex(msg.username, members)} />
)}
</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>
)}
{editing ? (
<input
autoFocus
@@ -65,25 +73,22 @@ export function MessageList({ messages, members, onEdit }: MessageListProps) {
onBlur={() => commitEdit(msg.id)}
/>
) : (
<div className={`message-bubble${mine ? ' message-bubble-mine' : ''}`}>
<div className="message-text">
{msg.content}
{mine && (
<button
type="button"
className="message-edit-link"
onClick={() => startEdit(msg)}
aria-label="Edit message"
>
Edit
</button>
)}
{msg.edited_at && <span className="message-edited"> (edited)</span>}
</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>
{mine && !editing && (
<button
type="button"
className="message-edit-link"
onClick={() => startEdit(msg)}
aria-label="Edit message"
>
Edit
</button>
)}
</div>
)
})}