diff --git a/_includes/components/fediverse-modal.njk b/_includes/components/fediverse-modal.njk new file mode 100644 index 0000000..f505418 --- /dev/null +++ b/_includes/components/fediverse-modal.njk @@ -0,0 +1,81 @@ +{# Shared fediverse instance picker modal #} +{# Used by post.njk (interact), fediverse-follow.njk (follow), share.njk (share) #} +{# Requires: modalTitle, modalDescription variables set before include #} + diff --git a/_includes/components/widgets/fediverse-follow.njk b/_includes/components/widgets/fediverse-follow.njk index d34f42d..10d081f 100644 --- a/_includes/components/widgets/fediverse-follow.njk +++ b/_includes/components/widgets/fediverse-follow.njk @@ -18,7 +18,7 @@ {% if actorUrl %} -
+

Follow Me

Follow me from your fediverse instance.

Follow on the Fediverse - - {# Modal overlay for instance entry — same pattern as post.njk #} - + {% set modalTitle = "Follow on the Fediverse" %} + {% set modalDescription = "Choose your instance to follow this account." %} + {% include "components/fediverse-modal.njk" %}
{% endif %} diff --git a/_includes/components/widgets/share.njk b/_includes/components/widgets/share.njk index 497a3d5..0c8d9b1 100644 --- a/_includes/components/widgets/share.njk +++ b/_includes/components/widgets/share.njk @@ -1,26 +1,31 @@ {# Share Widget #} +{% set shareText = title + " " + site.url + page.url %}

Share

- - + - - - - - + + + + + {% set modalTitle = "Share on Mastodon / Fediverse" %} + {% set modalDescription = "Choose your instance to share this post." %} + {% include "components/fediverse-modal.njk" %} +
diff --git a/_includes/layouts/post.njk b/_includes/layouts/post.njk index 6f8e38c..35e8c8b 100644 --- a/_includes/layouts/post.njk +++ b/_includes/layouts/post.njk @@ -91,7 +91,7 @@ withBlogSidebar: true
{# Fediverse remote interaction button (self-hosted ActivityPub) #} {% if selfHostedApUrl %} - + Fediverse - - {# Modal overlay for instance entry #} - + {% set modalTitle = "Fediverse Interaction" %} + {% set modalDescription = "Choose your instance to like, boost, or reply." %} + {% include "components/fediverse-modal.njk" %} {% endif %} diff --git a/js/fediverse-interact.js b/js/fediverse-interact.js index d3f62c1..82e3ef7 100644 --- a/js/fediverse-interact.js +++ b/js/fediverse-interact.js @@ -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)}`; + } }, })); });