Changeset d988fa0 in Klonkt


Ignore:
Timestamp:
06/24/2026 03:23:01 PM (3 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
00f669b
Parents:
914eb9f
Message:

feat(fediverse-client): like/boost/reply on timeline items

Timeline posts get ⭐ like, 🔁 boost (sendInteraction → signed Like/Announce to
the author + followers) and ↩ reply (reuses the remote-interaction popover).

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

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    r914eb9f rd988fa0  
    594594  if (site && actorUri) { try { await ActivityPubService.unfollowActor(site, actorUri); } catch (e) { /* ignore */ } }
    595595  res.redirect('/tijdlijn?success=' + encodeURIComponent('Ontvolgd'));
     596});
     597
     598router.post('/tijdlijn/like', requireSiteManager, async (req, res) => {
     599  const site = res.locals.site;
     600  if (site) { try { await ActivityPubService.sendInteraction(site, 'like', (req.body.note || '').toString(), (req.body.author || '').toString()); } catch (e) { /* ignore */ } }
     601  res.redirect('/tijdlijn?success=' + encodeURIComponent('Geliket ⭐'));
     602});
     603
     604router.post('/tijdlijn/boost', requireSiteManager, async (req, res) => {
     605  const site = res.locals.site;
     606  if (site) { try { await ActivityPubService.sendInteraction(site, 'boost', (req.body.note || '').toString(), (req.body.author || '').toString()); } catch (e) { /* ignore */ } }
     607  res.redirect('/tijdlijn?success=' + encodeURIComponent('Geboost 🔁'));
    596608});
    597609
  • src/services/ActivityPubService.js

    r914eb9f rd988fa0  
    714714}
    715715
     716// Send a Like or Announce (boost) on a remote note FROM this site.
     717export async function sendInteraction(site, kind, targetNoteId, authorUri) {
     718  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
     719  if (!base || !site || !site.slug || !targetNoteId) return { error: 'config' };
     720  const type = kind === 'boost' ? 'Announce' : 'Like';
     721  const me = actorId(base, site.slug);
     722  const keys = getOrCreateKeys(site.slug);
     723  const act = {
     724    '@context': 'https://www.w3.org/ns/activitystreams',
     725    id: `${me}#${type.toLowerCase()}-${Date.now()}`,
     726    type, actor: me, object: targetNoteId,
     727  };
     728  if (type === 'Announce') { act.to = [PUBLIC]; act.cc = [`${me}/followers`]; }
     729  const inboxes = new Set();
     730  if (authorUri) { const a = await fetchActor(authorUri).catch(() => null); if (a) inboxes.add((a.endpoints && a.endpoints.sharedInbox) || a.inbox); }
     731  // A boost is public → also deliver to our own followers so it shows for them.
     732  if (type === 'Announce') { for (const f of fStmts().list.all(site.slug)) inboxes.add(f.shared_inbox || f.inbox); }
     733  let delivered = 0;
     734  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 */ } }
     735  console.log('[AP]', type, site.slug, '→', targetNoteId, 'delivered', delivered);
     736  return { ok: true, delivered };
     737}
     738
    716739export default {
    717740  getOrCreateKeys, apWants, sendAP, actorId, noteId,
     
    720743  getInteractions, getInteractionById, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
    721744  listOutbox, deliverOutboxDelete,
    722   webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline,
     745  webfingerResolve, followActor, unfollowActor, listFollowing, getTimeline, sendInteraction,
    723746};
  • src/views/pages/timeline.ejs

    r914eb9f rd988fa0  
    4545            <% media.filter(function(m){ return !m.type || /^image\//.test(m.type); }).forEach(function(m){ %><img class="tl-media" src="<%= m.url %>" alt="" loading="lazy"><% }); %>
    4646            <% if (p.url) { %><p class="tl-orig"><a href="<%= p.url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a></p><% } %>
     47            <div class="tl-actions">
     48              <form method="post" action="/tijdlijn/like" class="tl-act-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act" title="<%= t('fedi.likes') %>">⭐</button></form>
     49              <form method="post" action="/tijdlijn/boost" class="tl-act-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act" title="<%= t('fedi.boosts') %>">🔁</button></form>
     50              <button type="button" class="tl-act fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>">↩</button>
     51            </div>
    4752          </div>
    4853        </li>
     
    8287  .tl-orig { margin: .5rem 0 0; font-size: .82rem; }
    8388  .tl-orig a { color: var(--accent, #06c); }
     89  .tl-actions { display: flex; gap: .35rem; margin-top: .55rem; }
     90  .tl-act-form { margin: 0; }
     91  .tl-act { background: none; border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); border-radius: 999px; padding: .2rem .6rem; cursor: pointer; font-size: .9rem; color: var(--ink-soft, #888); }
     92  .tl-act:hover { color: var(--ink, #000); border-color: color-mix(in srgb, var(--accent, #888) 50%, transparent); }
    8493</style>
     94
     95<script>
     96(function(){
     97  if (window.__fediRemoteWired) return; window.__fediRemoteWired = true;
     98  var current = null, currentBtn = null;
     99  function close(){ if (current) { current.remove(); current = null; currentBtn = null; } }
     100  function go(raw, uri){ var d=(raw||'').trim().replace(/^@?[^@\s]*@/,'').replace(/^https?:\/\//i,'').replace(/\/.*$/,'').trim(); if(d) location.href='https://'+d+'/authorize_interaction?uri='+encodeURIComponent(uri||''); }
     101  function place(f, b){ var r=b.getBoundingClientRect(); f.style.top=(r.bottom+window.scrollY+6)+'px'; f.style.left=Math.max(8, Math.min(r.left+window.scrollX, window.scrollX+window.innerWidth-340))+'px'; }
     102  document.addEventListener('click', function(e){
     103    if (e.target.closest && e.target.closest('.fedi-remote-cancel')) { close(); return; }
     104    if (current && e.target.closest && e.target.closest('.fedi-remote-form')) return;
     105    var b = e.target.closest && e.target.closest('.fedi-remote-reply-btn');
     106    if (b) {
     107      e.preventDefault();
     108      if (currentBtn === b) { close(); return; }
     109      close();
     110      var f = document.createElement('form'); f.className='fedi-remote-form'; f.dataset.uri = b.getAttribute('data-fedi-uri')||'';
     111      f.innerHTML = '<input type="text" autocomplete="off" spellcheck="false"><button type="submit" class="btn btn-primary fedi-remote-go" aria-label="ok">&rarr;</button><button type="button" class="fedi-remote-cancel" aria-label="x">&times;</button>';
     112      f.querySelector('input').placeholder = b.getAttribute('data-fedi-ph') || '@name@server.social';
     113      document.body.appendChild(f); place(f, b); current=f; currentBtn=b; f.querySelector('input').focus();
     114      return;
     115    }
     116    if (current) close();
     117  });
     118  document.addEventListener('submit', function(e){ var f=e.target.closest && e.target.closest('.fedi-remote-form'); if(!f) return; e.preventDefault(); go(f.querySelector('input').value, f.dataset.uri); });
     119  document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
     120  window.addEventListener('scroll', close, true);
     121})();
     122</script>
Note: See TracChangeset for help on using the changeset viewer.