Changeset 9eb6e1b in Klonkt for src/views


Ignore:
Timestamp:
09/30/2026 06:41:45 AM (4 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@…>

Location:
src/views
Files:
1 added
7 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/authorize-interaction.ejs

    r3ceaef6 r9eb6e1b  
    106106    </form>
    107107  <% } else { %>
    108     <%- include('../partials/remote-object', { target: target, uri: uri, reacted: (typeof reacted !== 'undefined' ? reacted : { liked: false, boosted: false }), siteTitle: siteTitle, cancelUrl: uri }) %>
     108    <%- include('../partials/remote-object', { target: target, uri: uri, reacted: (typeof reacted !== 'undefined' ? reacted : { liked: false, boosted: false }), siteTitle: siteTitle, replyOpen: typeof replyOpen !== 'undefined' && replyOpen, reportOpen: typeof reportOpen !== 'undefined' && reportOpen }) %>
    109109  <% } %>
    110110  <% } %>
  • src/views/pages/search.ejs

    r3ceaef6 r9eb6e1b  
    4040        <p class="auth-interact-note"><%= t('search.fedi_signin') %></p>
    4141      <% } else if (remote && remoteKind === 'note') { %>
    42         <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search' }) %>
     42        <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search?q=' + encodeURIComponent(query) }) %>
    4343      <% } else if (remote && remoteKind === 'actor') { %>
    44         <%- include('../partials/remote-actor', { actor: remote, cancelUrl: _base + '/search', siteTitle: siteTitle }) %>
     44        <%- include('../partials/remote-actor', { actor: remote, siteTitle: siteTitle }) %>
    4545      <% } else { %>
    4646        <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p>
  • src/views/partials/fedi-interact-styles.ejs

    r3ceaef6 r9eb6e1b  
    77  .auth-interact { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
    88  .auth-interact-title { margin: 0 0 1rem; }
    9   .auth-interact-meta { color: var(--ink-soft, #888); margin: 0 0 .75rem; }
    10   .auth-interact-preview {
    11     margin: 0 0 1rem; padding: .75rem 1rem; border-left: 3px solid var(--accent, #888);
    12     background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border-radius: 0 12px 12px 0;
    13     color: var(--ink, inherit);
    14   }
    159  .auth-interact-form { display: flex; flex-direction: column; gap: .75rem; }
    1610  .auth-interact-form textarea, .auth-interact-form input[type=url] {
    … …  
    2115  .auth-interact-actions { display: flex; gap: .5rem; align-items: center; }
    2216  .auth-interact-note { color: var(--ink-soft, #999); font-size: .85rem; margin-top: 1rem; }
    23   .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); }
    24   .auth-interact-content { line-height: 1.55; }
    25   .auth-interact-content p { margin: .35rem 0; }
    26   .auth-interact-content p:first-child { margin-top: 0; }
    27   .auth-interact-img { max-width: 100%; height: auto; border-radius: 10px; margin: .5rem 0 0; display: block; }
    28   .auth-interact-orig { margin: .75rem 0 0; font-size: .85rem; }
    29   .auth-interact-orig a { color: var(--accent, #06c); }
    30   .auth-view-src { display: inline-flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem; padding: .6rem 1rem;
    31     border: 1px solid color-mix(in srgb, var(--accent, #888) 45%, transparent); border-radius: 12px;
    32     background: color-mix(in srgb, var(--accent, #888) 10%, transparent); color: var(--accent, #06c);
    33     text-decoration: none; font-weight: 600; font-size: .92rem; transition: background .15s, border-color .15s; }
    34   .auth-view-src:hover { background: color-mix(in srgb, var(--accent, #888) 18%, transparent); border-color: var(--accent, #888); }
    35   .auth-interact-react { display: flex; gap: .6rem; flex-wrap: wrap; margin: 0 0 .25rem; }
    36   .auth-interact-react form { margin: 0; }
    37   .fedi-bigact { display: inline-flex; align-items: center; gap: .5rem; padding: .55rem 1.1rem; border-radius: 999px;
    38     border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent);
    39     background: color-mix(in srgb, var(--ink, #000) 4%, transparent);
    40     color: var(--ink, inherit); font: inherit; font-weight: 600; font-size: .92rem; cursor: pointer; white-space: nowrap;
    41     transition: background .12s, border-color .12s; }
    42   .fedi-bigact svg { width: 17px; height: 17px; flex: 0 0 17px; }
    43   .fedi-bigact-like svg { color: #e8b04b; }
    44   .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); }
    45   .fedi-bigact-boost svg { color: #2fa85a; }
    46   .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); }
    4717  .fedi-manage { list-style: none; padding: 0; margin: 1.25rem 0 1rem; display: flex; flex-direction: column; gap: .7rem; }
    4818  .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; }
    … …  
    8757  .fedi-edit-save { padding: .4rem 1rem; border: 0; border-radius: 999px; background: var(--accent); color: var(--paper, #fff);
    8858    font: inherit; font-weight: 600; font-size: .82rem; cursor: pointer; }
    89   /* Poll ballot / results on the interact page */
    90   .auth-poll { display: flex; flex-direction: column; gap: 8px; margin: 0 0 1rem; }
    91   .auth-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer;
     59
     60  /* ── De kaart van een post of profiel van een andere server ──────────────
     61     Eén kaart voor de live-preview, de zoekpagina en /authorize_interaction.
     62     De telefoon is het uitgangspunt: knoppen van minstens 44 punten, en op
     63     een smal scherm vallen de woorden weg en blijven de tekens staan. */
     64  .rp-card { position: relative; padding: 14px 16px 4px; border-radius: 16px;
     65    border: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent);
     66    background: color-mix(in srgb, var(--ink, #000) 2.5%, var(--paper, transparent));
     67    color: var(--ink, inherit); overflow: hidden; }
     68  .rp-head { display: flex; align-items: center; gap: 10px; min-width: 0; margin: 0 0 10px; }
     69  .rp-avatar { flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%; object-fit: cover;
     70    background: color-mix(in srgb, var(--ink, #000) 8%, transparent); }
     71  .rp-profile .rp-avatar { flex-basis: 56px; width: 56px; height: 56px; }
     72  .rp-avatar-empty { display: inline-flex; align-items: center; justify-content: center;
     73    font-weight: 700; color: var(--ink-soft, #888); }
     74  .rp-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
     75  .rp-name { font-weight: 700; color: var(--ink, inherit); text-decoration: none;
     76    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     77  .rp-name:hover { text-decoration: underline; }
     78  .rp-handle { font-size: .82rem; color: var(--ink-soft, #888); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
     79
     80  .rp-body { line-height: 1.55; overflow-wrap: anywhere; }
     81  .rp-body p { margin: .45rem 0; }
     82  .rp-body > :first-child { margin-top: 0; }
     83  .rp-body > :last-child { margin-bottom: 0; }
     84  .rp-body a { color: var(--accent, #06c); }
     85  .rp-body img { max-width: 100%; height: auto; }
     86
     87  /* Beeld als raster: één foto op volle breedte, meer als tegels, zodat een
     88     lange reeks de knoppen niet van het scherm duwt. */
     89  /* Vaste rijhoogtes in plaats van verhoudingen: met verhoudingen groeit het
     90     raster mee met de breedte, en werd op een desktop de eerste van drie foto's
     91     zo hoog dat de knoppen onder de vouw van de uitklaplijst verdwenen. */
     92  .rp-media { display: grid; gap: 3px; margin: 10px 0 0; border-radius: 12px; overflow: hidden; }
     93  .rp-media a { display: block; min-width: 0; }
     94  .rp-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
     95  .rp-media-1 img { height: auto; max-height: 480px; }
     96  .rp-media-2, .rp-media-3, .rp-media-4 { grid-template-columns: 1fr 1fr; grid-auto-rows: clamp(120px, 28vw, 220px); }
     97  .rp-media-3 a:first-child { grid-column: 1 / -1; }
     98  .rp-compact .rp-media-1 img { max-height: 240px; }
     99  .rp-compact .rp-media-2, .rp-compact .rp-media-3, .rp-compact .rp-media-4 { grid-auto-rows: 110px; }
     100
     101  /* Een inhoudswaarschuwing: dicht tot je ervoor kiest. */
     102  .rp-cw > summary { list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 10px;
     103    min-height: 44px; padding: 8px 12px; border-radius: 10px; cursor: pointer; font-weight: 600;
     104    background: color-mix(in srgb, #e8b04b 12%, transparent); border: 1px solid color-mix(in srgb, #e8b04b 35%, transparent); }
     105  .rp-cw > summary::-webkit-details-marker { display: none; }
     106  .rp-cw-text { min-width: 0; overflow-wrap: anywhere; }
     107  .rp-cw-open { flex: 0 0 auto; font-size: .8rem; color: var(--accent, #06c); }
     108  .rp-cw[open] > summary { margin: 0 0 10px; }
     109  .rp-cw[open] .rp-cw-open { display: none; }
     110
     111  /* De rij knoppen: gelijke tekens, het origineel rechts. */
     112  .rp-actions { display: flex; align-items: center; gap: 2px; margin: 10px -8px 0; padding: 2px 0;
     113    border-top: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent); }
     114  .rp-actions form { margin: 0; }
     115  .rp-act { display: inline-flex; align-items: center; justify-content: center; gap: 6px;
     116    min-width: 44px; min-height: 44px; padding: 0 12px; border: 0; border-radius: 999px;
     117    background: transparent; color: var(--ink-soft, #888); font: inherit; font-size: .88rem; font-weight: 600;
     118    text-decoration: none; cursor: pointer; white-space: nowrap; -webkit-tap-highlight-color: transparent;
     119    transition: background .12s, color .12s; }
     120  .rp-act svg { flex: 0 0 18px; width: 18px; height: 18px; }
     121  .rp-act:hover { background: color-mix(in srgb, var(--ink, #000) 6%, transparent); color: var(--ink, inherit); }
     122  .rp-act:focus-visible { outline: 2px solid var(--accent, #06c); outline-offset: 2px; }
     123  .rp-act[disabled] { opacity: .55; cursor: progress; }
     124  .rp-like.is-on { color: #d99a1e; }
     125  .rp-like.is-on svg { fill: currentColor; }
     126  .rp-boost.is-on { color: #2fa85a; }
     127  .rp-reply-btn[aria-expanded="true"] { color: var(--accent, #06c); }
     128  .rp-src { margin-left: auto; }
     129  .rp-primary { background: var(--accent, #06c); color: var(--paper, #fff); }
     130  .rp-primary:hover { background: var(--accent, #06c); color: var(--paper, #fff); filter: brightness(1.08); }
     131  .rp-profile .rp-actions { border-top: 0; margin-top: 4px; padding-bottom: 8px; }
     132  .rp-profile .rp-actions form { flex: 0 0 auto; }
     133
     134  /* Een peiling in de kaart. */
     135  .rp-poll { display: flex; flex-direction: column; gap: 6px; margin: 12px 0 0; }
     136  .rp-poll-choice { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; border-radius: 10px; cursor: pointer;
    92137    border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
    93   .auth-poll-choice:hover { border-color: var(--accent); }
    94   .auth-poll-choice input { accent-color: var(--accent); }
    95   .auth-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; display: flex; align-items: center; gap: 8px;
     138  .rp-poll-choice:hover { border-color: var(--accent, #06c); }
     139  .rp-poll-choice input { accent-color: var(--accent, #06c); }
     140  .rp-poll-res { position: relative; display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-radius: 10px; overflow: hidden;
    96141    background: color-mix(in srgb, var(--ink, #000) 6%, transparent); }
    97   .auth-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
    98   .auth-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
    99   .auth-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
    100   .auth-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); margin: .1rem 0 0; }
    101   .auth-report { margin: .6rem 0 0; }
    102   .auth-report summary { cursor: pointer; font-size: .85rem; color: var(--ink-soft, #888); }
    103   .auth-report summary:hover, .auth-report[open] summary { color: #c0392b; }
    104   .auth-report[open] summary { margin-bottom: .5rem; }
    105   .auth-report-note { font-size: .82rem; color: var(--ink-muted, #999); margin: 0 0 .5rem; }
    106   /* Het gezicht van een profiel dat we net hebben opgehaald. */
    107   .remote-actor { display: flex; align-items: center; gap: .75rem; }
    108   .remote-actor .auth-interact-meta { margin: 0; }
    109   .remote-actor-icon { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; flex: 0 0 48px;
    110     background: color-mix(in srgb, var(--ink, #000) 8%, transparent); }
     142  .rp-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent, #06c) 22%, transparent); }
     143  .rp-poll-name { position: relative; flex: 1; }
     144  .rp-poll-pct { position: relative; font-variant-numeric: tabular-nums; font-weight: 600; }
     145  .rp-poll-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap;
     146    font-size: .82rem; color: var(--ink-soft, #888); }
     147
     148  /* Wat er onder de kaart staat, op de volle pagina. */
     149  .rp-reply { margin: 12px 0 0; }
     150  .rp-reply-where { margin: 0 0 8px; font-size: .85rem; line-height: 1.5; color: var(--ink-soft, #888); }
     151  .rp-reply-foot { display: flex; justify-content: flex-end; margin-top: 4px; }
     152  .rp-as { margin: 12px 2px 0; font-size: .8rem; color: var(--ink-soft, #999); }
     153  .rp-report { margin: 6px 2px 0; }
     154  .rp-report > summary { display: inline-block; padding: 8px 0; cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); }
     155  .rp-report > summary:hover, .rp-report[open] > summary { color: #c0392b; }
     156  .rp-report-form { display: flex; flex-direction: column; gap: .6rem; }
     157  .rp-report-note { margin: 0; font-size: .82rem; color: var(--ink-soft, #999); }
     158  .rp-report-form textarea { width: 100%; box-sizing: border-box; padding: .6rem .8rem; font: inherit; resize: vertical; border-radius: 12px;
     159    border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: var(--paper, #fff); color: var(--ink, #000); }
     160
     161  /* De live-preview: open-link onder de kaart, en de kaart zonder rand-marge. */
     162  .rp-open { display: block; padding: 10px 4px 2px; text-align: right; font-weight: 600; font-size: .88rem;
     163    color: var(--accent, #06c); text-decoration: none; }
     164  .rp-open:hover { text-decoration: underline; }
     165  .ss-remote { padding: 4px 0 6px; }
     166  .ss-remote-note { padding: .8rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
     167  .ss-remote-loading { display: flex; align-items: center; gap: 10px; padding: .9rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
     168  .ss-remote-loading::before { content: ''; width: 16px; height: 16px; border-radius: 50%; flex: 0 0 16px;
     169    border: 2px solid color-mix(in srgb, var(--accent, #06c) 30%, transparent); border-top-color: var(--accent, #06c);
     170    animation: ss-spin .8s linear infinite; }
     171  @keyframes ss-spin { to { transform: rotate(360deg); } }
     172  @media (prefers-reduced-motion: reduce) { .ss-remote-loading::before { animation: none; } }
     173
     174  /* Smal scherm: alleen de tekens, gelijk verdeeld over de breedte. De woorden
     175     blijven bestaan voor wie het scherm voorgelezen krijgt. */
     176  @media (max-width: 480px) {
     177    .rp-card { padding: 12px 12px 2px; border-radius: 14px; }
     178    .rp-actions { margin: 8px -4px 0; justify-content: space-between; }
     179    .rp-act .rp-act-label { position: absolute; width: 1px; height: 1px; overflow: hidden;
     180      clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
     181    .rp-src { margin-left: 0; }
     182    .rp-media-1 img { max-height: 360px; }
     183  }
    111184</style>
  • src/views/partials/remote-actor.ejs

    r3ceaef6 r9eb6e1b  
    22  Een PERSOON van een andere server: wie het is, en de knop om te volgen.
    33
    4   Zelfde reden als remote-object: de zoekbalk en /authorize_interaction laten
    5   hetzelfde zien, dus staat het op één plek. Het volgen zelf gaat naar
    6   /authorize_interaction/follow, want daar zit de poort van de guardians
    7   (shaer-p729) en die hoort niet twee keer geschreven te worden.
     4  Dezelfde kaart als een post (remote-object), zodat een profiel en een post in
     5  de zoekbalk er als familie uitzien en niet als twee formulieren. Het volgen
     6  zelf gaat naar /authorize_interaction/follow, want daar zit de poort van de
     7  guardians (shaer-p729) en die hoort niet twee keer geschreven te worden.
    88
    9   Verwacht: actor (uit resolveRemoteActor). Optioneel: cancelUrl, siteTitle.
     9  Verwacht: actor (uit resolveRemoteActor). Optioneel: compact (de
     10  live-preview), siteTitle.
    1011%>
    11 <% const _annuleer = (typeof cancelUrl !== 'undefined' && cancelUrl) ? cancelUrl : '/'; %>
    12 <div class="remote-actor">
    13   <% if (actor.actor_icon) { %>
    14     <img class="remote-actor-icon" src="<%= actor.actor_icon %>" alt="" loading="lazy" width="48" height="48">
    15   <% } %>
    16   <p class="auth-interact-meta">
    17     <%= t('fedi.follow_intro') %>
    18     <a href="<%= actor.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= actor.actor_name %></strong></a>
    19     <span class="fedi-handle"><%= actor.actor_handle %></span>
    20   </p>
    21 </div>
    22 <form method="post" action="/authorize_interaction/follow" class="auth-interact-form">
    23   <input type="hidden" name="uri" value="<%= actor.actor_uri %>">
    24   <div class="auth-interact-actions">
    25     <button type="submit" class="btn btn-primary"><%= t('fedi.follow_btn') %></button>
    26     <a href="<%= _annuleer %>" class="btn"><%= t('comments.cancel') %></a>
    27   </div>
    28 </form>
    29 <p class="auth-interact-note"><%= t('fedi.remote_as', { site: typeof siteTitle !== 'undefined' ? siteTitle : '' }) %></p>
     12<%
     13  const _compact = !!locals.compact;
     14  const _initial = String(actor.actor_name || actor.actor_handle || '?').replace(/^@/, '').slice(0, 1).toUpperCase();
     15%>
     16<article class="rp-card rp-profile<%= _compact ? ' rp-compact' : '' %>">
     17  <header class="rp-head">
     18    <% if (actor.actor_icon) { %>
     19      <img class="rp-avatar" src="<%= actor.actor_icon %>" alt="" width="56" height="56" loading="lazy">
     20    <% } else { %>
     21      <span class="rp-avatar rp-avatar-empty" aria-hidden="true"><%= _initial %></span>
     22    <% } %>
     23    <div class="rp-who">
     24      <a class="rp-name" href="<%= actor.actor_url || actor.actor_uri %>" rel="nofollow noopener" target="_blank"><%= actor.actor_name || actor.actor_handle %></a>
     25      <span class="rp-handle"><%= actor.actor_handle %></span>
     26    </div>
     27  </header>
     28  <footer class="rp-actions">
     29    <form method="post" action="/authorize_interaction/follow">
     30      <input type="hidden" name="uri" value="<%= actor.actor_uri %>">
     31      <button type="submit" class="rp-act rp-primary">
     32        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg>
     33        <span><%= t('fedi.follow_btn') %></span>
     34      </button>
     35    </form>
     36    <a class="rp-act rp-src" href="<%= actor.actor_url || actor.actor_uri %>" target="_blank" rel="nofollow noopener" title="<%= t('fedi.view_profile') %>">
     37      <svg viewBox="0 0 24 24" 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>
     38      <span class="rp-act-label"><%= t('fedi.profile_short') %></span>
     39    </a>
     40  </footer>
     41</article>
     42<% if (!_compact) { %>
     43  <p class="rp-as"><%= t('fedi.remote_as', { site: typeof siteTitle !== 'undefined' ? siteTitle : '' }) %></p>
     44<% } %>
  • src/views/partials/remote-object.ejs

    r3ceaef6 r9eb6e1b  
    11<%#
    2   Een object van een andere server: hoe het eruit ziet, en wat je ermee kunt.
     2  Een post van een andere server: hoe hij eruit ziet, en wat je ermee kunt.
    33
    4   EEN PLEK, TWEE INGANGEN (de zoekbalk en /authorize_interaction). Dit stond
    5   alleen op die tweede pagina, en toen de zoekfunctie een URL leerde herkennen
    6   zou het een tweede render worden -- en daarmee een tweede plek waar een poort,
    7   een sanitizer of een knop vergeten kan worden. De formulieren posten dus ook
    8   vanaf de zoekpagina naar /authorize_interaction/..., want daar zit het werk en
    9   daar zit de rechtencontrole.
     4  EEN PLEK, DRIE INGANGEN: de live-preview onder de zoekbalk, de zoekpagina en
     5  /authorize_interaction. Eén render, zodat een poort, een sanitizer of een knop
     6  niet op één plek vergeten kan worden. De formulieren posten overal naar
     7  /authorize_interaction/..., want daar zit het werk en de rechtencontrole.
     8
     9  Hij ziet eruit als een POST, niet als een formulier over een post: kop met
     10  gezicht, de inhoud, en één rij knoppen eronder. Wat daar vroeger omheen stond
     11  (een zin "je reageert op", een aparte knop naar het origineel, twee grote
     12  pillen, "of reageer:" en een altijd open antwoordvenster) is teruggebracht tot
     13  die ene rij. Antwoorden klapt pas open als je erom vraagt, melden staat
     14  onderaan en niet in de weg.
    1015
    1116  Verwacht: target (uit resolveRemoteNote), uri, reacted {liked,boosted}.
    12   Optioneel: mayReply (false = alleen lezen en reageren, geen antwoordvenster),
    13   cancelUrl (waar Annuleren heen gaat, standaard het origineel), siteTitle.
     17  Optioneel:
     18    compact    de live-preview: geen antwoordvenster en geen meldformulier; die
     19               twee staan een tik verder op de volle pagina. Een editor in een
     20               uitklaplijst is op een telefoon geen plek om te schrijven.
     21    mayReply   false = alleen lezen en reageren, geen antwoordknop
     22    replyOpen  het antwoordvenster staat al open (?reply=1, de terugval zonder JS)
     23    reportOpen idem voor melden (?report=1)
     24    cancelUrl  waar Annuleren heen gaat zonder JS, standaard de volle pagina
     25    siteTitle
    1426%>
    15 <% const _cancel = (typeof cancelUrl !== 'undefined' && cancelUrl) ? cancelUrl : uri; %>
    16     <p class="auth-interact-meta">
    17       <%= t('fedi.remote_replying_to') %>
    18       <a href="<%= target.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= target.actor_name %></strong></a>
    19       <span class="fedi-handle"><%= target.actor_handle %></span>
    20     </p>
    21     <blockquote class="auth-interact-preview">
    22       <% if (target.content) { %><div class="auth-interact-content"><%- target.content %></div>
    23       <% } else if (target.preview) { %><%= target.preview %><% } %>
    24       <% (target.images || []).forEach(function(im){ %>
    25         <img class="auth-interact-img" src="<%= im %>" alt="" loading="lazy">
    26       <% }); %>
    27     </blockquote>
    28     <% if (target.url || uri) { %>
    29       <a class="auth-view-src" href="<%= target.url || uri %>" target="_blank" rel="nofollow noopener">
    30         <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>
    31         <span><%= t('fedi.remote_view_original') %></span>
     27<%
     28  const _compact = !!locals.compact;
     29  const _mayReply = locals.mayReply !== false;
     30  const _full = '/authorize_interaction?uri=' + encodeURIComponent(uri);
     31  const _cancel = locals.cancelUrl || _full;
     32  const _liked = !!(locals.reacted && reacted.liked);
     33  const _boosted = !!(locals.reacted && reacted.boosted);
     34  const _src = target.url || uri;
     35  // Een inhoudswaarschuwing wordt gerespecteerd. De oude render toonde alles
     36  // gewoon open, ook wat de schrijver achter een waarschuwing had gezet.
     37  const _warn = target.cw || (target.sensitive ? t('fedi.sensitive') : '');
     38  const _imgs = (target.images || []).slice(0, 4);
     39  const _initial = String(target.actor_name || target.actor_handle || '?').replace(/^@/, '').slice(0, 1).toUpperCase();
     40%>
     41<article class="rp-card<%= _compact ? ' rp-compact' : '' %>">
     42  <header class="rp-head">
     43    <% if (target.actor_icon) { %>
     44      <img class="rp-avatar" src="<%= target.actor_icon %>" alt="" width="40" height="40" loading="lazy">
     45    <% } else { %>
     46      <span class="rp-avatar rp-avatar-empty" aria-hidden="true"><%= _initial %></span>
     47    <% } %>
     48    <div class="rp-who">
     49      <a class="rp-name" href="<%= target.actor_url || target.actor_uri %>" rel="nofollow noopener" target="_blank"><%= target.actor_name || target.actor_handle %></a>
     50      <span class="rp-handle"><%= target.actor_handle %></span>
     51    </div>
     52  </header>
     53
     54  <% if (_warn) { %>
     55  <details class="rp-cw">
     56    <summary><span class="rp-cw-text"><%= _warn %></span><span class="rp-cw-open"><%= t('fedi.cw_open') %></span></summary>
     57  <% } %>
     58    <% if (target.content) { %>
     59      <div class="rp-body"><%- target.content %></div>
     60    <% } else if (target.preview) { %>
     61      <div class="rp-body"><p><%= target.preview %></p></div>
     62    <% } %>
     63    <% if (_imgs.length) { %>
     64      <div class="rp-media rp-media-<%= _imgs.length %>">
     65        <% _imgs.forEach(function (im) { %>
     66          <a href="<%= im %>" target="_blank" rel="nofollow noopener"><img src="<%= im %>" alt="" loading="lazy"></a>
     67        <% }); %>
     68      </div>
     69    <% } %>
     70  <% if (_warn) { %>
     71  </details>
     72  <% } %>
     73
     74  <% 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); %>
     75    <% if (!_pl.closed) { %>
     76      <form method="post" action="/authorize_interaction/vote" class="rp-poll">
     77        <input type="hidden" name="uri" value="<%= uri %>">
     78        <% _pl.options.forEach(function(o){ %>
     79          <label class="rp-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label>
     80        <% }); %>
     81        <div class="rp-poll-foot">
     82          <span><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %><% if (_pl.endTime) { %> · <%= t('poll.closes') %> <%= new Date(_pl.endTime).toLocaleString() %><% } %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></span>
     83          <button type="submit" class="rp-act rp-primary"><%= t('poll.vote') %></button>
     84        </div>
     85      </form>
     86    <% } else { %>
     87      <div class="rp-poll">
     88        <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %>
     89          <div class="rp-poll-res"><span class="rp-poll-fill" style="width:<%= _pct %>%"></span><span class="rp-poll-name"><%= o.name %></span><span class="rp-poll-pct"><%= _pct %>%</span></div>
     90        <% }); %>
     91        <div class="rp-poll-foot"><span><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.closed') %></span></div>
     92      </div>
     93    <% } %>
     94  <% } %>
     95
     96  <%# Eén rij. Op een smalle telefoon vallen de woorden weg en blijven de
     97      tekens staan, elk minstens 44 punten groot: een duim is geen muis. De
     98      woorden blijven voor een schermlezer wel gewoon bestaan. %>
     99  <footer class="rp-actions">
     100    <% if (_mayReply) { %>
     101      <a class="rp-act rp-reply-btn" href="<%= _full %>&amp;reply=1#rp-reply" data-rp-reply<% if (!_compact) { %> aria-controls="rp-reply" aria-expanded="<%= locals.replyOpen ? 'true' : 'false' %>"<% } %>>
     102        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
     103        <span class="rp-act-label"><%= t('fedi.reply_short') %></span>
    32104      </a>
    33105    <% } %>
    34     <% 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); %>
    35       <% if (!_pl.closed) { %>
    36         <form method="post" action="/authorize_interaction/vote" class="auth-poll">
    37           <input type="hidden" name="uri" value="<%= uri %>">
    38           <% _pl.options.forEach(function(o){ %>
    39             <label class="auth-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label>
    40           <% }); %>
    41           <div class="auth-interact-actions">
    42             <button type="submit" class="btn btn-primary"><%= t('poll.vote') %></button>
    43           </div>
    44           <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>
    45         </form>
    46       <% } else { %>
    47         <div class="auth-poll">
    48           <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %>
    49             <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>
    50           <% }); %>
    51           <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>
    52         </div>
    53       <% } %>
    54     <% } %>
    55     <% var _liked = (typeof reacted !== 'undefined' && reacted.liked); var _boosted = (typeof reacted !== 'undefined' && reacted.boosted); %>
    56     <div class="auth-interact-react">
    57       <form method="post" action="/authorize_interaction/like" class="fedi-react-form">
    58         <input type="hidden" name="uri" value="<%= uri %>">
    59         <button type="submit" class="fedi-bigact fedi-bigact-like<%= _liked ? ' is-on' : '' %>" data-on="<%= t('fedi.unlike_short') %>" data-off="<%= t('fedi.like_short') %>">
    60           <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>
    61           <span class="fedi-bigact-label"><%= _liked ? t('fedi.unlike_short') : t('fedi.like_short') %></span>
    62         </button>
    63       </form>
    64       <form method="post" action="/authorize_interaction/boost" class="fedi-react-form">
    65         <input type="hidden" name="uri" value="<%= uri %>">
    66         <button type="submit" class="fedi-bigact fedi-bigact-boost<%= _boosted ? ' is-on' : '' %>" data-on="<%= t('tl.unboost') %>" data-off="<%= t('fedi.boost_short') %>">
    67           <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>
    68           <span class="fedi-bigact-label"><%= _boosted ? t('tl.unboost') : t('fedi.boost_short') %></span>
    69         </button>
    70       </form>
    71     </div>
    72     <% if (locals.mayReply !== false) { %>
    73     <p class="auth-interact-or"><%= t('fedi.or_reply') %></p>
    74     <p class="auth-interact-where">💬 <%= t('fedi.reply_where') %></p>
    75     <div class="auth-interact-form">
    76       <%- include('./reply-editor', {
    77         action: '/authorize_interaction',
    78         hiddenFields: [{ name: 'uri', value: uri }],
    79         placeholder: t('fedi.reply_ph'),
    80         submitLabel: t('fedi.send'),
    81         rows: 4,
    82         participants: (target && target.actor_uri) ? [{
    83           uri: target.actor_uri,
    84           url: target.actor_url || target.actor_uri,
    85           handle: target.actor_handle || '',
    86         }] : [],
    87       }) %>
    88       <div class="auth-interact-actions"><a href="<%= _cancel %>" class="btn"><%= t('comments.cancel') %></a></div>
    89     </div>
    90     <% } %>
    91     <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
    92     <details class="auth-report">
    93       <summary>🚩 <%= t('fedi.report_open') %></summary>
    94       <form method="post" action="/authorize_interaction/report" class="auth-interact-form">
    95         <input type="hidden" name="uri" value="<%= uri %>">
    96         <input type="hidden" name="actor_uri" value="<%= target.actor_uri %>">
    97         <p class="auth-report-note"><%= t('fedi.report_where') %></p>
    98         <textarea name="reason" rows="3" maxlength="3000" placeholder="<%= t('fedi.report_ph') %>"></textarea>
    99         <div class="auth-interact-actions">
    100           <button type="submit" class="btn btn-danger"><%= t('fedi.report_send') %></button>
    101         </div>
    102       </form>
    103     </details>
     106    <form method="post" action="/authorize_interaction/like" class="fedi-react-form">
     107      <input type="hidden" name="uri" value="<%= uri %>">
     108      <button type="submit" class="rp-act rp-like<%= _liked ? ' is-on' : '' %>" aria-pressed="<%= _liked %>" data-on="<%= t('fedi.unlike_short') %>" data-off="<%= t('fedi.like_short') %>">
     109        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" 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>
     110        <span class="rp-act-label"><%= _liked ? t('fedi.unlike_short') : t('fedi.like_short') %></span>
     111      </button>
     112    </form>
     113    <form method="post" action="/authorize_interaction/boost" class="fedi-react-form">
     114      <input type="hidden" name="uri" value="<%= uri %>">
     115      <button type="submit" class="rp-act rp-boost<%= _boosted ? ' is-on' : '' %>" aria-pressed="<%= _boosted %>" data-on="<%= t('tl.unboost') %>" data-off="<%= t('fedi.boost_short') %>">
     116        <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>
     117        <span class="rp-act-label"><%= _boosted ? t('tl.unboost') : t('fedi.boost_short') %></span>
     118      </button>
     119    </form>
     120    <a class="rp-act rp-src" href="<%= _src %>" target="_blank" rel="nofollow noopener" title="<%= t('fedi.remote_view_original') %>">
     121      <svg viewBox="0 0 24 24" 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>
     122      <span class="rp-act-label"><%= t('fedi.src_short') %></span>
     123    </a>
     124  </footer>
     125</article>
     126
     127<% if (_compact) { %>
     128  <a class="rp-open" href="<%= _full %>"><%= t('fedi.open_full') %> →</a>
     129<% } else { %>
     130  <% if (_mayReply) { %>
     131  <%# Dicht tot je op Reageer tikt. Zonder JS opent ?reply=1 hem, zodat de
     132      knop ook dan ergens heen gaat. %>
     133  <section class="rp-reply" id="rp-reply"<%= locals.replyOpen ? '' : ' hidden' %>>
     134    <p class="rp-reply-where"><%= t('fedi.reply_where') %></p>
     135    <%- include('./reply-editor', {
     136      action: '/authorize_interaction',
     137      hiddenFields: [{ name: 'uri', value: uri }],
     138      placeholder: t('fedi.reply_ph'),
     139      submitLabel: t('fedi.send'),
     140      rows: 4,
     141      participants: (target && target.actor_uri) ? [{
     142        uri: target.actor_uri,
     143        url: target.actor_url || target.actor_uri,
     144        handle: target.actor_handle || '',
     145      }] : [],
     146    }) %>
     147    <div class="rp-reply-foot"><a href="<%= _cancel %>" class="rp-act" data-rp-reply-close><%= t('comments.cancel') %></a></div>
     148  </section>
     149  <% } %>
     150  <p class="rp-as"><%= t('fedi.remote_as', { site: siteTitle }) %></p>
     151  <details class="rp-report" id="rp-report"<%= locals.reportOpen ? ' open' : '' %>>
     152    <summary><%= t('fedi.report_open') %></summary>
     153    <form method="post" action="/authorize_interaction/report" class="rp-report-form">
     154      <input type="hidden" name="uri" value="<%= uri %>">
     155      <input type="hidden" name="actor_uri" value="<%= target.actor_uri %>">
     156      <p class="rp-report-note"><%= t('fedi.report_where') %></p>
     157      <textarea name="reason" rows="3" maxlength="3000" placeholder="<%= t('fedi.report_ph') %>"></textarea>
     158      <div><button type="submit" class="btn btn-danger"><%= t('fedi.report_send') %></button></div>
     159    </form>
     160  </details>
     161<% } %>
  • src/views/partials/topnav.ejs

    r3ceaef6 r9eb6e1b  
    178178<%# De teksten staan op het element dat ze gebruikt, niet in het script: een
    179179    module is een statisch bestand en kan geen vertaling interpoleren. %>
    180 <div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-action="<%= _siteUrlBase %>/search"
    181      data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty') }) %>">
     180<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-remote="<%= _siteUrlBase %>/search/remote" data-action="<%= _siteUrlBase %>/search"
     181     data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty'), looking: t('search.fedi_looking') }) %>">
    182182  <div class="container">
    183183    <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
    … …  
    467467}
    468468/* Live-resultaten dropdown */
    469 .search-suggest { margin-top: .6rem; max-height: 60vh; overflow-y: auto; display: flex; flex-direction: column; gap: .25rem; }
     469.search-suggest { margin-top: .6rem; max-height: 60vh; max-height: 70dvh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; gap: .25rem; }
    470470.search-suggest:empty { display: none; }
    471471.ss-group { padding: .25rem 0; }
  • src/views/shell.ejs

    r3ceaef6 r9eb6e1b  
    130130// ontwikkeling een jaar gecachet, dus zonder bump bereikt een reparatie
    131131// iedereen behalve wie de fout al heeft.
    132 const MOD_V = 64;
     132const MOD_V = 65;
    133133%><!DOCTYPE html>
    134134<html lang="<%- _e(lang) %>" data-palette="<%- _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>">
Note: See TracChangeset for help on using the changeset viewer.