Ignore:
Timestamp:
06/26/2026 12:16:33 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
4c47eff
Parents:
3384e95
Message:

feat(tijdlijn): unboost button — retract a boost (Undo Announce) from posts

The 🔁 on a timeline post is now a toggle: not boosted -> boost (Announce + mark
locally); boosted -> unboost (highlighted) which sends Undo(Announce) so followers'
servers remove the reblog, and clears the local flag. sendInteraction handles
'unboost' (Undo wrapping Announce, matched on actor+object — no record of the
original Announce needed). All operator-driven: your click sends it, nothing
automatic touches the fediverse.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r3384e95 r78b6d8a  
    10731073}
    10741074// Mark a timeline post as boosted so it shows in the Cirkel (mixed by date).
    1075 let _markBoost, _boostedCount;
     1075let _markBoost, _unmarkBoost, _boostedCount;
    10761076export function markBoosted(slug, noteId) {
    10771077  try { if (!_markBoost) _markBoost = db.prepare('UPDATE ap_timeline SET boosted = 1 WHERE slug = ? AND id = ?'); _markBoost.run(slug, noteId); } catch { /* ignore */ }
     1078}
     1079export function unmarkBoosted(slug, noteId) {
     1080  try { if (!_unmarkBoost) _unmarkBoost = db.prepare('UPDATE ap_timeline SET boosted = 0 WHERE slug = ? AND id = ?'); _unmarkBoost.run(slug, noteId); } catch { /* ignore */ }
    10781081}
    10791082export function boostedCount(slug) {
     
    12261229  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
    12271230  if (!base || !site || !site.slug || !targetNoteId) return { error: 'config' };
    1228   const type = kind === 'boost' ? 'Announce' : 'Like';
    12291231  const me = actorId(base, site.slug);
    12301232  const keys = getOrCreateKeys(site.slug);
    1231   const act = {
    1232     '@context': 'https://www.w3.org/ns/activitystreams',
    1233     id: `${me}#${type.toLowerCase()}-${Date.now()}-${rid()}`,
    1234     type, actor: me, object: targetNoteId,
    1235   };
    1236   if (type === 'Announce') { act.to = [PUBLIC]; act.cc = [`${me}/followers`]; }
     1233  // 'unboost' = Undo(Announce): retracts a boost so followers' servers remove the
     1234  // reblog (matched on actor+object — no record of the original Announce needed).
     1235  const fanout = (kind === 'boost' || kind === 'unboost'); // also goes to our followers
     1236  let act;
     1237  if (kind === 'unboost') {
     1238    act = {
     1239      '@context': 'https://www.w3.org/ns/activitystreams',
     1240      id: `${me}#undo-${Date.now()}-${rid()}`, type: 'Undo', actor: me,
     1241      to: [PUBLIC], cc: [`${me}/followers`],
     1242      object: { id: `${me}#announce-${Date.now()}-${rid()}`, type: 'Announce', actor: me, object: targetNoteId },
     1243    };
     1244  } else {
     1245    const type = kind === 'boost' ? 'Announce' : 'Like';
     1246    act = {
     1247      '@context': 'https://www.w3.org/ns/activitystreams',
     1248      id: `${me}#${type.toLowerCase()}-${Date.now()}-${rid()}`,
     1249      type, actor: me, object: targetNoteId,
     1250    };
     1251    if (type === 'Announce') { act.to = [PUBLIC]; act.cc = [`${me}/followers`]; }
     1252  }
    12371253  const inboxes = new Set();
    12381254  if (authorUri) { const a = await fetchActor(authorUri).catch(() => null); if (a) inboxes.add((a.endpoints && a.endpoints.sharedInbox) || a.inbox); }
    1239   // A boost is public → also deliver to our own followers so it shows for them.
    1240   if (type === 'Announce') { for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox); }
     1255  if (fanout) { for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox); }
    12411256  let delivered = 0;
    12421257  for (const inbox of [...inboxes].filter(Boolean)) { try { const st = await deliver(inbox, act, `${me}#main-key`, keys.private_pem); if (st >= 200 && st < 300) delivered++; } catch { /* best-effort */ } }
    1243   console.log('[AP]', type, site.slug, '→', targetNoteId, 'delivered', delivered);
     1258  console.log('[AP]', kind, site.slug, '→', targetNoteId, 'delivered', delivered);
    12441259  return { ok: true, delivered };
    12451260}
     
    13561371  listOutbox, deliverOutboxDelete,
    13571372  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,
    1358   autoBoostCount, boostedCount, markBoosted, getCirkelPosts, getCirkelMembers, autoMigrateCircles, selfHealTimeline, boostLatestN,
     1373  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, getCirkelPosts, getCirkelMembers, autoMigrateCircles, selfHealTimeline, boostLatestN,
    13591374  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    13601375  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
Note: See TracChangeset for help on using the changeset viewer.