Changeset c010b42 in Klonkt for src


Ignore:
Timestamp:
08/06/2026 08:46:01 AM (5 weeks ago)
Author:
Claude (agent) <aiclaude@…>
Branches:
main
Children:
0e27e54
Parents:
02fb7ce
git-author:
Robin <roboburr@…> (08/06/2026 08:46:00 AM)
git-committer:
Claude (agent) <aiclaude@…> (08/06/2026 08:46:01 AM)
Message:

Eén schrijfpad voor reacties: setReaction (shaer-9e9 fase 1, stap 2)

Alle plekken die "ik heb hierop gereageerd" opschreven gaan nu door
setReaction(slug, uri, kind, on, opts): de tussentabel ap_my_reactions is de
waarheid, de vlaggen op ap_timeline zijn de afgeleide. Dat zijn de twee takken
van de C2S-outbox (Like/Announce en Undo) en de vier webhandlers (interact-like,
interact-boost, tijdlijn-like, tijdlijn-boost).

Twee dingen zitten er bewust in.

opts.note, omdat een boost geen vlag zet maar de post je tijdlijn IN trekt als je
de auteur niet volgt -- anders heeft de vlag geen rij om op te landen en
verschijnt de boost nergens, ook niet in de Cirkel. Dat is de valkuil die in het
plan met naam staat.

opts.flagUri, omdat de twee bronnen vandaag verschillend gesleuteld worden: de
tussentabel op de URI die de client stuurde, de vlag op de opgeloste object-URI.
Meestal gelijk, niet gegarandeerd. Die naad houdt deze stap gedragsbehoudend;
het samentrekken van de sleutels is fase 2, met datamigratie.

CORRECTIE OP HET PLAN: ik schreef dat deze stap "per definitie" geen
gedragswijziging geeft omdat lezen niet verandert. Dat klopt niet. Nu de
interact-route ook de kolom vult, verschijnt een like die eerder alleen in de
tussentabel stond voortaan ook in de Krant. Dat is de bedoelde convergentie,
maar het is wel zichtbaar -- en het hoort in de notitie te staan in plaats van
weggeredeneerd.

CORRECTIE OP DE TESTS: de karakteriseringstests zouden volgens hun eigen kop
gaan falen na deze stap. Dat gebeurde niet, en terecht: ze roepen de primitieven
aan en die zijn niet veranderd. De kop is bijgewerkt en er zijn vier tests bij
die wel bewaken wat deze stap oplevert -- dat setReaction beide bronnen vult,
dat een boost met note de rij aanmaakt en in de Cirkel belandt, dat de
gescheiden sleutels blijven werken, en dat rommelige invoer niets doet in plaats
van iets halfs.

Suite 426/426.

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

Location:
src
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • src/routes/posts.js

    r02fb7ce rc010b42  
    843843      .then((note) => note && ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note.object_uri || uri, note.actor_uri))
    844844      .catch((e) => console.warn('[AP] remote like failed:', e.message));
    845     ActivityPubService.setMyReaction(site.slug, uri, 'like', on);
     845    // Eén schrijfpad (shaer-9e9): tussentabel + afgeleide vlag.
     846    ActivityPubService.setReaction(site.slug, uri, 'like', on);
    846847  }
    847848  if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
     
    862863        const id = note.object_uri || uri;
    863864        return Promise.resolve(ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', id, note.actor_uri))
    864           // Boost → store the post in the timeline (even if you don't follow the author) so it
    865           // surfaces in the Cirkel; unboost → just clear the flag.
    866           .then(() => on ? ActivityPubService.upsertBoostedNote(site.slug, note) : ActivityPubService.unmarkBoosted(site.slug, id));
     865          // De note gaat mee: een boost zet niet alleen een vlag maar trekt de
     866          // post je tijdlijn in, ook als je de auteur niet volgt, zodat hij in
     867          // de Cirkel verschijnt.
     868          .then(() => ActivityPubService.setReaction(site.slug, uri, 'boost', on, { flagUri: id, note: on ? note : null }));
    867869      })
    868870      .catch((e) => console.warn('[AP] remote boost failed:', e.message));
     871    // De tussentabel meteen, zodat de knop klopt voordat de resolve terug is.
    869872    ActivityPubService.setMyReaction(site.slug, uri, 'boost', on);
    870873  }
     
    12791282    on = !ActivityPubService.getTimelineReaction(site.slug, note).liked;
    12801283    try { await ActivityPubService.sendInteraction(site, on ? 'like' : 'unlike', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
    1281     if (on) ActivityPubService.markLiked(site.slug, note); else ActivityPubService.unmarkLiked(site.slug, note);
     1284    ActivityPubService.setReaction(site.slug, note, 'like', on);
    12821285  }
    12831286  if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
     
    12931296    on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted;
    12941297    try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
     1298    ActivityPubService.setReaction(site.slug, note, 'boost', on); // instant UI state
    12951299    if (on) {
    1296       ActivityPubService.markBoosted(site.slug, note); // instant UI state
    12971300      // Fire-and-forget: re-resolve the note so the cached row is refreshed
    12981301      // (cover/content) — boosting again heals a stale copy from EVERY boost
    12991302      // path, not just the interact page.
    13001303      ActivityPubService.resolveRemoteNote(note)
    1301         .then((n) => { if (n) ActivityPubService.upsertBoostedNote(site.slug, n); })
     1304        .then((n) => { if (n) ActivityPubService.setReaction(site.slug, note, 'boost', true, { note: n }); })
    13021305        .catch(() => { /* best-effort */ });
    1303     } else {
    1304       ActivityPubService.unmarkBoosted(site.slug, note);
    13051306    }
    13061307  }
  • src/services/ActivityPubService.js

    r02fb7ce rc010b42  
    26232623        const kind = type === 'Announce' ? 'boost' : 'like';
    26242624        await sendInteraction(site, kind, objUri, authorUri);
    2625         setMyReaction(site.slug, targetUri, kind, true);
    2626         if (type === 'Announce' && note) { try { upsertBoostedNote(site.slug, note); } catch { /* non-fatal */ } }
     2625        // Eén schrijfpad (shaer-9e9): tussentabel + afgeleide vlag in één keer.
     2626        // De note gaat mee zodat een boost de post je tijdlijn in trekt.
     2627        try { setReaction(site.slug, targetUri, kind, true, { flagUri: objUri, note: type === 'Announce' ? note : null }); }
     2628        catch { /* non-fatal: een reactie mag nooit de bezorging blokkeren */ }
    26272629        // Een Like uit een app moet ook in ap_timeline.liked landen, want dat
    26282630        // is wat de C2S-tijdlijn als shaer:liked teruggeeft. Zonder dit werd
     
    26342636        // in je tijdlijn te trekken, dus staat de post er niet in, dan is dit
    26352637        // terecht een no-op.
    2636         if (type === 'Like') { try { markLiked(site.slug, objUri); } catch { /* non-fatal */ } }
    26372638        return { status: 202, url: objUri };
    26382639      }
     
    26832684          const objUri = (note && note.object_uri) || innerTarget;
    26842685          await sendInteraction(site, kind, objUri, note && note.actor_uri);
    2685           setMyReaction(site.slug, innerTarget, innerType === 'Announce' ? 'boost' : 'like', false);
    2686           if (innerType === 'Announce') { try { unmarkBoosted(site.slug, objUri); } catch { /* non-fatal */ } }
    2687           if (innerType === 'Like') { try { unmarkLiked(site.slug, objUri); } catch { /* non-fatal */ } }
     2686          try { setReaction(site.slug, innerTarget, innerType === 'Announce' ? 'boost' : 'like', false, { flagUri: objUri }); }
     2687          catch { /* non-fatal */ }
    26882688          return { status: 202, url: objUri };
    26892689        }
     
    34113411  try { if (!_unmarkLike) _unmarkLike = db.prepare('UPDATE ap_timeline SET liked = 0 WHERE slug = ? AND id = ?'); _unmarkLike.run(slug, noteId); } catch { /* ignore */ }
    34123412}
     3413/**
     3414 * Zet een reactie van JOU op een object. Dit hoort het enige schrijfpad te zijn
     3415 * (shaer-9e9): de tussentabel ap_my_reactions is de waarheid, de vlaggen op
     3416 * ap_timeline zijn de afgeleide. Zolang markLiked en broers los aanroepbaar
     3417 * blijven kan een aanroeper ze vergeten, en dat is niet hypothetisch -- precies
     3418 * dat leverde de shaer:liked-bug op (04aca12).
     3419 *
     3420 * `opts.note` is de opgeloste remote note bij een boost. Die is niet optioneel
     3421 * uit netheid: een boost moet de post je tijdlijn IN trekken als je de auteur
     3422 * niet volgt, anders heeft de vlag geen rij om op te landen en verschijnt de
     3423 * boost nergens -- ook niet in de Cirkel.
     3424 *
     3425 * `opts.flagUri` bestaat omdat de twee bronnen vandaag verschillend gesleuteld
     3426 * worden: de tussentabel op de URI die de client stuurde, de vlag op de
     3427 * opgeloste object-URI. Meestal zijn die gelijk, maar niet gegarandeerd. Deze
     3428 * naad houdt fase 1 gedragsbehoudend; het samentrekken van die twee sleutels is
     3429 * werk voor fase 2, mét datamigratie.
     3430 */
     3431export function setReaction(slug, uri, kind, on, opts = {}) {
     3432  if (!slug || !uri || (kind !== 'like' && kind !== 'boost')) return;
     3433  setMyReaction(slug, uri, kind, !!on);
     3434  const flagUri = opts.flagUri || uri;
     3435  if (kind === 'boost') {
     3436    if (!on) unmarkBoosted(slug, flagUri);
     3437    else if (opts.note) upsertBoostedNote(slug, opts.note);
     3438    else markBoosted(slug, flagUri);
     3439  } else if (on) markLiked(slug, flagUri);
     3440  else unmarkLiked(slug, flagUri);
     3441}
     3442
    34133443export function getTimelineReaction(slug, noteId) {
    34143444  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 }; }
     
    47794809  gateOutgoingFollow, performApprovedFollow,
    47804810  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
    4781   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
     4811  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, setReaction, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
    47824812  getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
    47834813  deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
Note: See TracChangeset for help on using the changeset viewer.