"use client";
import { mergeCSSClasses } from "@blocknote/core";
import { CommentsExtension } from "@blocknote/core/comments";
import type { CommentData, ThreadData } from "@blocknote/core/comments";
import { MouseEvent, ReactNode, useCallback, useState } from "react";
import {
RiArrowGoBackFill,
RiCheckFill,
RiDeleteBinFill,
RiEditFill,
RiEmotionLine,
RiMoreFill,
} from "react-icons/ri";
import { useComponentsContext } from "../../editor/ComponentsContext.js";
import { useCreateBlockNote } from "../../hooks/useCreateBlockNote.js";
import { useExtension } from "../../hooks/useExtension.js";
import { useDictionary } from "../../i18n/dictionary.js";
import { CommentEditor } from "./CommentEditor.js";
import { EmojiPicker } from "./EmojiPicker.js";
import { ReactionBadge } from "./ReactionBadge.js";
import { defaultCommentEditorSchema } from "./defaultCommentEditorSchema.js";
import { useUser } from "./useUsers.js";
export type CommentProps = {
comment: CommentData;
thread: ThreadData;
showResolveButton?: boolean;
};
/**
* The Comment component displays a single comment with actions,
* a reaction list and an editor when editing.
*
* It's generally used in the `Thread` component for comments that have already been created.
*
*/
export const Comment = ({
comment,
thread,
showResolveButton,
}: CommentProps) => {
// TODO: if REST API becomes popular, all interactions (click handlers) should implement a loading state and error state
// (or optimistic local updates)
const comments = useExtension(CommentsExtension);
const dict = useDictionary();
const commentEditor = useCreateBlockNote({
initialContent: comment.body,
trailingBlock: false,
dictionary: {
...dict,
placeholders: {
emptyDocument: dict.placeholders.edit_comment,
},
},
schema: comments.commentEditorSchema || defaultCommentEditorSchema,
});
const Components = useComponentsContext()!;
const [isEditing, setEditing] = useState(false);
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false);
const threadStore = comments.threadStore;
const handleEdit = useCallback(() => {
setEditing(true);
}, []);
const onEditCancel = useCallback(() => {
commentEditor.replaceBlocks(commentEditor.document, comment.body);
setEditing(false);
}, [commentEditor, comment.body]);
const onEditSubmit = useCallback(
async (_event: MouseEvent) => {
await threadStore.updateComment({
commentId: comment.id,
comment: {
body: commentEditor.document,
},
threadId: thread.id,
});
setEditing(false);
},
[comment, thread.id, commentEditor, threadStore],
);
const onDelete = useCallback(async () => {
await threadStore.deleteComment({
commentId: comment.id,
threadId: thread.id,
});
}, [comment, thread.id, threadStore]);
const onReactionSelect = useCallback(
async (emoji: string) => {
if (threadStore.auth.canAddReaction(comment, emoji)) {
await threadStore.addReaction({
threadId: thread.id,
commentId: comment.id,
emoji,
});
} else if (threadStore.auth.canDeleteReaction(comment, emoji)) {
await threadStore.deleteReaction({
threadId: thread.id,
commentId: comment.id,
emoji,
});
}
},
[threadStore, comment, thread.id],
);
const onResolve = useCallback(async () => {
await threadStore.resolveThread({
threadId: thread.id,
});
}, [thread.id, threadStore]);
const onReopen = useCallback(async () => {
await threadStore.unresolveThread({
threadId: thread.id,
});
}, [thread.id, threadStore]);
const user = useUser(comment.userId);
if (!comment.body) {
return null;
}
let actions: ReactNode | undefined = undefined;
const canAddReaction = threadStore.auth.canAddReaction(comment);
const canDeleteComment = threadStore.auth.canDeleteComment(comment);
const canEditComment = threadStore.auth.canUpdateComment(comment);
const showResolveOrReopen =
showResolveButton &&
(thread.resolved
? threadStore.auth.canUnresolveThread(thread)
: threadStore.auth.canResolveThread(thread));
if (!isEditing) {
actions = (
{canAddReaction && (
onReactionSelect(emoji.native)
}
onOpenChange={setEmojiPickerOpen}
>
)}
{showResolveOrReopen &&
(thread.resolved ? (
) : (
))}
{(canDeleteComment || canEditComment) && (
{canEditComment && (
}
onClick={handleEdit}
>
{dict.comments.actions.edit_comment}
)}
{canDeleteComment && (
}
onClick={onDelete}
>
{dict.comments.actions.delete_comment}
)}
)}
);
}
const timeString = comment.createdAt.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
});
if (!comment.body) {
throw new Error("soft deletes are not yet supported");
}
return (
0 || isEditing
? ({ isEmpty }) => (
<>
{comment.reactions.length > 0 && !isEditing && (
{comment.reactions.map((reaction) => (
))}
{canAddReaction && (
onReactionSelect(emoji.native)
}
onOpenChange={setEmojiPickerOpen}
>
}
mainTooltip={dict.comments.actions.add_reaction}
/>
)}
)}
{isEditing && (
{dict.comments.save_button_text}
{dict.comments.cancel_button_text}
)}
>
)
: undefined
}
/>
);
};