fix(comments): replace optimistic updates with loading state

- Remove temp-xxx optimistic inserts from submitComment/submitReply
- Wait for API response, then insert real comment into timeline
- Add Loader2 spinner to comment/reply submit buttons during loading
- Remove hover card from Markdown.tsx (will be handled via NodeView later)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-03-31 16:03:13 +08:00
parent b9ea10c89d
commit 98829fad29
4 changed files with 15 additions and 47 deletions

View file

@ -3,7 +3,6 @@ import ReactMarkdown, { type Components } from 'react-markdown'
import rehypeRaw from 'rehype-raw'
import remarkGfm from 'remark-gfm'
import { cn } from '@/lib/utils'
import { MentionHoverCard } from '@/components/common/mention-hover-card'
import { CodeBlock, InlineCode } from './CodeBlock'
import { preprocessLinks } from './linkify'
@ -61,15 +60,10 @@ function createComponents(
a: ({ href, children }) => {
// Mention links: mention://member/id or mention://agent/id
if (href?.startsWith('mention://')) {
const parts = href.replace('mention://', '').split('/')
const mentionType = parts[0] ?? 'member'
const mentionId = parts[1] ?? ''
return (
<MentionHoverCard type={mentionType} id={mentionId}>
<span className="text-primary font-semibold mx-0.5">
{children}
</span>
</MentionHoverCard>
<span className="text-primary font-semibold mx-0.5">
{children}
</span>
)
}

View file

@ -1,7 +1,7 @@
"use client";
import { useRef, useState } from "react";
import { ArrowUp } from "lucide-react";
import { ArrowUp, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich-text-editor";
@ -44,7 +44,7 @@ function CommentInput({ onSubmit }: CommentInputProps) {
disabled={isEmpty || submitting}
onClick={handleSubmit}
>
<ArrowUp className="h-4 w-4" />
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <ArrowUp className="h-4 w-4" />}
</Button>
</div>
</div>

View file

@ -1,7 +1,7 @@
"use client";
import { useRef, useState } from "react";
import { ArrowUp } from "lucide-react";
import { ArrowUp, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { RichTextEditor, type RichTextEditorRef } from "@/components/common/rich-text-editor";
import { ActorAvatar } from "@/components/common/actor-avatar";
@ -83,7 +83,7 @@ function ReplyInput({
onClick={handleSubmit}
tabIndex={isEmpty ? -1 : 0}
>
<ArrowUp className="h-3.5 w-3.5" />
{submitting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <ArrowUp className="h-3.5 w-3.5" />}
</Button>
</div>
</div>

View file

@ -176,27 +176,14 @@ export function useIssueTimeline(issueId: string, userId?: string) {
const submitComment = useCallback(
async (content: string) => {
if (!content.trim() || submitting || !userId) return;
const tempId = "temp-" + Date.now();
const tempEntry: TimelineEntry = {
type: "comment",
id: tempId,
actor_type: "member",
actor_id: userId,
content,
parent_id: null,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
comment_type: "comment",
};
setTimeline((prev) => [...prev, tempEntry]);
setSubmitting(true);
try {
const comment = await api.createComment(issueId, content);
setTimeline((prev) =>
prev.map((e) => (e.id === tempId ? commentToTimelineEntry(comment) : e)),
);
setTimeline((prev) => {
if (prev.some((e) => e.id === comment.id)) return prev;
return [...prev, commentToTimelineEntry(comment)];
});
} catch {
setTimeline((prev) => prev.filter((e) => e.id !== tempId));
toast.error("Failed to send comment");
} finally {
setSubmitting(false);
@ -208,26 +195,13 @@ export function useIssueTimeline(issueId: string, userId?: string) {
const submitReply = useCallback(
async (parentId: string, content: string) => {
if (!content.trim() || !userId) return;
const tempId = "temp-" + Date.now();
const tempEntry: TimelineEntry = {
type: "comment",
id: tempId,
actor_type: "member",
actor_id: userId,
content,
parent_id: parentId,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
comment_type: "comment",
};
setTimeline((prev) => [...prev, tempEntry]);
try {
const comment = await api.createComment(issueId, content, "comment", parentId);
setTimeline((prev) =>
prev.map((e) => (e.id === tempId ? commentToTimelineEntry(comment) : e)),
);
setTimeline((prev) => {
if (prev.some((e) => e.id === comment.id)) return prev;
return [...prev, commentToTimelineEntry(comment)];
});
} catch {
setTimeline((prev) => prev.filter((e) => e.id !== tempId));
toast.error("Failed to send reply");
}
},