@liveblocks/emails 0.0.1 → 2.9.2-emails1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,638 @@
1
+ // src/index.ts
2
+ import { detectDupes } from "@liveblocks/core";
3
+
4
+ // src/version.ts
5
+ var PKG_NAME = "@liveblocks/emails";
6
+ var PKG_VERSION = "2.9.2-emails1";
7
+ var PKG_FORMAT = "esm";
8
+
9
+ // src/thread-notification.tsx
10
+ import {
11
+ generateCommentUrl,
12
+ getMentionedIdsFromCommentBody
13
+ } from "@liveblocks/core";
14
+
15
+ // src/comment-body.tsx
16
+ import {
17
+ html,
18
+ htmlSafe,
19
+ isCommentBodyLink,
20
+ isCommentBodyMention,
21
+ isCommentBodyText,
22
+ resolveUsersInCommentBody,
23
+ stringifyCommentBody,
24
+ toAbsoluteUrl
25
+ } from "@liveblocks/core";
26
+ import React from "react";
27
+
28
+ // src/lib/css-properties.ts
29
+ var VENDORS_PREFIXES = new RegExp(/^(webkit|moz|ms|o)-/);
30
+ var UNITLESS_PROPERTIES = [
31
+ "animationIterationCount",
32
+ "aspectRatio",
33
+ "borderImageOutset",
34
+ "borderImageSlice",
35
+ "borderImageWidth",
36
+ "boxFlex",
37
+ "boxFlexGroup",
38
+ "boxOrdinalGroup",
39
+ "columnCount",
40
+ "columns",
41
+ "flex",
42
+ "flexGrow",
43
+ "flexPositive",
44
+ "flexShrink",
45
+ "flexNegative",
46
+ "flexOrder",
47
+ "gridArea",
48
+ "gridRow",
49
+ "gridRowEnd",
50
+ "gridRowSpan",
51
+ "gridRowStart",
52
+ "gridColumn",
53
+ "gridColumnEnd",
54
+ "gridColumnSpan",
55
+ "gridColumnStart",
56
+ "fontWeight",
57
+ "lineClamp",
58
+ "lineHeight",
59
+ "opacity",
60
+ "order",
61
+ "orphans",
62
+ "scale",
63
+ "tabSize",
64
+ "widows",
65
+ "zIndex",
66
+ "zoom",
67
+ "fillOpacity",
68
+ "floodOpacity",
69
+ "stopOpacity",
70
+ "strokeDasharray",
71
+ "strokeDashoffset",
72
+ "strokeMiterlimit",
73
+ "strokeOpacity",
74
+ "strokeWidth",
75
+ "MozAnimationIterationCount",
76
+ "MozBoxFlex",
77
+ "MozBoxFlexGroup",
78
+ "MozLineClamp",
79
+ "msAnimationIterationCount",
80
+ "msFlex",
81
+ "msZoom",
82
+ "msFlexPositive",
83
+ "msGridColumns",
84
+ "msGridRows",
85
+ "WebkitAnimationIterationCount",
86
+ "WebkitBoxFlex",
87
+ "WebKitBoxFlexGroup",
88
+ "WebkitBoxOrdinalGroup",
89
+ "WebkitColumnCount",
90
+ "WebkitColumns",
91
+ "WebkitFlex",
92
+ "WebkitFlexGrow",
93
+ "WebkitFlexPositive",
94
+ "WebkitFlexShrink",
95
+ "WebkitLineClamp"
96
+ ];
97
+ function toInlineCSSString(styles) {
98
+ const entries = Object.entries(styles);
99
+ const inline = entries.map(([key, value]) => {
100
+ if (value === null || typeof value === "boolean" || value === "" || typeof value === "undefined") {
101
+ return "";
102
+ }
103
+ let property = key.replace(/([A-Z])/g, "-$1").toLowerCase();
104
+ if (VENDORS_PREFIXES.test(property)) {
105
+ property = `-${property}`;
106
+ }
107
+ if (typeof value === "number" && !UNITLESS_PROPERTIES.includes(key)) {
108
+ return `${property}:${value}px;`;
109
+ }
110
+ return `${property}:${String(value).trim()};`;
111
+ }).filter(Boolean).join("");
112
+ return inline;
113
+ }
114
+
115
+ // src/comment-body.tsx
116
+ var baseComponents = {
117
+ Container: ({ children }) => /* @__PURE__ */ React.createElement("div", null, children),
118
+ Paragraph: ({ children }) => /* @__PURE__ */ React.createElement("p", null, children),
119
+ Text: ({ element }) => {
120
+ let children = element.text;
121
+ if (element.bold) {
122
+ children = /* @__PURE__ */ React.createElement("strong", null, children);
123
+ }
124
+ if (element.italic) {
125
+ children = /* @__PURE__ */ React.createElement("em", null, children);
126
+ }
127
+ if (element.strikethrough) {
128
+ children = /* @__PURE__ */ React.createElement("s", null, children);
129
+ }
130
+ if (element.code) {
131
+ children = /* @__PURE__ */ React.createElement("code", null, children);
132
+ }
133
+ return /* @__PURE__ */ React.createElement("span", null, children);
134
+ },
135
+ Link: ({ element, href }) => /* @__PURE__ */ React.createElement("a", { href, target: "_blank", rel: "noopener noreferrer" }, element.text ?? element.url),
136
+ Mention: ({ element, user }) => /* @__PURE__ */ React.createElement("span", { "data-mention": true }, "@", user?.name ?? element.id)
137
+ };
138
+ async function convertCommentBodyAsReact(body, options) {
139
+ const Components = {
140
+ ...baseComponents,
141
+ ...options?.components
142
+ };
143
+ const resolvedUsers = await resolveUsersInCommentBody(
144
+ body,
145
+ options?.resolveUsers
146
+ );
147
+ const blocks = body.content.map((block, index) => {
148
+ switch (block.type) {
149
+ case "paragraph": {
150
+ const children = block.children.map((inline, inlineIndex) => {
151
+ if (isCommentBodyMention(inline)) {
152
+ return inline.id ? /* @__PURE__ */ React.createElement(
153
+ Components.Mention,
154
+ {
155
+ key: `lb-comment-body-mention-${inlineIndex}`,
156
+ element: inline,
157
+ user: resolvedUsers.get(inline.id)
158
+ }
159
+ ) : null;
160
+ }
161
+ if (isCommentBodyLink(inline)) {
162
+ const href = toAbsoluteUrl(inline.url) ?? inline.url;
163
+ return /* @__PURE__ */ React.createElement(
164
+ Components.Link,
165
+ {
166
+ key: `lb-comment-body-link-${inlineIndex}`,
167
+ element: inline,
168
+ href
169
+ }
170
+ );
171
+ }
172
+ if (isCommentBodyText(inline)) {
173
+ return /* @__PURE__ */ React.createElement(
174
+ Components.Text,
175
+ {
176
+ key: `lb-comment-body-text-${inlineIndex}`,
177
+ element: inline
178
+ }
179
+ );
180
+ }
181
+ return null;
182
+ });
183
+ return /* @__PURE__ */ React.createElement(Components.Paragraph, { key: `lb-comment-body-paragraph-${index}` }, children);
184
+ }
185
+ default:
186
+ console.warn(
187
+ `Unsupported comment body block type: "${JSON.stringify(block.type)}"`
188
+ );
189
+ return null;
190
+ }
191
+ });
192
+ return /* @__PURE__ */ React.createElement(Components.Container, { key: "lb-comment-body-container" }, blocks);
193
+ }
194
+ var baseStyles = {
195
+ paragraph: {
196
+ fontSize: "14px"
197
+ },
198
+ strong: {
199
+ fontWeight: 500
200
+ },
201
+ code: {
202
+ 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',
203
+ backgroundColor: "rgba(0,0,0,0.05)",
204
+ border: "solid 1px rgba(0,0,0,0.1)",
205
+ borderRadius: "4px"
206
+ },
207
+ mention: {
208
+ color: "blue"
209
+ },
210
+ link: {
211
+ textDecoration: "underline"
212
+ }
213
+ };
214
+ async function convertCommentBodyAsHtml(body, options) {
215
+ const styles = { ...baseStyles, ...options?.styles };
216
+ const htmlBody = await stringifyCommentBody(body, {
217
+ format: "html",
218
+ resolveUsers: options?.resolveUsers,
219
+ elements: {
220
+ // NOTE: using prettier-ignore to preserve template strings
221
+ paragraph: ({ children }) => (
222
+ // prettier-ignore
223
+ children ? html`<p style="${toInlineCSSString(styles.paragraph)}">${htmlSafe(children)}</p>` : children
224
+ ),
225
+ text: ({ element }) => {
226
+ let children = element.text;
227
+ if (!children) {
228
+ return children;
229
+ }
230
+ if (element.bold) {
231
+ children = html`<strong style="${toInlineCSSString(styles.strong)}">${children}</strong>`;
232
+ }
233
+ if (element.italic) {
234
+ children = html`<em>${children}</em>`;
235
+ }
236
+ if (element.strikethrough) {
237
+ children = html`<s>${children}</s>`;
238
+ }
239
+ if (element.code) {
240
+ children = html`<code style="${toInlineCSSString(styles.code)}">${children}</code>`;
241
+ }
242
+ return children;
243
+ },
244
+ link: ({ element, href }) => {
245
+ return html`<a href="${href}" target="_blank" rel="noopener noreferrer" style="${toInlineCSSString(styles.link)}">${element.text ?? element.url}</a>`;
246
+ },
247
+ mention: ({ element, user }) => {
248
+ return html`<span data-mention style="${toInlineCSSString(styles.mention)}">@${user?.name ?? element.id}</span>`;
249
+ }
250
+ }
251
+ });
252
+ return htmlBody;
253
+ }
254
+
255
+ // src/comment-with-body.ts
256
+ var isCommentDataWithBody = (comment) => {
257
+ return comment.body !== void 0 && comment.deletedAt === void 0;
258
+ };
259
+ function filterCommentsWithBody(comments) {
260
+ const commentsWithBody = [];
261
+ for (const comment of comments) {
262
+ if (isCommentDataWithBody(comment)) {
263
+ commentsWithBody.push(comment);
264
+ }
265
+ }
266
+ return commentsWithBody;
267
+ }
268
+
269
+ // src/lib/batch-users-resolver.ts
270
+ import { Promise_withResolvers } from "@liveblocks/core";
271
+
272
+ // src/lib/warning.ts
273
+ var createDevelopmentWarning = (condition, ...args) => {
274
+ let hasWarned = false;
275
+ if (process.env.NODE_ENV !== "production") {
276
+ return () => {
277
+ if (!hasWarned && (typeof condition === "function" ? condition() : condition)) {
278
+ console.warn(...args);
279
+ hasWarned = true;
280
+ }
281
+ };
282
+ } else {
283
+ return () => {
284
+ };
285
+ }
286
+ };
287
+
288
+ // src/lib/batch-users-resolver.ts
289
+ var BatchUsersResolver = class {
290
+ constructor(resolveUsers) {
291
+ this.resolveUsers = async (args) => {
292
+ if (this.isResolved) {
293
+ this.warnAsAlreadyResolved();
294
+ return void 0;
295
+ }
296
+ for (const userId of args.userIds) {
297
+ this.usersById.set(userId, void 0);
298
+ }
299
+ await this.resolvePromise;
300
+ return args.userIds.map((userId) => this.usersById.get(userId));
301
+ };
302
+ const { promise, resolve } = Promise_withResolvers();
303
+ this.isResolved = false;
304
+ this.markAsResolved = resolve;
305
+ this.resolvePromise = promise;
306
+ this.primeResolveUsersFn = resolveUsers;
307
+ this.usersById = /* @__PURE__ */ new Map();
308
+ this.warnAsAlreadyResolved = createDevelopmentWarning(
309
+ true,
310
+ "Batch users resolver promise already resolved. It can only resolve once."
311
+ );
312
+ }
313
+ async resolve() {
314
+ if (this.isResolved) {
315
+ this.warnAsAlreadyResolved();
316
+ return;
317
+ }
318
+ const userIds = Array.from(this.usersById.keys());
319
+ const users = await this.primeResolveUsersFn?.({ userIds });
320
+ for (const [index, userId] of userIds.entries()) {
321
+ const user = users?.[index];
322
+ this.usersById.set(userId, user);
323
+ }
324
+ this.isResolved = true;
325
+ this.markAsResolved();
326
+ }
327
+ };
328
+ function createBatchUsersResolver({
329
+ resolveUsers,
330
+ callerName
331
+ }) {
332
+ const warnIfNoResolveUsers = createDevelopmentWarning(
333
+ () => !resolveUsers,
334
+ `Set "resolveUsers" option in "${callerName}" to specify users info`
335
+ );
336
+ const batchUsersResolver = new BatchUsersResolver(resolveUsers);
337
+ const resolve = async () => {
338
+ warnIfNoResolveUsers();
339
+ await batchUsersResolver.resolve();
340
+ };
341
+ return {
342
+ resolveUsers: batchUsersResolver.resolveUsers,
343
+ resolve
344
+ };
345
+ }
346
+
347
+ // src/thread-notification.tsx
348
+ var getUnreadComments = ({
349
+ comments,
350
+ inboxNotification,
351
+ userId
352
+ }) => {
353
+ const commentsWithBody = filterCommentsWithBody(comments);
354
+ const readAt = inboxNotification.readAt;
355
+ return commentsWithBody.filter((c) => c.userId !== userId).filter(
356
+ (c) => readAt ? c.createdAt > readAt && c.createdAt <= inboxNotification.notifiedAt : c.createdAt <= inboxNotification.notifiedAt
357
+ );
358
+ };
359
+ var getLastUnreadCommentWithMention = ({
360
+ comments,
361
+ mentionedUserId
362
+ }) => {
363
+ return Array.from(comments).reverse().filter((c) => c.userId !== mentionedUserId).find((c) => {
364
+ const mentionedUserIds = getMentionedIdsFromCommentBody(c.body);
365
+ return mentionedUserIds.includes(mentionedUserId);
366
+ }) ?? null;
367
+ };
368
+ var extractThreadNotificationData = async ({
369
+ client,
370
+ event
371
+ }) => {
372
+ const { threadId, roomId, userId, inboxNotificationId } = event.data;
373
+ const [thread, inboxNotification] = await Promise.all([
374
+ client.getThread({ roomId, threadId }),
375
+ client.getInboxNotification({ inboxNotificationId, userId })
376
+ ]);
377
+ const unreadComments = getUnreadComments({
378
+ comments: thread.comments,
379
+ inboxNotification,
380
+ userId
381
+ });
382
+ if (unreadComments.length <= 0) {
383
+ return null;
384
+ }
385
+ const lastUnreadCommentWithMention = getLastUnreadCommentWithMention({
386
+ comments: unreadComments,
387
+ mentionedUserId: userId
388
+ });
389
+ if (lastUnreadCommentWithMention !== null) {
390
+ return { type: "unreadMention", comment: lastUnreadCommentWithMention };
391
+ }
392
+ return {
393
+ type: "unreadReplies",
394
+ comments: unreadComments
395
+ };
396
+ };
397
+ var makeCommentEmailBaseData = ({
398
+ roomInfo,
399
+ comment
400
+ }) => {
401
+ const url = roomInfo?.url ? generateCommentUrl({
402
+ roomUrl: roomInfo?.url,
403
+ commentId: comment.id
404
+ }) : void 0;
405
+ return {
406
+ id: comment.id,
407
+ userId: comment.userId,
408
+ threadId: comment.threadId,
409
+ roomId: comment.roomId,
410
+ createdAt: comment.createdAt,
411
+ url,
412
+ rawBody: comment.body
413
+ };
414
+ };
415
+ var prepareThreadNotificationEmailBaseData = async ({
416
+ client,
417
+ event,
418
+ options = {}
419
+ }) => {
420
+ const { roomId } = event.data;
421
+ const roomInfo = options.resolveRoomInfo ? await options.resolveRoomInfo({ roomId }) : void 0;
422
+ const resolvedRoomInfo = {
423
+ ...roomInfo,
424
+ name: roomInfo?.name ?? roomId
425
+ };
426
+ const data = await extractThreadNotificationData({ client, event });
427
+ if (data === null) {
428
+ return null;
429
+ }
430
+ switch (data.type) {
431
+ case "unreadMention":
432
+ return {
433
+ type: "unreadMention",
434
+ comment: makeCommentEmailBaseData({
435
+ roomInfo,
436
+ comment: data.comment
437
+ }),
438
+ roomInfo: resolvedRoomInfo
439
+ };
440
+ case "unreadReplies": {
441
+ return {
442
+ type: "unreadReplies",
443
+ comments: data.comments.map(
444
+ (comment) => makeCommentEmailBaseData({ roomInfo, comment })
445
+ ),
446
+ roomInfo: resolvedRoomInfo
447
+ };
448
+ }
449
+ }
450
+ };
451
+ var resolveAuthorsInfo = async ({
452
+ comments,
453
+ resolveUsers
454
+ }) => {
455
+ const resolvedAuthors = /* @__PURE__ */ new Map();
456
+ if (!resolveUsers) {
457
+ return resolvedAuthors;
458
+ }
459
+ const userIds = comments.map((c) => c.userId);
460
+ const users = await resolveUsers({ userIds });
461
+ for (const [index, userId] of userIds.entries()) {
462
+ const user = users?.[index];
463
+ if (user) {
464
+ resolvedAuthors.set(userId, user);
465
+ }
466
+ }
467
+ return resolvedAuthors;
468
+ };
469
+ async function prepareThreadNotificationEmailAsHtml(client, event, options = {}) {
470
+ const data = await prepareThreadNotificationEmailBaseData({
471
+ client,
472
+ event,
473
+ options: { resolveRoomInfo: options.resolveRoomInfo }
474
+ });
475
+ if (data === null) {
476
+ return null;
477
+ }
478
+ const batchUsersResolver = createBatchUsersResolver({
479
+ resolveUsers: options.resolveUsers,
480
+ callerName: "prepareThreadNotificationEmailAsHtml"
481
+ });
482
+ switch (data.type) {
483
+ case "unreadMention": {
484
+ const { comment } = data;
485
+ const authorsInfoPromise = resolveAuthorsInfo({
486
+ comments: [comment],
487
+ resolveUsers: batchUsersResolver.resolveUsers
488
+ });
489
+ const commentBodyPromise = convertCommentBodyAsHtml(comment.rawBody, {
490
+ resolveUsers: batchUsersResolver.resolveUsers,
491
+ styles: options.styles
492
+ });
493
+ await batchUsersResolver.resolve();
494
+ const [authorsInfo, commentBodyHtml] = await Promise.all([
495
+ authorsInfoPromise,
496
+ commentBodyPromise
497
+ ]);
498
+ const authorInfo = authorsInfo.get(comment.userId);
499
+ return {
500
+ type: "unreadMention",
501
+ comment: {
502
+ id: comment.id,
503
+ threadId: comment.threadId,
504
+ roomId: comment.roomId,
505
+ author: authorInfo ? { id: comment.userId, info: authorInfo } : { id: comment.userId, info: { name: comment.userId } },
506
+ createdAt: comment.createdAt,
507
+ url: comment.url,
508
+ htmlBody: commentBodyHtml
509
+ },
510
+ roomInfo: data.roomInfo
511
+ };
512
+ }
513
+ case "unreadReplies": {
514
+ const { comments } = data;
515
+ const authorsInfoPromise = resolveAuthorsInfo({
516
+ comments,
517
+ resolveUsers: batchUsersResolver.resolveUsers
518
+ });
519
+ const commentBodiesPromises = comments.map(
520
+ (c) => convertCommentBodyAsHtml(c.rawBody, {
521
+ resolveUsers: batchUsersResolver.resolveUsers,
522
+ styles: options.styles
523
+ })
524
+ );
525
+ await batchUsersResolver.resolve();
526
+ const [authorsInfo, ...commentBodies] = await Promise.all([
527
+ authorsInfoPromise,
528
+ ...commentBodiesPromises
529
+ ]);
530
+ return {
531
+ type: "unreadReplies",
532
+ comments: comments.map((comment, index) => {
533
+ const authorInfo = authorsInfo.get(comment.userId);
534
+ const commentBodyHtml = commentBodies[index];
535
+ return {
536
+ id: comment.id,
537
+ threadId: comment.threadId,
538
+ roomId: comment.roomId,
539
+ author: authorInfo ? { id: comment.userId, info: authorInfo } : { id: comment.userId, info: { name: comment.userId } },
540
+ createdAt: comment.createdAt,
541
+ url: comment.url,
542
+ htmlBody: commentBodyHtml ?? ""
543
+ };
544
+ }),
545
+ roomInfo: data.roomInfo
546
+ };
547
+ }
548
+ }
549
+ }
550
+ async function prepareThreadNotificationEmailAsReact(client, event, options = {}) {
551
+ const data = await prepareThreadNotificationEmailBaseData({
552
+ client,
553
+ event,
554
+ options: { resolveRoomInfo: options.resolveRoomInfo }
555
+ });
556
+ if (data === null) {
557
+ return null;
558
+ }
559
+ const batchUsersResolver = createBatchUsersResolver({
560
+ resolveUsers: options.resolveUsers,
561
+ callerName: "prepareThreadNotificationEmailAsReact"
562
+ });
563
+ switch (data.type) {
564
+ case "unreadMention": {
565
+ const { comment } = data;
566
+ const authorsInfoPromise = resolveAuthorsInfo({
567
+ comments: [comment],
568
+ resolveUsers: batchUsersResolver.resolveUsers
569
+ });
570
+ const commentBodyPromise = convertCommentBodyAsReact(comment.rawBody, {
571
+ resolveUsers: batchUsersResolver.resolveUsers,
572
+ components: options.components
573
+ });
574
+ await batchUsersResolver.resolve();
575
+ const [authorsInfo, commentBodyReact] = await Promise.all([
576
+ authorsInfoPromise,
577
+ commentBodyPromise
578
+ ]);
579
+ const authorInfo = authorsInfo.get(comment.userId);
580
+ return {
581
+ type: "unreadMention",
582
+ comment: {
583
+ id: comment.id,
584
+ threadId: comment.threadId,
585
+ roomId: comment.roomId,
586
+ author: authorInfo ? { id: comment.userId, info: authorInfo } : { id: comment.userId, info: { name: comment.userId } },
587
+ createdAt: comment.createdAt,
588
+ url: comment.url,
589
+ reactBody: commentBodyReact
590
+ },
591
+ roomInfo: data.roomInfo
592
+ };
593
+ }
594
+ case "unreadReplies": {
595
+ const { comments } = data;
596
+ const authorsInfoPromise = resolveAuthorsInfo({
597
+ comments,
598
+ resolveUsers: batchUsersResolver.resolveUsers
599
+ });
600
+ const commentBodiesPromises = comments.map(
601
+ (c) => convertCommentBodyAsReact(c.rawBody, {
602
+ resolveUsers: batchUsersResolver.resolveUsers,
603
+ components: options.components
604
+ })
605
+ );
606
+ await batchUsersResolver.resolve();
607
+ const [authorsInfo, ...commentBodies] = await Promise.all([
608
+ authorsInfoPromise,
609
+ ...commentBodiesPromises
610
+ ]);
611
+ return {
612
+ type: "unreadReplies",
613
+ comments: comments.map((comment, index) => {
614
+ const authorInfo = authorsInfo.get(comment.userId);
615
+ const commentBodyReact = commentBodies[index];
616
+ return {
617
+ id: comment.id,
618
+ threadId: comment.threadId,
619
+ roomId: comment.roomId,
620
+ author: authorInfo ? { id: comment.userId, info: authorInfo } : { id: comment.userId, info: { name: comment.userId } },
621
+ createdAt: comment.createdAt,
622
+ url: comment.url,
623
+ reactBody: commentBodyReact ?? null
624
+ };
625
+ }),
626
+ roomInfo: data.roomInfo
627
+ };
628
+ }
629
+ }
630
+ }
631
+
632
+ // src/index.ts
633
+ detectDupes(PKG_NAME, PKG_VERSION, PKG_FORMAT);
634
+ export {
635
+ prepareThreadNotificationEmailAsHtml,
636
+ prepareThreadNotificationEmailAsReact
637
+ };
638
+ //# sourceMappingURL=index.mjs.map