UNPKG

@liveblocks/emails

Version:

A set of functions and utilities to make sending emails based on Liveblocks notification events easy. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.

1,489 lines (1,440 loc) 48 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class;// src/index.ts var _core = require('@liveblocks/core'); // src/version.ts var PKG_NAME = "@liveblocks/emails"; var PKG_VERSION = "3.19.2"; var PKG_FORMAT = "cjs"; // src/text-mention-notification.tsx // src/lexical-editor.ts var _yjs = require('yjs'); var Y = _interopRequireWildcard(_yjs); var Y2 = _interopRequireWildcard(_yjs); // src/lib/utils.ts var isString = (value) => { return typeof value === "string"; }; var isMentionNodeAttributeId = (value) => { return isString(value) && value.startsWith("in_"); }; var exists = (input) => { return input !== null && input !== void 0; }; // src/lexical-editor.ts function createSerializedLexicalMapNode(item) { const type = item.get("__type"); if (typeof type !== "string") { throw new Error( `Expected ${item.constructor.name} to include type attribute` ); } const attributes = Object.fromEntries(item.entries()); if (type === "linebreak") { return { type, attributes, group: "linebreak" }; } return { type, attributes, text: "", group: "text" }; } function createSerializedLexicalDecoratorNode(item) { const type = item.getAttribute("__type"); if (typeof type !== "string") { throw new Error( `Expected ${item.constructor.name} to include type attribute` ); } const attributes = item.getAttributes(); return { type, attributes, group: "decorator" }; } function createSerializedLexicalElementNode(item) { const type = item.getAttribute("__type"); if (typeof type !== "string") { throw new Error( `Expected ${item.constructor.name} to include type attribute` ); } const attributes = item.getAttributes(); let start = item._start; const children = []; while (start !== null) { if (start.deleted) { start = start.right; continue; } if (start.content instanceof Y.ContentType) { const content = start.content.type; if (content instanceof Y.XmlText) { children.push(createSerializedLexicalElementNode(content)); } else if (content instanceof Y.Map) { children.push(createSerializedLexicalMapNode(content)); } else if (content instanceof Y.XmlElement) { children.push( createSerializedLexicalDecoratorNode(content) ); } } else if (start.content instanceof Y.ContentString) { if (children.length > 0) { const last = children[children.length - 1]; if (last && last.group === "text") { last.text += start.content.str; } } } start = start.right; } return { type, attributes, children, group: "element" }; } function createSerializedLexicalRootNode(root) { try { const children = []; let start = root._start; while (start !== null && start !== void 0) { if (start.deleted) { start = start.right; continue; } if (start.content instanceof Y.ContentType) { const content = start.content.type; if (content instanceof Y.XmlText) { children.push(createSerializedLexicalElementNode(content)); } else if (content instanceof Y.XmlElement) { children.push( createSerializedLexicalDecoratorNode(content) ); } } start = start.right; } return { children, type: "root", attributes: root.getAttributes() }; } catch (err) { console.error(err); return { children: [], type: "root", attributes: root.getAttributes() }; } } function getSerializedLexicalState({ buffer, key }) { const update = new Uint8Array(buffer); const document = new Y.Doc(); Y.applyUpdate(document, update); const root = document.get(key, Y.XmlText); const state = createSerializedLexicalRootNode(root); document.destroy(); return state; } var isSerializedLineBreakNode = (node) => { return node.group === "linebreak"; }; var isSerializedElementNode = (node) => { return node.group === "element" && node.children !== void 0; }; var isEmptySerializedElementNode = (node) => { return isSerializedElementNode(node) && node.children.length === 0; }; var isMentionNodeType = (type) => { return type === "lb-mention"; }; var isMentionNodeAttributeType = (type) => { return isString(type) && type === "lb-mention"; }; var isSerializedMentionNode = (node) => { const attributes = node.attributes; return isMentionNodeType(node.type) && isMentionNodeAttributeType(attributes.__type) && isMentionNodeAttributeId(attributes.__id) && isString(attributes.__userId); }; var isGroupMentionNodeType = (type) => { return type === "lb-group-mention"; }; var isGroupMentionNodeAttributeType = (type) => { return isString(type) && type === "lb-group-mention"; }; var isSerializedGroupMentionNode = (node) => { const attributes = node.attributes; return isGroupMentionNodeType(node.type) && isGroupMentionNodeAttributeType(attributes.__type) && isMentionNodeAttributeId(attributes.__id) && isString(attributes.__groupId) && (attributes.__userIds === void 0 || Array.isArray(attributes.__userIds)); }; var isFlattenedLexicalElementNodeMarker = (node) => { return node.group === "element-marker"; }; var flattenLexicalTree = (nodes) => { let flattenNodes = []; for (const node of nodes) { if (["text", "linebreak", "decorator"].includes(node.group) || isEmptySerializedElementNode(node)) { flattenNodes = [...flattenNodes, node]; } else if (node.group === "element") { flattenNodes = [ ...flattenNodes, { group: "element-marker", marker: "start" }, ...flattenLexicalTree(node.children), { group: "element-marker", marker: "end" } ]; } } return flattenNodes; }; function findLexicalMentionNodeWithContext({ root, textMentionId }) { const nodes = flattenLexicalTree(root.children); let mentionNodeIndex = -1; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.group === "decorator" && (isSerializedMentionNode(node) || isSerializedGroupMentionNode(node)) && node.attributes.__id === textMentionId) { mentionNodeIndex = i; break; } } if (mentionNodeIndex === -1) { return null; } const mentionNode = nodes[mentionNodeIndex]; const beforeNodes = []; const afterNodes = []; for (let i = mentionNodeIndex - 1; i >= 0; i--) { const node = nodes[i]; if (isFlattenedLexicalElementNodeMarker(node) || isSerializedLineBreakNode(node)) { break; } if (node.group === "decorator" && !isMentionNodeType(node.type)) { break; } beforeNodes.unshift(node); } for (let i = mentionNodeIndex + 1; i < nodes.length; i++) { const node = nodes[i]; if (isFlattenedLexicalElementNodeMarker(node) || isSerializedLineBreakNode(node)) { break; } if (node.group === "decorator" && !isMentionNodeType(node.type)) { break; } afterNodes.push(node); } return { before: beforeNodes, after: afterNodes, mention: mentionNode }; } function getMentionDataFromLexicalNode(node) { if (isSerializedMentionNode(node)) { return { kind: "user", id: node.attributes.__userId }; } else if (isSerializedGroupMentionNode(node)) { return { kind: "group", id: node.attributes.__groupId, userIds: node.attributes.__userIds }; } _core.assertNever.call(void 0, node, "Unknown mention kind"); } // src/lib/batch-resolvers.ts function getResolvedForId(id, ids, results) { const index = ids.indexOf(id); return _optionalChain([results, 'optionalAccess', _ => _[index]]); } var BatchResolver = (_class = class { __init() {this.ids = /* @__PURE__ */ new Set()} __init2() {this.results = /* @__PURE__ */ new Map()} __init3() {this.isResolved = false} constructor(callback, missingCallbackWarning) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);_class.prototype.__init3.call(this); this.callback = callback; const { promise, resolve } = _core.Promise_withResolvers.call(void 0, ); this.promise = promise; this.resolvePromise = resolve; this.missingCallbackWarning = missingCallbackWarning; } /** * Add IDs to the batch and return a promise that resolves when the entire batch is resolved. * It can't be called after the batch is resolved. */ async get(ids) { if (this.isResolved) { throw new Error("Batch has already been resolved."); } ids.forEach((id) => this.ids.add(id)); await this.promise; return ids.map((id) => this.results.get(id)); } #resolveBatch() { this.isResolved = true; this.resolvePromise(); } /** * Resolve all the IDs in the batch. * It can only be called once. */ async resolve() { if (this.isResolved) { throw new Error("Batch has already been resolved."); } if (!this.callback) { _core.warnOnce.call(void 0, this.missingCallbackWarning); this.#resolveBatch(); return; } const ids = Array.from(this.ids); try { const results = this.callback ? await this.callback(ids) : void 0; if (results !== void 0) { if (!Array.isArray(results)) { throw new Error("Callback must return an array."); } else if (ids.length !== results.length) { throw new Error( `Callback must return an array of the same length as the number of provided items. Expected ${ids.length}, but got ${results.length}.` ); } } ids.forEach((id, index) => { this.results.set(id, _optionalChain([results, 'optionalAccess', _2 => _2[index]])); }); } catch (error) { this.#resolveBatch(); throw error; } this.#resolveBatch(); } }, _class); function createBatchUsersResolver({ resolveUsers, callerName }) { return new BatchResolver( resolveUsers ? (userIds) => resolveUsers({ userIds }) : void 0, `Set "resolveUsers" in "${callerName}" to specify users info` ); } function createBatchGroupsInfoResolver({ resolveGroupsInfo, callerName }) { return new BatchResolver( resolveGroupsInfo ? (groupIds) => resolveGroupsInfo({ groupIds }) : void 0, `Set "resolveGroupsInfo" in "${callerName}" to specify groups info` ); } // src/lib/css-properties.ts var VENDORS_PREFIXES = new RegExp(/^(webkit|moz|ms|o)-/); var UNITLESS_PROPERTIES = [ "animationIterationCount", "aspectRatio", "borderImageOutset", "borderImageSlice", "borderImageWidth", "boxFlex", "boxFlexGroup", "boxOrdinalGroup", "columnCount", "columns", "flex", "flexGrow", "flexPositive", "flexShrink", "flexNegative", "flexOrder", "gridArea", "gridRow", "gridRowEnd", "gridRowSpan", "gridRowStart", "gridColumn", "gridColumnEnd", "gridColumnSpan", "gridColumnStart", "fontWeight", "lineClamp", "lineHeight", "opacity", "order", "orphans", "scale", "tabSize", "widows", "zIndex", "zoom", "fillOpacity", "floodOpacity", "stopOpacity", "strokeDasharray", "strokeDashoffset", "strokeMiterlimit", "strokeOpacity", "strokeWidth", "MozAnimationIterationCount", "MozBoxFlex", "MozBoxFlexGroup", "MozLineClamp", "msAnimationIterationCount", "msFlex", "msZoom", "msFlexPositive", "msGridColumns", "msGridRows", "WebkitAnimationIterationCount", "WebkitBoxFlex", "WebKitBoxFlexGroup", "WebkitBoxOrdinalGroup", "WebkitColumnCount", "WebkitColumns", "WebkitFlex", "WebkitFlexGrow", "WebkitFlexPositive", "WebkitFlexShrink", "WebkitLineClamp" ]; function toInlineCSSString(styles) { const entries = Object.entries(styles); const inline = entries.map(([key, value]) => { if (value === null || typeof value === "boolean" || value === "" || typeof value === "undefined") { return ""; } let property = key.replace(/([A-Z])/g, "-$1").toLowerCase(); if (VENDORS_PREFIXES.test(property)) { property = `-${property}`; } if (typeof value === "number" && !UNITLESS_PROPERTIES.includes(key)) { return `${property}:${value}px;`; } return `${property}:${String(value).trim()};`; }).filter(Boolean).join(""); return inline; } // src/tiptap-editor.ts var _yprosemirror = require('y-prosemirror'); function getSerializedTiptapState({ buffer, key }) { const update = new Uint8Array(buffer); const document = new Y2.Doc(); Y2.applyUpdate(document, update); const fragment = document.getXmlFragment(key); const state = _yprosemirror.yXmlFragmentToProsemirrorJSON.call(void 0, fragment); document.destroy(); return state; } var isSerializedEmptyParagraphNode = (node) => { return node.type === "paragraph" && typeof node.content === "undefined"; }; var isSerializedHardBreakNode = (node) => { return node.type === "hardBreak" && typeof node.content === "undefined"; }; var isSerializedTextNode = (node) => { return node.type === "text"; }; var isSerializedMentionNode2 = (node) => { return node.type === "liveblocksMention" && isMentionNodeAttributeId(node.attrs.notificationId); }; var isSerializedGroupMentionNode2 = (node) => { return node.type === "liveblocksGroupMention" && isMentionNodeAttributeId(node.attrs.notificationId); }; var isSerializedParagraphNode = (node) => { return node.type === "paragraph" && typeof node.content !== "undefined"; }; var isFlattenedTiptapParagraphNodeMarker = (node) => { return node.type === "paragraph-marker"; }; var flattenTiptapTree = (nodes) => { let flattenNodes = []; for (const node of nodes) { if (isSerializedEmptyParagraphNode(node) || isSerializedHardBreakNode(node) || isSerializedTextNode(node) || isSerializedMentionNode2(node) || isSerializedGroupMentionNode2(node)) { flattenNodes = [...flattenNodes, node]; } else if (isSerializedParagraphNode(node)) { flattenNodes = [ ...flattenNodes, { type: "paragraph-marker", marker: "start" }, ...flattenTiptapTree(node.content), { type: "paragraph-marker", marker: "end" } ]; } } return flattenNodes; }; function findTiptapMentionNodeWithContext({ root, textMentionId }) { const nodes = flattenTiptapTree(root.content); let mentionNodeIndex = -1; for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (!isFlattenedTiptapParagraphNodeMarker(node) && (isSerializedMentionNode2(node) || isSerializedGroupMentionNode2(node)) && node.attrs.notificationId === textMentionId) { mentionNodeIndex = i; break; } } if (mentionNodeIndex === -1) { return null; } const mentionNode = nodes[mentionNodeIndex]; const beforeNodes = []; const afterNodes = []; for (let i = mentionNodeIndex - 1; i >= 0; i--) { const node = nodes[i]; if (isFlattenedTiptapParagraphNodeMarker(node) || isSerializedEmptyParagraphNode(node) || isSerializedHardBreakNode(node)) { break; } beforeNodes.unshift(node); } for (let i = mentionNodeIndex + 1; i < nodes.length; i++) { const node = nodes[i]; if (isFlattenedTiptapParagraphNodeMarker(node) || isSerializedEmptyParagraphNode(node) || isSerializedHardBreakNode(node)) { break; } afterNodes.push(node); } return { before: beforeNodes, after: afterNodes, mention: mentionNode }; } function deserializeGroupUserIds(userIds) { if (typeof userIds !== "string") { return void 0; } try { const parsedUserIds = JSON.parse(userIds); if (Array.isArray(parsedUserIds)) { return parsedUserIds; } return void 0; } catch (e) { return void 0; } } function getMentionDataFromTiptapNode(node) { if (isSerializedMentionNode2(node)) { return { kind: "user", id: node.attrs.id }; } else if (isSerializedGroupMentionNode2(node)) { return { kind: "group", id: node.attrs.id, userIds: deserializeGroupUserIds(node.attrs.userIds) }; } _core.assertNever.call(void 0, node, "Unknown mention kind"); } // src/liveblocks-text-editor.ts var baseLiveblocksTextEditorTextFormat = { bold: false, italic: false, strikethrough: false, code: false }; var IS_LEXICAL_BOLD = 1; var IS_LEXICAL_ITALIC = 1 << 1; var IS_LEXICAL_STRIKETHROUGH = 1 << 2; var IS_LEXICAL_CODE = 1 << 4; var transformLexicalTextNodeFormatBitwiseInteger = (node) => { const attributes = node.attributes; if ("__format" in attributes && typeof attributes.__format === "number") { const format = attributes.__format; return { bold: (format & IS_LEXICAL_BOLD) !== 0, italic: (format & IS_LEXICAL_ITALIC) !== 0, strikethrough: (format & IS_LEXICAL_STRIKETHROUGH) !== 0, code: (format & IS_LEXICAL_CODE) !== 0 }; } return baseLiveblocksTextEditorTextFormat; }; var transformLexicalMentionNodeWithContext = (mentionNodeWithContext) => { const textEditorNodes = []; const { before, after, mention } = mentionNodeWithContext; const transform = (nodes) => { for (const node of nodes) { if (node.group === "text") { const format = transformLexicalTextNodeFormatBitwiseInteger(node); textEditorNodes.push({ type: "text", text: node.text, ...format }); } else if (node.group === "decorator" && isSerializedMentionNode(node)) { textEditorNodes.push({ type: "mention", kind: "user", id: node.attributes.__userId }); } else if (node.group === "decorator" && isSerializedGroupMentionNode(node)) { textEditorNodes.push({ type: "mention", kind: "group", id: node.attributes.__groupId }); } } }; transform(before); textEditorNodes.push({ type: "mention", kind: mention.type === "lb-group-mention" ? "group" : "user", id: mention.type === "lb-group-mention" ? mention.attributes.__groupId : mention.attributes.__userId }); transform(after); return textEditorNodes; }; var hasTiptapSerializedTextNodeMark = (marks, type) => marks.findIndex((mark) => mark.type === type) !== -1; var transformTiptapTextNodeFormatMarks = (node) => { if (!node.marks) { return baseLiveblocksTextEditorTextFormat; } const marks = node.marks; return { bold: hasTiptapSerializedTextNodeMark(marks, "bold"), italic: hasTiptapSerializedTextNodeMark(marks, "italic"), strikethrough: hasTiptapSerializedTextNodeMark(marks, "strike"), code: hasTiptapSerializedTextNodeMark(marks, "code") }; }; var transformTiptapMentionNodeWithContext = (mentionNodeWithContext) => { const textEditorNodes = []; const { before, after, mention } = mentionNodeWithContext; const transform = (nodes) => { for (const node of nodes) { if (node.type === "text") { const format = transformTiptapTextNodeFormatMarks(node); textEditorNodes.push({ type: "text", text: node.text, ...format }); } else if (isSerializedMentionNode2(node)) { textEditorNodes.push({ type: "mention", kind: "user", id: node.attrs.id }); } else if (isSerializedGroupMentionNode2(node)) { textEditorNodes.push({ type: "mention", kind: "group", id: node.attrs.id }); } } }; transform(before); textEditorNodes.push({ type: "mention", kind: mention.type === "liveblocksGroupMention" ? "group" : "user", id: mention.attrs.id }); transform(after); return textEditorNodes; }; function transformAsLiveblocksTextEditorNodes(transformableMention) { switch (transformableMention.editor) { case "lexical": { return transformLexicalMentionNodeWithContext( transformableMention.mention ); } case "tiptap": { return transformTiptapMentionNodeWithContext( transformableMention.mention ); } } } var resolveMentionsInLiveblocksTextEditorNodes = async (nodes, resolveUsers, resolveGroupsInfo) => { const resolvedUsers = /* @__PURE__ */ new Map(); const resolvedGroupsInfo = /* @__PURE__ */ new Map(); if (!resolveUsers && !resolveGroupsInfo) { return { users: resolvedUsers, groups: resolvedGroupsInfo }; } const mentionedUserIds = /* @__PURE__ */ new Set(); const mentionedGroupIds = /* @__PURE__ */ new Set(); for (const node of nodes) { if (node.type === "mention") { if (node.kind === "user") { mentionedUserIds.add(node.id); } else if (node.kind === "group") { mentionedGroupIds.add(node.id); } } } const userIds = Array.from(mentionedUserIds); const groupIds = Array.from(mentionedGroupIds); const [users, groups] = await Promise.all([ resolveUsers && userIds.length > 0 ? resolveUsers({ userIds }) : void 0, resolveGroupsInfo && groupIds.length > 0 ? resolveGroupsInfo({ groupIds }) : void 0 ]); if (users) { for (const [index, userId] of userIds.entries()) { const user = users[index]; if (user) { resolvedUsers.set(userId, user); } } } if (groups) { for (const [index, groupId] of groupIds.entries()) { const group = groups[index]; if (group) { resolvedGroupsInfo.set(groupId, group); } } } return { users: resolvedUsers, groups: resolvedGroupsInfo }; }; // src/text-mention-content.tsx async function convertTextMentionContent(nodes, options) { const { users: resolvedUsers, groups: resolvedGroupsInfo } = await resolveMentionsInLiveblocksTextEditorNodes( nodes, _optionalChain([options, 'optionalAccess', _3 => _3.resolveUsers]), _optionalChain([options, 'optionalAccess', _4 => _4.resolveGroupsInfo]) ); const blocks = nodes.map((node, index) => { switch (node.type) { case "mention": { return options.elements.mention( { node, user: node.kind === "user" ? resolvedUsers.get(node.id) : void 0, group: node.kind === "group" ? resolvedGroupsInfo.get(node.id) : void 0 }, index ); } case "text": { return options.elements.text({ node }, index); } } }); return options.elements.container({ children: blocks }); } // src/text-mention-notification.tsx var _jsxruntime = require('react/jsx-runtime'); var extractTextMentionNotificationData = async ({ client, event }) => { const { roomId, userId, inboxNotificationId } = event.data; const [room, inboxNotification] = await Promise.all([ client.getRoom(roomId), client.getInboxNotification({ inboxNotificationId, userId }) ]); if (inboxNotification.kind !== "textMention") { console.warn('Inbox notification is not of kind "textMention"'); return null; } const isUnread = inboxNotification.readAt === null || inboxNotification.notifiedAt > inboxNotification.readAt; if (!isUnread) { return null; } const textEditor = room.experimental_textEditor; if (!textEditor) { console.warn(`Room "${room.id}" does not a text editor associated with it`); return null; } const mentionCreatedAt = inboxNotification.notifiedAt; const mentionAuthorUserId = inboxNotification.createdBy; const buffer = await client.getYjsDocumentAsBinaryUpdate(roomId); const editorKey = textEditor.rootKey; const key = Array.isArray(editorKey) ? editorKey[0] : editorKey; switch (textEditor.type) { case "lexical": { const state = getSerializedLexicalState({ buffer, key }); const mentionNodeWithContext = findLexicalMentionNodeWithContext({ root: state, textMentionId: inboxNotification.mentionId }); if (mentionNodeWithContext === null) { return null; } const mentionData = getMentionDataFromLexicalNode( mentionNodeWithContext.mention ); return { editor: "lexical", mentionNodeWithContext, mentionData, createdAt: mentionCreatedAt, createdBy: mentionAuthorUserId }; } case "tiptap": { const state = getSerializedTiptapState({ buffer, key }); const mentionNodeWithContext = findTiptapMentionNodeWithContext({ root: state, textMentionId: inboxNotification.mentionId }); if (mentionNodeWithContext === null) { return null; } const mentionData = getMentionDataFromTiptapNode( mentionNodeWithContext.mention ); return { editor: "tiptap", mentionNodeWithContext, mentionData, createdAt: mentionCreatedAt, createdBy: mentionAuthorUserId }; } } }; async function prepareTextMentionNotificationEmail(client, event, options, elements, callerName) { const { roomId, mentionId } = event.data; const data = await extractTextMentionNotificationData({ client, event }); if (data === null) { return null; } const roomInfo = options.resolveRoomInfo ? await options.resolveRoomInfo({ roomId: event.data.roomId }) : void 0; const resolvedRoomInfo = { ...roomInfo, name: _nullishCoalesce(_optionalChain([roomInfo, 'optionalAccess', _5 => _5.name]), () => ( event.data.roomId)) }; const batchUsersResolver = createBatchUsersResolver({ resolveUsers: options.resolveUsers, callerName }); const batchGroupsInfoResolver = createBatchGroupsInfoResolver({ resolveGroupsInfo: options.resolveGroupsInfo, callerName }); const authorsIds = [data.createdBy]; const authorsInfoPromise = batchUsersResolver.get(authorsIds); let textEditorNodes = []; switch (data.editor) { case "lexical": { textEditorNodes = transformAsLiveblocksTextEditorNodes({ editor: "lexical", mention: data.mentionNodeWithContext }); break; } case "tiptap": { textEditorNodes = transformAsLiveblocksTextEditorNodes({ editor: "tiptap", mention: data.mentionNodeWithContext }); break; } } const contentPromise = convertTextMentionContent( textEditorNodes, { resolveUsers: ({ userIds }) => batchUsersResolver.get(userIds), resolveGroupsInfo: ({ groupIds }) => batchGroupsInfoResolver.get(groupIds), elements } ); await batchUsersResolver.resolve(); await batchGroupsInfoResolver.resolve(); const [authorsInfo, content] = await Promise.all([ authorsInfoPromise, contentPromise ]); const authorInfo = getResolvedForId(data.createdBy, authorsIds, authorsInfo); return { mention: { ...data.mentionData, textMentionId: mentionId, roomId, author: { id: data.createdBy, info: _nullishCoalesce(authorInfo, () => ( { name: data.createdBy })) }, content, createdAt: data.createdAt }, roomInfo: resolvedRoomInfo }; } var baseComponents = { Container: ({ children }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { children }), Mention: ({ element, user, group }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { "data-mention": true, children: [ _core.MENTION_CHARACTER, _nullishCoalesce(_nullishCoalesce(_optionalChain([user, 'optionalAccess', _6 => _6.name]), () => ( _optionalChain([group, 'optionalAccess', _7 => _7.name]))), () => ( element.id)) ] }), Text: ({ element }) => { let children = element.text; if (element.bold) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "strong", { children }); } if (element.italic) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "em", { children }); } if (element.strikethrough) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "s", { children }); } if (element.code) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "code", { children }); } return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children }); } }; async function prepareTextMentionNotificationEmailAsReact(client, event, options = {}) { const Components = { ...baseComponents, ...options.components }; const data = await prepareTextMentionNotificationEmail( client, event, { resolveRoomInfo: options.resolveRoomInfo, resolveUsers: options.resolveUsers }, { container: ({ children }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Container, { children }, "lb-text-editor-container"), mention: ({ node, user }, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Mention, { element: node, user }, `lb-text-editor-mention-${index}` ), text: ({ node }, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Text, { element: node }, `lb-text-editor-text-${index}`) }, "prepareTextMentionNotificationEmailAsReact" ); if (data === null) { return null; } return data; } var baseStyles = { container: { fontSize: "14px" }, strong: { fontWeight: 500 }, code: { fontFamily: 'ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Mono", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Consolas", "Courier New", monospace', backgroundColor: "rgba(0,0,0,0.05)", border: "solid 1px rgba(0,0,0,0.1)", borderRadius: "4px" }, mention: { color: "blue" } }; async function prepareTextMentionNotificationEmailAsHtml(client, event, options = {}) { const styles = { ...baseStyles, ...options.styles }; const data = await prepareTextMentionNotificationEmail( client, event, { resolveRoomInfo: options.resolveRoomInfo, resolveUsers: options.resolveUsers }, { container: ({ children }) => { const content = [ // prettier-ignore _core.html`<div style="${toInlineCSSString(styles.container)}">${_core.htmlSafe.call(void 0, children.join(""))}</div>` ]; return content.join("\n"); }, mention: ({ node, user, group }) => { return _core.html`<span data-mention style="${toInlineCSSString(styles.mention)}">${_core.MENTION_CHARACTER}${_optionalChain([user, 'optionalAccess', _8 => _8.name]) ? _core.html`${_optionalChain([user, 'optionalAccess', _9 => _9.name])}` : _optionalChain([group, 'optionalAccess', _10 => _10.name]) ? _core.html`${_optionalChain([group, 'optionalAccess', _11 => _11.name])}` : node.id}</span>`; }, text: ({ node }) => { let children = node.text; if (!children) { return _core.html`${children}`; } if (node.bold) { children = _core.html`<strong style="${toInlineCSSString(styles.strong)}">${children}</strong>`; } if (node.italic) { children = _core.html`<em>${children}</em>`; } if (node.strikethrough) { children = _core.html`<s>${children}</s>`; } if (node.code) { children = _core.html`<code style="${toInlineCSSString(styles.code)}">${children}</code>`; } return _core.html`${children}`; } }, "prepareTextMentionNotificationEmailAsHtml" ); if (data === null) { return null; } return data; } // src/thread-notification.tsx // src/comment-body.tsx async function convertCommentBody(body, options) { const { users: resolvedUsers, groups: resolvedGroupsInfo } = await _core.resolveMentionsInCommentBody.call(void 0, body, _optionalChain([options, 'optionalAccess', _12 => _12.resolveUsers]), _optionalChain([options, 'optionalAccess', _13 => _13.resolveGroupsInfo]) ); const blocks = body.content.map((block, index) => { switch (block.type) { case "paragraph": { const children = block.children.map((inline, inlineIndex) => { if (_core.isCommentBodyMention.call(void 0, inline)) { return options.elements.mention( { element: inline, user: inline.kind === "user" ? resolvedUsers.get(inline.id) : void 0, group: inline.kind === "group" ? resolvedGroupsInfo.get(inline.id) : void 0 }, inlineIndex ); } if (_core.isCommentBodyLink.call(void 0, inline)) { const href = _core.sanitizeUrl.call(void 0, inline.url); if (href === null) { return options.elements.text( { element: { text: _nullishCoalesce(inline.text, () => ( inline.url)) } }, inlineIndex ); } return options.elements.link( { element: inline, href }, inlineIndex ); } if (_core.isCommentBodyText.call(void 0, inline)) { return options.elements.text({ element: inline }, inlineIndex); } return null; }).filter(exists); return options.elements.paragraph( { element: block, children }, index ); } default: console.warn( `Unsupported comment body block type: "${JSON.stringify(block.type)}"` ); return null; } }).filter(exists); return options.elements.container({ children: blocks }); } // src/comment-with-body.ts var isCommentDataWithBody = (comment) => { return comment.body !== void 0 && comment.deletedAt === void 0; }; function filterCommentsWithBody(comments) { const commentsWithBody = []; for (const comment of comments) { if (isCommentDataWithBody(comment)) { commentsWithBody.push(comment); } } return commentsWithBody; } // src/thread-notification.tsx var getUnreadComments = ({ comments, inboxNotification, notificationTriggerAt, userId }) => { const commentsWithBody = filterCommentsWithBody(comments); const notAuthoredComments = commentsWithBody.filter( (c) => c.userId !== userId ); const readAt = inboxNotification.readAt; return notAuthoredComments.filter((c) => { if (readAt !== null) { return c.createdAt > readAt && c.createdAt >= notificationTriggerAt && c.createdAt <= inboxNotification.notifiedAt; } return c.createdAt >= notificationTriggerAt && c.createdAt <= inboxNotification.notifiedAt; }); }; async function getAllUserGroups(client, userId) { const groups = /* @__PURE__ */ new Map(); let cursor = void 0; while (true) { const { nextCursor, data } = await client.getUserGroups({ userId, startingAfter: cursor }); for (const group of data) { groups.set(group.id, group); } if (!nextCursor) { break; } cursor = nextCursor; } return groups; } var getLastUnreadCommentWithMention = ({ comments, groups, mentionedUserId }) => { if (!comments.length) { return null; } for (let i = comments.length - 1; i >= 0; i--) { const comment = comments[i]; if (comment.userId === mentionedUserId) { continue; } const mentions = _core.getMentionsFromCommentBody.call(void 0, comment.body); for (const mention of mentions) { if (mention.kind === "user" && mention.id === mentionedUserId) { return comment; } if (mention.kind === "group" && _optionalChain([mention, 'access', _14 => _14.userIds, 'optionalAccess', _15 => _15.includes, 'call', _16 => _16(mentionedUserId)])) { return comment; } if (mention.kind === "group" && mention.userIds === void 0) { const group = groups.get(mention.id); if (_optionalChain([group, 'optionalAccess', _17 => _17.members, 'access', _18 => _18.some, 'call', _19 => _19((member) => member.id === mentionedUserId)])) { return comment; } } } } return null; }; var extractThreadNotificationData = async ({ client, event }) => { const { threadId, roomId, userId, inboxNotificationId } = event.data; const [thread, inboxNotification] = await Promise.all([ client.getThread({ roomId, threadId }), client.getInboxNotification({ inboxNotificationId, userId }) ]); const notificationTriggerAt = new Date(event.data.triggeredAt); const unreadComments = getUnreadComments({ comments: thread.comments, inboxNotification, userId, notificationTriggerAt }); if (unreadComments.length <= 0) { return null; } const userGroups = await getAllUserGroups(client, userId); const lastUnreadCommentWithMention = getLastUnreadCommentWithMention({ comments: unreadComments, groups: userGroups, mentionedUserId: userId }); if (lastUnreadCommentWithMention !== null) { return { type: "unreadMention", comment: lastUnreadCommentWithMention }; } return { type: "unreadReplies", comments: unreadComments }; }; function generateCommentUrl({ roomUrl, commentId }) { if (!roomUrl) { return; } return _core.generateUrl.call(void 0, roomUrl, void 0, commentId); } async function prepareThreadNotificationEmail(client, event, options, elements, callerName) { const data = await extractThreadNotificationData({ client, event }); if (data === null) { return null; } const roomInfo = options.resolveRoomInfo ? await options.resolveRoomInfo({ roomId: event.data.roomId }) : void 0; const resolvedRoomInfo = { ...roomInfo, name: _nullishCoalesce(_optionalChain([roomInfo, 'optionalAccess', _20 => _20.name]), () => ( event.data.roomId)) }; const batchUsersResolver = createBatchUsersResolver({ resolveUsers: options.resolveUsers, callerName }); const batchGroupsInfoResolver = createBatchGroupsInfoResolver({ resolveGroupsInfo: options.resolveGroupsInfo, callerName }); switch (data.type) { case "unreadMention": { const { comment } = data; const authorsIds = [comment.userId]; const authorsInfoPromise = batchUsersResolver.get(authorsIds); const commentBodyPromise = convertCommentBody(comment.body, { resolveUsers: ({ userIds }) => batchUsersResolver.get(userIds), resolveGroupsInfo: ({ groupIds }) => batchGroupsInfoResolver.get(groupIds), elements }); await batchUsersResolver.resolve(); await batchGroupsInfoResolver.resolve(); const [authorsInfo, commentBody] = await Promise.all([ authorsInfoPromise, commentBodyPromise ]); const authorInfo = getResolvedForId( comment.userId, authorsIds, authorsInfo ); const url = _optionalChain([roomInfo, 'optionalAccess', _21 => _21.url]) ? generateCommentUrl({ roomUrl: _optionalChain([roomInfo, 'optionalAccess', _22 => _22.url]), commentId: comment.id }) : void 0; return { type: "unreadMention", comment: { id: comment.id, threadId: comment.threadId, roomId: comment.roomId, author: { id: comment.userId, info: _nullishCoalesce(authorInfo, () => ( { name: comment.userId })) }, createdAt: comment.createdAt, url, body: commentBody }, roomInfo: resolvedRoomInfo }; } case "unreadReplies": { const { comments } = data; const authorsIds = comments.map((c) => c.userId); const authorsInfoPromise = batchUsersResolver.get(authorsIds); const commentBodiesPromises = comments.map( (c) => convertCommentBody(c.body, { resolveUsers: ({ userIds }) => batchUsersResolver.get(userIds), resolveGroupsInfo: ({ groupIds }) => batchGroupsInfoResolver.get(groupIds), elements }) ); await batchUsersResolver.resolve(); await batchGroupsInfoResolver.resolve(); const [authorsInfo, ...commentBodies] = await Promise.all([ authorsInfoPromise, ...commentBodiesPromises ]); return { type: "unreadReplies", comments: comments.map((comment, index) => { const authorInfo = getResolvedForId( comment.userId, authorsIds, authorsInfo ); const commentBody = commentBodies[index]; const url = generateCommentUrl({ roomUrl: _optionalChain([roomInfo, 'optionalAccess', _23 => _23.url]), commentId: comment.id }); return { id: comment.id, threadId: comment.threadId, roomId: comment.roomId, author: { id: comment.userId, info: _nullishCoalesce(authorInfo, () => ( { name: comment.userId })) }, createdAt: comment.createdAt, url, body: commentBody }; }), roomInfo: resolvedRoomInfo }; } } } var baseStyles2 = { paragraph: { fontSize: "14px" }, strong: { fontWeight: 500 }, code: { fontFamily: 'ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Mono", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Consolas", "Courier New", monospace', backgroundColor: "rgba(0,0,0,0.05)", border: "solid 1px rgba(0,0,0,0.1)", borderRadius: "4px" }, mention: { color: "blue" }, link: { textDecoration: "underline" } }; async function prepareThreadNotificationEmailAsHtml(client, event, options = {}) { const styles = { ...baseStyles2, ..._optionalChain([options, 'optionalAccess', _24 => _24.styles]) }; const data = await prepareThreadNotificationEmail( client, event, { resolveUsers: options.resolveUsers, resolveGroupsInfo: options.resolveGroupsInfo, resolveRoomInfo: options.resolveRoomInfo }, { container: ({ children }) => children.join("\n"), paragraph: ({ children }) => { const unsafe = children.join(""); return unsafe ? _core.html`<p style="${toInlineCSSString(styles.paragraph)}">${_core.htmlSafe.call(void 0, unsafe)}</p>` : unsafe; }, text: ({ element }) => { let children = element.text; if (!children) { return _core.html`${children}`; } if (element.bold) { children = _core.html`<strong style="${toInlineCSSString(styles.strong)}">${children}</strong>`; } if (element.italic) { children = _core.html`<em>${children}</em>`; } if (element.strikethrough) { children = _core.html`<s>${children}</s>`; } if (element.code) { children = _core.html`<code style="${toInlineCSSString(styles.code)}">${children}</code>`; } return _core.html`${children}`; }, link: ({ element, href }) => { return _core.html`<a href="${href}" target="_blank" rel="noopener noreferrer" style="${toInlineCSSString(styles.link)}">${element.text ? _core.html`${element.text}` : element.url}</a>`; }, mention: ({ element, user, group }) => { return _core.html`<span data-mention style="${toInlineCSSString(styles.mention)}">${_core.MENTION_CHARACTER}${_optionalChain([user, 'optionalAccess', _25 => _25.name]) ? _core.html`${_optionalChain([user, 'optionalAccess', _26 => _26.name])}` : _optionalChain([group, 'optionalAccess', _27 => _27.name]) ? _core.html`${_optionalChain([group, 'optionalAccess', _28 => _28.name])}` : element.id}</span>`; } }, "prepareThreadNotificationEmailAsHtml" ); if (data === null) { return null; } return data; } var baseComponents2 = { Container: ({ children }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { children }), Paragraph: ({ children }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "p", { children }), Text: ({ element }) => { let children = element.text; if (element.bold) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "strong", { children }); } if (element.italic) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "em", { children }); } if (element.strikethrough) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "s", { children }); } if (element.code) { children = /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "code", { children }); } return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children }); }, Link: ({ element, href }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "a", { href, target: "_blank", rel: "noopener noreferrer", children: _nullishCoalesce(element.text, () => ( element.url)) }), Mention: ({ element, user, group }) => /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { "data-mention": true, children: [ _core.MENTION_CHARACTER, _nullishCoalesce(_nullishCoalesce(_optionalChain([user, 'optionalAccess', _29 => _29.name]), () => ( _optionalChain([group, 'optionalAccess', _30 => _30.name]))), () => ( element.id)) ] }) }; async function prepareThreadNotificationEmailAsReact(client, event, options = {}) { const Components = { ...baseComponents2, ..._optionalChain([options, 'optionalAccess', _31 => _31.components]) }; const data = await prepareThreadNotificationEmail( client, event, { resolveUsers: options.resolveUsers, resolveGroupsInfo: options.resolveGroupsInfo, resolveRoomInfo: options.resolveRoomInfo }, { container: ({ children }) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Container, { children }, "lb-comment-body-container"), paragraph: ({ children }, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Paragraph, { children }, `lb-comment-body-paragraph-${index}`), text: ({ element }, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Text, { element }, `lb-comment-body-text-${index}` ), link: ({ element, href }, index) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Link, { element, href }, `lb-comment-body-link-${index}` ), mention: ({ element, user, group }, index) => element.id ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, Components.Mention, { element, user, group }, `lb-comment-body-mention-${index}` ) : null }, "prepareThreadNotificationEmailAsReact" ); if (data === null) { return null; } return data; } // src/index.ts _core.detectDupes.call(void 0, PKG_NAME, PKG_VERSION, PKG_FORMAT); exports.prepareTextMentionNotificationEmailAsHtml = prepareTextMentionNotificationEmailAsHtml; exports.prepareTextMentionNotificationEmailAsReact = prepareTextMentionNotificationEmailAsReact; exports.prepareThreadNotificationEmailAsHtml = prepareThreadNotificationEmailAsHtml; exports.prepareThreadNotificationEmailAsReact = prepareThreadNotificationEmailAsReact; //# sourceMappingURL=index.cjs.map