message grouping + date separators + linkified URLs

- Consecutive same-sender messages within 5min collapse (compact mode)
- Compact: no avatar/name/badges, aligned to content area, minimal padding
- Date separators with horizontal lines between days
- URLs auto-linkified in orange with underline
- Links open in new tab, stopPropagation to not trigger select

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 21:40:37 +02:00
parent 4a05665d64
commit f88a4ef6a7
2 changed files with 71 additions and 34 deletions

View File

@@ -206,32 +206,53 @@ export default function App() {
no messages yet start typing below
</div>
) : (
messages.map((msg) => (
<MessageItem
key={msg.id}
message={msg}
replyTarget={msg.reply_to ? messagesById.get(msg.reply_to) : undefined}
currentUsername={getCurrentUsername()}
selected={selectedMessages.some((s) => s.id === msg.id)}
onSelect={(id) => {
setSelectedMessages((prev) => {
const exists = prev.find((s) => s.id === id);
if (exists) return prev.filter((s) => s.id !== id);
const target = messagesById.get(id);
if (!target) return prev;
return [...prev, { id, username: target.user.display_name, content: target.content }];
});
}}
onDelete={async (chId, msgId) => {
try {
await deleteMessage(chId, msgId);
loadMessages();
} catch {
// ignore
}
}}
/>
))
messages.map((msg, i) => {
const prev = i > 0 ? messages[i - 1] : null;
const sameSender = prev && prev.user.username === msg.user.username;
const withinWindow = prev && (new Date(msg.created_at).getTime() - new Date(prev.created_at).getTime()) < 5 * 60 * 1000;
const compact = !!(sameSender && withinWindow && !msg.reply_to);
const prevDate = prev ? new Date(prev.created_at).toDateString() : null;
const thisDate = new Date(msg.created_at).toDateString();
const showDate = prevDate !== thisDate;
return (
<div key={msg.id}>
{showDate && (
<div className="flex items-center gap-3 px-5 py-3">
<div className="flex-1 h-px bg-border" />
<span className="text-[10px] font-mono text-muted-foreground uppercase tracking-widest">
{new Date(msg.created_at).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" })}
</span>
<div className="flex-1 h-px bg-border" />
</div>
)}
<MessageItem
message={msg}
compact={compact}
replyTarget={msg.reply_to ? messagesById.get(msg.reply_to) : undefined}
currentUsername={getCurrentUsername()}
selected={selectedMessages.some((s) => s.id === msg.id)}
onSelect={(id) => {
setSelectedMessages((prevSel) => {
const exists = prevSel.find((s) => s.id === id);
if (exists) return prevSel.filter((s) => s.id !== id);
const target = messagesById.get(id);
if (!target) return prevSel;
return [...prevSel, { id, username: target.user.display_name, content: target.content }];
});
}}
onDelete={async (chId, msgId) => {
try {
await deleteMessage(chId, msgId);
loadMessages();
} catch {
// ignore
}
}}
/>
</div>
);
})
)}
</ScrollArea>