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:
@@ -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)}`;
|
||||
}
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user