Let the "Open on" button choose a browser #8

Merged
thatguygriff merged 2 commits from choose-browser-for-open-on into main 2026-08-29 22:47:42 +00:00
6 changed files with 28 additions and 21 deletions
Showing only changes of commit 899b6e38d8 - Show all commits
+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
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
by the script afterwards; with JavaScript off, or with the default left alone, nothing
changes. Once a browser is chosen the button says which one, because a scheme for a
browser that isn't installed opens nothing at all, and a silent tap needs an
explanation. The URL underneath stays selectable either way.
Until a browser is chosen there is no "Open on …" button at all — the plain address is
the one thing it must not offer, since following it only comes back here. The markup
carries that address anyway, hidden, and the script swaps in the scheme and reveals the
button once there is somewhere to send it. So with JavaScript off, or with the default
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
+8 -11
View File
@@ -1,7 +1,7 @@
// Progressive enhancement only. Without this file the page still shows the
// 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 BROWSER_KEY = 'antisocial:browser';
@@ -48,24 +48,21 @@ function storedBrowser() {
}
}
// The markup carries the plain address, so the link still goes somewhere
// with this file missing. Only once a browser has been chosen is the href
// swapped for that browser's scheme -- and the name goes onto the button,
// because a scheme for a browser that is not installed opens nothing and
// the tap would otherwise be a silent no-op.
// The markup carries the plain address, which is the one thing the button
// must not offer: followed in the browser the rewrite rules are installed
// in, it redirects straight back to this page. So the link ships hidden and
// is only revealed once a browser has been picked to hand it to. With no
// 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) {
const os = detectOs();
const label = browserById(id)?.label;
for (const link of document.querySelectorAll('.original__open')) {
link.dataset.original ??= link.getAttribute('href');
link.dataset.label ??= link.textContent.trim();
const href = openUrlFor(link.dataset.original, id, os);
link.href = href;
link.textContent = href === link.dataset.original
? link.dataset.label
: `${link.dataset.label} in ${label}`;
link.hidden = href === link.dataset.original;
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ export function browsersFor(os) {
return BROWSERS.filter((browser) => browser.id === 'default' || browser[os]);
}
export function browserById(id) {
function browserById(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">
<div class="original">
<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}
</a>
<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 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 {
return html`<div class="original">
<button type="button" class="copy" data-url="${originalUrl}">
<span class="copy__idle">Copy original link</span>
<span class="copy__done" hidden>Copied</span>
</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>
</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.',
});
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'));
// 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 {