diff --git a/examples/07-collaboration/05-comments/src/App.tsx b/examples/07-collaboration/05-comments/src/App.tsx index bd11b2539e..43a96eb3b1 100644 --- a/examples/07-collaboration/05-comments/src/App.tsx +++ b/examples/07-collaboration/05-comments/src/App.tsx @@ -100,7 +100,11 @@ function Document() { label={"User"} items={HARDCODED_USERS.map((user) => ({ text: `${user.username} (${ - user.role === "editor" ? "Editor" : "Commenter" + user.role === "editor" + ? "Editor" + : user.role === "comment" + ? "Commenter" + : "Content-Only Viewer" })`, icon: null, onClick: () => setActiveUser(user), diff --git a/examples/07-collaboration/05-comments/src/userdata.ts b/examples/07-collaboration/05-comments/src/userdata.ts index c54eaf0f9a..c0a43f074a 100644 --- a/examples/07-collaboration/05-comments/src/userdata.ts +++ b/examples/07-collaboration/05-comments/src/userdata.ts @@ -16,7 +16,7 @@ const getRandomElement = (list: any[]) => export const getRandomColor = () => getRandomElement(colors); export type MyUserType = User & { - role: "editor" | "comment"; + role: "editor" | "comment" | "document-only"; }; export const HARDCODED_USERS: MyUserType[] = [ @@ -44,4 +44,10 @@ export const HARDCODED_USERS: MyUserType[] = [ avatarUrl: "https://placehold.co/100x100?text=Betty", role: "comment", }, + { + id: "5", + username: "Donna Document", + avatarUrl: "https://placehold.co/100x100?text=Donna", + role: "document-only", + }, ]; diff --git a/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx b/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx index ed7bf38473..ecf8bfde05 100644 --- a/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx +++ b/examples/07-collaboration/06-comments-with-sidebar/src/App.tsx @@ -125,7 +125,11 @@ function Document() { label={"User"} items={HARDCODED_USERS.map((user) => ({ text: `${user.username} (${ - user.role === "editor" ? "Editor" : "Commenter" + user.role === "editor" + ? "Editor" + : user.role === "comment" + ? "Commenter" + : "Content-Only Viewer" })`, icon: null, onClick: () => { diff --git a/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts b/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts index c54eaf0f9a..c0a43f074a 100644 --- a/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts +++ b/examples/07-collaboration/06-comments-with-sidebar/src/userdata.ts @@ -16,7 +16,7 @@ const getRandomElement = (list: any[]) => export const getRandomColor = () => getRandomElement(colors); export type MyUserType = User & { - role: "editor" | "comment"; + role: "editor" | "comment" | "document-only"; }; export const HARDCODED_USERS: MyUserType[] = [ @@ -44,4 +44,10 @@ export const HARDCODED_USERS: MyUserType[] = [ avatarUrl: "https://placehold.co/100x100?text=Betty", role: "comment", }, + { + id: "5", + username: "Donna Document", + avatarUrl: "https://placehold.co/100x100?text=Donna", + role: "document-only", + }, ]; diff --git a/packages/core/src/comments/threadstore/DefaultThreadStoreAuth.ts b/packages/core/src/comments/threadstore/DefaultThreadStoreAuth.ts index 9b79a5aaf1..28c7cc35a7 100644 --- a/packages/core/src/comments/threadstore/DefaultThreadStoreAuth.ts +++ b/packages/core/src/comments/threadstore/DefaultThreadStoreAuth.ts @@ -18,58 +18,68 @@ import { ThreadStoreAuth } from "./ThreadStoreAuth.js"; export class DefaultThreadStoreAuth extends ThreadStoreAuth { constructor( private readonly userId: string, - private readonly role: "comment" | "editor", + private readonly role: "document-only" | "comment" | "editor", ) { super(); } + /** + * Auth: should be possible by anyone with comment access + */ + canViewComments(): boolean { + return this.role !== "document-only"; + } + /** * Auth: should be possible by anyone with comment access */ canCreateThread(): boolean { - return true; + return this.canViewComments(); } /** * Auth: should be possible by anyone with comment access */ canAddComment(_thread: ThreadData): boolean { - return true; + return this.canViewComments(); } /** * Auth: should only be possible by the comment author */ canUpdateComment(comment: CommentData): boolean { - return comment.userId === this.userId; + return this.canViewComments() && comment.userId === this.userId; } /** * Auth: should be possible by the comment author OR an editor of the document */ canDeleteComment(comment: CommentData): boolean { - return comment.userId === this.userId || this.role === "editor"; + return ( + this.canViewComments() && + (comment.userId === this.userId || this.role === "editor") + ); } /** * Auth: should only be possible by an editor of the document */ canDeleteThread(_thread: ThreadData): boolean { - return this.role === "editor"; + return this.canViewComments() && this.role === "editor"; } /** * Auth: should be possible by anyone with comment access */ canResolveThread(_thread: ThreadData): boolean { - return true; + return this.canViewComments(); } /** * Auth: should be possible by anyone with comment access */ canUnresolveThread(_thread: ThreadData): boolean { - return true; + return this.canViewComments(); } /** @@ -79,7 +89,7 @@ export class DefaultThreadStoreAuth extends ThreadStoreAuth { */ canAddReaction(comment: CommentData, emoji?: string): boolean { if (!emoji) { - return true; + return this.canViewComments(); } return !comment.reactions.some( @@ -95,7 +105,7 @@ export class DefaultThreadStoreAuth extends ThreadStoreAuth { */ canDeleteReaction(comment: CommentData, emoji?: string): boolean { if (!emoji) { - return true; + return this.canViewComments(); } return comment.reactions.some( diff --git a/packages/core/src/comments/threadstore/ThreadStoreAuth.ts b/packages/core/src/comments/threadstore/ThreadStoreAuth.ts index 452ad25b03..0f4cdb9f91 100644 --- a/packages/core/src/comments/threadstore/ThreadStoreAuth.ts +++ b/packages/core/src/comments/threadstore/ThreadStoreAuth.ts @@ -2,6 +2,7 @@ import { CommentData, ThreadData } from "../types.js"; export abstract class ThreadStoreAuth { abstract canCreateThread(): boolean; + abstract canViewComments(): boolean; abstract canAddComment(thread: ThreadData): boolean; abstract canUpdateComment(comment: CommentData): boolean; abstract canDeleteComment(comment: CommentData): boolean; diff --git a/packages/core/src/editor/Block.css b/packages/core/src/editor/Block.css index fdf4d31123..f523905077 100644 --- a/packages/core/src/editor/Block.css +++ b/packages/core/src/editor/Block.css @@ -717,10 +717,10 @@ NESTED BLOCKS padding-right: 0; } -.bn-thread-mark:not([data-orphan="true"]) { +.bn-editor:not([data-hide-comments]) .bn-thread-mark:not([data-orphan="true"]) { background: rgba(255, 200, 0, 0.15); } -.bn-thread-mark .bn-thread-mark-selected { +.bn-editor:not([data-hide-comments]) .bn-thread-mark .bn-thread-mark-selected { background: rgba(255, 200, 0, 0.25); } diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 0ef8ba10ba..3fb756e5d8 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -848,6 +848,9 @@ export class BlockNoteEditor< editorProps: { ...newOptions._tiptapOptions?.editorProps, attributes: { + ...(this.comments?.threadStore.auth.canViewComments() + ? {} + : { "data-hide-comments": "true" }), // As of TipTap v2.5.0 the tabIndex is removed when the editor is not // editable, so you can't focus it. We want to revert this as we have // UI behaviour that relies on it. diff --git a/packages/core/src/extensions/Comments/CommentsPlugin.ts b/packages/core/src/extensions/Comments/CommentsPlugin.ts index a5d1e24b6d..26d4439e16 100644 --- a/packages/core/src/extensions/Comments/CommentsPlugin.ts +++ b/packages/core/src/extensions/Comments/CommentsPlugin.ts @@ -180,6 +180,12 @@ export class CommentsPlugin extends BlockNoteExtension { if (!tr.docChanged && !action) { return state; } + if (!self.threadStore.auth.canViewComments()) { + // if the user doesn't have comment access, don't display the marks in the document + return { + decorations: DecorationSet.empty, + }; + } // only update threadPositions if the doc changed const threadPositions = tr.docChanged @@ -228,7 +234,10 @@ export class CommentsPlugin extends BlockNoteExtension { * Handle click on a thread mark and mark it as selected */ handleClick: (view, pos, event) => { - if (event.button !== 0) { + if ( + event.button !== 0 || + !self.threadStore.auth.canViewComments() + ) { return; } diff --git a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx index 3e85b2994f..fbd6d0f026 100644 --- a/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx +++ b/packages/react/src/components/FormattingToolbar/DefaultButtons/AddCommentButton.tsx @@ -24,7 +24,8 @@ export const AddCommentButton = () => { if ( // We manually check if a comment extension (like liveblocks) is installed // By adding default support for this, the user doesn't need to customize the formatting toolbar - !editor.comments + !editor.comments || + !editor.comments.threadStore.auth.canViewComments() ) { return null; }