mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
feat: add fediverse URL/handle lookup input to reader
Adds a search box at the top of the reader page where users can paste any fediverse URL or @user@domain handle. Uses Fedify's lookupObject() which natively resolves URLs, handles, and acct: URIs, then redirects to the internal post detail or remote profile view.
This commit is contained in:
@@ -4,6 +4,54 @@
|
||||
* Uses Indiekit CSS custom properties for automatic dark mode support
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
Fediverse Lookup
|
||||
========================================================================== */
|
||||
|
||||
.ap-lookup {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
|
||||
.ap-lookup__input {
|
||||
flex: 1;
|
||||
padding: var(--space-s) var(--space-m);
|
||||
border: var(--border-width-thin) solid var(--color-outline);
|
||||
border-radius: var(--border-radius-small);
|
||||
background: var(--color-offset);
|
||||
color: var(--color-on-background);
|
||||
font-size: var(--font-size-m);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.ap-lookup__input::placeholder {
|
||||
color: var(--color-on-offset);
|
||||
}
|
||||
|
||||
.ap-lookup__input:focus {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -1px;
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.ap-lookup__btn {
|
||||
padding: var(--space-s) var(--space-m);
|
||||
border: var(--border-width-thin) solid var(--color-primary);
|
||||
border-radius: var(--border-radius-small);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
font-size: var(--font-size-m);
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ap-lookup__btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Tab Navigation
|
||||
========================================================================== */
|
||||
|
||||
2
index.js
2
index.js
@@ -57,6 +57,7 @@ import {
|
||||
featuredTagsAddController,
|
||||
featuredTagsRemoveController,
|
||||
} from "./lib/controllers/featured-tags.js";
|
||||
import { resolveController } from "./lib/controllers/resolve.js";
|
||||
import {
|
||||
refollowPauseController,
|
||||
refollowResumeController,
|
||||
@@ -202,6 +203,7 @@ export default class ActivityPubEndpoint {
|
||||
router.post("/admin/reader/unlike", unlikeController(mp, this));
|
||||
router.post("/admin/reader/boost", boostController(mp, this));
|
||||
router.post("/admin/reader/unboost", unboostController(mp, this));
|
||||
router.get("/admin/reader/resolve", resolveController(mp, this));
|
||||
router.get("/admin/reader/profile", remoteProfileController(mp, this));
|
||||
router.get("/admin/reader/post", postDetailController(mp, this));
|
||||
router.post("/admin/reader/follow", followController(mp, this));
|
||||
|
||||
109
lib/controllers/resolve.js
Normal file
109
lib/controllers/resolve.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Resolve controller — accepts any fediverse URL or handle, resolves it
|
||||
* via lookupObject(), and redirects to the appropriate internal view.
|
||||
*/
|
||||
import {
|
||||
Article,
|
||||
Note,
|
||||
Person,
|
||||
Service,
|
||||
Application,
|
||||
Organization,
|
||||
Group,
|
||||
} from "@fedify/fedify";
|
||||
|
||||
/**
|
||||
* GET /admin/reader/resolve?q=<url-or-handle>
|
||||
* Resolves a fediverse URL or @user@domain handle and redirects to
|
||||
* the post detail or remote profile view.
|
||||
*/
|
||||
export function resolveController(mountPath, plugin) {
|
||||
return async (request, response, next) => {
|
||||
try {
|
||||
const query = (request.query.q || "").trim();
|
||||
|
||||
if (!query) {
|
||||
return response.redirect(`${mountPath}/admin/reader`);
|
||||
}
|
||||
|
||||
if (!plugin._federation) {
|
||||
return response.status(503).render("error", {
|
||||
title: "Error",
|
||||
content: "Federation not initialized",
|
||||
});
|
||||
}
|
||||
|
||||
const handle = plugin.options.actor.handle;
|
||||
const ctx = plugin._federation.createContext(
|
||||
new URL(plugin._publicationUrl),
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
|
||||
// Determine if input is a URL or a handle
|
||||
// lookupObject accepts: URLs, @user@domain, user@domain, acct:user@domain
|
||||
let lookupInput;
|
||||
|
||||
try {
|
||||
// If it parses as a URL, pass as URL object
|
||||
const parsed = new URL(query);
|
||||
lookupInput = parsed;
|
||||
} catch {
|
||||
// Not a URL — treat as handle (strip leading @ if present)
|
||||
lookupInput = query;
|
||||
}
|
||||
|
||||
let object;
|
||||
|
||||
try {
|
||||
object = await ctx.lookupObject(lookupInput, { documentLoader });
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[resolve] lookupObject failed for "${query}":`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
if (!object) {
|
||||
return response.status(404).render("error", {
|
||||
title: response.locals.__("activitypub.reader.resolve.notFoundTitle"),
|
||||
content: response.locals.__(
|
||||
"activitypub.reader.resolve.notFound",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Determine object type and redirect accordingly
|
||||
const objectUrl =
|
||||
object.id?.href || object.url?.href || query;
|
||||
|
||||
if (
|
||||
object instanceof Person ||
|
||||
object instanceof Service ||
|
||||
object instanceof Application ||
|
||||
object instanceof Organization ||
|
||||
object instanceof Group
|
||||
) {
|
||||
return response.redirect(
|
||||
`${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (object instanceof Note || object instanceof Article) {
|
||||
return response.redirect(
|
||||
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Unknown type — try post detail as fallback
|
||||
return response.redirect(
|
||||
`${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -191,6 +191,13 @@
|
||||
"loadingThread": "Loading thread...",
|
||||
"threadError": "Could not load full thread"
|
||||
},
|
||||
"resolve": {
|
||||
"placeholder": "Paste a fediverse URL or @user@domain handle…",
|
||||
"label": "Look up a fediverse post or account",
|
||||
"button": "Look up",
|
||||
"notFoundTitle": "Not found",
|
||||
"notFound": "Could not find this post or account. The URL may be invalid, the server may be unavailable, or the content may have been deleted."
|
||||
},
|
||||
"linkPreview": {
|
||||
"label": "Link preview"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
{% from "prose/macro.njk" import prose with context %}
|
||||
|
||||
{% block readercontent %}
|
||||
{{ heading({ text: __("activitypub.reader.title"), level: 1 }) }}
|
||||
{# Fediverse lookup #}
|
||||
<form action="{{ mountPath }}/admin/reader/resolve" method="get" class="ap-lookup">
|
||||
<input type="text" name="q" class="ap-lookup__input"
|
||||
placeholder="{{ __('activitypub.reader.resolve.placeholder') }}"
|
||||
aria-label="{{ __('activitypub.reader.resolve.label') }}">
|
||||
<button type="submit" class="ap-lookup__btn">{{ __("activitypub.reader.resolve.button") }}</button>
|
||||
</form>
|
||||
|
||||
{# Tab navigation #}
|
||||
<nav class="ap-tabs" role="tablist">
|
||||
|
||||
Reference in New Issue
Block a user