Changeset 3ceaef6 in Klonkt for src


Ignore:
Timestamp:
09/30/2026 06:16:32 AM (5 hours ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
9eb6e1b
Parents:
407353e
Message:

Zoeken op een adres: het object erachter, met de knoppen erbij

Plak je een link naar een fediverse-post of -profiel in de zoekbalk, dan is dat
geen zoekterm maar een aanwijzing. Klonkt haalt hem op en zet hem bovenaan de
resultaten, met dezelfde knoppen als /authorize_interaction: antwoorden,
waarderen, boosten, stemmen, volgen en melden.

De weg erheen was vooral opruimen. De render en de formulieren stonden als 200
regels in authorize-interaction.ejs, en er een tweede kopie van maken voor de
zoekpagina zou een tweede plek zijn waar een poort, een sanitizer of een knop
vergeten kan worden. Ze staan nu in partials/remote-object.ejs en
partials/remote-actor.ejs, met de opmaak in een derde. De pagina die ze had
gebruikt ze nu ook, dus er is één render en de formulieren posten in beide
gevallen naar /authorize_interaction/..., waar het werk en de rechtencontrole
al zaten.

DRIE DINGEN DIE VASTLIGGEN, en elk om een eigen reden:

Het schema is verplicht. "soundfabrics.nl" is een geldige zoekterm -- iemand
zoekt naar de naam -- en een kale domeinnaam als adres opvatten maakt van elke
zoekopdracht met een punt erin een netwerkverzoek. Met ​https:// ervoor is er
geen twijfel over de bedoeling.

Ophalen mag alleen een beheerder van de site laten doen. Dat is dezelfde grens
als /authorize_interaction, en met opzet: ophalen is een uitgaand verzoek dat
deze server namens de bezoeker doet. Zonder die grens is de zoekbalk een
haalservice waarmee een vreemde deze server laat aankloppen op adressen naar
keuze. Een bezoeker die toch een link plakt krijgt de gewone zoekresultaten en
een zin die zegt waarom er verder niets staat.

De ophaling gaat ONDERTEKEND als de site. Een post die alleen voor volgers
zichtbaar is weigert een anonieme GET, en dan lijkt een bestaande post te
ontbreken -- dezelfde val als bij het antwoorden op een post van een vriend.

Bijgekomen zonder dat erom gevraagd is, en het is het vermelden waard: het
profiel toont nu zijn avatar. Die hadden we al in handen (actor_icon uit
resolveRemoteActor) en hij stond op geen van beide schermen.

Getoetst: de herkenning zelf, dat een zoekterm GEEN verzoek wordt, dat de post
met zijn vier formulieren binnenkomt, dat een profiel de volgknop geeft, dat
een bezoeker niets laat ophalen, en dat /authorize_interaction na de verhuizing
nog steeds hetzelfde toont -- die pagina had zelf geen enkele toets, dus dat
was de bodem die er onder moest. Tegenproef per zaak.

Nog open: een @naam@server in de zoekbalk (webfinger kan het al, dus klein
werk) en een hint in de live-suggesties terwijl je plakt.

shaer-utpi

Location:
src
Files:
3 added
4 edited

Legend:

Unmodified
Added
Removed
  • src/routes/search.js

    r407353e r3ceaef6  
    99 *   4. Pages (Agenda / Downloads / Links / Press kit / Archive) by name — only
    1010 *      the available ones.
     11 *   5. Een ADRES van een andere server (shaer-utpi): plak je een link naar een
     12 *      fediverse-post of -profiel, dan is dat geen zoekterm maar een aanwijzing.
     13 *      Die halen we op en tonen we, met dezelfde knoppen als
     14 *      /authorize_interaction: antwoorden, waarderen, boosten, stemmen,
     15 *      volgen, melden.
    1116 *
    1217 * FTS5: user input is tokenised on non-letter/digit chars and each token is wrapped
    … …  
    2126import { premiumUnlocked } from '../services/PatreonService.js';
    2227import { t as i18nT, resolveLang } from '../services/i18n.js';
     28import ActivityPubService from '../services/ActivityPubService.js';
     29import PermissionsService from '../services/PermissionsService.js';
    2330
    2431const router = express.Router();
    … …  
    4552  if (!s || /^[…\s]*$/.test(s)) return esc((excerpt || '').slice(0, 160));
    4653  return s;
     54}
     55
     56/**
     57 * Is dit een ADRES en geen zoekterm? Dan het genormaliseerde adres, anders null.
     58 *
     59 * HET SCHEMA IS VERPLICHT, en dat is een keuze. "soundfabrics.nl" is een
     60 * geldige zoekterm -- iemand zoekt naar de naam -- en een bare domeinnaam als
     61 * adres opvatten maakt van elke zoekopdracht met een punt erin een
     62 * netwerkverzoek. Met https:// ervoor is er geen twijfel over de bedoeling.
     63 *
     64 * De fragmentverwijzing gaat eraf: die hoort bij de browser, niet bij het
     65 * object, en met #comment eraan is het een andere sleutel voor hetzelfde ding.
     66 */
     67export function lookupUri(q) {
     68  const s = String(q || '').trim();
     69  if (!/^https?:\/\//i.test(s) || /\s/.test(s)) return null;
     70  try {
     71    const u = new URL(s);
     72    if (u.protocol !== 'http:' && u.protocol !== 'https:') return null;
     73    // Een naam zonder punt is geen adres op het open net (localhost, een
     74    // intern hostname). De SSRF-bewaking in de transportlaag houdt de rest
     75    // tegen; dit scheelt het verzoek.
     76    if (!u.hostname.includes('.')) return null;
     77    u.hash = '';
     78    return u.toString();
     79  } catch {
     80    return null;
     81  }
     82}
     83
     84/**
     85 * Mag deze bezoeker een adres laten OPHALEN?
     86 *
     87 * Dezelfde grens als /authorize_interaction (requireSiteManager), en met opzet:
     88 * ophalen is een uitgaand verzoek dat deze server namens iemand anders doet.
     89 * Voor een willekeurige bezoeker zou de zoekbalk daarmee een haalservice zijn
     90 * waarmee je andermans server kunt laten aankloppen op adressen naar keuze.
     91 *
     92 * Dit spiegelt requireSiteManager in plaats van hem aan te roepen: die stuurt
     93 * je naar het inlogscherm, en een zoekopdracht hoort niemand weg te sturen.
     94 */
     95function mayLookUp(req, res) {
     96  const u = req.session && req.session.user;
     97  if (!u) return false;
     98  if (u.role === 'god' || u.role === 'kijker') return true;
     99  const site = res.locals.site;
     100  return !!(site && PermissionsService.canAdminSite(u, site));
    47101}
    48102
    … …  
    143197
    144198// ── Full results page ────────────────────────────────────────────────────────
    145 router.get('/', (req, res) => {
     199router.get('/', async (req, res) => {
    146200  const site = res.locals.site;
    147201  if (!site) return res.status(404).send('No site');
    … …  
    153207      results: [], tracks: [], events: [], pages: [], total: 0,
    154208    });
     209  }
     210
     211  // Een adres wordt OPGEHAALD, niet doorzocht (shaer-utpi). Eerst als post,
     212  // dan als profiel: een URL die geen van beide oplevert is gewoon een
     213  // zoekterm die toevallig op een link lijkt, en dan blijft de rest staan.
     214  const remoteUri = lookupUri(rawQ);
     215  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    }
    155227  }
    156228
    … …  
    161233    results: r.results, tracks: r.tracks, events: r.events, pages: r.pages,
    162234    total, queryError: r.queryError,
     235    remoteUri, remote, remoteKind, mayLookup,
     236    remoteReacted: (remoteKind === 'note') ? ActivityPubService.getReaction(site.slug, remote.object_uri || remoteUri) : { liked: false, boosted: false },
     237    siteTitle: site.title || '',
    163238  });
    164239});
  • src/services/i18n.js

    r407353e r3ceaef6  
    179179    'search.error': 'Kon die zoekopdracht niet uitvoeren. Probeer een eenvoudiger term.',
    180180    'search.results_one': '{n} resultaat voor “{q}”', 'search.results_other': '{n} resultaten voor “{q}”',
    181     'search.section_tracks': 'Nummers', 'search.section_posts': 'Posts',
     181    'search.section_tracks': 'Nummers', 'search.section_fediverse': 'Van het fediverse', 'search.fedi_signin': 'Log in als beheerder van deze site om dit adres op te halen.', 'search.section_posts': 'Posts',
    182182    'search.empty': 'Niets gevonden.', 'search.in_post': 'in post →',
    183183    'search.section_events': 'Evenementen', 'search.section_pages': 'Pagina’s',
    … …  
    11891189    'search.error': 'Couldn’t run that search. Try a simpler term.',
    11901190    'search.results_one': '{n} result for “{q}”', 'search.results_other': '{n} results for “{q}”',
    1191     'search.section_tracks': 'Tracks', 'search.section_posts': 'Posts',
     1191    'search.section_tracks': 'Tracks', 'search.section_fediverse': 'From the fediverse', 'search.fedi_signin': 'Sign in as a manager of this site to look this address up.', 'search.section_posts': 'Posts',
    11921192    'search.empty': 'Nothing found.', 'search.in_post': 'in post →',
    11931193    'search.section_events': 'Events', 'search.section_pages': 'Pages',
    … …  
    21982198    'search.error': 'Diese Suche konnte nicht ausgeführt werden. Versuch einen einfacheren Begriff.',
    21992199    'search.results_one': '{n} Ergebnis für „{q}“', 'search.results_other': '{n} Ergebnisse für „{q}“',
    2200     'search.section_tracks': 'Titel', 'search.section_posts': 'Beiträge',
     2200    'search.section_tracks': 'Titel', 'search.section_fediverse': 'Aus dem Fediverse', 'search.fedi_signin': 'Melde dich als Verwalter dieser Seite an, um diese Adresse abzurufen.', 'search.section_posts': 'Beiträge',
    22012201    'search.empty': 'Nichts gefunden.', 'search.in_post': 'im Beitrag →',
    22022202    'search.section_events': 'Veranstaltungen', 'search.section_pages': 'Seiten',
  • src/views/pages/authorize-interaction.ejs

    r407353e r3ceaef6  
    9595  <% } else if (typeof followTarget !== 'undefined' && followTarget) { %>
    9696  <h1 class="auth-interact-title"><%= t('fedi.follow_heading') %></h1>
    97   <p class="auth-interact-meta">
    98     <%= t('fedi.follow_intro') %>
    99     <a href="<%= followTarget.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= followTarget.actor_name %></strong></a>
    100     <span class="fedi-handle"><%= followTarget.actor_handle %></span>
    101   </p>
    102   <form method="post" action="/authorize_interaction/follow" class="auth-interact-form">
    103     <input type="hidden" name="uri" value="<%= followTarget.actor_uri %>">
    104     <div class="auth-interact-actions">
    105       <button type="submit" class="btn btn-primary"><%= t('fedi.follow_btn') %></button>
    106       <a href="/" class="btn"><%= t('comments.cancel') %></a>
    107     </div>
    108   </form>
    109   <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
     97  <%- include('../partials/remote-actor', { actor: followTarget, cancelUrl: '/', siteTitle: siteTitle }) %>
    11098  <% } else { %>
    11199  <h1 class="auth-interact-title"><%= t('fedi.remote_title') %></h1>
    … …  
    118106    </form>
    119107  <% } else { %>
    120     <p class="auth-interact-meta">
    121       <%= t('fedi.remote_replying_to') %>
    122       <a href="<%= target.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= target.actor_name %></strong></a>
    123       <span class="fedi-handle"><%= target.actor_handle %></span>
    124     </p>
    125     <blockquote class="auth-interact-preview">
    126       <% if (target.content) { %><div class="auth-interact-content"><%- target.content %></div>
    127       <% } else if (target.preview) { %><%= target.preview %><% } %>
    128       <% (target.images || []).forEach(function(im){ %>
    129         <img class="auth-interact-img" src="<%= im %>" alt="" loading="lazy">
    130       <% }); %>
    131     </blockquote>
    132     <% if (target.url || uri) { %>
    133       <a class="auth-view-src" href="<%= target.url || uri %>" target="_blank" rel="nofollow noopener">
    134         <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg>
    135         <span><%= t('fedi.remote_view_original') %></span>
    136       </a>
    137     <% } %>
    138     <% if (target.poll) { var _pl = target.poll; var _tot = _pl.options.reduce(function(s,o){return s+(o.count||0);},0); var _vn = (_pl.voters!=null?_pl.voters:_tot); %>
    139       <% if (!_pl.closed) { %>
    140         <form method="post" action="/authorize_interaction/vote" class="auth-poll">
    141           <input type="hidden" name="uri" value="<%= uri %>">
    142           <% _pl.options.forEach(function(o){ %>
    143             <label class="auth-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label>
    144           <% }); %>
    145           <div class="auth-interact-actions">
    146             <button type="submit" class="btn btn-primary"><%= t('poll.vote') %></button>
    147           </div>
    148           <p class="auth-poll-foot"><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.open') %><% if (_pl.endTime) { %> · <%= t('poll.closes') %> <%= new Date(_pl.endTime).toLocaleString() %><% } %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></p>
    149         </form>
    150       <% } else { %>
    151         <div class="auth-poll">
    152           <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %>
    153             <div class="auth-poll-res"><span class="auth-poll-fill" style="width:<%= _pct %>%"></span><span class="auth-poll-name"><%= o.name %></span><span class="auth-poll-pct"><%= _pct %>%</span></div>
    154           <% }); %>
    155           <p class="auth-poll-foot"><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.closed') %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></p>
    156         </div>
    157       <% } %>
    158     <% } %>
    159     <% var _liked = (typeof reacted !== 'undefined' && reacted.liked); var _boosted = (typeof reacted !== 'undefined' && reacted.boosted); %>
    160     <div class="auth-interact-react">
    161       <form method="post" action="/authorize_interaction/like" class="fedi-react-form">
    162         <input type="hidden" name="uri" value="<%= uri %>">
    163         <button type="submit" class="fedi-bigact fedi-bigact-like<%= _liked ? ' is-on' : '' %>" data-on="<%= t('fedi.unlike_short') %>" data-off="<%= t('fedi.like_short') %>">
    164           <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg>
    165           <span class="fedi-bigact-label"><%= _liked ? t('fedi.unlike_short') : t('fedi.like_short') %></span>
    166         </button>
    167       </form>
    168       <form method="post" action="/authorize_interaction/boost" class="fedi-react-form">
    169         <input type="hidden" name="uri" value="<%= uri %>">
    170         <button type="submit" class="fedi-bigact fedi-bigact-boost<%= _boosted ? ' is-on' : '' %>" data-on="<%= t('tl.unboost') %>" data-off="<%= t('fedi.boost_short') %>">
    171           <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>
    172           <span class="fedi-bigact-label"><%= _boosted ? t('tl.unboost') : t('fedi.boost_short') %></span>
    173         </button>
    174       </form>
    175     </div>
    176     <% if (locals.mayReply !== false) { %>
    177     <p class="auth-interact-or"><%= t('fedi.or_reply') %></p>
    178     <p class="auth-interact-where">💬 <%= t('fedi.reply_where') %></p>
    179     <div class="auth-interact-form">
    180       <%- include('../partials/reply-editor', {
    181         action: '/authorize_interaction',
    182         hiddenFields: [{ name: 'uri', value: uri }],
    183         placeholder: t('fedi.reply_ph'),
    184         submitLabel: t('fedi.send'),
    185         rows: 4,
    186         participants: (target && target.actor_uri) ? [{
    187           uri: target.actor_uri,
    188           url: target.actor_url || target.actor_uri,
    189           handle: target.actor_handle || '',
    190         }] : [],
    191       }) %>
    192       <div class="auth-interact-actions"><a href="<%= uri %>" class="btn"><%= t('comments.cancel') %></a></div>
    193     </div>
    194     <% } %>
    195     <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
    196     <details class="auth-report">
    197       <summary>🚩 <%= t('fedi.report_open') %></summary>
    198       <form method="post" action="/authorize_interaction/report" class="auth-interact-form">
    199         <input type="hidden" name="uri" value="<%= uri %>">
    200         <input type="hidden" name="actor_uri" value="<%= target.actor_uri %>">
    201         <p class="auth-report-note"><%= t('fedi.report_where') %></p>
    202         <textarea name="reason" rows="3" maxlength="3000" placeholder="<%= t('fedi.report_ph') %>"></textarea>
    203         <div class="auth-interact-actions">
    204           <button type="submit" class="btn btn-danger"><%= t('fedi.report_send') %></button>
    205         </div>
    206       </form>
    207     </details>
     108    <%- include('../partials/remote-object', { target: target, uri: uri, reacted: (typeof reacted !== 'undefined' ? reacted : { liked: false, boosted: false }), siteTitle: siteTitle, cancelUrl: uri }) %>
    208109  <% } %>
    209110  <% } %>
    210111</div>
    211112
    212 <style>
    213   .auth-interact { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
    214   .auth-interact-title { margin: 0 0 1rem; }
    215   .auth-interact-meta { color: var(--ink-soft, #888); margin: 0 0 .75rem; }
    216   .auth-interact-preview {
    217     margin: 0 0 1rem; padding: .75rem 1rem; border-left: 3px solid var(--accent, #888);
    218     background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border-radius: 0 12px 12px 0;
    219     color: var(--ink, inherit);
    220   }
    221   .auth-interact-form { display: flex; flex-direction: column; gap: .75rem; }
    222   .auth-interact-form textarea, .auth-interact-form input[type=url] {
    223     width: 100%; box-sizing: border-box; padding: .6rem .8rem; font: inherit; resize: vertical;
    224     border-radius: 12px; border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent);
    225     background: var(--paper, #fff); color: var(--ink, #000);
    226   }
    227   .auth-interact-actions { display: flex; gap: .5rem; align-items: center; }
    228   .auth-interact-note { color: var(--ink-soft, #999); font-size: .85rem; margin-top: 1rem; }
    229   .auth-interact-where { margin: .25rem 0 .85rem; padding: .6rem .8rem; font-size: .85rem; line-height: 1.5; border-radius: 10px; background: color-mix(in srgb, var(--accent, #888) 12%, transparent); color: var(--ink, inherit); }
    230   .auth-interact-content { line-height: 1.55; }
    231   .auth-interact-content p { margin: .35rem 0; }
    232   .auth-interact-content p:first-child { margin-top: 0; }
    233   .auth-interact-img { max-width: 100%; height: auto; border-radius: 10px; margin: .5rem 0 0; display: block; }
    234   .auth-interact-orig { margin: .75rem 0 0; font-size: .85rem; }
    235   .auth-interact-orig a { color: var(--accent, #06c); }
    236   .auth-view-src { display: inline-flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem; padding: .6rem 1rem;
    237     border: 1px solid color-mix(in srgb, var(--accent, #888) 45%, transparent); border-radius: 12px;
    238     background: color-mix(in srgb, var(--accent, #888) 10%, transparent); color: var(--accent, #06c);
    239     text-decoration: none; font-weight: 600; font-size: .92rem; transition: background .15s, border-color .15s; }
    240   .auth-view-src:hover { background: color-mix(in srgb, var(--accent, #888) 18%, transparent); border-color: var(--accent, #888); }
    241   .auth-interact-react { display: flex; gap: .6rem; flex-wrap: wrap; margin: 0 0 .25rem; }
    242   .auth-interact-react form { margin: 0; }
    243   .fedi-bigact { display: inline-flex; align-items: center; gap: .5rem; padding: .55rem 1.1rem; border-radius: 999px;
    244     border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent);
    245     background: color-mix(in srgb, var(--ink, #000) 4%, transparent);
    246     color: var(--ink, inherit); font: inherit; font-weight: 600; font-size: .92rem; cursor: pointer; white-space: nowrap;
    247     transition: background .12s, border-color .12s; }
    248   .fedi-bigact svg { width: 17px; height: 17px; flex: 0 0 17px; }
    249   .fedi-bigact-like svg { color: #e8b04b; }
    250   .fedi-bigact-like:hover, .fedi-bigact-like.is-on { background: color-mix(in srgb, #e8b04b 16%, transparent); border-color: color-mix(in srgb, #e8b04b 55%, transparent); }
    251   .fedi-bigact-boost svg { color: #2fa85a; }
    252   .fedi-bigact-boost:hover, .fedi-bigact-boost.is-on { background: color-mix(in srgb, #2fa85a 16%, transparent); border-color: color-mix(in srgb, #2fa85a 55%, transparent); }
    253   .fedi-manage { list-style: none; padding: 0; margin: 1.25rem 0 1rem; display: flex; flex-direction: column; gap: .7rem; }
    254   .fedi-manage-item { padding: .9rem 1rem; border-radius: 14px; background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent); border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent); transition: border-color .15s ease, box-shadow .15s ease; }
    255   .fedi-manage-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 28%, transparent); box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
    256   .fedi-manage-head { display: flex; align-items: center; gap: .5rem; margin: 0 0 .5rem; }
    257   .fedi-manage-icon { flex: 0 0 26px; width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
    258     background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); }
    259   .fedi-manage-icon svg { width: 14px; height: 14px; }
    260   .fedi-manage-to { font-weight: 600; color: var(--ink, inherit); font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
    261   .fedi-manage-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
    262   .fedi-manage-content { line-height: 1.55; overflow-wrap: anywhere; padding-left: calc(26px + .5rem); }
    263   .fedi-manage-content p { margin: .15rem 0; }
    264   .fedi-manage-content p:first-child { margin-top: 0; }
    265   .fedi-manage-content p:last-child { margin-bottom: 0; }
    266   .fedi-manage-foot { display: flex; align-items: center; justify-content: flex-start; flex-wrap: wrap; gap: .5rem; margin-top: .65rem; }
    267   .fedi-goto-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, var(--accent, #888) 45%, transparent); background: color-mix(in srgb, var(--accent, #888) 12%, transparent); color: var(--accent, #06c); border-radius: 999px; padding: .3rem .8rem; font-size: .82rem; text-decoration: none; transition: background .15s ease; }
    268   .fedi-goto-btn:hover { background: color-mix(in srgb, var(--accent, #888) 22%, transparent); }
    269   .fedi-del-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, #d9534f 45%, transparent); background: color-mix(in srgb, #d9534f 12%, transparent); color: #e06b66; border-radius: 999px; padding: .3rem .8rem; font: inherit; font-size: .82rem; cursor: pointer; transition: background .15s ease; }
    270   .fedi-del-btn:hover { background: color-mix(in srgb, #d9534f 24%, transparent); }
    271   .fedi-bm { margin: 0 0 1.5rem; padding: 1rem; border-radius: 14px;
    272     border: 1px dashed color-mix(in srgb, var(--accent, #888) 45%, transparent);
    273     background: color-mix(in srgb, var(--accent, #888) 7%, transparent); }
    274   .fedi-bm-btn { display: inline-flex; align-items: center; gap: .45rem; padding: .5rem 1rem; border-radius: 999px;
    275     background: var(--accent, #06c); color: #fff; font: inherit; font-weight: 600; font-size: .9rem; text-decoration: none;
    276     cursor: grab; user-select: none; box-shadow: 0 2px 8px color-mix(in srgb, var(--accent, #000) 30%, transparent); }
    277   .fedi-bm-btn:active { cursor: grabbing; }
    278   .fedi-bm-btn.nudge { animation: fedi-bm-nudge .6s ease; }
    279   @keyframes fedi-bm-nudge { 0%,100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
    280   .fedi-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .6rem 0 0; }
    281   .fedi-edit-btn { display: inline-flex; align-items: center; gap: .35rem; border: 1px solid color-mix(in srgb, var(--ink, #888) 28%, transparent);
    282     background: color-mix(in srgb, var(--ink, #888) 8%, transparent); color: var(--ink-soft, #aaa); border-radius: 999px; padding: .3rem .8rem;
    283     font: inherit; font-size: .82rem; cursor: pointer; transition: background .15s ease, color .15s ease; }
    284   .fedi-edit-btn:hover { background: color-mix(in srgb, var(--ink, #888) 16%, transparent); }
    285   .fedi-edit-btn.is-open { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
    286   .fedi-del-form { margin-left: auto; }
    287   .fedi-edit-form { display: none; margin: .55rem 0 0; }
    288   .fedi-edit-form.is-open { display: block; }
    289   .fedi-edit-ta { width: 100%; box-sizing: border-box; min-height: 70px; resize: vertical; padding: .55rem .7rem;
    290     border: 1.5px solid var(--rule, #333); border-radius: 10px; background: var(--paper-2, #14141a); color: var(--ink, #eee); font: inherit; font-size: .9rem; }
    291   .fedi-edit-ta:focus { outline: none; border-color: var(--accent); }
    292   .fedi-edit-actions { display: flex; justify-content: flex-end; margin-top: .5rem; }
    293   .fedi-edit-save { padding: .4rem 1rem; border: 0; border-radius: 999px; background: var(--accent); color: var(--paper, #fff);
    294     font: inherit; font-weight: 600; font-size: .82rem; cursor: pointer; }
    295   /* Poll ballot / results on the interact page */
    296   .auth-poll { display: flex; flex-direction: column; gap: 8px; margin: 0 0 1rem; }
    297   .auth-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer;
    298     border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
    299   .auth-poll-choice:hover { border-color: var(--accent); }
    300   .auth-poll-choice input { accent-color: var(--accent); }
    301   .auth-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; display: flex; align-items: center; gap: 8px;
    302     background: color-mix(in srgb, var(--ink, #000) 6%, transparent); }
    303   .auth-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
    304   .auth-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
    305   .auth-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
    306   .auth-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); margin: .1rem 0 0; }
    307   .auth-report { margin: .6rem 0 0; }
    308   .auth-report summary { cursor: pointer; font-size: .85rem; color: var(--ink-soft, #888); }
    309   .auth-report summary:hover, .auth-report[open] summary { color: #c0392b; }
    310   .auth-report[open] summary { margin-bottom: .5rem; }
    311   .auth-report-note { font-size: .82rem; color: var(--ink-muted, #999); margin: 0 0 .5rem; }
    312 </style>
    313 
    314 
     113<%- include('../partials/fedi-interact-styles') %>
  • src/views/pages/search.ejs

    r407353e r3ceaef6  
    2626      <%= t(total === 1 ? 'search.results_one' : 'search.results_other', { n: total, q: query }) %>
    2727    </p>
     28  <% } %>
     29
     30  <%# Een geplakt adres is geen zoekterm maar een aanwijzing (shaer-utpi): het
     31      object van de andere server staat bovenaan, met dezelfde knoppen als op
     32      /authorize_interaction -- dezelfde partial, dus één render en één plek
     33      waar een poort of een sanitizer kan wonen. %>
     34  <% if (typeof remoteUri !== 'undefined' && remoteUri) { %>
     35    <h2 class="search-section-title"><%= t('search.section_fediverse') %></h2>
     36    <div class="auth-interact search-remote">
     37      <% if (!mayLookup) { %>
     38        <%# Ophalen is een uitgaand verzoek namens deze server; dat doen we niet
     39            op verzoek van een willekeurige bezoeker. %>
     40        <p class="auth-interact-note"><%= t('search.fedi_signin') %></p>
     41      <% } else if (remote && remoteKind === 'note') { %>
     42        <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search' }) %>
     43      <% } else if (remote && remoteKind === 'actor') { %>
     44        <%- include('../partials/remote-actor', { actor: remote, cancelUrl: _base + '/search', siteTitle: siteTitle }) %>
     45      <% } else { %>
     46        <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p>
     47      <% } %>
     48    </div>
     49    <%- include('../partials/fedi-interact-styles') %>
    2850  <% } %>
    2951
Note: See TracChangeset for help on using the changeset viewer.