tinyrelaycommit

Commit 88d0641c9349

browse files at this commit | history

1diff --git a/internal/webui/bridge.js b/internal/webui/bridge.js
2index 13a5ed7..eaecd9b 100644
3--- a/internal/webui/bridge.js
4+++ b/internal/webui/bridge.js
5@@ -105,6 +105,9 @@
6 document.getElementById("menu")?.focus();
7 });
8 if ("serviceWorker" in navigator) navigator.serviceWorker.register(localPath("/sw.js")).catch(() => {});
9+ // An installed app asks to keep its cached shell and parked shares. Browsers
10+ // grant this silently for installed apps, so it never shows a prompt.
11+ if (matchMedia("(display-mode: standalone)").matches) navigator.storage?.persist?.().catch(() => {});
12 Object.assign(window.tiny, {root, localPath, sha256hex, authorization, signedFetch});
13 window.tinySignedFetch = signedFetch;
14
15@@ -115,16 +118,21 @@
16 location.assign(localPath("/"));
17 };
18
19+ // A remote signer session lives in this tab unless the person chose to
20+ // remember the device; then it survives the installed app being closed.
21 const forgetRemote = () => {
22- Object.values(storage).forEach(name => sessionStorage.removeItem(name));
23- sessionStorage.removeItem("tiny.bunker");
24+ for (const store of [sessionStorage, localStorage]) {
25+ Object.values(storage).forEach(name => store.removeItem(name));
26+ store.removeItem("tiny.bunker");
27+ }
28 };
29
30 const rememberRemote = async (uri, key, signer) => {
31- sessionStorage.setItem(storage.uri, uri);
32- sessionStorage.setItem(storage.key, window.NostrSigner.bytesToHex(key));
33- sessionStorage.setItem(storage.pubkey, await signer.getPublicKey());
34- sessionStorage.setItem(storage.remote, JSON.stringify(signer.bp));
35+ const store = document.getElementById("remember-signer")?.checked ? localStorage : sessionStorage;
36+ store.setItem(storage.uri, uri);
37+ store.setItem(storage.key, window.NostrSigner.bytesToHex(key));
38+ store.setItem(storage.pubkey, await signer.getPublicKey());
39+ store.setItem(storage.remote, JSON.stringify(signer.bp));
40 };
41
42 const setSigner = signer => {
43@@ -229,10 +237,11 @@
44
45 // Resume a remote signer stored for this tab so page loads keep working.
46 (async () => {
47- const uri = sessionStorage.getItem(storage.uri);
48- const keyHex = sessionStorage.getItem(storage.key);
49- const expected = sessionStorage.getItem(storage.pubkey);
50- const remote = sessionStorage.getItem(storage.remote);
51+ const store = sessionStorage.getItem(storage.key) ? sessionStorage : localStorage;
52+ const uri = store.getItem(storage.uri);
53+ const keyHex = store.getItem(storage.key);
54+ const expected = store.getItem(storage.pubkey);
55+ const remote = store.getItem(storage.remote);
56 if (!uri || !keyHex) return;
57 try {
58 const key = window.NostrSigner.hexToBytes(keyHex);
59diff --git a/internal/webui/components.js b/internal/webui/components.js
60index e3bc926..f1386c3 100644
61--- a/internal/webui/components.js
62+++ b/internal/webui/components.js
63@@ -26,6 +26,8 @@
64 // <nostr-compose kind="…" coordinate="30617:…" [root="…" root-pubkey="…" root-kind="…"] [parent="…" …]>
65 // Signs a NIP-34 issue, pull request, NIP-22 reply or status event and
66 // posts it to the relay.
67+// <share-link [url="…"] [title="…"]>
68+// The device share sheet for the page; empty without browser support.
69 // <push-toggle>
70 // Opts this browser into relay notifications; asks for permission only
71 // when pressed and registers the device with a signed request.
72@@ -218,7 +220,7 @@
73 connectedCallback() {
74 if (this.bound) return;
75 this.bound = true;
76- 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>';
77+ 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>';
78 this.output = this.querySelector("output");
79 this.querySelector("[data-copy]").addEventListener("click", () => this.copy(this.blossomURI()));
80 this.querySelector("[data-link]").addEventListener("click", () => this.copy(this.shareLink()));
81@@ -405,6 +407,24 @@
82 // JsonView renders any JSON value. Arrays of objects become tables, objects
83 // become definition lists, scalar arrays become lists, and deep nesting
84 // falls back to compact JSON so a response never explodes the page.
85+ // ShareLink offers the device's own share sheet for the current page, or
86+ // the url and title attributes when set. It stays empty where the browser
87+ // has no share support, so the page reads the same without it.
88+ class ShareLink extends HTMLElement {
89+ connectedCallback() {
90+ if (this.bound) return;
91+ this.bound = true;
92+ if (!navigator.share) return;
93+ const button = el("button", "Share…");
94+ button.type = "button";
95+ button.addEventListener("click", () => {
96+ const data = {url: this.getAttribute("url") || location.href.split("#", 1)[0], title: this.getAttribute("title") || document.title};
97+ navigator.share(data).catch(() => {});
98+ });
99+ this.replaceChildren(button);
100+ }
101+ }
102+
103 // PushToggle opts this browser into relay notifications. Nothing happens
104 // until the button is pressed: permission is requested then, and the
105 // device subscription is registered with a signed request.
106@@ -810,6 +830,7 @@
107 customElements.define("publish-list", PublishList);
108 customElements.define("nostr-compose", NostrCompose);
109 customElements.define("push-toggle", PushToggle);
110+ customElements.define("share-link", ShareLink);
111 customElements.define("nostr-key", NostrKey);
112 customElements.define("json-view", JsonView);
113
114diff --git a/internal/webui/file-workspace.js b/internal/webui/file-workspace.js
115index 53550e3..ffed562 100644
116--- a/internal/webui/file-workspace.js
117+++ b/internal/webui/file-workspace.js
118@@ -276,6 +276,8 @@
119 this.controller = new AbortController();
120 this.controls(true, false);
121 let completed = false;
122+ // Keep the phone awake for the length of the upload.
123+ const wakeLock = await navigator.wakeLock?.request?.("screen").catch(() => null);
124 try {
125 const {files, folder, encrypt} = this.selection;
126 let link;
127@@ -299,6 +301,7 @@
128 );
129 throw error;
130 } finally {
131+ wakeLock?.release?.().catch?.(() => {});
132 this.busy = false;
133 this.controls(false, completed);
134 if (completed) this.selection = this.chosen = null;
135diff --git a/internal/webui/public.html b/internal/webui/public.html
136index d5e6e89..225ae4a 100644
137--- a/internal/webui/public.html
138+++ b/internal/webui/public.html
139@@ -52,6 +52,7 @@
140 {{define "signerControls"}}<p><button id="session-login" type="button">Sign in with connected signer</button></p>
141 <h2>Phone signer</h2>
142 <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>
143+<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>
144 <p><button id="nostrconnect" type="button">Generate QR code</button> <a id="nostrconnect-open" hidden>Open in signer</a></p>
145 <img id="signer-qr" alt="Nostr Connect QR code" hidden width="240" height="240">
146 <p id="signer-status" role="status"></p>
147@@ -100,7 +101,7 @@
148
149 {{define "event"}}<section>
150 <h1>Event</h1>
151-{{if .Feed}}{{template "eventList" .}}{{else}}{{template "nostrEvent" .Event}}{{end}}
152+{{if .Feed}}{{template "eventList" .}}{{else}}{{template "nostrEvent" .Event}}<share-link></share-link>{{end}}
153 <p><a href="/feed.xml">Atom feed</a> | <a href="/articles.json">JSON feed</a></p>
154 </section>
155 {{end}}
156diff --git a/internal/webui/style.css b/internal/webui/style.css
157index 630fd67..ff19462 100644
158--- a/internal/webui/style.css
159+++ b/internal/webui/style.css
160@@ -37,6 +37,7 @@ img{max-width:100%}
161 #who{color:var(--muted)}#who b,#who nostr-key{color:var(--ink)}
162 #who button{border:0;background:none;padding:0;color:var(--accent);font:inherit}#who button:hover{text-decoration:underline}
163 #rail-tools{margin-top:12px}
164+share-link{display:inline-block}share-link:empty{display:none}
165 #theme{border:0;background:none;padding:0;color:var(--muted);font:12px var(--mono)}#theme:hover{color:var(--ink);text-decoration:underline}
166
167 /* Rail */
168diff --git a/scripts/signer.test.mjs b/scripts/signer.test.mjs
169index ed216c2..08b2771 100644
170--- a/scripts/signer.test.mjs
171+++ b/scripts/signer.test.mjs
172@@ -24,7 +24,7 @@ async function page({signedIn = false, bunkerResult = null} = {}) {
173 navigator: {clipboard: {writeText: async () => {} }},
174 getComputedStyle: () => ({getPropertyValue: () => ""}),
175 matchMedia: () => ({matches: false, addEventListener() {}}),
176- localStorage: {getItem: () => null, setItem() {}},
177+ localStorage: {getItem: () => null, setItem() {}, removeItem() {}},
178 sessionStorage: {getItem: key => saved.get(key), setItem: (key, value) => saved.set(key, value), removeItem: key => saved.delete(key)},
179 NostrSigner: {
180 generateSecretKey: () => new Uint8Array(32), getPublicKey: () => "b".repeat(64), createNostrConnectURI: () => "nostrconnect://test",

About

ownerc01503e8035f
visibilitypublic
head88d0641c9349

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs