source: Klonkt/src/views/pages/news.ejs@ 439f095

main
Last change on this file since 439f095 was d9ad6c5, checked in by Robin Genis <roboburr@…>, 6 weeks ago

Een hulpvraag hoort in Berichten, niet in de Krant

Een 🛟 van een ward kwam bij de guardian op twee plekken binnen: als mention in
Berichten en de Guardian PWA, maar ook als gewone post in de Krant. Op
sound-fabrics.com stonden vijf van de zes hulpvragen in allebei.

De oorzaak zat in de inbox: de tijdlijn-insert vroeg alleen "is dit een
top-level post van iemand die ik volg" en keek niet naar wie de post geadresseerd
was. Dat is nu belongsInTimeline: een directe note is aan iemand persoonlijk
gericht, dus een bericht en geen post. Dat dekt meteen de wave en de gewone DM,
die om dezelfde reden in de Krant terechtkwamen. De self-heal ruimt de al
opgeslagen exemplaren op, beperkt tot de twee soorten die achteraf nog te
herkennen zijn; een publieke mention van iemand die je volgt is wel een post en
blijft staan.

Tweede helft: de weergave gelijkgetrokken. De Krant rendert een post met
emoji's, een quote- of linkkaart en de media; Berichten liet daar niks van zien
(zelfs de shortcodes bleven staan, want ap_mentions had geen emoji_json) en de
Guardian PWA plakte de kale content in een div. Die opmaak zat bovendien in de
<style> van de Krant zelf, dus een post buiten de Krant kwam sowieso ongestyled
binnen.

Nu is er één partial, note-body, met de opmaak in shared-styles ernaast. Alle
drie de oppervlakken gebruiken hem: de PWA bouwt zijn kaarten in de browser en
krijgt de body server-side gerenderd mee. ap_mentions en ap_interactions kregen
de kolommen die daarvoor nodig zijn, gevuld bij binnenkomst, met de quote- of
linkkaart out of band zoals de tijdlijn dat al deed.

En passant: de embed-gate stond alleen op de C2S-read, dus een ward zag in de
web-Krant nog steeds linkvoorbeelden die de guardians hadden uitgezet. Die gate
zit nu ook op /news en /messages.

Changed files:
src/services/ActivityPubService.js

  • belongsInTimeline: een directe note is geen tijdlijn-post
  • self-heal v21 verwijdert al opgeslagen hulpvragen en waves uit ap_timeline
  • resolveCard: quote of linkvoorbeeld, één kaart, out of band opgelost
  • mentions en replies slaan emoji's, media en die kaart op
  • getNotifications geeft die kolommen door aan Berichten

src/config/database.js

  • kolommen op ap_mentions en ap_interactions voor emoji's, media, quote, embed

src/middleware/render.js

  • renderNoteBody: dezelfde partial als string, voor niet-EJS oppervlakken

src/routes/posts.js

  • gateEmbeds op /news en /messages (FEP-633c gated feature)

src/routes/guardian.js

  • hulpvragen krijgen body_html en name_html mee

src/assets/js/guardian.js

  • kaart rendert die body in plaats van de kale content

src/assets/css/guardian.css

  • opmaak voor de gedeelde post-body in de kleuren van de PWA

src/views/partials/tl-item.ejs

  • body vervangen door de gedeelde partial

src/views/partials/msg-item.ejs

  • idem, plus een 🛟-markering bij een hulpvraag

src/views/partials/shared-styles.ejs

  • .tl-content, .tl-quote* en .tl-media* hierheen verhuisd

src/views/pages/news.ejs

  • die regels weggehaald, alleen Krant-eigen opmaak blijft

src/services/i18n.js

  • msg.help_request in nl, en, de

New file:
src/views/partials/note-body.ejs

  • de body van een post: content, quote/linkkaart, media

test/help-request-timeline.test.js

  • een 🛟 blijft uit de Krant en in Berichten

test/note-body-shared.test.js

  • één renderer, en beide views gaan er doorheen

remarks: geverifieerd op een wegwerp-database in de browser: de hulpvraag
verdwijnt bij het opstarten uit de Krant en staat mét quote en capture in
Berichten en de PWA. Nog niet uitgerold.

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

  • Property mode set to 100644
File size: 17.7 KB
RevLine 
[914eb9f]1<div class="tl-wrap">
[9e5438e]2 <%- include('../partials/fedi-tabs', { active: 'feed' }) %>
[c6cdce6]3 <div class="tl-titlebar">
[37a297d]4 <h1 class="tl-title"><%= t('tl.title') %></h1>
5 <button type="button" id="tl-paste-btn" class="tl-paste-btn" title="<%= t('fedi.remote_interact') %>" aria-label="<%= t('fedi.remote_interact') %>">
6 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
7 </button>
8 </div>
[914eb9f]9 <p class="tl-lead"><%= t('tl.lead') %></p>
[37a297d]10
11 <div class="tl-paste" id="tl-paste">
12 <div class="tl-paste-card" role="dialog" aria-label="<%= t('fedi.remote_interact') %>">
13 <button type="button" class="tl-paste-x" aria-label="<%= t('fedi.cancel') %>">&times;</button>
14 <h3 class="tl-paste-title"><%= t('fedi.remote_interact') %></h3>
15 <input type="url" class="tl-paste-input" inputmode="url" autocomplete="off" autocapitalize="none" spellcheck="false" placeholder="<%= t('tl.paste_ph') %>">
16 <div class="tl-paste-actions">
17 <button type="button" class="tl-paste-cancel"><%= t('fedi.cancel') %></button>
18 <button type="button" class="tl-paste-go"><%= t('tl.paste_go') %></button>
19 </div>
20 </div>
21 </div>
22 <style>
[c6cdce6]23 .tl-titlebar { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
24 .tl-boost-by { display: flex; align-items: center; gap: .35rem; font-size: .8rem; color: var(--ink-soft); margin: 0 0 .45rem .2rem; }
25 .tl-boost-by svg { color: #16a34a; flex: 0 0 auto; }
26 .tl-boost-by strong { color: var(--ink); font-weight: 600; }
[37a297d]27 .tl-paste-btn { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px;
28 border: 1px solid var(--rule); border-radius: 999px; background: var(--paper-2); color: var(--ink-soft); cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
29 .tl-paste-btn:hover { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
30 .tl-paste { position: fixed; inset: 0; z-index: 2147483601; display: none; align-items: center; justify-content: center; padding: 1rem; background: rgba(0,0,0,.55); }
31 .tl-paste.is-open { display: flex; }
32 .tl-paste-card { position: relative; width: 100%; max-width: 460px; background: var(--paper); color: var(--ink); border: 1px solid var(--rule);
33 border-radius: 16px; padding: 1.4rem 1.4rem 1.2rem; box-shadow: 0 18px 50px rgba(0,0,0,.4); }
34 .tl-paste-x { position: absolute; top: .5rem; right: .6rem; width: 2rem; height: 2rem; border: 0; border-radius: 999px; background: transparent; color: var(--ink-soft); font-size: 1.4rem; line-height: 1; cursor: pointer; }
35 .tl-paste-title { margin: 0 0 .8rem; font-size: 1.1rem; }
36 .tl-paste-input { width: 100%; box-sizing: border-box; padding: .6rem .8rem; border: 1.5px solid var(--rule); border-radius: 10px; background: var(--paper-2); color: var(--ink); font: inherit; font-size: .95rem; }
37 .tl-paste-input:focus { outline: none; border-color: var(--accent); }
38 .tl-paste-actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: 1rem; }
39 .tl-paste-actions button { padding: .5rem 1.1rem; border-radius: 999px; font: inherit; font-weight: 600; font-size: .88rem; cursor: pointer; border: 0; }
40 .tl-paste-cancel { background: transparent; border: 1.5px solid var(--rule); color: var(--ink); }
41 .tl-paste-go { background: var(--accent); color: var(--paper); }
42 </style>
43 <script>
44 (function () {
45 if (window.__tlPasteWired) return; window.__tlPasteWired = true;
46 function modal() { return document.getElementById('tl-paste'); }
47 function openM() {
48 var m = modal(); if (!m) return;
49 var inp = m.querySelector('.tl-paste-input');
50 m.classList.add('is-open');
51 if (inp) {
52 inp.value = ''; inp.focus();
53 if (navigator.clipboard && navigator.clipboard.readText) {
54 navigator.clipboard.readText().then(function (t) {
55 t = (t || '').trim();
56 if (/^https?:\/\//i.test(t) && !inp.value) { inp.value = t; inp.select(); }
57 }).catch(function () {});
58 }
59 }
60 }
61 function closeM() { var m = modal(); if (m) m.classList.remove('is-open'); }
62 function go() {
63 var m = modal(); if (!m) return;
64 var inp = m.querySelector('.tl-paste-input'), v = (inp ? inp.value : '').trim();
65 if (!/^https?:\/\//i.test(v)) { if (inp) inp.focus(); return; }
66 location.href = '/authorize_interaction?uri=' + encodeURIComponent(v);
67 }
68 document.addEventListener('click', function (e) {
69 if (e.target.closest && e.target.closest('#tl-paste-btn')) { e.preventDefault(); openM(); return; }
70 if (e.target.closest && e.target.closest('.tl-paste-go')) { e.preventDefault(); go(); return; }
71 if (e.target.closest && (e.target.closest('.tl-paste-cancel') || e.target.closest('.tl-paste-x'))) { e.preventDefault(); closeM(); return; }
72 var op = document.querySelector('.tl-paste.is-open'); if (op && e.target === op) closeM();
73 });
74 document.addEventListener('keydown', function (e) {
75 var m = document.querySelector('.tl-paste.is-open'); if (!m) return;
76 if (e.key === 'Escape') closeM();
77 else if (e.key === 'Enter' && e.target.closest && e.target.closest('.tl-paste')) { e.preventDefault(); go(); }
78 });
79 })();
80 </script>
[914eb9f]81 <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
82 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
83
84 <% if (!timeline || !timeline.length) { %>
[1ecbf71]85 <div class="tl-empty">
86 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
87 <p><%= t('tl.empty') %></p>
88 </div>
[914eb9f]89 <% } else { %>
[7b04d3b]90 <ol class="tl-feed" id="tl-feed">
[914eb9f]91 <% timeline.forEach(function(p){ %>
[7b04d3b]92 <%- include('../partials/tl-item', { p: p }) %>
[914eb9f]93 <% }); %>
94 </ol>
[7b04d3b]95 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#tl-feed', morePath: '/news' }) %>
[914eb9f]96 <% } %>
97</div>
98
99<style>
[dee7f02]100 .tl-wrap { max-width: 640px; margin: 1rem auto 3rem; padding: 0 1rem; }
[1ecbf71]101 .tl-title { margin: 0 0 .15rem; font-size: clamp(1.5rem, 4vw, 1.8rem); }
102 .tl-lead { color: var(--ink-soft, #888); margin: 0 0 1.5rem; }
103 .tl-empty { text-align: center; color: var(--ink-soft, #888); margin: 3rem auto; }
104 .tl-empty svg { width: 44px; height: 44px; color: color-mix(in srgb, var(--accent, #888) 60%, transparent); margin: 0 auto .75rem; display: block; }
105
106 .tl-feed { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1rem; }
107 .tl-item { padding: 1.1rem 1.15rem; border-radius: 16px;
108 border: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent);
109 background: color-mix(in srgb, var(--ink, #000) 2.5%, transparent);
110 transition: border-color .15s ease, box-shadow .15s ease; }
111 .tl-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 32%, transparent);
112 box-shadow: 0 2px 14px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
113
114 .tl-head { display: flex; align-items: center; gap: .65rem; margin: 0 0 .65rem; }
115 .tl-avatar { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%; overflow: hidden;
116 display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.1rem;
117 background: color-mix(in srgb, var(--accent, #888) 22%, transparent); color: var(--accent, #555);
118 box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #888) 18%, transparent); }
[7eee449]119 .tl-avatar img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
[1ecbf71]120 .tl-id { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
121 .tl-author { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
[914eb9f]122 .tl-author:hover { text-decoration: underline; }
[1ecbf71]123 .tl-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
124 .tl-time { color: var(--ink-soft, #999); font-size: .78rem; flex: 0 0 auto; align-self: flex-start; white-space: nowrap; }
125
[d9ad6c5]126 /* .tl-content, .tl-quote* and .tl-media* now live in partials/shared-styles,
127 next to partials/note-body: the markup is shared with Berichten and the
128 Guardian PWA, so the styling has to be too. What stays here is Krant-only. */
[f1627b6]129 /* Long posts collapse to a max height with a fade + "read more" (added by JS only when it overflows). */
130 .tl-content.tl-clamp { max-height: 20em; overflow: hidden;
131 -webkit-mask-image: linear-gradient(180deg, #000 72%, transparent);
132 mask-image: linear-gradient(180deg, #000 72%, transparent); }
133 .tl-readmore { display: inline-block; margin: .15rem 0 .1rem; padding: .15rem 0;
134 background: none; border: 0; color: var(--accent, #06c); font: inherit; font-weight: 600;
135 font-size: .85rem; cursor: pointer; }
136 .tl-readmore:hover { text-decoration: underline; }
[1ecbf71]137
[6053c6c]138 .tl-poll { margin: .75rem 0 0; display: flex; flex-direction: column; gap: 8px; }
139 .tl-poll-form { display: flex; flex-direction: column; gap: 8px; }
140 .tl-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--line, rgba(128,128,128,.3)); border-radius: 10px; cursor: pointer; }
141 .tl-poll-choice:hover { border-color: var(--accent); }
142 .tl-poll-choice input { accent-color: var(--accent); }
143 .tl-poll-btn { align-self: flex-start; margin-top: 2px; }
144 .tl-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; background: var(--paper-2, rgba(128,128,128,.08)); display: flex; align-items: center; gap: 8px; }
145 .tl-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
146 .tl-poll-res.is-mine .tl-poll-fill { background: color-mix(in srgb, var(--accent) 40%, transparent); }
147 .tl-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
148 .tl-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
149 .tl-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); }
[1ecbf71]150
151 .tl-embed { margin: .75rem 0 0; border-radius: 12px; overflow: hidden; background: var(--paper-2, #111); }
152 .tl-embed-frame { width: 100%; border: 0; display: block; aspect-ratio: 16 / 9; }
153 .tl-embed-spotify { aspect-ratio: auto; height: 152px; }
154 .tl-embed-sc { aspect-ratio: auto; height: 166px; }
[84903a1]155 .tl-embed-klonkt { aspect-ratio: auto; height: 180px; background: var(--paper, #fff); }
[d22b55c]156 .tl-embed-bandcamp { aspect-ratio: auto; height: 470px; }
157 .tl-embed-apple { aspect-ratio: auto; height: 175px; }
[ca0ad44]158 .tl-embed-open { display: inline-block; margin: .45rem .9rem 0 0; font-size: .82rem; font-weight: 600; color: var(--accent); text-decoration: none; }
159 .tl-embed-open:hover { text-decoration: underline; }
[1ecbf71]160
161 .tl-orig { display: inline-block; margin: .7rem 0 0; font-size: .82rem; color: var(--accent, #06c); text-decoration: none; }
162 .tl-orig:hover { text-decoration: underline; }
163
[cb3d5fc]164 /* Uniform circular action buttons with coloured icons (no emoji). */
165 .tl-actions { display: flex; gap: .5rem; margin-top: .9rem; align-items: center; }
166 .tl-act-form { margin: 0; display: inline-flex; }
167 .tl-act { width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%; padding: 0; box-sizing: border-box;
168 display: inline-flex; align-items: center; justify-content: center;
169 border: 1px solid color-mix(in srgb, var(--ink, #000) 12%, transparent);
170 background: color-mix(in srgb, var(--ink, #000) 3%, transparent);
171 color: var(--ink-soft, #888); cursor: pointer; -webkit-tap-highlight-color: transparent;
172 transition: background .12s, border-color .12s, transform .1s; }
173 .tl-act svg { width: 18px; height: 18px; display: block; }
174 .tl-act:active { transform: scale(.9); }
175 .tl-act-like svg { color: #e8b04b; }
176 .tl-act-like:hover { background: color-mix(in srgb, #e8b04b 15%, transparent); border-color: color-mix(in srgb, #e8b04b 50%, transparent); }
[9d34855]177 .tl-act-like.is-on { background: color-mix(in srgb, #e8b04b 18%, transparent); border-color: color-mix(in srgb, #e8b04b 60%, transparent); }
[cb3d5fc]178 .tl-act-boost svg { color: #2fa85a; }
179 .tl-act-boost:hover { background: color-mix(in srgb, #2fa85a 15%, transparent); border-color: color-mix(in srgb, #2fa85a 50%, transparent); }
180 .tl-act-boost.is-on { background: color-mix(in srgb, #2fa85a 18%, transparent); border-color: color-mix(in srgb, #2fa85a 60%, transparent); }
181 .tl-act-reply svg { color: var(--accent, #06c); }
182 .tl-act-reply:hover { background: color-mix(in srgb, var(--accent, #888) 14%, transparent); border-color: color-mix(in srgb, var(--accent, #888) 50%, transparent); }
183 .tl-act-block svg { color: #e0524d; }
184 .tl-act-block:hover { background: color-mix(in srgb, #d9534f 14%, transparent); border-color: color-mix(in srgb, #d9534f 50%, transparent); }
[914eb9f]185</style>
[d988fa0]186
187<script>
188(function(){
189 if (window.__fediRemoteWired) return; window.__fediRemoteWired = true;
190 var current = null, currentBtn = null;
191 function close(){ if (current) { current.remove(); current = null; currentBtn = null; } }
[d22b55c]192 function go(raw, uri){ var d=(raw||'').trim().replace(/^@?[^@\s]*@/,'').replace(/^https?:\/\//i,'').replace(/\/.*$/,'').trim(); if(d){ try{ localStorage.setItem('pcmsFediServer', d); }catch(e){} location.href='https://'+d+'/authorize_interaction?uri='+encodeURIComponent(uri||''); } }
[d988fa0]193 function place(f, b){ var r=b.getBoundingClientRect(); f.style.top=(r.bottom+window.scrollY+6)+'px'; f.style.left=Math.max(8, Math.min(r.left+window.scrollX, window.scrollX+window.innerWidth-340))+'px'; }
194 document.addEventListener('click', function(e){
195 if (e.target.closest && e.target.closest('.fedi-remote-cancel')) { close(); return; }
196 if (current && e.target.closest && e.target.closest('.fedi-remote-form')) return;
197 var b = e.target.closest && e.target.closest('.fedi-remote-reply-btn');
198 if (b) {
199 e.preventDefault();
200 if (currentBtn === b) { close(); return; }
201 close();
202 var f = document.createElement('form'); f.className='fedi-remote-form'; f.dataset.uri = b.getAttribute('data-fedi-uri')||'';
[bca83f3]203 f.innerHTML = '<input type="text" autocomplete="off" spellcheck="false"><button type="submit" class="btn btn-primary fedi-remote-go" aria-label="ok">&rarr;</button><button type="button" class="fedi-remote-cancel" aria-label="x">&times;</button>';
[d22b55c]204 var _inp = f.querySelector('input'); _inp.placeholder = b.getAttribute('data-fedi-ph') || 'mastodon.social';
205 try{ var _sv = localStorage.getItem('pcmsFediServer'); if(_sv) _inp.value = _sv; }catch(e){}
206 document.body.appendChild(f); place(f, b); current=f; currentBtn=b; _inp.focus(); _inp.select();
[d988fa0]207 return;
208 }
209 if (current) close();
210 });
211 document.addEventListener('submit', function(e){ var f=e.target.closest && e.target.closest('.fedi-remote-form'); if(!f) return; e.preventDefault(); go(f.querySelector('input').value, f.dataset.uri); });
212 document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
213 window.addEventListener('scroll', close, true);
214})();
[0a75356]215
[fe680aa]216/* Short feed videos (gif/cover loops like an animated cover) autoplay + loop muted like a GIF;
217 longer real videos keep their controls. Decided on the actual duration once metadata loads. */
218(function(){
219 document.querySelectorAll('.tl-media-video').forEach(function(v){
220 if (v.dataset.gifWired) return; v.dataset.gifWired = '1';
221 var decide = function(){
222 if (v.duration && v.duration <= 30) {
223 v.removeAttribute('controls'); v.loop = true; v.muted = true; v.play().catch(function(){});
224 }
225 };
226 if (v.readyState >= 1) decide(); else v.addEventListener('loadedmetadata', decide, { once: true });
227 });
228})();
229
[0a75356]230/* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page (no reload, no banner). */
231(function(){
232 if (window.__newsReactWired) return; window.__newsReactWired = true;
233 document.addEventListener('submit', function(e){
234 var f = e.target.closest && e.target.closest('.tl-react-form');
235 if (!f) return;
236 e.preventDefault();
237 var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
238 btn.disabled = true;
239 var body = new URLSearchParams();
240 new FormData(f).forEach(function(v, k){ body.append(k, v); });
241 fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
242 .then(function(r){ return r.ok ? r.json() : null; })
243 .then(function(j){ if (j) btn.classList.toggle('is-on', !!j.on); })
244 .catch(function(){})
245 .then(function(){ btn.disabled = false; });
246 });
247})();
[d988fa0]248</script>
[f1627b6]249
250<script>
251// Collapse long post bodies to a max height with a "read more" toggle — only when the content
252// actually overflows. Runs on every /news render (full load + htmx swap); a per-element flag
253// prevents double-wiring.
254(function(){
255 var RM = '<%= t("tl.read_more") %>', SL = '<%= t("tl.show_less") %>';
256 document.querySelectorAll('.tl-content:not(.nsfw-media):not([data-rm])').forEach(function(c){
257 c.setAttribute('data-rm', '1');
258 if (c.scrollHeight > 360) {
259 c.classList.add('tl-clamp');
260 var b = document.createElement('button');
261 b.type = 'button'; b.className = 'tl-readmore'; b.textContent = RM;
262 b.addEventListener('click', function(){
263 b.textContent = c.classList.toggle('tl-clamp') ? RM : SL;
264 });
265 c.insertAdjacentElement('afterend', b);
266 }
267 });
268})();
269</script>
Note: See TracBrowser for help on using the repository browser.