Changeset 3d37c67 in Klonkt


Ignore:
Timestamp:
06/26/2026 11:38:25 PM (2 months ago)
Author:
Robin Genis <roboburr@…>
Branches:
main
Children:
2f42b60
Parents:
3289a64
Message:

feat(fedi): interact page like/boost are 2 icon+label toggle buttons

Replace the '★ Like this post' / '🔁 Boost this post' buttons with two pill buttons
(gold star + Like, green repeat + Boost) that are real toggles. New ap_my_reactions
table tracks your like/boost state on a REMOTE post; clicking again retracts (Undo
Like / Undo Announce). The like/boost POST now redirects back to the interact page so
the state shows in place (no more separate confirmation screen).

Location:
src
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • src/config/database.js

    r3289a64 r3d37c67  
    354354    );
    355355    CREATE INDEX IF NOT EXISTS idx_ap_outbox_post ON ap_outbox(post_id);
     356    -- Your like/boost state on a REMOTE post (the interact page), so those become toggles.
     357    CREATE TABLE IF NOT EXISTS ap_my_reactions (
     358      site_slug TEXT NOT NULL,
     359      target_uri TEXT NOT NULL,
     360      kind TEXT NOT NULL,             -- 'like' | 'boost'
     361      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     362      UNIQUE(site_slug, target_uri, kind)
     363    );
    356364  `);
    357365  ensureColumn('ap_interactions', 'parent_uri', 'TEXT'); // nesting (existing DBs)
  • src/routes/posts.js

    r3289a64 r3d37c67  
    502502    liked: !!req.query.liked,
    503503    boosted: !!req.query.boosted,
     504    reacted: (site && uri) ? ActivityPubService.getMyReactions(site.slug, uri) : { liked: false, boosted: false },
    504505    siteTitle: site ? site.title : '',
    505506  });
    506507});
    507508
    508 // ⭐ Like a remote post from your own site (the star flow lands here).
     509// ⭐ Like / unlike a remote post from your own site (toggle on the interact page).
    509510router.post('/authorize_interaction/like', requireSiteManager, (req, res) => {
    510511  const site = res.locals.site;
    511512  const uri = (req.body.uri || '').toString();
    512513  if (site && uri) {
     514    const on = !ActivityPubService.getMyReactions(site.slug, uri).liked;
    513515    ActivityPubService.resolveRemoteNote(uri)
    514       .then((note) => note && ActivityPubService.sendInteraction(site, 'like', note.object_uri || uri, note.actor_uri))
     516      .then((note) => note && ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note.object_uri || uri, note.actor_uri))
    515517      .catch((e) => console.warn('[AP] remote like failed:', e.message));
    516   }
    517   res.redirect('/authorize_interaction?liked=1&uri=' + encodeURIComponent(uri));
    518 });
    519 
    520 // 🔁 Boost a remote post from your own site. Also flags it for the Cirkel
    521 // (markBoosted is a no-op if the post isn't in your timeline).
     518    ActivityPubService.setMyReaction(site.slug, uri, 'like', on);
     519  }
     520  res.redirect('/authorize_interaction?uri=' + encodeURIComponent(uri));
     521});
     522
     523// 🔁 Boost / unboost a remote post from your own site (toggle on the interact page).
     524// Also flags it for the Cirkel (markBoosted is a no-op if the post isn't in your timeline).
    522525router.post('/authorize_interaction/boost', requireSiteManager, (req, res) => {
    523526  const site = res.locals.site;
    524527  const uri = (req.body.uri || '').toString();
    525528  if (site && uri) {
     529    const on = !ActivityPubService.getMyReactions(site.slug, uri).boosted;
    526530    ActivityPubService.resolveRemoteNote(uri)
    527531      .then((note) => {
    528532        if (!note) return;
    529533        const id = note.object_uri || uri;
    530         return Promise.resolve(ActivityPubService.sendInteraction(site, 'boost', id, note.actor_uri))
    531           .then(() => ActivityPubService.markBoosted(site.slug, id));
     534        return Promise.resolve(ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', id, note.actor_uri))
     535          .then(() => on ? ActivityPubService.markBoosted(site.slug, id) : ActivityPubService.unmarkBoosted(site.slug, id));
    532536      })
    533537      .catch((e) => console.warn('[AP] remote boost failed:', e.message));
    534   }
    535   res.redirect('/authorize_interaction?boosted=1&uri=' + encodeURIComponent(uri));
     538    ActivityPubService.setMyReaction(site.slug, uri, 'boost', on);
     539  }
     540  res.redirect('/authorize_interaction?uri=' + encodeURIComponent(uri));
    536541});
    537542
  • src/services/ActivityPubService.js

    r3289a64 r3d37c67  
    367367export function setInteractionLiked(id, on) {
    368368  db.prepare('UPDATE ap_interactions SET acted_like = ? WHERE id = ?').run(on ? 1 : 0, id);
     369}
     370// Your like/boost state on a REMOTE post (interact page toggles).
     371export function setMyReaction(slug, uri, kind, on) {
     372  if (on) db.prepare('INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind) VALUES (?,?,?)').run(slug, uri, kind);
     373  else db.prepare('DELETE FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ? AND kind = ?').run(slug, uri, kind);
     374}
     375export function getMyReactions(slug, uri) {
     376  const rows = (slug && uri) ? db.prepare('SELECT kind FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ?').all(slug, uri) : [];
     377  return { liked: rows.some((r) => r.kind === 'like'), boosted: rows.some((r) => r.kind === 'boost') };
    369378}
    370379
     
    13371346  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFeatured,
    13381347  followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
    1339   getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
     1348  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
    13401349  listOutbox, deliverOutboxDelete,
    13411350  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,
  • src/views/pages/authorize-interaction.ejs

    r3289a64 r3d37c67  
    9595      <form method="post" action="/authorize_interaction/like">
    9696        <input type="hidden" name="uri" value="<%= uri %>">
    97         <button type="submit" class="btn btn-primary auth-like-btn">★ <%= t('fedi.like_btn') %></button>
     97        <button type="submit" class="fedi-bigact fedi-bigact-like<%= (typeof reacted !== 'undefined' && reacted.liked) ? ' is-on' : '' %>">
     98          <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>
     99          <span><%= (typeof reacted !== 'undefined' && reacted.liked) ? t('fedi.unlike_short') : t('fedi.like_short') %></span>
     100        </button>
    98101      </form>
    99102      <form method="post" action="/authorize_interaction/boost">
    100103        <input type="hidden" name="uri" value="<%= uri %>">
    101         <button type="submit" class="btn auth-boost-btn">🔁 <%= t('fedi.boost_btn') %></button>
     104        <button type="submit" class="fedi-bigact fedi-bigact-boost<%= (typeof reacted !== 'undefined' && reacted.boosted) ? ' is-on' : '' %>">
     105          <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>
     106          <span><%= (typeof reacted !== 'undefined' && reacted.boosted) ? t('tl.unboost') : t('fedi.boost_short') %></span>
     107        </button>
    102108      </form>
    103109    </div>
     
    146152    text-decoration: none; font-weight: 600; font-size: .92rem; transition: background .15s, border-color .15s; }
    147153  .auth-view-src:hover { background: color-mix(in srgb, var(--accent, #888) 18%, transparent); border-color: var(--accent, #888); }
    148   .auth-interact-react { display: flex; gap: .5rem; flex-wrap: wrap; margin: 0 0 .25rem; }
     154  .auth-interact-react { display: flex; gap: .6rem; flex-wrap: wrap; margin: 0 0 .25rem; }
    149155  .auth-interact-react form { margin: 0; }
    150   .auth-boost-btn { white-space: nowrap; }
     156  .fedi-bigact { display: inline-flex; align-items: center; gap: .5rem; padding: .55rem 1.1rem; border-radius: 999px;
     157    border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent);
     158    background: color-mix(in srgb, var(--ink, #000) 4%, transparent);
     159    color: var(--ink, inherit); font: inherit; font-weight: 600; font-size: .92rem; cursor: pointer; white-space: nowrap;
     160    transition: background .12s, border-color .12s; }
     161  .fedi-bigact svg { width: 17px; height: 17px; flex: 0 0 17px; }
     162  .fedi-bigact-like svg { color: #e8b04b; }
     163  .fedi-bigact-like:hover, .fedi-bigact-like.is-on { background: color-mix(in srgb, #e8b04b 16%, transparent); border-color: color-mix(in srgb, #e8b04b 55%, transparent); }
     164  .fedi-bigact-boost svg { color: #2fa85a; }
     165  .fedi-bigact-boost:hover, .fedi-bigact-boost.is-on { background: color-mix(in srgb, #2fa85a 16%, transparent); border-color: color-mix(in srgb, #2fa85a 55%, transparent); }
    151166  .fedi-manage { list-style: none; padding: 0; margin: 1.25rem 0 1rem; display: flex; flex-direction: column; gap: .7rem; }
    152167  .fedi-manage-item { padding: .85rem 1rem; border-radius: 14px; background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent); }
Note: See TracChangeset for help on using the changeset viewer.