fix(comments) : show linked conversations in the normal comment list

This commit is contained in:
2026-10-13 00:06:57 +07:00 Unverified
parent 28517d9b29
commit d58dbcced9
4 changed files with 195 additions and 59 deletions
+4 -34
View File
@@ -14,14 +14,13 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";
import { cn } from "@/lib/utils";
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { CommentSkeleton, CommentThreadSkeleton } from "./comment-skeleton";
import { CommentSkeleton } from "./comment-skeleton";
import { Separator } from "@/components/ui/separator";
import { TableCell, TableRow } from "@/components/ui/table";
import type { CommentHistoryItem, CommentImage, CommentItem, CommentViewer } from "@/lib/comments/types";
import { CommentImageView } from "./comment-image";
import { CommentComposer } from "./comment-composer";
import { commentRequest, commentSignInHref } from "./client";
import { useCommentEvents } from "./use-comment-events";
import { useCommentFeed } from "./use-comment-feed";
// A single minute clock for all timestamps, with a stable SSR snapshot.
@@ -55,7 +54,7 @@ function CommentTimestamp({ value }: { value: string }) {
}
return <time dateTime={value} title={value} className="text-xs text-muted-foreground">{now > 0
? relativeTime()
: `${value.slice(0, 16).replace("T", " ")} UTC`}</time>;
: `${value.slice(0, 16).replace("T")}`}</time>;
}
export function CommentImages({ images }: { images: CommentImage[] }) {
@@ -87,7 +86,7 @@ export function CommentImages({ images }: { images: CommentImage[] }) {
}
function CommentText({ text }: { text: string }) {
return <p className="whitespace-pre-wrap break-words text-sm leading-relaxed">{text.split(/(https?:\/\/[^\s<>]+)/g).map((part, i) =>
return <p className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed">{text.split(/(https?:\/\/[^\s<>]+)/g).map((part, i) =>
/^https?:\/\//.test(part) ? <a key={i} href={part} rel="nofollow ugc noopener noreferrer" target="_blank" className="text-primary underline underline-offset-4">{part}</a> : part)}</p>;
}
@@ -233,7 +232,7 @@ export function CommentCard({ item, viewer, onChange, revision = 0, inbox = fals
{item.version > 1 && canReadHistory && <Button variant="link" size="xs" onClick={() => setHistory(true)}>{"แก้ไขแล้ว"}</Button>}
</div>
{/* Inbox replies reach across the 280px content column and 24px cell padding. */}
{!inbox && optionsMenu && <div className={cn("shrink-0", inboxReply && "md:absolute md:top-2 md:right-[-304px]")}>{optionsMenu}</div>}
{!inbox && optionsMenu && <div className={cn("shrink-0", inboxReply && "md:absolute md:top-2 md:-right-76")}>{optionsMenu}</div>}
</div>
<div className={cn("contents", fullWidthMobile && "max-sm:col-span-2 max-sm:flex max-sm:min-w-0 max-sm:flex-col max-sm:gap-2")}>
{item.replyToName && <p className="text-xs text-muted-foreground">{"ตอบกลับ"} {item.replyToName}</p>}
@@ -339,32 +338,3 @@ function CommentReplyTree({ items, viewer, onChange, focusReplyId, moderationDet
}
return <div className="flex flex-col gap-4">{branch(null)}</div>;
}
export function CommentThread({ rootId, expectedTarget, onChange, focusReplyId }: { focusReplyId?: string; rootId: string; expectedTarget: string; onChange: () => void | Promise<unknown> }) {
const [data, setData] = useState<{ item: CommentItem; viewer: CommentViewer | null } | null>(null);
const [error, setError] = useState("");
const [revision, setRevision] = useState(0);
const lastThread = useRef("");
const load = useCallback(async (forceReplies = false) => {
try {
const result = await commentRequest<{ item: CommentItem; viewer: CommentViewer | null }>(`/api/comments/${rootId}`);
if (result.item.target !== expectedTarget) throw new Error("การสนทนานี้เป็นของไกด์อื่น");
const payload = JSON.stringify(result);
if (lastThread.current !== payload) {
lastThread.current = payload; setData(result); setRevision((value) => value + 1);
} else if (forceReplies) setRevision((value) => value + 1);
setError("");
} catch (cause) { lastThread.current = ""; setData(null); setError(cause instanceof Error ? cause.message : "ไม่สามารถโหลดการสนทนาได้"); }
}, [rootId, expectedTarget]);
useCommentEvents({ target: expectedTarget }, load);
useEffect(() => {
void Promise.resolve().then(() => load());
}, [load]);
return <div className="flex min-w-0 flex-col gap-3" aria-busy={!data && !error}>
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
{data && <CommentCard item={data.item} viewer={data.viewer} moderationDetails={Boolean(data.viewer?.admin)} defaultExpanded focusReplyId={focusReplyId} revision={revision} onChange={async () => { await load(true); await onChange(); }} />}
{!data && !error && <CommentThreadSkeleton />}
{error && <Button variant="outline" onClick={() => void load()}>{"ลองใหม่"}</Button>}
</div>;
}