4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search), MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn. Co-authored-by: Cursor <cursoragent@cursor.com>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useState } from 'react';
|
|
import type { PlazaComment } from '@/types/plaza';
|
|
import { formatCount, formatRelativeTime } from '@/lib/format';
|
|
import { fetchComments } from '@/lib/api';
|
|
import { CommentReportButton } from '@/components/post/CommentReportButton';
|
|
|
|
type CommentThreadProps = {
|
|
postId: string;
|
|
comment: PlazaComment;
|
|
depth?: number;
|
|
onReply: (comment: PlazaComment) => void;
|
|
onToggleLike: (comment: PlazaComment) => void;
|
|
};
|
|
|
|
export function CommentThread({
|
|
postId,
|
|
comment,
|
|
depth = 0,
|
|
onReply,
|
|
onToggleLike,
|
|
}: CommentThreadProps) {
|
|
const [replies, setReplies] = useState<PlazaComment[]>([]);
|
|
const [expanded, setExpanded] = useState(false);
|
|
const [loadingReplies, setLoadingReplies] = useState(false);
|
|
|
|
const loadReplies = async () => {
|
|
if (loadingReplies || expanded) return;
|
|
setLoadingReplies(true);
|
|
try {
|
|
const data = await fetchComments(postId, { parent_id: comment.id });
|
|
setReplies(data.comments);
|
|
setExpanded(true);
|
|
} finally {
|
|
setLoadingReplies(false);
|
|
}
|
|
};
|
|
|
|
const canReply = depth === 0;
|
|
|
|
return (
|
|
<div className={depth > 0 ? 'border-l border-[#ebe4d6] pl-4' : undefined}>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<Link
|
|
href={`/u/${comment.author.slug}`}
|
|
className="font-semibold text-[#17221d] hover:text-[#2f6f57]"
|
|
>
|
|
{comment.author.display_name}
|
|
</Link>
|
|
<p className="mt-1 text-sm leading-relaxed text-[#4a5751]">{comment.content}</p>
|
|
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-[#8a928c]">
|
|
<span>{formatRelativeTime(comment.created_at)}</span>
|
|
{canReply ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => onReply(comment)}
|
|
className="font-semibold text-[#68716c] hover:text-[#2f6f57]"
|
|
>
|
|
回复
|
|
</button>
|
|
) : null}
|
|
<CommentReportButton commentId={comment.id} />
|
|
</div>
|
|
{canReply && comment.reply_count > 0 && !expanded ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => void loadReplies()}
|
|
className="mt-2 text-xs font-semibold text-[#2f6f57]"
|
|
>
|
|
{loadingReplies
|
|
? '加载回复中…'
|
|
: `查看 ${formatCount(comment.reply_count)} 条回复`}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => onToggleLike(comment)}
|
|
className={`shrink-0 rounded-full px-3 py-1 text-xs font-semibold ${
|
|
comment.viewer_liked
|
|
? 'bg-[#2f6f57] text-white'
|
|
: 'border border-[#d6d0c3] text-[#68716c]'
|
|
}`}
|
|
>
|
|
👍 {formatCount(comment.like_count)}
|
|
</button>
|
|
</div>
|
|
{expanded && replies.length > 0 ? (
|
|
<ul className="mt-3 space-y-3">
|
|
{replies.map((reply) => (
|
|
<li key={reply.id}>
|
|
<CommentThread
|
|
postId={postId}
|
|
comment={reply}
|
|
depth={depth + 1}
|
|
onReply={onReply}
|
|
onToggleLike={onToggleLike}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|