Let the "Open on" button choose a browser
CI / Typecheck, test, build (pull_request) Successful in 28s

The StopTheMadness rules are indiscriminate, which is the point, but they
catch the link on the way back out as well: in the browser they are
installed in, "Open on <platform>" redirects straight back here. The one
button meant to reach the app is the one that cannot.

Handing the address to a different browser is the way past it, and the only
way to do that from a page is that browser's own URL scheme. `/` gets a
picker for which one; the choice lives in that browser's localStorage,
because which browsers are installed is a fact about the device and the
phone's answer is not the Mac's.

The scheme table is its own module so a test can pin it -- every browser
spells it differently, and Edge differs between macOS and iOS. Firefox has
no scheme on macOS, so it is only offered on iOS, and an unknown or
schemeless choice keeps the plain link rather than producing a dead one.
The markup still carries the plain https address and the script swaps it
afterwards, so nothing changes without JavaScript. Once a browser is
chosen the button says which, since a scheme for a browser that is not
installed opens nothing and the tap would otherwise be silent.

Closes #7

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:36:00 -03:00
co-authored by Claude Opus 5
parent 3ff7375320
commit abf8ec317c
8 changed files with 243 additions and 0 deletions
+64
View File
@@ -1,7 +1,10 @@
// 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';
const VIEW_KEY = 'antisocial:view';
const BROWSER_KEY = 'antisocial:browser';
function setupCopy() {
for (const button of document.querySelectorAll('.copy')) {
@@ -37,6 +40,65 @@ function setupCopy() {
}
}
function storedBrowser() {
try {
return localStorage.getItem(BROWSER_KEY) ?? 'default';
} catch {
return 'default';
}
}
// 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.
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}`;
}
}
// Only on the index page, and only with this file running: the choice is
// useless without the rewriting above, so the control ships hidden.
function setupBrowserPicker() {
const select = document.querySelector('.picker__select');
if (!select) return;
for (const browser of browsersFor(detectOs())) {
const option = document.createElement('option');
option.value = browser.id;
option.textContent = browser.label;
select.append(option);
}
// A stored id with no scheme on this system would leave the control
// showing nothing at all, so fall back rather than render a blank.
const stored = storedBrowser();
select.value = [...select.options].some((option) => option.value === stored) ? stored : 'default';
select.addEventListener('change', () => {
try {
localStorage.setItem(BROWSER_KEY, select.value);
} catch {
// Private browsing. The choice just won't survive the page.
}
applyBrowser(select.value);
});
select.closest('.picker').hidden = false;
}
// One per rail: a post can carry several -- a thread of them, or a post and
// the post it quotes -- and wiring only the first leaves the rest inert.
function setupMedia(media) {
@@ -127,6 +189,8 @@ function setupComments() {
}
setupCopy();
applyBrowser(storedBrowser());
setupBrowserPicker();
for (const media of document.querySelectorAll('.media')) setupMedia(media);
setupComments();