Changeset 0a75356 in Klonkt


Ignore:
Timestamp:
06/26/2026 11:55:09 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
7e66e7e
Parents:
9d34855
Message:

feat(news): feed like+boost toggle in place (fetch, no reload, no banner)

/news/like and /news/boost are now single toggling routes that return JSON {on} for a
fetch request; news.ejs posts via fetch and flips the button is-on without a reload or a
success banner. No-JS falls back to a plain redirect. getTimelineReaction reads state.

Location:
src
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    r9d34855 r0a75356  
    693693});
    694694
     695// Like / unlike a feed post — a toggle. Fetch request → JSON {on} (stay on the page,
     696// no banner); no-JS → redirect back.
    695697router.post('/news/like', requireSiteManager, async (req, res) => {
    696698  const site = res.locals.site;
    697699  const note = (req.body.note || '').toString();
     700  let on = false;
    698701  if (site && note) {
    699     try { await ActivityPubService.sendInteraction(site, 'like', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    700     ActivityPubService.markLiked(site.slug, note);
    701   }
    702   res.redirect('/news?success=' + encodeURIComponent('Geliket ⭐'));
    703 });
    704 
    705 router.post('/news/unlike', requireSiteManager, async (req, res) => {
     702    on = !ActivityPubService.getTimelineReaction(site.slug, note).liked;
     703    try { await ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
     704    if (on) ActivityPubService.markLiked(site.slug, note); else ActivityPubService.unmarkLiked(site.slug, note);
     705  }
     706  if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
     707  res.redirect('/news');
     708});
     709
     710// Boost / unboost a feed post — a toggle. markBoosted also surfaces it in the Cirkel.
     711router.post('/news/boost', requireSiteManager, async (req, res) => {
    706712  const site = res.locals.site;
    707713  const note = (req.body.note || '').toString();
     714  let on = false;
    708715  if (site && note) {
    709     try { await ActivityPubService.sendInteraction(site, 'unlike', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    710     ActivityPubService.unmarkLiked(site.slug, note);
    711   }
    712   res.redirect('/news?success=' + encodeURIComponent('Like ingetrokken'));
    713 });
    714 
    715 router.post('/news/boost', requireSiteManager, async (req, res) => {
    716   const site = res.locals.site;
    717   const note = (req.body.note || '').toString();
    718   if (site && note) {
    719     try { await ActivityPubService.sendInteraction(site, 'boost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    720     ActivityPubService.markBoosted(site.slug, note); // also surface it in the Cirkel (mixed by date)
    721   }
    722   res.redirect('/news?success=' + encodeURIComponent('Geboost 🔁'));
    723 });
    724 
    725 // Unboost (retract): send Undo(Announce) + clear the local flag.
    726 router.post('/news/unboost', requireSiteManager, async (req, res) => {
    727   const site = res.locals.site;
    728   const note = (req.body.note || '').toString();
    729   if (site && note) {
    730     try { await ActivityPubService.sendInteraction(site, 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    731     ActivityPubService.unmarkBoosted(site.slug, note);
    732   }
    733   res.redirect('/news?success=' + encodeURIComponent('Boost ingetrokken'));
     716    on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted;
     717    try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
     718    if (on) ActivityPubService.markBoosted(site.slug, note); else ActivityPubService.unmarkBoosted(site.slug, note);
     719  }
     720  if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
     721  res.redirect('/news');
    734722});
    735723
  • src/services/ActivityPubService.js

    r9d34855 r0a75356  
    11061106  try { if (!_unmarkLike) _unmarkLike = db.prepare('UPDATE ap_timeline SET liked = 0 WHERE slug = ? AND id = ?'); _unmarkLike.run(slug, noteId); } catch { /* ignore */ }
    11071107}
     1108export function getTimelineReaction(slug, noteId) {
     1109  try { const r = db.prepare('SELECT liked, boosted FROM ap_timeline WHERE slug = ? AND id = ?').get(slug, noteId); return { liked: !!(r && r.liked), boosted: !!(r && r.boosted) }; } catch { return { liked: false, boosted: false }; }
     1110}
    11081111export function boostedCount(slug) {
    11091112  try { if (!_boostedCount) _boostedCount = db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE slug = ? AND boosted = 1'); return _boostedCount.get(slug).n; } catch { return 0; }
     
    13561359  listOutbox, deliverOutboxDelete,
    13571360  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,
    1358   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getCirkelPosts, getCirkelMembers, selfHealTimeline,
     1361  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, getCirkelPosts, getCirkelMembers, selfHealTimeline,
    13591362  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    13601363  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
  • src/views/pages/news.ejs

    r9d34855 r0a75356  
    4747
    4848          <div class="tl-actions">
    49             <% if (p.liked) { %>
    50             <form method="post" action="/news/unlike" 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 tl-act-like is-on" title="<%= t('fedi.unlike_short') %>" aria-label="<%= t('fedi.unlike_short') %>"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg></button></form>
    51             <% } else { %>
    52             <form method="post" action="/news/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 tl-act-like" title="<%= t('fedi.likes') %>" aria-label="<%= t('fedi.likes') %>"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg></button></form>
    53             <% } %>
    54             <% if (p.boosted) { %>
    55             <form method="post" action="/news/unboost" 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 tl-act-boost is-on" title="<%= t('tl.unboost') %>" aria-label="<%= t('tl.unboost') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></button></form>
    56             <% } else { %>
    57             <form method="post" action="/news/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 tl-act-boost" title="<%= t('fedi.boosts') %>" aria-label="<%= t('fedi.boosts') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></button></form>
    58             <% } %>
     49            <form method="post" action="/news/like" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-like<%= p.liked ? ' is-on' : '' %>" title="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>" aria-label="<%= p.liked ? t('fedi.unlike_short') : t('fedi.likes') %>"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg></button></form>
     50            <form method="post" action="/news/boost" class="tl-act-form tl-react-form"><input type="hidden" name="note" value="<%= p.id %>"><input type="hidden" name="author" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-boost<%= p.boosted ? ' is-on' : '' %>" title="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>" aria-label="<%= p.boosted ? t('tl.unboost') : t('fedi.boosts') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg></button></form>
    5951            <button type="button" class="tl-act tl-act-reply fedi-remote-reply-btn" data-fedi-uri="<%= p.id %>" data-fedi-ph="<%= t('fedi.remote_ph') %>" title="<%= t('fedi.remote_reply') %>" aria-label="<%= t('fedi.remote_reply') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg></button>
    6052            <form method="post" action="/blocking/add" class="tl-act-form" onsubmit="return confirm('<%= t('tl.block') %>?');"><input type="hidden" name="target" value="<%= p.author_uri %>"><button type="submit" class="tl-act tl-act-block" title="<%= t('tl.block') %>" aria-label="<%= t('tl.block') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="4.9" y1="4.9" x2="19.1" y2="19.1"/></svg></button></form>
     
    168160  window.addEventListener('scroll', close, true);
    169161})();
     162
     163/* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page (no reload, no banner). */
     164(function(){
     165  if (window.__newsReactWired) return; window.__newsReactWired = true;
     166  document.addEventListener('submit', function(e){
     167    var f = e.target.closest && e.target.closest('.tl-react-form');
     168    if (!f) return;
     169    e.preventDefault();
     170    var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
     171    btn.disabled = true;
     172    var body = new URLSearchParams();
     173    new FormData(f).forEach(function(v, k){ body.append(k, v); });
     174    fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
     175      .then(function(r){ return r.ok ? r.json() : null; })
     176      .then(function(j){ if (j) btn.classList.toggle('is-on', !!j.on); })
     177      .catch(function(){})
     178      .then(function(){ btn.disabled = false; });
     179  });
     180})();
    170181</script>
Note: See TracChangeset for help on using the changeset viewer.