// Progressive enhancement only. Without this file the page still shows the // media in a swipeable rail and the original URL as selectable text. const VIEW_KEY = 'antisocial:view'; function setupCopy() { for (const button of document.querySelectorAll('.copy')) { button.addEventListener('click', async () => { const url = button.dataset.url; if (!url) return; try { await navigator.clipboard.writeText(url); } catch { // No clipboard permission, or an insecure context. Select the URL // instead so one keystroke finishes the job. const code = button.closest('.original')?.querySelector('code'); if (code) { const range = document.createRange(); range.selectNodeContents(code); const selection = getSelection(); selection?.removeAllRanges(); selection?.addRange(range); } return; } button.dataset.copied = 'true'; button.querySelector('.copy__idle')?.setAttribute('hidden', ''); button.querySelector('.copy__done')?.removeAttribute('hidden'); setTimeout(() => { delete button.dataset.copied; button.querySelector('.copy__idle')?.removeAttribute('hidden'); button.querySelector('.copy__done')?.setAttribute('hidden', ''); }, 1600); }); } } function setupMedia() { const media = document.querySelector('.media'); if (!media) return; const rail = media.querySelector('.media__rail'); const items = [...media.querySelectorAll('.item')]; const at = media.querySelector('.media__at'); const buttons = [...media.querySelectorAll('.media__view')]; if (!rail || items.length < 2) return; function applyView(view, remember) { media.dataset.view = view; for (const button of buttons) { button.setAttribute('aria-pressed', String(button.dataset.view === view)); } if (remember) { try { localStorage.setItem(VIEW_KEY, view); } catch { // Private browsing. The choice just won't survive the page. } } } let stored = null; try { stored = localStorage.getItem(VIEW_KEY); } catch { stored = null; } applyView(stored === 'grid' ? 'grid' : 'swipe', false); for (const button of buttons) { button.addEventListener('click', () => applyView(button.dataset.view, true)); } // Tapping a thumbnail goes back to the swiper, at that image. for (const item of items) { item.addEventListener('click', (event) => { if (media.dataset.view !== 'grid') return; if (event.target.closest('video')) return; applyView('swipe', true); item.scrollIntoView({ inline: 'center', block: 'nearest' }); }); } // Keep the counter honest as the rail scrolls. if (at) { let frame = 0; rail.addEventListener('scroll', () => { if (frame) return; frame = requestAnimationFrame(() => { frame = 0; const index = Math.round(rail.scrollLeft / rail.clientWidth); at.textContent = String(Math.min(items.length, Math.max(1, index + 1))); }); }, { passive: true }); } } setupCopy(); setupMedia(); // --- Verification puzzles ------------------------------------------------- // // The puzzle is rendered in the browser on the server, not this one. So the // page shows a live screenshot of it and forwards pointer events back to be // replayed. Solving it leaves the cookie in that browser, and the post then // loads normally. function setupChallenge() { const article = document.querySelector('[data-challenge]'); if (!article) return; const id = article.dataset.challenge; const frame = document.getElementById('frame'); const status = document.getElementById('status'); const stage = document.getElementById('stage'); const giveUp = document.getElementById('give-up'); let box = parseBox(stage.dataset.box); let dragging = false; let lastSent = 0; let stopped = false; function parseBox(value) { const parts = (value || '').split(',').map(Number); return parts.length === 4 && parts.every(Number.isFinite) ? { x: parts[0], y: parts[1], width: parts[2], height: parts[3] } : null; } // Self-paced: the next frame is only asked for once the last one painted, // so a slow screenshot slows the stream instead of queueing behind itself. function pump() { if (stopped) return; frame.src = `/challenge/${id}/frame?t=${Date.now()}`; } frame.addEventListener('load', () => { status.hidden = true; setTimeout(pump, dragging ? 60 : 220); }); frame.addEventListener('error', () => { if (!stopped) setTimeout(pump, 1000); }); pump(); function toPageCoords(event) { const rect = frame.getBoundingClientRect(); if (!rect.width || !rect.height) return null; const fx = (event.clientX - rect.left) / rect.width; const fy = (event.clientY - rect.top) / rect.height; // Without a clip box the screenshot is the whole viewport, whose size // the server knows; fall back to the image's own pixel dimensions. const region = box ?? { x: 0, y: 0, width: frame.naturalWidth / 2, height: frame.naturalHeight / 2 }; return { x: region.x + fx * region.width, y: region.y + fy * region.height }; } async function send(type, point) { try { const response = await fetch(`/challenge/${id}/input`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ type, x: point.x, y: point.y }), }); if (response.status === 410) finish(); } catch { // A dropped event during a drag is survivable; the next one follows. } } frame.addEventListener('pointerdown', (event) => { const point = toPageCoords(event); if (!point) return; event.preventDefault(); dragging = true; frame.setPointerCapture(event.pointerId); void send('down', point); }); frame.addEventListener('pointermove', (event) => { if (!dragging) return; const now = Date.now(); if (now - lastSent < 25) return; lastSent = now; const point = toPageCoords(event); if (point) void send('move', point); }); for (const type of ['pointerup', 'pointercancel']) { frame.addEventListener(type, async (event) => { if (!dragging) return; dragging = false; const point = toPageCoords(event); if (point) await send('up', point); status.hidden = false; status.textContent = 'Checking…'; void poll(); }); } async function poll() { for (let attempt = 0; attempt < 12 && !stopped; attempt += 1) { await new Promise((r) => setTimeout(r, 500)); try { const response = await fetch(`/challenge/${id}/state`); if (response.status === 410) return finish(); const state = await response.json(); if (state.box) box = state.box; if (state.solved) return finish(); } catch { // Keep trying; the page may be mid-navigation. } } status.textContent = 'Still there — have another go.'; } async function finish() { if (stopped) return; stopped = true; status.hidden = false; status.textContent = 'Got it. Loading the post…'; try { const response = await fetch(`/challenge/${id}/done`, { method: 'POST' }); const { returnTo } = await response.json(); location.href = returnTo || '/'; } catch { location.reload(); } } giveUp?.addEventListener('click', () => { stopped = true; void fetch(`/challenge/${id}/done`, { method: 'POST' }).finally(() => { article.querySelector('.original__url')?.scrollIntoView({ block: 'center' }); status.textContent = 'Gave up. The original link is below.'; status.hidden = false; }); }); } setupChallenge();