@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
124 lines (112 loc) • 3.81 kB
text/typescript
import * as Y from "yjs";
import { CommentData, CommentReactionData, ThreadData } from "../../types.js";
export function commentToYMap(comment: CommentData) {
const yMap = new Y.Map<any>();
yMap.set("id", comment.id);
yMap.set("userId", comment.userId);
yMap.set("createdAt", comment.createdAt.getTime());
yMap.set("updatedAt", comment.updatedAt.getTime());
if (comment.deletedAt) {
yMap.set("deletedAt", comment.deletedAt.getTime());
yMap.set("body", undefined);
} else {
yMap.set("body", comment.body);
}
if (comment.reactions.length > 0) {
throw new Error("Reactions should be empty in commentToYMap");
}
/**
* Reactions are stored in a map keyed by {userId-emoji},
* this makes it easy to add / remove reactions and in a way that works local-first.
* The cost is that "reading" the reactions is a bit more complex (see yMapToReactions).
*/
yMap.set("reactionsByUser", new Y.Map());
yMap.set("metadata", comment.metadata);
return yMap;
}
export function threadToYMap(thread: ThreadData) {
const yMap = new Y.Map();
yMap.set("id", thread.id);
yMap.set("createdAt", thread.createdAt.getTime());
yMap.set("updatedAt", thread.updatedAt.getTime());
const commentsArray = new Y.Array<Y.Map<any>>();
commentsArray.push(thread.comments.map((comment) => commentToYMap(comment)));
yMap.set("comments", commentsArray);
yMap.set("resolved", thread.resolved);
yMap.set("resolvedUpdatedAt", thread.resolvedUpdatedAt?.getTime());
yMap.set("resolvedBy", thread.resolvedBy);
yMap.set("metadata", thread.metadata);
return yMap;
}
type SingleUserCommentReactionData = {
emoji: string;
createdAt: Date;
userId: string;
};
export function yMapToReaction(
yMap: Y.Map<any>,
): SingleUserCommentReactionData {
return {
emoji: yMap.get("emoji"),
createdAt: new Date(yMap.get("createdAt")),
userId: yMap.get("userId"),
};
}
function yMapToReactions(yMap: Y.Map<any>): CommentReactionData[] {
const flatReactions = [...yMap.values()].map((reaction: Y.Map<any>) =>
yMapToReaction(reaction),
);
// combine reactions by the same emoji
return flatReactions.reduce(
(acc: CommentReactionData[], reaction: SingleUserCommentReactionData) => {
const existingReaction = acc.find((r) => r.emoji === reaction.emoji);
if (existingReaction) {
existingReaction.userIds.push(reaction.userId);
existingReaction.createdAt = new Date(
Math.min(
existingReaction.createdAt.getTime(),
reaction.createdAt.getTime(),
),
);
} else {
acc.push({
emoji: reaction.emoji,
createdAt: reaction.createdAt,
userIds: [reaction.userId],
});
}
return acc;
},
[] as CommentReactionData[],
);
}
export function yMapToComment(yMap: Y.Map<any>): CommentData {
return {
type: "comment",
id: yMap.get("id"),
userId: yMap.get("userId"),
createdAt: new Date(yMap.get("createdAt")),
updatedAt: new Date(yMap.get("updatedAt")),
deletedAt: yMap.get("deletedAt")
? new Date(yMap.get("deletedAt"))
: undefined,
reactions: yMapToReactions(yMap.get("reactionsByUser")),
metadata: yMap.get("metadata"),
body: yMap.get("body"),
};
}
export function yMapToThread(yMap: Y.Map<any>): ThreadData {
return {
type: "thread",
id: yMap.get("id"),
createdAt: new Date(yMap.get("createdAt")),
updatedAt: new Date(yMap.get("updatedAt")),
comments: ((yMap.get("comments") as Y.Array<Y.Map<any>>) || []).map(
(comment) => yMapToComment(comment),
),
resolved: yMap.get("resolved"),
resolvedUpdatedAt: new Date(yMap.get("resolvedUpdatedAt")),
resolvedBy: yMap.get("resolvedBy"),
metadata: yMap.get("metadata"),
};
}