From 6b4654231bd92b52ad0c870763b352391e5e68cc Mon Sep 17 00:00:00 2001 From: Local Dev Date: Thu, 22 Oct 2026 03:12:28 +0300 Subject: [PATCH] hermes settings: scrollable modal + Address book vCard import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 () 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. --- bundled-addons/hermes/panel.html | 54 +++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) 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