UNPKG

@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
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"), }; }