Various commenting improvements (#4938)

* fix: New threads attached to previous as replies

* fix: Cannot use floating toolbar properly in comments

* perf: Avoid re-writing history on click in editor

* fix: Comment on text selection

* fix: 'Copy link' on comments uses wrong hostname

* Show comment buttons on input focus rather than non-empty input
Increase maximum sidebar size

* Allow opening comments from document menu

* fix: Clicking comment menu should not focus thread
This commit is contained in:
Tom Moor
2023-02-26 14:19:12 -05:00
committed by GitHub
parent b813f20f8f
commit 08df14618c
16 changed files with 219 additions and 141 deletions

View File

@@ -22,7 +22,7 @@ type Props = {
/** The document that the comment will be associated with */
documentId: string;
/** The comment thread that the comment will be associated with */
thread: Comment;
thread?: Comment;
/** Placeholder text to display in the editor */
placeholder?: string;
/** Whether to focus the editor on mount */
@@ -59,20 +59,22 @@ function CommentForm({
}: Props) {
const { editor } = useDocumentContext();
const [data, setData] = usePersistedState<Record<string, any> | undefined>(
`draft-${documentId}-${thread.id}`,
`draft-${documentId}-${thread?.id ?? "new"}`,
undefined
);
const formRef = React.useRef<HTMLFormElement>(null);
const editorRef = React.useRef<SharedEditor>(null);
const [forceRender, setForceRender] = React.useState(0);
const [inputFocused, setInputFocused] = React.useState(false);
const { t } = useTranslation();
const { showToast } = useToasts();
const { comments } = useStores();
const user = useCurrentUser();
const isEmpty = editorRef.current?.isEmpty() ?? true;
useOnClickOutside(formRef, () => {
if (isEmpty && thread.isNew) {
const isEmpty = editorRef.current?.isEmpty() ?? true;
if (isEmpty && thread?.isNew) {
if (thread.id) {
editor?.removeComment(thread.id);
}
@@ -86,19 +88,29 @@ function CommentForm({
setData(undefined);
setForceRender((s) => ++s);
thread
const comment =
thread ??
new Comment(
{
documentId,
data,
},
comments
);
comment
.save({
documentId,
data,
})
.catch(() => {
thread.isNew = true;
comment.isNew = true;
showToast(t("Error creating comment"), { type: "error" });
});
// optimistically update the comment model
thread.isNew = false;
thread.createdBy = user;
comment.isNew = false;
comment.createdBy = user;
});
const handleCreateReply = async (event: React.FormEvent) => {
@@ -145,6 +157,16 @@ function CommentForm({
setForceRender((s) => ++s);
};
const handleFocus = () => {
onFocus?.();
setInputFocused(true);
};
const handleBlur = () => {
onBlur?.();
setInputFocused(false);
};
// Focus the editor when it's a new comment just mounted, after a delay as the
// editor is mounted within a fade transition.
React.useEffect(() => {
@@ -199,21 +221,23 @@ function CommentForm({
ref={editorRef}
onChange={handleChange}
onSave={handleSave}
onFocus={onFocus}
onBlur={onBlur}
onFocus={handleFocus}
onBlur={handleBlur}
maxLength={CommentValidation.maxLength}
placeholder={
placeholder ||
// isNew is only the case for comments that exist in draft state,
// they are marks in the document, but not yet saved to the db.
(thread.isNew ? `${t("Add a comment")}` : `${t("Add a reply")}`)
(thread?.isNew
? `${t("Add a comment")}`
: `${t("Add a reply")}`)
}
/>
{!isEmpty && (
{inputFocused && (
<Flex justify={dir === "rtl" ? "flex-end" : "flex-start"} gap={8}>
<ButtonSmall type="submit" borderOnHover>
{thread.isNew ? t("Post") : t("Reply")}
{thread && !thread.isNew ? t("Reply") : t("Post")}
</ButtonSmall>
<ButtonSmall onClick={handleCancel} neutral borderOnHover>
{t("Cancel")}

View File

@@ -4,7 +4,6 @@ import * as React from "react";
import { useTranslation } from "react-i18next";
import { useRouteMatch } from "react-router-dom";
import styled from "styled-components";
import Comment from "~/models/Comment";
import Empty from "~/components/Empty";
import Flex from "~/components/Flex";
import useCurrentUser from "~/hooks/useCurrentUser";
@@ -16,7 +15,6 @@ import Sidebar from "./SidebarLayout";
function Comments() {
const { ui, comments, documents } = useStores();
const [newComment] = React.useState(new Comment({}, comments));
const { t } = useTranslation();
const user = useCurrentUser();
const match = useRouteMatch<{ documentSlug: string }>();
@@ -54,9 +52,7 @@ function Comments() {
<AnimatePresence initial={false}>
{!focusedComment && (
<NewCommentForm
key="new-comment-form"
documentId={document.id}
thread={newComment}
placeholder={`${t("Add a comment")}`}
autoFocus={false}
dir={document.dir}

View File

@@ -10,6 +10,7 @@ import Document from "~/models/Document";
import { RefHandle } from "~/components/ContentEditable";
import Editor, { Props as EditorProps } from "~/components/Editor";
import Flex from "~/components/Flex";
import useFocusedComment from "~/hooks/useFocusedComment";
import useStores from "~/hooks/useStores";
import {
documentHistoryUrl,
@@ -43,6 +44,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
const titleRef = React.useRef<RefHandle>(null);
const { t } = useTranslation();
const match = useRouteMatch();
const focusedComment = useFocusedComment();
const { ui, comments, auth } = useStores();
const { user, team } = auth;
const history = useHistory();
@@ -91,13 +93,13 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
pathname: window.location.pathname.replace(/\/history$/, ""),
state: { commentId },
});
} else {
} else if (focusedComment) {
history.replace({
pathname: window.location.pathname,
});
}
},
[ui, history]
[ui, focusedComment, history]
);
// Create a Comment model in local store when a comment mark is created, this
@@ -177,6 +179,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
readOnly={readOnly}
shareId={shareId}
userId={user?.id}
focusedCommentId={focusedComment?.id}
onClickCommentMark={handleClickComment}
onCreateCommentMark={
team?.getPreference(TeamPreference.Commenting)

View File

@@ -83,7 +83,7 @@ function Insights() {
</Text>
</Content>
<Content column>
<Heading>{t("Collaborators")}</Heading>
<Heading>{t("Contributors")}</Heading>
<Text type="secondary" size="small">
{t(`Created`)} <Time dateTime={document.createdAt} addSuffix />.
<br />
@@ -92,7 +92,7 @@ function Insights() {
</Text>
<ListSpacing>
<PaginatedList
aria-label={t("Collaborators")}
aria-label={t("Contributors")}
items={document.collaborators}
renderItem={(model: User) => (
<ListItem