@handled-ai/design-system 0.20.4 → 0.20.6
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 +28 -1
- package/dist/components/conversation-panel.js +180 -310
- 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.js +66 -42
- 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 +230 -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 +53 -1
- package/src/components/conversation-panel.tsx +227 -369
- 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 +73 -53
- package/src/index.ts +2 -0
- package/src/internal/__tests__/safe-html.test.ts +34 -2
- package/src/internal/safe-html.ts +79 -4
|
@@ -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;
|
|
66
|
+
function displayParticipant(person) {
|
|
67
|
+
return normalizeEmailSender({ name: person.name, email: person.email, fallbackName: person.email || person.name });
|
|
97
68
|
}
|
|
98
|
-
function
|
|
99
|
-
|
|
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 };
|
|
116
|
-
}
|
|
117
|
-
function splitInlineNodes(nodes, wrapper) {
|
|
118
|
-
const containsBr = hasDirectBr(nodes);
|
|
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
|
|
114
|
+
return null;
|
|
254
115
|
}
|
|
255
|
-
function
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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);
|
|
263
|
-
}
|
|
264
|
-
function splitFooterSegments(segments) {
|
|
265
|
-
const visibleIndexes = segments.map((segment, index) => segment.visibleText ? index : -1).filter((index) => index >= 0);
|
|
266
|
-
const visibleCount = visibleIndexes.length;
|
|
267
|
-
if (visibleCount < 2) return { bodySegments: segments, detailsSegments: [] };
|
|
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" },
|
|
@@ -350,15 +159,57 @@ const STATUS_DOT = {
|
|
|
350
159
|
function effectiveStatus(t) {
|
|
351
160
|
return t.canReply === false ? "viewing" : t.status;
|
|
352
161
|
}
|
|
162
|
+
function disabledOpenInGmailReason(thread) {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
return ((_a = thread.openInGmailDisabledReason) == null ? void 0 : _a.trim()) || ((_b = thread.replyDisabledReason) == null ? void 0 : _b.trim()) || "Gmail access is not available for this thread.";
|
|
165
|
+
}
|
|
166
|
+
function canOpenInGmail(thread, onOpenInGmail) {
|
|
167
|
+
return thread.openInGmailDisabled !== true && Boolean(thread.openInGmailUrl || onOpenInGmail);
|
|
168
|
+
}
|
|
169
|
+
function OpenInGmailButton({
|
|
170
|
+
thread,
|
|
171
|
+
onOpenInGmail,
|
|
172
|
+
label = "Open in Gmail"
|
|
173
|
+
}) {
|
|
174
|
+
const hasConfiguredAction = Boolean(thread.openInGmailUrl || onOpenInGmail || thread.openInGmailDisabled || thread.openInGmailDisabledReason);
|
|
175
|
+
if (!hasConfiguredAction) return null;
|
|
176
|
+
const disabled = !canOpenInGmail(thread, onOpenInGmail);
|
|
177
|
+
const disabledReason = disabled ? disabledOpenInGmailReason(thread) : void 0;
|
|
178
|
+
if (!disabled && thread.openInGmailUrl) {
|
|
179
|
+
return /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", asChild: true, children: /* @__PURE__ */ jsxs("a", { href: thread.openInGmailUrl, target: "_blank", rel: "noopener noreferrer", children: [
|
|
180
|
+
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
181
|
+
" ",
|
|
182
|
+
label
|
|
183
|
+
] }) });
|
|
184
|
+
}
|
|
185
|
+
return /* @__PURE__ */ jsx("span", { className: "inline-flex", title: disabledReason, children: /* @__PURE__ */ jsxs(
|
|
186
|
+
Button,
|
|
187
|
+
{
|
|
188
|
+
type: "button",
|
|
189
|
+
variant: "ghost",
|
|
190
|
+
size: "sm",
|
|
191
|
+
disabled,
|
|
192
|
+
"aria-disabled": disabled || void 0,
|
|
193
|
+
"aria-label": disabledReason ? `${label}: ${disabledReason}` : label,
|
|
194
|
+
onClick: disabled ? void 0 : () => onOpenInGmail == null ? void 0 : onOpenInGmail(thread.threadId),
|
|
195
|
+
children: [
|
|
196
|
+
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
197
|
+
" ",
|
|
198
|
+
label
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
) });
|
|
202
|
+
}
|
|
353
203
|
function MessageView({
|
|
354
204
|
message,
|
|
355
205
|
expanded,
|
|
356
206
|
onToggle,
|
|
357
207
|
me
|
|
358
208
|
}) {
|
|
359
|
-
var _a
|
|
209
|
+
var _a;
|
|
360
210
|
const [quoteOpen, setQuoteOpen] = React.useState(false);
|
|
361
|
-
const
|
|
211
|
+
const fromDisplay = displayParticipant(message.from);
|
|
212
|
+
const toDisplay = displayParticipant(message.to);
|
|
362
213
|
if (!expanded) {
|
|
363
214
|
const snippet = messageBodySnippet(message, 140);
|
|
364
215
|
return /* @__PURE__ */ jsxs(
|
|
@@ -371,7 +222,7 @@ function MessageView({
|
|
|
371
222
|
children: [
|
|
372
223
|
/* @__PURE__ */ jsx(PersonAvatar, { person: message.from }),
|
|
373
224
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground min-w-0 flex-1 truncate text-[13px]", children: [
|
|
374
|
-
/* @__PURE__ */ jsx("b", { className: "text-foreground", children: firstName(
|
|
225
|
+
/* @__PURE__ */ jsx("b", { className: "text-foreground", children: firstName(fromDisplay.name) }),
|
|
375
226
|
" \xB7 ",
|
|
376
227
|
snippet
|
|
377
228
|
] }),
|
|
@@ -381,10 +232,8 @@ function MessageView({
|
|
|
381
232
|
}
|
|
382
233
|
);
|
|
383
234
|
}
|
|
384
|
-
const
|
|
385
|
-
const
|
|
386
|
-
const textParts = message.bodyHtml ? null : splitMessageText((_b = message.body) != null ? _b : "");
|
|
387
|
-
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);
|
|
388
237
|
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-message", className: "rounded-md border border-border bg-background", children: [
|
|
389
238
|
/* @__PURE__ */ jsxs(
|
|
390
239
|
"button",
|
|
@@ -396,12 +245,12 @@ function MessageView({
|
|
|
396
245
|
/* @__PURE__ */ jsx(PersonAvatar, { person: message.from, size: "default" }),
|
|
397
246
|
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
398
247
|
/* @__PURE__ */ jsxs("span", { className: "flex flex-wrap items-baseline gap-x-1.5", children: [
|
|
399
|
-
/* @__PURE__ */ jsx("span", { className: "text-[13px] font-semibold", children:
|
|
400
|
-
/* @__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: [
|
|
401
250
|
"<",
|
|
402
|
-
|
|
251
|
+
fromDisplay.email,
|
|
403
252
|
">"
|
|
404
|
-
] })
|
|
253
|
+
] }) : null
|
|
405
254
|
] }),
|
|
406
255
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block text-xs", children: [
|
|
407
256
|
"to ",
|
|
@@ -420,20 +269,16 @@ function MessageView({
|
|
|
420
269
|
}
|
|
421
270
|
),
|
|
422
271
|
/* @__PURE__ */ jsxs("div", { className: "px-3 pb-2.5", children: [
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
),
|
|
435
|
-
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
|
|
436
|
-
] }) : 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
|
+
) }),
|
|
437
282
|
message.quoted ? /* @__PURE__ */ jsxs("div", { className: "mt-2", children: [
|
|
438
283
|
/* @__PURE__ */ jsx(
|
|
439
284
|
"button",
|
|
@@ -446,8 +291,8 @@ function MessageView({
|
|
|
446
291
|
}
|
|
447
292
|
),
|
|
448
293
|
quoteOpen ? /* @__PURE__ */ jsxs("div", { className: "border-border text-muted-foreground mt-1 border-l-2 pl-3 text-[13px]", children: [
|
|
449
|
-
/* @__PURE__ */ jsx("p", { className: "mb-1",
|
|
450
|
-
/* @__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 }) })
|
|
451
296
|
] }) : null
|
|
452
297
|
] }) : null
|
|
453
298
|
] })
|
|
@@ -462,9 +307,10 @@ function ReplyComposer({
|
|
|
462
307
|
onClose,
|
|
463
308
|
onSend,
|
|
464
309
|
onDraft,
|
|
465
|
-
onPreviewReply
|
|
310
|
+
onPreviewReply,
|
|
311
|
+
draftDisabledReason
|
|
466
312
|
}) {
|
|
467
|
-
var _a, _b, _c, _d;
|
|
313
|
+
var _a, _b, _c, _d, _e, _f;
|
|
468
314
|
const [body, setBody] = React.useState((_a = thread.draft) != null ? _a : "");
|
|
469
315
|
const [sig, setSig] = React.useState(true);
|
|
470
316
|
const [preview, setPreview] = React.useState(false);
|
|
@@ -473,13 +319,14 @@ function ReplyComposer({
|
|
|
473
319
|
const [sendError, setSendError] = React.useState(null);
|
|
474
320
|
const ccList = replyAll ? (_b = thread.cc) != null ? _b : [] : [];
|
|
475
321
|
const subject = /^re:/i.test(thread.subject) ? thread.subject : `Re: ${thread.subject}`;
|
|
322
|
+
const draftDisabled = Boolean(draftDisabledReason);
|
|
476
323
|
const localPreviewHtml = textToHtml(body) + (sig && thread.signature ? textToHtml(thread.signature) : "");
|
|
477
324
|
const openPreview = async () => {
|
|
478
325
|
var _a2, _b2;
|
|
479
326
|
setPreview(true);
|
|
480
327
|
setSendError(null);
|
|
481
328
|
if (!onPreviewReply) {
|
|
482
|
-
setPreviewState({ loading: false, html:
|
|
329
|
+
setPreviewState({ loading: false, html: localPreviewHtml, confirmationToken: null, error: null, local: true });
|
|
483
330
|
return;
|
|
484
331
|
}
|
|
485
332
|
setPreviewState({ loading: true, html: null, confirmationToken: null, error: null, local: false });
|
|
@@ -487,7 +334,7 @@ function ReplyComposer({
|
|
|
487
334
|
const result = await onPreviewReply({ threadId: thread.threadId, body, includeSignature: sig, replyAll });
|
|
488
335
|
setPreviewState({
|
|
489
336
|
loading: false,
|
|
490
|
-
html:
|
|
337
|
+
html: (_a2 = result.htmlBody) != null ? _a2 : "",
|
|
491
338
|
confirmationToken: (_b2 = result.confirmationToken) != null ? _b2 : null,
|
|
492
339
|
error: null,
|
|
493
340
|
local: false
|
|
@@ -516,6 +363,7 @@ function ReplyComposer({
|
|
|
516
363
|
}
|
|
517
364
|
};
|
|
518
365
|
const handleDraft = async () => {
|
|
366
|
+
if (draftDisabled) return;
|
|
519
367
|
setSending(true);
|
|
520
368
|
setSendError(null);
|
|
521
369
|
try {
|
|
@@ -541,7 +389,7 @@ function ReplyComposer({
|
|
|
541
389
|
] })
|
|
542
390
|
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
543
391
|
"Reply to ",
|
|
544
|
-
/* @__PURE__ */ jsx("b", { children: firstName(thread.contact.name) })
|
|
392
|
+
/* @__PURE__ */ jsx("b", { children: firstName(displayParticipant(thread.contact).name) })
|
|
545
393
|
] }) }),
|
|
546
394
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1 text-[11px]", children: [
|
|
547
395
|
/* @__PURE__ */ jsx(GitMerge, { size: 11 }),
|
|
@@ -552,12 +400,15 @@ function ReplyComposer({
|
|
|
552
400
|
/* @__PURE__ */ jsxs("div", { className: "border-border mb-2 space-y-1 border-b pb-2 text-[13px]", children: [
|
|
553
401
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
554
402
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "To" }),
|
|
555
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium", children: thread.contact.name }),
|
|
556
|
-
/* @__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 })
|
|
557
405
|
] }),
|
|
558
406
|
replyAll && ccList.length ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-1.5", children: [
|
|
559
407
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "Cc" }),
|
|
560
|
-
/* @__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
|
+
})) })
|
|
561
412
|
] }) : null,
|
|
562
413
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
563
414
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground w-12 shrink-0 text-[11px] font-medium", children: "Subject" }),
|
|
@@ -623,17 +474,20 @@ function ReplyComposer({
|
|
|
623
474
|
/* @__PURE__ */ jsxs("div", { className: "border-border space-y-1 rounded-md border p-3 text-[13px]", children: [
|
|
624
475
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
625
476
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "To " }),
|
|
626
|
-
/* @__PURE__ */ jsx("b", { children: thread.contact.name }),
|
|
477
|
+
/* @__PURE__ */ jsx("b", { children: displayParticipant(thread.contact).name }),
|
|
627
478
|
" ",
|
|
628
479
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/60", children: [
|
|
629
480
|
"<",
|
|
630
|
-
thread.contact.email,
|
|
481
|
+
(_d = displayParticipant(thread.contact).email) != null ? _d : thread.contact.email,
|
|
631
482
|
">"
|
|
632
483
|
] })
|
|
633
484
|
] }),
|
|
634
485
|
replyAll && ccList.length ? /* @__PURE__ */ jsxs("div", { className: "text-muted-foreground", children: [
|
|
635
486
|
"Cc ",
|
|
636
|
-
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
|
+
}))
|
|
637
491
|
] }) : null,
|
|
638
492
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
639
493
|
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Subject " }),
|
|
@@ -647,26 +501,27 @@ function ReplyComposer({
|
|
|
647
501
|
"div",
|
|
648
502
|
{
|
|
649
503
|
"data-slot": "conv-preview-body",
|
|
650
|
-
"data-confirmation-token": (
|
|
651
|
-
className:
|
|
652
|
-
|
|
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 })
|
|
653
507
|
}
|
|
654
508
|
),
|
|
655
509
|
sendError ? /* @__PURE__ */ jsx("p", { role: "alert", className: "text-destructive text-sm", children: sendError }) : null,
|
|
656
510
|
/* @__PURE__ */ jsxs(DialogFooter, { className: "sm:justify-between", children: [
|
|
657
|
-
/* @__PURE__ */ jsxs(
|
|
511
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex", title: draftDisabledReason != null ? draftDisabledReason : void 0, children: /* @__PURE__ */ jsxs(
|
|
658
512
|
"button",
|
|
659
513
|
{
|
|
660
514
|
type: "button",
|
|
661
|
-
disabled: sending || previewState.loading,
|
|
515
|
+
disabled: sending || previewState.loading || draftDisabled,
|
|
662
516
|
onClick: handleDraft,
|
|
517
|
+
"aria-label": draftDisabledReason ? `Open draft in Gmail: ${draftDisabledReason}` : "Open draft in Gmail",
|
|
663
518
|
className: "text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-[13px] disabled:pointer-events-none disabled:opacity-50",
|
|
664
519
|
children: [
|
|
665
520
|
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
666
521
|
" Open draft in Gmail"
|
|
667
522
|
]
|
|
668
523
|
}
|
|
669
|
-
),
|
|
524
|
+
) }),
|
|
670
525
|
/* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
671
526
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", disabled: sending, onClick: () => {
|
|
672
527
|
setPreview(false);
|
|
@@ -701,17 +556,30 @@ function ThreadBody({
|
|
|
701
556
|
onPreviewReply,
|
|
702
557
|
onOpenInGmail
|
|
703
558
|
}) {
|
|
559
|
+
var _a;
|
|
704
560
|
const canReply = thread.canReply !== false;
|
|
561
|
+
const replyDisabledReason = ((_a = thread.replyDisabledReason) == null ? void 0 : _a.trim()) || "You are not a participant on this thread, so replying is disabled here.";
|
|
562
|
+
const draftDisabledReason = onCreateGmailDraft ? null : "Gmail draft creation is not available for this thread.";
|
|
705
563
|
const hasCc = !!(thread.cc && thread.cc.length);
|
|
564
|
+
const sortedMessages = React.useMemo(() => sortMessagesChronologically(thread.messages), [thread.messages]);
|
|
706
565
|
const [mode, setMode] = React.useState("idle");
|
|
707
566
|
const [replyAll, setReplyAll] = React.useState(false);
|
|
708
567
|
const [expanded, setExpanded] = React.useState(() => {
|
|
709
568
|
const o = {};
|
|
710
|
-
|
|
711
|
-
o[m.id] = i ===
|
|
569
|
+
sortedMessages.forEach((m, i) => {
|
|
570
|
+
o[m.id] = i === sortedMessages.length - 1;
|
|
712
571
|
});
|
|
713
572
|
return o;
|
|
714
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]);
|
|
715
583
|
const toggle = (id) => setExpanded((e) => __spreadProps(__spreadValues({}, e), { [id]: !e[id] }));
|
|
716
584
|
return /* @__PURE__ */ jsxs("div", { "data-slot": "conv-thread-body", className: "space-y-2", children: [
|
|
717
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: [
|
|
@@ -725,13 +593,15 @@ function ThreadBody({
|
|
|
725
593
|
" or Gmail."
|
|
726
594
|
] })
|
|
727
595
|
] }) : null,
|
|
728
|
-
/* @__PURE__ */ jsx("div", { className: "space-y-1", children:
|
|
729
|
-
!canReply ? /* @__PURE__ */ jsxs("div", { className: "border-border bg-muted/30 text-muted-foreground flex items-start gap-2 rounded-md border p-2.5 text-[12px]", 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)) }),
|
|
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: [
|
|
730
598
|
/* @__PURE__ */ jsx(Eye, { size: 14, className: "mt-0.5 shrink-0" }),
|
|
731
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
599
|
+
/* @__PURE__ */ jsxs("span", { className: "min-w-0 flex-1", children: [
|
|
732
600
|
/* @__PURE__ */ jsx("b", { children: "Viewing only." }),
|
|
733
|
-
"
|
|
734
|
-
|
|
601
|
+
" ",
|
|
602
|
+
replyDisabledReason
|
|
603
|
+
] }),
|
|
604
|
+
/* @__PURE__ */ jsx(OpenInGmailButton, { thread, onOpenInGmail })
|
|
735
605
|
] }) : null,
|
|
736
606
|
canReply && mode === "idle" ? /* @__PURE__ */ jsxs("div", { "data-slot": "conv-action-row", className: "border-border/70 mt-1 flex flex-wrap items-center gap-x-3 gap-y-2 border-t pt-3", children: [
|
|
737
607
|
/* @__PURE__ */ jsxs(Button, { type: "button", size: "sm", onClick: () => {
|
|
@@ -748,10 +618,7 @@ function ThreadBody({
|
|
|
748
618
|
/* @__PURE__ */ jsx(ReplyAll, { size: 14 }),
|
|
749
619
|
" Reply all"
|
|
750
620
|
] }) : null,
|
|
751
|
-
/* @__PURE__ */
|
|
752
|
-
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
753
|
-
" Open in Gmail"
|
|
754
|
-
] }),
|
|
621
|
+
/* @__PURE__ */ jsx(OpenInGmailButton, { thread, onOpenInGmail }),
|
|
755
622
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground/70 ml-auto inline-flex items-center gap-1.5 text-[12px]", children: [
|
|
756
623
|
/* @__PURE__ */ jsx(GitMerge, { size: 13 }),
|
|
757
624
|
" Stays on this thread"
|
|
@@ -771,9 +638,11 @@ function ThreadBody({
|
|
|
771
638
|
setMode("sent");
|
|
772
639
|
},
|
|
773
640
|
onDraft: async (body, includeSignature) => {
|
|
774
|
-
|
|
641
|
+
if (!onCreateGmailDraft) return;
|
|
642
|
+
await onCreateGmailDraft({ threadId: thread.threadId, body, includeSignature, replyAll });
|
|
775
643
|
setMode("draft");
|
|
776
|
-
}
|
|
644
|
+
},
|
|
645
|
+
draftDisabledReason
|
|
777
646
|
}
|
|
778
647
|
) : null,
|
|
779
648
|
canReply && mode === "sent" ? /* @__PURE__ */ jsxs("div", { className: "border-status-active-border bg-status-active-bg flex items-center gap-2 rounded-md border p-3 text-[13px]", children: [
|
|
@@ -782,7 +651,7 @@ function ThreadBody({
|
|
|
782
651
|
/* @__PURE__ */ jsx("b", { children: replyAll ? "Reply all sent" : "Reply sent" }),
|
|
783
652
|
" \xB7 added to the thread. Delivered to",
|
|
784
653
|
" ",
|
|
785
|
-
/* @__PURE__ */ jsx("b", { children: thread.contact.name }),
|
|
654
|
+
/* @__PURE__ */ jsx("b", { children: displayParticipant(thread.contact).name }),
|
|
786
655
|
". This action stays ",
|
|
787
656
|
/* @__PURE__ */ jsx("b", { children: "Pending" }),
|
|
788
657
|
"; playbooks remain stopped."
|
|
@@ -800,10 +669,7 @@ function ThreadBody({
|
|
|
800
669
|
] }),
|
|
801
670
|
" thread; open it there to finish. Nothing was sent."
|
|
802
671
|
] }),
|
|
803
|
-
/* @__PURE__ */
|
|
804
|
-
/* @__PURE__ */ jsx(GmailMark, { size: 14 }),
|
|
805
|
-
" Open in Gmail"
|
|
806
|
-
] }),
|
|
672
|
+
/* @__PURE__ */ jsx(OpenInGmailButton, { thread, onOpenInGmail }),
|
|
807
673
|
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setMode("idle"), children: "Done" })
|
|
808
674
|
] }) : null
|
|
809
675
|
] });
|
|
@@ -819,9 +685,13 @@ function ThreadRow({
|
|
|
819
685
|
onPreviewReply,
|
|
820
686
|
onOpenInGmail
|
|
821
687
|
}) {
|
|
688
|
+
var _a;
|
|
822
689
|
const status = effectiveStatus(thread);
|
|
823
|
-
const
|
|
824
|
-
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 : "");
|
|
825
695
|
const lastSnippet = last ? messageBodySnippet(last, 120) : "";
|
|
826
696
|
const pill = STATUS_PILL[status];
|
|
827
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: [
|
|
@@ -840,7 +710,7 @@ function ThreadRow({
|
|
|
840
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 })
|
|
841
711
|
] }),
|
|
842
712
|
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground block truncate text-xs", children: [
|
|
843
|
-
/* @__PURE__ */ jsx("b", { className: "text-foreground/80", children: thread.contact.name }),
|
|
713
|
+
/* @__PURE__ */ jsx("b", { className: "text-foreground/80", children: displayParticipant(thread.contact).name }),
|
|
844
714
|
" \xB7 ",
|
|
845
715
|
who,
|
|
846
716
|
": ",
|