The "Also on fediverse" widget was passing the blog post URL directly to authorize_interaction. If a static file server intercepts the request before Node.js, the remote instance gets HTML instead of AP JSON and shows "Could not connect to the given address". Now fetches /activitypub/api/ap-url first to get the Fedify-served AP object URL (/activitypub/objects/…), which is always routed to Node.js and reliably returns AP JSON. Falls back to the original URL on error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
/**
|
|
* 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)}`;
|
|
}
|
|
},
|
|
}));
|
|
});
|