@handled-ai/design-system 0.20.5 → 0.20.7
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/components/conversation-panel.d.ts +19 -0
- package/dist/components/conversation-panel.js +116 -292
- package/dist/components/conversation-panel.js.map +1 -1
- package/dist/components/email-body.d.ts +15 -0
- package/dist/components/email-body.js +101 -0
- package/dist/components/email-body.js.map +1 -0
- package/dist/components/email-display-helpers.d.ts +34 -0
- package/dist/components/email-display-helpers.js +436 -0
- package/dist/components/email-display-helpers.js.map +1 -0
- package/dist/components/email-preview-card.d.ts +7 -4
- package/dist/components/email-preview-card.js +48 -25
- package/dist/components/email-preview-card.js.map +1 -1
- package/dist/components/timeline-activity.d.ts +1 -0
- package/dist/components/timeline-activity.js +116 -65
- package/dist/components/timeline-activity.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/safe-html.d.ts +1 -1
- package/dist/internal/safe-html.js +64 -3
- package/dist/internal/safe-html.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__tests__/conversation-panel.test.tsx +182 -22
- package/src/components/__tests__/email-body.test.tsx +83 -0
- package/src/components/__tests__/email-display-helpers.test.ts +91 -0
- package/src/components/__tests__/email-preview-card.test.tsx +36 -2
- package/src/components/__tests__/timeline-activity.test.tsx +87 -1
- package/src/components/conversation-panel.tsx +136 -350
- package/src/components/email-body.tsx +126 -0
- package/src/components/email-display-helpers.ts +557 -0
- package/src/components/email-preview-card.tsx +54 -29
- package/src/components/timeline-activity.tsx +105 -63
- package/src/index.ts +2 -0
- package/src/internal/__tests__/safe-html.test.ts +34 -2
- package/src/internal/safe-html.ts +79 -4
|
@@ -34,6 +34,25 @@ interface ConvMessage {
|
|
|
34
34
|
to: ConvParticipant;
|
|
35
35
|
/** Absolute timestamp label, e.g. "Jun 1, 2026, 9:12 AM". */
|
|
36
36
|
date: string;
|
|
37
|
+
/**
|
|
38
|
+
* Raw chronological timestamp for deterministic thread ordering. Prefer
|
|
39
|
+
* `sentAt` for outbound messages and `receivedAt` for inbound messages.
|
|
40
|
+
* Accepts ISO/RFC822 strings, Date objects, epoch milliseconds, or Gmail
|
|
41
|
+
* internalDate values as strings/numbers. Display-only `date` / `ago` labels
|
|
42
|
+
* are never parsed for ordering.
|
|
43
|
+
*/
|
|
44
|
+
timestamp?: string | number | Date | null;
|
|
45
|
+
rawTimestamp?: string | number | Date | null;
|
|
46
|
+
sentAt?: string | number | Date | null;
|
|
47
|
+
receivedAt?: string | number | Date | null;
|
|
48
|
+
/** Compatibility with data contracts that pass through source field names. */
|
|
49
|
+
sent_at?: string | number | Date | null;
|
|
50
|
+
received_at?: string | number | Date | null;
|
|
51
|
+
internalDate?: string | number | Date | null;
|
|
52
|
+
gmailInternalDate?: string | number | Date | null;
|
|
53
|
+
internal_date?: string | number | Date | null;
|
|
54
|
+
rfc822Date?: string | number | Date | null;
|
|
55
|
+
dateHeader?: string | number | Date | null;
|
|
37
56
|
/** Relative label, e.g. "2 days ago". */
|
|
38
57
|
ago?: string;
|
|
39
58
|
receipt?: {
|
|
@@ -42,12 +42,13 @@ import {
|
|
|
42
42
|
import { cn } from "../lib/utils.js";
|
|
43
43
|
import { getInitials } from "../lib/user-display.js";
|
|
44
44
|
import { BRAND_ICONS } from "../lib/icons.js";
|
|
45
|
-
import { htmlToTextSnippet, sanitizeHtml } from "../internal/safe-html.js";
|
|
46
45
|
import { Avatar, AvatarFallback, AvatarImage } from "./avatar.js";
|
|
47
46
|
import { Button } from "./button.js";
|
|
48
47
|
import { Switch } from "./switch.js";
|
|
49
48
|
import { Textarea } from "./textarea.js";
|
|
50
49
|
import { RichTextToolbar } from "./rich-text-toolbar.js";
|
|
50
|
+
import { EmailBody } from "./email-body.js";
|
|
51
|
+
import { decodeEmailDisplayText, emailBodySnippet, formatAddressList, normalizeEmailSender } from "./email-display-helpers.js";
|
|
51
52
|
import {
|
|
52
53
|
Dialog,
|
|
53
54
|
DialogContent,
|
|
@@ -56,260 +57,69 @@ import {
|
|
|
56
57
|
DialogDescription,
|
|
57
58
|
DialogFooter
|
|
58
59
|
} from "./dialog.js";
|
|
59
|
-
const PROSE = cn(
|
|
60
|
-
"text-sm leading-[1.62] text-foreground/90 break-words",
|
|
61
|
-
"[&_p]:my-2 [&_p:first-child]:mt-0 [&_p:last-child]:mb-0",
|
|
62
|
-
"[&_a]:text-[#1a73e8] [&_a]:underline-offset-2 hover:[&_a]:underline",
|
|
63
|
-
"[&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5 [&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5",
|
|
64
|
-
"[&_img]:max-w-full [&_img]:h-auto"
|
|
65
|
-
);
|
|
66
60
|
function escapeHtml(s) {
|
|
67
61
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
68
62
|
}
|
|
69
63
|
function textToHtml(text) {
|
|
70
|
-
return text.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean).map((p) => `<p>${escapeHtml(p).replace(/\n/g, "<br>")}</p>`).join("");
|
|
64
|
+
return decodeEmailDisplayText(text).split(/\n{2,}/).map((p) => p.trim()).filter(Boolean).map((p) => `<p>${escapeHtml(p).replace(/\n/g, "<br>")}</p>`).join("");
|
|
71
65
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const BLOCK_TAGS = /* @__PURE__ */ new Set([...SPLITTABLE_BLOCK_TAGS, ...WHOLE_BLOCK_TAGS]);
|
|
75
|
-
const BR_TAG_RE = /<br\s*\/?>/gi;
|
|
76
|
-
const HTML_BLOCK_START_RE = /<(p|div|blockquote|table|ul|ol|hr)\b[^>]*>/i;
|
|
77
|
-
const SIGNATURE_DELIMITER_RE = /^--\s*$/;
|
|
78
|
-
const SIGNOFF_RE = /^(?:thanks,|thank you,|best,|regards,|sincerely,)$/i;
|
|
79
|
-
const DETAILS_START_RE = /^(?:confidentiality notice\b|this message and any attachments\b|this email and any attachments\b|the information contained in this message\b|this communication may contain\b|unsubscribe\b|manage your preferences\b)/i;
|
|
80
|
-
const CONTACT_DETAIL_RE = /(?:@|https?:\/\/|www\.|\+?\d[\d\s().-]{6,}\d|\b(?:ceo|cfo|cto|coo|founder|co-founder|director|manager|vp|vice president|president|head of|sales|marketing|operations|account|customer success|success|support|engineer|consultant|partner|principal|advisor|associate)\b|\b(?:inc|llc|ltd|corp|corporation|company|co\.)\b)/i;
|
|
81
|
-
function decodeHtmlText(value) {
|
|
82
|
-
const withoutTags = value.replace(BR_TAG_RE, "\n").replace(/<\/(p|div|blockquote|li|tr|table|ul|ol)>/gi, "\n").replace(/<[^>]*>/g, "");
|
|
83
|
-
if (typeof document !== "undefined") {
|
|
84
|
-
const textarea = document.createElement("textarea");
|
|
85
|
-
textarea.innerHTML = withoutTags;
|
|
86
|
-
return textarea.value;
|
|
87
|
-
}
|
|
88
|
-
return withoutTags.replace(/ /gi, " ").replace(/&/gi, "&").replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/'|'/gi, "'");
|
|
89
|
-
}
|
|
90
|
-
function htmlToVisibleText(html) {
|
|
91
|
-
return decodeHtmlText(html).replace(/\u00a0/g, " ").replace(/[ \t]+/g, " ").trim();
|
|
92
|
-
}
|
|
93
|
-
function serializeNode(node) {
|
|
94
|
-
const host = document.createElement("div");
|
|
95
|
-
host.appendChild(node.cloneNode(true));
|
|
96
|
-
return host.innerHTML;
|
|
97
|
-
}
|
|
98
|
-
function wrapHtmlLike(element, innerHtml) {
|
|
99
|
-
const clone = element.cloneNode(false);
|
|
100
|
-
clone.innerHTML = innerHtml;
|
|
101
|
-
return clone.outerHTML;
|
|
102
|
-
}
|
|
103
|
-
function hasDirectBr(nodes) {
|
|
104
|
-
return nodes.some((node) => node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br");
|
|
105
|
-
}
|
|
106
|
-
function hasDirectBlockChild(element) {
|
|
107
|
-
return Array.from(element.children).some((child) => {
|
|
108
|
-
const tagName = child.tagName.toLowerCase();
|
|
109
|
-
return tagName !== "br" && BLOCK_TAGS.has(tagName);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
function makeHtmlSegment(html) {
|
|
113
|
-
const visibleText = htmlToVisibleText(html);
|
|
114
|
-
if (!html.trim() || !visibleText && !/<(?:img|hr)\b/i.test(html)) return null;
|
|
115
|
-
return { html, visibleText };
|
|
66
|
+
function displayParticipant(person) {
|
|
67
|
+
return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name });
|
|
116
68
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
const chunks = [[]];
|
|
120
|
-
nodes.forEach((node) => {
|
|
121
|
-
var _a;
|
|
122
|
-
if (node.nodeType === Node.ELEMENT_NODE && node.tagName.toLowerCase() === "br") {
|
|
123
|
-
chunks.push([]);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
(_a = chunks[chunks.length - 1]) == null ? void 0 : _a.push(serializeNode(node));
|
|
127
|
-
});
|
|
128
|
-
return chunks.map((chunk) => chunk.join("")).map((innerHtml) => {
|
|
129
|
-
if (wrapper) return wrapHtmlLike(wrapper, innerHtml);
|
|
130
|
-
return containsBr ? `<div>${innerHtml}</div>` : innerHtml;
|
|
131
|
-
}).map(makeHtmlSegment).filter((segment) => Boolean(segment));
|
|
69
|
+
function firstName(name) {
|
|
70
|
+
return decodeEmailDisplayText(name).split(" ")[0] || decodeEmailDisplayText(name);
|
|
132
71
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
if (tagName === "div" && hasDirectBlockChild(element)) {
|
|
136
|
-
const childSegments = splitHtmlNodes(Array.from(element.childNodes));
|
|
137
|
-
return childSegments.length ? childSegments : [makeHtmlSegment(element.outerHTML)].filter(Boolean);
|
|
138
|
-
}
|
|
139
|
-
if (SPLITTABLE_BLOCK_TAGS.has(tagName) && hasDirectBr(Array.from(element.childNodes)) && !hasDirectBlockChild(element)) {
|
|
140
|
-
return splitInlineNodes(Array.from(element.childNodes), element);
|
|
141
|
-
}
|
|
142
|
-
const segment = makeHtmlSegment(element.outerHTML);
|
|
143
|
-
return segment ? [segment] : [];
|
|
72
|
+
function sameEmail(a, b) {
|
|
73
|
+
return Boolean(a && b && a.trim().toLowerCase() === b.trim().toLowerCase());
|
|
144
74
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
let inlineNodes = [];
|
|
148
|
-
const flushInline = () => {
|
|
149
|
-
if (!inlineNodes.length) return;
|
|
150
|
-
segments.push(...splitInlineNodes(inlineNodes));
|
|
151
|
-
inlineNodes = [];
|
|
152
|
-
};
|
|
153
|
-
nodes.forEach((node) => {
|
|
154
|
-
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
155
|
-
const element = node;
|
|
156
|
-
const tagName = element.tagName.toLowerCase();
|
|
157
|
-
if (BLOCK_TAGS.has(tagName)) {
|
|
158
|
-
flushInline();
|
|
159
|
-
segments.push(...splitElementSegment(element));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
inlineNodes.push(node);
|
|
164
|
-
});
|
|
165
|
-
flushInline();
|
|
166
|
-
return segments;
|
|
75
|
+
function messageBodySnippet(message, maxLength = 140) {
|
|
76
|
+
return emailBodySnippet({ bodyHtml: message.bodyHtml, body: message.body }, maxLength);
|
|
167
77
|
}
|
|
168
|
-
function
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
let match;
|
|
174
|
-
while ((match = tagPattern.exec(html)) !== null) {
|
|
175
|
-
const rawTag = match[0];
|
|
176
|
-
if (/^<\//.test(rawTag)) depth -= 1;
|
|
177
|
-
else if (!/\/\s*>$/.test(rawTag)) depth += 1;
|
|
178
|
-
if (depth === 0) return tagPattern.lastIndex;
|
|
78
|
+
function parseMessageTimestampValue(value) {
|
|
79
|
+
if (value == null || value === "") return null;
|
|
80
|
+
if (value instanceof Date) {
|
|
81
|
+
const time = value.getTime();
|
|
82
|
+
return Number.isNaN(time) ? null : time;
|
|
179
83
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const segments = [];
|
|
184
|
-
let cursor = 0;
|
|
185
|
-
const pushInline = (inlineHtml) => {
|
|
186
|
-
const chunks = inlineHtml.split(BR_TAG_RE);
|
|
187
|
-
const hadBr = chunks.length > 1;
|
|
188
|
-
chunks.forEach((chunk) => {
|
|
189
|
-
const segment = makeHtmlSegment(hadBr ? `<div>${chunk}</div>` : chunk);
|
|
190
|
-
if (segment) segments.push(segment);
|
|
191
|
-
});
|
|
192
|
-
};
|
|
193
|
-
while (cursor < html.length) {
|
|
194
|
-
const rest = html.slice(cursor);
|
|
195
|
-
const match = HTML_BLOCK_START_RE.exec(rest);
|
|
196
|
-
if (!match || match.index === void 0) {
|
|
197
|
-
pushInline(rest);
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
|
-
if (match.index > 0) pushInline(rest.slice(0, match.index));
|
|
201
|
-
const tagStart = cursor + match.index;
|
|
202
|
-
const rawOpen = match[0];
|
|
203
|
-
const tagName = match[1].toLowerCase();
|
|
204
|
-
const openTagEnd = tagStart + rawOpen.length - 1;
|
|
205
|
-
const segmentEnd = findMatchingCloseTag(html, tagName, openTagEnd);
|
|
206
|
-
const blockHtml = html.slice(tagStart, segmentEnd);
|
|
207
|
-
if (SPLITTABLE_BLOCK_TAGS.has(tagName) && BR_TAG_RE.test(blockHtml)) {
|
|
208
|
-
const openTag = rawOpen;
|
|
209
|
-
const closeTag = `</${tagName}>`;
|
|
210
|
-
const inner = blockHtml.replace(new RegExp(`^${rawOpen.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i"), "").replace(new RegExp(`${closeTag}$`, "i"), "");
|
|
211
|
-
inner.split(BR_TAG_RE).forEach((chunk) => {
|
|
212
|
-
const segment = makeHtmlSegment(`${openTag}${chunk}${closeTag}`);
|
|
213
|
-
if (segment) segments.push(segment);
|
|
214
|
-
});
|
|
215
|
-
} else {
|
|
216
|
-
const segment = makeHtmlSegment(blockHtml);
|
|
217
|
-
if (segment) segments.push(segment);
|
|
218
|
-
}
|
|
219
|
-
cursor = segmentEnd;
|
|
84
|
+
if (typeof value === "number") {
|
|
85
|
+
if (!Number.isFinite(value)) return null;
|
|
86
|
+
return value < 1e10 ? value * 1e3 : value;
|
|
220
87
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
88
|
+
const trimmed = value.trim();
|
|
89
|
+
if (!trimmed) return null;
|
|
90
|
+
if (/^\d+$/.test(trimmed)) {
|
|
91
|
+
const numeric = Number(trimmed);
|
|
92
|
+
if (!Number.isFinite(numeric)) return null;
|
|
93
|
+
return numeric < 1e10 ? numeric * 1e3 : numeric;
|
|
226
94
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
return words.every((word) => /^[A-Z][A-Za-z'.-]*$/.test(word));
|
|
246
|
-
}
|
|
247
|
-
function nextVisibleSegmentText(segments, fromIndex) {
|
|
248
|
-
var _a, _b;
|
|
249
|
-
for (let index = fromIndex + 1; index < segments.length; index += 1) {
|
|
250
|
-
const text = firstVisibleLine((_b = (_a = segments[index]) == null ? void 0 : _a.visibleText) != null ? _b : "");
|
|
251
|
-
if (text) return text;
|
|
95
|
+
const parsed = Date.parse(trimmed);
|
|
96
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
97
|
+
}
|
|
98
|
+
function messageTimestamp(message) {
|
|
99
|
+
const directional = message.direction === "outbound" ? [message.sentAt, message.sent_at] : [message.receivedAt, message.received_at];
|
|
100
|
+
const candidates = [
|
|
101
|
+
...directional,
|
|
102
|
+
message.rawTimestamp,
|
|
103
|
+
message.timestamp,
|
|
104
|
+
message.gmailInternalDate,
|
|
105
|
+
message.internalDate,
|
|
106
|
+
message.internal_date,
|
|
107
|
+
message.rfc822Date,
|
|
108
|
+
message.dateHeader
|
|
109
|
+
];
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
const parsed = parseMessageTimestampValue(candidate);
|
|
112
|
+
if (parsed !== null) return parsed;
|
|
252
113
|
}
|
|
253
|
-
return
|
|
254
|
-
}
|
|
255
|
-
function isFooterBoundary(segments, index) {
|
|
256
|
-
var _a, _b;
|
|
257
|
-
const line = firstVisibleLine((_b = (_a = segments[index]) == null ? void 0 : _a.visibleText) != null ? _b : "");
|
|
258
|
-
if (!line) return false;
|
|
259
|
-
if (SIGNATURE_DELIMITER_RE.test(line) || DETAILS_START_RE.test(line)) return true;
|
|
260
|
-
const nextText = nextVisibleSegmentText(segments, index);
|
|
261
|
-
if (SIGNOFF_RE.test(line)) return Boolean(nextText && (isLikelySenderNameLine(nextText) || CONTACT_DETAIL_RE.test(nextText)));
|
|
262
|
-
return isLikelySenderNameLine(line) && CONTACT_DETAIL_RE.test(nextText);
|
|
114
|
+
return null;
|
|
263
115
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const trailingCount = Math.max(Math.ceil(visibleCount * 0.4), 8);
|
|
269
|
-
const firstTrailingOrdinal = Math.max(1, visibleCount - trailingCount);
|
|
270
|
-
for (let ordinal = firstTrailingOrdinal; ordinal < visibleCount; ordinal += 1) {
|
|
271
|
-
const index = visibleIndexes[ordinal];
|
|
272
|
-
if (ordinal > 0 && isFooterBoundary(segments, index)) {
|
|
273
|
-
return { bodySegments: segments.slice(0, index), detailsSegments: segments.slice(index) };
|
|
116
|
+
function sortMessagesChronologically(messages) {
|
|
117
|
+
return messages.map((message, index) => ({ message, index, timestamp: messageTimestamp(message) })).sort((a, b) => {
|
|
118
|
+
if (a.timestamp !== null && b.timestamp !== null && a.timestamp !== b.timestamp) {
|
|
119
|
+
return a.timestamp - b.timestamp;
|
|
274
120
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
function splitMessageHtml(rawHtml) {
|
|
279
|
-
const sanitizedHtml = sanitizeHtml(rawHtml);
|
|
280
|
-
const { bodySegments, detailsSegments } = splitFooterSegments(segmentHtmlMessage(sanitizedHtml));
|
|
281
|
-
if (!detailsSegments.length) return { bodyHtml: sanitizedHtml, detailsHtml: "" };
|
|
282
|
-
return {
|
|
283
|
-
bodyHtml: bodySegments.map((segment) => {
|
|
284
|
-
var _a;
|
|
285
|
-
return (_a = segment.html) != null ? _a : "";
|
|
286
|
-
}).join(""),
|
|
287
|
-
detailsHtml: detailsSegments.map((segment) => {
|
|
288
|
-
var _a;
|
|
289
|
-
return (_a = segment.html) != null ? _a : "";
|
|
290
|
-
}).join("")
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function splitMessageText(text) {
|
|
294
|
-
const { bodySegments, detailsSegments } = splitFooterSegments(segmentTextMessage(text));
|
|
295
|
-
if (!detailsSegments.length) return { bodyText: text, detailsText: "" };
|
|
296
|
-
return {
|
|
297
|
-
bodyText: bodySegments.map((segment) => {
|
|
298
|
-
var _a;
|
|
299
|
-
return (_a = segment.text) != null ? _a : "";
|
|
300
|
-
}).join(""),
|
|
301
|
-
detailsText: detailsSegments.map((segment) => {
|
|
302
|
-
var _a;
|
|
303
|
-
return (_a = segment.text) != null ? _a : "";
|
|
304
|
-
}).join("")
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
function messageBodySnippet(message, maxLength = 140) {
|
|
308
|
-
var _a, _b, _c;
|
|
309
|
-
if (message.bodyHtml) {
|
|
310
|
-
return htmlToTextSnippet(splitMessageHtml(message.bodyHtml).bodyHtml, maxLength);
|
|
311
|
-
}
|
|
312
|
-
return (_c = (_b = splitMessageText((_a = message.body) != null ? _a : "").bodyText.split("\n").find((line) => line.trim())) == null ? void 0 : _b.trim()) != null ? _c : "";
|
|
121
|
+
return a.index - b.index;
|
|
122
|
+
}).map((entry) => entry.message);
|
|
313
123
|
}
|
|
314
124
|
function GmailMark({ size = 14 }) {
|
|
315
125
|
return (
|
|
@@ -327,14 +137,13 @@ function GmailMark({ size = 14 }) {
|
|
|
327
137
|
);
|
|
328
138
|
}
|
|
329
139
|
function PersonAvatar({ person, size = "sm" }) {
|
|
140
|
+
var _a;
|
|
141
|
+
const display = displayParticipant(person);
|
|
330
142
|
return /* @__PURE__ */ jsxs(Avatar, { size, children: [
|
|
331
|
-
person.avatarUrl ? /* @__PURE__ */ jsx(AvatarImage, { src: person.avatarUrl, alt:
|
|
332
|
-
/* @__PURE__ */ jsx(AvatarFallback, { className: "bg-muted text-muted-foreground text-[10px] font-medium uppercase", children: getInitials({ name:
|
|
143
|
+
person.avatarUrl ? /* @__PURE__ */ jsx(AvatarImage, { src: person.avatarUrl, alt: display.name }) : null,
|
|
144
|
+
/* @__PURE__ */ jsx(AvatarFallback, { className: "bg-muted text-muted-foreground text-[10px] font-medium uppercase", children: getInitials({ name: display.name, email: (_a = display.email) != null ? _a : person.email }) })
|
|
333
145
|
] });
|
|
334
146
|
}
|
|
335
|
-
function firstName(name) {
|
|
336
|
-
return name.split(" ")[0] || name;
|
|
337
|
-
}
|
|
338
147
|
const STATUS_PILL = {
|
|
339
148
|
responded: { label: "New reply", cls: "bg-status-active-bg text-status-active-fg border-status-active-border" },
|
|
340
149
|
draft: { label: "Draft", cls: "bg-background text-foreground/80 border-border" },
|
|
@@ -397,9 +206,10 @@ function MessageView({
|
|
|
397
206
|
onToggle,
|
|
398
207
|
me
|
|
399
208
|
}) {
|
|
400
|
-
var _a
|
|
209
|
+
var _a;
|
|
401
210
|
const [quoteOpen, setQuoteOpen] = React.useState(false);
|
|
402
|
-
const
|
|
211
|
+
const fromDisplay = displayParticipant(message.from);
|
|
212
|
+
const toDisplay = displayParticipant(message.to);
|
|
403
213
|
if (!expanded) {
|
|
404
214
|
const snippet = messageBodySnippet(message, 140);
|
|
405
215
|
return /* @__PURE__ */ jsxs(
|
|
@@ -412,7 +222,7 @@ function MessageView({
|
|
|
412
222
|
children: [
|
|
413
223
|
/* @__PURE__ */ jsx(PersonAvatar, { person: message.from }),
|
|
414
224
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground min-w-0 flex-1 truncate text-[13px]", children: [
|
|
415
|
-
/* @__PURE__ */ jsx("b", { className: "text-foreground", children: firstName(
|
|
225
|
+
/* @__PURE__ */ jsx("b", { className: "text-foreground", children: firstName(fromDisplay.name) }),
|
|
416
226
|
" \xB7 ",
|
|
417
227
|
snippet
|
|
418
228
|
] }),
|
|
@@ -422,10 +232,8 @@ function MessageView({
|
|
|
422
232
|
}
|
|
423
233
|
);
|
|
424
234
|
}
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
const textParts = message.bodyHtml ? null : splitMessageText((_b = message.body) != null ? _b : "");
|
|
428
|
-
const hasDetails = Boolean((htmlParts == null ? void 0 : htmlParts.detailsHtml) || (textParts == null ? void 0 : textParts.detailsText));
|
|
235
|
+
const meDisplay = me ? displayParticipant(me) : null;
|
|
236
|
+
const toLabel = meDisplay && sameEmail(toDisplay.email, meDisplay.email) ? "me" : firstName(toDisplay.name);
|
|
429
237
|
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-message", className: "rounded-md border border-border bg-background", children: [
|
|
430
238
|
/* @__PURE__ */ jsxs(
|
|
431
239
|
"button",
|
|
@@ -437,12 +245,12 @@ function MessageView({
|
|
|
437
245
|
/* @__PURE__ */ jsx(PersonAvatar, { person: message.from, size: "default" }),
|
|
438
246
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
439
247
|
/* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-baseline gap-x-1.5", children: [
|
|
440
|
-
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-semibold", children:
|
|
441
|
-
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/60 truncate text-xs", children: [
|
|
248
|
+
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-semibold", children: fromDisplay.name }),
|
|
249
|
+
fromDisplay.email ? /* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/60 truncate text-xs", children: [
|
|
442
250
|
"<",
|
|
443
|
-
|
|
251
|
+
fromDisplay.email,
|
|
444
252
|
">"
|
|
445
|
-
] })
|
|
253
|
+
] }) : null
|
|
446
254
|
] }),
|
|
447
255
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block text-xs", children: [
|
|
448
256
|
"to ",
|
|
@@ -461,20 +269,16 @@ function MessageView({
|
|
|
461
269
|
}
|
|
462
270
|
),
|
|
463
271
|
/* @__PURE__ */ jsxs("div", { className: "px-3 pb-2.5", children: [
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
),
|
|
476
|
-
detailsOpen ? /* @__PURE__ */ jsx("div", { className: "border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]", children: htmlParts ? /* @__PURE__ */ jsx("div", { "data-slot": "conv-message-details", className: PROSE, dangerouslySetInnerHTML: { __html: htmlParts.detailsHtml } }) : /* @__PURE__ */ jsx("div", { "data-slot": "conv-message-details", className: cn(PROSE, "whitespace-pre-line"), children: textParts == null ? void 0 : textParts.detailsText }) }) : null
|
|
477
|
-
] }) : null,
|
|
272
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "conv-message-body", children: /* @__PURE__ */ jsx(
|
|
273
|
+
EmailBody,
|
|
274
|
+
{
|
|
275
|
+
html: message.bodyHtml,
|
|
276
|
+
text: message.body,
|
|
277
|
+
variant: "history",
|
|
278
|
+
collapseDetails: true,
|
|
279
|
+
className: "text-sm"
|
|
280
|
+
}
|
|
281
|
+
) }),
|
|
478
282
|
message.quoted ? /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
|
|
479
283
|
/* @__PURE__ */ jsx(
|
|
480
284
|
"button",
|
|
@@ -487,8 +291,8 @@ function MessageView({
|
|
|
487
291
|
}
|
|
488
292
|
),
|
|
489
293
|
quoteOpen ? /* @__PURE__ */ jsxs("div", { className: "border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]", children: [
|
|
490
|
-
/* @__PURE__ */ jsx("p", { className: "mb-1",
|
|
491
|
-
/* @__PURE__ */ jsx("div", {
|
|
294
|
+
/* @__PURE__ */ jsx("p", { className: "mb-1", children: decodeEmailDisplayText(message.quoted.attr) }),
|
|
295
|
+
/* @__PURE__ */ jsx("div", { "data-slot": "conv-quoted-body", children: /* @__PURE__ */ jsx(EmailBody, { html: message.quoted.html, variant: "history", collapseDetails: false }) })
|
|
492
296
|
] }) : null
|
|
493
297
|
] }) : null
|
|
494
298
|
] })
|
|
@@ -506,7 +310,7 @@ function ReplyComposer({
|
|
|
506
310
|
onPreviewReply,
|
|
507
311
|
draftDisabledReason
|
|
508
312
|
}) {
|
|
509
|
-
var _a, _b, _c, _d;
|
|
313
|
+
var _a, _b, _c, _d, _e, _f;
|
|
510
314
|
const [body, setBody] = React.useState((_a = thread.draft) != null ? _a : "");
|
|
511
315
|
const [sig, setSig] = React.useState(true);
|
|
512
316
|
const [preview, setPreview] = React.useState(false);
|
|
@@ -522,7 +326,7 @@ function ReplyComposer({
|
|
|
522
326
|
setPreview(true);
|
|
523
327
|
setSendError(null);
|
|
524
328
|
if (!onPreviewReply) {
|
|
525
|
-
setPreviewState({ loading: false, html:
|
|
329
|
+
setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true });
|
|
526
330
|
return;
|
|
527
331
|
}
|
|
528
332
|
setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false });
|
|
@@ -530,7 +334,7 @@ function ReplyComposer({
|
|
|
530
334
|
const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll });
|
|
531
335
|
setPreviewState({
|
|
532
336
|
loading: false,
|
|
533
|
-
html:
|
|
337
|
+
html: (_a2 = result.htmlBody) != null ? _a2 : "",
|
|
534
338
|
confirmationToken: (_b2 = result.confirmationToken) != null ? _b2 : null,
|
|
535
339
|
error: null,
|
|
536
340
|
local: false
|
|
@@ -585,7 +389,7 @@ function ReplyComposer({
|
|
|
585
389
|
] })
|
|
586
390
|
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
587
391
|
"Reply to ",
|
|
588
|
-
/* @__PURE__ */ jsx("b", { children: firstName(thread.contact.name) })
|
|
392
|
+
/* @__PURE__ */ jsx("b", { children: firstName(displayParticipant(thread.contact).name) })
|
|
589
393
|
] }) }),
|
|
590
394
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[11px]", children: [
|
|
591
395
|
/* @__PURE__ */ jsx(GitMerge, { size: 11 }),
|
|
@@ -596,12 +400,15 @@ function ReplyComposer({
|
|
|
596
400
|
/* @__PURE__ */ jsxs("div", { className: "border-border mb-2 space-y-1 border-b pb-2 text-[13px]", children: [
|
|
597
401
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
598
402
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "To" }),
|
|
599
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: thread.contact.name }),
|
|
600
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 truncate text-xs", children: thread.contact.email })
|
|
403
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: displayParticipant(thread.contact).name }),
|
|
404
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60 truncate text-xs", children: (_c = displayParticipant(thread.contact).email) != null ? _c : thread.contact.email })
|
|
601
405
|
] }),
|
|
602
406
|
replyAll && ccList.length ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
603
407
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "Cc" }),
|
|
604
|
-
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: ccList.map((c) =>
|
|
408
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: formatAddressList(ccList.map((c) => {
|
|
409
|
+
var _a2;
|
|
410
|
+
return `${displayParticipant(c).name} <${(_a2 = displayParticipant(c).email) != null ? _a2 : c.email}>`;
|
|
411
|
+
})) })
|
|
605
412
|
] }) : null,
|
|
606
413
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
607
414
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "Subject" }),
|
|
@@ -667,17 +474,20 @@ function ReplyComposer({
|
|
|
667
474
|
/* @__PURE__ */ jsxs("div", { className: "border-border space-y-1 rounded-md border p-3 text-[13px]", children: [
|
|
668
475
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
669
476
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "To " }),
|
|
670
|
-
/* @__PURE__ */ jsx("b", { children: thread.contact.name }),
|
|
477
|
+
/* @__PURE__ */ jsx("b", { children: displayParticipant(thread.contact).name }),
|
|
671
478
|
" ",
|
|
672
479
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/60", children: [
|
|
673
480
|
"<",
|
|
674
|
-
thread.contact.email,
|
|
481
|
+
(_d = displayParticipant(thread.contact).email) != null ? _d : thread.contact.email,
|
|
675
482
|
">"
|
|
676
483
|
] })
|
|
677
484
|
] }),
|
|
678
485
|
replyAll && ccList.length ? /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground", children: [
|
|
679
486
|
"Cc ",
|
|
680
|
-
ccList.map((c) =>
|
|
487
|
+
formatAddressList(ccList.map((c) => {
|
|
488
|
+
var _a2;
|
|
489
|
+
return `${displayParticipant(c).name} <${(_a2 = displayParticipant(c).email) != null ? _a2 : c.email}>`;
|
|
490
|
+
}))
|
|
681
491
|
] }) : null,
|
|
682
492
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
683
493
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Subject " }),
|
|
@@ -691,9 +501,9 @@ function ReplyComposer({
|
|
|
691
501
|
"div",
|
|
692
502
|
{
|
|
693
503
|
"data-slot": "conv-preview-body",
|
|
694
|
-
"data-confirmation-token": (
|
|
695
|
-
className:
|
|
696
|
-
|
|
504
|
+
"data-confirmation-token": (_e = previewState.confirmationToken) != null ? _e : void 0,
|
|
505
|
+
className: "max-h-72 overflow-auto",
|
|
506
|
+
children: /* @__PURE__ */ jsx(EmailBody, { html: (_f = previewState.html) != null ? _f : "", variant: "preview", collapseDetails: false, defaultDetailsOpen: true })
|
|
697
507
|
}
|
|
698
508
|
),
|
|
699
509
|
sendError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: sendError }) : null,
|
|
@@ -751,15 +561,25 @@ function ThreadBody({
|
|
|
751
561
|
const replyDisabledReason = ((_a = thread.replyDisabledReason) == null ? void 0 : _a.trim()) || "You are not a participant on this thread, so replying is disabled here.";
|
|
752
562
|
const draftDisabledReason = onCreateGmailDraft ? null : "Gmail draft creation is not available for this thread.";
|
|
753
563
|
const hasCc = !!(thread.cc && thread.cc.length);
|
|
564
|
+
const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages]);
|
|
754
565
|
const [mode, setMode] = React.useState("idle");
|
|
755
566
|
const [replyAll, setReplyAll] = React.useState(false);
|
|
756
567
|
const [expanded, setExpanded] = React.useState(() => {
|
|
757
568
|
const o = {};
|
|
758
|
-
|
|
759
|
-
o[m.id] = i ===
|
|
569
|
+
sortedMessages.forEach((m, i) => {
|
|
570
|
+
o[m.id] = i === sortedMessages.length - 1;
|
|
760
571
|
});
|
|
761
572
|
return o;
|
|
762
573
|
});
|
|
574
|
+
React.useEffect(() => {
|
|
575
|
+
setExpanded((current) => {
|
|
576
|
+
const next = __spreadValues({}, current);
|
|
577
|
+
sortedMessages.forEach((m, i) => {
|
|
578
|
+
if (next[m.id] === void 0) next[m.id] = i === sortedMessages.length - 1;
|
|
579
|
+
});
|
|
580
|
+
return next;
|
|
581
|
+
});
|
|
582
|
+
}, [sortedMessages]);
|
|
763
583
|
const toggle = (id) => setExpanded((e) => __spreadProps(__spreadValues({}, e), { [id]: !e[id] }));
|
|
764
584
|
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-2", children: [
|
|
765
585
|
canReply && thread.paused ? /* @__PURE__ */ jsxs("div", { className: "border-status-pending-border bg-status-pending-bg text-status-pending-fg flex items-start gap-2 rounded-md border p-2.5 text-[12px]", children: [
|
|
@@ -773,7 +593,7 @@ function ThreadBody({
|
|
|
773
593
|
" or Gmail."
|
|
774
594
|
] })
|
|
775
595
|
] }) : null,
|
|
776
|
-
/* @__PURE__ */ jsx("div", { className: "space-y-1", children:
|
|
596
|
+
/* @__PURE__ */ jsx("div", { className: "space-y-1", children: sortedMessages.map((m) => /* @__PURE__ */ jsx(MessageView, { message: m, expanded: !!expanded[m.id], onToggle: () => toggle(m.id), me }, m.id)) }),
|
|
777
597
|
!canReply ? /* @__PURE__ */ jsxs("div", { className: "border-border bg-muted/30 text-muted-foreground flex flex-wrap items-start gap-2 rounded-md border p-2.5 text-[12px]", children: [
|
|
778
598
|
/* @__PURE__ */ jsx(Eye, { size: 14, className: "mt-0.5 shrink-0" }),
|
|
779
599
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
@@ -831,7 +651,7 @@ function ThreadBody({
|
|
|
831
651
|
/* @__PURE__ */ jsx("b", { children: replyAll ? "Reply all sent" : "Reply sent" }),
|
|
832
652
|
" \xB7 added to the thread. Delivered to",
|
|
833
653
|
" ",
|
|
834
|
-
/* @__PURE__ */ jsx("b", { children: thread.contact.name }),
|
|
654
|
+
/* @__PURE__ */ jsx("b", { children: displayParticipant(thread.contact).name }),
|
|
835
655
|
". This action stays ",
|
|
836
656
|
/* @__PURE__ */ jsx("b", { children: "Pending" }),
|
|
837
657
|
"; playbooks remain stopped."
|
|
@@ -865,9 +685,13 @@ function ThreadRow({
|
|
|
865
685
|
onPreviewReply,
|
|
866
686
|
onOpenInGmail
|
|
867
687
|
}) {
|
|
688
|
+
var _a;
|
|
868
689
|
const status = effectiveStatus(thread);
|
|
869
|
-
const
|
|
870
|
-
const
|
|
690
|
+
const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages]);
|
|
691
|
+
const last = sortedMessages[sortedMessages.length - 1];
|
|
692
|
+
const lastSender = last ? displayParticipant(last.from) : null;
|
|
693
|
+
const meDisplay = me ? displayParticipant(me) : null;
|
|
694
|
+
const who = (last == null ? void 0 : last.direction) === "outbound" && sameEmail(lastSender == null ? void 0 : lastSender.email, meDisplay == null ? void 0 : meDisplay.email) ? "You" : firstName((_a = lastSender == null ? void 0 : lastSender.name) != null ? _a : "");
|
|
871
695
|
const lastSnippet = last ? messageBodySnippet(last, 120) : "";
|
|
872
696
|
const pill = STATUS_PILL[status];
|
|
873
697
|
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread", "data-open": open ? "true" : void 0, className: "border-border border-b last:border-b-0", children: [
|
|
@@ -886,7 +710,7 @@ function ThreadRow({
|
|
|
886
710
|
/* @__PURE__ */ jsx("span", { className: cn("shrink-0 rounded-md border px-1.5 py-px text-[10px] font-medium leading-4", pill.cls), children: pill.label })
|
|
887
711
|
] }),
|
|
888
712
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block truncate text-xs", children: [
|
|
889
|
-
/* @__PURE__ */ jsx("b", { className: "text-foreground/80", children: thread.contact.name }),
|
|
713
|
+
/* @__PURE__ */ jsx("b", { className: "text-foreground/80", children: displayParticipant(thread.contact).name }),
|
|
890
714
|
" \xB7 ",
|
|
891
715
|
who,
|
|
892
716
|
": ",
|