Changeset 9eb6e1b in Klonkt
- Timestamp:
- 09/30/2026 06:41:45 AM (62 minutes ago)
- Branches:
- main
- Children:
- 86cfa1a
- Parents:
- 3ceaef6
- Files:
-
- 1 added
- 13 edited
-
src/assets/js/mod/authorize-interaction.js (modified) (1 diff)
-
src/assets/js/mod/chrome.js (modified) (2 diffs)
-
src/routes/posts.js (modified) (2 diffs)
-
src/routes/search.js (modified) (4 diffs)
-
src/services/i18n.js (modified) (3 diffs)
-
src/views/pages/authorize-interaction.ejs (modified) (1 diff)
-
src/views/pages/search.ejs (modified) (1 diff)
-
src/views/partials/fedi-interact-styles.ejs (modified) (3 diffs)
-
src/views/partials/remote-actor.ejs (modified) (1 diff)
-
src/views/partials/remote-object.ejs (modified) (1 diff)
-
src/views/partials/remote-preview.ejs (added)
-
src/views/partials/topnav.ejs (modified) (2 diffs)
-
src/views/shell.ejs (modified) (1 diff)
-
test/search-fediverse.test.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/assets/js/mod/authorize-interaction.js
r3ceaef6 r9eb6e1b 41 41 // ── volgend blok ── 42 42 43 /* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page. */ 44 (function(){ 45 if (window.__fediReactWired) return; window.__fediReactWired = true; 46 document.addEventListener('submit', function(e){ 47 var f = e.target.closest && e.target.closest('.fedi-react-form'); 48 if (!f) return; 49 e.preventDefault(); 50 var btn = f.querySelector('button'); if (!btn || btn.disabled) return; 51 btn.disabled = true; 52 var body = new URLSearchParams(); 53 new FormData(f).forEach(function(v, k){ body.append(k, v); }); 54 fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' }) 55 .then(function(r){ return r.ok ? r.json() : null; }) 56 .then(function(j){ 57 if (j) { 58 var on = !!j.on; 59 btn.classList.toggle('is-on', on); 60 var lbl = btn.querySelector('.fedi-bigact-label'); 61 if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent); 62 } 63 }) 64 .catch(function(){}) 65 .then(function(){ btn.disabled = false; }); 66 }); 67 })(); 43 /* Waarderen en boosten ter plekke staat sinds de herbouw van de kaart in 44 chrome.js: de kaart staat nu ook in de zoekbalk, op elke pagina. */ 68 45 } -
src/assets/js/mod/chrome.js
r3ceaef6 r9eb6e1b 161 161 box.innerHTML = html; 162 162 } 163 var _sTimer; 163 // Een ADRES is geen zoekterm (shaer-utpi). Dezelfde regel als lookupUri in 164 // routes/search.js: een schema is verplicht, anders wordt zoeken naar 165 // "soundfabrics.nl" een netwerkverzoek. De server beslist daarna nog eens, 166 // met de rechten erbij; dit scheelt alleen een vraag die toch nee oplevert. 167 function lookupUri(q) { 168 if (!/^https?:\/\//i.test(q) || /\s/.test(q)) return null; 169 try { var u = new URL(q); return u.hostname.indexOf('.') < 0 ? null : u.href; } catch (e) { return null; } 170 } 171 function allLink(ov, q) { 172 var action = (ov && ov.getAttribute('data-action')) || '/search'; 173 return '<a class="ss-all" href="' + esc(action) + '?q=' + encodeURIComponent(q) + '">' + esc(_st().all) + '</a>'; 174 } 175 function fetchSuggest(box, q, ov, seq) { 176 var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest'; 177 fetch(url + '?q=' + encodeURIComponent(q)) 178 .then(function(r){ return r.ok ? r.json() : null; }) 179 .then(function(d){ if (d && seq === _sSeq) renderSuggest(box, d, q, ov); }) 180 .catch(function(){}); 181 } 182 // Elke toetsaanslag krijgt een volgnummer. Een ophaling van een andere 183 // server duurt soms seconden; zonder nummer overschrijft een traag antwoord 184 // op een oude vraag wat je inmiddels hebt getypt. 185 var _sTimer, _sSeq = 0; 164 186 document.addEventListener('input', function(e) { 165 187 var inp = e.target.closest && e.target.closest('#search-overlay input[name="q"]'); … … 168 190 if (!box) return; 169 191 var q = inp.value.trim(); 192 var seq = ++_sSeq; 170 193 clearTimeout(_sTimer); 171 194 if (q.length < 2) { box.innerHTML = ''; return; } 195 var ov = document.getElementById('search-overlay'); 196 var address = lookupUri(q); 197 if (!address) { _sTimer = setTimeout(function() { fetchSuggest(box, q, ov, seq); }, 200); return; } 198 // Een geplakt adres: de post zelf, niet de tekst van de link doorzocht. 199 box.innerHTML = '<div class="ss-remote-loading">' + esc(_st().looking) + '</div>'; 172 200 _sTimer = setTimeout(function() { 173 var ov = document.getElementById('search-overlay'); 174 var url = (ov && ov.getAttribute('data-suggest')) || '/search/suggest'; 175 fetch(url + '?q=' + encodeURIComponent(q)) 176 .then(function(r){ return r.ok ? r.json() : null; }) 177 .then(function(d){ if (d) renderSuggest(box, d, q, ov); }) 178 .catch(function(){}); 179 }, 200); 180 }); 181 // Click on a suggestion → close the overlay (the link/boost handles navigation). 201 var url = (ov && ov.getAttribute('data-remote')) || '/search/remote'; 202 fetch(url + '?q=' + encodeURIComponent(address), { credentials: 'same-origin', headers: { 'X-Requested-With': 'fetch' } }) 203 .then(function(r) { 204 if (seq !== _sSeq) return; 205 // 204: geen recht om op te halen (of toch geen adres). Dan is het 206 // gewoon een zoekterm en krijg je de gewone suggesties. 207 if (r.status === 204 || !r.ok) { fetchSuggest(box, q, ov, seq); return; } 208 return r.text().then(function(html) { if (seq === _sSeq) box.innerHTML = html + allLink(ov, q); }); 209 }) 210 .catch(function() { if (seq === _sSeq) fetchSuggest(box, q, ov, seq); }); 211 }, 300); 212 }); 213 // Een klik op een suggestie sluit de overlay, want de link gaat ergens heen. 214 // Behalve wat in een nieuw tabblad opent (het origineel, een profiel): dan 215 // blijf je hier, en hoort de kaart er nog te staan als je terugkomt. 182 216 document.addEventListener('click', function(e) { 183 if (e.target.closest && e.target.closest('#search-suggest a')) { var o = overlay(); if (o) o.hidden = true; } 217 var a = e.target.closest && e.target.closest('#search-suggest a'); 218 if (a && a.getAttribute('target') !== '_blank') { var o = overlay(); if (o) o.hidden = true; } 219 }); 220 })(); 221 222 // ── De kaart van een post van een andere server ───────────────────────── 223 // Staat hier en niet in authorize-interaction.js, want de kaart staat nu op 224 // drie plekken: de live-preview (elke pagina, via de zoekbalk), de zoekpagina 225 // en /authorize_interaction. Alles luistert op document, dus een kaart die 226 // later binnenkomt (de preview, een htmx-wissel) doet vanzelf mee. 227 (function() { 228 if (window.__remoteCardWired) return; window.__remoteCardWired = true; 229 230 // Waarderen en boosten ter plekke: POST via fetch, de knop omzetten, blijven 231 // waar je bent. Zonder JS doet het formulier hetzelfde met een omweg. 232 document.addEventListener('submit', function(e) { 233 var f = e.target.closest && e.target.closest('.fedi-react-form'); 234 if (!f) return; 235 e.preventDefault(); 236 var btn = f.querySelector('button'); if (!btn || btn.disabled) return; 237 btn.disabled = true; 238 var body = new URLSearchParams(); 239 new FormData(f).forEach(function(v, k) { body.append(k, v); }); 240 fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' }) 241 .then(function(r) { return r.ok ? r.json() : null; }) 242 .then(function(j) { 243 if (!j) return; 244 var on = !!j.on; 245 btn.classList.toggle('is-on', on); 246 btn.setAttribute('aria-pressed', on ? 'true' : 'false'); 247 var lbl = btn.querySelector('.rp-act-label'); 248 if (lbl) lbl.textContent = on ? (btn.getAttribute('data-on') || lbl.textContent) : (btn.getAttribute('data-off') || lbl.textContent); 249 }) 250 .catch(function() {}) 251 .then(function() { btn.disabled = false; }); 252 }); 253 254 // Antwoorden klapt open waar de kaart staat. Staat er geen venster (de 255 // preview), dan volgt de knop gewoon zijn link naar de volle pagina, met het 256 // venster daar al open. 257 document.addEventListener('click', function(e) { 258 var open = e.target.closest && e.target.closest('[data-rp-reply]'); 259 var close = !open && e.target.closest && e.target.closest('[data-rp-reply-close]'); 260 if (!open && !close) return; 261 var panel = document.getElementById('rp-reply'); 262 if (!panel) return; 263 e.preventDefault(); 264 var btn = document.querySelector('[data-rp-reply][aria-controls="rp-reply"]'); 265 var show = !!open && panel.hidden; 266 panel.hidden = !show; 267 if (btn) btn.setAttribute('aria-expanded', show ? 'true' : 'false'); 268 if (show) { 269 var ed = panel.querySelector('.re-editor, textarea'); 270 if (ed) ed.focus(); 271 // Op een telefoon staat het venster vaak onder de vouw. 272 if (panel.scrollIntoView) panel.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); 273 } else if (btn) { 274 btn.focus(); 275 } 184 276 }); 185 277 })(); -
src/routes/posts.js
r3ceaef6 r9eb6e1b 876 876 let target = null, followTarget = null; 877 877 if (!sent && !followed && !voted && !reported && uri) { 878 try { target = await ActivityPubService.resolveRemoteNote(uri); } catch { /* ignore */ } 878 // Ondertekend als de site, net als de zoekbalk: een post die alleen voor 879 // volgers zichtbaar is weigert een anonieme GET, en dan zegt deze pagina 880 // "niet gevonden" over een post die de preview wel liet zien. 881 try { target = await ActivityPubService.resolveRemoteNote(uri, site ? { asSlug: site.slug } : {}); } catch { /* ignore */ } 879 882 // Not a post? Maybe the URI is a profile/actor → offer Follow, not reply. 880 883 if (!target) { try { followTarget = await ActivityPubService.resolveRemoteActor(uri); } catch { /* ignore */ } } … … 895 898 reacted: (site && uri) ? ActivityPubService.getReaction(site.slug, uri) : { liked: false, boosted: false }, 896 899 siteTitle: site ? site.title : '', 900 // De knoppen in de preview wijzen hierheen met ?reply=1 of ?report=1: dan 901 // staat dat deel al open en hoef je niet twee keer te tikken. 902 replyOpen: !!req.query.reply, 903 reportOpen: !!req.query.report, 897 904 }); 898 905 }); -
src/routes/search.js
r3ceaef6 r9eb6e1b 27 27 import { t as i18nT, resolveLang } from '../services/i18n.js'; 28 28 import ActivityPubService from '../services/ActivityPubService.js'; 29 import ejs from 'ejs'; 30 import path from 'path'; 31 import { fileURLToPath } from 'url'; 29 32 import PermissionsService from '../services/PermissionsService.js'; 30 33 … … 99 102 const site = res.locals.site; 100 103 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 */ 116 async 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 }; 101 129 } 102 130 … … 214 242 const remoteUri = lookupUri(rawQ); 215 243 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 } }; 228 247 229 248 const r = searchSite(req, res, rawQ, { posts: 50, tracks: 25, events: 25, pages: 8 }); … … 234 253 total, queryError: r.queryError, 235 254 remoteUri, remote, remoteKind, mayLookup, 236 remoteReacted : (remoteKind === 'note') ? ActivityPubService.getReaction(site.slug, remote.object_uri || remoteUri) : { liked: false, boosted: false },255 remoteReacted, 237 256 siteTitle: site.title || '', 238 257 }); 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. 271 const VIEWS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'views'); 272 273 router.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); 239 290 }); 240 291 -
src/services/i18n.js
r3ceaef6 r9eb6e1b 166 166 'fedi.heading': 'Vanuit de fediverse', 'fedi.likes': 'sterren', 'fedi.boosts': 'boosts', 'fedi.replies': 'Reacties uit de fediverse', 167 167 'fedi.reply': 'Reageer', 'fedi.reply_ph': 'Je antwoord aan de fediverse…', 'fedi.send': 'Versturen', 'fedi.you': 'Jij', 168 'fedi.remote_title': 'Reageer via de fediverse', 'fedi.follow_heading': 'Volgen via de fediverse', 'fedi.profile_follow': 'Volg via de fediverse', 'profile.since': 'Op Klonkt sinds', 'profile.free': 'Gratis', 'fedi.follow_intro': 'Je staat op het punt te volgen:', 'fedi.follow_btn': 'Volgen', 'fedi.cancel': 'Annuleren', 'fedi.followed_title': 'Volgverzoek verstuurd ✅', 'fedi.followed_done': 'Je volgverzoek is onderweg. Zodra de andere kant het accepteert, verschijnen hun berichten in je tijdlijn.', 'fedi.view_profile': 'Bekijk profiel →', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres:', 'fedi.remote_notfound': 'Kon die post niet ophalen. Plak de volledige post-URL:', 'fedi.remote_load': 'Ophalen', 'fedi.remote_replying_to': 'Je reageert op', 'fedi. remote_as': 'Wordt verzonden als {site}.', 'fedi.remote_view_original': 'Bekijk de hele post + reacties op de bron →', 'fedi.remote_reply_short': 'via de fediverse', 'fedi.like_short': 'Like', 'fedi.unlike_short': 'Like intrekken', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'jouw server', 'fedi.remote_sent_title': 'Verzonden ✅', 'fedi.remote_sent': 'Je reactie is verstuurd. Hij verschijnt zo bij de originele post op de fediverse, niet op deze pagina. Bekijk hem daar:', 'fedi.reply_where': 'Je reactie verschijnt bij de originele post op de fediverse, niet op deze pagina. Via de link hierboven zie je hem daar.', 'fedi.remote_back': '← Terug naar je site', 'fedi.like_btn': 'Like deze post', 'fedi.or_reply': 'of reageer:', 'fedi.liked_title': 'Geliket', 'fedi.liked_done': 'Je like is onderweg naar de fediverse.', 'fedi.boost_btn': 'Boost deze post', 'fedi.boosted_title': 'Geboost', 'fedi.boosted_done': 'Je boost is onderweg naar de fediverse.', 'fedi.remote_interact': 'Interacteer via de fediverse', 'fedi.report_open': 'Deze post rapporteren', 'fedi.report_where': 'De melding gaat naar de instance en hun moderator(s).', 'fedi.report_ph': 'Wat is er mis? (optioneel)', 'fedi.report_send': 'Rapporteren', 'fedi.reported_title': 'Gerapporteerd', 'fedi.reported_done': 'Je melding is naar de server van de gebruiker gestuurd. Hun moderators bekijken het.', 'fedi.delete_confirm': 'Deze reactie verwijderen?', 'fedi.mod_remove_confirm': 'Deze reactie uit je thread verwijderen? Hij komt niet terug, ook niet via thread-aanvulling.', 'fedi.mod_report_confirm': 'Deze reactie rapporteren bij de server van de auteur?', 'fedi.manage_title': 'Mijn fediverse-reacties', 'fedi.manage_empty': 'Je hebt nog geen reacties verstuurd.', 'fedi.goto_post': 'Naar de post', 'fedi.edit': 'Bewerken', 'fedi.save_edit': 'Opslaan', 'fedi.bm_label': 'Interacteer via mijn site', 'fedi.bm_help': 'Sleep deze knop naar je bladwijzerbalk. Klik ’m daarna op elke fediverse-post (Mastodon, een andere Klonkt…) om er via jouw site op te reageren, te liken of te boosten.', 'tl.title': 'Krant', 'tl.lead': 'Volg accounts in de fediverse en zie hun berichten hier.', 'tl.follow_btn': 'Volgen', 'tl.following': 'Je volgt', 'tl.unfollow': 'Ontvolgen', 'tl.autoboost': 'Uitgelicht', 'tl.autoboost_follow': 'uitlichten in cirkel', 'tl.autoboost_hint': 'Hun nieuwe posts verschijnen doorlopend in jouw Cirkel (lokaal, geen fediverse-boost).', 'tl.moved_title': 'Dit account is verhuisd', 'tl.moved_lead': 'Nieuwe berichten, volgen, liken en reageren doe je voortaan vanaf', 'tl.moved_hint': 'Lezen blijft hier gewoon werken, en reacties op je oude posts komen nog binnen. Wil je terug? Maak het verhuisadres leeg bij Uiterlijk.', 'tl.move_title': 'Je volglijst meenemen', 'tl.move_hint': 'Verhuis je naar een ander adres? Je volgers krijgen dat vanzelf te horen, maar wie JIJ volgt niet. Neem die lijst hiermee mee. Werkt ook van en naar Mastodon.', 'tl.move_export': 'Lijst downloaden (CSV)', 'tl.move_import_file': 'Kies je gedownloade CSV-bestand:', 'tl.move_import_lbl': 'Of plak de lijst hier:', 'tl.move_import': 'Iedereen volgen', 'tl.pending': 'in afwachting', 'tl.unboost': 'Boost intrekken', 'tl.feed': 'Berichten', 'tl.tab_feed': 'Krant', 'tl.tab_following': 'Volgend', 'tl.tab_replies': 'Reacties', 'tl.tab_followers': 'Volgers', 'tl.followers': 'Volgers', 'tl.followers_lead': 'Wie jou volgt in de fediverse, met de laatste geslaagde bezorging. Rood = nog nooit bezorgd of laatste poging mislukt — kandidaat om op te ruimen na een check.', 'tl.empty_followers': 'Nog geen volgers.', 'tl.last_delivery': 'Laatste bezorging', 'tl.never_delivered': 'Nog nooit bezorgd', 'tl.delivery_failed': 'laatste poging mislukt', 'tl.remove_follower': 'Verwijderen', 'tl.folreq_title': 'Volgverzoeken', 'tl.folreq_sub': 'Deze wachten op jouw ja of nee. Tot die tijd ziet de aanvrager niets van je posts.', 'tl.folreq_accept': 'Accepteer', 'tl.folreq_deny': 'Weiger', 'tl.approve_toggle': 'Volgers eerst goedkeuren', 'tl.approve_toggle_hint': 'Aan: volgverzoeken wachten hier op jouw ja. Uit: iedereen mag direct volgen.', 'tl.remove_confirm': 'Deze volger verwijderen? Een actief account moet je dan opnieuw volgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'jij volgt', 'tl.dir_follower': 'volgt jou', 'tl.dir_mutual': 'wederzijds', 'tl.connect_empty': 'Nog geen connecties. Volg iemand hierboven om te beginnen.', 'tl.unreachable': 'Niet bereikbaar', 'tl.unreachable_lead': 'Deze volgers konden we niet bereiken (nooit bezorgd of laatste poging mislukt). Ruim ze op na een handmatige check.', 'msg.tab': 'Berichten', 'msg.title': 'Berichten', 'msg.filter_all': 'Alles', 'msg.filter_msgs': 'Berichten', 'msg.filter_conv': 'Gesprekken', 'msg.filter_act': 'Activiteit', 'msg.filter_mod': 'Moderatie', 'msg.filter_sent': 'Verzonden', 'msg.search_ph': 'Zoeken in berichten…', 'msg.no_match': 'Niets gevonden.', 'msg.poll_done': 'Je peiling is afgelopen', 'msg.poll_total': '{n} stemmers', 'msg.you': 'Jij', 'msg.sent_reply': 'reageerde via de fediverse', 'msg.and_more': 'en {n} anderen', 'msg.liked_many': 'liketen je post', 'msg.boosted_many': 'boostten je post', 'msg.private': 'privé', 'msg.private_hint': 'Alleen aan jou gericht; staat niet op de publieke postpagina.', 'msg.new': 'Nieuw sinds je laatste bezoek', 'msg.empty': 'Nog geen berichten. Reacties, vermeldingen en activiteit verschijnen hier.', 'oauth.title': 'App toegang geven', 'oauth.wants_access': 'wil verbinding maken met je Klonkt-account.', 'oauth.post_as': 'Plaatsen als', 'oauth.scope_read': 'Je berichten, reacties en meldingen lezen', 'oauth.scope_write': 'Namens jou posten, reageren, liken en volgen', 'oauth.allow': 'Toestaan', 'oauth.deny': 'Weigeren', 'oauth.foot': 'Je kunt de toegang later intrekken. Geef alleen apps toegang die je vertrouwt.', 're.title': 'Reactie schrijven', 're.bold': 'Vet', 're.italic': 'Cursief', 're.link': 'Link invoegen', 're.list': 'Opsomming', 're.quote': 'Citaat', 're.lang': 'Taal van je reactie', 're.attach': 'Media toevoegen (afbeelding, audio, video)', 're.attach_err': 'Upload mislukt', 're.to': 'Aan:', 're.mention_del': 'Deze persoon niet meer adresseren', 'tl.empty_following': 'Je volgt nog niemand.', 'tl.empty': 'Nog niks — volg iemand om hun berichten hier te zien.', 'tl.view_original': 'Bekijk origineel →', 'tl.open_player': 'Open de speler', 'feed.load_more': 'Meer laden', 'tl.paste_ph': 'Plak een fediverse-post-URL', 'tl.paste_go': 'Openen', 'tl.boosted': 'boostte dit', 'tl.read_more': 'Meer lezen', 'tl.show_less': 'Minder', 'poll.vote': 'Stem', 'poll.votes': 'stemmen', 'poll.closed': 'gesloten', 'poll.open': 'open', 'poll.aria': 'Peiling', 'poll.voter_one': 'stemmer', 'poll.voter_many': 'stemmers', 'poll.closes': 'sluit op', 'poll.multiple': 'meerkeuze', 'poll.fedi_only': 'Stemmen kan vanuit de fediverse — volg deze site en stem in je eigen app.', 'poll.voted_title': 'Stem verstuurd', 'poll.voted_done': 'Je stem is verstuurd naar de poll. De uitslag werkt bij zodra de maker die doorstuurt.',168 'fedi.remote_title': 'Reageer via de fediverse', 'fedi.follow_heading': 'Volgen via de fediverse', 'fedi.profile_follow': 'Volg via de fediverse', 'profile.since': 'Op Klonkt sinds', 'profile.free': 'Gratis', 'fedi.follow_intro': 'Je staat op het punt te volgen:', 'fedi.follow_btn': 'Volgen', 'fedi.cancel': 'Annuleren', 'fedi.followed_title': 'Volgverzoek verstuurd ✅', 'fedi.followed_done': 'Je volgverzoek is onderweg. Zodra de andere kant het accepteert, verschijnen hun berichten in je tijdlijn.', 'fedi.view_profile': 'Bekijk profiel →', 'fedi.remote_reply': 'Reageer via de fediverse', 'fedi.remote_prompt': 'Je fediverse-adres:', 'fedi.remote_notfound': 'Kon die post niet ophalen. Plak de volledige post-URL:', 'fedi.remote_load': 'Ophalen', 'fedi.remote_replying_to': 'Je reageert op', 'fedi.cw_open': 'tonen', 'fedi.sensitive': 'Gevoelige inhoud', 'fedi.open_full': 'Openen', 'fedi.src_short': 'Origineel', 'fedi.reply_short': 'Reageer', 'fedi.profile_short': 'Profiel', 'search.fedi_looking': 'Opzoeken op het fediverse…', 'fedi.remote_as': 'Wordt verzonden als {site}.', 'fedi.remote_view_original': 'Bekijk de hele post + reacties op de bron →', 'fedi.remote_reply_short': 'via de fediverse', 'fedi.like_short': 'Like', 'fedi.unlike_short': 'Like intrekken', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'jouw server', 'fedi.remote_sent_title': 'Verzonden ✅', 'fedi.remote_sent': 'Je reactie is verstuurd. Hij verschijnt zo bij de originele post op de fediverse, niet op deze pagina. Bekijk hem daar:', 'fedi.reply_where': 'Je reactie verschijnt bij de originele post op de fediverse, niet op deze pagina. Via de link hierboven zie je hem daar.', 'fedi.remote_back': '← Terug naar je site', 'fedi.like_btn': 'Like deze post', 'fedi.or_reply': 'of reageer:', 'fedi.liked_title': 'Geliket', 'fedi.liked_done': 'Je like is onderweg naar de fediverse.', 'fedi.boost_btn': 'Boost deze post', 'fedi.boosted_title': 'Geboost', 'fedi.boosted_done': 'Je boost is onderweg naar de fediverse.', 'fedi.remote_interact': 'Interacteer via de fediverse', 'fedi.report_open': 'Deze post rapporteren', 'fedi.report_where': 'De melding gaat naar de instance en hun moderator(s).', 'fedi.report_ph': 'Wat is er mis? (optioneel)', 'fedi.report_send': 'Rapporteren', 'fedi.reported_title': 'Gerapporteerd', 'fedi.reported_done': 'Je melding is naar de server van de gebruiker gestuurd. Hun moderators bekijken het.', 'fedi.delete_confirm': 'Deze reactie verwijderen?', 'fedi.mod_remove_confirm': 'Deze reactie uit je thread verwijderen? Hij komt niet terug, ook niet via thread-aanvulling.', 'fedi.mod_report_confirm': 'Deze reactie rapporteren bij de server van de auteur?', 'fedi.manage_title': 'Mijn fediverse-reacties', 'fedi.manage_empty': 'Je hebt nog geen reacties verstuurd.', 'fedi.goto_post': 'Naar de post', 'fedi.edit': 'Bewerken', 'fedi.save_edit': 'Opslaan', 'fedi.bm_label': 'Interacteer via mijn site', 'fedi.bm_help': 'Sleep deze knop naar je bladwijzerbalk. Klik ’m daarna op elke fediverse-post (Mastodon, een andere Klonkt…) om er via jouw site op te reageren, te liken of te boosten.', 'tl.title': 'Krant', 'tl.lead': 'Volg accounts in de fediverse en zie hun berichten hier.', 'tl.follow_btn': 'Volgen', 'tl.following': 'Je volgt', 'tl.unfollow': 'Ontvolgen', 'tl.autoboost': 'Uitgelicht', 'tl.autoboost_follow': 'uitlichten in cirkel', 'tl.autoboost_hint': 'Hun nieuwe posts verschijnen doorlopend in jouw Cirkel (lokaal, geen fediverse-boost).', 'tl.moved_title': 'Dit account is verhuisd', 'tl.moved_lead': 'Nieuwe berichten, volgen, liken en reageren doe je voortaan vanaf', 'tl.moved_hint': 'Lezen blijft hier gewoon werken, en reacties op je oude posts komen nog binnen. Wil je terug? Maak het verhuisadres leeg bij Uiterlijk.', 'tl.move_title': 'Je volglijst meenemen', 'tl.move_hint': 'Verhuis je naar een ander adres? Je volgers krijgen dat vanzelf te horen, maar wie JIJ volgt niet. Neem die lijst hiermee mee. Werkt ook van en naar Mastodon.', 'tl.move_export': 'Lijst downloaden (CSV)', 'tl.move_import_file': 'Kies je gedownloade CSV-bestand:', 'tl.move_import_lbl': 'Of plak de lijst hier:', 'tl.move_import': 'Iedereen volgen', 'tl.pending': 'in afwachting', 'tl.unboost': 'Boost intrekken', 'tl.feed': 'Berichten', 'tl.tab_feed': 'Krant', 'tl.tab_following': 'Volgend', 'tl.tab_replies': 'Reacties', 'tl.tab_followers': 'Volgers', 'tl.followers': 'Volgers', 'tl.followers_lead': 'Wie jou volgt in de fediverse, met de laatste geslaagde bezorging. Rood = nog nooit bezorgd of laatste poging mislukt — kandidaat om op te ruimen na een check.', 'tl.empty_followers': 'Nog geen volgers.', 'tl.last_delivery': 'Laatste bezorging', 'tl.never_delivered': 'Nog nooit bezorgd', 'tl.delivery_failed': 'laatste poging mislukt', 'tl.remove_follower': 'Verwijderen', 'tl.folreq_title': 'Volgverzoeken', 'tl.folreq_sub': 'Deze wachten op jouw ja of nee. Tot die tijd ziet de aanvrager niets van je posts.', 'tl.folreq_accept': 'Accepteer', 'tl.folreq_deny': 'Weiger', 'tl.approve_toggle': 'Volgers eerst goedkeuren', 'tl.approve_toggle_hint': 'Aan: volgverzoeken wachten hier op jouw ja. Uit: iedereen mag direct volgen.', 'tl.remove_confirm': 'Deze volger verwijderen? Een actief account moet je dan opnieuw volgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'jij volgt', 'tl.dir_follower': 'volgt jou', 'tl.dir_mutual': 'wederzijds', 'tl.connect_empty': 'Nog geen connecties. Volg iemand hierboven om te beginnen.', 'tl.unreachable': 'Niet bereikbaar', 'tl.unreachable_lead': 'Deze volgers konden we niet bereiken (nooit bezorgd of laatste poging mislukt). Ruim ze op na een handmatige check.', 'msg.tab': 'Berichten', 'msg.title': 'Berichten', 'msg.filter_all': 'Alles', 'msg.filter_msgs': 'Berichten', 'msg.filter_conv': 'Gesprekken', 'msg.filter_act': 'Activiteit', 'msg.filter_mod': 'Moderatie', 'msg.filter_sent': 'Verzonden', 'msg.search_ph': 'Zoeken in berichten…', 'msg.no_match': 'Niets gevonden.', 'msg.poll_done': 'Je peiling is afgelopen', 'msg.poll_total': '{n} stemmers', 'msg.you': 'Jij', 'msg.sent_reply': 'reageerde via de fediverse', 'msg.and_more': 'en {n} anderen', 'msg.liked_many': 'liketen je post', 'msg.boosted_many': 'boostten je post', 'msg.private': 'privé', 'msg.private_hint': 'Alleen aan jou gericht; staat niet op de publieke postpagina.', 'msg.new': 'Nieuw sinds je laatste bezoek', 'msg.empty': 'Nog geen berichten. Reacties, vermeldingen en activiteit verschijnen hier.', 'oauth.title': 'App toegang geven', 'oauth.wants_access': 'wil verbinding maken met je Klonkt-account.', 'oauth.post_as': 'Plaatsen als', 'oauth.scope_read': 'Je berichten, reacties en meldingen lezen', 'oauth.scope_write': 'Namens jou posten, reageren, liken en volgen', 'oauth.allow': 'Toestaan', 'oauth.deny': 'Weigeren', 'oauth.foot': 'Je kunt de toegang later intrekken. Geef alleen apps toegang die je vertrouwt.', 're.title': 'Reactie schrijven', 're.bold': 'Vet', 're.italic': 'Cursief', 're.link': 'Link invoegen', 're.list': 'Opsomming', 're.quote': 'Citaat', 're.lang': 'Taal van je reactie', 're.attach': 'Media toevoegen (afbeelding, audio, video)', 're.attach_err': 'Upload mislukt', 're.to': 'Aan:', 're.mention_del': 'Deze persoon niet meer adresseren', 'tl.empty_following': 'Je volgt nog niemand.', 'tl.empty': 'Nog niks — volg iemand om hun berichten hier te zien.', 'tl.view_original': 'Bekijk origineel →', 'tl.open_player': 'Open de speler', 'feed.load_more': 'Meer laden', 'tl.paste_ph': 'Plak een fediverse-post-URL', 'tl.paste_go': 'Openen', 'tl.boosted': 'boostte dit', 'tl.read_more': 'Meer lezen', 'tl.show_less': 'Minder', 'poll.vote': 'Stem', 'poll.votes': 'stemmen', 'poll.closed': 'gesloten', 'poll.open': 'open', 'poll.aria': 'Peiling', 'poll.voter_one': 'stemmer', 'poll.voter_many': 'stemmers', 'poll.closes': 'sluit op', 'poll.multiple': 'meerkeuze', 'poll.fedi_only': 'Stemmen kan vanuit de fediverse — volg deze site en stem in je eigen app.', 'poll.voted_title': 'Stem verstuurd', 'poll.voted_done': 'Je stem is verstuurd naar de poll. De uitslag werkt bij zodra de maker die doorstuurt.', 169 169 'comments.to_start': 'om de conversatie te starten.', 170 170 'comments.reply': 'Reageer', 'comments.delete': 'Verwijder', 'comments.cancel': 'Annuleren', … … 1177 1177 'fedi.heading': 'From the fediverse', 'fedi.likes': 'favourites', 'fedi.boosts': 'boosts', 'fedi.replies': 'Replies from the fediverse', 1178 1178 'fedi.reply': 'Reply', 'fedi.reply_ph': 'Your reply to the fediverse…', 'fedi.send': 'Send', 'fedi.you': 'You', 1179 'fedi.remote_title': 'Reply via the fediverse', 'fedi.follow_heading': 'Follow via the fediverse', 'fedi.profile_follow': 'Follow via the fediverse', 'profile.since': 'On Klonkt since', 'profile.free': 'Free', 'fedi.follow_intro': 'You are about to follow:', 'fedi.follow_btn': 'Follow', 'fedi.cancel': 'Cancel', 'fedi.followed_title': 'Follow request sent ✅', 'fedi.followed_done': 'Your follow request is on its way. Once accepted, their posts show up in your timeline.', 'fedi.view_profile': 'View profile →', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address:', 'fedi.remote_notfound': 'Could not fetch that post. Paste the full post URL:', 'fedi.remote_load': 'Fetch', 'fedi.remote_replying_to': 'Replying to', 'fedi. remote_as': 'Sent as {site}.', 'fedi.remote_view_original': 'View the full post + comments on the source →', 'fedi.remote_reply_short': 'via the fediverse', 'fedi.like_short': 'Like', 'fedi.unlike_short': 'Unlike', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'your server', 'fedi.remote_sent_title': 'Sent ✅', 'fedi.remote_sent': 'Your reply has been sent. It will show up on the original post on the fediverse, not on this page. See it there:', 'fedi.reply_where': 'Your reply appears on the original post on the fediverse, not on this page. Use the link above to see it there.', 'fedi.remote_back': '← Back to your site', 'fedi.like_btn': 'Like this post', 'fedi.or_reply': 'or reply:', 'fedi.liked_title': 'Liked', 'fedi.liked_done': 'Your like is on its way to the fediverse.', 'fedi.boost_btn': 'Boost this post', 'fedi.boosted_title': 'Boosted', 'fedi.boosted_done': 'Your boost is on its way to the fediverse.', 'fedi.remote_interact': 'Interact via the fediverse', 'fedi.report_open': 'Report this post', 'fedi.report_where': 'The report goes to the instance and their moderator(s).', 'fedi.report_ph': 'What is wrong? (optional)', 'fedi.report_send': 'Report', 'fedi.reported_title': 'Reported', 'fedi.reported_done': 'Your report has been sent to their server. Their moderators will review it.', 'fedi.delete_confirm': 'Delete this reply?', 'fedi.mod_remove_confirm': 'Remove this reply from your thread? It will not come back, not even via thread-filling.', 'fedi.mod_report_confirm': 'Report this reply to its author’s server?', 'fedi.manage_title': 'My fediverse replies', 'fedi.manage_empty': 'You have not sent any replies yet.', 'fedi.goto_post': 'Go to post', 'fedi.edit': 'Edit', 'fedi.save_edit': 'Save', 'fedi.bm_label': 'Interact via my site', 'fedi.bm_help': 'Drag this button to your bookmarks bar. Then click it on any fediverse post (Mastodon, another Klonkt…) to reply, like or boost it via your own site.', 'tl.title': 'News', 'tl.lead': 'Follow accounts on the fediverse and see their posts here.', 'tl.follow_btn': 'Follow', 'tl.following': 'Following', 'tl.unfollow': 'Unfollow', 'tl.autoboost': 'Featured', 'tl.autoboost_follow': 'feature in circle', 'tl.autoboost_hint': 'Their new posts keep showing in your Circle (local, no fediverse boost).', 'tl.moved_title': 'This account has moved', 'tl.moved_lead': 'New posts, follows, likes and replies now happen at', 'tl.moved_hint': 'Reading keeps working here, and replies to your old posts still arrive. Want to come back? Clear the move target under Appearance.', 'tl.move_title': 'Take your following list with you', 'tl.move_hint': 'Moving to another address? Your followers are told automatically, but who YOU follow is not. Take that list with you here. Works to and from Mastodon too.', 'tl.move_export': 'Download list (CSV)', 'tl.move_import_file': 'Choose your downloaded CSV file:', 'tl.move_import_lbl': 'Or paste the list here:', 'tl.move_import': 'Follow everyone', 'tl.pending': 'pending', 'tl.unboost': 'Unboost', 'tl.feed': 'Posts', 'tl.tab_feed': 'News', 'tl.tab_following': 'Following', 'tl.tab_replies': 'Replies', 'tl.tab_followers': 'Followers', 'tl.followers': 'Followers', 'tl.followers_lead': 'Who follows you on the fediverse, with the last successful delivery. Red = never delivered or last attempt failed — a candidate to clean up after a check.', 'tl.empty_followers': 'No followers yet.', 'tl.last_delivery': 'Last delivery', 'tl.never_delivered': 'Never delivered', 'tl.delivery_failed': 'last attempt failed', 'tl.remove_follower': 'Remove', 'tl.folreq_title': 'Follow requests', 'tl.folreq_sub': 'These wait for your yes or no. Until then the requester sees none of your posts.', 'tl.folreq_accept': 'Accept', 'tl.folreq_deny': 'Deny', 'tl.approve_toggle': 'Approve followers first', 'tl.approve_toggle_hint': 'On: follow requests wait here for your yes. Off: anyone may follow right away.', 'tl.remove_confirm': 'Remove this follower? An active account would have to follow you again.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'you follow', 'tl.dir_follower': 'follows you', 'tl.dir_mutual': 'mutual', 'tl.connect_empty': 'No connections yet. Follow someone above to get started.', 'tl.unreachable': 'Unreachable', 'tl.unreachable_lead': 'We could not reach these followers (never delivered or last attempt failed). Clean them up after a manual check.', 'msg.tab': 'Messages', 'msg.title': 'Messages', 'msg.filter_all': 'All', 'msg.filter_msgs': 'Messages', 'msg.filter_conv': 'Conversations', 'msg.filter_act': 'Activity', 'msg.filter_mod': 'Moderation', 'msg.filter_sent': 'Sent', 'msg.search_ph': 'Search messages…', 'msg.no_match': 'Nothing found.', 'msg.poll_done': 'Your poll has ended', 'msg.poll_total': '{n} voters', 'msg.you': 'You', 'msg.sent_reply': 'replied via the fediverse', 'msg.and_more': 'and {n} others', 'msg.liked_many': 'liked your post', 'msg.boosted_many': 'boosted your post', 'msg.private': 'private', 'msg.private_hint': 'Addressed to you only; not shown on the public post page.', 'msg.new': 'New since your last visit', 'msg.empty': 'No messages yet. Replies, mentions and activity show up here.', 'oauth.title': 'Authorize app', 'oauth.wants_access': 'wants to connect to your Klonkt account.', 'oauth.post_as': 'Post as', 'oauth.scope_read': 'Read your posts, replies and notifications', 'oauth.scope_write': 'Post, reply, like and follow on your behalf', 'oauth.allow': 'Allow', 'oauth.deny': 'Deny', 'oauth.foot': 'You can revoke access later. Only authorize apps you trust.', 're.title': 'Write a reply', 're.bold': 'Bold', 're.italic': 'Italic', 're.link': 'Insert link', 're.list': 'Bullet list', 're.quote': 'Quote', 're.lang': 'Language of your reply', 're.attach': 'Add media (image, audio, video)', 're.attach_err': 'Upload failed', 're.to': 'To:', 're.mention_del': 'Stop addressing this person', 'tl.empty_following': 'You do not follow anyone yet.', 'tl.empty': 'Nothing yet — follow someone to see their posts here.', 'tl.view_original': 'View original →', 'tl.open_player': 'Open the player', 'feed.load_more': 'Load more', 'tl.paste_ph': 'Paste a fediverse post URL', 'tl.paste_go': 'Open', 'tl.boosted': 'boosted', 'tl.read_more': 'Read more', 'tl.show_less': 'Show less', 'poll.vote': 'Vote', 'poll.votes': 'votes', 'poll.closed': 'closed', 'poll.open': 'open', 'poll.aria': 'Poll', 'poll.voter_one': 'voter', 'poll.voter_many': 'voters', 'poll.closes': 'closes', 'poll.multiple': 'multiple choice', 'poll.fedi_only': 'Voting happens on the fediverse — follow this site and vote from your own app.', 'poll.voted_title': 'Vote sent', 'poll.voted_done': 'Your vote is on its way to the poll. The results refresh once the author sends the update.',1179 'fedi.remote_title': 'Reply via the fediverse', 'fedi.follow_heading': 'Follow via the fediverse', 'fedi.profile_follow': 'Follow via the fediverse', 'profile.since': 'On Klonkt since', 'profile.free': 'Free', 'fedi.follow_intro': 'You are about to follow:', 'fedi.follow_btn': 'Follow', 'fedi.cancel': 'Cancel', 'fedi.followed_title': 'Follow request sent ✅', 'fedi.followed_done': 'Your follow request is on its way. Once accepted, their posts show up in your timeline.', 'fedi.view_profile': 'View profile →', 'fedi.remote_reply': 'Reply via the fediverse', 'fedi.remote_prompt': 'Your fediverse address:', 'fedi.remote_notfound': 'Could not fetch that post. Paste the full post URL:', 'fedi.remote_load': 'Fetch', 'fedi.remote_replying_to': 'Replying to', 'fedi.cw_open': 'show', 'fedi.sensitive': 'Sensitive content', 'fedi.open_full': 'Open', 'fedi.src_short': 'Original', 'fedi.reply_short': 'Reply', 'fedi.profile_short': 'Profile', 'search.fedi_looking': 'Looking it up on the fediverse…', 'fedi.remote_as': 'Sent as {site}.', 'fedi.remote_view_original': 'View the full post + comments on the source →', 'fedi.remote_reply_short': 'via the fediverse', 'fedi.like_short': 'Like', 'fedi.unlike_short': 'Unlike', 'fedi.boost_short': 'Boost', 'fedi.remote_ph': 'your server', 'fedi.remote_sent_title': 'Sent ✅', 'fedi.remote_sent': 'Your reply has been sent. It will show up on the original post on the fediverse, not on this page. See it there:', 'fedi.reply_where': 'Your reply appears on the original post on the fediverse, not on this page. Use the link above to see it there.', 'fedi.remote_back': '← Back to your site', 'fedi.like_btn': 'Like this post', 'fedi.or_reply': 'or reply:', 'fedi.liked_title': 'Liked', 'fedi.liked_done': 'Your like is on its way to the fediverse.', 'fedi.boost_btn': 'Boost this post', 'fedi.boosted_title': 'Boosted', 'fedi.boosted_done': 'Your boost is on its way to the fediverse.', 'fedi.remote_interact': 'Interact via the fediverse', 'fedi.report_open': 'Report this post', 'fedi.report_where': 'The report goes to the instance and their moderator(s).', 'fedi.report_ph': 'What is wrong? (optional)', 'fedi.report_send': 'Report', 'fedi.reported_title': 'Reported', 'fedi.reported_done': 'Your report has been sent to their server. Their moderators will review it.', 'fedi.delete_confirm': 'Delete this reply?', 'fedi.mod_remove_confirm': 'Remove this reply from your thread? It will not come back, not even via thread-filling.', 'fedi.mod_report_confirm': 'Report this reply to its author’s server?', 'fedi.manage_title': 'My fediverse replies', 'fedi.manage_empty': 'You have not sent any replies yet.', 'fedi.goto_post': 'Go to post', 'fedi.edit': 'Edit', 'fedi.save_edit': 'Save', 'fedi.bm_label': 'Interact via my site', 'fedi.bm_help': 'Drag this button to your bookmarks bar. Then click it on any fediverse post (Mastodon, another Klonkt…) to reply, like or boost it via your own site.', 'tl.title': 'News', 'tl.lead': 'Follow accounts on the fediverse and see their posts here.', 'tl.follow_btn': 'Follow', 'tl.following': 'Following', 'tl.unfollow': 'Unfollow', 'tl.autoboost': 'Featured', 'tl.autoboost_follow': 'feature in circle', 'tl.autoboost_hint': 'Their new posts keep showing in your Circle (local, no fediverse boost).', 'tl.moved_title': 'This account has moved', 'tl.moved_lead': 'New posts, follows, likes and replies now happen at', 'tl.moved_hint': 'Reading keeps working here, and replies to your old posts still arrive. Want to come back? Clear the move target under Appearance.', 'tl.move_title': 'Take your following list with you', 'tl.move_hint': 'Moving to another address? Your followers are told automatically, but who YOU follow is not. Take that list with you here. Works to and from Mastodon too.', 'tl.move_export': 'Download list (CSV)', 'tl.move_import_file': 'Choose your downloaded CSV file:', 'tl.move_import_lbl': 'Or paste the list here:', 'tl.move_import': 'Follow everyone', 'tl.pending': 'pending', 'tl.unboost': 'Unboost', 'tl.feed': 'Posts', 'tl.tab_feed': 'News', 'tl.tab_following': 'Following', 'tl.tab_replies': 'Replies', 'tl.tab_followers': 'Followers', 'tl.followers': 'Followers', 'tl.followers_lead': 'Who follows you on the fediverse, with the last successful delivery. Red = never delivered or last attempt failed — a candidate to clean up after a check.', 'tl.empty_followers': 'No followers yet.', 'tl.last_delivery': 'Last delivery', 'tl.never_delivered': 'Never delivered', 'tl.delivery_failed': 'last attempt failed', 'tl.remove_follower': 'Remove', 'tl.folreq_title': 'Follow requests', 'tl.folreq_sub': 'These wait for your yes or no. Until then the requester sees none of your posts.', 'tl.folreq_accept': 'Accept', 'tl.folreq_deny': 'Deny', 'tl.approve_toggle': 'Approve followers first', 'tl.approve_toggle_hint': 'On: follow requests wait here for your yes. Off: anyone may follow right away.', 'tl.remove_confirm': 'Remove this follower? An active account would have to follow you again.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'you follow', 'tl.dir_follower': 'follows you', 'tl.dir_mutual': 'mutual', 'tl.connect_empty': 'No connections yet. Follow someone above to get started.', 'tl.unreachable': 'Unreachable', 'tl.unreachable_lead': 'We could not reach these followers (never delivered or last attempt failed). Clean them up after a manual check.', 'msg.tab': 'Messages', 'msg.title': 'Messages', 'msg.filter_all': 'All', 'msg.filter_msgs': 'Messages', 'msg.filter_conv': 'Conversations', 'msg.filter_act': 'Activity', 'msg.filter_mod': 'Moderation', 'msg.filter_sent': 'Sent', 'msg.search_ph': 'Search messages…', 'msg.no_match': 'Nothing found.', 'msg.poll_done': 'Your poll has ended', 'msg.poll_total': '{n} voters', 'msg.you': 'You', 'msg.sent_reply': 'replied via the fediverse', 'msg.and_more': 'and {n} others', 'msg.liked_many': 'liked your post', 'msg.boosted_many': 'boosted your post', 'msg.private': 'private', 'msg.private_hint': 'Addressed to you only; not shown on the public post page.', 'msg.new': 'New since your last visit', 'msg.empty': 'No messages yet. Replies, mentions and activity show up here.', 'oauth.title': 'Authorize app', 'oauth.wants_access': 'wants to connect to your Klonkt account.', 'oauth.post_as': 'Post as', 'oauth.scope_read': 'Read your posts, replies and notifications', 'oauth.scope_write': 'Post, reply, like and follow on your behalf', 'oauth.allow': 'Allow', 'oauth.deny': 'Deny', 'oauth.foot': 'You can revoke access later. Only authorize apps you trust.', 're.title': 'Write a reply', 're.bold': 'Bold', 're.italic': 'Italic', 're.link': 'Insert link', 're.list': 'Bullet list', 're.quote': 'Quote', 're.lang': 'Language of your reply', 're.attach': 'Add media (image, audio, video)', 're.attach_err': 'Upload failed', 're.to': 'To:', 're.mention_del': 'Stop addressing this person', 'tl.empty_following': 'You do not follow anyone yet.', 'tl.empty': 'Nothing yet — follow someone to see their posts here.', 'tl.view_original': 'View original →', 'tl.open_player': 'Open the player', 'feed.load_more': 'Load more', 'tl.paste_ph': 'Paste a fediverse post URL', 'tl.paste_go': 'Open', 'tl.boosted': 'boosted', 'tl.read_more': 'Read more', 'tl.show_less': 'Show less', 'poll.vote': 'Vote', 'poll.votes': 'votes', 'poll.closed': 'closed', 'poll.open': 'open', 'poll.aria': 'Poll', 'poll.voter_one': 'voter', 'poll.voter_many': 'voters', 'poll.closes': 'closes', 'poll.multiple': 'multiple choice', 'poll.fedi_only': 'Voting happens on the fediverse — follow this site and vote from your own app.', 'poll.voted_title': 'Vote sent', 'poll.voted_done': 'Your vote is on its way to the poll. The results refresh once the author sends the update.', 1180 1180 'comments.to_start': 'to start the conversation.', 1181 1181 'comments.reply': 'Reply', 'comments.delete': 'Delete', 'comments.cancel': 'Cancel', … … 2186 2186 'fedi.heading': 'Aus dem Fediverse', 'fedi.likes': 'Favoriten', 'fedi.boosts': 'Boosts', 'fedi.replies': 'Antworten aus dem Fediverse', 2187 2187 'fedi.reply': 'Antworten', 'fedi.reply_ph': 'Deine Antwort an das Fediverse…', 'fedi.send': 'Senden', 'fedi.you': 'Du', 2188 'fedi.remote_title': 'Über das Fediverse antworten', 'fedi.follow_heading': 'Über das Fediverse folgen', 'fedi.profile_follow': 'Über das Fediverse folgen', 'profile.since': 'Auf Klonkt seit', 'profile.free': 'Kostenlos', 'fedi.follow_intro': 'Du folgst gleich:', 'fedi.follow_btn': 'Folgen', 'fedi.cancel': 'Abbrechen', 'fedi.followed_title': 'Folge-Anfrage gesendet ✅', 'fedi.followed_done': 'Deine Folge-Anfrage ist unterwegs. Sobald sie akzeptiert wird, erscheinen ihre Beiträge in deiner Timeline.', 'fedi.view_profile': 'Profil ansehen →', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse:', 'fedi.remote_notfound': 'Beitrag konnte nicht geladen werden. Füge die vollständige Beitrags-URL ein:', 'fedi.remote_load': 'Laden', 'fedi.remote_replying_to': 'Antwort an', 'fedi. remote_as': 'Wird als {site} gesendet.', 'fedi.remote_view_original': 'Ganzen Beitrag + Kommentare an der Quelle ansehen →', 'fedi.remote_reply_short': 'übers Fediverse', 'fedi.like_short': 'Liken', 'fedi.unlike_short': 'Like zurücknehmen', 'fedi.boost_short': 'Boosten', 'fedi.remote_ph': 'dein Server', 'fedi.remote_sent_title': 'Gesendet ✅', 'fedi.remote_sent': 'Deine Antwort wurde gesendet. Sie erscheint gleich beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Sieh sie dir dort an:', 'fedi.reply_where': 'Deine Antwort erscheint beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Über den Link oben siehst du sie dort.', 'fedi.remote_back': '← Zurück zu deiner Seite', 'fedi.like_btn': 'Diesen Beitrag liken', 'fedi.or_reply': 'oder antworten:', 'fedi.liked_title': 'Geliked', 'fedi.liked_done': 'Dein Like ist unterwegs ins Fediverse.', 'fedi.boost_btn': 'Diesen Beitrag boosten', 'fedi.boosted_title': 'Geboostet', 'fedi.boosted_done': 'Dein Boost ist unterwegs ins Fediverse.', 'fedi.remote_interact': 'Übers Fediverse interagieren', 'fedi.report_open': 'Diesen Beitrag melden', 'fedi.report_where': 'Die Meldung geht an die Instanz und deren Moderator(en).', 'fedi.report_ph': 'Was ist das Problem? (optional)', 'fedi.report_send': 'Melden', 'fedi.reported_title': 'Gemeldet', 'fedi.reported_done': 'Deine Meldung wurde an den Server der Person gesendet. Deren Moderatoren prüfen sie.', 'fedi.delete_confirm': 'Diese Antwort löschen?', 'fedi.mod_remove_confirm': 'Diese Antwort aus deinem Thread entfernen? Sie kommt nicht zurück, auch nicht über Thread-Auffüllung.', 'fedi.mod_report_confirm': 'Diese Antwort beim Server des Autors melden?', 'fedi.manage_title': 'Meine Fediverse-Antworten', 'fedi.manage_empty': 'Du hast noch keine Antworten gesendet.', 'fedi.goto_post': 'Zur Post', 'fedi.edit': 'Bearbeiten', 'fedi.save_edit': 'Speichern', 'fedi.bm_label': 'Über meine Seite interagieren', 'fedi.bm_help': 'Zieh diesen Button in deine Lesezeichenleiste. Klick ihn dann auf einem beliebigen Fediverse-Beitrag (Mastodon, ein anderes Klonkt…), um über deine eigene Seite zu antworten, zu liken oder zu boosten.', 'tl.title': 'Zeitung', 'tl.lead': 'Folge Konten im Fediverse und sieh ihre Beiträge hier.', 'tl.follow_btn': 'Folgen', 'tl.following': 'Du folgst', 'tl.unfollow': 'Entfolgen', 'tl.autoboost': 'Hervorgehoben', 'tl.autoboost_follow': 'im Zirkel hervorheben', 'tl.autoboost_hint': 'Ihre neuen Beiträge erscheinen laufend in deinem Zirkel (lokal, kein Fediverse-Boost).', 'tl.moved_title': 'Dieses Konto ist umgezogen', 'tl.moved_lead': 'Neue Beiträge, Folgen, Likes und Antworten laufen jetzt über', 'tl.moved_hint': 'Lesen funktioniert hier weiterhin, und Antworten auf deine alten Beiträge kommen noch an. Zurück? Leere das Umzugsziel unter Aussehen.', 'tl.move_title': 'Deine Folgeliste mitnehmen', 'tl.move_hint': 'Ziehst du auf eine andere Adresse um? Deine Follower erfahren das automatisch, wem DU folgst aber nicht. Nimm diese Liste hier mit. Funktioniert auch von und zu Mastodon.', 'tl.move_export': 'Liste herunterladen (CSV)', 'tl.move_import_file': 'Wähle deine heruntergeladene CSV-Datei:', 'tl.move_import_lbl': 'Oder füge die Liste hier ein:', 'tl.move_import': 'Allen folgen', 'tl.pending': 'ausstehend', 'tl.unboost': 'Boost zurücknehmen', 'tl.feed': 'Beiträge', 'tl.tab_feed': 'Zeitung', 'tl.tab_following': 'Folge ich', 'tl.tab_replies': 'Antworten', 'tl.tab_followers': 'Follower', 'tl.followers': 'Follower', 'tl.followers_lead': 'Wer dir im Fediverse folgt, mit der letzten erfolgreichen Zustellung. Rot = nie zugestellt oder letzter Versuch fehlgeschlagen — nach einer Prüfung ein Kandidat zum Aufräumen.', 'tl.empty_followers': 'Noch keine Follower.', 'tl.last_delivery': 'Letzte Zustellung', 'tl.never_delivered': 'Nie zugestellt', 'tl.delivery_failed': 'letzter Versuch fehlgeschlagen', 'tl.remove_follower': 'Entfernen', 'tl.folreq_title': 'Folgeanfragen', 'tl.folreq_sub': 'Diese warten auf dein Ja oder Nein. Bis dahin sieht die anfragende Person nichts von deinen Beiträgen.', 'tl.folreq_accept': 'Annehmen', 'tl.folreq_deny': 'Ablehnen', 'tl.approve_toggle': 'Follower zuerst genehmigen', 'tl.approve_toggle_hint': 'An: Folgeanfragen warten hier auf dein Ja. Aus: alle dürfen sofort folgen.', 'tl.remove_confirm': 'Diesen Follower entfernen? Ein aktives Konto müsste dir erneut folgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'du folgst', 'tl.dir_follower': 'folgt dir', 'tl.dir_mutual': 'gegenseitig', 'tl.connect_empty': 'Noch keine Verbindungen. Folge oben jemandem, um zu starten.', 'tl.unreachable': 'Nicht erreichbar', 'tl.unreachable_lead': 'Diese Follower konnten wir nicht erreichen (nie zugestellt oder letzter Versuch fehlgeschlagen). Räume sie nach einer Prüfung auf.', 'msg.tab': 'Nachrichten', 'msg.title': 'Nachrichten', 'msg.filter_all': 'Alle', 'msg.filter_msgs': 'Nachrichten', 'msg.filter_conv': 'Gespräche', 'msg.filter_act': 'Aktivität', 'msg.filter_mod': 'Moderation', 'msg.filter_sent': 'Gesendet', 'msg.search_ph': 'Nachrichten durchsuchen…', 'msg.no_match': 'Nichts gefunden.', 'msg.poll_done': 'Deine Umfrage ist beendet', 'msg.poll_total': '{n} Teilnehmer', 'msg.you': 'Du', 'msg.sent_reply': 'hat über das Fediverse geantwortet', 'msg.and_more': 'und {n} andere', 'msg.liked_many': 'gefällt dein Beitrag', 'msg.boosted_many': 'teilten deinen Beitrag', 'msg.private': 'privat', 'msg.private_hint': 'Nur an dich gerichtet; erscheint nicht auf der öffentlichen Beitragsseite.', 'msg.new': 'Neu seit deinem letzten Besuch', 'msg.empty': 'Noch keine Nachrichten. Antworten, Erwähnungen und Aktivität erscheinen hier.', 'oauth.title': 'App autorisieren', 'oauth.wants_access': 'möchte sich mit deinem Klonkt-Konto verbinden.', 'oauth.post_as': 'Posten als', 'oauth.scope_read': 'Deine Beiträge, Antworten und Meldungen lesen', 'oauth.scope_write': 'In deinem Namen posten, antworten, liken und folgen', 'oauth.allow': 'Erlauben', 'oauth.deny': 'Ablehnen', 'oauth.foot': 'Du kannst den Zugriff später widerrufen. Autorisiere nur Apps, denen du vertraust.', 're.title': 'Antwort schreiben', 're.bold': 'Fett', 're.italic': 'Kursiv', 're.link': 'Link einfügen', 're.list': 'Aufzählung', 're.quote': 'Zitat', 're.lang': 'Sprache deiner Antwort', 're.attach': 'Medien hinzufügen (Bild, Audio, Video)', 're.attach_err': 'Upload fehlgeschlagen', 're.to': 'An:', 're.mention_del': 'Diese Person nicht mehr adressieren', 'tl.empty_following': 'Du folgst noch niemandem.', 'tl.empty': 'Noch nichts — folge jemandem, um Beiträge hier zu sehen.', 'tl.view_original': 'Original ansehen →', 'tl.open_player': 'Player öffnen', 'feed.load_more': 'Mehr laden', 'tl.paste_ph': 'URL eines Fediverse-Beitrags einfügen', 'tl.paste_go': 'Öffnen', 'tl.boosted': 'hat geteilt', 'tl.read_more': 'Mehr lesen', 'tl.show_less': 'Weniger', 'poll.vote': 'Abstimmen', 'poll.votes': 'Stimmen', 'poll.closed': 'geschlossen', 'poll.open': 'offen', 'poll.aria': 'Umfrage', 'poll.voter_one': 'Teilnehmer', 'poll.voter_many': 'Teilnehmer', 'poll.closes': 'endet am', 'poll.multiple': 'Mehrfachauswahl', 'poll.fedi_only': 'Abstimmen geht über das Fediverse — folge dieser Seite und stimme in deiner eigenen App ab.', 'poll.voted_title': 'Stimme gesendet', 'poll.voted_done': 'Deine Stimme ist unterwegs zur Umfrage. Die Ergebnisse aktualisieren sich, sobald die Autorin oder der Autor das Update sendet.',2188 'fedi.remote_title': 'Über das Fediverse antworten', 'fedi.follow_heading': 'Über das Fediverse folgen', 'fedi.profile_follow': 'Über das Fediverse folgen', 'profile.since': 'Auf Klonkt seit', 'profile.free': 'Kostenlos', 'fedi.follow_intro': 'Du folgst gleich:', 'fedi.follow_btn': 'Folgen', 'fedi.cancel': 'Abbrechen', 'fedi.followed_title': 'Folge-Anfrage gesendet ✅', 'fedi.followed_done': 'Deine Folge-Anfrage ist unterwegs. Sobald sie akzeptiert wird, erscheinen ihre Beiträge in deiner Timeline.', 'fedi.view_profile': 'Profil ansehen →', 'fedi.remote_reply': 'Über das Fediverse antworten', 'fedi.remote_prompt': 'Deine Fediverse-Adresse:', 'fedi.remote_notfound': 'Beitrag konnte nicht geladen werden. Füge die vollständige Beitrags-URL ein:', 'fedi.remote_load': 'Laden', 'fedi.remote_replying_to': 'Antwort an', 'fedi.cw_open': 'anzeigen', 'fedi.sensitive': 'Sensible Inhalte', 'fedi.open_full': 'Öffnen', 'fedi.src_short': 'Original', 'fedi.reply_short': 'Antworten', 'fedi.profile_short': 'Profil', 'search.fedi_looking': 'Wird im Fediverse gesucht…', 'fedi.remote_as': 'Wird als {site} gesendet.', 'fedi.remote_view_original': 'Ganzen Beitrag + Kommentare an der Quelle ansehen →', 'fedi.remote_reply_short': 'übers Fediverse', 'fedi.like_short': 'Liken', 'fedi.unlike_short': 'Like zurücknehmen', 'fedi.boost_short': 'Boosten', 'fedi.remote_ph': 'dein Server', 'fedi.remote_sent_title': 'Gesendet ✅', 'fedi.remote_sent': 'Deine Antwort wurde gesendet. Sie erscheint gleich beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Sieh sie dir dort an:', 'fedi.reply_where': 'Deine Antwort erscheint beim Originalbeitrag im Fediverse, nicht auf dieser Seite. Über den Link oben siehst du sie dort.', 'fedi.remote_back': '← Zurück zu deiner Seite', 'fedi.like_btn': 'Diesen Beitrag liken', 'fedi.or_reply': 'oder antworten:', 'fedi.liked_title': 'Geliked', 'fedi.liked_done': 'Dein Like ist unterwegs ins Fediverse.', 'fedi.boost_btn': 'Diesen Beitrag boosten', 'fedi.boosted_title': 'Geboostet', 'fedi.boosted_done': 'Dein Boost ist unterwegs ins Fediverse.', 'fedi.remote_interact': 'Übers Fediverse interagieren', 'fedi.report_open': 'Diesen Beitrag melden', 'fedi.report_where': 'Die Meldung geht an die Instanz und deren Moderator(en).', 'fedi.report_ph': 'Was ist das Problem? (optional)', 'fedi.report_send': 'Melden', 'fedi.reported_title': 'Gemeldet', 'fedi.reported_done': 'Deine Meldung wurde an den Server der Person gesendet. Deren Moderatoren prüfen sie.', 'fedi.delete_confirm': 'Diese Antwort löschen?', 'fedi.mod_remove_confirm': 'Diese Antwort aus deinem Thread entfernen? Sie kommt nicht zurück, auch nicht über Thread-Auffüllung.', 'fedi.mod_report_confirm': 'Diese Antwort beim Server des Autors melden?', 'fedi.manage_title': 'Meine Fediverse-Antworten', 'fedi.manage_empty': 'Du hast noch keine Antworten gesendet.', 'fedi.goto_post': 'Zur Post', 'fedi.edit': 'Bearbeiten', 'fedi.save_edit': 'Speichern', 'fedi.bm_label': 'Über meine Seite interagieren', 'fedi.bm_help': 'Zieh diesen Button in deine Lesezeichenleiste. Klick ihn dann auf einem beliebigen Fediverse-Beitrag (Mastodon, ein anderes Klonkt…), um über deine eigene Seite zu antworten, zu liken oder zu boosten.', 'tl.title': 'Zeitung', 'tl.lead': 'Folge Konten im Fediverse und sieh ihre Beiträge hier.', 'tl.follow_btn': 'Folgen', 'tl.following': 'Du folgst', 'tl.unfollow': 'Entfolgen', 'tl.autoboost': 'Hervorgehoben', 'tl.autoboost_follow': 'im Zirkel hervorheben', 'tl.autoboost_hint': 'Ihre neuen Beiträge erscheinen laufend in deinem Zirkel (lokal, kein Fediverse-Boost).', 'tl.moved_title': 'Dieses Konto ist umgezogen', 'tl.moved_lead': 'Neue Beiträge, Folgen, Likes und Antworten laufen jetzt über', 'tl.moved_hint': 'Lesen funktioniert hier weiterhin, und Antworten auf deine alten Beiträge kommen noch an. Zurück? Leere das Umzugsziel unter Aussehen.', 'tl.move_title': 'Deine Folgeliste mitnehmen', 'tl.move_hint': 'Ziehst du auf eine andere Adresse um? Deine Follower erfahren das automatisch, wem DU folgst aber nicht. Nimm diese Liste hier mit. Funktioniert auch von und zu Mastodon.', 'tl.move_export': 'Liste herunterladen (CSV)', 'tl.move_import_file': 'Wähle deine heruntergeladene CSV-Datei:', 'tl.move_import_lbl': 'Oder füge die Liste hier ein:', 'tl.move_import': 'Allen folgen', 'tl.pending': 'ausstehend', 'tl.unboost': 'Boost zurücknehmen', 'tl.feed': 'Beiträge', 'tl.tab_feed': 'Zeitung', 'tl.tab_following': 'Folge ich', 'tl.tab_replies': 'Antworten', 'tl.tab_followers': 'Follower', 'tl.followers': 'Follower', 'tl.followers_lead': 'Wer dir im Fediverse folgt, mit der letzten erfolgreichen Zustellung. Rot = nie zugestellt oder letzter Versuch fehlgeschlagen — nach einer Prüfung ein Kandidat zum Aufräumen.', 'tl.empty_followers': 'Noch keine Follower.', 'tl.last_delivery': 'Letzte Zustellung', 'tl.never_delivered': 'Nie zugestellt', 'tl.delivery_failed': 'letzter Versuch fehlgeschlagen', 'tl.remove_follower': 'Entfernen', 'tl.folreq_title': 'Folgeanfragen', 'tl.folreq_sub': 'Diese warten auf dein Ja oder Nein. Bis dahin sieht die anfragende Person nichts von deinen Beiträgen.', 'tl.folreq_accept': 'Annehmen', 'tl.folreq_deny': 'Ablehnen', 'tl.approve_toggle': 'Follower zuerst genehmigen', 'tl.approve_toggle_hint': 'An: Folgeanfragen warten hier auf dein Ja. Aus: alle dürfen sofort folgen.', 'tl.remove_confirm': 'Diesen Follower entfernen? Ein aktives Konto müsste dir erneut folgen.', 'tl.tab_connect': 'Connect', 'tl.connect': 'Connect', 'tl.dir_following': 'du folgst', 'tl.dir_follower': 'folgt dir', 'tl.dir_mutual': 'gegenseitig', 'tl.connect_empty': 'Noch keine Verbindungen. Folge oben jemandem, um zu starten.', 'tl.unreachable': 'Nicht erreichbar', 'tl.unreachable_lead': 'Diese Follower konnten wir nicht erreichen (nie zugestellt oder letzter Versuch fehlgeschlagen). Räume sie nach einer Prüfung auf.', 'msg.tab': 'Nachrichten', 'msg.title': 'Nachrichten', 'msg.filter_all': 'Alle', 'msg.filter_msgs': 'Nachrichten', 'msg.filter_conv': 'Gespräche', 'msg.filter_act': 'Aktivität', 'msg.filter_mod': 'Moderation', 'msg.filter_sent': 'Gesendet', 'msg.search_ph': 'Nachrichten durchsuchen…', 'msg.no_match': 'Nichts gefunden.', 'msg.poll_done': 'Deine Umfrage ist beendet', 'msg.poll_total': '{n} Teilnehmer', 'msg.you': 'Du', 'msg.sent_reply': 'hat über das Fediverse geantwortet', 'msg.and_more': 'und {n} andere', 'msg.liked_many': 'gefällt dein Beitrag', 'msg.boosted_many': 'teilten deinen Beitrag', 'msg.private': 'privat', 'msg.private_hint': 'Nur an dich gerichtet; erscheint nicht auf der öffentlichen Beitragsseite.', 'msg.new': 'Neu seit deinem letzten Besuch', 'msg.empty': 'Noch keine Nachrichten. Antworten, Erwähnungen und Aktivität erscheinen hier.', 'oauth.title': 'App autorisieren', 'oauth.wants_access': 'möchte sich mit deinem Klonkt-Konto verbinden.', 'oauth.post_as': 'Posten als', 'oauth.scope_read': 'Deine Beiträge, Antworten und Meldungen lesen', 'oauth.scope_write': 'In deinem Namen posten, antworten, liken und folgen', 'oauth.allow': 'Erlauben', 'oauth.deny': 'Ablehnen', 'oauth.foot': 'Du kannst den Zugriff später widerrufen. Autorisiere nur Apps, denen du vertraust.', 're.title': 'Antwort schreiben', 're.bold': 'Fett', 're.italic': 'Kursiv', 're.link': 'Link einfügen', 're.list': 'Aufzählung', 're.quote': 'Zitat', 're.lang': 'Sprache deiner Antwort', 're.attach': 'Medien hinzufügen (Bild, Audio, Video)', 're.attach_err': 'Upload fehlgeschlagen', 're.to': 'An:', 're.mention_del': 'Diese Person nicht mehr adressieren', 'tl.empty_following': 'Du folgst noch niemandem.', 'tl.empty': 'Noch nichts — folge jemandem, um Beiträge hier zu sehen.', 'tl.view_original': 'Original ansehen →', 'tl.open_player': 'Player öffnen', 'feed.load_more': 'Mehr laden', 'tl.paste_ph': 'URL eines Fediverse-Beitrags einfügen', 'tl.paste_go': 'Öffnen', 'tl.boosted': 'hat geteilt', 'tl.read_more': 'Mehr lesen', 'tl.show_less': 'Weniger', 'poll.vote': 'Abstimmen', 'poll.votes': 'Stimmen', 'poll.closed': 'geschlossen', 'poll.open': 'offen', 'poll.aria': 'Umfrage', 'poll.voter_one': 'Teilnehmer', 'poll.voter_many': 'Teilnehmer', 'poll.closes': 'endet am', 'poll.multiple': 'Mehrfachauswahl', 'poll.fedi_only': 'Abstimmen geht über das Fediverse — folge dieser Seite und stimme in deiner eigenen App ab.', 'poll.voted_title': 'Stimme gesendet', 'poll.voted_done': 'Deine Stimme ist unterwegs zur Umfrage. Die Ergebnisse aktualisieren sich, sobald die Autorin oder der Autor das Update sendet.', 2189 2189 'comments.to_start': 'um das Gespräch zu starten.', 2190 2190 'comments.reply': 'Antworten', 'comments.delete': 'Löschen', 'comments.cancel': 'Abbrechen', -
src/views/pages/authorize-interaction.ejs
r3ceaef6 r9eb6e1b 106 106 </form> 107 107 <% } 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 }) %> 109 109 <% } %> 110 110 <% } %> -
src/views/pages/search.ejs
r3ceaef6 r9eb6e1b 40 40 <p class="auth-interact-note"><%= t('search.fedi_signin') %></p> 41 41 <% } 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) }) %> 43 43 <% } 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 }) %> 45 45 <% } else { %> 46 46 <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p> -
src/views/partials/fedi-interact-styles.ejs
r3ceaef6 r9eb6e1b 7 7 .auth-interact { max-width: 640px; margin: 1rem auto; padding: 0 1rem; } 8 8 .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 }15 9 .auth-interact-form { display: flex; flex-direction: column; gap: .75rem; } 16 10 .auth-interact-form textarea, .auth-interact-form input[type=url] { … … 21 15 .auth-interact-actions { display: flex; gap: .5rem; align-items: center; } 22 16 .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); }47 17 .fedi-manage { list-style: none; padding: 0; margin: 1.25rem 0 1rem; display: flex; flex-direction: column; gap: .7rem; } 48 18 .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; } … … 87 57 .fedi-edit-save { padding: .4rem 1rem; border: 0; border-radius: 999px; background: var(--accent); color: var(--paper, #fff); 88 58 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; 92 137 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; 96 141 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 } 111 184 </style> -
src/views/partials/remote-actor.ejs
r3ceaef6 r9eb6e1b 2 2 Een PERSOON van een andere server: wie het is, en de knop om te volgen. 3 3 4 Zelfde reden als remote-object: de zoekbalk en /authorize_interaction laten5 hetzelfde zien, dus staat het op één plek. Het volgen zelf gaat naar6 /authorize_interaction/follow, want daar zit de poort van de guardians7 (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. 8 8 9 Verwacht: actor (uit resolveRemoteActor). Optioneel: cancelUrl, siteTitle. 9 Verwacht: actor (uit resolveRemoteActor). Optioneel: compact (de 10 live-preview), siteTitle. 10 11 %> 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 1 1 <%# 2 Een object van een andere server: hoe heteruit ziet, en wat je ermee kunt.2 Een post van een andere server: hoe hij eruit ziet, en wat je ermee kunt. 3 3 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. 10 15 11 16 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 14 26 %> 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 %>&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> 32 104 </a> 33 105 <% } %> 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 178 178 <%# De teksten staan op het element dat ze gebruikt, niet in het script: een 179 179 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') }) %>"> 182 182 <div class="container"> 183 183 <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search"> … … 467 467 } 468 468 /* 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; } 470 470 .search-suggest:empty { display: none; } 471 471 .ss-group { padding: .25rem 0; } -
src/views/shell.ejs
r3ceaef6 r9eb6e1b 130 130 // ontwikkeling een jaar gecachet, dus zonder bump bereikt een reparatie 131 131 // iedereen behalve wie de fout al heeft. 132 const MOD_V = 6 4;132 const MOD_V = 65; 133 133 %><!DOCTYPE html> 134 134 <html lang="<%- _e(lang) %>" data-palette="<%- _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>"> -
test/search-fediverse.test.js
r3ceaef6 r9eb6e1b 37 37 // teller: "is er überhaupt opgehaald" is de helft van wat deze toets bewaakt. 38 38 let gevraagd = []; 39 AP.resolveRemoteNote = async (uri) => { 39 let opties = []; 40 AP.resolveRemoteNote = async (uri, opts) => { 40 41 gevraagd.push(['note', uri]); 42 opties.push(opts || {}); 41 43 if (!uri.includes('/notes/')) return null; 42 44 return { … … 45 47 actor_handle: '@oma@elders.test', actor_name: 'Oma', actor_icon: '', 46 48 content: '<p>Een bericht van ver weg</p>', images: [], media: [], poll: null, 47 preview: 'Een bericht van ver weg', threadInboxes: [], localPostId: '', sensitive: false, cw: '', 49 preview: 'Een bericht van ver weg', threadInboxes: [], localPostId: '', 50 sensitive: uri.includes('/cw'), cw: uri.includes('/cw') ? 'Spinnen' : '', 48 51 }; 49 52 }; … … 176 179 // En de opmaak reist mee: die staat sinds deze ronde in een eigen partial, 177 180 // en een render zonder opmaak ziet er kapot uit zonder dat er iets faalt. 178 assert.ok(html.includes('. auth-interact-preview'), 'de opmaak van het blokontbreekt');181 assert.ok(html.includes('.rp-card'), 'de opmaak van de kaart ontbreekt'); 179 182 }); 180 183 … … 188 191 assert.ok(html.includes('@oma@elders.test')); 189 192 }); 193 194 // ── De live-preview onder de zoekbalk ─────────────────────────────────────── 195 // 196 // Plak je een link, dan laadt de uitklaplijst de post zelf. Deze route wordt bij 197 // elke toetsaanslag geraakt, en dat maakt de rechtencheck hier zwaarder dan op 198 // de pagina: zonder hem is dit een haalservice waarmee iedereen deze server op 199 // adressen naar keuze laat afsturen. 200 201 const preview = async (q) => { 202 gevraagd = []; opties = []; 203 const r = await fetch(`http://127.0.0.1:${poort}/search/remote?q=${encodeURIComponent(q)}`, { signal: AbortSignal.timeout(10000) }); 204 return { status: r.status, html: await r.text(), cache: r.headers.get('cache-control') }; 205 }; 206 207 test('de preview laadt de post zelf, ondertekend als de site', async () => { 208 const { status, html, cache } = await preview('https://elders.test/notes/1'); 209 assert.equal(status, 200); 210 assert.ok(html.includes('Een bericht van ver weg'), 'de inhoud hoort in de preview te staan'); 211 assert.ok(html.includes('@oma@elders.test')); 212 assert.equal(opties[0].asSlug, 'robo', 'een post voor volgers weigert een anonieme GET'); 213 // Wat iemand ziet hangt van zijn rechten en reacties af. 214 assert.match(cache || '', /no-store/); 215 }); 216 217 test('in de preview kun je waarderen en boosten, antwoorden gaat naar de volle pagina', async () => { 218 const { html } = await preview('https://elders.test/notes/1'); 219 for (const actie of ['/authorize_interaction/like', '/authorize_interaction/boost']) { 220 assert.ok(html.includes(`action="${actie}"`), `${actie} hoort in de preview te staan`); 221 } 222 // Een editor in een uitklaplijst is op een telefoon geen plek om te 223 // schrijven: de antwoordknop wijst naar de volle pagina, met het venster open. 224 assert.ok(!html.includes('action="/authorize_interaction"'), 'geen antwoordvenster in de preview'); 225 assert.ok(!html.includes('action="/authorize_interaction/report"'), 'melden gebeurt op de volle pagina'); 226 assert.ok(html.includes('/authorize_interaction?uri=https%3A%2F%2Felders.test%2Fnotes%2F1&reply=1'), 227 'de antwoordknop moet naar de volle pagina met het venster al open'); 228 }); 229 230 test('een profiel in de preview geeft de volgknop', async () => { 231 const { status, html } = await preview('https://elders.test/u/oma'); 232 assert.equal(status, 200); 233 assert.ok(html.includes('action="/authorize_interaction/follow"')); 234 }); 235 236 test('een bezoeker laat via de preview niets ophalen', async () => { 237 ingelogd = false; 238 try { 239 const { status, html } = await preview('https://elders.test/notes/1'); 240 // 204 en geen 403: voor een bezoeker is een geplakte link gewoon een 241 // zoekterm, en de zoekbalk valt dan terug op de gewone suggesties. 242 assert.equal(status, 204); 243 assert.deepEqual(gevraagd, [], 'zonder rechten hoort er geen enkel verzoek uit te gaan'); 244 assert.equal(html, ''); 245 } finally { 246 ingelogd = true; 247 } 248 }); 249 250 test('een zoekterm in de preview haalt niets op', async () => { 251 const { status } = await preview('soundfabrics.nl'); 252 assert.equal(status, 204); 253 assert.deepEqual(gevraagd, []); 254 }); 255 256 test('een inhoudswaarschuwing blijft dicht tot je ervoor kiest', async () => { 257 // De oude render toonde alles open, ook wat de schrijver achter een 258 // waarschuwing had gezet. 259 const { html } = await preview('https://elders.test/notes/cw'); 260 const cw = html.indexOf('<details class="rp-cw">'); 261 assert.ok(cw >= 0, 'de post hoort achter een waarschuwing te staan'); 262 assert.ok(html.indexOf('Spinnen', cw) > cw, 'met de tekst van de waarschuwing erop'); 263 assert.ok(html.indexOf('Een bericht van ver weg') > cw, 'en de inhoud erachter, niet ervoor'); 264 }); 265 266 test('de interactiepagina opent het antwoordvenster op verzoek, en haalt ondertekend op', async () => { 267 const pagina = async (extra) => { 268 opties = []; 269 const r = await fetch( 270 `http://127.0.0.1:${poort2}/authorize_interaction?uri=${encodeURIComponent('https://elders.test/notes/1')}${extra}`, 271 { signal: AbortSignal.timeout(10000) }, 272 ); 273 return r.text(); 274 }; 275 const dicht = await pagina(''); 276 assert.match(dicht, /<section class="rp-reply" id="rp-reply" hidden>/, 'zonder verzoek is het venster dicht'); 277 assert.equal(opties[0].asSlug, 'robo', 'ook hier ondertekend, anders "niet gevonden" voor een volgerspost'); 278 const open = await pagina('&reply=1'); 279 assert.match(open, /<section class="rp-reply" id="rp-reply">/, 'met ?reply=1 staat het open, ook zonder JS'); 280 assert.ok(open.includes('action="/authorize_interaction"'), 'en het venster heeft zijn formulier'); 281 }); 282
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)