Changeset 21ef238 in Klonkt
- Timestamp:
- 08/10/2026 04:53:05 PM (4 weeks ago)
- Branches:
- main
- Children:
- e215090
- Parents:
- 41de7cb
- Files:
-
- 1 added
- 3 edited
-
src/config/database.js (modified) (1 diff)
-
src/routes/activitypub.js (modified) (4 diffs)
-
src/services/ActivityPubService.js (modified) (3 diffs)
-
test/unread.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
src/config/database.js
r41de7cb r21ef238 899 899 try { 900 900 db.exec(` 901 -- Tot waar jij een gesprek gelezen hebt (shaer-frontend-3tx). 902 -- 903 -- Een MARKERING, geen teller: het aantal ongelezen berichten is een 904 -- COUNT over de berichten die na deze cursor komen. Een opgeslagen 905 -- getal zou opgehoogd, verlaagd en gerepareerd moeten worden, en zou 906 -- blijven staan als er iets verwijderd wordt -- badge zegt 3, er is 907 -- niets. 908 -- 909 -- De cursor is samengesteld ('<stempel>|<ref>'), dezelfde vorm als de 910 -- gesprekspaginering en om dezelfde reden: twee berichten in dezelfde 911 -- seconde is bij DM's een gesprek, geen randgeval. 912 CREATE TABLE IF NOT EXISTS ap_read_markers ( 913 slug TEXT NOT NULL, 914 other TEXT NOT NULL, -- de tegenpartij (actor uri) 915 cursor TEXT NOT NULL, 916 at DATETIME DEFAULT CURRENT_TIMESTAMP, 917 PRIMARY KEY (slug, other) 918 ); 901 919 CREATE TABLE IF NOT EXISTS ap_feed_state ( 902 920 slug TEXT NOT NULL, -
src/routes/activitypub.js
r41de7cb r21ef238 492 492 const incoming = new Map(AP.messageRowsByUri(auth.site.slug, refs.filter((r) => r.direction === 'in').map((r) => r.ref)) 493 493 .map((m) => [m.object_uri, m])); 494 const items = []; 494 // PAREN, geen losse lijst: een kop levert niet altijd een item op (dichte 495 // poort, ontbrekende rij), en dan zou de aanroeper op index koppelen en de 496 // telling aan het verkeerde gesprek hangen. Stil, en pas te zien als iemand 497 // een badge op de verkeerde naam ziet staan. 498 const pairs = []; 495 499 for (const r of refs) { 496 500 if (r.direction === 'in') { … … 498 502 if (!m) continue; 499 503 if (!(P.messagesAllowed || m.help_request || guardianUris.has(m.actor_uri))) continue; 500 items.push(messageItem(m, ctx));504 pairs.push({ head: r, item: messageItem(m, ctx) }); 501 505 } else { 502 506 const n = AP.getOutboxNote(base, r.ref); 503 507 // Je eigen woorden blijven van jou: een dichte messages-poort verbergt 504 508 // niet wat je zelf gezegd hebt. 505 if (n) items.push(sentItem(n, { me, mine }));509 if (n) pairs.push({ head: r, item: sentItem(n, { me, mine }) }); 506 510 } 507 511 } 508 return items;512 return pairs; 509 513 } 510 514 … … 513 517 if (!auth || auth.site.slug !== req.params.slug) return res.status(403).end(); 514 518 const heads = AP.conversationHeads(auth.site.slug); 515 const items = conversationItems(req, auth, heads); 519 const pairs = conversationItems(req, auth, heads); 520 const items = pairs.map((x) => x.item); 521 // Ongelezen per gesprek (shaer-frontend-3tx): een COUNT, geen bijgehouden 522 // getal. Hij hangt aan het NIEUWSTE kopje van elke persoon -- er kunnen er 523 // twee zijn (zie conversationHeads) en het aantal hoort bij het gesprek, niet 524 // bij een bericht. 525 // 526 // AS2 heeft geen term voor ongelezen; dit is per-lezer-interactiestatus, 527 // dezelfde categorie als shaer:liked. Niet in totalItems persen: dat betekent 528 // 'hoeveel er zijn' en niet 'hoeveel jij nog niet zag'. 529 const ongelezen = AP.unreadPerConversation(auth.site.slug); 530 const gezien = new Set(); 531 for (const { head, item } of pairs) { 532 if (gezien.has(head.other)) continue; 533 gezien.add(head.other); 534 const u = ongelezen.get(head.other); 535 if (!u) continue; 536 item.object['shaer:unread'] = u.n; 537 // Een zwaai is geen aantal maar een zetje van een guardian: eigen teken. 538 if (u.wave) item.object['shaer:unreadWave'] = true; 539 } 516 540 AP.sendAP(res, { 517 541 '@context': AP.AP_CONTEXT, … … 533 557 limit: req.query.limit, 534 558 }); 535 const items = conversationItems(req, auth, page.rows) ;559 const items = conversationItems(req, auth, page.rows).map((x) => x.item); 536 560 // De paginagrootte reist mee in next: vroeg je om 30, dan hoort de volgende 537 561 // pagina er ook 30 te zijn. Zonder dit wordt hij stilletjes de standaard, en -
src/services/ActivityPubService.js
r41de7cb r21ef238 3238 3238 return await c2sCreatePost(base, site, user, object); 3239 3239 } 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 } 3240 3254 case 'Like': 3241 3255 case 'Announce': { … … 4348 4362 } 4349 4363 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 */ 4373 export 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 */ 4387 export 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 */ 4412 export 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 4350 4428 export function getDirectMessages(slug, limit) { 4351 4429 try { … … 6441 6519 getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote, 6442 6520 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, 6444 6522 acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision, 6445 6523 gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)