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>
156 lines
4.4 KiB
TypeScript
156 lines
4.4 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import type { PlazaComment } from '@/types/plaza';
|
|
import { LoginPrompt } from '@/components/auth/LoginPrompt';
|
|
import { CommentInput } from '@/components/comment/CommentInput';
|
|
import { CommentList } from '@/components/comment/CommentList';
|
|
import {
|
|
PlazaApiError,
|
|
createComment,
|
|
fetchComments,
|
|
toggleCommentLike,
|
|
} from '@/lib/api';
|
|
|
|
type CommentSectionProps = {
|
|
postId: string;
|
|
allowComment?: boolean;
|
|
};
|
|
|
|
export function CommentSection({ postId, allowComment = true }: CommentSectionProps) {
|
|
const [comments, setComments] = useState<PlazaComment[]>([]);
|
|
const [cursor, setCursor] = useState<string | null>(null);
|
|
const [hasMore, setHasMore] = useState(false);
|
|
const [loading, setLoading] = useState(true);
|
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
const [replyTo, setReplyTo] = useState<PlazaComment | null>(null);
|
|
const [loginOpen, setLoginOpen] = useState(false);
|
|
|
|
const loadComments = useCallback(
|
|
async (nextCursor?: string | null) => {
|
|
const data = await fetchComments(postId, { cursor: nextCursor ?? undefined });
|
|
if (nextCursor) {
|
|
setComments((current) => [...current, ...data.comments]);
|
|
} else {
|
|
setComments(data.comments);
|
|
}
|
|
setCursor(data.next_cursor);
|
|
setHasMore(data.has_more);
|
|
},
|
|
[postId],
|
|
);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setLoading(true);
|
|
loadComments()
|
|
.catch(() => {
|
|
if (!cancelled) setComments([]);
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [loadComments]);
|
|
|
|
const handleCreate = async (content: string) => {
|
|
try {
|
|
await createComment(postId, content, replyTo?.id ?? null);
|
|
setReplyTo(null);
|
|
await loadComments();
|
|
} catch (error) {
|
|
if (error instanceof PlazaApiError && error.code === 'unauthorized') {
|
|
setLoginOpen(true);
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const handleToggleLike = async (comment: PlazaComment) => {
|
|
try {
|
|
const liked = !comment.viewer_liked;
|
|
await toggleCommentLike(comment.id, liked);
|
|
setComments((current) =>
|
|
current.map((item) =>
|
|
item.id === comment.id
|
|
? {
|
|
...item,
|
|
viewer_liked: liked,
|
|
like_count: Math.max(0, item.like_count + (liked ? 1 : -1)),
|
|
}
|
|
: item,
|
|
),
|
|
);
|
|
} catch (error) {
|
|
if (error instanceof PlazaApiError && error.code === 'unauthorized') {
|
|
setLoginOpen(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleLoadMore = async () => {
|
|
if (!cursor || loadingMore) return;
|
|
setLoadingMore(true);
|
|
try {
|
|
await loadComments(cursor);
|
|
} finally {
|
|
setLoadingMore(false);
|
|
}
|
|
};
|
|
|
|
if (!allowComment) {
|
|
return (
|
|
<div className="rounded-2xl border border-[#d6d0c3] bg-[#fffdf7] px-5 py-4 text-sm text-[#68716c]">
|
|
作者已关闭评论
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="rounded-2xl border border-[#d6d0c3] bg-[#fffdf7] px-5 py-5">
|
|
<h2 className="text-lg font-semibold text-[#17221d]">评论</h2>
|
|
<div className="mt-4">
|
|
{replyTo ? (
|
|
<p className="mb-2 text-sm text-[#68716c]">
|
|
回复 @{replyTo.author.display_name}
|
|
</p>
|
|
) : null}
|
|
<CommentInput
|
|
placeholder={replyTo ? `回复 @${replyTo.author.display_name}` : '写下你的评论…'}
|
|
onSubmit={handleCreate}
|
|
onCancel={replyTo ? () => setReplyTo(null) : undefined}
|
|
/>
|
|
</div>
|
|
<div className="mt-6">
|
|
{loading ? (
|
|
<p className="py-6 text-center text-sm text-[#8a928c]">加载评论中…</p>
|
|
) : (
|
|
<CommentList
|
|
postId={postId}
|
|
comments={comments}
|
|
onReply={setReplyTo}
|
|
onToggleLike={handleToggleLike}
|
|
/>
|
|
)}
|
|
{hasMore ? (
|
|
<button
|
|
type="button"
|
|
disabled={loadingMore}
|
|
onClick={handleLoadMore}
|
|
className="mt-4 w-full rounded-full border border-[#d6d0c3] py-2.5 text-sm font-semibold text-[#2f6f57]"
|
|
>
|
|
{loadingMore ? '加载中…' : '加载更多评论'}
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
<LoginPrompt
|
|
open={loginOpen}
|
|
onClose={() => setLoginOpen(false)}
|
|
message="登录后即可发表评论"
|
|
/>
|
|
</section>
|
|
);
|
|
}
|