hermes settings: scrollable modal + Address book vCard import

Two Settings-pane fixes:

1. .modal was clipped on short viewports (mobile landscape, small
   laptop screens): place-items:center + fixed max-width with no
   max-height meant the content below Phone discovery / Backup fell
   off the bottom. Add max-height: calc(100vh - 24px) + overflow-y:
   auto so the whole thing scrolls inside the backdrop. This is why
   the user couldn't see Phone discovery on mobile.
2. Address book section between Phone discovery and Backup: 'Import
   contacts from vCard' triggers a hidden file picker (<input
   type=file accept=.vcf>) that reads the file client-side, extracts
   FN + TEL per VCARD block, hands the phone list to findByPhones,
   and auto-adds the matched Hermes identities as contacts with the
   vCard name as nickname.

Lives in both the Theseus addon panel and the hermes.x web messenger.
Phone discovery was already in Settings — the modal clipping just hid
it on smaller viewports.
This commit is contained in:
Local Dev 2026-10-22 03:12:28 +03:00
parent 5984d39253
commit 6b4654231b

View file

@ -144,7 +144,8 @@
display: none; place-items: center; z-index: 100; padding: 12px; }
.modal-bg.show { display: grid; }
.modal { background: var(--panel); border: 1px solid var(--line);
border-radius: 12px; padding: 16px; width: 100%; max-width: 320px; }
border-radius: 12px; padding: 16px; width: 100%; max-width: 320px;
max-height: calc(100vh - 24px); overflow-y: auto; -webkit-overflow-scrolling: touch; }
.modal h3 { margin: 0 0 10px; font-size: 14px; }
.modal input { font: inherit; font-size: 12.5px; width: 100%; padding: 8px 10px;
background: var(--panel-2); color: var(--ink);
@ -216,6 +217,15 @@
We hash the number locally and publish a Nostr claim for the hash. Anyone who <i>already knows</i> your number can find you — same tradeoff as number-based messengers. Unlink any time.
</div>
<div style="font-size:11.5px;color:var(--mut);margin-bottom:4px">Address book</div>
<input id="vcardFileInput" type="file" accept=".vcf,text/vcard,text/x-vcard" style="display:none">
<button class="secondary" id="importVcardBtn" style="font:inherit;font-size:12.5px;background:var(--panel-2);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:8px 12px;cursor:pointer;width:100%;margin-bottom:6px">Import contacts from vCard</button>
<div class="err" id="vcardErr" style="color:var(--err);font-size:11.5px;min-height:1em"></div>
<div id="vcardResult" style="color:var(--dim);font-size:11px;min-height:1em"></div>
<div style="color:var(--dim);font-size:11px;margin-bottom:14px">
Open a .vcf export from your contacts. We parse it locally, look up any linked phone numbers against Hermes, and show the matches to add — nothing uploaded.
</div>
<div style="font-size:11.5px;color:var(--mut);margin-bottom:4px">Backup</div>
<div id="backupBox">
<div id="backupNone" style="font-size:12px;color:var(--mut);margin-bottom:12px">No seed phrase stored for this account.<br><span style="color:var(--dim);font-size:11px">(created-elsewhere / vault / mnemonic signins don't retain the words)</span></div>
@ -774,6 +784,48 @@ $("openSettingsBtn").addEventListener("click", async () => {
await openSettings();
});
$("settingsClose").addEventListener("click", () => $("settingsModal").classList.remove("show"));
// --- vCard import (Address book) ---
$("importVcardBtn").addEventListener("click", () => $("vcardFileInput").click());
$("vcardFileInput").addEventListener("change", async () => {
const f = $("vcardFileInput").files && $("vcardFileInput").files[0];
if (!f) return;
err($("vcardErr"), "");
$("vcardResult").textContent = "Reading " + f.name + "\u2026";
try {
const text = await f.text();
// Minimal RFC 6350 subset: pull FN + TEL per VCARD block.
const entries = [];
for (const block of text.split(/BEGIN:VCARD/i).slice(1)) {
const body = block.split(/END:VCARD/i)[0] || "";
const fn = (body.match(/^FN(?:;[^:]*)?:(.+)$/mi) || [])[1];
const tel = (body.match(/^TEL(?:;[^:]*)?:(.+)$/mi) || [])[1];
if (!tel) continue;
entries.push({ name: (fn || "").trim(), phone: tel.trim() });
}
if (!entries.length) { $("vcardResult").textContent = "No phone numbers in that file."; return; }
$("vcardResult").textContent = "Looking up " + entries.length + " number(s) on Hermes\u2026";
const r = await bridge.invoke("findByPhones", { phones: entries.map(e => e.phone) });
if (!r || !r.ok) throw new Error((r && r.err) || "lookup failed");
const found = r.matches || [];
if (!found.length) { $("vcardResult").textContent = "No matches among your contacts yet."; return; }
let added = 0;
for (const m of found) {
const label = (entries.find(e => e.phone.replace(/\D+/g, "") === m.phone.replace(/\D+/g, "")) || {}).name || m.phone;
try {
const a = await bridge.invoke("contactsAdd", { name: m.pkHex, nickname: label });
if (a && a.ok) added++;
} catch {}
}
$("vcardResult").textContent = "Added " + added + " of " + found.length + " matches. Refresh the chat list.";
try { const r2 = await bridge.invoke("contactsList"); if (r2 && r2.ok) { state.contacts = r2.contacts || []; renderContacts(); } } catch {}
} catch (e) {
err($("vcardErr"), (e && e.message) || String(e));
$("vcardResult").textContent = "";
} finally {
$("vcardFileInput").value = "";
}
});
$("settingsModal").addEventListener("click", (e) => { if (e.target === $("settingsModal")) $("settingsModal").classList.remove("show"); });
// Display-mode picker. Persisted per-profile via silentmode.storage (the