Changeset 9eb6e1b in Klonkt for src/routes/search.js


Ignore:
Timestamp:
09/30/2026 06:41:45 AM (2 hours ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
86cfa1a
Parents:
3ceaef6
Message:

Een fediverse-post ziet eruit als een post, ook in de zoekbalk

Plak je een link in de zoekbalk, dan laadt de uitklaplijst nu de post zelf
in plaats van te zoeken naar de tekst van de link. Dezelfde kaart staat op de
zoekpagina en op /authorize_interaction, dus één render op drie plekken.

DE KAART. Wat eromheen stond is teruggebracht tot een kop met gezicht, de
inhoud, en één rij knoppen: Reageer, Like, Boost en het origineel. Weg zijn de
zin "je reageert op", de aparte knop naar het origineel, de twee grote pillen,
"of reageer:" en het antwoordvenster dat altijd openstond. Antwoorden klapt
open als je erom vraagt; melden staat onderaan en niet in de weg.

Foto's staan in een raster met vaste rijhoogtes. Met verhoudingen groeide het
mee met de breedte, en werd op een desktop de eerste van drie foto's zo hoog
dat de knoppen onder de vouw van de uitklaplijst verdwenen.

EEN INHOUDSWAARSCHUWING WORDT GERESPECTEERD. De oude render toonde alles open,
ook wat de schrijver achter een waarschuwing had gezet.

MOBIEL. Elke knop minstens 44 punten. Op een smal scherm vallen de woorden weg
en blijven de tekens staan, gelijk verdeeld; de woorden blijven bestaan voor
wie het scherm voorgelezen krijgt. Nagemeten op 375 pixels: geen horizontale
overloop, kleinste tikdoel 44, de knoppen in beeld zonder te scrollen.

DE PREVIEW IS COMPACT. Waarderen en boosten kan ter plekke; antwoorden en
melden gaan naar de volle pagina, met ?reply=1 of ?report=1 zodat dat deel
daar al openstaat. Een editor in een uitklaplijst is op een telefoon geen plek
om te schrijven. Zonder JS doet dezelfde link hetzelfde.

/search/remote levert het fragment, achter dezelfde grens als de volle
pagina: alleen wie de site beheert laat ophalen. Die grens weegt hier zwaarder,
want deze route wordt bij elke toetsaanslag geraakt; zonder hem is hij een
haalservice die iedereen deze server op adressen naar keuze laat afsturen.
Voor een bezoeker 204, en dan valt de zoekbalk terug op de gewone suggesties.
Elke toetsaanslag krijgt een volgnummer, zodat een traag antwoord op een oude
vraag niet overschrijft wat je inmiddels hebt getypt.

Waarderen en boosten ter plekke verhuisde van authorize-interaction.js naar
chrome.js: de kaart staat nu op elke pagina, via de zoekbalk. De zoekpagina
stuurde je bij een like eerder weg naar /authorize_interaction.

Onderweg: /authorize_interaction haalt nu ook ondertekend op, net als de
zoekbalk. Anders zegt die pagina "niet gevonden" over een post voor volgers die
de preview wel liet zien.

Getoetst: de preview met de post, ondertekend; wat er in de compacte kaart wel
en niet staat; dat een bezoeker via de preview niets laat ophalen; dat een
zoekterm niets ophaalt; de waarschuwing; het venster op verzoek. Tegenproef:
rechtencheck eruit, waarschuwing genegeerd, compact genegeerd en anoniem
ophalen laten elk hun eigen toets omvallen. Volledige suite 1283 groen.

Het gedrag in chrome.js is in de browser nagelopen met een nagebootste
server: herkennen, laden, invoegen, de volgorde bij een traag antwoord, de
terugval bij 204, waarderen en boosten heen en terug, en het antwoordvenster
open en dicht met de focus op de goede plek.

shaer-utpi

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/routes/search.js

    r3ceaef6 r9eb6e1b  
    2727import { t as i18nT, resolveLang } from '../services/i18n.js';
    2828import ActivityPubService from '../services/ActivityPubService.js';
     29import ejs from 'ejs';
     30import path from 'path';
     31import { fileURLToPath } from 'url';
    2932import PermissionsService from '../services/PermissionsService.js';
    3033
    … …  
    99102  const site = res.locals.site;
    100103  return !!(site && PermissionsService.canAdminSite(u, site));
     104}
     105
     106/**
     107 * Haal het object achter een adres op: eerst als post, dan als profiel.
     108 *
     109 * Eén weg voor de zoekpagina en de live-preview. Staan ze los, dan verschilt er
     110 * vroeg of laat iets -- de ondertekening bijvoorbeeld, en dan toont de preview
     111 * een post als ontbrekend die de pagina wel vindt.
     112 *
     113 * ONDERTEKEND als de site: een post die alleen voor volgers zichtbaar is weigert
     114 * een anonieme GET, en dan lijkt een bestaande post te ontbreken.
     115 */
     116async function lookUpRemote(remoteUri, site) {
     117  let remote = null;
     118  let remoteKind = null;
     119  try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ }
     120  if (remote) remoteKind = 'note';
     121  if (!remote) {
     122    try { remote = await ActivityPubService.resolveRemoteActor(remoteUri); } catch { /* onbereikbaar */ }
     123    if (remote) remoteKind = 'actor';
     124  }
     125  const reacted = remoteKind === 'note'
     126    ? ActivityPubService.getReaction(site.slug, remote.object_uri || remoteUri)
     127    : { liked: false, boosted: false };
     128  return { remote, remoteKind, reacted };
    101129}
    102130
    … …  
    214242  const remoteUri = lookupUri(rawQ);
    215243  const mayLookup = remoteUri ? mayLookUp(req, res) : false;
    216   let remote = null;
    217   let remoteKind = null;
    218   if (remoteUri && mayLookup) {
    219     // Ondertekend als deze site: een post die alleen voor volgers zichtbaar is
    220     // weigert een anonieme GET, en dan lijkt een bestaande post te ontbreken.
    221     try { remote = await ActivityPubService.resolveRemoteNote(remoteUri, { asSlug: site.slug }); } catch { /* onbereikbaar */ }
    222     if (remote) remoteKind = 'note';
    223     if (!remote) {
    224       try { remote = await ActivityPubService.resolveRemoteActor(remoteUri); } catch { /* onbereikbaar */ }
    225       if (remote) remoteKind = 'actor';
    226     }
    227   }
     244  const { remote, remoteKind, reacted: remoteReacted } = (remoteUri && mayLookup)
     245    ? await lookUpRemote(remoteUri, site)
     246    : { remote: null, remoteKind: null, reacted: { liked: false, boosted: false } };
    228247
    229248  const r = searchSite(req, res, rawQ, { posts: 50, tracks: 25, events: 25, pages: 8 });
    … …  
    234253    total, queryError: r.queryError,
    235254    remoteUri, remote, remoteKind, mayLookup,
    236     remoteReacted: (remoteKind === 'note') ? ActivityPubService.getReaction(site.slug, remote.object_uri || remoteUri) : { liked: false, boosted: false },
     255    remoteReacted,
    237256    siteTitle: site.title || '',
    238257  });
     258});
     259
     260// ── De live-preview van een adres (HTML-fragment) ─────────────────────────────
     261//
     262// Plak je een link in de zoekbalk, dan haalt de uitklaplijst de post zelf op
     263// in plaats van te zoeken naar de tekst van de link (shaer-utpi).
     264//
     265// Dezelfde grens als de volle pagina, en hier weegt hij zwaarder: deze route
     266// wordt bij elke toetsaanslag geraakt, dus zonder die grens is hij een
     267// haalservice die iedereen deze server op adressen naar keuze laat afsturen.
     268// Mag je niet ophalen, of is het geen adres, dan 204: de zoekbalk valt dan
     269// terug op de gewone suggesties. Geen 403, want voor een bezoeker is een
     270// geplakte link gewoon een zoekterm en geen geweigerde handeling.
     271const VIEWS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'views');
     272
     273router.get('/remote', async (req, res) => {
     274  const site = res.locals.site;
     275  const remoteUri = lookupUri(String(req.query.q || '').slice(0, 2048));
     276  if (!site || !remoteUri || !mayLookUp(req, res)) return res.status(204).end();
     277  const { remote, remoteKind, reacted } = await lookUpRemote(remoteUri, site);
     278  // Dezelfde taal als de pagina eromheen, anders staat er een Engelse kaart
     279  // in een Nederlandse zoekbalk.
     280  const lang = resolveLang(req, { userLang: req.session?.user?.lang, defaultLang: getSetting('default_lang') });
     281  const html = await ejs.renderFile(path.join(VIEWS_DIR, 'partials', 'remote-preview.ejs'), {
     282    t: (k, vars) => i18nT(lang, k, vars),
     283    remote, remoteKind, remoteUri, reacted,
     284    siteTitle: site.title || '',
     285  }, { async: false });
     286  // Wat deze bezoeker ziet hangt van zijn rechten en zijn reacties af: niet
     287  // bewaren, niet delen.
     288  res.set('Cache-Control', 'private, no-store');
     289  res.type('html').send(html);
    239290});
    240291
Note: See TracChangeset for help on using the changeset viewer.