- commit
- 88d0641c934981c0a13bde054eb84989ab60007c
- parent
- 791c183f5afe15152fffc30662e28d97366cce51
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 29 | +++++++++++++++++++---------- |
| M | internal/ | 23 | ++++++++++++++++++++++- |
| M | internal/ | 3 | +++ |
| M | internal/ | 3 | ++- |
| M | internal/ | 1 | + |
| M | scripts/ | 2 | +- |
@@ -105,6 +105,9 @@
105105 document.getElementById("menu")?.focus();106106 });107107 if ("serviceWorker" in navigator) navigator.serviceWorker.register(localPath("/sw.js")).catch(() => {});108+ // An installed app asks to keep its cached shell and parked shares. Browsers109+ // grant this silently for installed apps, so it never shows a prompt.110+ if (matchMedia("(display-mode: standalone)").matches) navigator.storage?.persist?.().catch(() => {});108111 Object.assign(window.tiny, {root, localPath, sha256hex, authorization, signedFetch});109112 window.tinySignedFetch = signedFetch;110113 @@ -115,16 +118,21 @@
115118 location.assign(localPath("/"));116119 };117120 121+ // A remote signer session lives in this tab unless the person chose to122+ // remember the device; then it survives the installed app being closed.118123 const forgetRemote = () => {119- Object.values(storage).forEach(name => sessionStorage.removeItem(name));120- sessionStorage.removeItem("tiny.bunker");124+ for (const store of [sessionStorage, localStorage]) {125+ Object.values(storage).forEach(name => store.removeItem(name));126+ store.removeItem("tiny.bunker");127+ }121128 };122129 123130 const rememberRemote = async (uri, key, signer) => {124- sessionStorage.setItem(storage.uri, uri);125- sessionStorage.setItem(storage.key, window.NostrSigner.bytesToHex(key));126- sessionStorage.setItem(storage.pubkey, await signer.getPublicKey());127- sessionStorage.setItem(storage.remote, JSON.stringify(signer.bp));131+ const store = document.getElementById("remember-signer")?.checked ? localStorage : sessionStorage;132+ store.setItem(storage.uri, uri);133+ store.setItem(storage.key, window.NostrSigner.bytesToHex(key));134+ store.setItem(storage.pubkey, await signer.getPublicKey());135+ store.setItem(storage.remote, JSON.stringify(signer.bp));128136 };129137 130138 const setSigner = signer => {@@ -229,10 +237,11 @@
229237 230238 // Resume a remote signer stored for this tab so page loads keep working.231239 (async () => {232- const uri = sessionStorage.getItem(storage.uri);233- const keyHex = sessionStorage.getItem(storage.key);234- const expected = sessionStorage.getItem(storage.pubkey);235- const remote = sessionStorage.getItem(storage.remote);240+ const store = sessionStorage.getItem(storage.key) ? sessionStorage : localStorage;241+ const uri = store.getItem(storage.uri);242+ const keyHex = store.getItem(storage.key);243+ const expected = store.getItem(storage.pubkey);244+ const remote = store.getItem(storage.remote);236245 if (!uri || !keyHex) return;237246 try {238247 const key = window.NostrSigner.hexToBytes(keyHex);@@ -26,6 +26,8 @@
2626 // <nostr-compose kind="…" coordinate="30617:…" [root="…" root-pubkey="…" root-kind="…"] [parent="…" …]>2727 // Signs a NIP-34 issue, pull request, NIP-22 reply or status event and2828 // posts it to the relay.29+// <share-link [url="…"] [title="…"]>30+// The device share sheet for the page; empty without browser support.2931 // <push-toggle>3032 // Opts this browser into relay notifications; asks for permission only3133 // when pressed and registers the device with a signed request.@@ -218,7 +220,7 @@
218220 connectedCallback() {219221 if (this.bound) return;220222 this.bound = true;221- this.innerHTML = '<h3>Share</h3><p><button type="button" data-copy>Copy Blossom URI</button> <button type="button" data-link>Copy share link</button></p><form data-share><label>Send to <input name="recipient" inputmode="text" placeholder="npub or hex pubkey"></label><button>Send privately</button></form><output role="status"></output>';223+ this.innerHTML = '<h3>Share</h3><p><button type="button" data-copy>Copy Blossom URI</button> <button type="button" data-link>Copy share link</button> <share-link></share-link></p><form data-share><label>Send to <input name="recipient" inputmode="text" placeholder="npub or hex pubkey"></label><button>Send privately</button></form><output role="status"></output>';222224 this.output = this.querySelector("output");223225 this.querySelector("[data-copy]").addEventListener("click", () => this.copy(this.blossomURI()));224226 this.querySelector("[data-link]").addEventListener("click", () => this.copy(this.shareLink()));@@ -405,6 +407,24 @@
405407 // JsonView renders any JSON value. Arrays of objects become tables, objects406408 // become definition lists, scalar arrays become lists, and deep nesting407409 // falls back to compact JSON so a response never explodes the page.410+ // ShareLink offers the device's own share sheet for the current page, or411+ // the url and title attributes when set. It stays empty where the browser412+ // has no share support, so the page reads the same without it.413+ class ShareLink extends HTMLElement {414+ connectedCallback() {415+ if (this.bound) return;416+ this.bound = true;417+ if (!navigator.share) return;418+ const button = el("button", "Share…");419+ button.type = "button";420+ button.addEventListener("click", () => {421+ const data = {url: this.getAttribute("url") || location.href.split("#", 1)[0], title: this.getAttribute("title") || document.title};422+ navigator.share(data).catch(() => {});423+ });424+ this.replaceChildren(button);425+ }426+ }427+408428 // PushToggle opts this browser into relay notifications. Nothing happens409429 // until the button is pressed: permission is requested then, and the410430 // device subscription is registered with a signed request.@@ -810,6 +830,7 @@
810830 customElements.define("publish-list", PublishList);811831 customElements.define("nostr-compose", NostrCompose);812832 customElements.define("push-toggle", PushToggle);833+ customElements.define("share-link", ShareLink);813834 customElements.define("nostr-key", NostrKey);814835 customElements.define("json-view", JsonView);815836 @@ -276,6 +276,8 @@
276276 this.controller = new AbortController();277277 this.controls(true, false);278278 let completed = false;279+ // Keep the phone awake for the length of the upload.280+ const wakeLock = await navigator.wakeLock?.request?.("screen").catch(() => null);279281 try {280282 const {files, folder, encrypt} = this.selection;281283 let link;@@ -299,6 +301,7 @@
299301 );300302 throw error;301303 } finally {304+ wakeLock?.release?.().catch?.(() => {});302305 this.busy = false;303306 this.controls(false, completed);304307 if (completed) this.selection = this.chosen = null;@@ -52,6 +52,7 @@
5252 {{define "signerControls"}}<p><button id="session-login" type="button">Sign in with connected signer</button></p>5353 <h2>Phone signer</h2>5454 <p>Scan the QR code with Amber and approve the connection and sign-in request. This page will open your relay when you finish.</p>55+<p><label><input type="checkbox" id="remember-signer"> Remember this device</label> <small>Keeps the phone signer connected after the app closes. Sign out to forget it.</small></p>5556 <p><button id="nostrconnect" type="button">Generate QR code</button> <a id="nostrconnect-open" hidden>Open in signer</a></p>5657 <img id="signer-qr" alt="Nostr Connect QR code" hidden width="240" height="240">5758 <p id="signer-status" role="status"></p>@@ -100,7 +101,7 @@
100101 101102 {{define "event"}}<section>102103 <h1>Event</h1>103-{{if .Feed}}{{template "eventList" .}}{{else}}{{template "nostrEvent" .Event}}{{end}}104+{{if .Feed}}{{template "eventList" .}}{{else}}{{template "nostrEvent" .Event}}<share-link></share-link>{{end}}104105 <p><a href="/feed.xml">Atom feed</a> | <a href="/articles.json">JSON feed</a></p>105106 </section>106107 {{end}}@@ -37,6 +37,7 @@ img{max-width:100%}
3737 #who{color:var(--muted)}#who b,#who nostr-key{color:var(--ink)}3838 #who button{border:0;background:none;padding:0;color:var(--accent);font:inherit}#who button:hover{text-decoration:underline}3939 #rail-tools{margin-top:12px}40+share-link{display:inline-block}share-link:empty{display:none}4041 #theme{border:0;background:none;padding:0;color:var(--muted);font:12px var(--mono)}#theme:hover{color:var(--ink);text-decoration:underline}4142 4243 /* Rail */@@ -24,7 +24,7 @@ async function page({signedIn = false, bunkerResult = null} = {}) {
2424 navigator: {clipboard: {writeText: async () => {} }},2525 getComputedStyle: () => ({getPropertyValue: () => ""}),2626 matchMedia: () => ({matches: false, addEventListener() {}}),27- localStorage: {getItem: () => null, setItem() {}},27+ localStorage: {getItem: () => null, setItem() {}, removeItem() {}},2828 sessionStorage: {getItem: key => saved.get(key), setItem: (key, value) => saved.set(key, value), removeItem: key => saved.delete(key)},2929 NostrSigner: {3030 generateSecretKey: () => new Uint8Array(32), getPublicKey: () => "b".repeat(64), createNostrConnectURI: () => "nostrconnect://test",