@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
595 lines (594 loc) • 17.5 kB
JavaScript
var A = Object.defineProperty;
var p = (d, r, e) => r in d ? A(d, r, { enumerable: !0, configurable: !0, writable: !0, value: e }) : d[r] = e;
var o = (d, r, e) => p(d, typeof r != "symbol" ? r + "" : r, e);
import * as T from "yjs";
import { v4 as I } from "uuid";
class v {
}
class b extends v {
constructor(r, e) {
super(), this.userId = r, this.role = e;
}
/**
* Auth: should be possible by anyone with comment access
*/
canCreateThread() {
return !0;
}
/**
* Auth: should be possible by anyone with comment access
*/
canAddComment(r) {
return !0;
}
/**
* Auth: should only be possible by the comment author
*/
canUpdateComment(r) {
return r.userId === this.userId;
}
/**
* Auth: should be possible by the comment author OR an editor of the document
*/
canDeleteComment(r) {
return r.userId === this.userId || this.role === "editor";
}
/**
* Auth: should only be possible by an editor of the document
*/
canDeleteThread(r) {
return this.role === "editor";
}
/**
* Auth: should be possible by anyone with comment access
*/
canResolveThread(r) {
return !0;
}
/**
* Auth: should be possible by anyone with comment access
*/
canUnresolveThread(r) {
return !0;
}
/**
* Auth: should be possible by anyone with comment access
*
* Note: will also check if the user has already reacted with the same emoji. TBD: is that a nice design or should this responsibility be outside of auth?
*/
canAddReaction(r, e) {
return e ? !r.reactions.some(
(t) => t.emoji === e && t.userIds.includes(this.userId)
) : !0;
}
/**
* Auth: should be possible by anyone with comment access
*
* Note: will also check if the user has already reacted with the same emoji. TBD: is that a nice design or should this responsibility be outside of auth?
*/
canDeleteReaction(r, e) {
return e ? r.reactions.some(
(t) => t.emoji === e && t.userIds.includes(this.userId)
) : !0;
}
}
class w {
constructor(r) {
o(this, "auth");
this.auth = r;
}
}
class j extends w {
constructor(e, t, a) {
super(a);
// TipTapThreadStore does not support addThreadToDocument
o(this, "addThreadToDocument");
this.userId = e, this.provider = t;
}
/**
* Creates a new thread with an initial comment.
*/
async createThread(e) {
let t = this.provider.createThread({
data: e.metadata
});
return t = this.provider.addComment(t.id, {
content: e.initialComment.body,
data: {
metadata: e.initialComment.metadata,
userId: this.userId
}
}), this.tiptapThreadToThreadData(t);
}
/**
* Adds a comment to a thread.
*/
async addComment(e) {
const t = this.provider.addComment(e.threadId, {
content: e.comment.body,
data: {
metadata: e.comment.metadata,
userId: this.userId
}
});
return this.tiptapCommentToCommentData(
t.comments[t.comments.length - 1]
);
}
/**
* Updates a comment in a thread.
*/
async updateComment(e) {
const t = this.provider.getThreadComment(
e.threadId,
e.commentId,
!0
);
if (!t)
throw new Error("Comment not found");
this.provider.updateComment(e.threadId, e.commentId, {
content: e.comment.body,
data: {
...t.data,
metadata: e.comment.metadata
}
});
}
tiptapCommentToCommentData(e) {
var a, s, n;
const t = [];
for (const h of ((a = e.data) == null ? void 0 : a.reactions) || []) {
const m = t.find(
(i) => i.emoji === h.emoji
);
m ? (m.userIds.push(h.userId), m.createdAt = new Date(
Math.min(m.createdAt.getTime(), h.createdAt)
)) : t.push({
emoji: h.emoji,
createdAt: new Date(h.createdAt),
userIds: [h.userId]
});
}
return {
type: "comment",
id: e.id,
body: e.content,
metadata: (s = e.data) == null ? void 0 : s.metadata,
userId: (n = e.data) == null ? void 0 : n.userId,
createdAt: new Date(e.createdAt),
updatedAt: new Date(e.updatedAt),
reactions: t
};
}
tiptapThreadToThreadData(e) {
var t;
return {
type: "thread",
id: e.id,
comments: e.comments.map(
(a) => this.tiptapCommentToCommentData(a)
),
resolved: !!e.resolvedAt,
metadata: (t = e.data) == null ? void 0 : t.metadata,
createdAt: new Date(e.createdAt),
updatedAt: new Date(e.updatedAt)
};
}
/**
* Deletes a comment from a thread.
*/
async deleteComment(e) {
this.provider.deleteComment(e.threadId, e.commentId);
}
/**
* Deletes a thread.
*/
async deleteThread(e) {
this.provider.deleteThread(e.threadId);
}
/**
* Marks a thread as resolved.
*/
async resolveThread(e) {
this.provider.updateThread(e.threadId, {
resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
});
}
/**
* Marks a thread as unresolved.
*/
async unresolveThread(e) {
this.provider.updateThread(e.threadId, {
resolvedAt: null
});
}
/**
* Adds a reaction to a comment.
*
* Auth: should be possible by anyone with comment access
*/
async addReaction(e) {
var a;
const t = this.provider.getThreadComment(
e.threadId,
e.commentId,
!0
);
if (!t)
throw new Error("Comment not found");
this.provider.updateComment(e.threadId, e.commentId, {
data: {
...t.data,
reactions: [
...((a = t.data) == null ? void 0 : a.reactions) || [],
{
emoji: e.emoji,
createdAt: Date.now(),
userId: this.userId
}
]
}
});
}
/**
* Deletes a reaction from a comment.
*
* Auth: should be possible by the reaction author
*/
async deleteReaction(e) {
var a;
const t = this.provider.getThreadComment(
e.threadId,
e.commentId,
!0
);
if (!t)
throw new Error("Comment not found");
this.provider.updateComment(e.threadId, e.commentId, {
data: {
...t.data,
reactions: (((a = t.data) == null ? void 0 : a.reactions) || []).filter(
(s) => s.emoji !== e.emoji && s.userId !== this.userId
)
}
});
}
getThread(e) {
const t = this.provider.getThread(e);
if (!t)
throw new Error("Thread not found");
return this.tiptapThreadToThreadData(t);
}
getThreads() {
return new Map(
this.provider.getThreads().map((e) => [e.id, this.tiptapThreadToThreadData(e)])
);
}
subscribe(e) {
const t = () => {
e(this.getThreads());
};
return this.provider.watchThreads(t), () => {
this.provider.unwatchThreads(t);
};
}
}
function g(d) {
const r = new T.Map();
if (r.set("id", d.id), r.set("userId", d.userId), r.set("createdAt", d.createdAt.getTime()), r.set("updatedAt", d.updatedAt.getTime()), d.deletedAt ? (r.set("deletedAt", d.deletedAt.getTime()), r.set("body", void 0)) : r.set("body", d.body), d.reactions.length > 0)
throw new Error("Reactions should be empty in commentToYMap");
return r.set("reactionsByUser", new T.Map()), r.set("metadata", d.metadata), r;
}
function y(d) {
var t;
const r = new T.Map();
r.set("id", d.id), r.set("createdAt", d.createdAt.getTime()), r.set("updatedAt", d.updatedAt.getTime());
const e = new T.Array();
return e.push(d.comments.map((a) => g(a))), r.set("comments", e), r.set("resolved", d.resolved), r.set("resolvedUpdatedAt", (t = d.resolvedUpdatedAt) == null ? void 0 : t.getTime()), r.set("resolvedBy", d.resolvedBy), r.set("metadata", d.metadata), r;
}
function C(d) {
return {
emoji: d.get("emoji"),
createdAt: new Date(d.get("createdAt")),
userId: d.get("userId")
};
}
function D(d) {
return [...d.values()].map(
(e) => C(e)
).reduce(
(e, t) => {
const a = e.find((s) => s.emoji === t.emoji);
return a ? (a.userIds.push(t.userId), a.createdAt = new Date(
Math.min(
a.createdAt.getTime(),
t.createdAt.getTime()
)
)) : e.push({
emoji: t.emoji,
createdAt: t.createdAt,
userIds: [t.userId]
}), e;
},
[]
);
}
function u(d) {
return {
type: "comment",
id: d.get("id"),
userId: d.get("userId"),
createdAt: new Date(d.get("createdAt")),
updatedAt: new Date(d.get("updatedAt")),
deletedAt: d.get("deletedAt") ? new Date(d.get("deletedAt")) : void 0,
reactions: D(d.get("reactionsByUser")),
metadata: d.get("metadata"),
body: d.get("body")
};
}
function c(d) {
return {
type: "thread",
id: d.get("id"),
createdAt: new Date(d.get("createdAt")),
updatedAt: new Date(d.get("updatedAt")),
comments: (d.get("comments") || []).map(
(r) => u(r)
),
resolved: d.get("resolved"),
resolvedUpdatedAt: new Date(d.get("resolvedUpdatedAt")),
resolvedBy: d.get("resolvedBy"),
metadata: d.get("metadata")
};
}
class f extends w {
constructor(r, e) {
super(e), this.threadsYMap = r;
}
// TODO: async / reactive interface?
getThread(r) {
const e = this.threadsYMap.get(r);
if (!e)
throw new Error("Thread not found");
return c(e);
}
getThreads() {
const r = /* @__PURE__ */ new Map();
return this.threadsYMap.forEach((e, t) => {
r.set(t, c(e));
}), r;
}
subscribe(r) {
const e = () => {
r(this.getThreads());
};
return this.threadsYMap.observeDeep(e), () => {
this.threadsYMap.unobserveDeep(e);
};
}
}
class Y extends f {
constructor(e, t, a, s) {
super(a, s);
o(this, "doRequest", async (e, t, a) => {
const s = await fetch(`${this.BASE_URL}${e}`, {
method: t,
body: JSON.stringify(a),
headers: {
"Content-Type": "application/json",
...this.headers
}
});
if (!s.ok)
throw new Error(`Failed to ${t} ${e}: ${s.statusText}`);
return s.json();
});
o(this, "addThreadToDocument", async (e) => {
const { threadId: t, ...a } = e;
return this.doRequest(`/${t}/addToDocument`, "POST", a);
});
o(this, "createThread", async (e) => this.doRequest("", "POST", e));
o(this, "addComment", (e) => {
const { threadId: t, ...a } = e;
return this.doRequest(`/${t}/comments`, "POST", a);
});
o(this, "updateComment", (e) => {
const { threadId: t, commentId: a, ...s } = e;
return this.doRequest(`/${t}/comments/${a}`, "PUT", s);
});
o(this, "deleteComment", (e) => {
const { threadId: t, commentId: a, ...s } = e;
return this.doRequest(
`/${t}/comments/${a}?soft=${!!s.softDelete}`,
"DELETE"
);
});
o(this, "deleteThread", (e) => this.doRequest(`/${e.threadId}`, "DELETE"));
o(this, "resolveThread", (e) => this.doRequest(`/${e.threadId}/resolve`, "POST"));
o(this, "unresolveThread", (e) => this.doRequest(`/${e.threadId}/unresolve`, "POST"));
o(this, "addReaction", (e) => {
const { threadId: t, commentId: a, ...s } = e;
return this.doRequest(
`/${t}/comments/${a}/reactions`,
"POST",
s
);
});
o(this, "deleteReaction", (e) => this.doRequest(
`/${e.threadId}/comments/${e.commentId}/reactions/${e.emoji}`,
"DELETE"
));
this.BASE_URL = e, this.headers = t;
}
}
class $ extends f {
constructor(e, t, a) {
super(t, a);
o(this, "transact", (e) => async (t) => this.threadsYMap.doc.transact(() => e(t)));
o(this, "createThread", this.transact(
(e) => {
if (!this.auth.canCreateThread())
throw new Error("Not authorized");
const t = /* @__PURE__ */ new Date(), a = {
type: "comment",
id: I(),
userId: this.userId,
createdAt: t,
updatedAt: t,
reactions: [],
metadata: e.initialComment.metadata,
body: e.initialComment.body
}, s = {
type: "thread",
id: I(),
createdAt: t,
updatedAt: t,
comments: [a],
resolved: !1,
metadata: e.metadata
};
return this.threadsYMap.set(s.id, y(s)), s;
}
));
// YjsThreadStore does not support addThreadToDocument
o(this, "addThreadToDocument");
o(this, "addComment", this.transact(
(e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
if (!this.auth.canAddComment(c(t)))
throw new Error("Not authorized");
const a = /* @__PURE__ */ new Date(), s = {
type: "comment",
id: I(),
userId: this.userId,
createdAt: a,
updatedAt: a,
deletedAt: void 0,
reactions: [],
metadata: e.comment.metadata,
body: e.comment.body
};
return t.get("comments").push([
g(s)
]), t.set("updatedAt", (/* @__PURE__ */ new Date()).getTime()), s;
}
));
o(this, "updateComment", this.transact(
(e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
const a = l(
t.get("comments"),
(n) => n.get("id") === e.commentId
);
if (a === -1)
throw new Error("Comment not found");
const s = t.get("comments").get(a);
if (!this.auth.canUpdateComment(u(s)))
throw new Error("Not authorized");
s.set("body", e.comment.body), s.set("updatedAt", (/* @__PURE__ */ new Date()).getTime()), s.set("metadata", e.comment.metadata);
}
));
o(this, "deleteComment", this.transact(
(e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
const a = l(
t.get("comments"),
(n) => n.get("id") === e.commentId
);
if (a === -1)
throw new Error("Comment not found");
const s = t.get("comments").get(a);
if (!this.auth.canDeleteComment(u(s)))
throw new Error("Not authorized");
if (s.get("deletedAt"))
throw new Error("Comment already deleted");
e.softDelete ? (s.set("deletedAt", (/* @__PURE__ */ new Date()).getTime()), s.set("body", void 0)) : t.get("comments").delete(a), t.get("comments").toArray().every((n) => n.get("deletedAt")) && (e.softDelete ? t.set("deletedAt", (/* @__PURE__ */ new Date()).getTime()) : this.threadsYMap.delete(e.threadId)), t.set("updatedAt", (/* @__PURE__ */ new Date()).getTime());
}
));
o(this, "deleteThread", this.transact((e) => {
if (!this.auth.canDeleteThread(
c(this.threadsYMap.get(e.threadId))
))
throw new Error("Not authorized");
this.threadsYMap.delete(e.threadId);
}));
o(this, "resolveThread", this.transact((e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
if (!this.auth.canResolveThread(c(t)))
throw new Error("Not authorized");
t.set("resolved", !0), t.set("resolvedUpdatedAt", (/* @__PURE__ */ new Date()).getTime()), t.set("resolvedBy", this.userId);
}));
o(this, "unresolveThread", this.transact((e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
if (!this.auth.canUnresolveThread(c(t)))
throw new Error("Not authorized");
t.set("resolved", !1), t.set("resolvedUpdatedAt", (/* @__PURE__ */ new Date()).getTime());
}));
o(this, "addReaction", this.transact(
(e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
const a = l(
t.get("comments"),
(i) => i.get("id") === e.commentId
);
if (a === -1)
throw new Error("Comment not found");
const s = t.get("comments").get(a);
if (!this.auth.canAddReaction(u(s), e.emoji))
throw new Error("Not authorized");
const n = /* @__PURE__ */ new Date(), h = `${this.userId}-${e.emoji}`, m = s.get("reactionsByUser");
if (!m.has(h)) {
const i = new T.Map();
i.set("emoji", e.emoji), i.set("createdAt", n.getTime()), i.set("userId", this.userId), m.set(h, i);
}
}
));
o(this, "deleteReaction", this.transact(
(e) => {
const t = this.threadsYMap.get(e.threadId);
if (!t)
throw new Error("Thread not found");
const a = l(
t.get("comments"),
(m) => m.get("id") === e.commentId
);
if (a === -1)
throw new Error("Comment not found");
const s = t.get("comments").get(a);
if (!this.auth.canDeleteReaction(u(s), e.emoji))
throw new Error("Not authorized");
const n = `${this.userId}-${e.emoji}`;
s.get("reactionsByUser").delete(n);
}
));
this.userId = e;
}
}
function l(d, r) {
for (let e = 0; e < d.length; e++)
if (r(d.get(e)))
return e;
return -1;
}
export {
b as DefaultThreadStoreAuth,
Y as RESTYjsThreadStore,
w as ThreadStore,
v as ThreadStoreAuth,
j as TiptapThreadStore,
$ as YjsThreadStore,
f as YjsThreadStoreBase
};
//# sourceMappingURL=comments.js.map