source: Klonkt/src/views/partials/msg-item.ejs@ 2c0c338

main
Last change on this file since 2c0c338 was 2c0c338, checked in by Robin <roboburr@…>, 2 weeks ago

Berichten: binnen een gesprek nieuwste bovenaan, en de zwaai in de kop

Twee dingen die bij elkaar horen (Robins besluit, 25-8).

Binnen een DRAAD leest het nu van nieuw naar oud: wat er net bij kwam
staat er meteen, in plaats van onderaan een lange draad. De lijst van
gesprekken en de meldingen blijven staan zoals ze stonden -- alleen
binnen een gesprek draait de volgorde om.

Op een kopie van de array, want groupConversations sorteert oplopend en
de rest van de pagina rekent daarop (_tnew en _tto lezen dezelfde
array). En in de markup, niet met column-reverse: dat draait alleen het
beeld om terwijl een schermlezer de oude volgorde houdt.

Een ONTVANGEN zwaai staat nu ook in de kop van de draad. Ingeklapt zijn
de bubbels weg, en een zwaai is het bericht dat om een antwoord vraagt
zonder iets te zeggen -- die mocht niet achter een dichtgeklapt gesprek
verdwijnen. Blijft staan als de draad open is, net als het aantal. Een
zwaai die je zelf stuurde zet geen teken: dat is geen uitnodiging aan
jezelf.

Vijf toetsen die de echte partial renderen (test/messages-thread-order):
EJS-fouten bestaan alleen tijdens het renderen, dus die vielen tot nu
toe nergens in de suite om. Tegenbewijs gedraaid tegen de oude partial:
de volgorde-toets faalt daar, zoals het hoort.

  • Property mode set to 100644
File size: 17.8 KB
Line 
1<% if (n.type === 'thread') { %>
2<%
3 var _tseen = (typeof seen !== 'undefined' && seen) ? seen : 0;
4 // Een draad is nieuw zodra EEN bericht erin nieuw is.
5 var _tnew = !!_tseen && (n.messages || []).some(function (m) {
6 return m.created_at && Date.parse(m.created_at) > _tseen;
7 });
8 var _tpeople = (n.people || []).map(function (p) { return p.name || p.handle || '?'; });
9 // Alleen jij aan het woord: dan is de tegenpartij het adres waaraan
10 // je schreef, niet een lege naam.
11 var _tto = ((n.messages || []).find(function (m) { return m.to_handle; }) || {}).to_handle;
12 var _tlabel = _tpeople.length ? _tpeople.join(', ') : (_tto || t('msg.you'));
13 // Zwaaide er iemand in dit gesprek? Dan hoort dat in de KOP
14 // (Robins aanwijzing, 25-8). Ingeklapt zijn de bubbels weg, en juist
15 // een zwaai is het bericht dat om een antwoord vraagt zonder iets te
16 // zeggen -- die mag niet achter een dichtgeklapt gesprek verdwijnen.
17 // Alleen een ONTVANGEN zwaai: je eigen zwaai staat er als 'sent' en
18 // is geen uitnodiging aan jezelf.
19 var _twave = (n.messages || []).some(function (m) { return m.type === 'mention' && m.wave; });
20 %>
21 <li class="msg-item msg-thread<%= _tnew ? ' is-new' : '' %>" data-kind="conv" data-who="<%= (_tpeople.join(' ') + ' ' + (_tto || '')).toLowerCase() %>">
22 <div class="msg-thread-head">
23 <%# Tegenpartij en aantal in EEN uitsparing: het is een label bij het
24 gesprek, geen kop erboven. Zo houdt de inhoud de ruimte.
25
26 Deze uitsparing is ook de knop die het gesprek in- en uitklapt
27 (Barts aanwijzing): tikken op de tegenpartij is waar je van
28 nature naar wijst als je "dit gesprek" bedoelt. Een BUTTON en
29 geen span met een klikhandler, want dan werkt hij ook met het
30 toetsenbord en leest een schermlezer de stand voor.
31
32 Zonder JavaScript doet hij niets, en dan staat het gesprek
33 gewoon open -- dat is ook de stand waar we mee beginnen. %>
34 <button type="button" class="msg-thread-who" aria-expanded="true">
35 <span class="msg-thread-name"><%= _tlabel %></span>
36 <% if (n.count > 1) { %><span class="msg-thread-count"><%= n.count %></span><% } %>
37 <svg class="msg-thread-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
38 </button>
39 <%# Blijft staan als het gesprek dicht is, net als het aantal in de
40 uitsparing hierboven -- daar is hij juist het meest waard. %>
41 <% if (_twave) { %><span class="msg-thread-wave" role="img" aria-label="<%= t('msg.waved_at_you') %>" title="<%= t('msg.waved_at_you') %>">&#128075;</span><% } %>
42 <% if (_tnew) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
43 <%# De context bij de draad: waar gaat dit gesprek over? Zonder deze
44 link is een antwoord in een lijst niet te plaatsen. %>
45 <% if (n.post) { %>
46 <a class="msg-thread-post" href="/<%= n.post.slug %>">
47 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
48 <%= n.post.title || n.post.slug %>
49 </a>
50 <% } %>
51 </div>
52 <%# NIEUWSTE BOVENAAN binnen een gesprek (Robins besluit, 25-8). De
53 LIJST van gesprekken en de meldingen blijven staan zoals ze
54 stonden; alleen binnen een draad leest het van nieuw naar oud, dus
55 wat er net bij kwam staat er meteen en niet onderaan een lange
56 draad.
57
58 Op een KOPIE, want groupConversations sorteert `messages`
59 oplopend en de rest van deze pagina rekent daarop -- `_tnew` en
60 `_tto` hierboven lezen dezelfde array. Een reverse() zonder
61 slice() draait het origineel om en dat werkt door naar alles wat
62 er daarna nog naar kijkt.
63
64 In de MARKUP en niet met column-reverse: die draait alleen het
65 beeld om, terwijl een schermlezer en het toetsenbord de oude
66 volgorde houden. %>
67 <ul class="msg-thread-msgs">
68 <% (n.messages || []).slice().reverse().forEach(function (m) { %><%- include('msg-item', { n: m, seen: _tseen, inThread: true }) %><% }); %>
69 </ul>
70 <%# Antwoorden vanuit de draad zelf. Ingeklapt, want een open editor
71 onder elk gesprek maakt de lijst onleesbaar -- juist het probleem
72 dat deze weergave moest oplossen. De route kiest het pad: een
73 draad aan een post krijgt een reply, een draad aan een persoon een
74 direct bericht. %>
75 <%# shaer-r4c: precies de tak die de server ook kiest. /messages/reply
76 gaat naar deliverReply als er een interaction+post is (dat is een
77 ANTWOORD en valt onder de poort), en anders naar deliverDirectNote
78 (dat is een bericht, andere poort). Alleen de eerste verbergen. %>
79 <% var _rt = n.replyTo || {}; var _thread = _rt.interactionId && _rt.postSlug; %>
80 <% var _kan = canMutate && ((_thread && locals.mayReply !== false) || (!_thread && _rt.actorUri)); %>
81 <% if (_kan) { %>
82 <div class="msg-thread-foot">
83 <details class="msg-reply">
84 <summary><%= t('fedi.reply') %></summary>
85 <div class="msg-reply-form">
86 <%- include('../partials/reply-editor', {
87 action: (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') + '/messages/reply',
88 hiddenFields: [
89 { name: 'interaction_id', value: _rt.interactionId || '' },
90 { name: 'post_slug', value: _rt.postSlug || '' },
91 { name: 'to', value: _rt.actorUri || '' },
92 ],
93 placeholder: t('fedi.reply_ph'),
94 submitLabel: t('fedi.reply'),
95 rows: 2,
96 }) %>
97 </div>
98 </details>
99 <%# De zwaai blijft een eigen knop: het is geen antwoord maar een
100 seintje (FEP-633c), en die twee horen niet op een hoop. %>
101 <% if (_rt.actorUri) { %>
102 <form method="post" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/messages/quick-reply" class="msg-wave" data-sent="<%= t('msg.wave_sent') %>">
103 <input type="hidden" name="to" value="<%= _rt.actorUri %>">
104 <button class="btn small" name="text" value="&#128075;" title="<%= t('msg.wave_back') %>">&#128075;</button>
105 </form>
106 <% } %>
107 </div>
108 <% } %>
109 </li>
110<% } else { %>
111<%
112 var _seen = (typeof seen !== 'undefined' && seen) ? seen : 0;
113 var _t = n.type === 'announce' ? 'boost' : n.type; // reply|mention|report|like|boost|follow|sent
114 var _new = _seen && n.created_at ? (Date.parse(n.created_at) > _seen) : false;
115 var _who = n.name || n.handle || '';
116 var _priv0 = (n.visibility === 'followers' || n.visibility === 'direct');
117 // Emmers: Activiteit = likes/boosts/follows/polls, Moderatie =
118 // reports, en al het overige is gesprek. De oude emmers 'msgs' en
119 // 'sent' bestaan niet meer sinds Berichten, Gesprekken en Verzonden
120 // samenvielen -- en dat is geen cosmetische hernoeming: een
121 // gespreksregel die om wat voor reden dan ook geen draad werd (geen
122 // post, geen herkenbare tegenpartij) hield anders een data-kind
123 // waarvoor geen chip meer bestond, en was daarmee onder ELKE chip
124 // behalve Alles onzichtbaar.
125 var _kind = (_t === 'like' || _t === 'boost' || _t === 'follow' || _t === 'poll_done') ? 'act'
126 : _t === 'report' ? 'mod'
127 : 'conv';
128 // Binnen een draad is dit geen zelfstandige regel: de chip filtert op
129 // de draad, niet op de losse zinnen erin.
130 var _sub = (typeof inThread !== 'undefined' && inThread);
131 var _init = String(_who || '?').replace(/^@/, '').charAt(0).toUpperCase();
132 var _priv = (n.visibility === 'followers' || n.visibility === 'direct');
133 %>
134 <li class="<%= _sub ? 'msg-sub' : 'msg-item' %> msg-<%= _t %><%= _new ? ' is-new' : '' %>"<% if (!_sub) { %> data-kind="<%= _kind %>"<% } %> data-who="<%= String(_who).toLowerCase() %>">
135 <span class="msg-av<%= _t === 'sent' ? ' msg-av-sent' : '' %>" aria-hidden="true">
136 <% if (_t === 'sent') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
137 <% } else if (_t === 'poll_done') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
138 <% } else if (n.icon) { %><img src="<%= avatar(n.icon, 96) %>" alt="" loading="lazy">
139 <% } else { %><%= _init %><% } %>
140 <span class="msg-dot msg-dot-<%= _t %>">
141 <% if (_t === 'like') { %><svg viewBox="0 0 24 24" fill="currentColor"><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>
142 <% } else if (_t === 'boost') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><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>
143 <% } else if (_t === 'follow') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><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>
144 <% } else if (_t === 'report') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>
145 <% } else if (_t === 'mention') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/></svg>
146 <% } else if (_t === 'sent') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg>
147 <% } else if (_t === 'poll_done') { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
148 <% } else { %><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><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><% } %>
149 </span>
150 </span>
151
152 <div class="msg-body">
153 <div class="msg-line">
154 <% if (_t === 'sent') { %>
155 <span class="msg-who"><%= t('msg.you') %></span>
156 <% if (n.to_handle && !_sub) { %><span class="msg-handle">→ <%= n.to_handle %></span><% } %>
157 <% } else { %>
158 <a class="msg-who" href="<%= n.url %>" target="_blank" rel="nofollow noopener"><%- emojiName(_who, n.actor_emoji_json) %></a>
159 <% /* De naam blijft: binnen een draad moet je zien WIE wat zei.
160 De handle erachter niet -- die staat in de kop. */ %>
161 <% if (n.handle && n.name && !_sub) { %><span class="msg-handle"><%= n.handle %></span><% } %>
162 <% } %>
163 <% if (_new) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
164 <% /* When the post was written, like de Krant and de Guardian PWA
165 show it. created_at (when it reached us) stays the sort key
166 and drives the "new since your last visit" dot above. */ %>
167 <% if (n.published || n.created_at) { %><span class="msg-time"><%= formatDateTime(n.published || n.created_at) %></span><% } %>
168 </div>
169
170 <% var _hasWhat = !_sub || _priv
171 || (_t === 'mention' && (n.help_request || n.wave || n.note_url))
172 || (_t === 'sent' && n.in_reply_to); %>
173 <% if (_hasWhat) { %>
174 <div class="msg-what">
175 <% if (n.count && n.count > 1) { %>
176 <strong><%= t('msg.and_more', { n: n.count - 1 }) %></strong>
177 <%= _t === 'like' ? t('msg.liked_many') : t('msg.boosted_many') %>
178 <% } else if (_t === 'follow') { %><%= t('notif.followed') %>
179 <% } else if (_t === 'like') { %><%= t('notif.liked') %>
180 <% } else if (_t === 'boost') { %><%= t('notif.boosted') %>
181 <% } else if (_t === 'report') { %><%= t('notif.reported') %>
182 <% } else if (_t === 'mention' && n.help_request) { %>🛟 <%= t('msg.help_request') %>
183 <% } else if (_t === 'mention' && n.wave) { %>👋 <%= t('msg.waved_at_you') %>
184 <% } else if (_sub) { %><%# binnen een draad zegt de kop al dat dit een gesprek is %>
185 <% } else if (_t === 'mention') { %><%= t('notif.mentioned') %>
186 <% } else if (_t === 'sent') { %><%= t('msg.sent_reply') %>
187 <% } else if (_t === 'poll_done') { %><strong><%= t('msg.poll_done') %></strong>
188 <% } else { %><%= t('notif.replied') %><% } %>
189 <% if (_priv) { %><span class="msg-priv" title="<%= t('msg.private_hint') %>">🔒 <%= t('msg.private') %></span><% } %>
190 <% if (n.post_slug && !_sub) { %><a class="msg-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
191 <% if (_t === 'mention' && n.note_url) { %><a class="msg-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
192 <% if (_t === 'sent' && n.in_reply_to) { %><a class="msg-post" href="<%= n.in_reply_to %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
193 </div>
194 <% } %>
195
196 <% if (_t === 'report' && n.content) { %><div class="msg-content"><%= n.content %></div>
197 <% } else if ((_t === 'reply' || _t === 'mention' || _t === 'sent') && n.content) { %><div class="msg-content msg-note"><%- include('../partials/note-body', { nb: n }) %></div>
198 <% if (_t === 'mention' && n.wave && n.actorUri && canMutate) { %>
199 <form class="msg-quickreply" method="post" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/messages/quick-reply" data-sent="<%= t('msg.wave_sent') %>" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">
200 <input type="hidden" name="to" value="<%= n.actorUri %>">
201 <button class="btn small" name="text" value="<%= t('msg.wave_r1') %>"><%= t('msg.wave_r1') %></button>
202 <button class="btn small" name="text" value="<%= t('msg.wave_r2') %>"><%= t('msg.wave_r2') %></button>
203 <button class="btn small" name="text" value="👋"><%= t('msg.wave_back') %></button>
204 </form>
205 <% } %>
206 <% } else if (_t === 'poll_done' && n.poll) { %>
207 <div class="msg-poll" role="group">
208 <% n.poll.options.forEach(function (o) { %>
209 <div class="msg-poll-opt">
210 <div class="msg-poll-top"><span class="msg-poll-name"><%= o.name %></span><span class="msg-poll-pct"><%= o.pct %>%</span></div>
211 <div class="msg-poll-bar"><span style="width:<%= o.pct %>%"></span></div>
212 </div>
213 <% }); %>
214 <div class="msg-poll-total"><%= t('msg.poll_total', { n: n.poll.voters }) %></div>
215 </div>
216 <% } %>
217
218 <% if (_t === 'sent' && n.outboxId) { %>
219 <div class="msg-actions">
220 <details class="msg-edit">
221 <summary><%= t('fedi.edit') %></summary>
222 <div class="msg-edit-form">
223 <%- include('../partials/reply-editor', {
224 action: '/fediverse/' + n.outboxId + '/edit',
225 placeholder: t('fedi.reply_ph'),
226 submitLabel: t('fedi.save_edit'),
227 rows: 2,
228 initialHtml: n.content,
229 initialText: n.editable,
230 defaultLang: n.language,
231 noAttach: true,
232 }) %>
233 </div>
234 </details>
235 <form method="post" action="/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>">
236 <button type="submit" class="msg-del"><%= t('comments.delete') %></button>
237 </form>
238 </div>
239 <% } %>
240 </div>
241 </li>
242<% } %>
Note: See TracBrowser for help on using the repository browser.