-
-
Notifications
You must be signed in to change notification settings - Fork 502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
wip: feat: comments #1376
base: main
Are you sure you want to change the base?
wip: feat: comments #1376
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
} | ||
|
||
/* https://ariakit.org/components/hovercard */ | ||
/*.bn-ak-button {*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed?
.bn-toolbar.bn-ak-toolbar { | ||
overflow-x: auto; | ||
max-width: 100vw; | ||
.bn-ak-toolbar { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that we don't care about the max-width anymore? That would cause it from overflowing on small screens.
* Store the positions of all threads in the document. | ||
* this can be used later to implement a floating sidebar | ||
*/ | ||
threadPositions: Map<string, { from: number; to: number }>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm working on some bugs in the suggestion plugin that will be related to this. It isn't safe to store positions like this. Would likely also need to keep around a relative position as well. But, we can figure this out later
tr.removeMark(trimmedFrom, trimmedTo, markType); | ||
tr.addMark( | ||
trimmedFrom, | ||
trimmedTo, | ||
markType.create({ | ||
...mark.attrs, | ||
orphan: isOrphan, | ||
}) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't this just be that you update the mark attr instead of removing & re-applying it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvm, I see there isn't an easy API for that. I just remember a step specifically for updating markattrs but isn't exposed on transactions for some reason
|
||
editor.onCreate(() => { | ||
this.updateMarksFromThreads(this.threadStore.getThreads()); | ||
editor.onSelectionChange(() => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels weird to register this after the create hook, why not just register in the constructor separately?
// Note: Plugins are currently not destroyed when the editor is destroyed. | ||
// We should unsubscribe from the threadStore when the editor is destroyed. | ||
this.threadStore.subscribe(this.updateMarksFromThreads); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, we need an API for this
@@ -105,6 +105,7 @@ | |||
"@types/emoji-mart": "^3.0.14", | |||
"@types/hast": "^3.0.0", | |||
"@types/uuid": "^8.3.4", | |||
"@hocuspocus/provider": "^2.15.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is peerOptional no?
https://stackoverflow.com/questions/62047806/how-do-i-set-a-peer-dependency-optional#66228639
TS might care for someone who imports it
useEffect(() => { | ||
editor.ForceSelectionVisible = !!state?.pendingComment; | ||
}, [editor, state?.pendingComment]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is weird. Can we describe why this is needed, it also feels like it should not be handled by React, because it is being derived from the editor state?
// Positioning with [data-bn-thread-id] attribute is a bit hacky, | ||
// we could probably also use the thread position from the plugin state? | ||
// for now, this works ok | ||
const updateRef = useCallback(() => { | ||
if (!state?.selectedThreadId) { | ||
return; | ||
} | ||
|
||
const el = editor.domElement?.querySelector( | ||
`[data-bn-thread-id="${state?.selectedThreadId}"]` | ||
); | ||
if (el) { | ||
setReference(el); | ||
} | ||
}, [setReference, editor, state?.selectedThreadId]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely strange, I don't quite understand how this all works
const handleFocusIn = (event: FocusEvent) => { | ||
if (!focusedRef.current) { | ||
_setFocused(true); | ||
onFocus?.(event); | ||
} | ||
}; | ||
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const handleFocusOut = (event: FocusEvent) => { | ||
if (focusedRef.current && !containsRelatedTarget(event)) { | ||
_setFocused(false); | ||
onBlur?.(event); | ||
} | ||
}; | ||
|
||
useEffect(() => { | ||
const node = ref.current; | ||
|
||
if (node) { | ||
node.addEventListener("focusin", handleFocusIn); | ||
node.addEventListener("focusout", handleFocusOut); | ||
|
||
return () => { | ||
node?.removeEventListener("focusin", handleFocusIn); | ||
node?.removeEventListener("focusout", handleFocusOut); | ||
}; | ||
} | ||
|
||
return undefined; | ||
}, [handleFocusIn, handleFocusOut]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this was copied over, but just wanted to say that this is strangely wasteful, it constantly adds and removes the event listeners when it doesn't need to
This PR adds comments to BlockNote!
See docs
ThreadStores
YjsThreadStore
The YjsThreadStore provides direct Yjs-based storage for comments, storing thread data directly in the Yjs document. This implementation is ideal for simple collaborative setups where all users have write access to the document.
The downside of this is that comments require a more advanced permission model than text documents, and Yjs doesn't provide a straightforward way to secure document updates.
RESTYjsThreadStore
The RESTYjsThreadStore combines Yjs storage with a REST API backend, providing secure comment management while maintaining real-time collaboration. This implementation is ideal when you have strong authentication requirements, but is a little more work to set up.
In this implementation, data is written to the Yjs document via a REST API which can handle access control. Data is still retrieved from the Yjs document directly (after it's been updated by the REST API), this way all comment information automatically syncs between clients using the existing collaboration provider.
We created a separate repository with an example server.
TipTap
We integrate directly with the TiptapCollabProvider with
TiptapThreadStore
.LiveBlocks (later)
The UI Components are roughly based on the LiveBlocks open source components. LiveBlocks users will have two different ways to use LiveBlocks:
TODO
TBD:
Yjs next steps
To fix later
Important:
Nice to haves:
closes #695
closes #1109
closes #1148
closes #703
closes #1200