@blocknote/core 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/blocknote.cjs +12 -0
- package/dist/blocknote.cjs.map +1 -0
- package/dist/blocknote.js +4754 -3514
- package/dist/blocknote.js.map +1 -1
- package/dist/comments.cjs +2 -0
- package/dist/comments.cjs.map +1 -0
- package/dist/comments.js +593 -0
- package/dist/comments.js.map +1 -0
- package/dist/style.css +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/webpack-stats.json +1 -1
- package/package.json +39 -26
- package/src/api/blockManipulation/commands/insertBlocks/__snapshots__/insertBlocks.test.ts.snap +1022 -378
- package/src/api/blockManipulation/commands/mergeBlocks/__snapshots__/mergeBlocks.test.ts.snap +730 -270
- package/src/api/blockManipulation/commands/moveBlocks/__snapshots__/moveBlocks.test.ts.snap +3100 -1260
- package/src/api/blockManipulation/commands/removeBlocks/__snapshots__/removeBlocks.test.ts.snap +438 -162
- package/src/api/blockManipulation/commands/replaceBlocks/__snapshots__/replaceBlocks.test.ts.snap +1168 -432
- package/src/api/blockManipulation/commands/splitBlock/__snapshots__/splitBlock.test.ts.snap +930 -378
- package/src/api/blockManipulation/commands/updateBlock/__snapshots__/updateBlock.test.ts.snap +2485 -1015
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.test.ts +28 -1
- package/src/api/blockManipulation/commands/updateBlock/updateBlock.ts +1 -1
- package/src/api/blockManipulation/selections/__snapshots__/selection.test.ts.snap +292 -108
- package/src/api/blockManipulation/setupTestEnv.ts +14 -1
- package/src/api/blockManipulation/tables/tables.test.ts +1987 -0
- package/src/api/blockManipulation/tables/tables.ts +887 -0
- package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraph.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteEndOfParagraphText.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteImage.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteParagraphInCustomBlock.html +66 -24
- package/src/api/clipboard/__snapshots__/external/pasteTable.html +132 -48
- package/src/api/clipboard/__snapshots__/external/pasteTableInExistingTable.html +136 -44
- package/src/api/clipboard/toClipboard/copyExtension.ts +2 -3
- package/src/api/exporters/html/__snapshots__/table/headerCols/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerCols/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerRows/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headerRows/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headersRows/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/headersRows/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedCellColors/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedCellColors/internal.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/external.html +1 -0
- package/src/api/exporters/html/__snapshots__/table/mixedRowspansAndColspans/internal.html +1 -0
- package/src/api/exporters/markdown/__snapshots__/table/headerCols/markdown.md +4 -0
- package/src/api/exporters/markdown/__snapshots__/table/headerRows/markdown.md +4 -0
- package/src/api/exporters/markdown/__snapshots__/table/mixedCellColors/markdown.md +5 -0
- package/src/api/exporters/markdown/__snapshots__/table/mixedRowspansAndColspans/markdown.md +5 -0
- package/src/api/nodeConversions/__snapshots__/nodeConversions.test.ts.snap +985 -20
- package/src/api/nodeConversions/blockToNode.ts +63 -20
- package/src/api/nodeConversions/nodeToBlock.ts +75 -13
- package/src/api/parsers/html/__snapshots__/parse-notion-html.json +145 -54
- package/src/api/testUtil/cases/defaultSchema.ts +782 -9
- package/src/api/testUtil/partialBlockTestUtil.ts +39 -4
- package/src/blocks/TableBlockContent/TableBlockContent.ts +11 -5
- package/src/blocks/defaultBlockTypeGuards.ts +8 -0
- package/src/comments/index.ts +9 -0
- package/src/comments/models/User.ts +8 -0
- package/src/comments/threadstore/DefaultThreadStoreAuth.ts +106 -0
- package/src/comments/threadstore/ThreadStore.ts +134 -0
- package/src/comments/threadstore/ThreadStoreAuth.ts +13 -0
- package/src/comments/threadstore/TipTapThreadStore.ts +292 -0
- package/src/comments/threadstore/yjs/RESTYjsThreadStore.ts +144 -0
- package/src/comments/threadstore/yjs/YjsThreadStore.test.ts +294 -0
- package/src/comments/threadstore/yjs/YjsThreadStore.ts +340 -0
- package/src/comments/threadstore/yjs/YjsThreadStoreBase.ts +48 -0
- package/src/comments/threadstore/yjs/yjsHelpers.ts +121 -0
- package/src/comments/types.ts +117 -0
- package/src/editor/Block.css +16 -8
- package/src/editor/BlockNoteEditor.ts +269 -92
- package/src/editor/BlockNoteExtensions.ts +24 -1
- package/src/editor/BlockNoteTipTapEditor.ts +5 -1
- package/src/editor/editor.css +17 -0
- package/src/extensions/BackgroundColor/BackgroundColorExtension.ts +1 -1
- package/src/extensions/Comments/CommentMark.ts +61 -0
- package/src/extensions/Comments/CommentsPlugin.ts +301 -0
- package/src/extensions/Comments/userstore/UserStore.ts +72 -0
- package/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +9 -5
- package/src/extensions/LinkToolbar/LinkToolbarPlugin.ts +3 -3
- package/src/extensions/ShowSelection/ShowSelectionPlugin.ts +52 -0
- package/src/extensions/TableHandles/TableHandlesPlugin.ts +409 -57
- package/src/extensions/TextAlignment/TextAlignmentExtension.ts +2 -0
- package/src/extensions/TextColor/TextColorExtension.ts +1 -1
- package/src/i18n/locales/ar.ts +23 -0
- package/src/i18n/locales/de.ts +15 -0
- package/src/i18n/locales/en.ts +25 -1
- package/src/i18n/locales/es.ts +16 -1
- package/src/i18n/locales/fr.ts +23 -0
- package/src/i18n/locales/hr.ts +18 -0
- package/src/i18n/locales/is.ts +24 -1
- package/src/i18n/locales/it.ts +15 -0
- package/src/i18n/locales/ja.ts +23 -0
- package/src/i18n/locales/ko.ts +23 -0
- package/src/i18n/locales/nl.ts +23 -0
- package/src/i18n/locales/no.ts +23 -0
- package/src/i18n/locales/pl.ts +23 -0
- package/src/i18n/locales/pt.ts +23 -0
- package/src/i18n/locales/ru.ts +23 -0
- package/src/i18n/locales/uk.ts +23 -0
- package/src/i18n/locales/vi.ts +23 -0
- package/src/i18n/locales/zh.ts +23 -0
- package/src/index.ts +6 -4
- package/src/schema/blocks/types.ts +32 -2
- package/src/util/browser.ts +1 -1
- package/src/util/table.ts +107 -0
- package/types/src/api/blockManipulation/tables/tables.d.ts +343 -0
- package/types/src/api/blockManipulation/tables/tables.test.d.ts +1 -0
- package/types/src/api/clipboard/toClipboard/copyExtension.d.ts +1 -1
- package/types/src/blocks/TableBlockContent/TableBlockContent.d.ts +1 -2
- package/types/src/blocks/defaultBlockTypeGuards.d.ts +3 -0
- package/types/src/comments/index.d.ts +9 -0
- package/types/src/comments/models/User.d.ts +8 -0
- package/types/src/comments/threadstore/DefaultThreadStoreAuth.d.ts +47 -0
- package/types/src/comments/threadstore/ThreadStore.d.ts +121 -0
- package/types/src/comments/threadstore/ThreadStoreAuth.d.ts +12 -0
- package/types/src/comments/threadstore/TipTapThreadStore.d.ts +97 -0
- package/types/src/comments/threadstore/yjs/RESTYjsThreadStore.d.ts +83 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStore.d.ts +79 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStore.test.d.ts +1 -0
- package/types/src/comments/threadstore/yjs/YjsThreadStoreBase.d.ts +15 -0
- package/types/src/comments/threadstore/yjs/yjsHelpers.d.ts +13 -0
- package/types/src/comments/types.d.ts +109 -0
- package/types/src/editor/BlockNoteEditor.d.ts +146 -66
- package/types/src/editor/BlockNoteExtensions.d.ts +4 -0
- package/types/src/extensions/Collaboration/createCollaborationExtensions.d.ts +1 -1
- package/types/src/extensions/Comments/CommentMark.d.ts +2 -0
- package/types/src/extensions/Comments/CommentsPlugin.d.ts +49 -0
- package/types/src/extensions/Comments/userstore/UserStore.d.ts +31 -0
- package/types/src/extensions/ShowSelection/ShowSelectionPlugin.d.ts +15 -0
- package/types/src/extensions/TableHandles/TableHandlesPlugin.d.ts +66 -1
- package/types/src/i18n/locales/de.d.ts +15 -0
- package/types/src/i18n/locales/en.d.ts +20 -0
- package/types/src/i18n/locales/es.d.ts +15 -0
- package/types/src/i18n/locales/hr.d.ts +18 -0
- package/types/src/i18n/locales/it.d.ts +15 -0
- package/types/src/index.d.ts +5 -4
- package/types/src/pm-nodes/BlockContainer.d.ts +2 -2
- package/types/src/pm-nodes/BlockGroup.d.ts +2 -2
- package/types/src/schema/blocks/types.d.ts +23 -2
- package/types/src/util/browser.d.ts +1 -1
- package/types/src/util/table.d.ts +12 -0
- package/dist/blocknote.umd.cjs +0 -11
- package/dist/blocknote.umd.cjs.map +0 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { v4 } from "uuid";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { CommentBody, CommentData, ThreadData } from "../../types.js";
|
|
4
|
+
import { ThreadStoreAuth } from "../ThreadStoreAuth.js";
|
|
5
|
+
import { YjsThreadStoreBase } from "./YjsThreadStoreBase.js";
|
|
6
|
+
import {
|
|
7
|
+
commentToYMap,
|
|
8
|
+
threadToYMap,
|
|
9
|
+
yMapToComment,
|
|
10
|
+
yMapToThread,
|
|
11
|
+
} from "./yjsHelpers.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* This is a Yjs-based implementation of the ThreadStore interface.
|
|
15
|
+
*
|
|
16
|
+
* It reads and writes thread / comments information directly to the underlying Yjs Document.
|
|
17
|
+
*
|
|
18
|
+
* @important While this is the easiest to add to your app, there are two challenges:
|
|
19
|
+
* - The user needs to be able to write to the Yjs document to store the information.
|
|
20
|
+
* So a user without write access to the Yjs document cannot leave any comments.
|
|
21
|
+
* - Even with write access, the operations are not secure. Unless your Yjs server
|
|
22
|
+
* guards against malicious operations, it's technically possible for one user to make changes to another user's comments, etc.
|
|
23
|
+
* (even though these options are not visible in the UI, a malicious user can make unauthorized changes to the underlying Yjs document)
|
|
24
|
+
*/
|
|
25
|
+
export class YjsThreadStore extends YjsThreadStoreBase {
|
|
26
|
+
constructor(
|
|
27
|
+
private readonly userId: string,
|
|
28
|
+
threadsYMap: Y.Map<any>,
|
|
29
|
+
auth: ThreadStoreAuth
|
|
30
|
+
) {
|
|
31
|
+
super(threadsYMap, auth);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private transact = <T, R>(
|
|
35
|
+
fn: (options: T) => R
|
|
36
|
+
): ((options: T) => Promise<R>) => {
|
|
37
|
+
return async (options: T) => {
|
|
38
|
+
return this.threadsYMap.doc!.transact(() => {
|
|
39
|
+
return fn(options);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
public createThread = this.transact(
|
|
45
|
+
(options: {
|
|
46
|
+
initialComment: {
|
|
47
|
+
body: CommentBody;
|
|
48
|
+
metadata?: any;
|
|
49
|
+
};
|
|
50
|
+
metadata?: any;
|
|
51
|
+
}) => {
|
|
52
|
+
if (!this.auth.canCreateThread()) {
|
|
53
|
+
throw new Error("Not authorized");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const date = new Date();
|
|
57
|
+
|
|
58
|
+
const comment: CommentData = {
|
|
59
|
+
type: "comment",
|
|
60
|
+
id: v4(),
|
|
61
|
+
userId: this.userId,
|
|
62
|
+
createdAt: date,
|
|
63
|
+
updatedAt: date,
|
|
64
|
+
reactions: [],
|
|
65
|
+
metadata: options.initialComment.metadata,
|
|
66
|
+
body: options.initialComment.body,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const thread: ThreadData = {
|
|
70
|
+
type: "thread",
|
|
71
|
+
id: v4(),
|
|
72
|
+
createdAt: date,
|
|
73
|
+
updatedAt: date,
|
|
74
|
+
comments: [comment],
|
|
75
|
+
resolved: false,
|
|
76
|
+
metadata: options.metadata,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
this.threadsYMap.set(thread.id, threadToYMap(thread));
|
|
80
|
+
|
|
81
|
+
return thread;
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// YjsThreadStore does not support addThreadToDocument
|
|
86
|
+
public addThreadToDocument = undefined;
|
|
87
|
+
|
|
88
|
+
public addComment = this.transact(
|
|
89
|
+
(options: {
|
|
90
|
+
comment: {
|
|
91
|
+
body: CommentBody;
|
|
92
|
+
metadata?: any;
|
|
93
|
+
};
|
|
94
|
+
threadId: string;
|
|
95
|
+
}) => {
|
|
96
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
97
|
+
if (!yThread) {
|
|
98
|
+
throw new Error("Thread not found");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!this.auth.canAddComment(yMapToThread(yThread))) {
|
|
102
|
+
throw new Error("Not authorized");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const date = new Date();
|
|
106
|
+
const comment: CommentData = {
|
|
107
|
+
type: "comment",
|
|
108
|
+
id: v4(),
|
|
109
|
+
userId: this.userId,
|
|
110
|
+
createdAt: date,
|
|
111
|
+
updatedAt: date,
|
|
112
|
+
deletedAt: undefined,
|
|
113
|
+
reactions: [],
|
|
114
|
+
metadata: options.comment.metadata,
|
|
115
|
+
body: options.comment.body,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
(yThread.get("comments") as Y.Array<Y.Map<any>>).push([
|
|
119
|
+
commentToYMap(comment),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
yThread.set("updatedAt", new Date().getTime());
|
|
123
|
+
return comment;
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
public updateComment = this.transact(
|
|
128
|
+
(options: {
|
|
129
|
+
comment: {
|
|
130
|
+
body: CommentBody;
|
|
131
|
+
metadata?: any;
|
|
132
|
+
};
|
|
133
|
+
threadId: string;
|
|
134
|
+
commentId: string;
|
|
135
|
+
}) => {
|
|
136
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
137
|
+
if (!yThread) {
|
|
138
|
+
throw new Error("Thread not found");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const yCommentIndex = yArrayFindIndex(
|
|
142
|
+
yThread.get("comments"),
|
|
143
|
+
(comment) => comment.get("id") === options.commentId
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (yCommentIndex === -1) {
|
|
147
|
+
throw new Error("Comment not found");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const yComment = yThread.get("comments").get(yCommentIndex);
|
|
151
|
+
|
|
152
|
+
if (!this.auth.canUpdateComment(yMapToComment(yComment))) {
|
|
153
|
+
throw new Error("Not authorized");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
yComment.set("body", options.comment.body);
|
|
157
|
+
yComment.set("updatedAt", new Date().getTime());
|
|
158
|
+
yComment.set("metadata", options.comment.metadata);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
public deleteComment = this.transact(
|
|
163
|
+
(options: {
|
|
164
|
+
threadId: string;
|
|
165
|
+
commentId: string;
|
|
166
|
+
softDelete?: boolean;
|
|
167
|
+
}) => {
|
|
168
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
169
|
+
if (!yThread) {
|
|
170
|
+
throw new Error("Thread not found");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const yCommentIndex = yArrayFindIndex(
|
|
174
|
+
yThread.get("comments"),
|
|
175
|
+
(comment) => comment.get("id") === options.commentId
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (yCommentIndex === -1) {
|
|
179
|
+
throw new Error("Comment not found");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const yComment = yThread.get("comments").get(yCommentIndex);
|
|
183
|
+
|
|
184
|
+
if (!this.auth.canDeleteComment(yMapToComment(yComment))) {
|
|
185
|
+
throw new Error("Not authorized");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (yComment.get("deletedAt")) {
|
|
189
|
+
throw new Error("Comment already deleted");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.softDelete) {
|
|
193
|
+
yComment.set("deletedAt", new Date().getTime());
|
|
194
|
+
yComment.set("body", undefined);
|
|
195
|
+
} else {
|
|
196
|
+
yThread.get("comments").delete(yCommentIndex);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
(yThread.get("comments") as Y.Array<any>)
|
|
201
|
+
.toArray()
|
|
202
|
+
.every((comment) => comment.get("deletedAt"))
|
|
203
|
+
) {
|
|
204
|
+
// all comments deleted
|
|
205
|
+
if (options.softDelete) {
|
|
206
|
+
yThread.set("deletedAt", new Date().getTime());
|
|
207
|
+
} else {
|
|
208
|
+
this.threadsYMap.delete(options.threadId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
yThread.set("updatedAt", new Date().getTime());
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
public deleteThread = this.transact((options: { threadId: string }) => {
|
|
217
|
+
if (
|
|
218
|
+
!this.auth.canDeleteThread(
|
|
219
|
+
yMapToThread(this.threadsYMap.get(options.threadId))
|
|
220
|
+
)
|
|
221
|
+
) {
|
|
222
|
+
throw new Error("Not authorized");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.threadsYMap.delete(options.threadId);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
public resolveThread = this.transact((options: { threadId: string }) => {
|
|
229
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
230
|
+
if (!yThread) {
|
|
231
|
+
throw new Error("Thread not found");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!this.auth.canResolveThread(yMapToThread(yThread))) {
|
|
235
|
+
throw new Error("Not authorized");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
yThread.set("resolved", true);
|
|
239
|
+
yThread.set("resolvedUpdatedAt", new Date().getTime());
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
public unresolveThread = this.transact((options: { threadId: string }) => {
|
|
243
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
244
|
+
if (!yThread) {
|
|
245
|
+
throw new Error("Thread not found");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!this.auth.canUnresolveThread(yMapToThread(yThread))) {
|
|
249
|
+
throw new Error("Not authorized");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
yThread.set("resolved", false);
|
|
253
|
+
yThread.set("resolvedUpdatedAt", new Date().getTime());
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
public addReaction = this.transact(
|
|
257
|
+
(options: { threadId: string; commentId: string; emoji: string }) => {
|
|
258
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
259
|
+
if (!yThread) {
|
|
260
|
+
throw new Error("Thread not found");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const yCommentIndex = yArrayFindIndex(
|
|
264
|
+
yThread.get("comments"),
|
|
265
|
+
(comment) => comment.get("id") === options.commentId
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (yCommentIndex === -1) {
|
|
269
|
+
throw new Error("Comment not found");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const yComment = yThread.get("comments").get(yCommentIndex);
|
|
273
|
+
|
|
274
|
+
if (!this.auth.canAddReaction(yMapToComment(yComment), options.emoji)) {
|
|
275
|
+
throw new Error("Not authorized");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const date = new Date();
|
|
279
|
+
|
|
280
|
+
const key = `${this.userId}-${options.emoji}`;
|
|
281
|
+
|
|
282
|
+
const reactionsByUser = yComment.get("reactionsByUser");
|
|
283
|
+
|
|
284
|
+
if (reactionsByUser.has(key)) {
|
|
285
|
+
// already exists
|
|
286
|
+
return;
|
|
287
|
+
} else {
|
|
288
|
+
const reaction = new Y.Map();
|
|
289
|
+
reaction.set("emoji", options.emoji);
|
|
290
|
+
reaction.set("createdAt", date.getTime());
|
|
291
|
+
reaction.set("userId", this.userId);
|
|
292
|
+
reactionsByUser.set(key, reaction);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
public deleteReaction = this.transact(
|
|
298
|
+
(options: { threadId: string; commentId: string; emoji: string }) => {
|
|
299
|
+
const yThread = this.threadsYMap.get(options.threadId);
|
|
300
|
+
if (!yThread) {
|
|
301
|
+
throw new Error("Thread not found");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const yCommentIndex = yArrayFindIndex(
|
|
305
|
+
yThread.get("comments"),
|
|
306
|
+
(comment) => comment.get("id") === options.commentId
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (yCommentIndex === -1) {
|
|
310
|
+
throw new Error("Comment not found");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const yComment = yThread.get("comments").get(yCommentIndex);
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
!this.auth.canDeleteReaction(yMapToComment(yComment), options.emoji)
|
|
317
|
+
) {
|
|
318
|
+
throw new Error("Not authorized");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const key = `${this.userId}-${options.emoji}`;
|
|
322
|
+
|
|
323
|
+
const reactionsByUser = yComment.get("reactionsByUser");
|
|
324
|
+
|
|
325
|
+
reactionsByUser.delete(key);
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function yArrayFindIndex(
|
|
331
|
+
yArray: Y.Array<any>,
|
|
332
|
+
predicate: (item: any) => boolean
|
|
333
|
+
) {
|
|
334
|
+
for (let i = 0; i < yArray.length; i++) {
|
|
335
|
+
if (predicate(yArray.get(i))) {
|
|
336
|
+
return i;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return -1;
|
|
340
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { ThreadData } from "../../types.js";
|
|
3
|
+
import { ThreadStore } from "../ThreadStore.js";
|
|
4
|
+
import { ThreadStoreAuth } from "../ThreadStoreAuth.js";
|
|
5
|
+
import { yMapToThread } from "./yjsHelpers.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is an abstract class that only implements the READ methods required by the ThreadStore interface.
|
|
9
|
+
* The data is read from a Yjs Map.
|
|
10
|
+
*/
|
|
11
|
+
export abstract class YjsThreadStoreBase extends ThreadStore {
|
|
12
|
+
constructor(
|
|
13
|
+
protected readonly threadsYMap: Y.Map<any>,
|
|
14
|
+
auth: ThreadStoreAuth
|
|
15
|
+
) {
|
|
16
|
+
super(auth);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// TODO: async / reactive interface?
|
|
20
|
+
public getThread(threadId: string) {
|
|
21
|
+
const yThread = this.threadsYMap.get(threadId);
|
|
22
|
+
if (!yThread) {
|
|
23
|
+
throw new Error("Thread not found");
|
|
24
|
+
}
|
|
25
|
+
const thread = yMapToThread(yThread);
|
|
26
|
+
return thread;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public getThreads(): Map<string, ThreadData> {
|
|
30
|
+
const threadMap = new Map<string, ThreadData>();
|
|
31
|
+
this.threadsYMap.forEach((yThread, id) => {
|
|
32
|
+
threadMap.set(id, yMapToThread(yThread));
|
|
33
|
+
});
|
|
34
|
+
return threadMap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public subscribe(cb: (threads: Map<string, ThreadData>) => void) {
|
|
38
|
+
const observer = () => {
|
|
39
|
+
cb(this.getThreads());
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.threadsYMap.observeDeep(observer);
|
|
43
|
+
|
|
44
|
+
return () => {
|
|
45
|
+
this.threadsYMap.unobserveDeep(observer);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { CommentData, CommentReactionData, ThreadData } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
export function commentToYMap(comment: CommentData) {
|
|
5
|
+
const yMap = new Y.Map<any>();
|
|
6
|
+
yMap.set("id", comment.id);
|
|
7
|
+
yMap.set("userId", comment.userId);
|
|
8
|
+
yMap.set("createdAt", comment.createdAt.getTime());
|
|
9
|
+
yMap.set("updatedAt", comment.updatedAt.getTime());
|
|
10
|
+
if (comment.deletedAt) {
|
|
11
|
+
yMap.set("deletedAt", comment.deletedAt.getTime());
|
|
12
|
+
yMap.set("body", undefined);
|
|
13
|
+
} else {
|
|
14
|
+
yMap.set("body", comment.body);
|
|
15
|
+
}
|
|
16
|
+
if (comment.reactions.length > 0) {
|
|
17
|
+
throw new Error("Reactions should be empty in commentToYMap");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Reactions are stored in a map keyed by {userId-emoji},
|
|
22
|
+
* this makes it easy to add / remove reactions and in a way that works local-first.
|
|
23
|
+
* The cost is that "reading" the reactions is a bit more complex (see yMapToReactions).
|
|
24
|
+
*/
|
|
25
|
+
yMap.set("reactionsByUser", new Y.Map());
|
|
26
|
+
yMap.set("metadata", comment.metadata);
|
|
27
|
+
|
|
28
|
+
return yMap;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function threadToYMap(thread: ThreadData) {
|
|
32
|
+
const yMap = new Y.Map();
|
|
33
|
+
yMap.set("id", thread.id);
|
|
34
|
+
yMap.set("createdAt", thread.createdAt.getTime());
|
|
35
|
+
yMap.set("updatedAt", thread.updatedAt.getTime());
|
|
36
|
+
const commentsArray = new Y.Array<Y.Map<any>>();
|
|
37
|
+
|
|
38
|
+
commentsArray.push(thread.comments.map((comment) => commentToYMap(comment)));
|
|
39
|
+
|
|
40
|
+
yMap.set("comments", commentsArray);
|
|
41
|
+
yMap.set("resolved", thread.resolved);
|
|
42
|
+
yMap.set("resolvedUpdatedAt", thread.resolvedUpdatedAt?.getTime());
|
|
43
|
+
yMap.set("metadata", thread.metadata);
|
|
44
|
+
return yMap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type SingleUserCommentReactionData = {
|
|
48
|
+
emoji: string;
|
|
49
|
+
createdAt: Date;
|
|
50
|
+
userId: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function yMapToReaction(
|
|
54
|
+
yMap: Y.Map<any>
|
|
55
|
+
): SingleUserCommentReactionData {
|
|
56
|
+
return {
|
|
57
|
+
emoji: yMap.get("emoji"),
|
|
58
|
+
createdAt: new Date(yMap.get("createdAt")),
|
|
59
|
+
userId: yMap.get("userId"),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function yMapToReactions(yMap: Y.Map<any>): CommentReactionData[] {
|
|
64
|
+
const flatReactions = [...yMap.values()].map((reaction: Y.Map<any>) =>
|
|
65
|
+
yMapToReaction(reaction)
|
|
66
|
+
);
|
|
67
|
+
// combine reactions by the same emoji
|
|
68
|
+
return flatReactions.reduce(
|
|
69
|
+
(acc: CommentReactionData[], reaction: SingleUserCommentReactionData) => {
|
|
70
|
+
const existingReaction = acc.find((r) => r.emoji === reaction.emoji);
|
|
71
|
+
if (existingReaction) {
|
|
72
|
+
existingReaction.userIds.push(reaction.userId);
|
|
73
|
+
existingReaction.createdAt = new Date(
|
|
74
|
+
Math.min(
|
|
75
|
+
existingReaction.createdAt.getTime(),
|
|
76
|
+
reaction.createdAt.getTime()
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
acc.push({
|
|
81
|
+
emoji: reaction.emoji,
|
|
82
|
+
createdAt: reaction.createdAt,
|
|
83
|
+
userIds: [reaction.userId],
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return acc;
|
|
87
|
+
},
|
|
88
|
+
[] as CommentReactionData[]
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function yMapToComment(yMap: Y.Map<any>): CommentData {
|
|
93
|
+
return {
|
|
94
|
+
type: "comment",
|
|
95
|
+
id: yMap.get("id"),
|
|
96
|
+
userId: yMap.get("userId"),
|
|
97
|
+
createdAt: new Date(yMap.get("createdAt")),
|
|
98
|
+
updatedAt: new Date(yMap.get("updatedAt")),
|
|
99
|
+
deletedAt: yMap.get("deletedAt")
|
|
100
|
+
? new Date(yMap.get("deletedAt"))
|
|
101
|
+
: undefined,
|
|
102
|
+
reactions: yMapToReactions(yMap.get("reactionsByUser")),
|
|
103
|
+
metadata: yMap.get("metadata"),
|
|
104
|
+
body: yMap.get("body"),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function yMapToThread(yMap: Y.Map<any>): ThreadData {
|
|
109
|
+
return {
|
|
110
|
+
type: "thread",
|
|
111
|
+
id: yMap.get("id"),
|
|
112
|
+
createdAt: new Date(yMap.get("createdAt")),
|
|
113
|
+
updatedAt: new Date(yMap.get("updatedAt")),
|
|
114
|
+
comments: ((yMap.get("comments") as Y.Array<Y.Map<any>>) || []).map(
|
|
115
|
+
(comment) => yMapToComment(comment)
|
|
116
|
+
),
|
|
117
|
+
resolved: yMap.get("resolved"),
|
|
118
|
+
resolvedUpdatedAt: yMap.get("resolvedUpdatedAt"),
|
|
119
|
+
metadata: yMap.get("metadata"),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The body of a comment. This actually is a BlockNote document (array of blocks)
|
|
3
|
+
*/
|
|
4
|
+
export type CommentBody = any;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A reaction to a comment.
|
|
8
|
+
*/
|
|
9
|
+
export type CommentReactionData = {
|
|
10
|
+
/**
|
|
11
|
+
* The emoji that was reacted to the comment.
|
|
12
|
+
*/
|
|
13
|
+
emoji: string;
|
|
14
|
+
/**
|
|
15
|
+
* The date the first user reacted to the comment with this emoji.
|
|
16
|
+
*/
|
|
17
|
+
createdAt: Date;
|
|
18
|
+
/**
|
|
19
|
+
* The user ids of the users that have reacted to the comment with this emoji
|
|
20
|
+
*/
|
|
21
|
+
userIds: string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Information about a comment.
|
|
26
|
+
*/
|
|
27
|
+
export type CommentData = {
|
|
28
|
+
type: "comment";
|
|
29
|
+
/**
|
|
30
|
+
* The unique identifier for the comment.
|
|
31
|
+
*/
|
|
32
|
+
id: string;
|
|
33
|
+
/**
|
|
34
|
+
* The user id of the author of the comment.
|
|
35
|
+
*/
|
|
36
|
+
userId: string;
|
|
37
|
+
/**
|
|
38
|
+
* The date when the comment was created.
|
|
39
|
+
*/
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
/**
|
|
42
|
+
* The date when the comment was last updated.
|
|
43
|
+
*/
|
|
44
|
+
updatedAt: Date;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The reactions (emoji reactions) to the comment.
|
|
48
|
+
*/
|
|
49
|
+
reactions: CommentReactionData[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* You can use this store any additional information about the comment.
|
|
53
|
+
*/
|
|
54
|
+
metadata: any;
|
|
55
|
+
} & (
|
|
56
|
+
| {
|
|
57
|
+
/**
|
|
58
|
+
* The date when the comment was deleted. This applies only for "soft deletes",
|
|
59
|
+
* otherwise the comment is removed entirely.
|
|
60
|
+
*/
|
|
61
|
+
deletedAt: Date;
|
|
62
|
+
/**
|
|
63
|
+
* The body of the comment is undefined if the comment is deleted.
|
|
64
|
+
*/
|
|
65
|
+
body: undefined;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
/**
|
|
69
|
+
* In case of a non-deleted comment, this is not set
|
|
70
|
+
*/
|
|
71
|
+
deletedAt?: never;
|
|
72
|
+
/**
|
|
73
|
+
* The body of the comment.
|
|
74
|
+
*/
|
|
75
|
+
body: CommentBody;
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Information about a thread. A thread holds a list of comments.
|
|
81
|
+
*/
|
|
82
|
+
export type ThreadData = {
|
|
83
|
+
type: "thread";
|
|
84
|
+
/**
|
|
85
|
+
* The unique identifier for the thread.
|
|
86
|
+
*/
|
|
87
|
+
id: string;
|
|
88
|
+
/**
|
|
89
|
+
* The date when the thread was created.
|
|
90
|
+
*/
|
|
91
|
+
createdAt: Date;
|
|
92
|
+
/**
|
|
93
|
+
* The date when the thread was last updated.
|
|
94
|
+
*/
|
|
95
|
+
updatedAt: Date;
|
|
96
|
+
/**
|
|
97
|
+
* The comments in the thread.
|
|
98
|
+
*/
|
|
99
|
+
comments: CommentData[];
|
|
100
|
+
/**
|
|
101
|
+
* Whether the thread has been marked as resolved.
|
|
102
|
+
*/
|
|
103
|
+
resolved: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The date when the thread was marked as resolved.
|
|
106
|
+
*/
|
|
107
|
+
resolvedUpdatedAt?: Date;
|
|
108
|
+
/**
|
|
109
|
+
* You can use this store any additional information about the thread.
|
|
110
|
+
*/
|
|
111
|
+
metadata: any;
|
|
112
|
+
/**
|
|
113
|
+
* The date when the thread was deleted. (or undefined if it is not deleted)
|
|
114
|
+
* This only applies for "soft deletes", otherwise the thread is removed entirely.
|
|
115
|
+
*/
|
|
116
|
+
deletedAt?: Date;
|
|
117
|
+
};
|
package/src/editor/Block.css
CHANGED
|
@@ -499,23 +499,23 @@ NESTED BLOCKS
|
|
|
499
499
|
|
|
500
500
|
/* TEXT ALIGNMENT */
|
|
501
501
|
[data-text-alignment="left"] {
|
|
502
|
-
justify-content: flex-start;
|
|
503
|
-
text-align: left;
|
|
502
|
+
justify-content: flex-start !important;
|
|
503
|
+
text-align: left !important;
|
|
504
504
|
}
|
|
505
505
|
|
|
506
506
|
[data-text-alignment="center"] {
|
|
507
|
-
justify-content: center;
|
|
508
|
-
text-align: center;
|
|
507
|
+
justify-content: center !important;
|
|
508
|
+
text-align: center !important;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
[data-text-alignment="right"] {
|
|
512
|
-
justify-content: flex-end;
|
|
513
|
-
text-align: right;
|
|
512
|
+
justify-content: flex-end !important;
|
|
513
|
+
text-align: right !important;
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
[data-text-alignment="justify"] {
|
|
517
|
-
justify-content: flex-start;
|
|
518
|
-
text-align: justify;
|
|
517
|
+
justify-content: flex-start !important;
|
|
518
|
+
text-align: justify !important;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
.bn-block-column-list {
|
|
@@ -537,3 +537,11 @@ NESTED BLOCKS
|
|
|
537
537
|
.bn-block-column:last-child {
|
|
538
538
|
padding-right: 0;
|
|
539
539
|
}
|
|
540
|
+
|
|
541
|
+
.bn-thread-mark:not([data-orphan="true"]) {
|
|
542
|
+
background: rgba(255, 200, 0, 0.15);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.bn-thread-mark:not([data-orphan="true"]) .bn-thread-mark-selected {
|
|
546
|
+
background: rgba(255, 200, 0, 0.25);
|
|
547
|
+
}
|