mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
Reader now resolves ActivityPub links internally instead of navigating to external instances. Actor links open the profile view, post links open a new post detail view with thread context (parent chain + replies). External links in post content get rich preview cards (title, description, image, favicon) fetched via unfurl.js at ingest time with fire-and-forget async processing and concurrency limiting. New files: post-detail controller, og-unfurl module, lookup-cache, link preview template/CSS, client-side link interception JS. Includes SSRF protection for OG fetching and GoToSocial URL support.
89 lines
2.5 KiB
JavaScript
89 lines
2.5 KiB
JavaScript
/**
|
|
* Client-side AP link interception for internal navigation
|
|
* Redirects ActivityPub links to internal reader views
|
|
*/
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
// Fediverse URL patterns that should open internally
|
|
const AP_URL_PATTERN =
|
|
/\/@[\w.-]+\/\d+|\/@[\w.-]+\/statuses\/[\w]+|\/users\/[\w.-]+\/statuses\/\d+|\/objects\/[\w-]+|\/notice\/[\w]+|\/notes\/[\w]+|\/post\/\d+|\/comment\/\d+|\/p\/[\w.-]+\/\d+/;
|
|
|
|
// Get mount path from DOM
|
|
function getMountPath() {
|
|
// Look for data-mount-path on reader container or header
|
|
const container = document.querySelector(
|
|
"[data-mount-path]",
|
|
);
|
|
return container ? container.dataset.mountPath : "/activitypub";
|
|
}
|
|
|
|
// Check if a link should be intercepted
|
|
function shouldInterceptLink(link) {
|
|
const href = link.getAttribute("href");
|
|
if (!href) return null;
|
|
|
|
const classes = link.className || "";
|
|
|
|
// Mention links → profile view
|
|
if (classes.includes("mention")) {
|
|
return { type: "profile", url: href };
|
|
}
|
|
|
|
// AP object URL patterns → post detail view
|
|
if (AP_URL_PATTERN.test(href)) {
|
|
return { type: "post", url: href };
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Handle link click
|
|
function handleLinkClick(event) {
|
|
const link = event.target.closest("a");
|
|
if (!link) return;
|
|
|
|
// Only intercept links inside post content
|
|
const contentDiv = link.closest(".ap-card__content");
|
|
if (!contentDiv) return;
|
|
|
|
const interception = shouldInterceptLink(link);
|
|
if (!interception) return;
|
|
|
|
// Prevent default navigation
|
|
event.preventDefault();
|
|
|
|
const mountPath = getMountPath();
|
|
const encodedUrl = encodeURIComponent(interception.url);
|
|
|
|
if (interception.type === "profile") {
|
|
window.location.href = `${mountPath}/admin/reader/profile?url=${encodedUrl}`;
|
|
} else if (interception.type === "post") {
|
|
window.location.href = `${mountPath}/admin/reader/post?url=${encodedUrl}`;
|
|
}
|
|
}
|
|
|
|
// Initialize on DOM ready
|
|
function init() {
|
|
// Use event delegation on timeline container
|
|
const timeline = document.querySelector(".ap-timeline");
|
|
if (timeline) {
|
|
timeline.addEventListener("click", handleLinkClick);
|
|
}
|
|
|
|
// Also set up on post detail view
|
|
const postDetail = document.querySelector(".ap-post-detail");
|
|
if (postDetail) {
|
|
postDetail.addEventListener("click", handleLinkClick);
|
|
}
|
|
}
|
|
|
|
// Run on DOMContentLoaded or immediately if already loaded
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|