No browser chosen, no "Open on" button
CI / Typecheck, test, build (pull_request) Successful in 26s

The plain address is the one thing that button must not offer -- followed
in the browser the rewrite rules are installed in, it comes straight back
here -- so there is nothing to show until there is a browser to hand it to.
The copy button and the selectable URL were always the part carrying the
weight; the open link now ships hidden and the script reveals it along with
the scheme.

That also settles what the button should say. It went back to "Open on
<platform>": the browser's name was there to explain a tap that went
nowhere, and a button that is not shown until it works needs no such
explanation.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01KF5YF3iZVKbezwALap8LYd
This commit is contained in:
2026-08-29 19:40:39 -03:00
co-authored by Claude Opus 5
parent abf8ec317c
commit 899b6e38d8
6 changed files with 28 additions and 21 deletions
+6 -5
View File
@@ -95,11 +95,12 @@ which is the browser being escaped. The choice is kept in that browser's own
`localStorage` — not a cookie and not synced, because "which browser is installed" is a `localStorage` — not a cookie and not synced, because "which browser is installed" is a
fact about the device, not about you, and the phone's answer is not the Mac's. fact about the device, not about you, and the phone's answer is not the Mac's.
The link in the page is always the plain `https://` address, and the scheme is swapped in Until a browser is chosen there is no "Open on …" button at all — the plain address is
by the script afterwards; with JavaScript off, or with the default left alone, nothing the one thing it must not offer, since following it only comes back here. The markup
changes. Once a browser is chosen the button says which one, because a scheme for a carries that address anyway, hidden, and the script swaps in the scheme and reveals the
browser that isn't installed opens nothing at all, and a silent tap needs an button once there is somewhere to send it. So with JavaScript off, or with the default
explanation. The URL underneath stays selectable either way. left alone, the copy button and the selectable URL are the whole of it, which is all
that was ever load-bearing.
## How it works ## How it works
+8 -11
View File
@@ -1,7 +1,7 @@
// Progressive enhancement only. Without this file the page still shows the // Progressive enhancement only. Without this file the page still shows the
// media in a swipeable rail and the original URL as selectable text. // media in a swipeable rail and the original URL as selectable text.
import { browserById, browsersFor, detectOs, openUrlFor } from './browsers.js'; import { browsersFor, detectOs, openUrlFor } from './browsers.js';
const VIEW_KEY = 'antisocial:view'; const VIEW_KEY = 'antisocial:view';
const BROWSER_KEY = 'antisocial:browser'; const BROWSER_KEY = 'antisocial:browser';
@@ -48,24 +48,21 @@ function storedBrowser() {
} }
} }
// The markup carries the plain address, so the link still goes somewhere // The markup carries the plain address, which is the one thing the button
// with this file missing. Only once a browser has been chosen is the href // must not offer: followed in the browser the rewrite rules are installed
// swapped for that browser's scheme -- and the name goes onto the button, // in, it redirects straight back to this page. So the link ships hidden and
// because a scheme for a browser that is not installed opens nothing and // is only revealed once a browser has been picked to hand it to. With no
// the tap would otherwise be a silent no-op. // choice made -- or none this system has a scheme for -- the copy button
// and the URL below it are the whole of it.
function applyBrowser(id) { function applyBrowser(id) {
const os = detectOs(); const os = detectOs();
const label = browserById(id)?.label;
for (const link of document.querySelectorAll('.original__open')) { for (const link of document.querySelectorAll('.original__open')) {
link.dataset.original ??= link.getAttribute('href'); link.dataset.original ??= link.getAttribute('href');
link.dataset.label ??= link.textContent.trim();
const href = openUrlFor(link.dataset.original, id, os); const href = openUrlFor(link.dataset.original, id, os);
link.href = href; link.href = href;
link.textContent = href === link.dataset.original link.hidden = href === link.dataset.original;
? link.dataset.label
: `${link.dataset.label} in ${label}`;
} }
} }
+1 -1
View File
@@ -47,7 +47,7 @@ export function browsersFor(os) {
return BROWSERS.filter((browser) => browser.id === 'default' || browser[os]); return BROWSERS.filter((browser) => browser.id === 'default' || browser[os]);
} }
export function browserById(id) { function browserById(id) {
return BROWSERS.find((browser) => browser.id === id); return BROWSERS.find((browser) => browser.id === id);
} }
+1 -1
View File
@@ -28,7 +28,7 @@ export function renderChallenge(challenge: Challenge, box: Box | undefined): str
<footer class="post__foot"> <footer class="post__foot">
<div class="original"> <div class="original">
<button type="button" class="copy" id="give-up">Give up and show me the link</button> <button type="button" class="copy" id="give-up">Give up and show me the link</button>
<a class="original__open" href="${challenge.originalUrl}" rel="noopener noreferrer nofollow" target="_blank"> <a class="original__open" href="${challenge.originalUrl}" rel="noopener noreferrer nofollow" target="_blank" hidden>
Open on ${challenge.platformLabel} Open on ${challenge.platformLabel}
</a> </a>
<p class="original__url"><code>${challenge.originalUrl}</code></p> <p class="original__url"><code>${challenge.originalUrl}</code></p>
+7 -2
View File
@@ -43,14 +43,19 @@ export function badge(platform: string, label: string): Raw {
} }
/** The copy control, plus the URL itself so it is always selectable even if /** The copy control, plus the URL itself so it is always selectable even if
* the clipboard API is unavailable. */ * the clipboard API is unavailable.
*
* The open link ships hidden: followed in the browser the rewrite rules are
* installed in, it redirects straight back here, so it is only worth showing
* once a browser has been picked to hand it to. The script reveals it, and
* the href in the markup is the plain address it starts from. */
export function originalUrlBlock(originalUrl: string, platformLabel: string): Raw { export function originalUrlBlock(originalUrl: string, platformLabel: string): Raw {
return html`<div class="original"> return html`<div class="original">
<button type="button" class="copy" data-url="${originalUrl}"> <button type="button" class="copy" data-url="${originalUrl}">
<span class="copy__idle">Copy original link</span> <span class="copy__idle">Copy original link</span>
<span class="copy__done" hidden>Copied</span> <span class="copy__done" hidden>Copied</span>
</button> </button>
<a class="original__open" href="${originalUrl}" rel="noopener noreferrer nofollow" target="_blank">Open on ${platformLabel}</a> <a class="original__open" href="${originalUrl}" rel="noopener noreferrer nofollow" target="_blank" hidden>Open on ${platformLabel}</a>
<p class="original__url"><code>${originalUrl}</code></p> <p class="original__url"><code>${originalUrl}</code></p>
</div>`; </div>`;
} }
+5 -1
View File
@@ -105,8 +105,12 @@ test('a failure still hands the link back', () => {
detail: 'TikTok showed a verification puzzle instead of the post.', detail: 'TikTok showed a verification puzzle instead of the post.',
}); });
assert.ok(page.includes('data-url="https://www.tiktok.com/@a/video/1"')); assert.ok(page.includes('data-url="https://www.tiktok.com/@a/video/1"'));
assert.ok(page.includes('Open on TikTok')); assert.ok(page.includes('<code>https://www.tiktok.com/@a/video/1</code>'));
assert.ok(page.includes('verification puzzle')); assert.ok(page.includes('verification puzzle'));
// Following the original link in the browser the rewrite rules are
// installed in only comes back here, so it stays hidden until the script
// has a browser to hand it to.
assert.ok(/<a class="original__open"[^>]* hidden>Open on TikTok<\/a>/.test(page));
}); });
function redditPost(overrides: Partial<Post> = {}): Post { function redditPost(overrides: Partial<Post> = {}): Post {