feat: render custom emoji in reader (Release 1)

Extract custom emoji from ActivityPub objects (Fedify Emoji tags) and
Mastodon API (status.emojis, account.emojis). Replace :shortcode:
patterns with <img> tags in the unified processing pipeline.

Emoji rendering applies to post content, author display names, boost
attribution, and quote embed authors. Uses the shared postProcessItems()
pipeline so both reader and explore views get emoji automatically.

Bump version to 2.5.1.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
This commit is contained in:
Ricardo
2026-03-03 13:13:28 +01:00
parent ab2363d123
commit 02d449d03c
8 changed files with 137 additions and 8 deletions

View File

@@ -3,7 +3,7 @@
* @module timeline-store
*/
import { Article, Hashtag, Mention } from "@fedify/fedify/vocab";
import { Article, Emoji, Hashtag, Mention } from "@fedify/fedify/vocab";
import sanitizeHtml from "sanitize-html";
/**
@@ -82,7 +82,26 @@ export async function extractActorInfo(actor, options = {}) {
// Invalid URL, keep handle empty
}
return { name, url, photo, handle };
// Extract custom emoji from actor tags
const emojis = [];
try {
if (typeof actor.getTags === "function") {
const tags = await actor.getTags(loaderOpts);
for await (const tag of tags) {
if (tag instanceof Emoji) {
const shortcode = (tag.name?.toString() || "").replace(/^:|:$/g, "");
const iconUrl = tag.iconId?.href || "";
if (shortcode && iconUrl) {
emojis.push({ shortcode, url: iconUrl });
}
}
}
}
} catch {
// Emoji extraction failed — non-critical
}
return { name, url, photo, handle, emojis };
}
/**
@@ -190,8 +209,10 @@ export async function extractObjectData(object, options = {}) {
// Extract tags — Fedify uses async getTags() which returns typed vocab objects.
// Hashtag → category[] (plain strings, # prefix stripped)
// Mention → mentions[] ({ name, url } objects for profile linking)
// Emoji → emojis[] ({ shortcode, url } for custom emoji rendering)
const category = [];
const mentions = [];
const emojis = [];
try {
if (typeof object.getTags === "function") {
const tags = await object.getTags(loaderOpts);
@@ -206,6 +227,13 @@ export async function extractObjectData(object, options = {}) {
// tag.href is a URL object — use .href to get the string
const mentionUrl = tag.href?.href || "";
if (mentionName) mentions.push({ name: mentionName, url: mentionUrl });
} else if (tag instanceof Emoji) {
// Custom emoji: name is ":shortcode:", icon is an Image with url
const shortcode = (tag.name?.toString() || "").replace(/^:|:$/g, "");
const iconUrl = tag.iconId?.href || "";
if (shortcode && iconUrl) {
emojis.push({ shortcode, url: iconUrl });
}
}
}
}
@@ -259,6 +287,7 @@ export async function extractObjectData(object, options = {}) {
author,
category,
mentions,
emojis,
photo,
video,
audio,