react-os-shell 0.2.68 → 0.3.0

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.
Files changed (69) hide show
  1. package/README.md +35 -15
  2. package/dist/{Browser-UGZQMWKW.js → Browser-H5KDP5OH.js} +3 -3
  3. package/dist/{Browser-UGZQMWKW.js.map → Browser-H5KDP5OH.js.map} +1 -1
  4. package/dist/{Calculator-3ZXNXWDH.js → Calculator-QQ7NF53Q.js} +4 -4
  5. package/dist/{Calculator-3ZXNXWDH.js.map → Calculator-QQ7NF53Q.js.map} +1 -1
  6. package/dist/{Calendar-ON4AQ54T.js → Calendar-J6L7FGHS.js} +175 -108
  7. package/dist/Calendar-J6L7FGHS.js.map +1 -0
  8. package/dist/{CurrencyConverter-ACTLK72N.js → CurrencyConverter-YHOGBUPH.js} +4 -4
  9. package/dist/{CurrencyConverter-ACTLK72N.js.map → CurrencyConverter-YHOGBUPH.js.map} +1 -1
  10. package/dist/{Documents-STDXQ7I4.js → Documents-6DYALASM.js} +3 -3
  11. package/dist/{Documents-STDXQ7I4.js.map → Documents-6DYALASM.js.map} +1 -1
  12. package/dist/Email-U2U5Z4DL.js +475 -0
  13. package/dist/Email-U2U5Z4DL.js.map +1 -0
  14. package/dist/Files-T62M4V5I.js +11 -0
  15. package/dist/{Files-ASKLEUNU.js.map → Files-T62M4V5I.js.map} +1 -1
  16. package/dist/{Minesweeper-CZNIO75H.js → Minesweeper-S2JHXYLX.js} +3 -3
  17. package/dist/{Minesweeper-CZNIO75H.js.map → Minesweeper-S2JHXYLX.js.map} +1 -1
  18. package/dist/{Notepad-ZYYH4ZXN.js → Notepad-2YF7X3XO.js} +3 -3
  19. package/dist/{Notepad-ZYYH4ZXN.js.map → Notepad-2YF7X3XO.js.map} +1 -1
  20. package/dist/{PomodoroTimer-2MNIEAUM.js → PomodoroTimer-3J7Z3NVQ.js} +4 -4
  21. package/dist/{PomodoroTimer-2MNIEAUM.js.map → PomodoroTimer-3J7Z3NVQ.js.map} +1 -1
  22. package/dist/Preview-WM6ZP5PZ.js +8 -0
  23. package/dist/{Preview-U72UL74H.js.map → Preview-WM6ZP5PZ.js.map} +1 -1
  24. package/dist/Spreadsheet-ZIE2SXAF.js +6 -0
  25. package/dist/{Spreadsheet-T7Y7PRD6.js.map → Spreadsheet-ZIE2SXAF.js.map} +1 -1
  26. package/dist/{TodoList-7JZ2SLDI.js → TodoList-QGXCDEIE.js} +18 -204
  27. package/dist/TodoList-QGXCDEIE.js.map +1 -0
  28. package/dist/{Weather-DYCTCB6T.js → Weather-2GFPSZ5V.js} +4 -4
  29. package/dist/{Weather-DYCTCB6T.js.map → Weather-2GFPSZ5V.js.map} +1 -1
  30. package/dist/{WorldClock-XL4OMFOY.js → WorldClock-P4JR5I6X.js} +4 -4
  31. package/dist/{WorldClock-XL4OMFOY.js.map → WorldClock-P4JR5I6X.js.map} +1 -1
  32. package/dist/apps/index.d.ts +2 -5
  33. package/dist/apps/index.js +23 -26
  34. package/dist/apps/index.js.map +1 -1
  35. package/dist/chunk-57B3WALN.js +114 -0
  36. package/dist/chunk-57B3WALN.js.map +1 -0
  37. package/dist/{chunk-4SHZ7BZO.js → chunk-62FC2FHC.js} +92 -21
  38. package/dist/chunk-62FC2FHC.js.map +1 -0
  39. package/dist/{chunk-FXAOT23O.js → chunk-ATQVRDDQ.js} +3 -3
  40. package/dist/{chunk-FXAOT23O.js.map → chunk-ATQVRDDQ.js.map} +1 -1
  41. package/dist/{chunk-GP4Y3VCB.js → chunk-KMGWSDEI.js} +480 -4
  42. package/dist/chunk-KMGWSDEI.js.map +1 -0
  43. package/dist/{chunk-SU6XVJND.js → chunk-O6FJZAFM.js} +3 -3
  44. package/dist/{chunk-SU6XVJND.js.map → chunk-O6FJZAFM.js.map} +1 -1
  45. package/dist/{chunk-YL47AVBA.js → chunk-SEV7UXGN.js} +4 -4
  46. package/dist/{chunk-YL47AVBA.js.map → chunk-SEV7UXGN.js.map} +1 -1
  47. package/dist/{chunk-L2AFKNSQ.js → chunk-ZBRFMK3E.js} +4 -4
  48. package/dist/{chunk-L2AFKNSQ.js.map → chunk-ZBRFMK3E.js.map} +1 -1
  49. package/dist/index.d.ts +55 -1
  50. package/dist/index.js +241 -135
  51. package/dist/index.js.map +1 -1
  52. package/package.json +10 -3
  53. package/dist/Calendar-ON4AQ54T.js.map +0 -1
  54. package/dist/Email-5WL3TWC6.js +0 -1883
  55. package/dist/Email-5WL3TWC6.js.map +0 -1
  56. package/dist/Files-ASKLEUNU.js +0 -12
  57. package/dist/GeminiChat-XTEBZIVK.js +0 -184
  58. package/dist/GeminiChat-XTEBZIVK.js.map +0 -1
  59. package/dist/Preview-U72UL74H.js +0 -8
  60. package/dist/Spreadsheet-T7Y7PRD6.js +0 -7
  61. package/dist/TodoList-7JZ2SLDI.js.map +0 -1
  62. package/dist/chunk-4SHZ7BZO.js.map +0 -1
  63. package/dist/chunk-5VXRBUEH.js +0 -104
  64. package/dist/chunk-5VXRBUEH.js.map +0 -1
  65. package/dist/chunk-GP4Y3VCB.js.map +0 -1
  66. package/dist/chunk-MUXDKEOC.js +0 -485
  67. package/dist/chunk-MUXDKEOC.js.map +0 -1
  68. package/dist/chunk-MVWEL34Y.js +0 -209
  69. package/dist/chunk-MVWEL34Y.js.map +0 -1
@@ -1,1883 +0,0 @@
1
- import { isDemoMode, getDemoEmails } from './chunk-5VXRBUEH.js';
2
- import { setEmailUnreadCount } from './chunk-PDFQNHW7.js';
3
- import { formatDateTime, formatDate } from './chunk-NSU7OHPC.js';
4
- import { useGoogleAuth } from './chunk-MVWEL34Y.js';
5
- import { toast_default } from './chunk-WIJ45SYD.js';
6
- import { EditableGrid } from './chunk-GP4Y3VCB.js';
7
- import { Modal, ModalActions } from './chunk-4SHZ7BZO.js';
8
- import './chunk-PLGHQ7QW.js';
9
- import { glassStyle } from './chunk-SSA762W5.js';
10
- import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
11
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
-
13
- var GMAIL_API = "https://gmail.googleapis.com/gmail/v1/users/me";
14
- function getHeader(msg, name) {
15
- return msg.payload.headers.find((h) => h.name.toLowerCase() === name.toLowerCase())?.value || "";
16
- }
17
- function parseFrom(from) {
18
- const match = from.match(/^(.+?)\s*<(.+?)>$/);
19
- if (match) return { name: match[1].replace(/"/g, ""), email: match[2] };
20
- return { name: from, email: from };
21
- }
22
- function decodeBase64(data) {
23
- try {
24
- return decodeURIComponent(escape(atob(data.replace(/-/g, "+").replace(/_/g, "/"))));
25
- } catch {
26
- try {
27
- return atob(data.replace(/-/g, "+").replace(/_/g, "/"));
28
- } catch {
29
- return "";
30
- }
31
- }
32
- }
33
- function buildCidMap(parts) {
34
- const map = {};
35
- if (!parts) return map;
36
- for (const part of parts) {
37
- if (part.body?.attachmentId) {
38
- const cidHeader = (part.headers || []).find((h) => h.name.toLowerCase() === "content-id");
39
- if (cidHeader) {
40
- const cid = cidHeader.value.replace(/^<|>$/g, "");
41
- map[cid] = { attachmentId: part.body.attachmentId, mimeType: part.mimeType };
42
- }
43
- }
44
- if (part.parts) Object.assign(map, buildCidMap(part.parts));
45
- }
46
- return map;
47
- }
48
- function getAttachments(msg) {
49
- const attachments = [];
50
- const walk = (parts) => {
51
- if (!parts) return;
52
- for (const part of parts) {
53
- if (part.filename && part.filename.length > 0 && part.body?.attachmentId) {
54
- attachments.push({ filename: part.filename, mimeType: part.mimeType, attachmentId: part.body.attachmentId, size: part.body.size });
55
- }
56
- if (part.parts) walk(part.parts);
57
- }
58
- };
59
- walk(msg.payload.parts);
60
- return attachments;
61
- }
62
- function getMessageBody(msg) {
63
- const findPart = (parts, mime) => {
64
- if (!parts) return null;
65
- for (const part of parts) {
66
- if (part.mimeType === mime && part.body?.data) return decodeBase64(part.body.data);
67
- if (part.parts) {
68
- const found = findPart(part.parts, mime);
69
- if (found) return found;
70
- }
71
- }
72
- return null;
73
- };
74
- let html = null;
75
- if (msg.payload.parts) {
76
- html = findPart(msg.payload.parts, "text/html");
77
- if (!html) {
78
- const text = findPart(msg.payload.parts, "text/plain");
79
- if (text) return `<pre style="white-space:pre-wrap;font-family:inherit;">${text.replace(/</g, "&lt;")}</pre>`;
80
- }
81
- }
82
- if (!html && msg.payload.body?.data) {
83
- const decoded = decodeBase64(msg.payload.body.data);
84
- if (msg.payload.mimeType === "text/html") html = decoded;
85
- else return `<pre style="white-space:pre-wrap;font-family:inherit;">${decoded.replace(/</g, "&lt;")}</pre>`;
86
- }
87
- if (!html) return '<p style="color:#999;">No content</p>';
88
- const cidMap = buildCidMap(msg.payload.parts);
89
- html = html.replace(/src=["']cid:([^"']+)["']/gi, (_match, cid) => {
90
- const info = cidMap[cid];
91
- if (info) {
92
- return `src="" data-cid-attachment="${info.attachmentId}" data-cid-mime="${info.mimeType}" data-msg-id="${msg.id}"`;
93
- }
94
- return _match;
95
- });
96
- return html;
97
- }
98
- function timeAgo(date) {
99
- const diff = Date.now() - date.getTime();
100
- const mins = Math.floor(diff / 6e4);
101
- if (mins < 1) return "now";
102
- if (mins < 60) return `${mins}m`;
103
- const hrs = Math.floor(mins / 60);
104
- if (hrs < 24) return `${hrs}h`;
105
- const days = Math.floor(hrs / 24);
106
- if (days < 7) return `${days}d`;
107
- return formatDate(date.toISOString());
108
- }
109
- function avatarColor(name) {
110
- const colors = ["bg-blue-500", "bg-red-500", "bg-green-500", "bg-purple-500", "bg-yellow-500", "bg-pink-500", "bg-indigo-500", "bg-teal-500"];
111
- let hash = 0;
112
- for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
113
- return colors[Math.abs(hash) % colors.length];
114
- }
115
- function isImageAttachment(att) {
116
- return /^image\/(jpe?g|png|gif|webp|bmp|svg)$/i.test(att.mimeType);
117
- }
118
- async function batchFetchMessages(token, msgIds) {
119
- if (msgIds.length === 0) return [];
120
- const boundary = `batch_${Date.now()}`;
121
- const parts = msgIds.map(
122
- (m, i) => `--${boundary}\r
123
- Content-Type: application/http\r
124
- Content-ID: <item${i}>\r
125
- \r
126
- GET /gmail/v1/users/me/messages/${m.id}?format=metadata&metadataHeaders=From&metadataHeaders=Subject&fields=id,threadId,snippet,internalDate,labelIds,historyId,payload(headers,parts(filename))\r
127
- `
128
- );
129
- const body = parts.join("") + `--${boundary}--`;
130
- const res = await fetch("https://www.googleapis.com/batch/gmail/v1", {
131
- method: "POST",
132
- headers: {
133
- Authorization: `Bearer ${token}`,
134
- "Content-Type": `multipart/mixed; boundary=${boundary}`
135
- },
136
- body
137
- });
138
- if (!res.ok) {
139
- const results = await Promise.all(
140
- msgIds.map(
141
- (m) => fetch(`${GMAIL_API}/messages/${m.id}?format=metadata&metadataHeaders=From&metadataHeaders=Subject&fields=id,threadId,snippet,internalDate,labelIds,historyId,payload(headers,parts(filename))`, {
142
- headers: { Authorization: `Bearer ${token}` }
143
- }).then((r) => r.json())
144
- )
145
- );
146
- return addThreadCounts(results.map(parseMessageToItem));
147
- }
148
- const text = await res.text();
149
- const responseBoundary = res.headers.get("content-type")?.match(/boundary=(.+)/)?.[1] || "";
150
- const responseParts = text.split(`--${responseBoundary}`).filter((p) => p.includes("HTTP/"));
151
- const items = [];
152
- for (const part of responseParts) {
153
- const jsonMatch = part.match(/\{[\s\S]*\}/);
154
- if (jsonMatch) {
155
- try {
156
- const msg = JSON.parse(jsonMatch[0]);
157
- if (msg.id) items.push(parseMessageToItem(msg));
158
- } catch {
159
- }
160
- }
161
- }
162
- return addThreadCounts(items);
163
- }
164
- function parseMessageToItem(msg) {
165
- const from = parseFrom(getHeader(msg, "From"));
166
- const parts = msg.payload?.parts || [];
167
- const attachments = parts.filter((p) => p.filename && p.filename.length > 0);
168
- return {
169
- id: msg.id,
170
- threadId: msg.threadId,
171
- from: from.name,
172
- fromEmail: from.email,
173
- subject: getHeader(msg, "Subject") || "(no subject)",
174
- snippet: msg.snippet || "",
175
- date: new Date(parseInt(msg.internalDate)),
176
- isUnread: (msg.labelIds || []).includes("UNREAD"),
177
- isStarred: (msg.labelIds || []).includes("STARRED"),
178
- hasAttachment: attachments.length > 0,
179
- attachmentCount: attachments.length,
180
- labelIds: msg.labelIds || [],
181
- threadCount: 1
182
- };
183
- }
184
- function addThreadCounts(items) {
185
- const threadCounts = /* @__PURE__ */ new Map();
186
- for (const item of items) {
187
- threadCounts.set(item.threadId, (threadCounts.get(item.threadId) || 0) + 1);
188
- }
189
- return items.map((item) => ({ ...item, threadCount: threadCounts.get(item.threadId) || 1 }));
190
- }
191
- function getCategoryLabel(labelIds) {
192
- if (labelIds.includes("CATEGORY_SOCIAL")) return { text: "Social", color: "bg-blue-100 text-blue-700" };
193
- if (labelIds.includes("CATEGORY_PROMOTIONS")) return { text: "Promo", color: "bg-green-100 text-green-700" };
194
- if (labelIds.includes("CATEGORY_UPDATES")) return { text: "Updates", color: "bg-yellow-100 text-yellow-700" };
195
- if (labelIds.includes("CATEGORY_FORUMS")) return { text: "Forums", color: "bg-purple-100 text-purple-700" };
196
- return null;
197
- }
198
- function showUndoToast(message, onUndo, durationMs = 5e3) {
199
- const container = (() => {
200
- let el = document.getElementById("toast-container");
201
- if (!el) {
202
- el = document.createElement("div");
203
- el.id = "toast-container";
204
- el.className = "fixed top-4 right-4 z-[9999] flex flex-col gap-2 items-end pointer-events-none";
205
- document.body.appendChild(el);
206
- }
207
- return el;
208
- })();
209
- const toastEl = document.createElement("div");
210
- toastEl.className = "bg-gray-800 text-white px-5 py-3 rounded-lg shadow-lg text-sm font-medium flex items-center gap-3 max-w-lg pointer-events-auto";
211
- toastEl.style.opacity = "0";
212
- toastEl.style.transform = "translateX(20px)";
213
- toastEl.style.transition = "opacity 300ms ease, transform 300ms ease";
214
- const span = document.createElement("span");
215
- span.textContent = message;
216
- toastEl.appendChild(span);
217
- const progressBar = document.createElement("div");
218
- progressBar.className = "absolute bottom-0 left-0 h-0.5 bg-blue-400 rounded-b-lg";
219
- progressBar.style.width = "100%";
220
- progressBar.style.transition = `width ${durationMs}ms linear`;
221
- toastEl.style.position = "relative";
222
- toastEl.style.overflow = "hidden";
223
- toastEl.appendChild(progressBar);
224
- const undoBtn = document.createElement("button");
225
- undoBtn.textContent = "Undo";
226
- undoBtn.className = "text-blue-300 hover:text-blue-100 font-semibold text-sm shrink-0";
227
- undoBtn.onclick = () => {
228
- onUndo();
229
- dismiss();
230
- };
231
- toastEl.appendChild(undoBtn);
232
- container.appendChild(toastEl);
233
- requestAnimationFrame(() => {
234
- toastEl.style.opacity = "1";
235
- toastEl.style.transform = "translateX(0)";
236
- requestAnimationFrame(() => {
237
- progressBar.style.width = "0%";
238
- });
239
- });
240
- let dismissed = false;
241
- const dismiss = () => {
242
- if (dismissed) return;
243
- dismissed = true;
244
- toastEl.style.opacity = "0";
245
- toastEl.style.transform = "translateX(20px)";
246
- setTimeout(() => toastEl.remove(), 300);
247
- };
248
- const timer = setTimeout(dismiss, durationMs);
249
- return {
250
- dismiss: () => {
251
- clearTimeout(timer);
252
- dismiss();
253
- }
254
- };
255
- }
256
- var RECENT_RECIPIENTS_KEY = "email_recent_recipients";
257
- function getRecentRecipients() {
258
- try {
259
- return JSON.parse(localStorage.getItem(RECENT_RECIPIENTS_KEY) || "[]");
260
- } catch {
261
- return [];
262
- }
263
- }
264
- function addRecentRecipient(email, name) {
265
- const list = getRecentRecipients();
266
- const existing = list.findIndex((r) => r.email === email);
267
- if (existing >= 0) list.splice(existing, 1);
268
- list.unshift({ name: email, email });
269
- localStorage.setItem(RECENT_RECIPIENTS_KEY, JSON.stringify(list.slice(0, 50)));
270
- }
271
- function Email() {
272
- const { isConnected, user, accessToken, connect, disconnect, loading: authLoading, error: authError, hasClientId, setClientId } = useGoogleAuth();
273
- const [messages, setMessages] = useState([]);
274
- const [loadingMsgs, setLoadingMsgs] = useState(false);
275
- const [selectedId, setSelectedId] = useState(null);
276
- const [selectedMsg, setSelectedMsg] = useState(null);
277
- const [threadMsgs, setThreadMsgs] = useState([]);
278
- const [loadingMsg, setLoadingMsg] = useState(false);
279
- const [composing, setComposing] = useState(false);
280
- const [replyTo, setReplyTo] = useState(null);
281
- const [forwardBody, setForwardBody] = useState(null);
282
- const [label, setLabel] = useState("INBOX");
283
- const [clientIdInput, setClientIdInput] = useState("");
284
- const [nextPageToken, setNextPageToken] = useState(null);
285
- const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
286
- const [searchQuery, setSearchQuery] = useState("");
287
- const [activeSearch, setActiveSearch] = useState("");
288
- const [splitPane, setSplitPane] = useState(false);
289
- const [focusIdx, setFocusIdx] = useState(-1);
290
- const listRef = useRef(null);
291
- const [userLabels, setUserLabels] = useState([]);
292
- const [spreadsheetData, setSpreadsheetData] = useState(null);
293
- const [lightbox, setLightbox] = useState(null);
294
- const [collapsedThreadMsgs, setCollapsedThreadMsgs] = useState(/* @__PURE__ */ new Set());
295
- const isSpreadsheet = (filename) => /\.(xlsx|xls|csv|tsv|ods)$/i.test(filename);
296
- const openSpreadsheet = async (msgId, attachmentId, filename) => {
297
- if (!accessToken) return;
298
- try {
299
- toast_default.info(`Opening ${filename}...`);
300
- const res = await fetch(`${GMAIL_API}/messages/${msgId}/attachments/${attachmentId}`, {
301
- headers: { Authorization: `Bearer ${accessToken}` }
302
- });
303
- const data = await res.json();
304
- if (!data.data) {
305
- toast_default.error("No attachment data");
306
- return;
307
- }
308
- const b64 = data.data.replace(/-/g, "+").replace(/_/g, "/");
309
- const byteChars = atob(b64);
310
- const byteArr = new Uint8Array(byteChars.length);
311
- for (let i = 0; i < byteChars.length; i++) byteArr[i] = byteChars.charCodeAt(i);
312
- const XLSX = await import('xlsx');
313
- const wb = XLSX.read(byteArr, { type: "array" });
314
- const sheets = {};
315
- for (const name of wb.SheetNames) {
316
- const rows = XLSX.utils.sheet_to_json(wb.Sheets[name], { header: 1, defval: "" });
317
- const maxCols = rows.reduce((m, r) => Math.max(m, r.length), 0);
318
- sheets[name] = rows.map((r) => {
319
- while (r.length < maxCols) r.push("");
320
- return r.map((c) => String(c ?? ""));
321
- });
322
- }
323
- setSpreadsheetData({ name: filename, sheetNames: wb.SheetNames, sheets });
324
- } catch (err) {
325
- toast_default.error(`Failed to open spreadsheet: ${err.message || "Unknown error"}`);
326
- }
327
- };
328
- useMemo(() => messages.filter((m) => m.isUnread).length, [messages]);
329
- useEffect(() => {
330
- const allIds = /* @__PURE__ */ new Set();
331
- let total = 0;
332
- for (const m of messages) {
333
- if (m.isUnread && !allIds.has(m.id)) {
334
- allIds.add(m.id);
335
- total++;
336
- }
337
- }
338
- for (const [, cached] of Object.entries(msgCacheRef.current)) {
339
- for (const m of cached.items) {
340
- if (m.isUnread && !allIds.has(m.id)) {
341
- allIds.add(m.id);
342
- total++;
343
- }
344
- }
345
- }
346
- setEmailUnreadCount(total);
347
- }, [messages]);
348
- const msgBodyCacheRef = useRef({});
349
- const MSG_BODY_TTL = 5 * 60 * 1e3;
350
- const latestHistoryIdRef = useRef(null);
351
- const HISTORY_ID_KEY = "email_history_id";
352
- useEffect(() => {
353
- try {
354
- latestHistoryIdRef.current = localStorage.getItem(HISTORY_ID_KEY);
355
- } catch {
356
- }
357
- }, []);
358
- const updateHistoryId = useCallback((historyId) => {
359
- if (!historyId) return;
360
- const current = latestHistoryIdRef.current;
361
- if (!current || BigInt(historyId) > BigInt(current)) {
362
- latestHistoryIdRef.current = historyId;
363
- try {
364
- localStorage.setItem(HISTORY_ID_KEY, historyId);
365
- } catch {
366
- }
367
- }
368
- }, []);
369
- useEffect(() => {
370
- if (!accessToken) return;
371
- fetch(`${GMAIL_API}/labels`, { headers: { Authorization: `Bearer ${accessToken}` } }).then((r) => r.json()).then((data) => {
372
- const labels = (data.labels || []).filter((l) => l.type === "user").sort((a, b) => a.name.localeCompare(b.name));
373
- setUserLabels(labels);
374
- }).catch(() => {
375
- });
376
- }, [accessToken]);
377
- const msgCacheRef = useRef({});
378
- const MSG_CACHE_TTL = 2 * 60 * 1e3;
379
- const PERSIST_KEY = "email_msg_cache";
380
- useEffect(() => {
381
- try {
382
- const stored = JSON.parse(localStorage.getItem(PERSIST_KEY) || "{}");
383
- for (const [key, val] of Object.entries(stored)) {
384
- const v = val;
385
- if (v.items) {
386
- v.items = v.items.map((m) => ({ ...m, date: new Date(m.date) }));
387
- msgCacheRef.current[key] = v;
388
- }
389
- }
390
- } catch {
391
- }
392
- }, []);
393
- const persistCache = useCallback(() => {
394
- try {
395
- const toSave = {};
396
- for (const [key, val] of Object.entries(msgCacheRef.current)) {
397
- if (["INBOX", "STARRED", "SENT", "DRAFT"].includes(key)) toSave[key] = val;
398
- }
399
- localStorage.setItem(PERSIST_KEY, JSON.stringify(toSave));
400
- } catch {
401
- }
402
- }, []);
403
- const fetchMessages = useCallback(async (labelId, pageToken, query, force = false) => {
404
- if (!accessToken) return;
405
- const cacheKey = query ? `q:${query}` : labelId;
406
- if (!force && !pageToken && msgCacheRef.current[cacheKey]) {
407
- const cached = msgCacheRef.current[cacheKey];
408
- if (Date.now() - cached.ts < MSG_CACHE_TTL) {
409
- setMessages(cached.items);
410
- setLoadingMsgs(false);
411
- return;
412
- }
413
- setMessages(cached.items);
414
- }
415
- if (!msgCacheRef.current[cacheKey]) setLoadingMsgs(true);
416
- try {
417
- const params = new URLSearchParams({ maxResults: "30" });
418
- if (query) {
419
- params.set("q", query);
420
- } else {
421
- params.set("labelIds", labelId);
422
- }
423
- if (pageToken) params.set("pageToken", pageToken);
424
- const res = await fetch(`${GMAIL_API}/messages?${params}`, {
425
- headers: { Authorization: `Bearer ${accessToken}` }
426
- });
427
- if (!res.ok) throw new Error("Failed to fetch messages");
428
- const data = await res.json();
429
- const msgIds = data.messages || [];
430
- setNextPageToken(data.nextPageToken || null);
431
- const details = await batchFetchMessages(accessToken, msgIds);
432
- for (const item of details) {
433
- }
434
- if (pageToken) {
435
- setMessages((prev) => {
436
- const merged = [...prev, ...details];
437
- msgCacheRef.current[cacheKey] = { items: merged, ts: Date.now() };
438
- persistCache();
439
- return merged;
440
- });
441
- } else {
442
- setMessages(details);
443
- msgCacheRef.current[cacheKey] = { items: details, ts: Date.now() };
444
- persistCache();
445
- }
446
- } catch (err) {
447
- if (!msgCacheRef.current[cacheKey]) toast_default.error(err.message || "Failed to load emails");
448
- }
449
- setLoadingMsgs(false);
450
- }, [accessToken]);
451
- useEffect(() => {
452
- if (isConnected) fetchMessages(label, void 0, activeSearch || void 0);
453
- }, [isConnected, label, fetchMessages, activeSearch]);
454
- const prefetchedRef = useRef(false);
455
- useEffect(() => {
456
- if (!isConnected || prefetchedRef.current) return;
457
- prefetchedRef.current = true;
458
- const timer = setTimeout(() => {
459
- ["STARRED", "SENT", "DRAFT"].forEach((l) => {
460
- if (l !== label && !msgCacheRef.current[l]) fetchMessages(l);
461
- });
462
- }, 3e3);
463
- return () => clearTimeout(timer);
464
- }, [isConnected, label, fetchMessages]);
465
- useCallback(async () => {
466
- if (!accessToken || !latestHistoryIdRef.current) return false;
467
- try {
468
- const params = new URLSearchParams({
469
- startHistoryId: latestHistoryIdRef.current,
470
- labelId: label,
471
- historyTypes: "messageAdded,messageDeleted,labelAdded,labelRemoved"
472
- });
473
- const res = await fetch(`${GMAIL_API}/history?${params}`, {
474
- headers: { Authorization: `Bearer ${accessToken}` }
475
- });
476
- if (res.status === 404) return false;
477
- if (!res.ok) return false;
478
- const data = await res.json();
479
- if (data.historyId) updateHistoryId(data.historyId);
480
- if (!data.history || data.history.length === 0) return true;
481
- return false;
482
- } catch {
483
- return false;
484
- }
485
- }, [accessToken, label, updateHistoryId]);
486
- const captureHistoryId = useCallback((msg) => {
487
- if (msg.historyId) updateHistoryId(msg.historyId);
488
- }, [updateHistoryId]);
489
- const openDraft = async (msgId) => {
490
- if (!accessToken) return;
491
- try {
492
- const draftsRes = await fetch(`${GMAIL_API}/drafts`, { headers: { Authorization: `Bearer ${accessToken}` } });
493
- const draftsData = await draftsRes.json();
494
- const draft = (draftsData.drafts || []).find((d) => d.message?.id === msgId);
495
- if (!draft) {
496
- openMessage(msgId);
497
- return;
498
- }
499
- const res = await fetch(`${GMAIL_API}/drafts/${draft.id}`, { headers: { Authorization: `Bearer ${accessToken}` } });
500
- const draftData = await res.json();
501
- const msg = draftData.message;
502
- const to = getHeader(msg, "To");
503
- const subject = getHeader(msg, "Subject");
504
- const body = getMessageBody(msg);
505
- setReplyTo({ to, subject, threadId: msg.threadId || "", messageId: "" });
506
- setForwardBody(null);
507
- setComposing(true);
508
- setTimeout(() => {
509
- const editor = document.querySelector('[contenteditable="true"]');
510
- if (editor) editor.innerHTML = body;
511
- }, 100);
512
- } catch {
513
- toast_default.error("Failed to open draft");
514
- }
515
- };
516
- const openMessage = async (id, threadId) => {
517
- if (!accessToken) return;
518
- setSelectedId(id);
519
- setCollapsedThreadMsgs(/* @__PURE__ */ new Set());
520
- const tId = threadId || id;
521
- const cached = msgBodyCacheRef.current[tId];
522
- if (cached && Date.now() - cached.ts < MSG_BODY_TTL) {
523
- setThreadMsgs(cached.msgs);
524
- setSelectedMsg(cached.msgs[cached.msgs.length - 1]);
525
- const collapsed = new Set(cached.msgs.slice(0, -1).map((m) => m.id));
526
- setCollapsedThreadMsgs(collapsed);
527
- } else {
528
- setLoadingMsg(true);
529
- setThreadMsgs([]);
530
- }
531
- const msgItem = messages.find((m) => m.id === id);
532
- if (msgItem?.isUnread) {
533
- setMessages((prev) => prev.map((m) => m.id === id ? { ...m, isUnread: false } : m));
534
- fetch(`${GMAIL_API}/messages/${id}/modify`, {
535
- method: "POST",
536
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
537
- body: JSON.stringify({ removeLabelIds: ["UNREAD"] })
538
- }).catch(() => {
539
- });
540
- }
541
- if (!cached || Date.now() - cached.ts >= MSG_BODY_TTL) {
542
- try {
543
- const threadRes = await fetch(`${GMAIL_API}/threads/${tId}?format=full`, {
544
- headers: { Authorization: `Bearer ${accessToken}` }
545
- });
546
- if (threadRes.ok) {
547
- const threadData = await threadRes.json();
548
- const allMsgs = threadData.messages || [];
549
- if (allMsgs.length > 0) {
550
- setThreadMsgs(allMsgs);
551
- setSelectedMsg(allMsgs[allMsgs.length - 1]);
552
- msgBodyCacheRef.current[tId] = { msgs: allMsgs, ts: Date.now() };
553
- const collapsed = new Set(allMsgs.slice(0, -1).map((m) => m.id));
554
- setCollapsedThreadMsgs(collapsed);
555
- allMsgs.forEach(captureHistoryId);
556
- }
557
- } else {
558
- const res = await fetch(`${GMAIL_API}/messages/${id}?format=full`, {
559
- headers: { Authorization: `Bearer ${accessToken}` }
560
- });
561
- const msg = await res.json();
562
- setSelectedMsg(msg);
563
- setThreadMsgs([msg]);
564
- msgBodyCacheRef.current[tId] = { msgs: [msg], ts: Date.now() };
565
- captureHistoryId(msg);
566
- }
567
- } catch {
568
- toast_default.error("Failed to load message");
569
- }
570
- setLoadingMsg(false);
571
- }
572
- };
573
- const toggleStar = async (id, isStarred) => {
574
- if (!accessToken) return;
575
- const body = isStarred ? { removeLabelIds: ["STARRED"] } : { addLabelIds: ["STARRED"] };
576
- await fetch(`${GMAIL_API}/messages/${id}/modify`, {
577
- method: "POST",
578
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
579
- body: JSON.stringify(body)
580
- });
581
- setMessages((prev) => prev.map((m) => m.id === id ? { ...m, isStarred: !isStarred } : m));
582
- };
583
- const archiveMessage = async (id) => {
584
- if (!accessToken) return;
585
- setMessages((prev) => prev.filter((m) => m.id !== id));
586
- if (selectedId === id) {
587
- setSelectedId(null);
588
- setSelectedMsg(null);
589
- setThreadMsgs([]);
590
- }
591
- toast_default.success("Archived.");
592
- fetch(`${GMAIL_API}/messages/${id}/modify`, {
593
- method: "POST",
594
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
595
- body: JSON.stringify({ removeLabelIds: ["INBOX"] })
596
- }).catch(() => toast_default.error("Failed to archive"));
597
- };
598
- const trashMessage = async (id) => {
599
- if (!accessToken) return;
600
- setMessages((prev) => prev.filter((m) => m.id !== id));
601
- if (selectedId === id) {
602
- setSelectedId(null);
603
- setSelectedMsg(null);
604
- setThreadMsgs([]);
605
- }
606
- toast_default.success("Moved to Trash.");
607
- fetch(`${GMAIL_API}/messages/${id}/trash`, {
608
- method: "POST",
609
- headers: { Authorization: `Bearer ${accessToken}` }
610
- }).catch(() => toast_default.error("Failed to delete"));
611
- };
612
- const modifyMessages = async (ids, addLabels, removeLabels) => {
613
- if (!accessToken) return;
614
- await Promise.all(ids.map(
615
- (id) => fetch(`${GMAIL_API}/messages/${id}/modify`, {
616
- method: "POST",
617
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
618
- body: JSON.stringify({ addLabelIds: addLabels, removeLabelIds: removeLabels })
619
- })
620
- ));
621
- };
622
- const bulkArchive = async () => {
623
- const ids = [...selectedIds];
624
- await modifyMessages(ids, [], ["INBOX"]);
625
- setMessages((prev) => prev.filter((m) => !selectedIds.has(m.id)));
626
- setSelectedIds(/* @__PURE__ */ new Set());
627
- toast_default.success(`Archived ${ids.length} message(s).`);
628
- };
629
- const bulkDelete = async () => {
630
- const ids = [...selectedIds];
631
- await Promise.all(ids.map((id) => fetch(`${GMAIL_API}/messages/${id}/trash`, {
632
- method: "POST",
633
- headers: { Authorization: `Bearer ${accessToken}` }
634
- })));
635
- setMessages((prev) => prev.filter((m) => !selectedIds.has(m.id)));
636
- setSelectedIds(/* @__PURE__ */ new Set());
637
- toast_default.success(`Deleted ${ids.length} message(s).`);
638
- };
639
- const bulkMarkRead = async () => {
640
- const ids = [...selectedIds];
641
- await modifyMessages(ids, [], ["UNREAD"]);
642
- setMessages((prev) => prev.map((m) => selectedIds.has(m.id) ? { ...m, isUnread: false } : m));
643
- setSelectedIds(/* @__PURE__ */ new Set());
644
- toast_default.success("Marked as read.");
645
- };
646
- const bulkMarkUnread = async () => {
647
- const ids = [...selectedIds];
648
- await modifyMessages(ids, ["UNREAD"], []);
649
- setMessages((prev) => prev.map((m) => selectedIds.has(m.id) ? { ...m, isUnread: true } : m));
650
- setSelectedIds(/* @__PURE__ */ new Set());
651
- toast_default.success("Marked as unread.");
652
- };
653
- const toggleSelect = (id) => {
654
- setSelectedIds((prev) => {
655
- const next = new Set(prev);
656
- if (next.has(id)) next.delete(id);
657
- else next.add(id);
658
- return next;
659
- });
660
- };
661
- const toggleSelectAll = () => {
662
- if (selectedIds.size === messages.length) setSelectedIds(/* @__PURE__ */ new Set());
663
- else setSelectedIds(new Set(messages.map((m) => m.id)));
664
- };
665
- const isImage = (filename) => /\.(jpg|jpeg|png|gif|webp|bmp|svg)$/i.test(filename);
666
- const downloadAttachment = async (msgId, attachmentId, filename) => {
667
- if (!accessToken) return;
668
- try {
669
- const res = await fetch(`${GMAIL_API}/messages/${msgId}/attachments/${attachmentId}`, {
670
- headers: { Authorization: `Bearer ${accessToken}` }
671
- });
672
- const data = await res.json();
673
- if (data.data) {
674
- const b64 = data.data.replace(/-/g, "+").replace(/_/g, "/");
675
- const byteChars = atob(b64);
676
- const byteArr = new Uint8Array(byteChars.length);
677
- for (let i = 0; i < byteChars.length; i++) byteArr[i] = byteChars.charCodeAt(i);
678
- const blob = new Blob([byteArr]);
679
- const url = URL.createObjectURL(blob);
680
- const a = document.createElement("a");
681
- a.href = url;
682
- a.download = filename;
683
- a.click();
684
- URL.revokeObjectURL(url);
685
- }
686
- } catch {
687
- toast_default.error("Failed to download attachment");
688
- }
689
- };
690
- const openAttachment = async (msgId, attachmentId, filename, mimeType) => {
691
- if (!accessToken) return;
692
- if (isImage(filename) || mimeType.startsWith("image/")) {
693
- try {
694
- const res = await fetch(`${GMAIL_API}/messages/${msgId}/attachments/${attachmentId}`, {
695
- headers: { Authorization: `Bearer ${accessToken}` }
696
- });
697
- const data = await res.json();
698
- if (data.data) {
699
- const b64 = data.data.replace(/-/g, "+").replace(/_/g, "/");
700
- setLightbox({ src: `data:${mimeType};base64,${b64}`, filename });
701
- }
702
- } catch {
703
- toast_default.error("Failed to open image");
704
- }
705
- } else {
706
- downloadAttachment(msgId, attachmentId, filename);
707
- }
708
- };
709
- const handleSearch = (e) => {
710
- e.preventDefault();
711
- setActiveSearch(searchQuery);
712
- setSelectedId(null);
713
- setSelectedMsg(null);
714
- setThreadMsgs([]);
715
- };
716
- const clearSearch = () => {
717
- setSearchQuery("");
718
- setActiveSearch("");
719
- };
720
- useEffect(() => {
721
- const handler = (e) => {
722
- const target = e.target;
723
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) return;
724
- if (e.key === "c" && !composing) {
725
- setComposing(true);
726
- return;
727
- }
728
- if (e.key === "e" && selectedId) {
729
- archiveMessage(selectedId);
730
- return;
731
- }
732
- if (e.key === "#" && selectedId) {
733
- trashMessage(selectedId);
734
- return;
735
- }
736
- if (e.key === "r" && selectedMsg) {
737
- e.preventDefault();
738
- setReplyTo({
739
- to: parseFrom(getHeader(selectedMsg, "From")).email,
740
- subject: `Re: ${getHeader(selectedMsg, "Subject")}`,
741
- threadId: selectedMsg.threadId,
742
- messageId: getHeader(selectedMsg, "Message-ID") || selectedMsg.id
743
- });
744
- setComposing(true);
745
- return;
746
- }
747
- if (e.key === "j") {
748
- setFocusIdx((prev) => Math.min(prev + 1, messages.length - 1));
749
- return;
750
- }
751
- if (e.key === "k") {
752
- setFocusIdx((prev) => Math.max(prev - 1, 0));
753
- return;
754
- }
755
- if (e.key === "Enter" && focusIdx >= 0 && focusIdx < messages.length && !selectedMsg) {
756
- const msg = messages[focusIdx];
757
- openMessage(msg.id, msg.threadId);
758
- return;
759
- }
760
- if (e.key === "x" && focusIdx >= 0 && focusIdx < messages.length) {
761
- toggleSelect(messages[focusIdx].id);
762
- return;
763
- }
764
- if (e.key === "Escape") {
765
- if (selectedMsg) {
766
- setSelectedId(null);
767
- setSelectedMsg(null);
768
- setThreadMsgs([]);
769
- }
770
- return;
771
- }
772
- };
773
- window.addEventListener("keydown", handler);
774
- return () => window.removeEventListener("keydown", handler);
775
- }, [selectedId, selectedMsg, messages, focusIdx]);
776
- const LABELS = [
777
- { id: "INBOX", label: "Inbox", icon: "M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.013 1.244h3.218a2.25 2.25 0 002.013-1.244l.256-.512a2.25 2.25 0 012.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H6.911a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661z" },
778
- { id: "STARRED", label: "Starred", icon: "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" },
779
- { id: "SENT", label: "Sent", icon: "M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" },
780
- { id: "DRAFT", label: "Drafts", icon: "M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" }
781
- ];
782
- if (!isConnected) {
783
- if (isDemoMode()) return /* @__PURE__ */ jsx(EmailDemoView, {});
784
- return /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full", children: /* @__PURE__ */ jsxs("div", { className: "text-center max-w-md space-y-4 px-6", children: [
785
- /* @__PURE__ */ jsx("svg", { className: "h-16 w-16 mx-auto text-gray-300", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" }) }),
786
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-gray-900", children: "Connect Gmail" }),
787
- /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-500", children: "Sign in with your Google account to access Gmail, Calendar, and Gemini AI." }),
788
- !hasClientId && /* @__PURE__ */ jsxs("div", { className: "text-left space-y-2 bg-gray-50 rounded-lg p-4", children: [
789
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium text-gray-700", children: "Google OAuth Client ID" }),
790
- /* @__PURE__ */ jsx(
791
- "input",
792
- {
793
- value: clientIdInput,
794
- onChange: (e) => setClientIdInput(e.target.value),
795
- placeholder: "123456789.apps.googleusercontent.com",
796
- className: "w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-blue-500"
797
- }
798
- ),
799
- /* @__PURE__ */ jsx(
800
- "button",
801
- {
802
- onClick: () => {
803
- if (clientIdInput.trim()) setClientId(clientIdInput.trim());
804
- },
805
- disabled: !clientIdInput.trim(),
806
- className: "w-full bg-gray-900 text-white px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-800 disabled:opacity-40",
807
- children: "Save Client ID"
808
- }
809
- ),
810
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-gray-400", children: "Create one at console.cloud.google.com > APIs > Credentials > OAuth 2.0 Client ID (Web application)" })
811
- ] }),
812
- hasClientId && /* @__PURE__ */ jsxs(
813
- "button",
814
- {
815
- onClick: connect,
816
- disabled: authLoading,
817
- className: "inline-flex items-center gap-2 bg-white border border-gray-300 shadow-sm px-6 py-2.5 text-sm font-medium rounded-lg hover:bg-gray-50 disabled:opacity-50",
818
- children: [
819
- /* @__PURE__ */ jsxs("svg", { className: "h-5 w-5", viewBox: "0 0 24 24", children: [
820
- /* @__PURE__ */ jsx("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z", fill: "#4285F4" }),
821
- /* @__PURE__ */ jsx("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }),
822
- /* @__PURE__ */ jsx("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }),
823
- /* @__PURE__ */ jsx("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })
824
- ] }),
825
- authLoading ? "Connecting..." : "Sign in with Google"
826
- ]
827
- }
828
- ),
829
- authError && /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600", children: authError })
830
- ] }) });
831
- }
832
- const renderDetail = () => {
833
- if (!selectedMsg) return null;
834
- const lastMsg = threadMsgs.length > 0 ? threadMsgs[threadMsgs.length - 1] : selectedMsg;
835
- return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
836
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-3 py-1.5 border-b border-gray-200 shrink-0", children: [
837
- /* @__PURE__ */ jsx("button", { onClick: () => {
838
- setSelectedId(null);
839
- setSelectedMsg(null);
840
- setThreadMsgs([]);
841
- }, className: "p-1 rounded hover:bg-gray-100", title: "Back (Esc)", children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 text-gray-600", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" }) }) }),
842
- /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold text-gray-900 truncate flex-1", children: getHeader(lastMsg, "Subject") }),
843
- /* @__PURE__ */ jsxs(
844
- "button",
845
- {
846
- onClick: () => archiveMessage(lastMsg.id),
847
- title: "Archive (e)",
848
- className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50",
849
- children: [
850
- /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" }) }),
851
- "Done"
852
- ]
853
- }
854
- ),
855
- /* @__PURE__ */ jsxs("button", { onClick: () => {
856
- setReplyTo({
857
- to: parseFrom(getHeader(lastMsg, "From")).email,
858
- subject: `Re: ${getHeader(lastMsg, "Subject")}`,
859
- threadId: lastMsg.threadId,
860
- messageId: getHeader(lastMsg, "Message-ID") || lastMsg.id
861
- });
862
- setComposing(true);
863
- }, className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50", title: "Reply (r)", children: [
864
- /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" }) }),
865
- "Reply"
866
- ] }),
867
- /* @__PURE__ */ jsxs("button", { onClick: () => {
868
- const allRecipients = [getHeader(lastMsg, "From"), getHeader(lastMsg, "To"), getHeader(lastMsg, "Cc")].join(",").split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseFrom(s).email).filter((e, i, a) => a.indexOf(e) === i && e !== user?.email);
869
- setReplyTo({
870
- to: allRecipients.join(", "),
871
- subject: `Re: ${getHeader(lastMsg, "Subject")}`,
872
- threadId: lastMsg.threadId,
873
- messageId: getHeader(lastMsg, "Message-ID") || lastMsg.id
874
- });
875
- setComposing(true);
876
- }, className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50", title: "Reply All", children: [
877
- /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
878
- /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" }),
879
- /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13 15L7 9m0 0l6-6", opacity: "0.5" })
880
- ] }),
881
- "All"
882
- ] }),
883
- /* @__PURE__ */ jsxs("button", { onClick: () => {
884
- const fwd = `
885
-
886
- ---------- Forwarded message ----------
887
- From: ${getHeader(lastMsg, "From")}
888
- Date: ${formatDateTime(new Date(parseInt(lastMsg.internalDate)).toISOString())}
889
- Subject: ${getHeader(lastMsg, "Subject")}
890
- To: ${getHeader(lastMsg, "To")}
891
-
892
- `;
893
- setForwardBody(fwd);
894
- setReplyTo({ to: "", subject: `Fwd: ${getHeader(lastMsg, "Subject")}`, threadId: "", messageId: "" });
895
- setComposing(true);
896
- }, className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 border border-gray-300 rounded-md hover:bg-gray-50", title: "Forward", children: [
897
- /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M15 15l6-6m0 0l-6-6m6 6H9a6 6 0 000 12h3" }) }),
898
- "Forward"
899
- ] }),
900
- /* @__PURE__ */ jsx(
901
- "button",
902
- {
903
- onClick: () => trashMessage(lastMsg.id),
904
- title: "Delete (#)",
905
- className: "inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-600 border border-gray-300 rounded-md hover:bg-red-50 hover:text-red-600 hover:border-red-300",
906
- children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" }) })
907
- }
908
- )
909
- ] }),
910
- loadingMsg ? /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-sm text-gray-400", children: "Loading..." }) : /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: threadMsgs.map((tmsg, idx) => {
911
- const from = parseFrom(getHeader(tmsg, "From"));
912
- const isLast = idx === threadMsgs.length - 1;
913
- const isCollapsed = collapsedThreadMsgs.has(tmsg.id);
914
- return /* @__PURE__ */ jsxs("div", { className: `border-b border-gray-100 ${!isLast ? "bg-gray-50/50" : ""}`, children: [
915
- /* @__PURE__ */ jsxs(
916
- "div",
917
- {
918
- className: `px-4 py-2 flex items-center gap-2 ${!isLast ? "cursor-pointer hover:bg-gray-100/50" : ""}`,
919
- onClick: () => {
920
- if (isLast) return;
921
- setCollapsedThreadMsgs((prev) => {
922
- const next = new Set(prev);
923
- if (next.has(tmsg.id)) next.delete(tmsg.id);
924
- else next.add(tmsg.id);
925
- return next;
926
- });
927
- },
928
- children: [
929
- /* @__PURE__ */ jsx("div", { className: `h-7 w-7 rounded-full flex items-center justify-center text-white text-xs font-medium shrink-0 ${avatarColor(from.name)}`, children: from.name.charAt(0).toUpperCase() }),
930
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
931
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
932
- /* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-gray-900 truncate", children: from.name }),
933
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400", children: formatDateTime(new Date(parseInt(tmsg.internalDate)).toISOString()) })
934
- ] }),
935
- isCollapsed ? /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 truncate", children: tmsg.snippet }) : /* @__PURE__ */ jsxs("p", { className: "text-xs text-gray-500 truncate", children: [
936
- "To: ",
937
- getHeader(tmsg, "To")
938
- ] })
939
- ] }),
940
- threadMsgs.length > 1 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
941
- !isLast && /* @__PURE__ */ jsx("svg", { className: `h-3 w-3 text-gray-400 transition-transform ${isCollapsed ? "-rotate-90" : ""}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 8.25l-7.5 7.5-7.5-7.5" }) }),
942
- /* @__PURE__ */ jsxs("span", { className: "text-[10px] text-gray-400", children: [
943
- idx + 1,
944
- "/",
945
- threadMsgs.length
946
- ] })
947
- ] })
948
- ]
949
- }
950
- ),
951
- !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [
952
- /* @__PURE__ */ jsx("div", { className: "px-4 pb-3", children: /* @__PURE__ */ jsx(EmailBody, { html: getMessageBody(tmsg), accessToken }) }),
953
- (() => {
954
- const msgAtts = getAttachments(tmsg);
955
- if (msgAtts.length === 0) return null;
956
- return /* @__PURE__ */ jsx("div", { className: "px-4 pb-3", children: /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: msgAtts.map((att, i) => /* @__PURE__ */ jsx("div", { children: isImageAttachment(att) ? /* @__PURE__ */ jsx(InlineImagePreview, { msgId: tmsg.id, att, accessToken, onDownload: downloadAttachment, onLightbox: (src, fn) => setLightbox({ src, filename: fn }) }) : /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center border border-gray-200 rounded-lg bg-white overflow-hidden", children: [
957
- /* @__PURE__ */ jsxs(
958
- "button",
959
- {
960
- onClick: () => openAttachment(tmsg.id, att.attachmentId, att.filename, att.mimeType),
961
- className: "inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs hover:bg-gray-50 text-gray-700",
962
- children: [
963
- /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5 text-gray-400 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" }) }),
964
- /* @__PURE__ */ jsx("span", { className: "truncate max-w-[140px]", children: att.filename })
965
- ]
966
- }
967
- ),
968
- isSpreadsheet(att.filename) && /* @__PURE__ */ jsx(
969
- "button",
970
- {
971
- onClick: () => openSpreadsheet(tmsg.id, att.attachmentId, att.filename),
972
- title: "Open in spreadsheet viewer",
973
- className: "px-2 py-1.5 text-xs text-green-700 hover:bg-green-50 border-l border-gray-200 font-medium",
974
- children: "Open"
975
- }
976
- ),
977
- /* @__PURE__ */ jsx(
978
- "button",
979
- {
980
- onClick: () => downloadAttachment(tmsg.id, att.attachmentId, att.filename),
981
- title: "Download",
982
- className: "px-1.5 py-1.5 text-gray-400 hover:bg-gray-50 border-l border-gray-200",
983
- children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) })
984
- }
985
- )
986
- ] }) }, i)) }) });
987
- })()
988
- ] })
989
- ] }, tmsg.id);
990
- }) })
991
- ] });
992
- };
993
- const renderList = () => /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", ref: listRef, children: [
994
- /* @__PURE__ */ jsxs("form", { onSubmit: handleSearch, className: "px-3 py-2 border-b border-gray-200 flex items-center gap-2 shrink-0", children: [
995
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1", children: [
996
- /* @__PURE__ */ jsx("svg", { className: "absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" }) }),
997
- /* @__PURE__ */ jsx(
998
- "input",
999
- {
1000
- value: searchQuery,
1001
- onChange: (e) => setSearchQuery(e.target.value),
1002
- placeholder: "Search mail...",
1003
- className: "w-full pl-8 pr-8 py-1.5 text-sm border border-gray-200 rounded-md focus:border-blue-400 focus:ring-1 focus:ring-blue-400 bg-gray-50 focus:bg-white"
1004
- }
1005
- ),
1006
- activeSearch && /* @__PURE__ */ jsx("button", { type: "button", onClick: clearSearch, className: "absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) })
1007
- ] }),
1008
- /* @__PURE__ */ jsx(
1009
- "button",
1010
- {
1011
- type: "button",
1012
- onClick: () => fetchMessages(label, void 0, activeSearch || void 0, true),
1013
- title: "Refresh",
1014
- className: "p-1.5 rounded-md hover:bg-gray-100 text-gray-500",
1015
- children: /* @__PURE__ */ jsx("svg", { className: `h-4 w-4 ${loadingMsgs ? "animate-spin" : ""}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M2.985 19.644l3.181-3.182" }) })
1016
- }
1017
- ),
1018
- /* @__PURE__ */ jsx(
1019
- "button",
1020
- {
1021
- type: "button",
1022
- onClick: () => setSplitPane((p) => !p),
1023
- title: splitPane ? "Full view" : "Split pane",
1024
- className: `p-1.5 rounded-md hover:bg-gray-100 ${splitPane ? "text-blue-600 bg-blue-50" : "text-gray-500"}`,
1025
- children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9 4.5v15m6-15v15M4.5 19.5h15a2.25 2.25 0 002.25-2.25V6.75a2.25 2.25 0 00-2.25-2.25h-15A2.25 2.25 0 002.25 6.75v10.5a2.25 2.25 0 002.25 2.25z" }) })
1026
- }
1027
- )
1028
- ] }),
1029
- selectedIds.size > 0 && /* @__PURE__ */ jsxs("div", { className: "px-3 py-1.5 border-b border-gray-200 bg-blue-50 flex items-center gap-2 shrink-0", children: [
1030
- /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-blue-700", children: [
1031
- selectedIds.size,
1032
- " selected"
1033
- ] }),
1034
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1 ml-auto", children: [
1035
- /* @__PURE__ */ jsx("button", { onClick: bulkArchive, className: "px-2 py-1 text-xs font-medium rounded bg-white border border-gray-300 text-gray-700 hover:bg-gray-50", children: "Archive" }),
1036
- /* @__PURE__ */ jsx("button", { onClick: bulkMarkRead, className: "px-2 py-1 text-xs font-medium rounded bg-white border border-gray-300 text-gray-700 hover:bg-gray-50", children: "Mark read" }),
1037
- /* @__PURE__ */ jsx("button", { onClick: bulkMarkUnread, className: "px-2 py-1 text-xs font-medium rounded bg-white border border-gray-300 text-gray-700 hover:bg-gray-50", children: "Mark unread" }),
1038
- /* @__PURE__ */ jsx("button", { onClick: bulkDelete, className: "px-2 py-1 text-xs font-medium rounded bg-white border border-red-300 text-red-600 hover:bg-red-50", children: "Delete" }),
1039
- /* @__PURE__ */ jsx("button", { onClick: () => setSelectedIds(/* @__PURE__ */ new Set()), className: "px-2 py-1 text-xs text-gray-500 hover:text-gray-700", children: "Clear" })
1040
- ] })
1041
- ] }),
1042
- messages.length > 0 && selectedIds.size === 0 && /* @__PURE__ */ jsx("div", { className: "px-3 py-1 border-b border-gray-100 shrink-0", children: /* @__PURE__ */ jsxs("label", { className: "inline-flex items-center gap-2 text-xs text-gray-400 cursor-pointer hover:text-gray-600", children: [
1043
- /* @__PURE__ */ jsx(
1044
- "input",
1045
- {
1046
- type: "checkbox",
1047
- checked: selectedIds.size === messages.length,
1048
- onChange: toggleSelectAll,
1049
- className: "h-3 w-3 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
1050
- }
1051
- ),
1052
- "Select all"
1053
- ] }) }),
1054
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto", children: loadingMsgs && messages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12 text-sm text-gray-400", children: "Loading..." }) : messages.length === 0 ? /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-12 text-sm text-gray-400", children: activeSearch ? "No results" : "No messages" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
1055
- messages.map((msg, idx) => {
1056
- const cat = getCategoryLabel(msg.labelIds);
1057
- return /* @__PURE__ */ jsxs(
1058
- "div",
1059
- {
1060
- onClick: () => {
1061
- if (label === "DRAFT") {
1062
- openDraft(msg.id);
1063
- } else {
1064
- openMessage(msg.id, msg.threadId);
1065
- }
1066
- },
1067
- className: `w-full text-left px-3 py-2 border-b border-gray-100 flex items-center gap-2 hover:bg-gray-50 transition-colors cursor-pointer group
1068
- ${msg.isUnread ? "bg-blue-50/40" : ""} ${focusIdx === idx ? "ring-1 ring-inset ring-blue-400" : ""} ${selectedIds.has(msg.id) ? "bg-blue-50" : ""}`,
1069
- children: [
1070
- /* @__PURE__ */ jsx(
1071
- "input",
1072
- {
1073
- type: "checkbox",
1074
- checked: selectedIds.has(msg.id),
1075
- onChange: (e) => {
1076
- e.stopPropagation();
1077
- toggleSelect(msg.id);
1078
- },
1079
- onClick: (e) => e.stopPropagation(),
1080
- className: "h-3.5 w-3.5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
1081
- }
1082
- ),
1083
- /* @__PURE__ */ jsx(
1084
- "button",
1085
- {
1086
- onClick: (e) => {
1087
- e.stopPropagation();
1088
- toggleStar(msg.id, msg.isStarred);
1089
- },
1090
- className: `shrink-0 ${msg.isStarred ? "text-yellow-500" : "text-gray-300 hover:text-yellow-400"}`,
1091
- children: /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: msg.isStarred ? "currentColor" : "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" }) })
1092
- }
1093
- ),
1094
- /* @__PURE__ */ jsx("div", { className: `h-7 w-7 rounded-full flex items-center justify-center text-white text-xs font-medium shrink-0 ${avatarColor(msg.from)}`, children: msg.from.charAt(0).toUpperCase() }),
1095
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1096
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
1097
- /* @__PURE__ */ jsx("span", { className: `text-sm truncate ${msg.isUnread ? "font-semibold text-gray-900" : "text-gray-700"}`, children: msg.from }),
1098
- msg.threadCount > 1 && /* @__PURE__ */ jsx("span", { className: "text-[10px] px-1 py-0.5 rounded bg-gray-200 text-gray-600 font-medium", children: msg.threadCount }),
1099
- cat && /* @__PURE__ */ jsx("span", { className: `text-[10px] px-1.5 py-0.5 rounded-full font-medium ${cat.color}`, children: cat.text }),
1100
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 shrink-0 ml-auto", children: [
1101
- msg.attachmentCount > 0 && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5 text-gray-400", children: [
1102
- /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M18.375 12.739l-7.693 7.693a4.5 4.5 0 01-6.364-6.364l10.94-10.94A3 3 0 1119.5 7.372L8.552 18.32m.009-.01l-.01.01m5.699-9.941l-7.81 7.81a1.5 1.5 0 002.112 2.13" }) }),
1103
- msg.attachmentCount > 1 && /* @__PURE__ */ jsx("span", { className: "text-[10px]", children: msg.attachmentCount })
1104
- ] }),
1105
- /* @__PURE__ */ jsx(
1106
- "button",
1107
- {
1108
- onClick: (e) => {
1109
- e.stopPropagation();
1110
- archiveMessage(msg.id);
1111
- },
1112
- title: "Archive",
1113
- className: "opacity-0 group-hover:opacity-100 text-gray-300 hover:text-gray-600 transition-all p-0.5",
1114
- children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" }) })
1115
- }
1116
- ),
1117
- /* @__PURE__ */ jsx(
1118
- "button",
1119
- {
1120
- onClick: (e) => {
1121
- e.stopPropagation();
1122
- trashMessage(msg.id);
1123
- },
1124
- title: "Delete",
1125
- className: "opacity-0 group-hover:opacity-100 text-gray-300 hover:text-red-500 transition-all p-0.5",
1126
- children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" }) })
1127
- }
1128
- ),
1129
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 ml-1 w-10 text-right", children: timeAgo(msg.date) })
1130
- ] })
1131
- ] }),
1132
- /* @__PURE__ */ jsx("p", { className: `text-sm truncate ${msg.isUnread ? "font-medium text-gray-900" : "text-gray-600"}`, children: msg.subject }),
1133
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-400 truncate", children: msg.snippet })
1134
- ] })
1135
- ]
1136
- },
1137
- msg.id
1138
- );
1139
- }),
1140
- nextPageToken && /* @__PURE__ */ jsx("div", { className: "py-3 text-center", children: /* @__PURE__ */ jsx(
1141
- "button",
1142
- {
1143
- onClick: () => fetchMessages(label, nextPageToken, activeSearch || void 0),
1144
- disabled: loadingMsgs,
1145
- className: "text-sm text-blue-600 hover:text-blue-800 font-medium disabled:opacity-50",
1146
- children: loadingMsgs ? "Loading..." : "Load more"
1147
- }
1148
- ) })
1149
- ] }) })
1150
- ] });
1151
- return /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
1152
- /* @__PURE__ */ jsxs("div", { className: "w-48 shrink-0 border-r border-gray-200 flex flex-col", children: [
1153
- /* @__PURE__ */ jsx("div", { className: "p-2", children: /* @__PURE__ */ jsxs(
1154
- "button",
1155
- {
1156
- onClick: () => setComposing(true),
1157
- className: "w-full flex items-center justify-center gap-1.5 rounded-lg bg-blue-600 text-white px-3 py-2 text-sm font-medium hover:bg-blue-700",
1158
- children: [
1159
- /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z" }) }),
1160
- "Compose"
1161
- ]
1162
- }
1163
- ) }),
1164
- /* @__PURE__ */ jsxs("nav", { className: "flex-1 px-1 space-y-0.5", children: [
1165
- LABELS.map((l) => /* @__PURE__ */ jsxs(
1166
- "button",
1167
- {
1168
- onClick: () => {
1169
- setLabel(l.id);
1170
- setSelectedId(null);
1171
- setSelectedMsg(null);
1172
- setThreadMsgs([]);
1173
- clearSearch();
1174
- },
1175
- className: `w-full flex items-center gap-2 rounded-md px-3 py-1.5 text-sm transition-colors ${label === l.id && !activeSearch ? "bg-blue-100 text-blue-800 font-medium" : "text-gray-700 hover:bg-gray-100"}`,
1176
- children: [
1177
- /* @__PURE__ */ jsx("svg", { className: "h-4 w-4 shrink-0", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: l.icon }) }),
1178
- /* @__PURE__ */ jsx("span", { className: "flex-1 text-left", children: l.label }),
1179
- (() => {
1180
- const cached = l.id === label ? messages : msgCacheRef.current[l.id]?.items;
1181
- const count = cached?.filter((m) => m.isUnread).length || 0;
1182
- return count > 0 ? /* @__PURE__ */ jsx("span", { className: "bg-blue-600 text-white text-[10px] font-bold rounded-full px-1.5 py-0.5 min-w-[18px] text-center", children: count }) : null;
1183
- })()
1184
- ]
1185
- },
1186
- l.id
1187
- )),
1188
- userLabels.length > 0 && /* @__PURE__ */ jsx(
1189
- LabelTree,
1190
- {
1191
- labels: userLabels,
1192
- activeLabel: label,
1193
- activeSearch,
1194
- onSelect: (id) => {
1195
- setLabel(id);
1196
- setSelectedId(null);
1197
- setSelectedMsg(null);
1198
- setThreadMsgs([]);
1199
- clearSearch();
1200
- }
1201
- }
1202
- )
1203
- ] }),
1204
- /* @__PURE__ */ jsx("div", { className: "p-3 border-t border-gray-200", children: /* @__PURE__ */ jsxs("button", { onClick: () => window.dispatchEvent(new Event("open-google-connect")), className: "flex items-center gap-2 w-full hover:bg-gray-50 rounded-md p-1 transition-colors", title: "Google Services", children: [
1205
- user?.picture ? /* @__PURE__ */ jsx("img", { src: user.picture, alt: "", className: "h-6 w-6 rounded-full" }) : /* @__PURE__ */ jsx("div", { className: "h-6 w-6 rounded-full bg-gray-200" }),
1206
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 text-left", children: [
1207
- /* @__PURE__ */ jsx("p", { className: "text-[11px] font-medium text-gray-900 truncate", children: user?.name }),
1208
- /* @__PURE__ */ jsx("p", { className: "text-[10px] text-gray-500 truncate", children: user?.email })
1209
- ] })
1210
- ] }) })
1211
- ] }),
1212
- splitPane ? /* @__PURE__ */ jsxs("div", { className: "flex-1 flex", children: [
1213
- /* @__PURE__ */ jsx("div", { className: "w-[340px] shrink-0 border-r border-gray-200 flex flex-col overflow-hidden", children: renderList() }),
1214
- /* @__PURE__ */ jsx("div", { className: "flex-1 flex flex-col overflow-hidden", children: selectedMsg ? renderDetail() : /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-sm text-gray-400", children: "Select a message" }) })
1215
- ] }) : selectedMsg ? renderDetail() : renderList(),
1216
- composing && /* @__PURE__ */ jsx(
1217
- ComposeEmail,
1218
- {
1219
- accessToken,
1220
- userEmail: user?.email || "",
1221
- replyTo,
1222
- forwardBody,
1223
- onClose: () => {
1224
- setComposing(false);
1225
- setReplyTo(null);
1226
- setForwardBody(null);
1227
- },
1228
- onSent: () => {
1229
- setComposing(false);
1230
- setReplyTo(null);
1231
- setForwardBody(null);
1232
- fetchMessages(label, void 0, activeSearch || void 0, true);
1233
- }
1234
- }
1235
- ),
1236
- spreadsheetData && /* @__PURE__ */ jsx(SpreadsheetViewer, { data: spreadsheetData, onClose: () => setSpreadsheetData(null) }),
1237
- lightbox && /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[9999] bg-black/80 flex items-center justify-center", onClick: () => setLightbox(null), children: /* @__PURE__ */ jsxs("div", { className: "relative max-w-[90vw] max-h-[90vh]", onClick: (e) => e.stopPropagation(), children: [
1238
- /* @__PURE__ */ jsx("img", { src: lightbox.src, alt: lightbox.filename, className: "max-w-full max-h-[85vh] object-contain rounded-lg" }),
1239
- /* @__PURE__ */ jsxs("div", { className: "absolute top-2 right-2 flex gap-1", children: [
1240
- /* @__PURE__ */ jsx(
1241
- "a",
1242
- {
1243
- href: lightbox.src,
1244
- download: lightbox.filename,
1245
- title: "Download",
1246
- className: "p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors",
1247
- children: /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) })
1248
- }
1249
- ),
1250
- /* @__PURE__ */ jsx(
1251
- "button",
1252
- {
1253
- onClick: () => setLightbox(null),
1254
- title: "Close",
1255
- className: "p-2 rounded-full bg-black/50 text-white hover:bg-black/70 transition-colors",
1256
- children: /* @__PURE__ */ jsx("svg", { className: "h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) })
1257
- }
1258
- )
1259
- ] }),
1260
- /* @__PURE__ */ jsx("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 bg-black/50 text-white text-xs px-3 py-1 rounded-full", children: lightbox.filename })
1261
- ] }) })
1262
- ] });
1263
- }
1264
- function InlineImagePreview({ msgId, att, accessToken, onDownload, onLightbox }) {
1265
- const [src, setSrc] = useState(null);
1266
- const [expanded, setExpanded] = useState(false);
1267
- useEffect(() => {
1268
- let cancelled = false;
1269
- fetch(`${GMAIL_API}/messages/${msgId}/attachments/${att.attachmentId}`, {
1270
- headers: { Authorization: `Bearer ${accessToken}` }
1271
- }).then((r) => r.json()).then((data) => {
1272
- if (cancelled || !data.data) return;
1273
- const b64 = data.data.replace(/-/g, "+").replace(/_/g, "/");
1274
- setSrc(`data:${att.mimeType};base64,${b64}`);
1275
- }).catch(() => {
1276
- });
1277
- return () => {
1278
- cancelled = true;
1279
- };
1280
- }, [msgId, att.attachmentId, att.mimeType, accessToken]);
1281
- return /* @__PURE__ */ jsxs("div", { className: "inline-flex flex-col border border-gray-200 rounded-lg bg-white overflow-hidden", children: [
1282
- src ? /* @__PURE__ */ jsx("button", { onClick: () => {
1283
- if (expanded && onLightbox) onLightbox(src, att.filename);
1284
- else setExpanded(!expanded);
1285
- }, className: "block hover:opacity-90 transition-opacity", children: /* @__PURE__ */ jsx("img", { src, alt: att.filename, className: expanded ? "max-w-md max-h-96 object-contain" : "w-24 h-24 object-cover" }) }) : /* @__PURE__ */ jsx("div", { className: "w-24 h-24 flex items-center justify-center bg-gray-50 text-gray-400 text-xs", children: "Loading..." }),
1286
- /* @__PURE__ */ jsxs("div", { className: "flex items-center border-t border-gray-200", children: [
1287
- /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-500 truncate px-2 flex-1 max-w-[120px]", children: att.filename }),
1288
- /* @__PURE__ */ jsx(
1289
- "button",
1290
- {
1291
- onClick: () => onDownload(msgId, att.attachmentId, att.filename),
1292
- title: "Download",
1293
- className: "px-1.5 py-1 text-gray-400 hover:bg-gray-50 border-l border-gray-200",
1294
- children: /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" }) })
1295
- }
1296
- )
1297
- ] })
1298
- ] });
1299
- }
1300
- function RichTextToolbar() {
1301
- const exec = (cmd, val) => {
1302
- document.execCommand(cmd, false, val);
1303
- };
1304
- return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-0.5 px-2 py-1 border-b border-gray-200 bg-gray-50", children: [
1305
- /* @__PURE__ */ jsx(
1306
- "button",
1307
- {
1308
- type: "button",
1309
- onMouseDown: (e) => {
1310
- e.preventDefault();
1311
- exec("bold");
1312
- },
1313
- title: "Bold",
1314
- className: "p-1.5 rounded hover:bg-gray-200 text-gray-600 text-xs font-bold",
1315
- children: "B"
1316
- }
1317
- ),
1318
- /* @__PURE__ */ jsx(
1319
- "button",
1320
- {
1321
- type: "button",
1322
- onMouseDown: (e) => {
1323
- e.preventDefault();
1324
- exec("italic");
1325
- },
1326
- title: "Italic",
1327
- className: "p-1.5 rounded hover:bg-gray-200 text-gray-600 text-xs italic",
1328
- children: "I"
1329
- }
1330
- ),
1331
- /* @__PURE__ */ jsx(
1332
- "button",
1333
- {
1334
- type: "button",
1335
- onMouseDown: (e) => {
1336
- e.preventDefault();
1337
- exec("underline");
1338
- },
1339
- title: "Underline",
1340
- className: "p-1.5 rounded hover:bg-gray-200 text-gray-600 text-xs underline",
1341
- children: "U"
1342
- }
1343
- ),
1344
- /* @__PURE__ */ jsx("div", { className: "w-px h-4 bg-gray-300 mx-1" }),
1345
- /* @__PURE__ */ jsx(
1346
- "button",
1347
- {
1348
- type: "button",
1349
- onMouseDown: (e) => {
1350
- e.preventDefault();
1351
- const url = prompt("Enter URL:");
1352
- if (url) exec("createLink", url);
1353
- },
1354
- title: "Link",
1355
- className: "p-1.5 rounded hover:bg-gray-200 text-gray-600",
1356
- children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" }) })
1357
- }
1358
- ),
1359
- /* @__PURE__ */ jsx(
1360
- "button",
1361
- {
1362
- type: "button",
1363
- onMouseDown: (e) => {
1364
- e.preventDefault();
1365
- exec("insertUnorderedList");
1366
- },
1367
- title: "Bullet list",
1368
- className: "p-1.5 rounded hover:bg-gray-200 text-gray-600",
1369
- children: /* @__PURE__ */ jsx("svg", { className: "h-3.5 w-3.5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" }) })
1370
- }
1371
- )
1372
- ] });
1373
- }
1374
- function ContactAutocomplete({ value, onChange, className }) {
1375
- const [suggestions, setSuggestions] = useState([]);
1376
- const [showSuggestions, setShowSuggestions] = useState(false);
1377
- const [selectedIdx, setSelectedIdx] = useState(-1);
1378
- const inputRef = useRef(null);
1379
- const suggestionsRef = useRef(null);
1380
- useEffect(() => {
1381
- const handler = (e) => {
1382
- if (suggestionsRef.current && !suggestionsRef.current.contains(e.target) && inputRef.current && !inputRef.current.contains(e.target)) {
1383
- setShowSuggestions(false);
1384
- }
1385
- };
1386
- document.addEventListener("mousedown", handler);
1387
- return () => document.removeEventListener("mousedown", handler);
1388
- }, []);
1389
- const handleInputChange = (val) => {
1390
- onChange(val);
1391
- const parts = val.split(",");
1392
- const currentPart = parts[parts.length - 1].trim().toLowerCase();
1393
- if (currentPart.length < 2) {
1394
- setSuggestions([]);
1395
- setShowSuggestions(false);
1396
- return;
1397
- }
1398
- const recent = getRecentRecipients();
1399
- const matches = recent.filter(
1400
- (r) => r.email.toLowerCase().includes(currentPart) || r.name.toLowerCase().includes(currentPart)
1401
- ).slice(0, 8);
1402
- setSuggestions(matches);
1403
- setShowSuggestions(matches.length > 0);
1404
- setSelectedIdx(-1);
1405
- };
1406
- const selectSuggestion = (s) => {
1407
- const parts = value.split(",");
1408
- parts[parts.length - 1] = ` ${s.email}`;
1409
- onChange(parts.join(",") + ", ");
1410
- setShowSuggestions(false);
1411
- inputRef.current?.focus();
1412
- };
1413
- const handleKeyDown = (e) => {
1414
- if (!showSuggestions) return;
1415
- if (e.key === "ArrowDown") {
1416
- e.preventDefault();
1417
- setSelectedIdx((prev) => Math.min(prev + 1, suggestions.length - 1));
1418
- }
1419
- if (e.key === "ArrowUp") {
1420
- e.preventDefault();
1421
- setSelectedIdx((prev) => Math.max(prev - 1, 0));
1422
- }
1423
- if (e.key === "Enter" && selectedIdx >= 0) {
1424
- e.preventDefault();
1425
- selectSuggestion(suggestions[selectedIdx]);
1426
- }
1427
- if (e.key === "Escape") setShowSuggestions(false);
1428
- };
1429
- return /* @__PURE__ */ jsxs("div", { className: "relative", children: [
1430
- /* @__PURE__ */ jsx(
1431
- "input",
1432
- {
1433
- ref: inputRef,
1434
- value,
1435
- onChange: (e) => handleInputChange(e.target.value),
1436
- onKeyDown: handleKeyDown,
1437
- onFocus: () => {
1438
- if (suggestions.length > 0) setShowSuggestions(true);
1439
- },
1440
- className,
1441
- placeholder: "recipient@example.com",
1442
- autoFocus: true
1443
- }
1444
- ),
1445
- showSuggestions && /* @__PURE__ */ jsx("div", { ref: suggestionsRef, className: "absolute z-50 left-0 right-0 top-full mt-1 rounded-2xl max-h-48 overflow-y-auto", style: glassStyle(), children: suggestions.map((s, i) => /* @__PURE__ */ jsxs(
1446
- "button",
1447
- {
1448
- onClick: () => selectSuggestion(s),
1449
- className: `w-full text-left px-3 py-2 text-sm hover:bg-gray-50 flex items-center gap-2 ${i === selectedIdx ? "bg-blue-50" : ""}`,
1450
- children: [
1451
- /* @__PURE__ */ jsx("div", { className: `h-6 w-6 rounded-full flex items-center justify-center text-white text-xs font-medium shrink-0 ${avatarColor(s.name)}`, children: s.name.charAt(0).toUpperCase() }),
1452
- /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
1453
- /* @__PURE__ */ jsx("p", { className: "text-sm text-gray-900 truncate", children: s.name }),
1454
- /* @__PURE__ */ jsx("p", { className: "text-xs text-gray-500 truncate", children: s.email })
1455
- ] })
1456
- ]
1457
- },
1458
- s.email
1459
- )) })
1460
- ] });
1461
- }
1462
- function ComposeEmail({ accessToken, userEmail, replyTo, forwardBody, onClose, onSent }) {
1463
- const [to, setTo] = useState(replyTo?.to || "");
1464
- const [subject, setSubject] = useState(replyTo?.subject || "");
1465
- const [sending, setSending] = useState(false);
1466
- const editorRef = useRef(null);
1467
- const [draftId, setDraftId] = useState(null);
1468
- const draftTimerRef = useRef(null);
1469
- const [showSignatureEditor, setShowSignatureEditor] = useState(false);
1470
- const [signatureText, setSignatureText] = useState(() => localStorage.getItem("email_signature") || "");
1471
- const isForward = !!forwardBody;
1472
- const isReply = !!replyTo && !isForward;
1473
- useEffect(() => {
1474
- if (editorRef.current && forwardBody) {
1475
- editorRef.current.innerHTML = `<br><br><pre style="white-space:pre-wrap;font-family:inherit;color:#666;">${forwardBody.replace(/</g, "&lt;")}</pre>`;
1476
- } else if (editorRef.current) {
1477
- const sig = localStorage.getItem("email_signature");
1478
- if (sig) {
1479
- editorRef.current.innerHTML = `<br><br><div style="color:#888;border-top:1px solid #eee;padding-top:8px;margin-top:8px;">--<br>${sig.replace(/\n/g, "<br>")}</div>`;
1480
- }
1481
- }
1482
- }, [forwardBody]);
1483
- useEffect(() => {
1484
- draftTimerRef.current = setInterval(async () => {
1485
- if (!editorRef.current) return;
1486
- const bodyHtml = editorRef.current.innerHTML;
1487
- if (!bodyHtml.trim() && !to.trim() && !subject.trim()) return;
1488
- try {
1489
- const headers = [
1490
- `From: ${userEmail}`,
1491
- `To: ${to}`,
1492
- `Subject: ${subject}`,
1493
- "Content-Type: text/html; charset=utf-8"
1494
- ];
1495
- const rawMessage = [...headers, "", bodyHtml].join("\r\n");
1496
- const encoded = btoa(unescape(encodeURIComponent(rawMessage))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1497
- const draftPayload = { message: { raw: encoded } };
1498
- if (isReply && replyTo?.threadId) draftPayload.message.threadId = replyTo.threadId;
1499
- if (draftId) {
1500
- await fetch(`${GMAIL_API}/drafts/${draftId}`, {
1501
- method: "PUT",
1502
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
1503
- body: JSON.stringify(draftPayload)
1504
- });
1505
- } else {
1506
- const res = await fetch(`${GMAIL_API}/drafts`, {
1507
- method: "POST",
1508
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
1509
- body: JSON.stringify(draftPayload)
1510
- });
1511
- if (res.ok) {
1512
- const data = await res.json();
1513
- setDraftId(data.id);
1514
- }
1515
- }
1516
- } catch {
1517
- }
1518
- }, 3e4);
1519
- return () => {
1520
- if (draftTimerRef.current) clearInterval(draftTimerRef.current);
1521
- };
1522
- }, [accessToken, to, subject, draftId, userEmail, isReply, replyTo]);
1523
- const handleSend = async () => {
1524
- if (!to.trim() || !subject.trim()) {
1525
- toast_default.error("To and Subject are required.");
1526
- return;
1527
- }
1528
- if (!editorRef.current) return;
1529
- setSending(true);
1530
- const bodyHtml = editorRef.current.innerHTML;
1531
- let cancelled = false;
1532
- const undoToast = showUndoToast("Sending email...", () => {
1533
- cancelled = true;
1534
- setSending(false);
1535
- toast_default.info("Send cancelled.");
1536
- }, 5e3);
1537
- await new Promise((resolve) => setTimeout(resolve, 5e3));
1538
- if (cancelled) return;
1539
- undoToast.dismiss();
1540
- try {
1541
- const headers = [
1542
- `From: ${userEmail}`,
1543
- `To: ${to}`,
1544
- `Subject: ${subject}`,
1545
- "Content-Type: text/html; charset=utf-8"
1546
- ];
1547
- if (isReply && replyTo?.messageId) {
1548
- headers.push(`In-Reply-To: ${replyTo.messageId}`);
1549
- headers.push(`References: ${replyTo.messageId}`);
1550
- }
1551
- const rawMessage = [...headers, "", bodyHtml].join("\r\n");
1552
- const encoded = btoa(unescape(encodeURIComponent(rawMessage))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1553
- const payload = { raw: encoded };
1554
- if (isReply && replyTo?.threadId) payload.threadId = replyTo.threadId;
1555
- const res = await fetch(`${GMAIL_API}/messages/send`, {
1556
- method: "POST",
1557
- headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
1558
- body: JSON.stringify(payload)
1559
- });
1560
- if (!res.ok) throw new Error("Failed to send");
1561
- to.split(",").map((s) => s.trim()).filter(Boolean).forEach((email) => {
1562
- addRecentRecipient(email);
1563
- });
1564
- if (draftId) {
1565
- fetch(`${GMAIL_API}/drafts/${draftId}`, {
1566
- method: "DELETE",
1567
- headers: { Authorization: `Bearer ${accessToken}` }
1568
- }).catch(() => {
1569
- });
1570
- }
1571
- toast_default.success("Email sent.");
1572
- onSent();
1573
- } catch (err) {
1574
- toast_default.error(err.message || "Failed to send email.");
1575
- }
1576
- setSending(false);
1577
- };
1578
- const saveSignature = () => {
1579
- localStorage.setItem("email_signature", signatureText);
1580
- setShowSignatureEditor(false);
1581
- toast_default.success("Signature saved.");
1582
- };
1583
- const inp = "block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm";
1584
- return /* @__PURE__ */ jsxs(Modal, { open: true, onClose, title: /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: isForward ? "Forward" : isReply ? "Reply" : "New Email" }), size: "md", children: [
1585
- /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1586
- /* @__PURE__ */ jsxs("div", { children: [
1587
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium text-gray-500 mb-1", children: "To" }),
1588
- /* @__PURE__ */ jsx(ContactAutocomplete, { value: to, onChange: setTo, className: inp })
1589
- ] }),
1590
- /* @__PURE__ */ jsxs("div", { children: [
1591
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium text-gray-500 mb-1", children: "Subject" }),
1592
- /* @__PURE__ */ jsx("input", { value: subject, onChange: (e) => setSubject(e.target.value), className: inp })
1593
- ] }),
1594
- /* @__PURE__ */ jsxs("div", { children: [
1595
- /* @__PURE__ */ jsx("label", { className: "block text-xs font-medium text-gray-500 mb-1", children: "Message" }),
1596
- /* @__PURE__ */ jsxs("div", { className: "border border-gray-300 rounded-md overflow-hidden shadow-sm focus-within:border-blue-500 focus-within:ring-1 focus-within:ring-blue-500", children: [
1597
- /* @__PURE__ */ jsx(RichTextToolbar, {}),
1598
- /* @__PURE__ */ jsx(
1599
- "div",
1600
- {
1601
- ref: editorRef,
1602
- contentEditable: true,
1603
- suppressContentEditableWarning: true,
1604
- className: "px-3 py-2 min-h-[200px] max-h-[400px] overflow-y-auto text-sm focus:outline-none",
1605
- style: { whiteSpace: "pre-wrap" }
1606
- }
1607
- )
1608
- ] })
1609
- ] }),
1610
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1611
- signatureText ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs text-gray-500", children: [
1612
- /* @__PURE__ */ jsx("svg", { className: "h-3 w-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4.5 12.75l6 6 9-13.5" }) }),
1613
- "Signature attached",
1614
- /* @__PURE__ */ jsx("button", { onClick: () => setShowSignatureEditor(true), className: "text-blue-600 hover:text-blue-800", children: "Edit" })
1615
- ] }) : /* @__PURE__ */ jsx("button", { onClick: () => setShowSignatureEditor(true), className: "text-xs text-blue-600 hover:text-blue-800", children: "Add signature" }),
1616
- draftId && /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 ml-auto", children: "Draft saved" })
1617
- ] })
1618
- ] }),
1619
- /* @__PURE__ */ jsx(ModalActions, { children: /* @__PURE__ */ jsxs(
1620
- "button",
1621
- {
1622
- onClick: handleSend,
1623
- disabled: sending,
1624
- className: "inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 text-sm font-medium rounded-lg hover:bg-blue-700 disabled:opacity-50",
1625
- children: [
1626
- /* @__PURE__ */ jsx("svg", { className: "h-4 w-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" }) }),
1627
- sending ? "Sending..." : "Send"
1628
- ]
1629
- }
1630
- ) }),
1631
- showSignatureEditor && /* @__PURE__ */ jsxs(Modal, { open: true, onClose: () => setShowSignatureEditor(false), title: "Edit Signature", size: "sm", children: [
1632
- /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1633
- /* @__PURE__ */ jsx(
1634
- "textarea",
1635
- {
1636
- value: signatureText,
1637
- onChange: (e) => setSignatureText(e.target.value),
1638
- rows: 6,
1639
- placeholder: "Your email signature...",
1640
- className: "block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1641
- }
1642
- ),
1643
- signatureText && /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-500", children: [
1644
- /* @__PURE__ */ jsx("p", { className: "font-medium mb-1", children: "Preview:" }),
1645
- /* @__PURE__ */ jsx("div", { className: "border-t border-gray-200 pt-2 whitespace-pre-wrap", children: signatureText })
1646
- ] })
1647
- ] }),
1648
- /* @__PURE__ */ jsx(ModalActions, { children: /* @__PURE__ */ jsx(
1649
- "button",
1650
- {
1651
- onClick: saveSignature,
1652
- className: "bg-blue-600 text-white px-4 py-2 text-sm font-medium rounded-lg hover:bg-blue-700",
1653
- children: "Save"
1654
- }
1655
- ) })
1656
- ] })
1657
- ] });
1658
- }
1659
- function EmailBody({ html, accessToken }) {
1660
- const iframeRef = useRef(null);
1661
- useEffect(() => {
1662
- const iframe = iframeRef.current;
1663
- if (!iframe) return;
1664
- const doc = iframe.contentDocument;
1665
- if (!doc) return;
1666
- doc.open();
1667
- doc.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><style>
1668
- body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; line-height: 1.5; color: #333; word-wrap: break-word; }
1669
- img { max-width: 100%; height: auto; }
1670
- a { color: #2563eb; }
1671
- blockquote { border-left: 3px solid #ddd; margin: 8px 0; padding-left: 12px; color: #666; }
1672
- pre { white-space: pre-wrap; font-family: inherit; }
1673
- </style></head><body>${html}</body></html>`);
1674
- doc.close();
1675
- const resize = () => {
1676
- if (iframe.contentDocument?.body) {
1677
- iframe.style.height = iframe.contentDocument.body.scrollHeight + "px";
1678
- }
1679
- };
1680
- setTimeout(resize, 100);
1681
- setTimeout(resize, 500);
1682
- const imgs = doc.querySelectorAll("img[data-cid-attachment]");
1683
- imgs.forEach(async (img) => {
1684
- const attachmentId = img.getAttribute("data-cid-attachment");
1685
- const mimeType = img.getAttribute("data-cid-mime") || "image/png";
1686
- const msgId = img.getAttribute("data-msg-id");
1687
- if (!attachmentId || !msgId) return;
1688
- try {
1689
- const res = await fetch(`${GMAIL_API}/messages/${msgId}/attachments/${attachmentId}`, {
1690
- headers: { Authorization: `Bearer ${accessToken}` }
1691
- });
1692
- if (!res.ok) return;
1693
- const data = await res.json();
1694
- if (data.data) {
1695
- const b64 = data.data.replace(/-/g, "+").replace(/_/g, "/");
1696
- img.src = `data:${mimeType};base64,${b64}`;
1697
- setTimeout(resize, 100);
1698
- }
1699
- } catch {
1700
- }
1701
- });
1702
- doc.addEventListener("click", (e) => {
1703
- const a = e.target.closest("a");
1704
- if (a?.href) {
1705
- e.preventDefault();
1706
- window.open(a.href, "_blank");
1707
- }
1708
- });
1709
- }, [html, accessToken]);
1710
- return /* @__PURE__ */ jsx(
1711
- "iframe",
1712
- {
1713
- ref: iframeRef,
1714
- className: "w-full border-0",
1715
- style: { minHeight: 100 },
1716
- sandbox: "allow-same-origin",
1717
- title: "Email content"
1718
- }
1719
- );
1720
- }
1721
- function buildLabelTree(labels) {
1722
- const root = [];
1723
- const map = /* @__PURE__ */ new Map();
1724
- const sorted = [...labels].sort((a, b) => a.name.localeCompare(b.name));
1725
- for (const l of sorted) {
1726
- const parts = l.name.split("/");
1727
- const leafName = parts[parts.length - 1];
1728
- const node = { id: l.id, name: leafName, children: [] };
1729
- map.set(l.name, node);
1730
- if (parts.length === 1) {
1731
- root.push(node);
1732
- } else {
1733
- const parentPath = parts.slice(0, -1).join("/");
1734
- const parent = map.get(parentPath);
1735
- if (parent) {
1736
- parent.children.push(node);
1737
- } else {
1738
- root.push(node);
1739
- }
1740
- }
1741
- }
1742
- return root;
1743
- }
1744
- function LabelTree({ labels, activeLabel, activeSearch, onSelect }) {
1745
- const tree = useMemo(() => buildLabelTree(labels), [labels]);
1746
- const LABEL_STATE_KEY = "email_label_expanded";
1747
- const [expanded, setExpanded] = useState(() => {
1748
- try {
1749
- const saved = JSON.parse(localStorage.getItem(LABEL_STATE_KEY) || "[]");
1750
- return new Set(saved);
1751
- } catch {
1752
- return /* @__PURE__ */ new Set();
1753
- }
1754
- });
1755
- const toggle = (name) => {
1756
- setExpanded((prev) => {
1757
- const next = new Set(prev);
1758
- if (next.has(name)) next.delete(name);
1759
- else next.add(name);
1760
- localStorage.setItem(LABEL_STATE_KEY, JSON.stringify([...next]));
1761
- return next;
1762
- });
1763
- };
1764
- const renderNodes = (nodes, depth) => {
1765
- return nodes.map((node) => {
1766
- const hasChildren = node.children.length > 0;
1767
- const isOpen = expanded.has(node.name);
1768
- const isActive = node.id === activeLabel && !activeSearch;
1769
- return /* @__PURE__ */ jsxs("div", { children: [
1770
- /* @__PURE__ */ jsxs(
1771
- "button",
1772
- {
1773
- onClick: () => {
1774
- if (node.id) onSelect(node.id);
1775
- if (hasChildren) toggle(node.name);
1776
- },
1777
- className: `w-full flex items-center gap-1.5 rounded-md py-1 text-sm transition-colors ${isActive ? "bg-blue-100 text-blue-800 font-medium" : "text-gray-700 hover:bg-gray-100"}`,
1778
- style: { paddingLeft: 12 + depth * 16 },
1779
- children: [
1780
- hasChildren ? /* @__PURE__ */ jsx("svg", { className: `h-3 w-3 shrink-0 text-gray-400 transition-transform ${isOpen ? "" : "-rotate-90"}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19.5 8.25l-7.5 7.5-7.5-7.5" }) }) : /* @__PURE__ */ jsx("span", { className: "w-3 shrink-0" }),
1781
- /* @__PURE__ */ jsxs("svg", { className: "h-3.5 w-3.5 shrink-0 text-gray-400", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 1.5, children: [
1782
- /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" }),
1783
- /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 6h.008v.008H6V6z" })
1784
- ] }),
1785
- /* @__PURE__ */ jsx("span", { className: "flex-1 text-left truncate", children: node.name })
1786
- ]
1787
- }
1788
- ),
1789
- hasChildren && isOpen && renderNodes(node.children, depth + 1)
1790
- ] }, node.id || node.name);
1791
- });
1792
- };
1793
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1794
- /* @__PURE__ */ jsx("div", { className: "border-t border-gray-200 mt-2 pt-2 mb-1", children: /* @__PURE__ */ jsx("span", { className: "px-3 text-[10px] font-medium text-gray-400 uppercase tracking-wider", children: "Labels" }) }),
1795
- renderNodes(tree, 0)
1796
- ] });
1797
- }
1798
- function SpreadsheetViewer({ data, onClose }) {
1799
- const [activeSheet, setActiveSheet] = useState(data.sheetNames[0] || "");
1800
- const rows = data.sheets[activeSheet] || [];
1801
- const colCount = rows.reduce((m, r) => Math.max(m, r.length), 0);
1802
- const gridColumns = Array.from({ length: colCount }, (_, i) => ({
1803
- key: `col_${i}`,
1804
- title: rows[0]?.[i] || String.fromCharCode(65 + i % 26),
1805
- width: 120,
1806
- readOnly: true
1807
- }));
1808
- const gridData = rows.length > 1 ? rows.slice(1) : rows;
1809
- return /* @__PURE__ */ jsx(Modal, { open: true, onClose, title: data.name, size: "2xl", bodyScroll: false, children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full overflow-hidden", children: [
1810
- /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsx(
1811
- EditableGrid,
1812
- {
1813
- columns: gridColumns,
1814
- data: gridData,
1815
- onChange: () => {
1816
- },
1817
- fixedRows: true,
1818
- maxHeight: "70vh"
1819
- }
1820
- ) }),
1821
- /* @__PURE__ */ jsxs("div", { className: "flex items-center border-t border-gray-200 bg-gray-50 shrink-0", children: [
1822
- /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5 px-1 py-1 overflow-x-auto flex-1 min-w-0", children: data.sheetNames.map((name) => /* @__PURE__ */ jsx(
1823
- "button",
1824
- {
1825
- onClick: () => setActiveSheet(name),
1826
- className: `px-3 py-1 text-xs font-medium rounded-t whitespace-nowrap transition-colors ${activeSheet === name ? "bg-white text-blue-700 border border-b-0 border-gray-300 -mb-px relative z-10" : "text-gray-500 hover:text-gray-700 hover:bg-gray-100"}`,
1827
- children: name
1828
- },
1829
- name
1830
- )) }),
1831
- /* @__PURE__ */ jsx("span", { className: "text-xs text-gray-400 px-3 shrink-0", children: "Select cells and Ctrl+C to copy" })
1832
- ] })
1833
- ] }) });
1834
- }
1835
- function EmailDemoView() {
1836
- const [emails] = useState(() => getDemoEmails());
1837
- const [selectedId, setSelectedId] = useState(emails[0]?.id ?? null);
1838
- const selected = emails.find((e) => e.id === selectedId);
1839
- const formatTime = (iso) => {
1840
- const diff = Date.now() - new Date(iso).getTime();
1841
- const mins = Math.floor(diff / 6e4);
1842
- if (mins < 1) return "just now";
1843
- if (mins < 60) return `${mins}m ago`;
1844
- const hrs = Math.floor(mins / 60);
1845
- if (hrs < 24) return `${hrs}h ago`;
1846
- return `${Math.floor(hrs / 24)}d ago`;
1847
- };
1848
- return /* @__PURE__ */ jsxs("div", { className: "flex flex-col h-full", children: [
1849
- /* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 border-b border-amber-200 bg-amber-50 text-[11px] text-amber-800", children: "Demo mode \u2014 these emails are sample data. Set up a Google OAuth Client ID in Customization to see your real Gmail." }),
1850
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 min-h-0", children: [
1851
- /* @__PURE__ */ jsx("div", { className: "w-80 shrink-0 border-r border-gray-200 overflow-y-auto", children: emails.map((e) => /* @__PURE__ */ jsxs(
1852
- "button",
1853
- {
1854
- onClick: () => setSelectedId(e.id),
1855
- className: `w-full text-left px-3 py-2 border-b border-gray-100 ${selectedId === e.id ? "bg-blue-50" : "hover:bg-gray-50"}`,
1856
- children: [
1857
- /* @__PURE__ */ jsxs("div", { className: "flex items-baseline justify-between gap-2", children: [
1858
- /* @__PURE__ */ jsx("span", { className: `truncate text-sm ${e.unread ? "font-semibold text-gray-900" : "text-gray-700"}`, children: e.from }),
1859
- /* @__PURE__ */ jsx("span", { className: "text-[10px] text-gray-400 shrink-0 tabular-nums", children: formatTime(e.receivedAt) })
1860
- ] }),
1861
- /* @__PURE__ */ jsx("div", { className: `truncate text-sm ${e.unread ? "font-medium text-gray-800" : "text-gray-600"}`, children: e.subject }),
1862
- /* @__PURE__ */ jsx("div", { className: "truncate text-xs text-gray-500", children: e.snippet })
1863
- ]
1864
- },
1865
- e.id
1866
- )) }),
1867
- /* @__PURE__ */ jsx("div", { className: "flex-1 p-6 overflow-y-auto bg-white", children: selected ? /* @__PURE__ */ jsxs(Fragment, { children: [
1868
- /* @__PURE__ */ jsx("h2", { className: "text-xl font-semibold text-gray-900 mb-1", children: selected.subject }),
1869
- /* @__PURE__ */ jsxs("div", { className: "text-xs text-gray-500 mb-4", children: [
1870
- "From ",
1871
- selected.from,
1872
- " \xB7 ",
1873
- formatTime(selected.receivedAt)
1874
- ] }),
1875
- /* @__PURE__ */ jsx("pre", { className: "whitespace-pre-wrap font-sans text-sm text-gray-800 leading-relaxed", children: selected.body })
1876
- ] }) : /* @__PURE__ */ jsx("div", { className: "text-sm text-gray-400 text-center pt-20", children: "Select a message" }) })
1877
- ] })
1878
- ] });
1879
- }
1880
-
1881
- export { Email as default };
1882
- //# sourceMappingURL=Email-5WL3TWC6.js.map
1883
- //# sourceMappingURL=Email-5WL3TWC6.js.map