@bobfrankston/mailx 1.0.443 → 1.0.444
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/bin/lean-accounts.js +172 -0
- package/bin/mailx.js +52 -2
- package/client/app.js +9 -10
- package/client/components/message-viewer.js +11 -0
- package/client/compose/compose.js +10 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -258,6 +258,10 @@ mailx -repair Re-sync message metadata (fix garbled subjects)
|
|
|
258
258
|
mailx -rebuild Wipe local cache and re-download everything from IMAP
|
|
259
259
|
mailx -setup Interactive first-time setup (CLI)
|
|
260
260
|
mailx -test Test IMAP/SMTP connectivity for all accounts
|
|
261
|
+
mailx -reauth Clear cached OAuth tokens; next start re-consents
|
|
262
|
+
mailx -lean-accounts Strip defaulted fields from accounts.jsonc on
|
|
263
|
+
Google Drive (port, tls, auth, enabled, ...).
|
|
264
|
+
Add --dry-run to preview without writing.
|
|
261
265
|
mailx -v Show version
|
|
262
266
|
```
|
|
263
267
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lean-accounts.js — strip default-valued fields from an accounts.jsonc.
|
|
4
|
+
*
|
|
5
|
+
* Reads any accounts.jsonc (the one mailx writes when it bloats it with
|
|
6
|
+
* port: 993, tls: true, auth: "password", enabled: true, sig.html: false,
|
|
7
|
+
* etc.) and emits a compact form. Same logic as the Lean button in the
|
|
8
|
+
* config editor, but standalone so it works without mailx running.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node lean-accounts.js <path> prints lean output to stdout
|
|
12
|
+
* node lean-accounts.js <path> --inplace overwrites the file
|
|
13
|
+
*
|
|
14
|
+
* Drops:
|
|
15
|
+
* - imap/smtp host/port/tls/auth that match the email's provider defaults
|
|
16
|
+
* - imap/smtp user when it equals the email
|
|
17
|
+
* - enabled: true (default)
|
|
18
|
+
* - sig.html: false (default)
|
|
19
|
+
* - per-account name when it matches the file-level "name"
|
|
20
|
+
*
|
|
21
|
+
* Promotes a shared "name" to file-level when every account has the same.
|
|
22
|
+
*
|
|
23
|
+
* Requires: jsonc-parser (any version that exports `parse` with comment+
|
|
24
|
+
* trailing-comma tolerance). The mailx repo's node_modules has it.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import fs from "node:fs";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
30
|
+
|
|
31
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
|
|
33
|
+
// ── Provider defaults: same set mailx-settings uses ────────────────────────
|
|
34
|
+
const PROVIDERS = {
|
|
35
|
+
"gmail.com": {
|
|
36
|
+
label: "Gmail",
|
|
37
|
+
imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
|
|
38
|
+
smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
|
|
39
|
+
},
|
|
40
|
+
"googlemail.com": {
|
|
41
|
+
label: "Gmail",
|
|
42
|
+
imap: { host: "imap.gmail.com", port: 993, tls: true, auth: "oauth2" },
|
|
43
|
+
smtp: { host: "smtp.gmail.com", port: 587, tls: true, auth: "oauth2" },
|
|
44
|
+
},
|
|
45
|
+
"outlook.com": {
|
|
46
|
+
label: "Outlook",
|
|
47
|
+
imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
|
|
48
|
+
smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
|
|
49
|
+
},
|
|
50
|
+
"hotmail.com": {
|
|
51
|
+
label: "Outlook",
|
|
52
|
+
imap: { host: "outlook.office365.com", port: 993, tls: true, auth: "oauth2" },
|
|
53
|
+
smtp: { host: "smtp.office365.com", port: 587, tls: true, auth: "oauth2" },
|
|
54
|
+
},
|
|
55
|
+
"yahoo.com": {
|
|
56
|
+
label: "Yahoo",
|
|
57
|
+
imap: { host: "imap.mail.yahoo.com", port: 993, tls: true, auth: "oauth2" },
|
|
58
|
+
smtp: { host: "smtp.mail.yahoo.com", port: 587, tls: true, auth: "oauth2" },
|
|
59
|
+
},
|
|
60
|
+
"icloud.com": {
|
|
61
|
+
label: "iCloud",
|
|
62
|
+
imap: { host: "imap.mail.me.com", port: 993, tls: true, auth: "oauth2" },
|
|
63
|
+
smtp: { host: "smtp.mail.me.com", port: 587, tls: true, auth: "oauth2" },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function denormalizeAccount(acct, globalName) {
|
|
68
|
+
const domain = (acct.email || "").split("@")[1]?.toLowerCase() || "";
|
|
69
|
+
const provider = PROVIDERS[domain];
|
|
70
|
+
const out = {};
|
|
71
|
+
out.id = acct.id;
|
|
72
|
+
if (acct.label && acct.label !== provider?.label && acct.label !== acct.id) out.label = acct.label;
|
|
73
|
+
out.email = acct.email;
|
|
74
|
+
if (acct.name && acct.name !== globalName) out.name = acct.name;
|
|
75
|
+
if (acct.primary) out.primary = true;
|
|
76
|
+
if (acct.primaryCalendar !== undefined) out.primaryCalendar = acct.primaryCalendar;
|
|
77
|
+
if (acct.primaryTasks !== undefined) out.primaryTasks = acct.primaryTasks;
|
|
78
|
+
if (acct.primaryContacts !== undefined) out.primaryContacts = acct.primaryContacts;
|
|
79
|
+
|
|
80
|
+
const imapOut = {};
|
|
81
|
+
if (acct.imap?.host && acct.imap.host !== provider?.imap.host) imapOut.host = acct.imap.host;
|
|
82
|
+
if (acct.imap?.user && acct.imap.user !== acct.email) imapOut.user = acct.imap.user;
|
|
83
|
+
if (acct.imap?.password) imapOut.password = acct.imap.password;
|
|
84
|
+
if (acct.imap?.port && acct.imap.port !== (provider?.imap.port ?? 993)) imapOut.port = acct.imap.port;
|
|
85
|
+
if (acct.imap?.tls !== undefined && acct.imap.tls !== (provider?.imap.tls ?? true)) imapOut.tls = acct.imap.tls;
|
|
86
|
+
if (acct.imap?.auth && acct.imap.auth !== (provider?.imap.auth ?? "password")) imapOut.auth = acct.imap.auth;
|
|
87
|
+
if (Object.keys(imapOut).length > 0) out.imap = imapOut;
|
|
88
|
+
|
|
89
|
+
const smtpOut = {};
|
|
90
|
+
if (acct.smtp?.host && acct.smtp.host !== provider?.smtp.host) smtpOut.host = acct.smtp.host;
|
|
91
|
+
if (acct.smtp?.user && acct.smtp.user !== acct.email && acct.smtp.user !== acct.imap?.user) smtpOut.user = acct.smtp.user;
|
|
92
|
+
if (acct.smtp?.password) smtpOut.password = acct.smtp.password;
|
|
93
|
+
if (acct.smtp?.port && acct.smtp.port !== (provider?.smtp.port ?? 587)) smtpOut.port = acct.smtp.port;
|
|
94
|
+
if (acct.smtp?.tls !== undefined && acct.smtp.tls !== (provider?.smtp.tls ?? true)) smtpOut.tls = acct.smtp.tls;
|
|
95
|
+
if (acct.smtp?.auth && acct.smtp.auth !== (provider?.smtp.auth ?? "password")) smtpOut.auth = acct.smtp.auth;
|
|
96
|
+
if (Object.keys(smtpOut).length > 0) out.smtp = smtpOut;
|
|
97
|
+
|
|
98
|
+
if (acct.defaultSend) out.defaultSend = true;
|
|
99
|
+
if (acct.enabled === false) out.enabled = false;
|
|
100
|
+
if (acct.relayDomains?.length > 0) out.relayDomains = acct.relayDomains;
|
|
101
|
+
if (acct.deliveredToPrefix?.length > 0) out.deliveredToPrefix = acct.deliveredToPrefix;
|
|
102
|
+
if (acct.identityDomains?.length > 0) out.identityDomains = acct.identityDomains;
|
|
103
|
+
|
|
104
|
+
const syncContactsDefault = provider?.imap.auth === "oauth2";
|
|
105
|
+
if (acct.syncContacts !== undefined && acct.syncContacts !== syncContactsDefault) {
|
|
106
|
+
out.syncContacts = acct.syncContacts;
|
|
107
|
+
}
|
|
108
|
+
if (acct.signature) out.signature = acct.signature;
|
|
109
|
+
if (acct.sig?.text) {
|
|
110
|
+
out.sig = acct.sig.html ? { text: acct.sig.text, html: true } : { text: acct.sig.text };
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const args = process.argv.slice(2);
|
|
117
|
+
const inputPath = args.find(a => !a.startsWith("--"));
|
|
118
|
+
const inplace = args.includes("--inplace");
|
|
119
|
+
if (!inputPath) {
|
|
120
|
+
console.error("Usage: node lean-accounts.js <path-to-accounts.jsonc> [--inplace]");
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
if (!fs.existsSync(inputPath)) {
|
|
124
|
+
console.error(`File not found: ${inputPath}`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Resolve jsonc-parser from the local mailx repo's node_modules so the
|
|
129
|
+
// script doesn't require a global install. The repo root is two levels
|
|
130
|
+
// up from bin/ — and we walk up further if invoked from elsewhere.
|
|
131
|
+
let parseJsonc;
|
|
132
|
+
for (const dir of [
|
|
133
|
+
path.join(__dirname, "..", "node_modules", "jsonc-parser"),
|
|
134
|
+
path.join(__dirname, "..", "..", "node_modules", "jsonc-parser"),
|
|
135
|
+
path.join(process.cwd(), "node_modules", "jsonc-parser"),
|
|
136
|
+
]) {
|
|
137
|
+
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
138
|
+
const mod = await import(pathToFileURL(path.join(dir, "lib", "esm", "main.js")).href).catch(() => null) ||
|
|
139
|
+
await import("jsonc-parser").catch(() => null);
|
|
140
|
+
if (mod) { parseJsonc = mod.parse; break; }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!parseJsonc) {
|
|
144
|
+
try { parseJsonc = (await import("jsonc-parser")).parse; }
|
|
145
|
+
catch { console.error("jsonc-parser not found. Install or run from inside the mailx repo."); process.exit(1); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const raw = fs.readFileSync(inputPath, "utf-8");
|
|
149
|
+
const errors = [];
|
|
150
|
+
const cfg = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
151
|
+
if (errors.length) {
|
|
152
|
+
console.error(`JSONC parse error: ${errors.map(e => JSON.stringify(e)).join(", ")}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const accounts = cfg?.accounts || (Array.isArray(cfg) ? cfg : []);
|
|
157
|
+
// Promote shared name to file-level when every entry has the same.
|
|
158
|
+
const names = new Set(accounts.map(a => a.name).filter(Boolean));
|
|
159
|
+
const sharedName = names.size === 1 ? [...names][0] : cfg?.name;
|
|
160
|
+
const lean = accounts.map(a => denormalizeAccount(a, sharedName));
|
|
161
|
+
const payload = sharedName ? { name: sharedName, accounts: lean } : { accounts: lean };
|
|
162
|
+
const output = JSON.stringify(payload, null, 2) + "\n";
|
|
163
|
+
|
|
164
|
+
if (inplace) {
|
|
165
|
+
fs.writeFileSync(inputPath, output);
|
|
166
|
+
console.error(`Wrote lean ${inputPath}`);
|
|
167
|
+
} else {
|
|
168
|
+
process.stdout.write(output);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/bin/mailx.js
CHANGED
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* mailx -repair Re-sync metadata (fix corrupt subjects) keeping .eml files
|
|
18
18
|
* mailx -reauth Clear cached OAuth tokens; next start re-consents
|
|
19
19
|
* (use when new Google scopes have been added)
|
|
20
|
+
* mailx -lean-accounts Strip defaulted fields from the GDrive copy of
|
|
21
|
+
* accounts.jsonc (port, tls, auth, enabled, ...).
|
|
22
|
+
* Add --dry-run to preview without writing.
|
|
20
23
|
*/
|
|
21
24
|
import fs from "node:fs";
|
|
22
25
|
import path from "node:path";
|
|
@@ -89,7 +92,7 @@ function pidAlive(pid) {
|
|
|
89
92
|
// on an old UI with no indication that the install has been upgraded.
|
|
90
93
|
// Skip this logic for command-only flags (kill, rebuild, setup, ...) and for
|
|
91
94
|
// the internal --daemon respawn.
|
|
92
|
-
const __commandFlags = ["kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "import", "log", "reauth"];
|
|
95
|
+
const __commandFlags = ["kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "import", "log", "reauth", "lean-accounts"];
|
|
93
96
|
const __isCommandInvocation = process.argv.slice(2).some(a => __commandFlags.includes(a.replace(/^--?/, "")));
|
|
94
97
|
if (!isDaemon && !__isCommandInvocation) {
|
|
95
98
|
const inst = readInstanceFile();
|
|
@@ -147,8 +150,9 @@ const testMode = hasFlag("test");
|
|
|
147
150
|
const rebuildMode = hasFlag("rebuild");
|
|
148
151
|
const repairMode = hasFlag("repair");
|
|
149
152
|
const importMode = hasFlag("import");
|
|
153
|
+
const leanAccountsMode = hasFlag("lean-accounts");
|
|
150
154
|
// Validate arguments
|
|
151
|
-
const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "log", "import", "email", "mail", "daemon"];
|
|
155
|
+
const knownFlags = ["verbose", "kill", "v", "version", "setup", "add", "test", "rebuild", "repair", "log", "import", "lean-accounts", "email", "mail", "daemon"];
|
|
152
156
|
for (const arg of args) {
|
|
153
157
|
const flag = arg.replace(/^--?/, "");
|
|
154
158
|
if (arg.startsWith("-") && !knownFlags.includes(flag)) {
|
|
@@ -363,6 +367,52 @@ if (repairMode) {
|
|
|
363
367
|
console.log(" Run 'mailx' to re-sync from IMAP with correct encoding.");
|
|
364
368
|
process.exit(0);
|
|
365
369
|
}
|
|
370
|
+
// Strip default-valued fields from the GDrive copy of accounts.jsonc.
|
|
371
|
+
// Operates on the cloud version directly — the local ~/.mailx/accounts.jsonc
|
|
372
|
+
// is just a cache and gets updated on the next mailx run anyway.
|
|
373
|
+
if (leanAccountsMode) {
|
|
374
|
+
const { parse: parseJsonc } = await import("jsonc-parser");
|
|
375
|
+
const { initCloudConfig, normalizeAccount, denormalizeAccount } = await import("@bobfrankston/mailx-settings");
|
|
376
|
+
const { cloudRead, cloudWrite } = await import("@bobfrankston/mailx-settings");
|
|
377
|
+
await initCloudConfig("gdrive");
|
|
378
|
+
const raw = await cloudRead("accounts.jsonc");
|
|
379
|
+
if (!raw) {
|
|
380
|
+
console.error("No accounts.jsonc found in GDrive.");
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
const errors = [];
|
|
384
|
+
const cfg = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
385
|
+
if (errors.length) {
|
|
386
|
+
console.error(`JSONC parse error: ${errors.map((e) => JSON.stringify(e)).join(", ")}`);
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
const accountsRaw = cfg?.accounts || (Array.isArray(cfg) ? cfg : []);
|
|
390
|
+
if (accountsRaw.length === 0) {
|
|
391
|
+
console.error("No accounts found in GDrive accounts.jsonc.");
|
|
392
|
+
process.exit(1);
|
|
393
|
+
}
|
|
394
|
+
const globalName = cfg?.name;
|
|
395
|
+
const normalized = accountsRaw.map(a => normalizeAccount(a, globalName));
|
|
396
|
+
const names = new Set(normalized.map((a) => a.name).filter(Boolean));
|
|
397
|
+
const sharedName = names.size === 1 ? [...names][0] : globalName;
|
|
398
|
+
const lean = normalized.map((a) => denormalizeAccount(a, sharedName));
|
|
399
|
+
const payload = sharedName ? { name: sharedName, accounts: lean } : { accounts: lean };
|
|
400
|
+
const output = JSON.stringify(payload, null, 2);
|
|
401
|
+
const before = raw.length;
|
|
402
|
+
const after = output.length;
|
|
403
|
+
console.log(`Read ${accountsRaw.length} account(s) from GDrive.`);
|
|
404
|
+
console.log(`Before: ${before} bytes`);
|
|
405
|
+
console.log(`After: ${after} bytes (${Math.round(100 * (1 - after / before))}% smaller)`);
|
|
406
|
+
if (process.argv.includes("--dry-run") || process.argv.includes("-n")) {
|
|
407
|
+
console.log("--- Lean output (dry run, not written) ---");
|
|
408
|
+
console.log(output);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
await cloudWrite("accounts.jsonc", output);
|
|
412
|
+
console.log("Wrote lean accounts.jsonc back to GDrive.");
|
|
413
|
+
}
|
|
414
|
+
process.exit(0);
|
|
415
|
+
}
|
|
366
416
|
// Import accounts from a local file into GDrive
|
|
367
417
|
if (importMode) {
|
|
368
418
|
const importPath = args.find(a => !a.startsWith("-"));
|
package/client/app.js
CHANGED
|
@@ -1924,23 +1924,22 @@ document.addEventListener("keydown", (e) => {
|
|
|
1924
1924
|
e.preventDefault();
|
|
1925
1925
|
openCompose("new");
|
|
1926
1926
|
}
|
|
1927
|
-
// Ctrl+
|
|
1928
|
-
if (e.ctrlKey && e.key === "
|
|
1929
|
-
e.preventDefault();
|
|
1930
|
-
openCompose("forward");
|
|
1931
|
-
}
|
|
1932
|
-
// Ctrl+R = Reply
|
|
1933
|
-
if (e.ctrlKey && e.key === "r" && !e.shiftKey) {
|
|
1927
|
+
// Ctrl+R = Reply (without Shift)
|
|
1928
|
+
if (e.ctrlKey && e.key === "r" && !e.shiftKey && !e.altKey && !e.metaKey) {
|
|
1934
1929
|
e.preventDefault();
|
|
1935
1930
|
openCompose("reply");
|
|
1936
1931
|
}
|
|
1937
1932
|
// Ctrl+Shift+R = Reply All
|
|
1938
|
-
if (e.ctrlKey && e.shiftKey && e.key === "R") {
|
|
1933
|
+
if (e.ctrlKey && e.shiftKey && e.key === "R" && !e.altKey && !e.metaKey) {
|
|
1939
1934
|
e.preventDefault();
|
|
1940
1935
|
openCompose("replyAll");
|
|
1941
1936
|
}
|
|
1942
|
-
// Ctrl+F = Forward
|
|
1943
|
-
|
|
1937
|
+
// Ctrl+F = Forward (without Shift). Use toLowerCase so a Caps-Lock or
|
|
1938
|
+
// shifted state doesn't bypass us. Single handler — the previous
|
|
1939
|
+
// duplicate fired openCompose twice, which double-loaded the compose
|
|
1940
|
+
// iframe and the second copy got an empty sessionStorage (the first
|
|
1941
|
+
// had already consumed it), producing an empty Forward form.
|
|
1942
|
+
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && (e.key === "f" || e.key === "F")) {
|
|
1944
1943
|
e.preventDefault();
|
|
1945
1944
|
openCompose("forward");
|
|
1946
1945
|
}
|
|
@@ -1113,6 +1113,17 @@ ${csp}
|
|
|
1113
1113
|
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
|
|
1114
1114
|
// Zoom keys handled by parent-side installPreviewControls; don't double-send.
|
|
1115
1115
|
if (e.ctrlKey && (e.key === "=" || e.key === "+" || e.key === "-" || e.key === "0")) return;
|
|
1116
|
+
// Preventing the iframe's default for keys we forward to the parent
|
|
1117
|
+
// is essential — the parent's preventDefault on the synthetic
|
|
1118
|
+
// keydown can't suppress the browser's reaction to the ORIGINAL
|
|
1119
|
+
// event (Ctrl+R reload, Ctrl+F find, etc.). Suppress here so the
|
|
1120
|
+
// browser doesn't act before the parent processes the action.
|
|
1121
|
+
var k = (e.key || "").toLowerCase();
|
|
1122
|
+
var isShortcut = e.ctrlKey && !e.altKey && !e.metaKey && (
|
|
1123
|
+
k === "r" || k === "f" || k === "n" || k === "a" || k === "d" ||
|
|
1124
|
+
k === "z" || k === "y" || k === "k"
|
|
1125
|
+
);
|
|
1126
|
+
if (isShortcut) e.preventDefault();
|
|
1116
1127
|
window.parent.postMessage({
|
|
1117
1128
|
type: "previewKey",
|
|
1118
1129
|
key: e.key, code: e.code,
|
|
@@ -92,23 +92,26 @@ catch { /* private-mode / SecurityError — default quill */ }
|
|
|
92
92
|
}
|
|
93
93
|
catch { /* non-fatal */ }
|
|
94
94
|
})();
|
|
95
|
-
// Whatever happens in editor init, surface failures to the mailx log
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
95
|
+
// Whatever happens in editor init, surface failures to the mailx log
|
|
96
|
+
// AND fall through to a plain-contenteditable fallback so the rest of
|
|
97
|
+
// the compose script (Discard, X, Send, save-draft) still runs. Earlier
|
|
98
|
+
// versions re-threw asset-load failures, which left compose with dead
|
|
99
|
+
// buttons and the user with no recourse — exactly the "Reply window
|
|
100
|
+
// won't close" symptom we hit when Quill's CDN was unreachable.
|
|
101
101
|
let editor;
|
|
102
|
+
let editorAssetError = null;
|
|
102
103
|
try {
|
|
103
104
|
await loadEditorAssets(editorType);
|
|
104
105
|
}
|
|
105
106
|
catch (e) {
|
|
107
|
+
editorAssetError = e;
|
|
106
108
|
logClientEvent("compose-editor-assets-failed", { type: editorType, error: String(e?.message || e) });
|
|
107
|
-
throw e;
|
|
108
109
|
}
|
|
109
110
|
const container = document.getElementById("compose-editor");
|
|
110
111
|
container.classList.add(editorType === "tiptap" ? "editor-tiptap" : "editor-quill");
|
|
111
112
|
try {
|
|
113
|
+
if (editorAssetError)
|
|
114
|
+
throw editorAssetError;
|
|
112
115
|
editor = await createEditor(container, editorType);
|
|
113
116
|
}
|
|
114
117
|
catch (e) {
|