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:
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user