Ignore:
Timestamp:
08/10/2026 02:03:09 PM (4 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
f549cf5
Parents:
86ec01b
Message:

Gesprekken: eerst wie, dan pas wat

shaer-frontend-yso, stap 1 van 3: de serverkant NAAST het bestaande pad, want
de apps in het veld lezen de oude inbox-lezing nog.

De oude lezing geeft de nieuwste 60 berichten over ALLE gesprekken samen. Bij
DM's is dat veel erger dan bij posts -- meer en kortere berichten, dus een druk
gesprek eet de 60 in zijn eentje op en duwt de rest eruit. Viel het laatste
bericht van iemand erbuiten, dan verdween die persoon HELEMAAL uit Messages:
de avatarhemel plaatst mensen op de leeftijd van hun laatste bericht, dus geen
bericht is geen gezicht. test/conversations.test.js legt die bug eerst vast.

Nu twee lezingen:

GET /ap/users/:slug/conversations een rij per tegenpartij, compleet van

vorm -- het aantal rijen is het aantal
mensen, niet het aantal berichten

GET /ap/users/:slug/messages?with= het gesprek zelf, beide kanten onder EEN

limiet, met next zolang er meer is

Beide kanten onder een limiet, want in de oude lezing werden jouw kant
(getSentNotes) en hun kant apart afgekapt en kon een gesprek eenzijdig lijken.

DE CURSOR IS SAMENGESTELD (stempel|ref) en niet alleen de stempel. Twee
berichten in dezelfde seconde is bij DM's een gesprek en geen randgeval; met
'stamp < before' viel alles wat die grensseconde deelde stil weg. De testen
vonden dat, en tellen nu dat de drie paginas samen precies 80 berichten zijn.
De paginagrootte reist mee in next, anders wordt pagina twee stilletjes de
standaard.

Om te voorkomen dat de kaartvorm twee keer beschreven staat: de poorten
(leesPoorten) en de vorm (berichtItem/verzondenItem) zijn uit de inbox-lezing
gehesen en worden nu door allebei gebruikt. Zelfde gedrag, 843/843 groen.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r86ec01b r09fc5fb  
    41914191}
    41924192
     4193// ── Gesprekken: eerst wie, dan pas wat (shaer-frontend-yso) ──────────
     4194//
     4195// De oude lezing gaf de nieuwste 60 berichten over ALLE gesprekken samen. Dat
     4196// knipt geschiedenis weg zonder dat iemand het merkt, en het is bij DM's veel
     4197// erger dan bij posts: dat zijn er meer en het zijn kortere berichten, dus een
     4198// druk gesprek kan de 60 in zijn eentje opeten en de rest uit de lezing duwen.
     4199// Viel het laatste bericht van iemand erbuiten, dan verdween die persoon
     4200// helemaal uit Messages -- de avatarhemel plaatst mensen op de leeftijd van hun
     4201// laatste bericht, dus geen bericht is geen gezicht.
     4202//
     4203// Vandaar twee lezingen. Deze geeft EEN rij per tegenpartij, hoe druk iemand
     4204// ook is, en conversationHistory hieronder geeft het gesprek zelf met een
     4205// cursor. Wat de client van de hemel nodig heeft -- wie, wanneer, en waarmee --
     4206// zit in die ene nieuwste note.
     4207//
     4208// Een gesprek is hier hetzelfde als in de app: incoming zijn de ap_mentions
     4209// (die tabel IS de aan ons gerichte post), uitgaand zijn de eigen notes met
     4210// visibility 'direct'. Een publiek antwoord is geen gesprek en hoort niet als
     4211// gezicht in de hemel.
     4212const GESPREK_UNIE = `
     4213  SELECT m.actor_uri AS other, COALESCE(m.published, m.created_at) AS stamp,
     4214         'in' AS richting, m.object_uri AS ref
     4215    FROM ap_mentions m
     4216   WHERE m.slug = @slug AND m.actor_uri IS NOT NULL AND m.actor_uri <> ''
     4217  UNION ALL
     4218  SELECT j.value AS other, o.created_at AS stamp,
     4219         'uit' AS richting, o.id AS ref
     4220    FROM ap_outbox o
     4221    JOIN json_each(COALESCE(NULLIF(o.to_actors, ''), json_array(o.to_actor))) j
     4222   WHERE o.site_slug = @slug AND o.visibility = 'direct'
     4223     AND j.value IS NOT NULL AND j.value <> ''`;
     4224
     4225/**
     4226 * Een rij per tegenpartij: zijn nieuwste bericht, nieuwste gesprek eerst.
     4227 *
     4228 * Compleet van vorm -- het aantal rijen is het aantal mensen, niet het aantal
     4229 * berichten -- dus de hemel kan niemand meer kwijtraken doordat een ander druk
     4230 * was. Zonder limiet, en dat mag: dit schaalt met je kring.
     4231 */
     4232export function conversationHeads(slug) {
     4233  try {
     4234    return db.prepare(`
     4235      SELECT other, stamp, richting, ref FROM (
     4236        SELECT *, ROW_NUMBER() OVER (PARTITION BY other ORDER BY stamp DESC, ref DESC) AS rn
     4237          FROM (${GESPREK_UNIE})
     4238      ) WHERE rn = 1
     4239      ORDER BY stamp DESC, ref DESC`).all({ slug });
     4240  } catch { return []; }
     4241}
     4242
     4243/**
     4244 * Een gesprek, nieuwste eerst, met een cursor.
     4245 *
     4246 * BEIDE KANTEN ONDER EEN LIMIET. In de oude lezing werden jouw kant
     4247 * (getSentNotes) en hun kant apart afgekapt, waardoor een gesprek eenzijdig
     4248 * kon lijken -- alsof iemand nooit geantwoord had. Hier is de limiet er een
     4249 * voor het gesprek als geheel.
     4250 *
     4251 * `before` is de cursor van het OUDSTE bericht dat je al hebt; je krijgt wat
     4252 * daarvoor ligt. Er komt er een extra op om te weten of er nog meer is: de
     4253 * client hoort dat te weten zonder te moeten gokken, en zonder dat weten kan
     4254 * 'load more' niet eerlijk verschijnen.
     4255 *
     4256 * DE CURSOR IS SAMENGESTELD -- '<stempel>|<ref>' -- en niet alleen de stempel.
     4257 * Twee berichten in dezelfde seconde is bij DM's geen randgeval maar een
     4258 * gesprek, en met 'stamp < before' zou alles wat die grensseconde deelt stil
     4259 * overgeslagen worden. Je zou het niet merken: de pagina komt gewoon, er
     4260 * ontbreekt alleen iets in het midden.
     4261 */
     4262const cursorVan = (r) => (r ? `${r.stamp}|${r.ref}` : null);
     4263
     4264export function conversationHistory(slug, other, { before = null, limit = 60 } = {}) {
     4265  try {
     4266    const n = Math.min(Math.max(parseInt(limit, 10) || 60, 1), 200);
     4267    const knip = String(before || '').indexOf('|');
     4268    const bStamp = before && knip > 0 ? String(before).slice(0, knip) : null;
     4269    const bRef = before && knip > 0 ? String(before).slice(knip + 1) : null;
     4270    const rijen = db.prepare(`
     4271      SELECT other, stamp, richting, ref FROM (${GESPREK_UNIE})
     4272       WHERE other = @other
     4273         AND (@bStamp IS NULL OR stamp < @bStamp OR (stamp = @bStamp AND ref < @bRef))
     4274       ORDER BY stamp DESC, ref DESC LIMIT @n`).all({ slug, other, bStamp, bRef, n: n + 1 });
     4275    const meer = rijen.length > n;
     4276    const uit = meer ? rijen.slice(0, n) : rijen;
     4277    return { rijen: uit, meer, oudste: cursorVan(uit[uit.length - 1]) };
     4278  } catch { return { rijen: [], meer: false, oudste: null }; }
     4279}
     4280
     4281// De kolommen die een bericht tot kaart maken. Een constante, want de
     4282// gesprekslezing haalt dezelfde rijen op: twee lijsten die uiteenlopen leveren
     4283// een kaart die op de ene plek een plaatje heeft en op de andere niet.
     4284const BERICHT_KOLOMMEN = `
     4285      m.object_uri, m.note_url, m.actor_uri, m.actor_name, m.actor_handle, m.actor_icon, m.actor_url,
     4286      m.content, m.published, m.created_at, m.wave, m.help_request,
     4287      m.emoji_json, m.actor_emoji_json, m.media_json, m.quote_json, m.embed_json`;
     4288
     4289/** Dezelfde berichtrijen, maar op object-uri -- voor een gesprek. */
     4290export function messageRowsByUri(slug, uris) {
     4291  const lijst = (uris || []).filter((u) => typeof u === 'string' && u);
     4292  if (!lijst.length) return [];
     4293  try {
     4294    const gaten = lijst.map(() => '?').join(',');
     4295    return db.prepare(`SELECT ${BERICHT_KOLOMMEN} FROM ap_mentions m
     4296                        WHERE m.slug = ? AND m.object_uri IN (${gaten})`).all(slug, ...lijst);
     4297  } catch { return []; }
     4298}
     4299
    41934300export function getDirectMessages(slug, limit) {
    41944301  try {
    41954302    return db.prepare(`
    4196       SELECT m.object_uri, m.note_url, m.actor_uri, m.actor_name, m.actor_handle, m.actor_icon, m.actor_url,
    4197              m.content, m.published, m.created_at, m.wave, m.help_request,
    4198              m.emoji_json, m.actor_emoji_json, m.media_json, m.quote_json, m.embed_json
     4303      SELECT ${BERICHT_KOLOMMEN}
    41994304      FROM ap_mentions m
    42004305      WHERE m.slug = ?
     
    62866391  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
    62876392  listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
    6288   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
     6393  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, messageRowsByUri, conversationHeads, conversationHistory, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
    62896394  acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
    62906395  gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Note: See TracChangeset for help on using the changeset viewer.