Ignore:
Timestamp:
08/10/2026 04:53:05 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
e215090
Parents:
41de7cb
Message:

Ongelezen per gesprek: een COUNT, en Read als gebeurtenis

shaer-frontend-3tx. De kopjeslezing draagt nu shaer:unread per gesprek, plus
shaer:unreadWave als er een zwaai bij zit -- een zetje van een guardian is geen
gesprek en hoort niet opgeteld te worden.

GEEN BIJGEHOUDEN GETAL (Barts besluit): het aantal is een COUNT over de
berichten na de leesmarkering. Niets om op te hogen bij bezorging, niets om te
verlagen bij lezen, en bij een verwijdering klopt het vanzelf weer -- een
teller zou blijven staan en dan zegt de badge 3 terwijl er niets is. Er staat
een toets op precies dat geval.

READ IS EEN GEBEURTENIS, geen zetbare stand (Barts vraag: een stille client
stuurt toch geen Undo{Read}?). AS2 kent Read; markRead neemt het maximum, dus
een toestel dat een week uit stond kan gelezen berichten niet terugzetten op
ongelezen. Ook daar staat een toets op. Geen shaer:seen verzonnen.

De markering is de samengestelde cursor (stempel|ref), zelfde vorm als de
gesprekspaginering: twee berichten in dezelfde seconde is bij DM's een gesprek
en geen randgeval.

En conversationItems geeft nu PAREN terug in plaats van een losse lijst. Een
kop levert niet altijd een item op (dichte poort, ontbrekende rij), en op index
koppelen zou de telling stil aan het verkeerde gesprek hangen.

862/862 groen.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r41de7cb r21ef238  
    32383238        return await c2sCreatePost(base, site, user, object);
    32393239      }
     3240      // ── Gelezen tot hier (shaer-frontend-3tx) ───────────────────
     3241      //
     3242      // AS2 kent Read: 'the actor has read the object'. Geen shaer:seen
     3243      // verzinnen, en geen zetbare stand: dit is een GEBEURTENIS, dus twee
     3244      // toestellen kunnen elkaar niet terugzetten. Blijft lokaal -- een
     3245      // leesbevestiging heeft in de fediverse niets te zoeken.
     3246      case 'Read': {
     3247        const targetUri = c2sIdOf(object);
     3248        if (!targetUri) return { status: 400, error: 'missing_object' };
     3249        const uit = markRead(site.slug, targetUri);
     3250        // Kennen we die note niet, dan is er niets gelezen om te onthouden.
     3251        // Geen fout: een client mag best een oud bericht aanwijzen.
     3252        return { status: uit ? 200 : 202 };
     3253      }
    32403254      case 'Like':
    32413255      case 'Announce': {
     
    43484362}
    43494363
     4364/**
     4365 * Tot waar deze lezer elk gesprek gelezen heeft (shaer-frontend-3tx).
     4366 *
     4367 * De markering komt uit AS2 `Read`-activiteiten, en die zijn OPTELLEND: het
     4368 * lezen van bericht N maakt niets anders ongelezen. Daarom is achteruit gaan
     4369 * geen regel die iemand moet onthouden maar een eigenschap van het model --
     4370 * markRead neemt het maximum. Een 'zet mijn markering op X' zou een toestel
     4371 * dat een week uit stond je gelezen berichten weer op ongelezen laten zetten.
     4372 */
     4373export function readMarkers(slug) {
     4374  try {
     4375    return new Map(db.prepare('SELECT other, cursor FROM ap_read_markers WHERE slug = ?')
     4376      .all(slug).map((r) => [r.other, r.cursor]));
     4377  } catch { return new Map(); }
     4378}
     4379
     4380/**
     4381 * Markeer een gesprek als gelezen tot en met dit bericht.
     4382 *
     4383 * Het object van de Read is een berichturi; welk gesprek dat is en waar het in
     4384 * de tijd staat weet de server zelf, dus de client hoeft niets uit te rekenen
     4385 * en kan er ook niet naast zitten.
     4386 */
     4387export function markRead(slug, objectUri) {
     4388  try {
     4389    const rij = db.prepare(`SELECT other, stamp, ref FROM (${CONVERSATION_UNION})
     4390                             WHERE ref = @ref ORDER BY stamp DESC LIMIT 1`)
     4391      .get({ slug, ref: String(objectUri || '') });
     4392    if (!rij) return null;
     4393    const cursor = `${rij.stamp}|${rij.ref}`;
     4394    db.prepare(`INSERT INTO ap_read_markers (slug, other, cursor) VALUES (?,?,?)
     4395                ON CONFLICT(slug, other) DO UPDATE SET cursor = MAX(cursor, excluded.cursor), at = CURRENT_TIMESTAMP`)
     4396      .run(slug, rij.other, cursor);
     4397    return { other: rij.other, cursor };
     4398  } catch { return null; }
     4399}
     4400
     4401/**
     4402 * Hoeveel er per gesprek nog ongelezen is, en of daar een zwaai bij zit.
     4403 *
     4404 * Een COUNT en geen bijgehouden getal (Barts besluit): niets om op te hogen
     4405 * bij bezorging, niets om te verlagen bij lezen, en bij een verwijdering klopt
     4406 * het vanzelf weer.
     4407 *
     4408 * Een zwaai telt apart, want dat is geen gesprek maar een zetje van een
     4409 * guardian -- die hoort een eigen teken te krijgen en niet opgeteld te worden.
     4410 * Eigen berichten tellen nooit mee: je hebt jezelf gelezen.
     4411 */
     4412export function unreadPerConversation(slug) {
     4413  try {
     4414    const rijen = db.prepare(`
     4415      SELECT u.other AS other,
     4416             COUNT(*) AS n,
     4417             MAX(CASE WHEN m.wave = 1 THEN 1 ELSE 0 END) AS wave
     4418        FROM (${CONVERSATION_UNION}) u
     4419        LEFT JOIN ap_read_markers r ON r.slug = @slug AND r.other = u.other
     4420        LEFT JOIN ap_mentions m ON m.slug = @slug AND m.object_uri = u.ref
     4421       WHERE u.direction = 'in'
     4422         AND (r.cursor IS NULL OR (u.stamp || '|' || u.ref) > r.cursor)
     4423       GROUP BY u.other`).all({ slug });
     4424    return new Map(rijen.map((r) => [r.other, { n: r.n, wave: !!r.wave }]));
     4425  } catch { return new Map(); }
     4426}
     4427
    43504428export function getDirectMessages(slug, limit) {
    43514429  try {
     
    64416519  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    64426520  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    6443   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineRowsByIds, getDirectMessages, messageRowsByUri, replyRowsByUri, conversationHeads, conversationHistory, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
     6521  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineRowsByIds, getDirectMessages, readMarkers, markRead, unreadPerConversation, messageRowsByUri, replyRowsByUri, conversationHeads, conversationHistory, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
    64446522  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    64456523  gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note: See TracChangeset for help on using the changeset viewer.