source: Klonkt/src/views/partials/msg-item.ejs@ 691311f

main
Last change on this file since 691311f 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: 14.7 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 %>
14 <li class="msg-item msg-thread<%= _tnew ? ' is-new' : '' %>" data-kind="conv" data-who="<%= (_tpeople.join(' ') + ' ' + (_tto || '')).toLowerCase() %>">
15 <div class="msg-thread-head">
16 <%# Tegenpartij en aantal in EEN uitsparing: het is een label bij het
17 gesprek, geen kop erboven. Zo houdt de inhoud de ruimte. %>
18 <span class="msg-thread-who"><%= _tlabel %><% if (n.count > 1) { %><span class="msg-thread-count"><%= n.count %></span><% } %></span>
19 <% if (_tnew) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
20 <%# De context bij de draad: waar gaat dit gesprek over? Zonder deze
21 link is een antwoord in een lijst niet te plaatsen. %>
22 <% if (n.post) { %>
23 <a class="msg-thread-post" href="/<%= n.post.slug %>">
24 <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>
25 <%= n.post.title || n.post.slug %>
26 </a>
27 <% } %>
28 </div>
29 <ul class="msg-thread-msgs">
30 <% (n.messages || []).forEach(function (m) { %><%- include('msg-item', { n: m, seen: _tseen, inThread: true }) %><% }); %>
31 </ul>
32 <%# Antwoorden vanuit de draad zelf. Ingeklapt, want een open editor
33 onder elk gesprek maakt de lijst onleesbaar -- juist het probleem
34 dat deze weergave moest oplossen. De route kiest het pad: een
35 draad aan een post krijgt een reply, een draad aan een persoon een
36 direct bericht. %>
37 <% var _rt = n.replyTo || {}; var _kan = canMutate && ((_rt.interactionId && _rt.postSlug) || _rt.actorUri); %>
38 <% if (_kan) { %>
39 <div class="msg-thread-foot">
40 <details class="msg-reply">
41 <summary><%= t('fedi.reply') %></summary>
42 <div class="msg-reply-form">
43 <%- include('../partials/reply-editor', {
44 action: (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') + '/messages/reply',
45 hiddenFields: [
46 { name: 'interaction_id', value: _rt.interactionId || '' },
47 { name: 'post_slug', value: _rt.postSlug || '' },
48 { name: 'to', value: _rt.actorUri || '' },
49 ],
50 placeholder: t('fedi.reply_ph'),
51 submitLabel: t('fedi.reply'),
52 rows: 2,
53 }) %>
54 </div>
55 </details>
56 <%# De zwaai blijft een eigen knop: het is geen antwoord maar een
57 seintje (FEP-633c), en die twee horen niet op een hoop. %>
58 <% if (_rt.actorUri) { %>
59 <form method="post" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/messages/quick-reply" class="msg-wave">
60 <input type="hidden" name="to" value="<%= _rt.actorUri %>">
61 <button class="btn small" name="text" value="&#128075;" title="<%= t('msg.wave_back') %>">&#128075;</button>
62 </form>
63 <% } %>
64 </div>
65 <% } %>
66 </li>
67<% } else { %>
68<%
69 var _seen = (typeof seen !== 'undefined' && seen) ? seen : 0;
70 var _t = n.type === 'announce' ? 'boost' : n.type; // reply|mention|report|like|boost|follow|sent
71 var _new = _seen && n.created_at ? (Date.parse(n.created_at) > _seen) : false;
72 var _who = n.name || n.handle || '';
73 var _priv0 = (n.visibility === 'followers' || n.visibility === 'direct');
74 // Emmers: Activiteit = likes/boosts/follows/polls, Moderatie =
75 // reports, en al het overige is gesprek. De oude emmers 'msgs' en
76 // 'sent' bestaan niet meer sinds Berichten, Gesprekken en Verzonden
77 // samenvielen -- en dat is geen cosmetische hernoeming: een
78 // gespreksregel die om wat voor reden dan ook geen draad werd (geen
79 // post, geen herkenbare tegenpartij) hield anders een data-kind
80 // waarvoor geen chip meer bestond, en was daarmee onder ELKE chip
81 // behalve Alles onzichtbaar.
82 var _kind = (_t === 'like' || _t === 'boost' || _t === 'follow' || _t === 'poll_done') ? 'act'
83 : _t === 'report' ? 'mod'
84 : 'conv';
85 // Binnen een draad is dit geen zelfstandige regel: de chip filtert op
86 // de draad, niet op de losse zinnen erin.
87 var _sub = (typeof inThread !== 'undefined' && inThread);
88 var _init = String(_who || '?').replace(/^@/, '').charAt(0).toUpperCase();
89 var _priv = (n.visibility === 'followers' || n.visibility === 'direct');
90 %>
91 <li class="<%= _sub ? 'msg-sub' : 'msg-item' %> msg-<%= _t %><%= _new ? ' is-new' : '' %>"<% if (!_sub) { %> data-kind="<%= _kind %>"<% } %> data-who="<%= String(_who).toLowerCase() %>">
92 <span class="msg-av<%= _t === 'sent' ? ' msg-av-sent' : '' %>" aria-hidden="true">
93 <% 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>
94 <% } 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>
95 <% } else if (n.icon) { %><img src="<%= avatar(n.icon, 96) %>" alt="" loading="lazy">
96 <% } else { %><%= _init %><% } %>
97 <span class="msg-dot msg-dot-<%= _t %>">
98 <% 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>
99 <% } 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>
100 <% } 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>
101 <% } 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>
102 <% } 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>
103 <% } 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>
104 <% } 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>
105 <% } 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><% } %>
106 </span>
107 </span>
108
109 <div class="msg-body">
110 <div class="msg-line">
111 <% if (_t === 'sent') { %>
112 <span class="msg-who"><%= t('msg.you') %></span>
113 <% if (n.to_handle && !_sub) { %><span class="msg-handle">→ <%= n.to_handle %></span><% } %>
114 <% } else { %>
115 <a class="msg-who" href="<%= n.url %>" target="_blank" rel="nofollow noopener"><%- emojiName(_who, n.actor_emoji_json) %></a>
116 <% /* De naam blijft: binnen een draad moet je zien WIE wat zei.
117 De handle erachter niet -- die staat in de kop. */ %>
118 <% if (n.handle && n.name && !_sub) { %><span class="msg-handle"><%= n.handle %></span><% } %>
119 <% } %>
120 <% if (_new) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
121 <% /* When the post was written, like de Krant and de Guardian PWA
122 show it. created_at (when it reached us) stays the sort key
123 and drives the "new since your last visit" dot above. */ %>
124 <% if (n.published || n.created_at) { %><span class="msg-time"><%= formatDateTime(n.published || n.created_at) %></span><% } %>
125 </div>
126
127 <% var _hasWhat = !_sub || _priv
128 || (_t === 'mention' && (n.help_request || n.wave || n.note_url))
129 || (_t === 'sent' && n.in_reply_to); %>
130 <% if (_hasWhat) { %>
131 <div class="msg-what">
132 <% if (n.count && n.count > 1) { %>
133 <strong><%= t('msg.and_more', { n: n.count - 1 }) %></strong>
134 <%= _t === 'like' ? t('msg.liked_many') : t('msg.boosted_many') %>
135 <% } else if (_t === 'follow') { %><%= t('notif.followed') %>
136 <% } else if (_t === 'like') { %><%= t('notif.liked') %>
137 <% } else if (_t === 'boost') { %><%= t('notif.boosted') %>
138 <% } else if (_t === 'report') { %><%= t('notif.reported') %>
139 <% } else if (_t === 'mention' && n.help_request) { %>🛟 <%= t('msg.help_request') %>
140 <% } else if (_t === 'mention' && n.wave) { %>👋 <%= t('msg.waved_at_you') %>
141 <% } else if (_sub) { %><%# binnen een draad zegt de kop al dat dit een gesprek is %>
142 <% } else if (_t === 'mention') { %><%= t('notif.mentioned') %>
143 <% } else if (_t === 'sent') { %><%= t('msg.sent_reply') %>
144 <% } else if (_t === 'poll_done') { %><strong><%= t('msg.poll_done') %></strong>
145 <% } else { %><%= t('notif.replied') %><% } %>
146 <% if (_priv) { %><span class="msg-priv" title="<%= t('msg.private_hint') %>">🔒 <%= t('msg.private') %></span><% } %>
147 <% if (n.post_slug && !_sub) { %><a class="msg-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
148 <% if (_t === 'mention' && n.note_url) { %><a class="msg-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
149 <% 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><% } %>
150 </div>
151 <% } %>
152
153 <% if (_t === 'report' && n.content) { %><div class="msg-content"><%= n.content %></div>
154 <% } else if ((_t === 'reply' || _t === 'mention' || _t === 'sent') && n.content) { %><div class="msg-content msg-note"><%- include('../partials/note-body', { nb: n }) %></div>
155 <% if (_t === 'mention' && n.wave && n.actorUri && canMutate) { %>
156 <form class="msg-quickreply" method="post" action="<%= (typeof siteUrlBase !== 'undefined' ? siteUrlBase : '') %>/messages/quick-reply" style="display:flex;gap:6px;flex-wrap:wrap;margin-top:6px">
157 <input type="hidden" name="to" value="<%= n.actorUri %>">
158 <button class="btn small" name="text" value="<%= t('msg.wave_r1') %>"><%= t('msg.wave_r1') %></button>
159 <button class="btn small" name="text" value="<%= t('msg.wave_r2') %>"><%= t('msg.wave_r2') %></button>
160 <button class="btn small" name="text" value="👋"><%= t('msg.wave_back') %></button>
161 </form>
162 <% } %>
163 <% } else if (_t === 'poll_done' && n.poll) { %>
164 <div class="msg-poll" role="group">
165 <% n.poll.options.forEach(function (o) { %>
166 <div class="msg-poll-opt">
167 <div class="msg-poll-top"><span class="msg-poll-name"><%= o.name %></span><span class="msg-poll-pct"><%= o.pct %>%</span></div>
168 <div class="msg-poll-bar"><span style="width:<%= o.pct %>%"></span></div>
169 </div>
170 <% }); %>
171 <div class="msg-poll-total"><%= t('msg.poll_total', { n: n.poll.voters }) %></div>
172 </div>
173 <% } %>
174
175 <% if (_t === 'sent' && n.outboxId) { %>
176 <div class="msg-actions">
177 <details class="msg-edit">
178 <summary><%= t('fedi.edit') %></summary>
179 <div class="msg-edit-form">
180 <%- include('../partials/reply-editor', {
181 action: '/fediverse/' + n.outboxId + '/edit',
182 placeholder: t('fedi.reply_ph'),
183 submitLabel: t('fedi.save_edit'),
184 rows: 2,
185 initialHtml: n.content,
186 initialText: n.editable,
187 defaultLang: n.language,
188 noAttach: true,
189 }) %>
190 </div>
191 </details>
192 <form method="post" action="/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>">
193 <button type="submit" class="msg-del"><%= t('comments.delete') %></button>
194 </form>
195 </div>
196 <% } %>
197 </div>
198 </li>
199<% } %>
Note: See TracBrowser for help on using the repository browser.