Improve message markdown display and formatting (#7)

This commit is contained in:
Wes
2026-03-09 16:34:56 -07:00
committed by GitHub
parent 0705327312
commit 774bd62fb3
7 changed files with 1052 additions and 34 deletions

View File

@@ -28,6 +28,9 @@
"lucide-react": "^0.577.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {

894
desktop/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,14 +5,14 @@ import {
useChannelsQuery,
useSelectedChannel,
} from "@/features/channels/hooks";
import { MessageComposer } from "@/features/chat/ui/MessageComposer";
import { MessageTimeline } from "@/features/chat/ui/MessageTimeline";
import {
useChannelMessagesQuery,
useChannelSubscription,
useSendMessageMutation,
} from "@/features/messages/hooks";
import { formatTimelineMessages } from "@/features/messages/lib/formatTimelineMessages";
import { MessageComposer } from "@/features/messages/ui/MessageComposer";
import { MessageTimeline } from "@/features/messages/ui/MessageTimeline";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
import { useIdentityQuery } from "@/shared/api/hooks";
import { SidebarInset, SidebarProvider } from "@/shared/ui/sidebar";

View File

@@ -1,28 +0,0 @@
export type Message = {
author: string;
role: string;
time: string;
body: string;
accent?: boolean;
};
export const currentChannel = {
name: "product-launch",
description: "Static channel layout with a simple chat pane.",
};
export const messages: Message[] = [
{
author: "Maya",
role: "Product",
time: "9:12 AM",
body: "Let’s keep this first pass focused on layout: sidebar, message area, and composer.",
},
{
author: "Elena",
role: "Engineering",
time: "9:24 AM",
body: "Agreed. The content can stay minimal until we wire up real channels and messages.",
accent: true,
},
];

View File

@@ -1,5 +1,6 @@
import type { TimelineMessage } from "@/features/messages/types";
import { cn } from "@/shared/lib/cn";
import { Markdown } from "@/shared/ui/markdown";
import { Separator } from "@/shared/ui/separator";
import { Skeleton } from "@/shared/ui/skeleton";
@@ -31,7 +32,7 @@ function MessageRow({ message }: { message: TimelineMessage }) {
{initials}
</div>
<div className="min-w-0 flex-1 space-y-1">
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 className="font-semibold tracking-tight">{message.author}</h3>
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
@@ -44,9 +45,7 @@ function MessageRow({ message }: { message: TimelineMessage }) {
</p>
) : null}
</div>
<p className="max-w-3xl break-words text-sm leading-7 text-foreground/90">
{message.body}
</p>
<Markdown className="max-w-3xl" content={message.body} />
</div>
</article>
);

View File

@@ -0,0 +1,150 @@
import type * as React from "react";
import ReactMarkdown, { type Components } from "react-markdown";
import remarkBreaks from "remark-breaks";
import remarkGfm from "remark-gfm";
import { cn } from "@/shared/lib/cn";
type MarkdownProps = {
className?: string;
content: string;
};
const markdownComponents: Components = {
a: ({ children, href, ...props }) => (
<a
{...props}
className="font-medium text-primary underline underline-offset-4 transition-colors hover:text-primary/80"
href={href}
rel="noreferrer"
target="_blank"
>
{children}
</a>
),
blockquote: ({ children }) => (
<blockquote className="border-l-2 border-border pl-4 italic text-muted-foreground">
{children}
</blockquote>
),
br: () => <br />,
code: ({
children,
className,
...props
}: React.ComponentProps<"code"> & { inline?: boolean }) => {
const code = String(children).replace(/\n$/, "");
const isBlock =
typeof className === "string" && className.includes("language-")
? true
: code.includes("\n");
if (isBlock) {
return (
<code
{...props}
className={cn(
"block min-w-full whitespace-pre font-mono text-[13px] leading-6 text-foreground",
className,
)}
>
{code}
</code>
);
}
return (
<code
{...props}
className={cn(
"rounded-md bg-muted px-1.5 py-0.5 font-mono text-[13px] text-foreground",
className,
)}
>
{children}
</code>
);
},
h1: ({ children }) => (
<h1 className="text-lg font-semibold tracking-tight">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-base font-semibold tracking-tight">{children}</h2>
),
h3: ({ children }) => (
<h3 className="font-semibold tracking-tight">{children}</h3>
),
hr: () => <hr className="border-border/80" />,
img: ({ alt, src }) => (
<img
alt={alt}
className="max-h-96 rounded-2xl border border-border/70 object-cover"
src={src}
/>
),
li: ({ children }) => <li className="my-1 [&_p]:inline">{children}</li>,
ol: ({ children }) => (
<ol className="list-decimal space-y-1 pl-6 marker:text-muted-foreground">
{children}
</ol>
),
p: ({ children }) => <p className="leading-7">{children}</p>,
pre: ({ children }) => (
<pre className="overflow-x-auto rounded-2xl border border-border/70 bg-muted/60 px-4 py-3 shadow-sm">
{children}
</pre>
),
strong: ({ children }) => (
<strong className="font-semibold">{children}</strong>
),
table: ({ children }) => (
<div className="overflow-x-auto rounded-2xl border border-border/70">
<table className="w-full border-collapse text-left text-sm">
{children}
</table>
</div>
),
td: ({ children }) => (
<td className="border-t border-border/70 px-3 py-2 align-top">
{children}
</td>
),
th: ({ children }) => (
<th className="bg-muted/60 px-3 py-2 font-semibold text-foreground">
{children}
</th>
),
ul: ({ children }) => (
<ul className="list-disc space-y-1 pl-6 marker:text-muted-foreground">
{children}
</ul>
),
};
export function Markdown({ className, content }: MarkdownProps) {
let processedContent = content;
if (/^(?:\s{2}\n)+/.test(content)) {
processedContent = `\u200B${processedContent}`;
}
if (/(?:\s{2}\n)+$/.test(content)) {
processedContent = `${processedContent}\u200B`;
}
return (
<div
className={cn(
"max-w-none break-words text-sm leading-7 text-foreground/90 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&>*]:my-3",
className,
)}
>
<ReactMarkdown
components={markdownComponents}
remarkPlugins={[remarkGfm, remarkBreaks]}
>
{processedContent}
</ReactMarkdown>
</div>
);
}