Changeset f1956d7 in Klonkt


Ignore:
Timestamp:
07/01/2026 01:56:09 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
7b07035
Parents:
c45c187
Message:

feat(fediverse): inline @mentions in posts (link + notify)

Typing @user@host in a post now federates as a real Mastodon-style mention: the
handle is linked to the actor, added to the note's Mention tags + cc, and the note
is delivered to the mentioned person's inbox so they're notified — even if they
don't follow the site. Mirrors the existing reply-mention flow (which resolved
mentions but posts didn't). On-site the post text is unchanged for now.

  • src/services/ActivityPubService.js — buildNote emits Mention tags + adds the mentioned actors to cc (from already-linked content); deliverCreate/deliverUpdate resolve inline mentions at send time (WebFinger), link the note content, and add the mentioned inboxes to the delivery set (so a mention reaches non-followers too).
  • test/mentions.test.js — Mention tag shape, cc addressing, and the no-mention no-op.
  • CHANGELOG(.nl/.de).md — "Mention people in a post" under Unreleased.

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

Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • CHANGELOG.de.md

    rc45c187 rf1956d7  
    77
    88### Hinzugefügt
     9- **Erwähne Personen in einem Beitrag.** `@benutzer@server` in einem Beitrag verlinkt jetzt auf ihr
     10  Profil und benachrichtigt sie im Fediverse — auch wenn sie dir nicht folgen — wie eine Erwähnung
     11  in einer Antwort.
    912- **Kurze Videos im Feed spielen automatisch ab und wiederholen sich.** Ein animiertes Cover oder ein
    1013  kurzer (≤30s) Clip im News-Feed wird jetzt automatisch stumm in Schleife abgespielt, wie ein GIF;
  • CHANGELOG.md

    rc45c187 rf1956d7  
    77
    88### Added
     9- **Mention people in a post.** Typing `@user@server` in a post now links to their profile and
     10  notifies them on the fediverse — even if they don't follow you — just like a mention in a reply.
    911- **Short videos in the feed autoplay and loop.** An animated cover or a short (≤30s) clip in the
    1012  News feed now plays automatically and loops muted, like a GIF; longer videos keep their controls.
  • CHANGELOG.nl.md

    rc45c187 rf1956d7  
    77
    88### Toegevoegd
     9- **Noem mensen in een post.** `@gebruiker@server` in een post linkt nu naar hun profiel en stuurt
     10  ze een melding op de fediverse — ook als ze je niet volgen — net als een vermelding in een reactie.
    911- **Korte video's in de feed spelen automatisch af en loopen.** Een geanimeerde cover of een korte
    1012  (≤30s) clip in de News-feed speelt nu automatisch geluidloos in een lus, als een GIF; langere
  • src/services/ActivityPubService.js

    rc45c187 rf1956d7  
    352352  for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players
    353353
     354  // Inline @user@host mentions: the Mention tag objects + the mentioned actor URIs. Only
     355  // present when the content was already mention-linked (deliverCreate/Update resolve them
     356  // at send time); a plain buildNote (outbox/notes) yields none.
     357  const _mentionTags = mentionTags(body);
     358  const _mentionCc = _mentionTags.map((t) => t.href);
     359
    354360  const note = {
    355361    id,
     
    362368    // but not addressed to Public, so Mastodon shows it only to them and can't boost it).
    363369    to: post.fan_only ? [`${aId}/followers`] : [PUBLIC],
    364     cc: post.fan_only ? [] : [`${aId}/followers`],
    365     tag: buildHashtagList(base, post.tags, body),
     370    // Mentioned actors (from inline @user@host links the caller resolved) are addressed in cc
     371    // so Mastodon notifies them; empty unless the content was mention-linked (delivery time).
     372    cc: [...new Set([...(post.fan_only ? [] : [`${aId}/followers`]), ..._mentionCc])],
     373    tag: [...buildHashtagList(base, post.tags, body), ..._mentionTags],
    366374    replies: `${id}/replies`,
    367375    // NSFW → Mastodon-style content warning: sensitive (blurs media) + a summary/spoiler
     
    11381146  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    11391147  if (!base || !site || !site.slug) return;
     1148  // Resolve inline @user@host mentions → link them in the note + collect their inboxes, so a
     1149  // mentioned person is notified even if they don't follow us (Mastodon-standard mention).
     1150  const mres = await resolveMentionsInText(base, post.content || '');
     1151  const post2 = mres.inboxes.length ? { ...post, content: mres.html } : post;
    11401152  const followers = fStmts().list.all(site.slug);
    1141   if (!followers.length) return;
    1142   const inboxes = [...new Set(followers.map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
     1153  const inboxes = [...new Set([...followers.map((f) => f.shared_inbox || f.inbox), ...mres.inboxes].filter(Boolean))];
     1154  if (!inboxes.length) return; // no followers and no one mentioned
    11431155  const keys = getOrCreateKeys(site.slug);
    11441156  const keyId = `${actorId(base, site.slug)}#main-key`;
    1145   const create = buildCreate(base, site, post);
     1157  const create = buildCreate(base, site, post2);
    11461158  for (const inbox of inboxes) deliverWithRetry(site.slug, inbox, create, keyId, keys.private_pem);
    11471159}
     
    11951207  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    11961208  if (!base || !site || !site.slug || !post || !post.id) return;
     1209  const mres = await resolveMentionsInText(base, post.content || ''); // link mentions + collect inboxes
     1210  const post2 = mres.inboxes.length ? { ...post, content: mres.html } : post;
    11971211  const followers = fStmts().list.all(site.slug);
    1198   if (!followers.length) return;
    1199   const inboxes = [...new Set(followers.map((f) => f.shared_inbox || f.inbox).filter(Boolean))];
     1212  const inboxes = [...new Set([...followers.map((f) => f.shared_inbox || f.inbox), ...mres.inboxes].filter(Boolean))];
     1213  if (!inboxes.length) return;
    12001214  const keys = getOrCreateKeys(site.slug);
    12011215  const me = actorId(base, site.slug);
    1202   const note = buildNote(base, site, post);
     1216  const note = buildNote(base, site, post2);
    12031217  note.updated = new Date().toISOString();
    12041218  const update = {
    12051219    '@context': AP_CONTEXT,
    12061220    id: `${noteId(base, post.id)}#update-${Date.now()}-${rid()}`,
    1207     type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
     1221    type: 'Update', actor: me, to: [PUBLIC], cc: note.cc,
    12081222    object: note,
    12091223  };
Note: See TracChangeset for help on using the changeset viewer.