source: Klonkt/src/views/pages/messages.ejs@ e64a49b

main
Last change on this file since e64a49b was e64a49b, checked in by roboburr <roboburr@…>, 5 weeks ago

Berichten uit inline script naar een module (shaer-bqr, stap 2)

De enige bevestigde stukke plek. Kwam je hier via een link BINNEN de site, dan
arriveerde het script via htmx met een nonce die het document niet kent, en
weigerde de CSP het (shaer-0i6). Chips, zoeken en het in-/uitklappen deden dan
niets, en de reply-editor laadde niet -- dat was Barts melding.

Een pagina vraagt nu om zijn module met pageJs; de shell zet dat op body[data-js]
en de bootstrap importeert het. De waarde wordt een PAD, dus hij gaat door
/[a-z0-9 -]*$/ voordat hij de locals in mag.

TWEE DINGEN MOESTEN VERANDEREN, en niet omdat de code fout was: een module leeft
anders dan een inline script.

NIETS VASTHOUDEN het inline script pakte .msg-list, #msg-q en .msg-nomatch

een keer bij het inladen. Een module wordt per document maar
EEN keer geimporteerd, dus wie Berichten verlaat en
terugkomt houdt verwijzingen over naar elementen die er niet
meer zijn. Nu per keer opzoeken, en de invoerluisteraar
gedelegeerd in plaats van op het veld zelf.

OPNIEUW INDEXEREN bij binnenkomst op de pagina, niet alleen bij "meer laden".

Anders is de lijst na een navigatie niet geindexeerd en
filtert het zoeken op een lege verzameling.

Dat eerste is precies de val waar de chrome-partials al een comment over hebben
staan ("a once-captured reference goes stale"), en het geldt hier dubbel omdat
een module niet opnieuw draait.

Templates compileren, suite 556/556.

  • Property mode set to 100644
File size: 15.5 KB
Line 
1<div class="msg-wrap">
2 <%- include('../partials/fedi-tabs', { active: 'berichten' }) %>
3 <h1 class="msg-title"><%= t('msg.title') %></h1>
4 <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success === 'guardian_accepted' ? t('msg.guard_accepted') : (success === 'guardian_rejected' ? t('msg.guard_rejected') : (success === 'wave_sent' ? t('msg.wave_sent') : (success === 'reply_sent' ? t('msg.reply_sent') : success))) %></div><% } %>
5 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error === 'guardianship' ? t('msg.guard_failed') : (error === 'reply_empty' ? t('msg.reply_empty') : ((error === 'reply_failed' || error === 'reply_target') ? t('msg.reply_failed') : error)) %></div><% } %>
6
7 <%# FEP-633c: a pending guardianship offer is a special message: the kid
8 answers here (accept/reject travels the same C2S pipeline as the apps). %>
9 <% if (typeof guardianOffers !== 'undefined' && guardianOffers.length) { %>
10 <% guardianOffers.forEach(function(o){ %>
11 <div class="alert" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-left:3px solid #ff6b35;">
12 <span style="font-size:1.4em;">&#128735;</span>
13 <div style="flex:1;min-width:200px;">
14 <strong><%= o['shaer:candidateHandle'] || o['shaer:candidate'] %></strong><br>
15 <span><%= t('msg.guard_offer') %></span>
16 </div>
17 <form method="post" action="<%= (typeof moreBase !== 'undefined' ? moreBase : '') %>/messages/guardianship" style="display:flex;gap:8px;">
18 <input type="hidden" name="offer" value="<%= o.id %>">
19 <button class="btn" type="submit" name="answer" value="accept"><%= t('msg.guard_accept') %></button>
20 <button class="btn" type="submit" name="answer" value="reject" style="opacity:.7;"><%= t('msg.guard_reject') %></button>
21 </form>
22 </div>
23 <% }); %>
24 <% } %>
25
26 <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
27 <%# Vier chips, geen zes: Berichten, Gesprekken en Verzonden gingen op in
28 een enkele Gesprekken-view, waarin verzonden en ontvangen in dezelfde
29 draad staan. Activiteit en Moderatie blijven wat ze waren. %>
30 <button type="button" class="msg-chip is-on" data-show="all"><%= t('msg.filter_all') %></button>
31 <button type="button" class="msg-chip" data-show="conv"><%= t('msg.filter_conv') %></button>
32 <button type="button" class="msg-chip" data-show="act"><%= t('msg.filter_act') %></button>
33 <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
34 </div>
35 <div class="msg-search">
36 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
37 <input type="search" id="msg-q" placeholder="<%= t('msg.search_ph') %>" autocomplete="off" spellcheck="false" aria-label="<%= t('msg.search_ph') %>">
38 </div>
39
40 <% var _seen = (typeof seenAt !== 'undefined' && seenAt) ? seenAt : 0; %>
41 <% if (!items || !items.length) { %>
42 <p class="msg-empty"><%= t('msg.empty') %></p>
43 <% } else { %>
44 <ul class="msg-list" data-show="all" id="msg-list">
45 <% items.forEach(function(n){ %><%- include('../partials/msg-item', { n: n, seen: _seen }) %><% }); %>
46 </ul>
47 <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
48 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#msg-list', morePath: '/messages' }) %>
49 <% } %>
50
51 <%# Interact bookmarklet — moved here from the old Reacties page. %>
52 <details class="msg-bm">
53 <summary><%= t('fedi.bm_label') %></summary>
54 <p class="msg-bm-help"><%= t('fedi.bm_help') %></p>
55 <a class="fedi-bm-btn" id="fedi-bm-btn" href="#" draggable="true" title="<%= t('fedi.bm_help') %>">
56 <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>
57 <%= t('fedi.bm_label') %>
58 </a>
59 </details>
60</div>
61
62<%# Filteren, zoeken en het in-/uitklappen zitten in assets/js/mod/messages.js.
63 Inline script hier wordt door de CSP geweigerd zodra je deze pagina via een
64 link BINNEN de site opent -- zie shaer-0i6. De shell laadt de module op
65 body[data-js]; deze pagina vraagt erom met pageJs (routes/posts.js). %>
66
67<style>
68 .msg-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
69 .msg-title { margin: 0 0 .9rem; }
70 .msg-empty { color: var(--ink-soft, #888); }
71
72 .msg-filters { display: flex; gap: .4rem; margin: 0 0 1.1rem; flex-wrap: wrap; }
73 .msg-chip { border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: none;
74 color: var(--ink-soft, #888); font: inherit; font-size: .84rem; font-weight: 600;
75 padding: .3rem .85rem; border-radius: 999px; cursor: pointer; }
76 .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
77
78 .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
79 padding: .45rem .7rem; border-radius: 10px;
80 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
81 .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
82 .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
83 .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
84 .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
85
86 .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
87
88
89 .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
90 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
91 border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
92 transition: border-color .15s ease, box-shadow .15s ease; }
93 .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
94 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
95 .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
96 background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
97 .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
98
99 /* ── Gesprekken ──────────────────────────────────────────────────────
100 NA .msg-item, en dat is geen smaak: .msg-item zet display:flex, en bij
101 gelijke specificiteit wint de laatste regel. Stond dit ervoor, dan kwam de
102 draadkop NAAST de bubbels te staan in plaats van erboven. */
103 .msg-thread { display: block; }
104 .msg-thread-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: .4rem; }
105 /* De tegenpartij is een labeltje bij het gesprek, geen kop erboven: een
106 kleine verzonken uitsparing, zodat de bubbels eronder de ruimte krijgen.
107 Het aantal zit in dezelfde uitsparing in plaats van ernaast. */
108 /* Ook de knop die het gesprek in- en uitklapt. Een button erft geen font en
109 geen kleur, dus die staan hier expliciet; zonder dat wordt het ineens
110 systeemblauw in 13px. */
111 .msg-thread-who { display: inline-flex; align-items: center; gap: .3rem; min-width: 0;
112 font: inherit; font-size: .78em; line-height: 1.5; font-weight: 500;
113 padding: .05rem .5rem; border-radius: 999px; border: 0; cursor: pointer;
114 color: color-mix(in srgb, var(--ink, #000) 62%, transparent);
115 background: color-mix(in srgb, var(--ink, #000) 5%, transparent);
116 box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink, #000) 9%, transparent);
117 max-width: 100%; overflow: hidden; white-space: nowrap;
118 /* Ruimer aanraakvlak dan de tekst: een chip van 18px hoog is op een telefoon
119 niet te raken. De uitsparing blijft klein, het doel wordt groter. */
120 min-height: 32px; }
121 .msg-thread-who:hover { background: color-mix(in srgb, var(--ink, #000) 9%, transparent); }
122 .msg-thread-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
123 .msg-thread-chevron { width: 12px; height: 12px; flex: none; opacity: .55;
124 transition: transform .18s ease; }
125 /* Ingeklapt: de bubbels en het antwoordveld verdwijnen, de uitsparing met de
126 naam en het AANTAL blijft staan -- dat aantal is juist ingeklapt het meest
127 waard. */
128 .msg-thread.is-collapsed .msg-thread-msgs,
129 .msg-thread.is-collapsed .msg-thread-foot { display: none; }
130 /* Tijdens het zoeken wint de treffer van de dichtgeklapte stand. */
131 .msg-thread.is-collapsed.is-search-open .msg-thread-msgs { display: flex; }
132 .msg-thread.is-collapsed.is-search-open .msg-thread-foot { display: flex; }
133 .msg-thread.is-collapsed .msg-thread-chevron { transform: rotate(-90deg); }
134 .msg-thread.is-collapsed .msg-thread-head { margin-bottom: 0; }
135 @media (prefers-reduced-motion: reduce) { .msg-thread-chevron { transition: none; } }
136 .msg-thread-count { font-variant-numeric: tabular-nums; opacity: .75; flex: 0 0 auto;
137 padding-inline-start: .3rem; border-inline-start: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
138 .msg-thread-post { display: inline-flex; align-items: center; gap: .3rem; font-size: .85em;
139 margin-inline-start: auto; min-width: 0; max-width: 100%;
140 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
141 .msg-thread-post svg { width: 13px; height: 13px; flex: none; }
142 .msg-thread-msgs { list-style: none; margin: 0; padding: 0 0 0 .7rem; display: flex; flex-direction: column; gap: .5rem;
143 border-inline-start: 2px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
144 .msg-sub { display: flex; gap: .6rem; align-items: flex-start; min-width: 0; }
145 .msg-sub > .msg-body { min-width: 0; }
146 /* Jouw eigen bijdrage onderscheidt zich met een tint en de accentlijn die
147 .msg-sent al had -- niet met row-reverse: dat duwde de avatar buiten de
148 kaart zodra de regel smaller werd dan zijn inhoud. */
149 .msg-sub.msg-sent { border-radius: 10px; padding: .3rem .45rem;
150 background: color-mix(in srgb, var(--accent, #06c) 5%, transparent); }
151 /* Een lange naam of tijd mag de bubbel niet uit de kaart duwen. */
152 .msg-sub .msg-line { flex-wrap: wrap; }
153
154 /* De voet van een gesprek: antwoorden en zwaaien naast elkaar, ingeklapt.
155 Een open editor onder elk gesprek maakt de lijst weer onleesbaar -- precies
156 wat deze weergave moest oplossen. */
157 .msg-thread-foot { display: flex; align-items: flex-start; gap: .5rem; margin-top: .5rem; flex-wrap: wrap; }
158 .msg-reply { flex: 1 1 12rem; min-width: 0; }
159 .msg-reply > summary { cursor: pointer; font-size: .82rem; color: var(--ink-soft, #888); }
160 .msg-reply[open] > summary { margin-bottom: .4rem; }
161 .msg-reply-form { min-width: 0; }
162 .msg-wave button { line-height: 1.2; }
163
164 @media (max-width: 560px) {
165 /* Op een telefoon is 40px avatar per bubbel puur verlies: de naam staat er
166 al naast, en de inhoud heeft de breedte harder nodig. */
167 .msg-sub > .msg-av { display: none; }
168 .msg-thread-msgs { padding-inline-start: .55rem; }
169 .msg-thread-post { margin-inline-start: 0; }
170 .msg-item { padding: .7rem .6rem; gap: .6rem; }
171 }
172
173 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
174 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
175 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
176 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
177 .msg-av-sent svg { width: 17px; height: 17px; }
178 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
179 display: inline-flex; align-items: center; justify-content: center;
180 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
181 .msg-dot svg { width: 10px; height: 10px; }
182 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
183 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
184
185 .msg-body { flex: 1; min-width: 0; }
186 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
187 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
188 a.msg-who:hover { text-decoration: underline; }
189 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
190 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
191 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
192 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
193 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
194 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
195 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
196 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
197 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
198 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
199
200 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
201 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
202 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
203 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
204 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
205 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
206 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
207 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
208
209 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
210 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
211 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
212 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
213 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
214 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
215 color: var(--ink-soft, #888); cursor: pointer; }
216 .msg-del:hover { color: #c0392b; }
217
218 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
219 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
220 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
221</style>
Note: See TracBrowser for help on using the repository browser.