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,9 +22,10 @@ import {
LightBulbIcon, LightBulbIcon,
UnpublishIcon, UnpublishIcon,
PublishIcon, PublishIcon,
CommentIcon,
} from "outline-icons"; } from "outline-icons";
import * as React from "react"; import * as React from "react";
import { ExportContentType } from "@shared/types"; import { ExportContentType, TeamPreference } from "@shared/types";
import { getEventFiles } from "@shared/utils/files"; import { getEventFiles } from "@shared/utils/files";
import DocumentDelete from "~/scenes/DocumentDelete"; import DocumentDelete from "~/scenes/DocumentDelete";
import DocumentMove from "~/scenes/DocumentMove"; import DocumentMove from "~/scenes/DocumentMove";
@@ -466,7 +467,7 @@ export const printDocument = createAction({
icon: <PrintIcon />, icon: <PrintIcon />,
visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print), visible: ({ activeDocumentId }) => !!(activeDocumentId && window.print),
perform: async () => { perform: async () => {
window.print(); queueMicrotask(window.print);
}, },
}); });
@@ -708,6 +709,29 @@ export const permanentlyDeleteDocument = createAction({
}, },
}); });
export const openDocumentComments = createAction({
name: ({ t }) => t("Comments"),
analyticsName: "Open comments",
section: DocumentSection,
icon: <CommentIcon />,
visible: ({ activeDocumentId, stores }) => {
const can = stores.policies.abilities(activeDocumentId ?? "");
return (
!!activeDocumentId &&
can.read &&
!can.restore &&
!!stores.auth.team?.getPreference(TeamPreference.Commenting)
);
},
perform: ({ activeDocumentId, stores }) => {
if (!activeDocumentId) {
return;
}
stores.ui.toggleComments();
},
});
export const openDocumentHistory = createAction({ export const openDocumentHistory = createAction({
name: ({ t }) => t("History"), name: ({ t }) => t("History"),
analyticsName: "Open document history", analyticsName: "Open document history",
@@ -771,6 +795,7 @@ export const rootDocumentActions = [
printDocument, printDocument,
pinDocumentToCollection, pinDocumentToCollection,
pinDocumentToHome, pinDocumentToHome,
openDocumentComments,
openDocumentHistory, openDocumentHistory,
openDocumentInsights, openDocumentInsights,
]; ];

View File

@@ -21,7 +21,6 @@ import HoverPreview from "~/components/HoverPreview";
import type { Props as EditorProps, Editor as SharedEditor } from "~/editor"; import type { Props as EditorProps, Editor as SharedEditor } from "~/editor";
import useDictionary from "~/hooks/useDictionary"; import useDictionary from "~/hooks/useDictionary";
import useEmbeds from "~/hooks/useEmbeds"; import useEmbeds from "~/hooks/useEmbeds";
import useFocusedComment from "~/hooks/useFocusedComment";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts"; import useToasts from "~/hooks/useToasts";
import { NotFoundError } from "~/utils/errors"; import { NotFoundError } from "~/utils/errors";
@@ -61,7 +60,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
onDeleteCommentMark, onDeleteCommentMark,
} = props; } = props;
const { auth, comments, documents } = useStores(); const { auth, comments, documents } = useStores();
const focusedComment = useFocusedComment();
const { showToast } = useToasts(); const { showToast } = useToasts();
const dictionary = useDictionary(); const dictionary = useDictionary();
const embeds = useEmbeds(!shareId); const embeds = useEmbeds(!shareId);
@@ -343,7 +341,6 @@ function Editor(props: Props, ref: React.RefObject<SharedEditor> | null) {
onChange={handleChange} onChange={handleChange}
placeholder={props.placeholder || ""} placeholder={props.placeholder || ""}
defaultValue={props.defaultValue || ""} defaultValue={props.defaultValue || ""}
focusedCommentId={focusedComment?.id}
/> />
{props.bottomPadding && !props.readOnly && ( {props.bottomPadding && !props.readOnly && (
<ClickablePadding <ClickablePadding

18
app/components/Portal.tsx Normal file
View File

@@ -0,0 +1,18 @@
import * as React from "react";
import { Portal as ReactPortal } from "react-portal";
/**
* A React context that provides a dom node for portals to be rendered into.
*/
export const PortalContext = React.createContext<
HTMLElement | null | undefined
>(undefined);
/**
* A portal component that uses context to render into a different dom node
* or the root of body if no context is available.
*/
export function Portal(props: { children: React.ReactNode }) {
const node = React.useContext(PortalContext);
return <ReactPortal node={node}>{props.children}</ReactPortal>;
}

View File

@@ -3,7 +3,6 @@ import { findDomRefAtPos, findParentNode } from "prosemirror-utils";
import { EditorView } from "prosemirror-view"; import { EditorView } from "prosemirror-view";
import * as React from "react"; import * as React from "react";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import { Portal } from "react-portal";
import { VisuallyHidden } from "reakit/VisuallyHidden"; import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components"; import styled from "styled-components";
import insertFiles from "@shared/editor/commands/insertFiles"; import insertFiles from "@shared/editor/commands/insertFiles";
@@ -14,6 +13,7 @@ import { MenuItem } from "@shared/editor/types";
import { depths } from "@shared/styles"; import { depths } from "@shared/styles";
import { getEventFiles } from "@shared/utils/files"; import { getEventFiles } from "@shared/utils/files";
import { AttachmentValidation } from "@shared/validations"; import { AttachmentValidation } from "@shared/validations";
import { Portal } from "~/components/Portal";
import Scrollable from "~/components/Scrollable"; import Scrollable from "~/components/Scrollable";
import { Dictionary } from "~/hooks/useDictionary"; import { Dictionary } from "~/hooks/useDictionary";
import Input from "./Input"; import Input from "./Input";
@@ -406,7 +406,16 @@ class CommandMenu<T extends MenuItem> extends React.Component<Props<T>, State> {
const { top, bottom, right } = paragraph.node.getBoundingClientRect(); const { top, bottom, right } = paragraph.node.getBoundingClientRect();
const margin = 24; const margin = 24;
let leftPos = left + window.scrollX; const offsetParent = ref?.offsetParent
? ref.offsetParent.getBoundingClientRect()
: ({
width: 0,
height: 0,
top: 0,
left: 0,
} as DOMRect);
let leftPos = left - offsetParent.left;
if (props.rtl && ref) { if (props.rtl && ref) {
leftPos = right - ref.scrollWidth; leftPos = right - ref.scrollWidth;
} }
@@ -414,14 +423,14 @@ class CommandMenu<T extends MenuItem> extends React.Component<Props<T>, State> {
if (startPos.top - offsetHeight > margin) { if (startPos.top - offsetHeight > margin) {
return { return {
left: leftPos, left: leftPos,
top: undefined, top: top - offsetParent.top - offsetHeight,
bottom: window.innerHeight - top - window.scrollY, bottom: undefined,
isAbove: false, isAbove: false,
}; };
} else { } else {
return { return {
left: leftPos, left: leftPos,
top: bottom + window.scrollY, top: bottom - offsetParent.top,
bottom: undefined, bottom: undefined,
isAbove: true, isAbove: true,
}; };

View File

@@ -1,9 +1,9 @@
import { NodeSelection } from "prosemirror-state"; import { NodeSelection } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables"; import { CellSelection } from "prosemirror-tables";
import * as React from "react"; import * as React from "react";
import { Portal } from "react-portal";
import styled from "styled-components"; import styled from "styled-components";
import { depths } from "@shared/styles"; import { depths } from "@shared/styles";
import { Portal } from "~/components/Portal";
import useComponentSize from "~/hooks/useComponentSize"; import useComponentSize from "~/hooks/useComponentSize";
import useEventListener from "~/hooks/useEventListener"; import useEventListener from "~/hooks/useEventListener";
import useMediaQuery from "~/hooks/useMediaQuery"; import useMediaQuery from "~/hooks/useMediaQuery";
@@ -80,6 +80,15 @@ function usePosition({
right: Math.max(fromPos.right, toPos.right), right: Math.max(fromPos.right, toPos.right),
}; };
const offsetParent = menuRef.current.offsetParent
? menuRef.current.offsetParent.getBoundingClientRect()
: ({
width: 0,
height: 0,
top: 0,
left: 0,
} as DOMRect);
// tables are an oddity, and need their own positioning logic // tables are an oddity, and need their own positioning logic
const isColSelection = const isColSelection =
selection instanceof CellSelection && selection instanceof CellSelection &&
@@ -116,8 +125,8 @@ function usePosition({
const { left, top, width } = imageElement.getBoundingClientRect(); const { left, top, width } = imageElement.getBoundingClientRect();
return { return {
left: Math.round(left + width / 2 + window.scrollX - menuWidth / 2), left: Math.round(left + width / 2 - menuWidth / 2 - offsetParent.left),
top: Math.round(top + window.scrollY - menuHeight), top: Math.round(top - menuHeight - offsetParent.top),
offset: 0, offset: 0,
visible: true, visible: true,
}; };
@@ -145,8 +154,8 @@ function usePosition({
// of the selection still // of the selection still
const offset = left - (centerOfSelection - menuWidth / 2); const offset = left - (centerOfSelection - menuWidth / 2);
return { return {
left: Math.round(left + window.scrollX), left: Math.round(left - offsetParent.left),
top: Math.round(top + window.scrollY), top: Math.round(top - offsetParent.top),
offset: Math.round(offset), offset: Math.round(offset),
visible: true, visible: true,
}; };

View File

@@ -2,7 +2,9 @@ import styled from "styled-components";
type Props = { active?: boolean; disabled?: boolean }; type Props = { active?: boolean; disabled?: boolean };
export default styled.button<Props>` export default styled.button.attrs((props) => ({
type: props.type || "button",
}))<Props>`
display: inline-block; display: inline-block;
flex: 0; flex: 0;
width: 24px; width: 24px;

View File

@@ -32,6 +32,7 @@ import { UserPreferences } from "@shared/types";
import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper"; import ProsemirrorHelper from "@shared/utils/ProsemirrorHelper";
import EventEmitter from "@shared/utils/events"; import EventEmitter from "@shared/utils/events";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import { PortalContext } from "~/components/Portal";
import { Dictionary } from "~/hooks/useDictionary"; import { Dictionary } from "~/hooks/useDictionary";
import Logger from "~/utils/Logger"; import Logger from "~/utils/Logger";
import BlockMenu from "./components/BlockMenu"; import BlockMenu from "./components/BlockMenu";
@@ -178,7 +179,8 @@ export class Editor extends React.PureComponent<
isBlurred: boolean; isBlurred: boolean;
extensions: ExtensionManager; extensions: ExtensionManager;
element = React.createRef<HTMLDivElement>(); elementRef = React.createRef<HTMLDivElement>();
wrapperRef = React.createRef<HTMLDivElement>();
view: EditorView; view: EditorView;
schema: Schema; schema: Schema;
serializer: MarkdownSerializer; serializer: MarkdownSerializer;
@@ -435,7 +437,7 @@ export class Editor extends React.PureComponent<
} }
private createView() { private createView() {
if (!this.element.current) { if (!this.elementRef.current) {
throw new Error("createView called before ref available"); throw new Error("createView called before ref available");
} }
@@ -448,7 +450,7 @@ export class Editor extends React.PureComponent<
}; };
const self = this; // eslint-disable-line const self = this; // eslint-disable-line
const view = new EditorView(this.element.current, { const view = new EditorView(this.elementRef.current, {
handleDOMEvents: { handleDOMEvents: {
blur: this.handleEditorBlur, blur: this.handleEditorBlur,
focus: this.handleEditorFocus, focus: this.handleEditorFocus,
@@ -521,13 +523,13 @@ export class Editor extends React.PureComponent<
}; };
private calculateDir = () => { private calculateDir = () => {
if (!this.element.current) { if (!this.elementRef.current) {
return; return;
} }
const isRTL = const isRTL =
this.props.dir === "rtl" || this.props.dir === "rtl" ||
getComputedStyle(this.element.current).direction === "rtl"; getComputedStyle(this.elementRef.current).direction === "rtl";
if (this.state.isRTL !== isRTL) { if (this.state.isRTL !== isRTL) {
this.setState({ isRTL }); this.setState({ isRTL });
@@ -718,75 +720,78 @@ export class Editor extends React.PureComponent<
const { isRTL } = this.state; const { isRTL } = this.state;
return ( return (
<EditorContext.Provider value={this}> <PortalContext.Provider value={this.wrapperRef.current}>
<Flex <EditorContext.Provider value={this}>
onKeyDown={onKeyDown} <Flex
style={style} ref={this.wrapperRef}
className={className} onKeyDown={onKeyDown}
align="flex-start" style={style}
justify="center" className={className}
column align="flex-start"
> justify="center"
<EditorContainer column
dir={dir} >
rtl={isRTL} <EditorContainer
grow={grow} dir={dir}
readOnly={readOnly} rtl={isRTL}
readOnlyWriteCheckboxes={readOnlyWriteCheckboxes} grow={grow}
focusedCommentId={this.props.focusedCommentId} readOnly={readOnly}
ref={this.element} readOnlyWriteCheckboxes={readOnlyWriteCheckboxes}
/> focusedCommentId={this.props.focusedCommentId}
{!readOnly && this.view && ( ref={this.elementRef}
<> />
<SelectionToolbar {!readOnly && this.view && (
view={this.view} <>
dictionary={dictionary} <SelectionToolbar
commands={this.commands} view={this.view}
rtl={isRTL} dictionary={dictionary}
isTemplate={this.props.template === true} commands={this.commands}
onOpen={this.handleOpenSelectionMenu} rtl={isRTL}
onClose={this.handleCloseSelectionMenu} isTemplate={this.props.template === true}
onSearchLink={this.props.onSearchLink} onOpen={this.handleOpenSelectionMenu}
onClickLink={this.props.onClickLink} onClose={this.handleCloseSelectionMenu}
onCreateLink={this.props.onCreateLink} onSearchLink={this.props.onSearchLink}
onShowToast={this.props.onShowToast} onClickLink={this.props.onClickLink}
/> onCreateLink={this.props.onCreateLink}
<LinkToolbar onShowToast={this.props.onShowToast}
isActive={this.state.linkMenuOpen} />
onCreateLink={this.props.onCreateLink} <LinkToolbar
onSearchLink={this.props.onSearchLink} isActive={this.state.linkMenuOpen}
onClickLink={this.props.onClickLink} onCreateLink={this.props.onCreateLink}
onClose={this.handleCloseLinkMenu} onSearchLink={this.props.onSearchLink}
/> onClickLink={this.props.onClickLink}
<EmojiMenu onClose={this.handleCloseLinkMenu}
view={this.view} />
commands={this.commands} <EmojiMenu
dictionary={dictionary} view={this.view}
rtl={isRTL} commands={this.commands}
onShowToast={this.props.onShowToast} dictionary={dictionary}
isActive={this.state.emojiMenuOpen} rtl={isRTL}
search={this.state.blockMenuSearch} onShowToast={this.props.onShowToast}
onClose={this.handleCloseEmojiMenu} isActive={this.state.emojiMenuOpen}
/> search={this.state.blockMenuSearch}
<BlockMenu onClose={this.handleCloseEmojiMenu}
view={this.view} />
commands={this.commands} <BlockMenu
dictionary={dictionary} view={this.view}
rtl={isRTL} commands={this.commands}
isActive={this.state.blockMenuOpen} dictionary={dictionary}
search={this.state.blockMenuSearch} rtl={isRTL}
onClose={this.handleCloseBlockMenu} isActive={this.state.blockMenuOpen}
uploadFile={this.props.uploadFile} search={this.state.blockMenuSearch}
onLinkToolbarOpen={this.handleOpenLinkMenu} onClose={this.handleCloseBlockMenu}
onFileUploadStart={this.props.onFileUploadStart} uploadFile={this.props.uploadFile}
onFileUploadStop={this.props.onFileUploadStop} onLinkToolbarOpen={this.handleOpenLinkMenu}
onShowToast={this.props.onShowToast} onFileUploadStart={this.props.onFileUploadStart}
embeds={this.props.embeds} onFileUploadStop={this.props.onFileUploadStop}
/> onShowToast={this.props.onShowToast}
</> embeds={this.props.embeds}
)} />
</Flex> </>
</EditorContext.Provider> )}
</Flex>
</EditorContext.Provider>
</PortalContext.Provider>
); );
} }
} }

View File

@@ -9,6 +9,7 @@ import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem"; import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton"; import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import Separator from "~/components/ContextMenu/Separator"; import Separator from "~/components/ContextMenu/Separator";
import EventBoundary from "~/components/EventBoundary";
import usePolicy from "~/hooks/usePolicy"; import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts"; import useToasts from "~/hooks/useToasts";
@@ -52,11 +53,14 @@ function CommentMenu({ comment, onEdit, onDelete, className }: Props) {
return ( return (
<> <>
<OverflowMenuButton <EventBoundary>
aria-label={t("Show menu")} <OverflowMenuButton
className={className} aria-label={t("Show menu")}
{...menu} className={className}
/> {...menu}
/>
</EventBoundary>
<ContextMenu {...menu} aria-label={t("Comment options")}> <ContextMenu {...menu} aria-label={t("Comment options")}>
{can.update && ( {can.update && (
<MenuItem {...menu} onClick={onEdit}> <MenuItem {...menu} onClick={onEdit}>

View File

@@ -1,10 +1,5 @@
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { import { EditIcon, NewDocumentIcon, RestoreIcon } from "outline-icons";
EditIcon,
PrintIcon,
NewDocumentIcon,
RestoreIcon,
} from "outline-icons";
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@@ -40,6 +35,8 @@ import {
openDocumentInsights, openDocumentInsights,
publishDocument, publishDocument,
unpublishDocument, unpublishDocument,
printDocument,
openDocumentComments,
} from "~/actions/definitions/documents"; } from "~/actions/definitions/documents";
import useActionContext from "~/hooks/useActionContext"; import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam"; import useCurrentTeam from "~/hooks/useCurrentTeam";
@@ -125,11 +122,6 @@ function DocumentMenu({
[showToast, t, document] [showToast, t, document]
); );
const handlePrint = React.useCallback(() => {
menu.hide();
window.print();
}, [menu]);
const collection = collections.get(document.collectionId); const collection = collections.get(document.collectionId);
const can = usePolicy(document); const can = usePolicy(document);
const restoreItems = React.useMemo( const restoreItems = React.useMemo(
@@ -291,16 +283,11 @@ function DocumentMenu({
{ {
type: "separator", type: "separator",
}, },
actionToMenuItem(downloadDocument, context), actionToMenuItem(openDocumentComments, context),
actionToMenuItem(openDocumentHistory, context), actionToMenuItem(openDocumentHistory, context),
actionToMenuItem(openDocumentInsights, context), actionToMenuItem(openDocumentInsights, context),
{ actionToMenuItem(downloadDocument, context),
type: "button", actionToMenuItem(printDocument, context),
title: t("Print"),
onClick: handlePrint,
visible: !!showDisplayOptions,
icon: <PrintIcon />,
},
{ {
type: "separator", type: "separator",
}, },

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import queryString from "query-string";
import Collection from "~/models/Collection"; import Collection from "~/models/Collection";
import Comment from "~/models/Comment"; import Comment from "~/models/Comment";
import Document from "~/models/Document"; import Document from "~/models/Document";
import env from "~/env";
export function homePath(): string { export function homePath(): string {
return "/home"; return "/home";
@@ -138,7 +137,7 @@ export function notFoundUrl(): string {
} }
export function urlify(path: string): string { export function urlify(path: string): string {
return `${env.URL}${path}`; return `${window.location.host}${path}`;
} }
export const matchDocumentSlug = export const matchDocumentSlug =

View File

@@ -52,6 +52,7 @@
"Delete {{ documentName }}": "Delete {{ documentName }}", "Delete {{ documentName }}": "Delete {{ documentName }}",
"Permanently delete": "Permanently delete", "Permanently delete": "Permanently delete",
"Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}", "Permanently delete {{ documentName }}": "Permanently delete {{ documentName }}",
"Comments": "Comments",
"History": "History", "History": "History",
"Insights": "Insights", "Insights": "Insights",
"Home": "Home", "Home": "Home",
@@ -436,10 +437,9 @@
"Error creating comment": "Error creating comment", "Error creating comment": "Error creating comment",
"Add a comment": "Add a comment", "Add a comment": "Add a comment",
"Add a reply": "Add a reply", "Add a reply": "Add a reply",
"Post": "Post",
"Reply": "Reply", "Reply": "Reply",
"Post": "Post",
"Cancel": "Cancel", "Cancel": "Cancel",
"Comments": "Comments",
"No comments yet": "No comments yet", "No comments yet": "No comments yet",
"Error updating comment": "Error updating comment", "Error updating comment": "Error updating comment",
"Document updated by {{userName}}": "Document updated by {{userName}}", "Document updated by {{userName}}": "Document updated by {{userName}}",
@@ -476,7 +476,7 @@
"{{ count }} words selected_plural": "{{ count }} words selected", "{{ count }} words selected_plural": "{{ count }} words selected",
"{{ count }} characters selected": "{{ count }} character selected", "{{ count }} characters selected": "{{ count }} character selected",
"{{ count }} characters selected_plural": "{{ count }} characters selected", "{{ count }} characters selected_plural": "{{ count }} characters selected",
"Collaborators": "Collaborators", "Contributors": "Contributors",
"Created": "Created", "Created": "Created",
"Last updated": "Last updated", "Last updated": "Last updated",
"Creator": "Creator", "Creator": "Creator",

View File

@@ -50,7 +50,7 @@ const spacing = {
sidebarWidth: 260, sidebarWidth: 260,
sidebarCollapsedWidth: 16, sidebarCollapsedWidth: 16,
sidebarMinWidth: 200, sidebarMinWidth: 200,
sidebarMaxWidth: 400, sidebarMaxWidth: 600,
}; };
const buildBaseTheme = (input: Partial<Colors>) => { const buildBaseTheme = (input: Partial<Colors>) => {