Changeset 443f982 in Klonkt


Ignore:
Timestamp:
06/30/2026 02:37:32 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
1a0b007
Parents:
28b59e7
Message:

feat(federation): let Mastodon card external embeds — drop image attachments when a post has an embed

Mastodon shows EITHER media attachments OR a link preview card, never both. A post with an
external embed (Spotify/YouTube/SoundCloud/Vimeo/Bandcamp/Apple) plus a cover therefore showed the
cover and rendered the embed link as plain text — no player. Now, when the post has an embed link,
buildNote skips the image attachments so Mastodon renders the embed's player card. Klonkt's own
rendering is unchanged (cover + embed player still show on-site). Detection reuses
AudioEmbedService.detectProvider (single source of truth, covers all 6 providers).

  • src/services/ActivityPubService.js — buildNote suppresses image attachments when an embed is present

Co-Authored-By: Claude <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r28b59e7 r443f982  
    2121import db from '../config/database.js';
    2222import HtmlSanitizerService from './HtmlSanitizerService.js';
     23import AudioEmbedService from './AudioEmbedService.js';
    2324
    2425const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
     
    225226  const hadAudio = /\[\[(track|album|playlist):/i.test(post.content || '');
    226227  const playable = hasPlayableAudio(post.content || '', site && site.id);
     228  // A post with an external embed (Spotify/YouTube/SoundCloud/Vimeo/Bandcamp/Apple) should let
     229  // Mastodon render the embed's player CARD. Mastodon shows EITHER media attachments OR a link
     230  // card, never both — so when the post has an embed link we skip the image attachments so the
     231  // card wins. (On Klonkt nothing changes: the cover + the embed player still render.)
     232  const hasEmbed = (() => {
     233    const c = post.content || '';
     234    if (/\[\[embed:/i.test(c)) return true;
     235    for (const m of c.matchAll(/https?:\/\/[^\s"'<>]+/gi)) if (AudioEmbedService.detectProvider(m[0])) return true;
     236    return false;
     237  })();
     238  const noImages = playable || hasEmbed; // suppress image attachments → let the player/embed card show
    227239  const urls = [];
    228240  // Posts with PLAYABLE hosted audio suppress image attachments so Mastodon renders
     
    230242  // link/player card are mutually exclusive on Mastodon. Link-only audio (external)
    231243  // keeps its cover (no player card to show).
    232   if (post.cover_image_url && !playable) urls.push(abs(post.cover_image_url));
     244  if (post.cover_image_url && !noImages) urls.push(abs(post.cover_image_url));
    233245  let body = post.content || '';
    234246  // Only federate inline images we can actually serve: absolute http(s) URLs, or our own
    235247  // /media/ uploads. A relative path we don't host (e.g. a stale /images/... ref) would 404
    236248  // and show up as a black tile in Mastodon's attachment grid.
    237   if (!playable) for (const m of body.matchAll(/<img\b[^>]*\bsrc="([^"]+)"[^>]*>/gi)) {
     249  if (!noImages) for (const m of body.matchAll(/<img\b[^>]*\bsrc="([^"]+)"[^>]*>/gi)) {
    238250    const src = m[1];
    239251    if (/^https?:\/\//i.test(src) || src.startsWith('/media/')) urls.push(abs(src));
Note: See TracChangeset for help on using the changeset viewer.