| 1 | /**
|
|---|
| 2 | * ap-reactions.js — het reactiecluster (stap 6 van shaer-drc).
|
|---|
| 3 | *
|
|---|
| 4 | * De waarheid over "heb ik hierop gereageerd" (ap_my_reactions), de afgeleide
|
|---|
| 5 | * vlaggen op ap_timeline die setReaction daaruit bijhoudt, de eenmalige
|
|---|
| 6 | * migratie, en de boost-upsert die een vreemde post de tijdlijn in trekt.
|
|---|
| 7 | *
|
|---|
| 8 | * Twee koppelingen, allebei bewust zo:
|
|---|
| 9 | * - tlStmts komt STATISCH uit ap-timeline: reacties schrijven de
|
|---|
| 10 | * tijdlijnvlaggen, dus die pijl wijst een kant op en mag gewoon een import
|
|---|
| 11 | * zijn.
|
|---|
| 12 | * - de omgekeerde pijl (de tijdlijn-leeskant heeft getReactionsFor nodig)
|
|---|
| 13 | * blijft de injectie via wireTimeline in ActivityPubService -- twee
|
|---|
| 14 | * zustermodules die elkaar importeren zou precies de kring zijn die
|
|---|
| 15 | * shaer-drc vermijdt.
|
|---|
| 16 | * movedLock komt uit de dienstlaag (FEP-7628) en dus via wireReactions binnen.
|
|---|
| 17 | */
|
|---|
| 18 | import db from '../config/database.js';
|
|---|
| 19 | import { tlStmts } from './ap-timeline.js';
|
|---|
| 20 |
|
|---|
| 21 | // Het ene werktuig uit de dienstlaag; ActivityPubService vult het onderaan.
|
|---|
| 22 | let movedLock;
|
|---|
| 23 | export function wireReactions(deps) {
|
|---|
| 24 | ({ movedLock } = deps);
|
|---|
| 25 | }
|
|---|
| 26 |
|
|---|
| 27 | // Your like/boost state on a REMOTE post (interact page toggles).
|
|---|
| 28 | export function setMyReaction(slug, uri, kind, on) {
|
|---|
| 29 | if (on) db.prepare('INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind) VALUES (?,?,?)').run(slug, uri, kind);
|
|---|
| 30 | else db.prepare('DELETE FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ? AND kind = ?').run(slug, uri, kind);
|
|---|
| 31 | }
|
|---|
| 32 | export function getMyReactions(slug, uri) {
|
|---|
| 33 | const rows = (slug && uri) ? db.prepare('SELECT kind FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ?').all(slug, uri) : [];
|
|---|
| 34 | return { liked: rows.some((r) => r.kind === 'like'), boosted: rows.some((r) => r.kind === 'boost') };
|
|---|
| 35 | }
|
|---|
| 36 |
|
|---|
| 37 | // AFGELEIDE, GEEN BRON (shaer-9e9). De waarheid over "heb ik hierop gereageerd"
|
|---|
| 38 | // staat in ap_my_reactions; deze vlaggen worden daaruit bijgehouden door
|
|---|
| 39 | // setReaction en door niets anders. Roep ze niet los aan -- dan schrijf je de
|
|---|
| 40 | // helft, en dat is precies hoe shaer:liked maandenlang false bleef (04aca12).
|
|---|
| 41 | //
|
|---|
| 42 | // ap_timeline.boosted verdient zijn bestaan wel: hij staat in de WHERE van de
|
|---|
| 43 | // Cirkel-feed (getCirkelPosts) en in boostedCount, dus hij is een index en geen
|
|---|
| 44 | // kopie. ap_timeline.liked wordt nergens als verzameling bevraagd en kan weg
|
|---|
| 45 | // zodra fase 2 lang genoeg goed staat; hij is nu nog het vangnet waarmee
|
|---|
| 46 | // terugdraaien een code-revert blijft in plaats van dataherstel.
|
|---|
| 47 | let _markBoost, _unmarkBoost, _boostedCount;
|
|---|
| 48 | export function markBoosted(slug, noteId) {
|
|---|
| 49 | try { if (!_markBoost) _markBoost = db.prepare('UPDATE ap_timeline SET boosted = 1 WHERE slug = ? AND id = ?'); _markBoost.run(slug, noteId); } catch { /* ignore */ }
|
|---|
| 50 | }
|
|---|
| 51 | export function unmarkBoosted(slug, noteId) {
|
|---|
| 52 | try { if (!_unmarkBoost) _unmarkBoost = db.prepare('UPDATE ap_timeline SET boosted = 0 WHERE slug = ? AND id = ?'); _unmarkBoost.run(slug, noteId); } catch { /* ignore */ }
|
|---|
| 53 | }
|
|---|
| 54 | let _markLike, _unmarkLike;
|
|---|
| 55 | export function markLiked(slug, noteId) {
|
|---|
| 56 | try { if (!_markLike) _markLike = db.prepare('UPDATE ap_timeline SET liked = 1 WHERE slug = ? AND id = ?'); _markLike.run(slug, noteId); } catch { /* ignore */ }
|
|---|
| 57 | }
|
|---|
| 58 | export function unmarkLiked(slug, noteId) {
|
|---|
| 59 | try { if (!_unmarkLike) _unmarkLike = db.prepare('UPDATE ap_timeline SET liked = 0 WHERE slug = ? AND id = ?'); _unmarkLike.run(slug, noteId); } catch { /* ignore */ }
|
|---|
| 60 | }
|
|---|
| 61 | /**
|
|---|
| 62 | * Zet een reactie van JOU op een object. Dit hoort het enige schrijfpad te zijn
|
|---|
| 63 | * (shaer-9e9): de tussentabel ap_my_reactions is de waarheid, de vlaggen op
|
|---|
| 64 | * ap_timeline zijn de afgeleide. Zolang markLiked en broers los aanroepbaar
|
|---|
| 65 | * blijven kan een aanroeper ze vergeten, en dat is niet hypothetisch -- precies
|
|---|
| 66 | * dat leverde de shaer:liked-bug op (04aca12).
|
|---|
| 67 | *
|
|---|
| 68 | * `opts.note` is de opgeloste remote note bij een boost. Die is niet optioneel
|
|---|
| 69 | * uit netheid: een boost moet de post je tijdlijn IN trekken als je de auteur
|
|---|
| 70 | * niet volgt, anders heeft de vlag geen rij om op te landen en verschijnt de
|
|---|
| 71 | * boost nergens -- ook niet in de Cirkel.
|
|---|
| 72 | *
|
|---|
| 73 | * `opts.flagUri` bestaat omdat de twee bronnen vandaag verschillend gesleuteld
|
|---|
| 74 | * worden: de tussentabel op de URI die de client stuurde, de vlag op de
|
|---|
| 75 | * opgeloste object-URI. Meestal zijn die gelijk, maar niet gegarandeerd. Deze
|
|---|
| 76 | * naad houdt fase 1 gedragsbehoudend; het samentrekken van die twee sleutels is
|
|---|
| 77 | * werk voor fase 2, mét datamigratie.
|
|---|
| 78 | */
|
|---|
| 79 | // Reactie-migratie (shaer-9e9). Draait bij boot, EEN keer per bump, net als
|
|---|
| 80 | // selfHealTimeline. Bewust automatisch: klonkt-update tilt een hele vloot in een
|
|---|
| 81 | // stap naar nieuwe code, en een handmatig script per instance wordt vergeten --
|
|---|
| 82 | // terwijl het falen stil is (een reactie die niemand meer ziet geeft geen fout).
|
|---|
| 83 | // v2 haalt de derde bron erbij: ap_interactions.acted_* (shaer-ipb). Een bump
|
|---|
| 84 | // laat alle stappen opnieuw lopen, en dat mag -- ze zijn alle drie idempotent.
|
|---|
| 85 | const REACTIONS_MIGRATION_VERSION = 2;
|
|---|
| 86 |
|
|---|
| 87 | /**
|
|---|
| 88 | * Brengt alle reacties naar de tussentabel, onder de canonieke object-URI.
|
|---|
| 89 | *
|
|---|
| 90 | * Twee stappen, en ze zijn allebei nodig:
|
|---|
| 91 | *
|
|---|
| 92 | * 1. HERSLEUTELEN. De oude interact-route bewaarde de URI waarmee je binnenkwam
|
|---|
| 93 | * en de bookmarklet geeft window.location.href door, dus de permalink. Sinds
|
|---|
| 94 | * canonicalReactionUri wordt er op de object-URI gezocht, waardoor die rijen
|
|---|
| 95 | * wees zouden zijn. De created_at reist mee: bij hersleutelen weten we
|
|---|
| 96 | * wanneer je reageerde, bij aanvullen niet.
|
|---|
| 97 | * 2. AANVULLEN vanuit de afgeleide kolommen. Alles wat op oude code via de
|
|---|
| 98 | * Krant is gegeven staat alleen daar; zonder deze stap toont het als
|
|---|
| 99 | * niet-gereageerd en klikt een gebruiker opnieuw -- met een tweede Like de
|
|---|
| 100 | * fediverse in als gevolg.
|
|---|
| 101 | *
|
|---|
| 102 | * Idempotent. Geeft terug wat er gebeurd is, zodat het script het kan tonen.
|
|---|
| 103 | */
|
|---|
| 104 | export function migrateReactions(opts = {}) {
|
|---|
| 105 | const uit = { hersleuteld: 0, aangevuld: 0, reacties: 0, overgeslagen: false };
|
|---|
| 106 | try {
|
|---|
| 107 | if (!opts.force) {
|
|---|
| 108 | const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('reactions_migration_version');
|
|---|
| 109 | const cur = r ? (parseInt(r.value, 10) || 0) : 0;
|
|---|
| 110 | if (cur >= REACTIONS_MIGRATION_VERSION) { uit.overgeslagen = true; return uit; }
|
|---|
| 111 | }
|
|---|
| 112 | } catch { return uit; } // geen app_settings → deze database is te oud om aan te raken
|
|---|
| 113 |
|
|---|
| 114 | // Een rij die NIET op een tijdlijn-id staat maar wel op een tijdlijn-url.
|
|---|
| 115 | const wees = `
|
|---|
| 116 | FROM ap_my_reactions r JOIN ap_timeline t ON t.slug = r.site_slug AND t.url = r.target_uri
|
|---|
| 117 | WHERE NOT EXISTS (SELECT 1 FROM ap_timeline t2 WHERE t2.slug = r.site_slug AND t2.id = r.target_uri)`;
|
|---|
| 118 | const scheef = (kind, kolom) => `
|
|---|
| 119 | FROM ap_timeline t
|
|---|
| 120 | WHERE t.${kolom} = 1
|
|---|
| 121 | AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
|
|---|
| 122 | WHERE r.site_slug = t.slug AND r.target_uri = t.id AND r.kind = '${kind}')`;
|
|---|
| 123 | // 3. De derde bron: wat JIJ deed met een reactie onder je eigen post. De slug
|
|---|
| 124 | // hangt hier niet aan de rij maar aan de post; vandaar de twee joins. Een
|
|---|
| 125 | // rij zonder object_uri kan nooit een reactie dragen (fedi-react eist hem),
|
|---|
| 126 | // dus die uitsluiting verliest per constructie niets.
|
|---|
| 127 | const acted = (kind, kolom) => `
|
|---|
| 128 | FROM ap_interactions i
|
|---|
| 129 | JOIN posts p ON p.id = i.post_id
|
|---|
| 130 | JOIN sites s ON s.id = p.site_id
|
|---|
| 131 | WHERE i.${kolom} = 1 AND IFNULL(i.object_uri, '') <> ''
|
|---|
| 132 | AND NOT EXISTS (SELECT 1 FROM ap_my_reactions r
|
|---|
| 133 | WHERE r.site_slug = s.slug AND r.target_uri = i.object_uri AND r.kind = '${kind}')`;
|
|---|
| 134 |
|
|---|
| 135 | if (opts.dryRun) {
|
|---|
| 136 | const tel = (sql) => { try { return db.prepare(`SELECT COUNT(*) AS n ${sql}`).get().n; } catch { return 0; } };
|
|---|
| 137 | uit.hersleuteld = tel(wees);
|
|---|
| 138 | uit.aangevuld = tel(scheef('like', 'liked')) + tel(scheef('boost', 'boosted'));
|
|---|
| 139 | uit.reacties = tel(acted('like', 'acted_like')) + tel(acted('boost', 'acted_boost'));
|
|---|
| 140 | return uit;
|
|---|
| 141 | }
|
|---|
| 142 |
|
|---|
| 143 | try {
|
|---|
| 144 | db.transaction(() => {
|
|---|
| 145 | // 1. Hersleutelen: eerst de canonieke variant erbij, dan de permalink weg.
|
|---|
| 146 | // In die volgorde, zodat een onderbreking hooguit een dubbele rij
|
|---|
| 147 | // oplevert en nooit een verdwenen reactie.
|
|---|
| 148 | uit.hersleuteld = db.prepare(`
|
|---|
| 149 | INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind, created_at)
|
|---|
| 150 | SELECT r.site_slug, t.id, r.kind, r.created_at ${wees}`).run().changes;
|
|---|
| 151 | db.prepare(`DELETE FROM ap_my_reactions WHERE rowid IN (SELECT r.rowid ${wees})`).run();
|
|---|
| 152 |
|
|---|
| 153 | // 2. Aanvullen vanuit de kolommen.
|
|---|
| 154 | for (const [kind, kolom] of [['like', 'liked'], ['boost', 'boosted']]) {
|
|---|
| 155 | uit.aangevuld += db.prepare(`
|
|---|
| 156 | INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind)
|
|---|
| 157 | SELECT t.slug, t.id, '${kind}' ${scheef(kind, kolom)}`).run().changes;
|
|---|
| 158 | }
|
|---|
| 159 |
|
|---|
| 160 | // 3. En vanuit acted_* op de reacties onder je eigen posts.
|
|---|
| 161 | for (const [kind, kolom] of [['like', 'acted_like'], ['boost', 'acted_boost']]) {
|
|---|
| 162 | uit.reacties += db.prepare(`
|
|---|
| 163 | INSERT OR IGNORE INTO ap_my_reactions (site_slug, target_uri, kind)
|
|---|
| 164 | SELECT s.slug, i.object_uri, '${kind}' ${acted(kind, kolom)}`).run().changes;
|
|---|
| 165 | }
|
|---|
| 166 | })();
|
|---|
| 167 | if (uit.hersleuteld || uit.aangevuld || uit.reacties) {
|
|---|
| 168 | console.log(`[AP] reaction migration v${REACTIONS_MIGRATION_VERSION}: ${uit.hersleuteld} re-keyed, ${uit.aangevuld} backfilled, ${uit.reacties} from comments`);
|
|---|
| 169 | }
|
|---|
| 170 | if (!opts.force) {
|
|---|
| 171 | db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)')
|
|---|
| 172 | .run('reactions_migration_version', String(REACTIONS_MIGRATION_VERSION));
|
|---|
| 173 | }
|
|---|
| 174 | } catch (e) {
|
|---|
| 175 | // Niet fataal: de kolommen staan er nog, dus de oude waarheid is niet weg.
|
|---|
| 176 | // Een volgende boot probeert het opnieuw, want de versie is niet gezet.
|
|---|
| 177 | console.warn('[AP] reaction migration failed:', e.message);
|
|---|
| 178 | }
|
|---|
| 179 | return uit;
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | /**
|
|---|
| 183 | * Van wat de client stuurde naar de canonieke sleutel voor een reactie.
|
|---|
| 184 | *
|
|---|
| 185 | * Een post heeft twee URI's: zijn AP-object-id (.../ap/notes/<uuid>) en zijn
|
|---|
| 186 | * leesbare permalink (.../effortlesseffect). De Krant en het C2S-pad spreken de
|
|---|
| 187 | * eerste, de interact-pagina de tweede. Werden reacties onder allebei opgeslagen,
|
|---|
| 188 | * dan bestond dezelfde like twee keer -- en erger: een like uit de Krant was op
|
|---|
| 189 | * de interact-pagina onzichtbaar, want daar werd op de permalink gezocht.
|
|---|
| 190 | *
|
|---|
| 191 | * Dit was de naad die fase 1 bewust open liet ("samentrekken is werk voor fase
|
|---|
| 192 | * 2"). Robin liep er meteen tegenaan: een geboost en geliket bericht toonde geen
|
|---|
| 193 | * highlight. Vandaar hier, en niet later.
|
|---|
| 194 | *
|
|---|
| 195 | * De object-URI wint, want dat is waar ap_timeline op sleutelt en waar de
|
|---|
| 196 | * backfill op is gebaseerd. Kennen we de post niet, dan blijft de invoer staan:
|
|---|
| 197 | * een reactie op iets buiten je tijdlijn moet gewoon werken.
|
|---|
| 198 | */
|
|---|
| 199 | export function canonicalReactionUri(slug, uri) {
|
|---|
| 200 | if (!slug || !uri) return uri;
|
|---|
| 201 | try {
|
|---|
| 202 | if (db.prepare('SELECT 1 FROM ap_timeline WHERE slug = ? AND id = ?').get(slug, uri)) return uri;
|
|---|
| 203 | const row = db.prepare('SELECT id FROM ap_timeline WHERE slug = ? AND url = ? LIMIT 1').get(slug, uri);
|
|---|
| 204 | return (row && row.id) || uri;
|
|---|
| 205 | } catch { return uri; }
|
|---|
| 206 | }
|
|---|
| 207 |
|
|---|
| 208 | /**
|
|---|
| 209 | * Wat heb IK met dit object gedaan? Leest de tussentabel, de bron van waarheid
|
|---|
| 210 | * sinds shaer-9e9 fase 2. Vervangt getMyReactions en getTimelineReaction, die
|
|---|
| 211 | * dezelfde vraag beantwoordden uit twee verschillende bronnen.
|
|---|
| 212 | */
|
|---|
| 213 | export function getReaction(slug, uri) {
|
|---|
| 214 | try {
|
|---|
| 215 | const key = canonicalReactionUri(slug, uri);
|
|---|
| 216 | const rows = (slug && key)
|
|---|
| 217 | ? db.prepare('SELECT kind FROM ap_my_reactions WHERE site_slug = ? AND target_uri = ?').all(slug, key)
|
|---|
| 218 | : [];
|
|---|
| 219 | return { liked: rows.some((r) => r.kind === 'like'), boosted: rows.some((r) => r.kind === 'boost') };
|
|---|
| 220 | } catch { return { liked: false, boosted: false }; }
|
|---|
| 221 | }
|
|---|
| 222 |
|
|---|
| 223 | /**
|
|---|
| 224 | * Dezelfde vraag voor een hele pagina in EEN query. De C2S-tijdlijn zet
|
|---|
| 225 | * shaer:liked op elke post; per rij vragen zou dat een N+1 maken, en dan had je
|
|---|
| 226 | * een consistentiebug geruild voor een traagheidsbug.
|
|---|
| 227 | */
|
|---|
| 228 | export function getReactionsFor(slug, uris) {
|
|---|
| 229 | const out = new Map();
|
|---|
| 230 | const list = [...new Set((uris || []).filter(Boolean))].slice(0, 500);
|
|---|
| 231 | if (!slug || !list.length) return out;
|
|---|
| 232 | try {
|
|---|
| 233 | const rows = db.prepare(
|
|---|
| 234 | `SELECT target_uri, kind FROM ap_my_reactions
|
|---|
| 235 | WHERE site_slug = ? AND target_uri IN (${list.map(() => '?').join(',')})`,
|
|---|
| 236 | ).all(slug, ...list);
|
|---|
| 237 | for (const r of rows) {
|
|---|
| 238 | const cur = out.get(r.target_uri) || { liked: false, boosted: false };
|
|---|
| 239 | if (r.kind === 'like') cur.liked = true;
|
|---|
| 240 | if (r.kind === 'boost') cur.boosted = true;
|
|---|
| 241 | out.set(r.target_uri, cur);
|
|---|
| 242 | }
|
|---|
| 243 | } catch { /* leeg = niets gereageerd, en dat is een veilige uitkomst */ }
|
|---|
| 244 | return out;
|
|---|
| 245 | }
|
|---|
| 246 |
|
|---|
| 247 | export function setReaction(slug, uri, kind, on, opts = {}) {
|
|---|
| 248 | if (!slug || !uri || (kind !== 'like' && kind !== 'boost')) return;
|
|---|
| 249 | // Ook hier, en niet alleen bij sendInteraction. Deze functie schrijft ALLEEN de
|
|---|
| 250 | // lokale vlag; het versturen gebeurt elders. Zonder deze poort zou je op een
|
|---|
| 251 | // verhuisd account een like zien staan die nooit de deur uit is gegaan, en dat
|
|---|
| 252 | // is de halve toestand die erger is dan een duidelijke weigering.
|
|---|
| 253 | try {
|
|---|
| 254 | const s = db.prepare('SELECT moved_to FROM sites WHERE slug = ?').get(slug);
|
|---|
| 255 | if (movedLock(s).locked) { console.warn('[AP] reactie geweigerd, account verhuisd:', slug, kind); return; }
|
|---|
| 256 | } catch { /* geen sites-tabel = geen verhuizing */ }
|
|---|
| 257 | // EEN sleutel voor beide bronnen. opts.flagUri is de opgeloste object-URI van
|
|---|
| 258 | // de aanroeper (het C2S-pad kent die uit resolveRemoteNote en dat is
|
|---|
| 259 | // betrouwbaarder dan onze cache); anders leiden we hem af. Vroeger kreeg de
|
|---|
| 260 | // tussentabel de URI die de client stuurde en de vlag de opgeloste -- dat
|
|---|
| 261 | // maakte dezelfde like onvindbaar vanaf de andere pagina.
|
|---|
| 262 | const flagUri = opts.flagUri || canonicalReactionUri(slug, uri);
|
|---|
| 263 | setMyReaction(slug, flagUri, kind, !!on);
|
|---|
| 264 | if (kind === 'boost') {
|
|---|
| 265 | if (!on) unmarkBoosted(slug, flagUri);
|
|---|
| 266 | else if (opts.note) upsertBoostedNote(slug, opts.note);
|
|---|
| 267 | else markBoosted(slug, flagUri);
|
|---|
| 268 | } else if (on) markLiked(slug, flagUri);
|
|---|
| 269 | else unmarkLiked(slug, flagUri);
|
|---|
| 270 | }
|
|---|
| 271 |
|
|---|
| 272 | export function getTimelineReaction(slug, noteId) {
|
|---|
| 273 | 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 }; }
|
|---|
| 274 | }
|
|---|
| 275 | // Boost a REMOTE post that may not be in your timeline (you don't follow the author):
|
|---|
| 276 | // store it in ap_timeline (INSERT OR IGNORE → no dup for followed posts) so it shows in
|
|---|
| 277 | // the Cirkel with a Boost badge, then flag it boosted.
|
|---|
| 278 | export function upsertBoostedNote(slug, note) {
|
|---|
| 279 | if (!slug || !note || !note.object_uri) return;
|
|---|
| 280 | const id = note.object_uri;
|
|---|
| 281 | // Prefer the full typed media (incl. video/mp4 — a Loops boost is video-only and
|
|---|
| 282 | // rendered a bare text tile); fall back to the image-only list for older callers.
|
|---|
| 283 | const media = (note.media && note.media !== '[]')
|
|---|
| 284 | ? note.media
|
|---|
| 285 | : JSON.stringify((note.images || []).map((u) => ({ url: u, type: 'image/jpeg' })));
|
|---|
| 286 | try {
|
|---|
| 287 | const r = tlStmts().ins.run(id, slug, note.actor_uri || '', note.actor_name || '', note.actor_handle || '',
|
|---|
| 288 | note.actor_icon || '', note.actor_url || '', note.content || '', note.url || null,
|
|---|
| 289 | new Date().toISOString(), media, note.sensitive ? 1 : 0, note.cw || null);
|
|---|
| 290 | if (!r.changes) {
|
|---|
| 291 | // Row already cached (INSERT OR IGNORE) → refresh it with the freshly
|
|---|
| 292 | // resolved note. Without this a row cached without its cover (or with
|
|---|
| 293 | // stale content) stayed stale forever — even boosting again didn't heal it.
|
|---|
| 294 | // Keep the CACHED media when the resolve yielded none: an empty re-resolve
|
|---|
| 295 | // used to clobber a good media_json (the followed copy had the video, the
|
|---|
| 296 | // boost wiped it to []).
|
|---|
| 297 | db.prepare(`UPDATE ap_timeline SET content = ?, media_json = CASE WHEN ? = '[]' THEN media_json ELSE ? END,
|
|---|
| 298 | nsfw = ?, cw = ?, url = COALESCE(?, url) WHERE slug = ? AND id = ?`)
|
|---|
| 299 | .run(note.content || '', media, media, note.sensitive ? 1 : 0, note.cw || null, note.url || null, slug, id);
|
|---|
| 300 | }
|
|---|
| 301 | } catch { /* ignore */ }
|
|---|
| 302 | markBoosted(slug, id);
|
|---|
| 303 | }
|
|---|
| 304 | export function boostedCount(slug) {
|
|---|
| 305 | // Geboost EN in je tijdlijn, zoals voorheen: de tussentabel kan ook een boost
|
|---|
| 306 | // bevatten van iets dat er (nog) niet in staat.
|
|---|
| 307 | try {
|
|---|
| 308 | if (!_boostedCount) _boostedCount = db.prepare(`SELECT COUNT(*) AS n FROM ap_my_reactions r
|
|---|
| 309 | JOIN ap_timeline t ON t.slug = r.site_slug AND t.id = r.target_uri
|
|---|
| 310 | WHERE r.site_slug = ? AND r.kind = 'boost'`);
|
|---|
| 311 | return _boostedCount.get(slug).n;
|
|---|
| 312 | } catch { return 0; }
|
|---|
| 313 | }
|
|---|