feat: multi-domain fediverse support and share-to-mastodon upgrade

- Replace single localStorage string with versioned multi-domain store
  (fediverse_domains_v1) with usage tracking, inspired by Mastodon's
  share.joinmastodon.org project
- Add domain validation via URL constructor before redirecting
- Add mode param to fediverseInteract component: "interact" for
  authorize_interaction, "share" for /share?text=...
- Migrate old fediverse_instance key automatically on first load
- Extract shared modal partial (fediverse-modal.njk) used by post
  interaction, follow widget, and share widget
- Share widget now prompts visitors for their own instance instead of
  hardcoding site owner's Mastodon instance

Confab-Link: http://localhost:8080/sessions/0ec83454-d346-4329-8aaf-6b12139bf596
This commit is contained in:
Ricardo
2026-03-03 11:09:29 +01:00
parent 9b5fe6014d
commit 760058d0e4
5 changed files with 214 additions and 112 deletions

View File

@@ -1,31 +1,102 @@
/**
* Fediverse remote interaction component (Alpine.js)
* Enables users to like/boost/reply from their own fediverse instance
* via the authorize_interaction endpoint.
* 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", (postUrl) => ({
postUrl,
Alpine.data("fediverseInteract", (targetUrl, mode) => ({
targetUrl,
mode: mode || "interact",
showModal: false,
instance: "",
savedDomains: [],
showInput: false,
error: "",
handleClick(event) {
event.preventDefault();
const saved = localStorage.getItem("fediverse_instance");
if (saved && !event.shiftKey) {
this.redirectToInstance(saved);
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;
}
this.openModal(saved);
if (this.savedDomains.length === 0) {
this.showInput = true;
} else {
this.showInput = false;
}
this.instance = "";
this.error = "";
this.showModal = true;
},
openModal(prefill) {
this.instance = prefill || "";
this.showModal = true;
showAddNew() {
this.showInput = true;
this.instance = "";
this.error = "";
this.$nextTick(() => {
const input = this.$refs.instanceInput;
if (input) input.focus();
@@ -37,14 +108,37 @@ document.addEventListener("alpine:init", () => {
if (!domain) return;
// Strip protocol and trailing slashes
domain = domain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
localStorage.setItem("fediverse_instance", domain);
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;
}
},
redirectToInstance(domain) {
const url = `https://${domain}/authorize_interaction?uri=${encodeURIComponent(this.postUrl)}`;
window.location.href = url;
if (this.mode === "share") {
window.location.href = `https://${domain}/share?text=${encodeURIComponent(this.targetUrl)}`;
} else {
window.location.href = `https://${domain}/authorize_interaction?uri=${encodeURIComponent(this.targetUrl)}`;
}
},
}));
});