diff --git a/bundled-addons/hermes/panel.html b/bundled-addons/hermes/panel.html
index 3acdb1b1..cc7d9fc1 100644
--- a/bundled-addons/hermes/panel.html
+++ b/bundled-addons/hermes/panel.html
@@ -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 already knows your number can find you — same tradeoff as number-based messengers. Unlink any time.
+
Address book
+
+
+
+
+
+ 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.
+
+
Backup
No seed phrase stored for this account. (created-elsewhere / vault / mnemonic signins don't retain the words)
@@ -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