import { differenceInMilliseconds } from "date-fns"; import { toJS } from "mobx"; import { observer } from "mobx-react"; import { darken } from "polished"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import styled, { css } from "styled-components"; import breakpoint from "styled-components-breakpoint"; import EventBoundary from "@shared/components/EventBoundary"; import { s } from "@shared/styles"; import { ProsemirrorData } from "@shared/types"; import { dateToRelative } from "@shared/utils/date"; import { Minute } from "@shared/utils/time"; import Comment from "~/models/Comment"; import Avatar from "~/components/Avatar"; import ButtonSmall from "~/components/ButtonSmall"; import Flex from "~/components/Flex"; import Text from "~/components/Text"; import Time from "~/components/Time"; import useBoolean from "~/hooks/useBoolean"; import CommentMenu from "~/menus/CommentMenu"; import { hover } from "~/styles"; import CommentEditor from "./CommentEditor"; import { HighlightedText } from "./HighlightText"; /** * Hook to calculate if we should display a timestamp on a comment * * @param createdAt The date the comment was created * @param previousCreatedAt The date of the previous comment, if any * @returns boolean if to show timestamp */ function useShowTime( createdAt: string | undefined, previousCreatedAt: string | undefined ): boolean { if (!createdAt) { return false; } const previousTimeStamp = previousCreatedAt ? dateToRelative(Date.parse(previousCreatedAt)) : undefined; const currentTimeStamp = dateToRelative(Date.parse(createdAt)); const msSincePreviousComment = previousCreatedAt ? differenceInMilliseconds( Date.parse(createdAt), Date.parse(previousCreatedAt) ) : 0; return ( !msSincePreviousComment || (msSincePreviousComment > 15 * Minute && previousTimeStamp !== currentTimeStamp) ); } type Props = { /** The comment to render */ comment: Comment; /** The text direction of the editor */ dir?: "rtl" | "ltr"; /** Whether this is the first comment in the thread */ firstOfThread?: boolean; /** Whether this is the last comment in the thread */ lastOfThread?: boolean; /** Whether this is the first consecutive comment by this author */ firstOfAuthor?: boolean; /** Whether this is the last consecutive comment by this author */ lastOfAuthor?: boolean; /** The date of the previous comment in the thread */ previousCommentCreatedAt?: string; /** Whether the user can reply in the thread */ canReply: boolean; /** Callback when the comment has been deleted */ onDelete: () => void; /** Callback when the comment has been updated */ onUpdate: (attrs: { resolved: boolean }) => void; /** Text to highlight at the top of the comment */ highlightedText?: string; }; function CommentThreadItem({ comment, firstOfAuthor, firstOfThread, lastOfThread, dir, previousCommentCreatedAt, canReply, onDelete, onUpdate, highlightedText, }: Props) { const { t } = useTranslation(); const [forceRender, setForceRender] = React.useState(0); const [data, setData] = React.useState(toJS(comment.data)); const showAuthor = firstOfAuthor; const showTime = useShowTime(comment.createdAt, previousCommentCreatedAt); const showEdited = comment.updatedAt && comment.updatedAt !== comment.createdAt && !comment.isResolved; const [isEditing, setEditing, setReadOnly] = useBoolean(); const formRef = React.useRef(null); const handleChange = (value: (asString: boolean) => ProsemirrorData) => { setData(value(false)); }; const handleSave = () => { formRef.current?.dispatchEvent( new Event("submit", { cancelable: true, bubbles: true }) ); }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); try { setReadOnly(); await comment.save({ data, }); } catch (error) { setEditing(); toast.error(t("Error updating comment")); } }; const handleCancel = () => { setData(toJS(comment.data)); setReadOnly(); setForceRender((i) => ++i); }; React.useEffect(() => { setData(toJS(comment.data)); setForceRender((i) => ++i); }, [comment.data]); return ( {firstOfAuthor && ( )} {(showAuthor || showTime) && ( {showAuthor && {comment.createdBy.name}} {showAuthor && showTime && <> · } {showTime && (