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

@@ -7,6 +7,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp
interface Props {
message: Message;
compact?: boolean;
replyTarget?: Message;
onSelect: (id: string) => void;
onDelete: (channelId: string, msgId: string) => void;
@@ -33,8 +34,8 @@ function timeAgo(dateStr: string): string {
}
function renderContent(text: string) {
// Split on @mentions and render them as highlighted spans
const parts = text.split(/(@[\w-]+)/g);
// Split on @mentions and URLs
const parts = text.split(/((?:https?:\/\/)[^\s]+|@[\w-]+)/g);
return parts.map((part, i) => {
if (part.startsWith("@")) {
return (
@@ -43,6 +44,20 @@ function renderContent(text: string) {
</span>
);
}
if (part.startsWith("http://") || part.startsWith("https://")) {
return (
<a
key={i}
href={part}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="text-primary underline underline-offset-2 hover:text-primary/80"
>
{part}
</a>
);
}
return part;
});
}
@@ -56,7 +71,7 @@ function userHue(username: string): number {
return Math.abs(hash) % 360;
}
export function MessageItem({ message, replyTarget, onSelect, onDelete, currentUsername, selected }: Props) {
export function MessageItem({ message, compact, replyTarget, onSelect, onDelete, currentUsername, selected }: Props) {
const [metaOpen, setMetaOpen] = useState(false);
const isAgent = message.user.role === "agent";
const isDeleted = !!message.deleted_at;
@@ -77,7 +92,8 @@ export function MessageItem({ message, replyTarget, onSelect, onDelete, currentU
id={`msg-${message.id}`}
onClick={() => onSelect(message.id)}
className={cn(
"group relative border-b border-border/50 border-l-4 transition-all duration-300 cursor-pointer",
"group relative border-l-4 transition-all duration-300 cursor-pointer",
compact ? "" : "border-b border-border/50",
cfg.border,
selected ? "!border-l-primary bg-primary/5" : isAgent ? "bg-card" : "bg-background",
"hover:bg-muted/30",
@@ -96,9 +112,9 @@ export function MessageItem({ message, replyTarget, onSelect, onDelete, currentU
</button>
)}
<div className="px-4 py-3 md:px-5 md:py-4">
{/* Header */}
<div className="flex items-center gap-2.5 text-[11px] flex-wrap">
<div className={cn("px-4 md:px-5", compact ? "py-0.5 pl-[52px] md:pl-[56px]" : "py-3 md:py-4")}>
{/* Header — hidden in compact mode */}
{!compact && <div className="flex items-center gap-2.5 text-[11px] flex-wrap">
{/* Avatar — ape emoji with OKLCH color, agents get first letter */}
<Avatar size="sm" className="rounded-none">
<AvatarFallback
@@ -151,7 +167,7 @@ export function MessageItem({ message, replyTarget, onSelect, onDelete, currentU
</Tooltip>
</TooltipProvider>
</div>
</div>}
{/* Floating action pill — top-right, appears on hover */}
<div className="absolute -top-3 right-3 md:opacity-0 md:translate-y-1 md:group-hover:opacity-100 md:group-hover:translate-y-0 transition-all duration-150 flex border-2 border-border bg-card shadow-lg z-10">