Compare commits

...

1 Commits

Author SHA1 Message Date
01zulfi
26b76f12dc editor: add comment functionality
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-11-20 16:18:36 +05:00
17 changed files with 769 additions and 90 deletions

View File

@@ -9,71 +9,72 @@ The following keyboard shortcuts will help you navigate Notesnook faster.
### General
| Description | Web | Windows/Linux | Mac |
| -------------------------------------------------- | ------ | ------------- | --- |
| Search in notes list view if editor is not focused | Ctrl F | Ctrl F | ⌘ F |
| Settings | Ctrl , | Ctrl , | ⌘ , |
| Keyboard shortcuts | Ctrl / | Ctrl / | ⌘ / |
| New note | - | Ctrl N | ⌘ N |
| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |
| Search in notes list view if editor is not focused | Ctrl F | Ctrl F | ⌘ F |
| Settings | Ctrl , | Ctrl , | ⌘ , |
| Keyboard shortcuts | Ctrl / | Ctrl / | ⌘ / |
| New note | - | Ctrl N | ⌘ N |
### Navigation
| Description | Web | Windows/Linux | Mac |
| ---------------- | ------------------------- | ------------- | ------- |
| Next tab | Ctrl Alt → / Ctrl Alt ⇧ → | Ctrl tab | ⌘ tab |
| Previous tab | Ctrl Alt ← / Ctrl Alt ⇧ ← | Ctrl ⇧ tab | ⌘ ⇧ tab |
| Command palette | Ctrl ⇧ P | Ctrl ⇧ P | ⌘ ⇧ P |
| Quick open | Ctrl P | Ctrl P | ⌘ P |
| New tab | - | Ctrl T | ⌘ T |
| Close active tab | - | Ctrl W | ⌘ W |
| Close all tabs | - | Ctrl ⇧ W | ⌘ ⇧ W |
| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |
| Next tab | Ctrl Alt → / Ctrl Alt ⇧ → | Ctrl tab | ⌘ tab |
| Previous tab | Ctrl Alt ← / Ctrl Alt ⇧ ← | Ctrl ⇧ tab | ⌘ ⇧ tab |
| Command palette | Ctrl ⇧ P | Ctrl ⇧ P | ⌘ ⇧ P |
| Quick open | Ctrl P | Ctrl P | ⌘ P |
| New tab | - | Ctrl T | ⌘ T |
| Close active tab | - | Ctrl W | ⌘ W |
| Close all tabs | - | Ctrl ⇧ W | ⌘ ⇧ W |
### Editor
| Description | Web | Windows/Linux | Mac |
| ---------------------------------- | ----------------- | ----------------- | ----------- |
| Add attachment | Ctrl ⇧ A | Ctrl ⇧ A | ⌘ ⇧ A |
| Insert blockquote | Ctrl ⇧ B | Ctrl ⇧ B | ⌘ ⇧ B |
| Toggle bold | Ctrl B | Ctrl B | ⌘ B |
| Toggle bullet list | Ctrl ⇧ 8 | Ctrl ⇧ 8 | ⌘ ⇧ 8 |
| Toggle check list | Ctrl ⇧ 9 | Ctrl ⇧ 9 | ⌘ ⇧ 9 |
| Split list item | ↵ | ↵ | ↵ |
| Lift list item | ⇧ Tab | ⇧ Tab | ⇧ Tab |
| Sink list item | Ctrl ⇧ Down | Ctrl ⇧ Down | ⌘ ⇧ Down |
| Toggle code | Ctrl E | Ctrl E | ⌘ E |
| Toggle code block | Ctrl ⇧ C | Ctrl ⇧ C | ⌘ ⇧ C |
| Insert date | Alt D | Alt D | ⌥ D |
| Insert time | Alt T | Alt T | ⌥ T |
| Insert date and time | Ctrl Alt D | Ctrl Alt D | ⌘ ⌥ D |
| Insert date and time with timezone | Ctrl Alt Z | Ctrl Alt Z | ⌘ ⌥ Z |
| Increase font size | Ctrl [ | Ctrl [ | ⌘ [ |
| Decrease font size | Ctrl ] | Ctrl ] | ⌘ ] |
| Insert paragraph | Ctrl ⇧ 0 | Ctrl ⇧ 0 | ⌘ 0 |
| Insert heading 1 | Ctrl Alt 1 | Ctrl Alt 1 | ⌘ ⌥ 1 |
| Insert heading 2 | Ctrl Alt 2 | Ctrl Alt 2 | ⌘ ⌥ 2 |
| Insert heading 3 | Ctrl Alt 3 | Ctrl Alt 3 | ⌘ ⌥ 3 |
| Insert heading 4 | Ctrl Alt 4 | Ctrl Alt 4 | ⌘ ⌥ 4 |
| Insert heading 5 | Ctrl Alt 5 | Ctrl Alt 5 | ⌘ ⌥ 5 |
| Insert heading 6 | Ctrl Alt 6 | Ctrl Alt 6 | ⌘ ⌥ 6 |
| Undo | Ctrl Z | Ctrl Z | ⌘ Z |
| Redo | Ctrl ⇧ Z / Ctrl Y | Ctrl ⇧ Z / Ctrl Y | ⌘ ⇧ Z / ⌘ Y |
| Add image | Ctrl ⇧ I | Ctrl ⇧ I | ⌘ ⇧ I |
| Toggle italic | Ctrl I | Ctrl I | ⌘ I |
| Remove formatting in selection | Ctrl \ | Ctrl \ | ⌘ \ |
| Insert internal link | Ctrl ⇧ K | Ctrl ⇧ K | ⌘ ⇧ K |
| Insert link | Ctrl K | Ctrl K | ⌘ K |
| Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M |
| Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 |
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
| Open search | Ctrl F | Ctrl F | ⌘ F |
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
| Toggle task list | Ctrl ⇧ T | Ctrl ⇧ T | ⌘ ⇧ T |
| Text align center | Ctrl ⇧ E | Ctrl ⇧ E | ⌘ ⇧ E |
| Text align justify | Ctrl ⇧ J | Ctrl ⇧ J | ⌘ ⇧ J |
| Text align left | Ctrl ⇧ L | Ctrl ⇧ L | ⌘ ⇧ L |
| Text align right | Ctrl ⇧ R | Ctrl ⇧ R | ⌘ ⇧ R |
| Underline | Ctrl U | Ctrl U | ⌘ U |
| Toggle highlight | Ctrl Alt H | Ctrl Alt H | ⌘ ⌥ H |
| Description | Web | Windows/Linux | Mac |
| --- | --- | --- | --- |
| Add attachment | Ctrl ⇧ A | Ctrl ⇧ A | ⌘ ⇧ A |
| Insert blockquote | Ctrl ⇧ B | Ctrl ⇧ B | ⌘ ⇧ B |
| Toggle bold | Ctrl B | Ctrl B | ⌘ B |
| Toggle bullet list | Ctrl ⇧ 8 | Ctrl ⇧ 8 | ⌘ ⇧ 8 |
| Toggle check list | Ctrl ⇧ 9 | Ctrl ⇧ 9 | ⌘ ⇧ 9 |
| Split list item | ↵ | ↵ | ↵ |
| Lift list item | ⇧ Tab | ⇧ Tab | ⇧ Tab |
| Sink list item | Ctrl ⇧ Down | Ctrl ⇧ Down | ⌘ ⇧ Down |
| Toggle code | Ctrl E | Ctrl E | ⌘ E |
| Toggle code block | Ctrl ⇧ C | Ctrl ⇧ C | ⌘ ⇧ C |
| Insert date | Alt D | Alt D | ⌥ D |
| Insert time | Alt T | Alt T | ⌥ T |
| Insert date and time | Ctrl Alt D | Ctrl Alt D | ⌘ ⌥ D |
| Insert date and time with timezone | Ctrl Alt Z | Ctrl Alt Z | ⌘ ⌥ Z |
| Increase font size | Ctrl [ | Ctrl [ | ⌘ [ |
| Decrease font size | Ctrl ] | Ctrl ] | ⌘ ] |
| Insert paragraph | Ctrl Alt 0 | Ctrl Alt 0 | ⌘ 0 |
| Insert heading 1 | Ctrl Alt 1 | Ctrl Alt 1 | ⌘ ⌥ 1 |
| Insert heading 2 | Ctrl Alt 2 | Ctrl Alt 2 | ⌘ ⌥ 2 |
| Insert heading 3 | Ctrl Alt 3 | Ctrl Alt 3 | ⌘ ⌥ 3 |
| Insert heading 4 | Ctrl Alt 4 | Ctrl Alt 4 | ⌘ ⌥ 4 |
| Insert heading 5 | Ctrl Alt 5 | Ctrl Alt 5 | ⌘ ⌥ 5 |
| Insert heading 6 | Ctrl Alt 6 | Ctrl Alt 6 | ⌘ ⌥ 6 |
| Undo | Ctrl Z | Ctrl Z | ⌘ Z |
| Redo | Ctrl ⇧ Z / Ctrl Y | Ctrl ⇧ Z / Ctrl Y | ⌘ ⇧ Z / ⌘ Y |
| Add image | Ctrl ⇧ I | Ctrl ⇧ I | ⌘ ⇧ I |
| Toggle italic | Ctrl I | Ctrl I | ⌘ I |
| Remove formatting in selection | Ctrl \ | Ctrl \ | ⌘ \ |
| Insert internal link | Ctrl ⇧ K | Ctrl ⇧ K | ⌘ ⇧ K |
| Insert link | Ctrl K | Ctrl K | ⌘ K |
| Insert math block | Ctrl ⇧ M | Ctrl ⇧ M | ⌘ ⇧ M |
| Toggle ordered list | Ctrl ⇧ 7 | Ctrl ⇧ 7 | ⌘ ⇧ 7 |
| Toggle outline list | Ctrl ⇧ O | Ctrl ⇧ O | ⌘ ⇧ O |
| Toggle outline list expand | Ctrl Space | Ctrl Space | ⌘ Space |
| Open search | Ctrl F | Ctrl F | ⌘ F |
| Toggle strike | Ctrl ⇧ S | Ctrl ⇧ S | ⌘ ⇧ S |
| Toggle subscript | Ctrl , | Ctrl , | ⌘ , |
| Toggle superscript | Ctrl . | Ctrl . | ⌘ . |
| Toggle task list | Ctrl ⇧ T | Ctrl ⇧ T | ⌘ ⇧ T |
| Text align center | Ctrl ⇧ E | Ctrl ⇧ E | ⌘ ⇧ E |
| Text align justify | Ctrl ⇧ J | Ctrl ⇧ J | ⌘ ⇧ J |
| Text align left | Ctrl ⇧ L | Ctrl ⇧ L | ⌘ ⇧ L |
| Text align right | Ctrl ⇧ R | Ctrl ⇧ R | ⌘ ⇧ R |
| Underline | Ctrl U | Ctrl U | ⌘ U |
| Toggle highlight | Ctrl Alt H | Ctrl Alt H | ⌘ ⌥ H |
| Insert comment | Ctrl Alt M | Ctrl Alt M | ⌘ ⌥ M |

View File

@@ -394,6 +394,12 @@ export const tiptapKeys = {
description: "Toggle highlight",
category: "Editor",
type: "tiptap"
},
insertComment: {
keys: "Mod-Alt-m",
description: "Insert comment",
category: "Editor",
type: "tiptap"
}
} satisfies Record<string, TipTapKey>;

View File

@@ -0,0 +1,120 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Mark, mergeAttributes } from "@tiptap/core";
interface CommentOptions {
HTMLAttributes: Record<string, any>;
}
export type CommentAttributes = {
id: string;
text: string;
timestamp: number;
};
declare module "@tiptap/core" {
interface Commands<ReturnType> {
comment: {
/**
* Set a comment mark
*/
setComment: (attributes: CommentAttributes) => ReturnType;
/**
* Unset a comment mark
*/
unsetComment: () => ReturnType;
};
}
}
export const Comment = Mark.create<CommentOptions>({
name: "comment",
priority: 1000,
keepOnSplit: false,
addOptions() {
return {
HTMLAttributes: {
class: "comment-highlight"
}
};
},
addAttributes() {
return {
id: {
default: null,
parseHTML: (element) => element.getAttribute("data-comment-id"),
renderHTML: (attributes) => {
if (!attributes.id) return {};
return { "data-comment-id": attributes.id };
}
},
text: {
default: null,
parseHTML: (element) => element.getAttribute("data-comment-text"),
renderHTML: (attributes) => {
if (!attributes.text) return {};
return { "data-comment-text": attributes.text };
}
},
timestamp: {
default: null,
parseHTML: (element) => {
const ts = element.getAttribute("data-comment-timestamp");
return ts ? parseInt(ts, 10) : null;
},
renderHTML: (attributes) => {
if (!attributes.timestamp) return {};
return { "data-comment-timestamp": attributes.timestamp.toString() };
}
}
};
},
parseHTML() {
return [{ tag: "span[data-comment-id]" }];
},
renderHTML({ HTMLAttributes }) {
return [
"span",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0
];
},
addCommands() {
return {
setComment:
(attributes) =>
({ commands }) => {
return commands.setMark(this.name, attributes);
},
unsetComment:
() =>
({ commands }) => {
return commands.unsetMark(this.name);
}
};
}
});

View File

@@ -0,0 +1,20 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export * from "./comment.js";

View File

@@ -23,6 +23,7 @@ import { CodeBlock } from "../code-block/index.js";
import { showLinkPopup } from "../../toolbar/popups/link-popup.js";
import { isListActive } from "../../utils/list.js";
import { tiptapKeys } from "@notesnook/common";
import { showCommentPopup } from "../../toolbar/popups/comment-popup.js";
export const KeyMap = Extension.create({
name: "key-map",
@@ -71,6 +72,10 @@ export const KeyMap = Extension.create({
[tiptapKeys.insertLink.keys]: ({ editor }) => {
showLinkPopup(editor);
return true;
},
[tiptapKeys.insertComment.keys]: ({ editor }) => {
showCommentPopup(editor);
return true;
}
};
}

View File

@@ -26,6 +26,7 @@ import CharacterCount from "@tiptap/extension-character-count";
import Color from "@tiptap/extension-color";
import HorizontalRule from "@tiptap/extension-horizontal-rule";
import { Link, LinkAttributes } from "./extensions/link/index.js";
import { Comment } from "./extensions/comment/index.js";
import Placeholder from "@tiptap/extension-placeholder";
import Subscript from "@tiptap/extension-subscript";
import Superscript from "@tiptap/extension-superscript";
@@ -282,6 +283,7 @@ const useTiptap = (
autolink: false,
linkOnPaste: true
}),
Comment.configure({}),
Table.configure({
resizable: true,
allowTableNodeSelection: true,

View File

@@ -0,0 +1,60 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ToolbarGroup } from "../../components/toolbar-group.js";
import { HoverPopupProps } from "./index.js";
import { useHoverPopupContext } from "./context.js";
import { ToolbarGroupDefinition } from "../../types.js";
const COMMENT_TOOLS: ToolbarGroupDefinition = ["viewComment"];
function CommentHoverPopup(props: HoverPopupProps) {
const { editor } = props;
const { selectedNode } = useHoverPopupContext();
const { node } = selectedNode || {};
if (
!node?.isText ||
node.marks.length <= 0 ||
!node.marks.some((mark) => mark.type.name === "comment")
) {
return null;
}
return (
<ToolbarGroup
force
tools={COMMENT_TOOLS}
groupId={"commentHoverTools"}
editor={editor}
sx={{
bg: "background",
boxShadow: "menu",
borderRadius: "default",
p: 1
}}
/>
);
}
export const CommentHoverPopupHandler = {
isActive: (e: HTMLElement) => !!e.closest("[data-comment-id]"),
popup: CommentHoverPopup,
openOnClick: true
};

View File

@@ -22,13 +22,23 @@ import { showPopup } from "../../../components/popup-presenter/index.js";
import { Editor } from "../../../types.js";
import { FloatingMenuProps } from "../types.js";
import { LinkHoverPopupHandler } from "./link.js";
import { CommentHoverPopupHandler } from "./comment.js";
import { HoverPopupContextProvider } from "./context.js";
export type HoverPopupProps = {
editor: Editor;
};
const handlers = [LinkHoverPopupHandler];
type HoverPopupHandler = {
isActive: (element: HTMLElement) => boolean;
popup: (props: HoverPopupProps) => React.ReactNode;
openOnClick?: boolean;
};
const handlers: HoverPopupHandler[] = [
LinkHoverPopupHandler,
CommentHoverPopupHandler
];
const HOVER_TIMEOUT = 1000;
@@ -59,29 +69,11 @@ export function HoverPopupHandler(props: FloatingMenuProps) {
clearTimeout(hoverTimeoutId.current);
}
function onMouseOver(e: MouseEvent) {
if (
!e.target ||
!(e.target instanceof HTMLElement) ||
e.target.classList.contains("ProseMirror")
)
return;
const element = e.target;
if (activePopup.current) {
const isOutsideEditor = !element.closest(".ProseMirror");
const isInsidePopup = element.closest(".popup-presenter-portal");
const isActiveElement = activePopup.current.element === element;
if (isInsidePopup) return;
if (isOutsideEditor || !isActiveElement) {
activePopup.current.hide();
activePopup.current = undefined;
return;
}
}
function showPopupForElement(
element: HTMLElement,
timeout: number = HOVER_TIMEOUT
) {
clearTimeout(hoverTimeoutId.current);
hoverTimeoutId.current = setTimeout(
@@ -125,20 +117,79 @@ export function HoverPopupHandler(props: FloatingMenuProps) {
align: "start",
location: "top",
isTargetAbsolute: true,
yOffset: -10,
yOffset: 0,
xOffset: -30
}
});
activePopup.current = { element, hide: hidePopup };
},
HOVER_TIMEOUT,
timeout,
{}
);
}
function onClick(e: MouseEvent) {
if (
!e.target ||
!(e.target instanceof HTMLElement) ||
e.target.classList.contains("ProseMirror")
)
return;
const element = e.target;
const handler = handlers.find((h) => h.isActive(element));
if (!handler?.openOnClick) return;
if (activePopup.current?.element === element) return;
if (activePopup.current) {
activePopup.current.hide();
activePopup.current = undefined;
}
showPopupForElement(element, 0);
}
function onMouseOver(e: MouseEvent) {
if (
!e.target ||
!(e.target instanceof HTMLElement) ||
e.target.classList.contains("ProseMirror")
)
return;
const element = e.target;
if (activePopup.current) {
const isOutsideEditor = !element.closest(".ProseMirror");
const isInsidePopup = element.closest(".popup-presenter-portal");
const isActiveElement = activePopup.current.element === element;
if (isInsidePopup) return;
if (isOutsideEditor) {
activePopup.current.hide();
activePopup.current = undefined;
return;
}
if (!isActiveElement) {
activePopup.current.hide();
activePopup.current = undefined;
} else {
return;
}
}
showPopupForElement(element);
}
window.addEventListener("mouseover", onMouseOver);
window.addEventListener("click", onClick);
window.addEventListener("contextmenu", onContextMenu);
return () => {
window.removeEventListener("mouseover", onMouseOver);
window.removeEventListener("click", onClick);
window.removeEventListener("contextmenu", onContextMenu);
};
},

View File

@@ -122,7 +122,9 @@ import {
mdiCheckboxMultipleMarked,
mdiMessageOutline,
mdiVectorLink,
mdiPinOutline
mdiPinOutline,
mdiCommentOutline,
mdiCommentEditOutline
} from "@mdi/js";
export const Icons = {
@@ -254,6 +256,8 @@ export const Icons = {
selectAllChecked: mdiCheckboxMultipleMarked,
checkList: mdiCheckboxMarkedOutline,
pin: mdiPinOutline,
comment: mdiCommentOutline,
commentEdit: mdiCommentEditOutline,
none: ""
};

View File

@@ -0,0 +1,125 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { Textarea } from "@theme-ui/components";
import { Flex } from "@theme-ui/components";
import { useState } from "react";
import { Popup } from "../components/popup.js";
import { strings } from "@notesnook/intl";
import { showPopup } from "../../components/popup-presenter/index.js";
import { ResponsivePresenter } from "../../components/responsive/index.js";
import { nanoid } from "nanoid";
import { CommentAttributes } from "../../extensions/comment/comment.js";
import { Editor } from "@tiptap/core";
export type CommentPopupProps = {
onDone: (text: string) => void;
onClose: () => void;
};
export function CommentPopup(props: CommentPopupProps) {
const { onDone, onClose } = props;
const [commentText, setCommentText] = useState("");
return (
<Popup
title={strings.addComment()}
onClose={onClose}
action={{
title: strings.add(),
disabled: !commentText.trim(),
onClick: () => onDone(commentText)
}}
>
<Flex
sx={{ p: 1, flexDirection: "column", width: ["auto", 300] }}
onKeyUp={(e) => {
if (e.key === "Enter" && e.ctrlKey && commentText.trim()) {
onDone(commentText);
}
}}
>
<Textarea
autoFocus
placeholder={strings.writeComment()}
value={commentText}
rows={2}
onChange={(e) => setCommentText(e.target.value)}
sx={{
fontFamily: "body",
fontSize: "body",
resize: "vertical"
}}
/>
</Flex>
</Popup>
);
}
export function showCommentPopup(editor: Editor) {
if (!editor || editor.state.selection.empty || editor.isActive("comment")) {
return;
}
showPopup({
popup: (hide) => (
<ResponsivePresenter
mobile="sheet"
desktop="popup"
position={{
target:
(document.querySelector("#tool-comment") as HTMLElement) ||
undefined,
isTargetAbsolute: true,
location: "below",
align: "center",
yOffset: 5
}}
title={strings.addComment()}
isOpen={true}
items={[]}
onClose={() => {
hide();
editor.commands.focus();
}}
focusOnRender={true}
>
<CommentPopup
onClose={() => {
hide();
editor.commands.focus();
}}
onDone={(text: string) => {
const commentId = `comment-${nanoid(8)}`;
const attributes: CommentAttributes = {
id: commentId,
text,
timestamp: Date.now()
};
editor.commands.setComment(attributes);
hide();
editor.commands.focus();
}}
/>
</ResponsivePresenter>
),
blocking: true,
focusOnRender: true
});
}

View File

@@ -75,6 +75,15 @@ const tools: Record<ToolId, ToolDefinition> = {
title: strings.linkSettings(),
conditional: true
},
addComment: {
icon: "comment",
title: strings.addComment()
},
viewComment: {
icon: "comment",
title: strings.viewComment(),
conditional: true
},
code: {
icon: "code",
title: strings.code()
@@ -413,7 +422,7 @@ const defaultPresets: Record<"default" | "minimal", ToolbarDefinition> = {
["fontSize"],
["headings", "fontFamily"],
["checkList", "numberedList", "bulletList"],
["addLink", "addInternalLink"],
["addLink", "addInternalLink", "addComment"],
["alignment", "textDirection"],
["clearformatting"]
],

View File

@@ -0,0 +1,208 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ToolProps } from "../types.js";
import { ToolButton } from "../components/tool-button.js";
import { useRef, useState } from "react";
import { ResponsivePresenter } from "../../components/responsive/index.js";
import { CommentPopup } from "../popups/comment-popup.js";
import { useRefValue } from "../../hooks/use-ref-value.js";
import { findMark, selectionToOffset } from "../../utils/prosemirror.js";
import { Box, Flex, Input, Text } from "@theme-ui/components";
import { useHoverPopupContext } from "../floating-menus/hover-popup/context.js";
import { strings } from "@notesnook/intl";
import { CommentAttributes } from "../../extensions/comment/index.js";
import { nanoid } from "nanoid";
import { getFormattedDate } from "@notesnook/common";
export function AddComment(props: ToolProps) {
const { editor } = props;
const buttonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false);
const isActive = editor.isActive("comment");
if (isActive) return null;
return (
<>
<ToolButton
{...props}
buttonRef={buttonRef}
onClick={() => setIsOpen(true)}
toggled={isOpen}
disabled={editor.state.selection.empty}
/>
<ResponsivePresenter
mobile="sheet"
desktop="popup"
position={{
target: buttonRef.current || undefined,
isTargetAbsolute: true,
location: "below",
align: "center",
yOffset: 5
}}
title={strings.addComment()}
isOpen={isOpen}
items={[]}
onClose={() => {
setIsOpen(false);
editor.commands.focus();
}}
focusOnRender={false}
>
<CommentPopup
onClose={() => setIsOpen(false)}
onDone={(text: string) => {
const commentId = `comment-${nanoid(8)}`;
const attributes: CommentAttributes = {
id: commentId,
text,
timestamp: Date.now()
};
editor.commands.setComment(attributes);
setIsOpen(false);
}}
/>
</ResponsivePresenter>
</>
);
}
export function ViewComment(props: ToolProps) {
const { editor } = props;
const { selectedNode: _selectedNode, hide } = useHoverPopupContext();
const selectedNode = useRefValue(
_selectedNode || selectionToOffset(editor.state)
);
const { node } = selectedNode.current || {};
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const comment = node ? findMark(node, "comment") : null;
const [commentAttributes, setCommentAttributes] = useState(
comment ? (comment.attrs as CommentAttributes) : null
);
if (!commentAttributes) return null;
function saveComment() {
if (!commentAttributes) return null;
if (selectedNode.current) {
editor.chain().focus().setTextSelection(selectedNode.current).run();
}
const text = inputRef.current?.value || "";
const attributes: CommentAttributes = {
id: commentAttributes.id,
text,
timestamp: Date.now()
};
editor.commands.setComment(attributes);
setIsEditing(false);
setCommentAttributes(attributes);
}
return (
<Flex sx={{ flexDirection: "column", gap: 1, px: 1 }}>
<Flex sx={{ alignItems: "center", justifyContent: "space-between" }}>
<Text sx={{ fontSize: "subBody" }}>
{getFormattedDate(commentAttributes.timestamp, "date-time")}
</Text>
<Flex sx={{ alignItems: "center" }}>
<ToolButton
icon={"delete"}
title={strings.deleteComment()}
iconSize={"small"}
onClick={() => {
if (_selectedNode)
editor.commands.setTextSelection({
from: _selectedNode.from,
to: _selectedNode.to
});
editor.chain().focus().unsetComment().run();
hide();
}}
/>
{isEditing ? (
<ToolButton
icon={"save"}
title={strings.saveComment()}
iconSize={"small"}
onClick={saveComment}
/>
) : (
<ToolButton
icon={"commentEdit"}
title={strings.editComment()}
iconSize={"small"}
onClick={() => {
if (editor.isEditable) {
setIsEditing(true);
}
}}
/>
)}
</Flex>
</Flex>
<Box
sx={{
minWidth: 200,
maxWidth: 400,
pb: 2
}}
>
{isEditing ? (
<Input
autoFocus
as={"textarea"}
ref={inputRef}
defaultValue={commentAttributes.text}
onKeyUp={(e) => {
if (e.key === "Enter" && e.ctrlKey) {
saveComment();
}
if (e.key === "Escape") {
e.stopPropagation();
setIsEditing(false);
}
}}
sx={{
fontSize: "body",
width: "100%",
height: "fit-content"
}}
/>
) : (
<Text
variant="body"
sx={{
textOverflow: "ellipsis",
whiteSpace: "pre-wrap",
wordBreak: "break-word"
}}
>
{commentAttributes.text}
</Text>
)}
</Box>
</Flex>
);
}

View File

@@ -96,6 +96,7 @@ import {
OpenLink,
CopyLink
} from "./link.js";
import { AddComment, ViewComment } from "./comment.js";
import {
WebClipFullScreen,
WebClipSettings,
@@ -121,6 +122,8 @@ const tools = {
copyLink: CopyLink,
linkSettings: LinkSettings,
openLink: OpenLink,
addComment: AddComment,
viewComment: ViewComment,
insertBlock: InsertBlock,
numberedList: NumberedList,
bulletList: BulletList,

View File

@@ -99,6 +99,17 @@
cursor: pointer;
}
.ProseMirror span.comment-highlight {
text-decoration: underline;
text-decoration-style: dashed;
text-decoration-color: var(--accent);
text-decoration-thickness: 2px;
}
.ProseMirror span.comment-highlight:hover {
text-decoration-style: solid;
}
.ProseMirror a[href^="nn://"]::before {
display: inline-block;
cursor: pointer;

View File

@@ -699,6 +699,10 @@ msgstr "Add a tag"
msgid "Add color"
msgstr "Add color"
#: src/strings.ts:2609
msgid "Add comment"
msgstr "Add comment"
#: src/strings.ts:894
msgid "Add notebook"
msgstr "Add notebook"
@@ -2101,6 +2105,10 @@ msgstr "Delete collapsed section"
msgid "Delete column"
msgstr "Delete column"
#: src/strings.ts:2612
msgid "Delete comment"
msgstr "Delete comment"
#: src/strings.ts:1313
msgid "Delete group"
msgstr "Delete group"
@@ -2358,6 +2366,10 @@ msgstr "Easy access"
msgid "Edit"
msgstr "Edit"
#: src/strings.ts:2610
msgid "Edit comment"
msgstr "Edit comment"
#: src/strings.ts:526
msgid "Edit internal link"
msgstr "Edit internal link"
@@ -5425,6 +5437,10 @@ msgstr "Save account recovery key"
msgid "Save and continue"
msgstr "Save and continue"
#: src/strings.ts:2614
msgid "Save comment"
msgstr "Save comment"
#: src/strings.ts:1047
msgid "Save data recovery key"
msgstr "Save data recovery key"
@@ -7014,6 +7030,10 @@ msgstr "View all linked notebooks"
msgid "View and share debug logs"
msgstr "View and share debug logs"
#: src/strings.ts:2611
msgid "View comment"
msgstr "View comment"
#: src/strings.ts:1746
msgid "View receipt"
msgstr "View receipt"
@@ -7147,6 +7167,10 @@ msgstr "Words"
msgid "Work & Office"
msgstr "Work & Office"
#: src/strings.ts:2613
msgid "Write a comment..."
msgstr "Write a comment..."
#: src/strings.ts:822
msgid "Write notes with freedom, no spying, no tracking."
msgstr "Write notes with freedom, no spying, no tracking."

View File

@@ -699,6 +699,10 @@ msgstr ""
msgid "Add color"
msgstr ""
#: src/strings.ts:2609
msgid "Add comment"
msgstr ""
#: src/strings.ts:894
msgid "Add notebook"
msgstr ""
@@ -2090,6 +2094,10 @@ msgstr ""
msgid "Delete column"
msgstr ""
#: src/strings.ts:2612
msgid "Delete comment"
msgstr ""
#: src/strings.ts:1313
msgid "Delete group"
msgstr ""
@@ -2347,6 +2355,10 @@ msgstr ""
msgid "Edit"
msgstr ""
#: src/strings.ts:2610
msgid "Edit comment"
msgstr ""
#: src/strings.ts:526
msgid "Edit internal link"
msgstr ""
@@ -5399,6 +5411,10 @@ msgstr ""
msgid "Save and continue"
msgstr ""
#: src/strings.ts:2614
msgid "Save comment"
msgstr ""
#: src/strings.ts:1047
msgid "Save data recovery key"
msgstr ""
@@ -6965,6 +6981,10 @@ msgstr ""
msgid "View and share debug logs"
msgstr ""
#: src/strings.ts:2611
msgid "View comment"
msgstr ""
#: src/strings.ts:1746
msgid "View receipt"
msgstr ""
@@ -7098,6 +7118,10 @@ msgstr ""
msgid "Work & Office"
msgstr ""
#: src/strings.ts:2613
msgid "Write a comment..."
msgstr ""
#: src/strings.ts:822
msgid "Write notes with freedom, no spying, no tracking."
msgstr ""

View File

@@ -2605,5 +2605,11 @@ Use this if changes from other devices are not appearing on this device. This wi
clickToDirectlyClaimPromo: () =>
t`Click here to directly claim the promotion.`,
loginToUploadAttachments: () =>
t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)`
t`Login to upload attachments. [Read more](https://help.notesnook.com/faqs/login-to-upload-attachments)`,
addComment: () => t`Add comment`,
editComment: () => t`Edit comment`,
viewComment: () => t`View comment`,
deleteComment: () => t`Delete comment`,
writeComment: () => t`Write a comment...`,
saveComment: () => t`Save comment`
};