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

main
Last change on this file since 5296349 was 189e335, checked in by Claude (agent) <aiclaude@…>, 5 weeks ago

Antwoorden vanuit een gesprek, rijk, met de zwaai als eigen knop

Berichten kon je gesprekken tonen maar er niet in antwoorden. Het invoerveld dat
er stond was msg-quickreply: platte tekst, 200 tekens, en het verstuurde geen
antwoord maar een zwaai (deliverDirectNote met wave:true, een FEP-633c-seintje).
Dat is iets anders dat toevallig op een reply leek.

Onder elke draad staat nu de gedeelde reply-editor, ingeklapt achter een
"Reageer" -- open onder elk gesprek maakt de lijst weer onleesbaar, precies wat
deze weergave moest oplossen. De zwaai blijft ernaast als eigen knop, want een
seintje en een antwoord horen niet op een hoop.

/messages/reply kiest het pad op wat de draad zelf meedraagt (replyTo):

  • draad aan een post -> deliverReply op het nieuwste ontvangen bericht erin, met een controle dat die parent ook echt bij die post hoort; anders kon een aangepast formulier een antwoord onder andermans draad hangen.
  • draad aan een persoon -> een direct bericht terug.

Voor dat tweede pad kon deliverDirectNote nog geen rijke inhoud: het escapete
platte tekst. Het accepteert nu html, door dezelfde sanitizer als deliverReply,
zodat een antwoord uit Berichten via één poort gaat. De mention-anker blijft een
eigen alinea vooraan, want de ontvanger moet genoemd staan ook als de inhoud met
een kop of lijst begint. Levert de sanitizer niets bruikbaars op, dan valt het
terug op de escaped tekst -- een leeggepoetste editor mag geen leeg bericht
versturen.

Meldingen in nl/en/de erbij, en getNotifications geeft nu het interactie-id en
de actor-uri mee, want zonder die twee weet een antwoord niet waar het heen moet.

Geverifieerd op dev: beide draden daar adresseren hun eigen tegenpartij, het
formulier draagt de juiste verborgen velden en de zwaai staat er los naast.
Suite 412/412.

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

  • Property mode set to 100644
File size: 16.7 KB
RevLine 
[f1a23b8]1<div class="msg-wrap">
2 <%- include('../partials/fedi-tabs', { active: 'berichten' }) %>
3 <h1 class="msg-title"><%= t('msg.title') %></h1>
[189e335]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><% } %>
[e84ce32]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;">
[780a7c6]14 <strong><%= o['shaer:candidateHandle'] || o['shaer:candidate'] %></strong><br>
[e84ce32]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;">
[780a7c6]18 <input type="hidden" name="offer" value="<%= o.id %>">
[e84ce32]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 <% } %>
[f1a23b8]25
26 <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
[7875a80]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. %>
[f1a23b8]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>
[8ed65a6]33 <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
[f1a23b8]34 </div>
[8ed65a6]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>
[f1a23b8]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 { %>
[1485933]44 <ul class="msg-list" data-show="all" id="msg-list">
45 <% items.forEach(function(n){ %><%- include('../partials/msg-item', { n: n, seen: _seen }) %><% }); %>
[f1a23b8]46 </ul>
[8ed65a6]47 <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
[1485933]48 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#msg-list', morePath: '/messages' }) %>
[f1a23b8]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<script>
63(function () {
64 if (window.__msgWired) return; window.__msgWired = true;
[8ed65a6]65
66 // Filtering: kind chip AND free-text search, combined in JS (search over the
67 // sender and the rendered message text). Empty search + "all" = show all.
68 var list = document.querySelector('.msg-list');
69 var noMatch = document.querySelector('.msg-nomatch');
70 var q = document.getElementById('msg-q');
71 var kind = 'all';
[1485933]72 var items = [];
73 function indexItem(li) {
[7875a80]74 // Een draad indexeren op zijn eerste bubbel zou de rest onvindbaar maken,
75 // dus daar nemen we de hele tekst; losse regels blijven gericht geindexeerd.
76 if (li.classList.contains('msg-thread')) {
77 li._search = ((li.getAttribute('data-who') || '') + ' ' + li.textContent).toLowerCase();
78 return;
79 }
[1485933]80 // Index once: sender + message body + linked post title + poll text.
[8ed65a6]81 var body = li.querySelector('.msg-content');
82 var post = li.querySelector('.msg-post');
[544e9c2]83 var poll = li.querySelector('.msg-poll');
[8ed65a6]84 li._search = ((li.getAttribute('data-who') || '') + ' ' +
[544e9c2]85 (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
86 (poll ? poll.textContent : '')).toLowerCase();
[1485933]87 }
88 // Re-collect + index; called on load and after each "Load more" append so new
89 // items join the filter/search (and inherit the active chip via apply()).
90 function reindex() {
[7875a80]91 items = list ? Array.prototype.slice.call(list.querySelectorAll(':scope > .msg-item')) : [];
[1485933]92 items.forEach(function (li) { if (!li._search) indexItem(li); });
93 }
94 reindex();
[8ed65a6]95 function apply() {
96 if (!list) return;
97 var term = (q && q.value || '').trim().toLowerCase();
98 var shown = 0;
99 items.forEach(function (li) {
100 var ok = (kind === 'all' || li.getAttribute('data-kind') === kind) &&
101 (!term || li._search.indexOf(term) !== -1);
102 li.style.display = ok ? '' : 'none';
103 if (ok) shown++;
104 });
105 if (noMatch) noMatch.hidden = shown !== 0;
106 }
[f1a23b8]107 document.addEventListener('click', function (e) {
108 var chip = e.target.closest('.msg-chip'); if (!chip) return;
[8ed65a6]109 kind = chip.getAttribute('data-show');
[f1a23b8]110 document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
[8ed65a6]111 apply();
[f1a23b8]112 });
[8ed65a6]113 if (q) q.addEventListener('input', apply);
[1485933]114 // After a "Load more" append (htmx), index the new rows and re-apply the filter.
115 document.body.addEventListener('htmx:afterSettle', function (e) {
116 if (e.target && e.target.id === 'msg-list') { reindex(); apply(); }
117 });
[8ed65a6]118
[f1a23b8]119 var a = document.getElementById('fedi-bm-btn');
120 if (a) {
121 a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
122 a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); });
123 }
124})();
125</script>
126
127<style>
128 .msg-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
129 .msg-title { margin: 0 0 .9rem; }
130 .msg-empty { color: var(--ink-soft, #888); }
131
132 .msg-filters { display: flex; gap: .4rem; margin: 0 0 1.1rem; flex-wrap: wrap; }
133 .msg-chip { border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: none;
134 color: var(--ink-soft, #888); font: inherit; font-size: .84rem; font-weight: 600;
135 padding: .3rem .85rem; border-radius: 999px; cursor: pointer; }
136 .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
137
[8ed65a6]138 .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
139 padding: .45rem .7rem; border-radius: 10px;
140 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
141 .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
142 .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
143 .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
144 .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
145
[f1a23b8]146 .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
147
[50489c7]148
149 .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
150 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
151 border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
152 transition: border-color .15s ease, box-shadow .15s ease; }
153 .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
154 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
155 .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
156 background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
157 .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
158
159 /* ── Gesprekken ──────────────────────────────────────────────────────
160 NA .msg-item, en dat is geen smaak: .msg-item zet display:flex, en bij
161 gelijke specificiteit wint de laatste regel. Stond dit ervoor, dan kwam de
162 draadkop NAAST de bubbels te staan in plaats van erboven. */
[7875a80]163 .msg-thread { display: block; }
[50489c7]164 .msg-thread-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: .4rem; }
[a78a0ef]165 /* De tegenpartij is een labeltje bij het gesprek, geen kop erboven: een
166 kleine verzonken uitsparing, zodat de bubbels eronder de ruimte krijgen.
167 Het aantal zit in dezelfde uitsparing in plaats van ernaast. */
[50489c7]168 .msg-thread-who { display: inline-flex; align-items: center; gap: .3rem; min-width: 0;
[a78a0ef]169 font-size: .78em; line-height: 1.5; font-weight: 500;
170 padding: .05rem .5rem; border-radius: 999px;
171 color: color-mix(in srgb, var(--ink, #000) 62%, transparent);
172 background: color-mix(in srgb, var(--ink, #000) 5%, transparent);
173 box-shadow: inset 0 1px 2px color-mix(in srgb, var(--ink, #000) 9%, transparent);
[50489c7]174 max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
175 .msg-thread-count { font-variant-numeric: tabular-nums; opacity: .75; flex: 0 0 auto;
[a78a0ef]176 padding-inline-start: .3rem; border-inline-start: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
[7875a80]177 .msg-thread-post { display: inline-flex; align-items: center; gap: .3rem; font-size: .85em;
[50489c7]178 margin-inline-start: auto; min-width: 0; max-width: 100%;
179 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
[7875a80]180 .msg-thread-post svg { width: 13px; height: 13px; flex: none; }
[50489c7]181 .msg-thread-msgs { list-style: none; margin: 0; padding: 0 0 0 .7rem; display: flex; flex-direction: column; gap: .5rem;
[7875a80]182 border-inline-start: 2px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
[50489c7]183 .msg-sub { display: flex; gap: .6rem; align-items: flex-start; min-width: 0; }
184 .msg-sub > .msg-body { min-width: 0; }
185 /* Jouw eigen bijdrage onderscheidt zich met een tint en de accentlijn die
186 .msg-sent al had -- niet met row-reverse: dat duwde de avatar buiten de
187 kaart zodra de regel smaller werd dan zijn inhoud. */
188 .msg-sub.msg-sent { border-radius: 10px; padding: .3rem .45rem;
189 background: color-mix(in srgb, var(--accent, #06c) 5%, transparent); }
190 /* Een lange naam of tijd mag de bubbel niet uit de kaart duwen. */
191 .msg-sub .msg-line { flex-wrap: wrap; }
[7875a80]192
[189e335]193 /* De voet van een gesprek: antwoorden en zwaaien naast elkaar, ingeklapt.
194 Een open editor onder elk gesprek maakt de lijst weer onleesbaar -- precies
195 wat deze weergave moest oplossen. */
196 .msg-thread-foot { display: flex; align-items: flex-start; gap: .5rem; margin-top: .5rem; flex-wrap: wrap; }
197 .msg-reply { flex: 1 1 12rem; min-width: 0; }
198 .msg-reply > summary { cursor: pointer; font-size: .82rem; color: var(--ink-soft, #888); }
199 .msg-reply[open] > summary { margin-bottom: .4rem; }
200 .msg-reply-form { min-width: 0; }
201 .msg-wave button { line-height: 1.2; }
202
[50489c7]203 @media (max-width: 560px) {
204 /* Op een telefoon is 40px avatar per bubbel puur verlies: de naam staat er
205 al naast, en de inhoud heeft de breedte harder nodig. */
206 .msg-sub > .msg-av { display: none; }
207 .msg-thread-msgs { padding-inline-start: .55rem; }
208 .msg-thread-post { margin-inline-start: 0; }
209 .msg-item { padding: .7rem .6rem; gap: .6rem; }
210 }
[f1a23b8]211
212 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
213 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
214 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
215 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
216 .msg-av-sent svg { width: 17px; height: 17px; }
217 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
218 display: inline-flex; align-items: center; justify-content: center;
219 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
220 .msg-dot svg { width: 10px; height: 10px; }
221 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
222 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
223
224 .msg-body { flex: 1; min-width: 0; }
225 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
226 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
227 a.msg-who:hover { text-decoration: underline; }
228 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
229 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
230 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
231 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
232 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
233 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
234 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
235 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
236 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
237 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
238
[544e9c2]239 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
240 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
241 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
242 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
243 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
244 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
245 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
246 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
247
[f1a23b8]248 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
249 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
250 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
251 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
252 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
253 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
254 color: var(--ink-soft, #888); cursor: pointer; }
255 .msg-del:hover { color: #c0392b; }
256
257 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
258 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
259 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
260</style>
Note: See TracBrowser for help on using the repository browser.