@bobfrankston/rmfmail 1.2.188 → 1.2.191
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/TODO.md +12 -9
- package/client/android-bootstrap.bundle.js +76 -49
- package/client/android-bootstrap.bundle.js.map +2 -2
- package/client/app.bundle.js +32 -0
- package/client/app.bundle.js.map +3 -3
- package/client/app.js +18 -0
- package/client/app.js.map +1 -1
- package/client/app.ts +15 -0
- package/client/components/calendar-sidebar.js +12 -1
- package/client/components/calendar-sidebar.js.map +1 -1
- package/client/components/calendar-sidebar.ts +8 -1
- package/client/components/message-viewer.js +19 -0
- package/client/components/message-viewer.js.map +1 -1
- package/client/components/message-viewer.ts +17 -0
- package/client/compose/compose.bundle.js +4 -2
- package/client/compose/compose.bundle.js.map +2 -2
- package/client/compose/editor.js +11 -2
- package/client/compose/editor.js.map +1 -1
- package/client/compose/editor.ts +11 -2
- package/client/package.json +1 -1
- package/docs/TODO-prereorg-snapshot-2026-07-15.md +1130 -0
- package/package.json +5 -5
- package/packages/mailx-store-web/android-bootstrap.d.ts.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.js +108 -52
- package/packages/mailx-store-web/android-bootstrap.js.map +1 -1
- package/packages/mailx-store-web/android-bootstrap.ts +111 -53
- package/packages/mailx-store-web/package.json +1 -1
- /package/packages/mailx-imap/{node_modules.npmglobalize-stash-33816 → node_modules.npmglobalize-stash-47576}/.package-lock.json +0 -0
|
@@ -58,6 +58,15 @@ function vlog(msg: string): void {
|
|
|
58
58
|
} catch { /* ignore */ }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/** C156: user-visible bootstrap narration. A new-device install looked
|
|
62
|
+
* frozen (Bob 2026-07-22) because the stage breadcrumbs only went to vlog.
|
|
63
|
+
* This logs (console capture shows it pre-UI) AND emits a bootstrapStatus
|
|
64
|
+
* event the client renders in the status bar + folder-tree loading slot. */
|
|
65
|
+
function narrate(msg: string): void {
|
|
66
|
+
console.log(`[android] ${msg}`);
|
|
67
|
+
emitEvent({ type: "bootstrapStatus", message: msg });
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
// ── Sync Manager ──
|
|
62
71
|
|
|
63
72
|
/** Race a promise against a wall-clock timeout. On timeout it REJECTS so the
|
|
@@ -1024,61 +1033,81 @@ async function refreshAccessToken(refreshToken: string): Promise<{ access_token:
|
|
|
1024
1033
|
// ── Token provider (browser OAuth, same as desktop) ──
|
|
1025
1034
|
|
|
1026
1035
|
function createNativeTokenProvider(email: string): () => Promise<string> {
|
|
1027
|
-
return
|
|
1028
|
-
//
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1036
|
+
return () => {
|
|
1037
|
+
// C158: single-flight per email. Startup fires several concurrent
|
|
1038
|
+
// token requests (GDrive reconcile, contacts sync, syncAll) — with
|
|
1039
|
+
// no cached token each one launched its OWN browser-consent intent
|
|
1040
|
+
// (three "Starting OAuth flow" in one boot, 2026-07-22 logit trail).
|
|
1041
|
+
// Concurrent callers now share one resolution. Keyed on a window
|
|
1042
|
+
// global, not a module local, because the double-init bug that
|
|
1043
|
+
// exposed this also loads the module twice — two module instances
|
|
1044
|
+
// must still share the guard.
|
|
1045
|
+
const w = window as any;
|
|
1046
|
+
const inflight: Map<string, Promise<string>> = w.__rmfOAuthInflight ||= new Map();
|
|
1047
|
+
const key = canonEmail(email);
|
|
1048
|
+
const existing = inflight.get(key);
|
|
1049
|
+
if (existing) return existing;
|
|
1050
|
+
const p = fetchTokenForEmail(email).finally(() => inflight.delete(key));
|
|
1051
|
+
inflight.set(key, p);
|
|
1052
|
+
return p;
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async function fetchTokenForEmail(email: string): Promise<string> {
|
|
1057
|
+
// Check cached token first
|
|
1058
|
+
const cached = await getCachedToken(email);
|
|
1059
|
+
if (cached?.access_token) {
|
|
1060
|
+
const expiresAt = cached.expires_at || 0;
|
|
1061
|
+
const bufferMs = 5 * 60 * 1000; // 5 min buffer
|
|
1062
|
+
if (Date.now() < expiresAt - bufferMs) {
|
|
1063
|
+
return cached.access_token;
|
|
1064
|
+
}
|
|
1065
|
+
// Try refresh
|
|
1066
|
+
if (cached.refresh_token) {
|
|
1067
|
+
try {
|
|
1068
|
+
console.log(`[oauth] Refreshing token for ${email}`);
|
|
1069
|
+
const refreshed = await refreshAccessToken(cached.refresh_token);
|
|
1070
|
+
const token = {
|
|
1071
|
+
access_token: refreshed.access_token,
|
|
1072
|
+
refresh_token: cached.refresh_token,
|
|
1073
|
+
expires_at: Date.now() + refreshed.expires_in * 1000,
|
|
1074
|
+
};
|
|
1075
|
+
await setCachedToken(email, token);
|
|
1076
|
+
return token.access_token;
|
|
1077
|
+
} catch (e: any) {
|
|
1078
|
+
console.warn(`[oauth] Refresh failed: ${e.message}, starting new flow`);
|
|
1051
1079
|
}
|
|
1052
1080
|
}
|
|
1081
|
+
}
|
|
1053
1082
|
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1083
|
+
// No valid token — start browser OAuth flow
|
|
1084
|
+
const bridge = (window as any)._nativeBridge;
|
|
1085
|
+
if (!bridge?.app?.startOAuth) {
|
|
1086
|
+
throw new Error("No native OAuth bridge");
|
|
1087
|
+
}
|
|
1059
1088
|
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
await setCachedToken(email, token);
|
|
1079
|
-
console.log(`[oauth] Token obtained for ${email}`);
|
|
1080
|
-
return token.access_token;
|
|
1089
|
+
const authUrl = `${OAUTH_CLIENT.authUri}?` + new URLSearchParams({
|
|
1090
|
+
client_id: OAUTH_CLIENT.clientId,
|
|
1091
|
+
redirect_uri: OAUTH_CLIENT.redirectUri,
|
|
1092
|
+
response_type: "code",
|
|
1093
|
+
scope: OAUTH_SCOPES,
|
|
1094
|
+
access_type: "offline",
|
|
1095
|
+
prompt: "consent",
|
|
1096
|
+
login_hint: email,
|
|
1097
|
+
}).toString();
|
|
1098
|
+
|
|
1099
|
+
console.log(`[oauth] Starting browser consent for ${email}`);
|
|
1100
|
+
narrate(`Signing into ${email}…`);
|
|
1101
|
+
const code = await bridge.app.startOAuth(authUrl);
|
|
1102
|
+
const tokens = await exchangeCodeForTokens(code);
|
|
1103
|
+
const token = {
|
|
1104
|
+
access_token: tokens.access_token,
|
|
1105
|
+
refresh_token: tokens.refresh_token,
|
|
1106
|
+
expires_at: Date.now() + tokens.expires_in * 1000,
|
|
1081
1107
|
};
|
|
1108
|
+
await setCachedToken(email, token);
|
|
1109
|
+
console.log(`[oauth] Token obtained for ${email}`);
|
|
1110
|
+
return token.access_token;
|
|
1082
1111
|
}
|
|
1083
1112
|
|
|
1084
1113
|
// ── GDrive folder lookup ──
|
|
@@ -1339,7 +1368,23 @@ async function waitForNativeBridge(timeoutMs: number = 5000): Promise<void> {
|
|
|
1339
1368
|
});
|
|
1340
1369
|
}
|
|
1341
1370
|
|
|
1342
|
-
export
|
|
1371
|
+
export function initAndroid(): Promise<void> {
|
|
1372
|
+
// C158: idempotency guard. The 2026-07-22 fold/unfold logit trail showed
|
|
1373
|
+
// ONE WebView reload executing the boot module TWICE — duplicate "bridge
|
|
1374
|
+
// installed", duplicate GDrive lookups, tripled OAuth launches. The
|
|
1375
|
+
// guard lives on window (not a module local) so it holds even when the
|
|
1376
|
+
// module itself is instantiated twice (bundle + package-path specifiers
|
|
1377
|
+
// resolve to distinct module instances).
|
|
1378
|
+
const w = window as any;
|
|
1379
|
+
if (w.__rmfInitAndroid) {
|
|
1380
|
+
console.warn("[android] initAndroid called again — duplicate suppressed (C158)");
|
|
1381
|
+
vlog("C158: duplicate initAndroid call suppressed");
|
|
1382
|
+
return w.__rmfInitAndroid;
|
|
1383
|
+
}
|
|
1384
|
+
return w.__rmfInitAndroid = initAndroidOnce();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async function initAndroidOnce(): Promise<void> {
|
|
1343
1388
|
console.log("[android] Initializing mailx (main-thread mode)...");
|
|
1344
1389
|
|
|
1345
1390
|
// Main-thread path: async I/O (fetch, TCP bridge) doesn't block the UI,
|
|
@@ -1410,7 +1455,7 @@ export async function initAndroid(): Promise<void> {
|
|
|
1410
1455
|
reconcileTry++;
|
|
1411
1456
|
setGDriveTokenProvider(tp);
|
|
1412
1457
|
try {
|
|
1413
|
-
|
|
1458
|
+
narrate(`Fetching settings from Google Drive… (try ${reconcileTry})`);
|
|
1414
1459
|
vlog(`gdrive-reconcile try ${reconcileTry}: token fetch`);
|
|
1415
1460
|
// Watchdog the token fetch — it crosses into native code and has
|
|
1416
1461
|
// hung on cold boots; without a timeout the whole reconcile just
|
|
@@ -1449,7 +1494,7 @@ export async function initAndroid(): Promise<void> {
|
|
|
1449
1494
|
console.warn(`[android] List debug: ${e.message}`);
|
|
1450
1495
|
}
|
|
1451
1496
|
// Read accounts directly from GDrive (bypass IndexedDB cache)
|
|
1452
|
-
|
|
1497
|
+
narrate("Loading accounts from Google Drive…");
|
|
1453
1498
|
const gdriveAccounts = await loadAccountsFromCloud();
|
|
1454
1499
|
console.log(`[android] GDrive returned ${gdriveAccounts.length} accounts: ${gdriveAccounts.map(a => a.id).join(",")}`);
|
|
1455
1500
|
if (gdriveAccounts.length > 0) {
|
|
@@ -1468,6 +1513,15 @@ export async function initAndroid(): Promise<void> {
|
|
|
1468
1513
|
await syncManager.addAccount(account);
|
|
1469
1514
|
}
|
|
1470
1515
|
console.log(`[android] Loaded ${accounts.length} accounts from GDrive`);
|
|
1516
|
+
// C157: sync NOW. The boot-time syncAll ran before these
|
|
1517
|
+
// providers existed (fresh install: zero local accounts),
|
|
1518
|
+
// so without this kick the folder tree stayed empty until
|
|
1519
|
+
// the next 60 s poll tick — on top of OAuth consent time,
|
|
1520
|
+
// that read as "frozen for minutes". syncAll's inbox
|
|
1521
|
+
// phase lists folders first, so the tree paints in
|
|
1522
|
+
// seconds; message counts stream in behind.
|
|
1523
|
+
narrate(`Syncing ${accounts.length} account(s)…`);
|
|
1524
|
+
syncManager.syncAll().catch(e => console.error(`[android] post-reconcile sync: ${e.message}`));
|
|
1471
1525
|
}
|
|
1472
1526
|
// Register this Android device in clients.jsonc
|
|
1473
1527
|
await registerDeviceInGDrive(tp, folderId, accounts.map(a => a.id));
|
|
@@ -1755,6 +1809,10 @@ function installBridge(): void {
|
|
|
1755
1809
|
await syncManager.addAccount(account);
|
|
1756
1810
|
db.upsertAccount(account.id, account.name, account.email, JSON.stringify(account));
|
|
1757
1811
|
console.log(`[android] Account added: ${email}`);
|
|
1812
|
+
// C157: the "Syncing..." message below was aspirational — no
|
|
1813
|
+
// sync was actually kicked, so the new account showed nothing
|
|
1814
|
+
// until the next 60 s poll tick.
|
|
1815
|
+
syncManager.syncAll().catch(e => console.error(`[android] post-setup sync: ${e.message}`));
|
|
1758
1816
|
return { ok: true, message: `Added ${email}. Syncing...` };
|
|
1759
1817
|
} catch (e: any) {
|
|
1760
1818
|
return { ok: false, error: e.message };
|