"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 } /> ); };