Ignore:
Timestamp:
06/28/2026 10:25:40 AM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
0596945
Parents:
37a297d
Message:

feat(news): show boosts from followed accounts in the News feed

An Announce (boost) from an account you follow, of a REMOTE post, was dropped — only Creates
and boosts of YOUR own posts were handled. Now such a boost resolves the boosted note and
stores it in ap_timeline (marked with who boosted it) so it appears in News with a 'X boosted'
label. We only store it for display and NEVER auto-Announce it onward (anti-feedback-loop);
published = now so it surfaces as fresh activity.

  • config/database.js — ap_timeline.reblog_name/reblog_handle/reblog_icon
  • services/ActivityPubService.js — handleInbox Announce branch stores remote boosts from followed accounts
  • views/pages/news.ejs — 'boosted by' label; renamed the page-header div to tl-titlebar to avoid clashing with the card's tl-head class
  • services/i18n.js — tl.boosted (nl/en/de)
File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r37a297d rc6cdce6  
    729729  if (type === 'Like' || type === 'Announce') {
    730730    const tgt = act.object;
    731     const pid = postIdFromNoteUrl(typeof tgt === 'string' ? tgt : (tgt && tgt.id), base);
     731    const objUrl = typeof tgt === 'string' ? tgt : (tgt && tgt.id);
     732    const pid = postIdFromNoteUrl(objUrl, base);
    732733    if (pid && actorUri && !isLocalActor && localPostExists(pid)) {
    733734      const ai = actorInfo(await resolveActor(actorUri), actorUri);
    734735      iStmts().ins.run(type.toLowerCase(), pid, '', actorUri, ai.name, ai.handle, ai.url, ai.icon, null, null, null);
    735736      console.log('[AP]', type === 'Like' ? 'like' : 'boost', actorUri, '→', pid);
     737    } else if (type === 'Announce' && objUrl && actorUri && !isLocalActor) {
     738      // A boost FROM an account we follow, of a REMOTE post → show it in the News feed.
     739      // We only STORE it for display; we NEVER auto-Announce it onward (anti-feedback-loop:
     740      // re-announcing an incoming Announce would cascade boosts across the network).
     741      let subs = []; try { subs = db.prepare('SELECT slug FROM ap_following WHERE actor_uri = ?').all(actorUri); } catch { /* table may not exist */ }
     742      if (subs.length) {
     743        const bn = await fetchNoteAP(objUrl);
     744        if (bn && bn !== 404 && (bn.type === 'Note' || bn.type === 'Article') && bn.id) {
     745          const origUri = actorUriOf(bn.attributedTo);
     746          const oai = actorInfo(await resolveActor(origUri), origUri);
     747          const html = HtmlSanitizerService.sanitize(bn.content || '');
     748          const media = mediaFromNote(bn);
     749          const booster = actorInfo(await resolveActor(actorUri), actorUri);
     750          for (const s of subs) {
     751            // published = now → the boost shows as fresh activity at the top (Mastodon shows
     752            // reblogs at reblog-time, not the original's date). INSERT OR IGNORE: if we already
     753            // have the note (e.g. we also follow the author), keep it and DON'T relabel it.
     754            let inserted = false;
     755            try { const r = tlStmts().ins.run(bn.id, s.slug, origUri || '', oai.name, oai.handle, oai.icon, oai.url, html, bn.url || null, new Date().toISOString(), media, bn.sensitive ? 1 : 0, bn.summary || null); inserted = r.changes > 0; } catch { /* ignore */ }
     756            if (inserted) { try { db.prepare('UPDATE ap_timeline SET reblog_name = ?, reblog_handle = ?, reblog_icon = ? WHERE slug = ? AND id = ?').run(booster.name, booster.handle, booster.icon, s.slug, bn.id); } catch { /* ignore */ } }
     757          }
     758          console.log('[AP] timeline boost +', actorUri, 'x' + subs.length);
     759        }
     760      }
    736761    }
    737762    return 202;
Note: See TracChangeset for help on using the changeset viewer.