fix: synthesize timeline content for likes/bookmarks/reposts

Interaction types (likes, bookmarks, reposts) have no body content in
their JF2 properties. The timeline entry was created with empty content,
showing blank posts in Phanpy/Moshidon. Now synthesizes display content
(e.g. "Liked: https://...") matching backfill-timeline.js behavior.
This commit is contained in:
Ricardo
2026-03-27 15:32:15 +01:00
parent 42c0959c8a
commit f1ad18d92d
2 changed files with 47 additions and 9 deletions

View File

@@ -224,7 +224,7 @@ export function createSyndicator(plugin) {
// timelines (Phanpy/Moshidon). Uses $setOnInsert — idempotent. // timelines (Phanpy/Moshidon). Uses $setOnInsert — idempotent.
try { try {
const profile = await plugin._collections.ap_profile?.findOne({}); const profile = await plugin._collections.ap_profile?.findOne({});
const content = normalizeContent(properties.content); const content = buildTimelineContent(properties);
const timelineItem = { const timelineItem = {
uid: properties.url, uid: properties.url,
url: properties.url, url: properties.url,
@@ -289,13 +289,51 @@ export function createSyndicator(plugin) {
// ─── Timeline helpers ─────────────────────────────────────────────────────── // ─── Timeline helpers ───────────────────────────────────────────────────────
function normalizeContent(content) { /**
if (!content) return { text: "", html: "" }; * Build content from JF2 properties. Synthesizes content for interaction
* types (likes, bookmarks, reposts) that have no body text.
*/
function buildTimelineContent(properties) {
const content = properties.content;
if (content) {
if (typeof content === "string") return { text: content, html: `<p>${content}</p>` }; if (typeof content === "string") return { text: content, html: `<p>${content}</p>` };
if (content.text || content.html) {
return { return {
text: content.text || content.value || "", text: content.text || content.value || "",
html: content.html || content.text || content.value || "", html: content.html || content.text || content.value || "",
}; };
}
}
// Synthesize for interaction types
const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
const likeOf = properties["like-of"];
if (likeOf) {
return {
text: `Liked: ${likeOf}`,
html: `<p>Liked: <a href="${esc(likeOf)}">${esc(likeOf)}</a></p>`,
};
}
const bookmarkOf = properties["bookmark-of"];
if (bookmarkOf) {
const label = properties.name || bookmarkOf;
return {
text: `Bookmarked: ${label}`,
html: `<p>Bookmarked: <a href="${esc(bookmarkOf)}">${esc(label)}</a></p>`,
};
}
const repostOf = properties["repost-of"];
if (repostOf) {
const label = properties.name || repostOf;
return {
text: `Reposted: ${label}`,
html: `<p>Reposted: <a href="${esc(repostOf)}">${esc(label)}</a></p>`,
};
}
if (properties.name) {
return { text: properties.name, html: `<p>${esc(properties.name)}</p>` };
}
return { text: "", html: "" };
} }
function mapPostType(postType) { function mapPostType(postType) {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "3.10.1", "version": "3.10.2",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",