2024-07-02 03:55:16 -07:00

196 lines
5.9 KiB
TypeScript

import { AnimatePresence } from "framer-motion";
import { observer } from "mobx-react";
import { DoneIcon } from "outline-icons";
import queryString from "query-string";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useHistory, useLocation, useRouteMatch } from "react-router-dom";
import styled, { css } from "styled-components";
import { ProsemirrorData } from "@shared/types";
import Button from "~/components/Button";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import Scrollable from "~/components/Scrollable";
import Tooltip from "~/components/Tooltip";
import useCurrentUser from "~/hooks/useCurrentUser";
import useFocusedComment from "~/hooks/useFocusedComment";
import useKeyDown from "~/hooks/useKeyDown";
import usePersistedState from "~/hooks/usePersistedState";
import usePolicy from "~/hooks/usePolicy";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
import { bigPulse } from "~/styles/animations";
import CommentForm from "./CommentForm";
import CommentThread from "./CommentThread";
import Sidebar from "./SidebarLayout";
function Comments() {
const { ui, comments, documents } = useStores();
const { t } = useTranslation();
const user = useCurrentUser();
const location = useLocation();
const history = useHistory();
const match = useRouteMatch<{ documentSlug: string }>();
const params = useQuery();
const [pulse, setPulse] = React.useState(false);
const document = documents.getByUrl(match.params.documentSlug);
const focusedComment = useFocusedComment();
const can = usePolicy(document);
useKeyDown("Escape", () => document && ui.collapseComments(document?.id));
const [draft, onSaveDraft] = usePersistedState<ProsemirrorData | undefined>(
`draft-${document?.id}-new`,
undefined
);
const viewingResolved = params.get("resolved") === "";
const resolvedThreads = document
? comments.resolvedThreadsInDocument(document.id)
: [];
const resolvedThreadsCount = resolvedThreads.length;
React.useEffect(() => {
setPulse(true);
const timeout = setTimeout(() => setPulse(false), 250);
return () => {
clearTimeout(timeout);
setPulse(false);
};
}, [resolvedThreadsCount]);
if (!document) {
return null;
}
const threads = (
viewingResolved
? resolvedThreads
: comments.unresolvedThreadsInDocument(document.id)
).filter((thread) => thread.createdById === user.id);
const hasComments = threads.length > 0;
const toggleViewingResolved = () => {
history.push({
search: queryString.stringify({
...queryString.parse(location.search),
resolved: viewingResolved ? undefined : "",
}),
pathname: location.pathname,
});
};
return (
<Sidebar
title={
<Flex align="center" justify="space-between" auto>
{viewingResolved ? (
<React.Fragment key="resolved">
<span>{t("Resolved comments")}</span>
<Tooltip delay={500} content={t("View comments")}>
<ResolvedButton
neutral
borderOnHover
icon={<DoneIcon />}
onClick={toggleViewingResolved}
/>
</Tooltip>
</React.Fragment>
) : (
<React.Fragment>
<span>{t("Comments")}</span>
<Tooltip delay={250} content={t("View resolved comments")}>
<ResolvedButton
neutral
borderOnHover
icon={<DoneIcon outline />}
onClick={toggleViewingResolved}
$pulse={pulse}
/>
</Tooltip>
</React.Fragment>
)}
</Flex>
}
onClose={() => ui.collapseComments(document?.id)}
scrollable={false}
>
<Scrollable
id="comments"
bottomShadow={!focusedComment}
hiddenScrollbars
topShadow
>
<Wrapper $hasComments={hasComments}>
{hasComments ? (
threads.map((thread) => (
<CommentThread
key={thread.id}
comment={thread}
document={document}
recessed={!!focusedComment && focusedComment.id !== thread.id}
focused={focusedComment?.id === thread.id}
/>
))
) : (
<NoComments align="center" justify="center" auto>
<PositionedEmpty>
{viewingResolved
? t("No resolved comments")
: t("No comments yet")}
</PositionedEmpty>
</NoComments>
)}
</Wrapper>
</Scrollable>
<AnimatePresence initial={false}>
{!focusedComment && can.comment && !viewingResolved && (
<NewCommentForm
draft={draft}
onSaveDraft={onSaveDraft}
documentId={document.id}
placeholder={`${t("Add a comment")}`}
autoFocus={false}
dir={document.dir}
animatePresence
standalone
/>
)}
</AnimatePresence>
</Sidebar>
);
}
const ResolvedButton = styled(Button)<{ $pulse: boolean }>`
${(props) =>
props.$pulse &&
css`
animation: ${bigPulse} 250ms 1;
`}
`;
const PositionedEmpty = styled(Empty)`
position: absolute;
top: calc(50vh - 30px);
transform: translateY(-50%);
`;
const NoComments = styled(Flex)`
padding-bottom: 65px;
height: 100%;
`;
const Wrapper = styled.div<{ $hasComments: boolean }>`
padding-bottom: ${(props) => (props.$hasComments ? "50vh" : "0")};
height: ${(props) => (props.$hasComments ? "auto" : "100%")};
`;
const NewCommentForm = styled(CommentForm)<{ dir?: "ltr" | "rtl" }>`
padding: 12px;
padding-right: ${(props) => (props.dir !== "rtl" ? "18px" : "12px")};
padding-left: ${(props) => (props.dir === "rtl" ? "18px" : "12px")};
`;
export default observer(Comments);