import React, { useRef, useState } from "react"; import { Link } from "react-router"; import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { API_URL } from "../../config/api.ts"; import { can } from "../../utils/auth/permissions.ts"; import type { Comment, CreateCommentRequest, RawComment, UpdateCommentRequest, User, } from "../../model.ts"; import { deserializeComment } from "../../model.ts"; import { Avatar } from "../shared/Avatar.tsx"; import { Markdown } from "../editor/Markdown.tsx"; import type { TextEditorHandle } from "../editor/TextEditor.tsx"; import { LikeButton } from "../social/LikeButton.tsx"; import { relativeTime } from "../../utils/shared/relativeTime.ts"; import { Tooltip } from "../shared/Tooltip.tsx"; import { ConfirmModal } from "../shared/ConfirmModal.tsx"; import { useHeightTransition } from "../../hooks/shared/useHeightTransition.ts"; import { useWS } from "../../hooks/ws/useWS.ts"; import { useAuth } from "../../hooks/auth/useAuth.ts"; import { expectOk } from "../../utils/shared/api.ts"; import { CommentForm } from "./CommentForm.tsx"; interface CommentNodeProps { comment: Comment; tree: Map; depth: number; dumpId: string; currentUser: User | null; token: string | null; onCommentCreated: (comment: Comment) => void; onCommentDeleted: (commentId: string) => void; onCommentUpdated: (comment: Comment) => void; } export function CommentNode({ comment, tree, depth, dumpId, currentUser, token, onCommentCreated, onCommentDeleted, onCommentUpdated, }: CommentNodeProps) { const [replyOpen, setReplyOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const [deleteFailed, setDeleteFailed] = useState(false); const innerRef = useHeightTransition( `${editOpen}:${replyOpen}`, ); const replyEditorRef = useRef(null); const editEditorRef = useRef(null); const { likeCounts, myLikes, castCommentLike, removeCommentLike } = useWS(); const { authFetch } = useAuth(); const children = tree.get(comment.id) ?? []; async function handleReply(body: string) { const res = await authFetch(`${API_URL}/api/dumps/${dumpId}/comments`, { method: "POST", body: JSON.stringify( { body, parentId: comment.id } satisfies CreateCommentRequest, ), }); onCommentCreated(deserializeComment(await expectOk(res))); setReplyOpen(false); } async function handleDelete() { if (!token) return; setDeleteFailed(false); const res = await authFetch(`${API_URL}/api/comments/${comment.id}`, { method: "DELETE", }).catch(() => null); if (res?.ok) { onCommentDeleted(comment.id); } else { setDeleteFailed(true); } } async function handleEditSave(body: string) { // Saving it unchanged would only stamp it "edited". if (body === comment.body) { setEditOpen(false); return; } const res = await authFetch(`${API_URL}/api/comments/${comment.id}`, { method: "PATCH", body: JSON.stringify({ body } satisfies UpdateCommentRequest), }); onCommentUpdated(deserializeComment(await expectOk(res))); setEditOpen(false); } const canModerate = can(currentUser, "comment:moderate"); const canDelete = !comment.deleted && !!currentUser && (currentUser.id === comment.userId || canModerate); const canEdit = !comment.deleted && !!currentUser && (currentUser.id === comment.userId || canModerate); if (comment.deleted) { return (
  • [deleted]

    {children.length > 0 && (
      {children.map((child) => ( ))}
    )}
  • ); } return (
  • {comment.authorUsername} {comment.updatedAt && ( edited {relativeTime(comment.updatedAt)} )}
    {editOpen ? ( Save} pendingLabel={Saving…} errorTitle={t`Failed to save edit`} className="animate-in" onCancel={() => setEditOpen(false)} /> ) : {comment.body}}
    {!editOpen && ( )} {currentUser && !editOpen && ( )} {canEdit && !editOpen && ( )} {canDelete && !editOpen && ( )} {confirmDelete && ( { setConfirmDelete(false); handleDelete(); }} onCancel={() => setConfirmDelete(false)} /> )}
    {deleteFailed && (

    This comment couldn't be deleted. Try again.

    )} {replyOpen && ( Post reply} pendingLabel={Posting…} errorTitle={t`Failed to post reply`} className="animate-in" placeholder={t`Write a reply…`} onCancel={() => setReplyOpen(false)} /> )}
    {children.length > 0 && (
      {children.map((child) => ( ))}
    )}
  • ); }