/** * Fediverse interaction & sharing component (Alpine.js) * * Two modes: * - "interact" (default): redirect to authorize_interaction for like/boost/reply/follow * - "share": redirect to /share?text=... for composing a new post * * Stores multiple domains in localStorage with usage tracking. * Registered via Alpine.data() so the component is available * regardless of script loading order. */ const STORAGE_KEY = "fediverse_domains_v1"; const OLD_STORAGE_KEY = "fediverse_instance"; function loadDomains() { try { const json = localStorage.getItem(STORAGE_KEY); if (json) return JSON.parse(json); } catch { /* corrupted data */ } // Migrate from old single-domain key const old = localStorage.getItem(OLD_STORAGE_KEY); if (old) { const domains = [{ domain: old, used: 1, lastUsed: new Date().toISOString() }]; localStorage.setItem(STORAGE_KEY, JSON.stringify(domains)); localStorage.removeItem(OLD_STORAGE_KEY); return domains; } return []; } function saveDomains(domains) { localStorage.setItem(STORAGE_KEY, JSON.stringify(domains)); } function addDomain(domain) { const domains = loadDomains(); const existing = domains.find((d) => d.domain === domain); if (existing) { existing.used += 1; existing.lastUsed = new Date().toISOString(); } else { domains.push({ domain, used: 1, lastUsed: new Date().toISOString() }); } saveDomains(domains); return domains; } function removeDomain(domain) { const domains = loadDomains().filter((d) => d.domain !== domain); saveDomains(domains); return domains; } function isValidDomain(str) { try { return new URL(`https://${str}`).hostname === str; } catch { return false; } } document.addEventListener("alpine:init", () => { Alpine.data("fediverseInteract", (targetUrl, mode) => ({ targetUrl, mode: mode || "interact", showModal: false, instance: "", savedDomains: [], showInput: false, error: "", handleClick(event) { event.preventDefault(); this.savedDomains = loadDomains().sort((a, b) => b.used - a.used); if (this.savedDomains.length === 1 && !event.shiftKey) { addDomain(this.savedDomains[0].domain); this.redirectToInstance(this.savedDomains[0].domain); return; } if (this.savedDomains.length === 0) { this.showInput = true; } else { this.showInput = false; } this.instance = ""; this.error = ""; this.showModal = true; }, showAddNew() { this.showInput = true; this.instance = ""; this.error = ""; this.$nextTick(() => { const input = this.$refs.instanceInput; if (input) input.focus(); }); }, confirm() { let domain = this.instance.trim(); if (!domain) return; // Strip protocol and trailing slashes domain = domain.replace(/^https?:\/\//, "").replace(/\/+$/, ""); if (!isValidDomain(domain)) { this.error = "Please enter a valid domain (e.g. mastodon.social)"; return; } this.error = ""; this.savedDomains = addDomain(domain); this.showModal = false; this.redirectToInstance(domain); }, useSaved(domain) { this.savedDomains = addDomain(domain); this.showModal = false; this.redirectToInstance(domain); }, deleteSaved(domain) { this.savedDomains = removeDomain(domain); if (this.savedDomains.length === 0) { this.showInput = true; } }, trapFocus(event) { const focusable = [...this.$el.querySelectorAll('button, input, a, [tabindex]:not([tabindex="-1"])')].filter(el => !el.closest('[x-show]') || el.closest('[x-show]').style.display !== 'none'); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }, async redirectToInstance(domain) { if (this.mode === "share") { window.location.href = `https://${domain}/share?text=${encodeURIComponent(this.targetUrl)}`; } else { // Resolve the blog post URL to its Fedify-served AP object URL. // Fedify URLs (/activitypub/objects/…) are always routed to Node.js, // ensuring reliable AP content negotiation when the remote instance // fetches the URI to process authorize_interaction. let interactUrl = this.targetUrl; try { const resp = await fetch(`/activitypub/api/ap-url?post=${encodeURIComponent(this.targetUrl)}`); if (resp.ok) { const data = await resp.json(); if (data.apUrl) interactUrl = data.apUrl; } } catch { /* network error — fall back to blog post URL */ } window.location.href = `https://${domain}/authorize_interaction?uri=${encodeURIComponent(interactUrl)}`; } }, })); });