@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
126 lines (114 loc) • 4.01 kB
text/typescript
import * as Y from "@y/y";
import type {
CommentData,
CommentReactionData,
ThreadData,
} from "../../comments/types.js";
export function commentToYType(comment: CommentData) {
const yType = new Y.Type();
yType.setAttr("id", comment.id);
yType.setAttr("userId", comment.userId);
yType.setAttr("createdAt", comment.createdAt.getTime());
yType.setAttr("updatedAt", comment.updatedAt.getTime());
if (comment.deletedAt) {
yType.setAttr("deletedAt", comment.deletedAt.getTime());
yType.setAttr("body", undefined);
} else {
yType.setAttr("body", comment.body);
}
if (comment.reactions.length > 0) {
throw new Error("Reactions should be empty in commentToYType");
}
/**
* 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 yTypeToReactions).
*/
yType.setAttr("reactionsByUser", new Y.Type());
yType.setAttr("metadata", comment.metadata);
return yType;
}
export function threadToYType(thread: ThreadData) {
const yType = new Y.Type();
yType.setAttr("id", thread.id);
yType.setAttr("createdAt", thread.createdAt.getTime());
yType.setAttr("updatedAt", thread.updatedAt.getTime());
const commentsType = new Y.Type();
commentsType.push(thread.comments.map((comment) => commentToYType(comment)));
yType.setAttr("comments", commentsType);
yType.setAttr("resolved", thread.resolved);
yType.setAttr("resolvedUpdatedAt", thread.resolvedUpdatedAt?.getTime());
yType.setAttr("resolvedBy", thread.resolvedBy);
yType.setAttr("metadata", thread.metadata);
return yType;
}
type SingleUserCommentReactionData = {
emoji: string;
createdAt: Date;
userId: string;
};
export function yTypeToReaction(yType: Y.Type): SingleUserCommentReactionData {
return {
emoji: yType.getAttr("emoji"),
createdAt: new Date(yType.getAttr("createdAt")),
userId: yType.getAttr("userId"),
};
}
function yTypeToReactions(yType: Y.Type): CommentReactionData[] {
const flatReactions = [...yType.attrValues()].map((reaction: Y.Type) =>
yTypeToReaction(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 yTypeToComment(yType: Y.Type): CommentData {
return {
type: "comment",
id: yType.getAttr("id"),
userId: yType.getAttr("userId"),
createdAt: new Date(yType.getAttr("createdAt")),
updatedAt: new Date(yType.getAttr("updatedAt")),
deletedAt: yType.getAttr("deletedAt")
? new Date(yType.getAttr("deletedAt"))
: undefined,
reactions: yTypeToReactions(yType.getAttr("reactionsByUser")),
metadata: yType.getAttr("metadata"),
body: yType.getAttr("body"),
};
}
export function yTypeToThread(yType: Y.Type): ThreadData {
return {
type: "thread",
id: yType.getAttr("id"),
createdAt: new Date(yType.getAttr("createdAt")),
updatedAt: new Date(yType.getAttr("updatedAt")),
comments: ((yType.getAttr("comments") as Y.Type)?.toArray() || []).map(
(comment) => yTypeToComment(comment as Y.Type),
),
resolved: yType.getAttr("resolved"),
resolvedUpdatedAt: new Date(yType.getAttr("resolvedUpdatedAt")),
resolvedBy: yType.getAttr("resolvedBy"),
metadata: yType.getAttr("metadata"),
};
}