source: Klonkt/src/views/partials/msg-item.ejs@ 7875a80

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

Berichten toont gesprekken: Berichten, Gesprekken en Verzonden vouwen samen

Berichten had zes chips, waarvan drie over hetzelfde gesprek gingen. Een
uitwisseling viel daardoor uit elkaar: wat jij stuurde stond onder Verzonden,
wat je terugkreeg onder Berichten of Gesprekken, en om een draad te volgen moest
je tussen chips heen en weer. Dat waren geen aparte pagina's maar client-side
filters over een enkele lijst, dus het probleem zat in het model, niet in de
weergave.

Antwoorden, mentions en je eigen verzonden berichten vouwen nu samen tot draden.
De sleutel: hangt het bericht aan een post van jou, dan is dat de draad --
ontvangen antwoorden kennen hun post via de join op posts, verzonden antwoorden
via ap_outbox.post_slug, en dat is dezelfde sleutel. Hangt het aan niets van jou
(een mention), dan loopt de draad per tegenpartij. De post wint van de persoon:
twee mensen onder dezelfde post voeren een gesprek, geen twee.

De context hoort bij de draad. Gaat het gesprek over een post, dan staat de link
ernaartoe in de kop, want zonder die context is een los antwoord in een lijst
niet te plaatsen. Binnen de draad leest het van oud naar nieuw, en jouw eigen
bijdragen staan aan de andere kant van de lijn zodat je de beurten ziet zonder
namen te lezen.

Likes, boosts, follows, reports en poll-uitslagen blijven losse regels: dat is
geen gesprek. De chips gaan van zes naar vier: Alles, Gesprekken, Activiteit,
Moderatie.

Twee dingen die daarbij moesten:

  • listOutbox selecteert post_slug (stond in de tabel, werd niet gelezen).
  • De filter kijkt naar ':scope > .msg-item', want de bubbels in een draad zijn geen zelfstandige regels meer; zoeken indexeert een draad op zijn hele tekst in plaats van op de eerste bubbel.

De titel van de post blijft leeg zolang hij onbekend is in plaats van terug te
vallen op de slug: het nieuwste bericht in een draad is vaak je eigen antwoord,
en dat kent alleen de slug -- zou die de titel worden, dan kon het ontvangen
antwoord eronder de echte titel niet meer invullen.

Getest tegen dev.klonkt.com: de groepering en de template gedraaid op de echte
database daar, en op een kopie ervan met een nagebouwd gesprek (twee ontvangen
antwoorden, jouw antwoord ertussen, plus een like). Dat levert een draad van
drie berichten op volgorde met de post als context, terwijl de like een losse
regel blijft. Suite 408/408.

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

  • Property mode set to 100644
File size: 11.9 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 <span class="msg-thread-who"><%= _tlabel %></span>
17 <% if (n.count > 1) { %><span class="msg-thread-count"><%= n.count %></span><% } %>
18 <% if (_tnew) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
19 <%# De context bij de draad: waar gaat dit gesprek over? Zonder deze
20 link is een antwoord in een lijst niet te plaatsen. %>
21 <% if (n.post) { %>
22 <a class="msg-thread-post" href="/<%= n.post.slug %>">
23 <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>
24 <%= n.post.title || n.post.slug %>
25 </a>
26 <% } %>
27 </div>
28 <ul class="msg-thread-msgs">
29 <% (n.messages || []).forEach(function (m) { %><%- include('msg-item', { n: m, seen: _tseen, inThread: true }) %><% }); %>
30 </ul>
31 </li>
32<% } else { %>
33<%
34 var _seen = (typeof seen !== 'undefined' && seen) ? seen : 0;
35 var _t = n.type === 'announce' ? 'boost' : n.type; // reply|mention|report|like|boost|follow|sent
36 var _new = _seen && n.created_at ? (Date.parse(n.created_at) > _seen) : false;
37 var _who = n.name || n.handle || '';
38 var _priv0 = (n.visibility === 'followers' || n.visibility === 'direct');
39 // Buckets (klonkt-demo-3jf): Messages = @mentions + private (DM) replies;
40 // Activity = likes/boosts/follows; Moderation = reports; Sent = your
41 // replies; Conversations = every other (public) reply.
42 var _kind = _t === 'sent' ? 'sent'
43 : (_t === 'like' || _t === 'boost' || _t === 'follow' || _t === 'poll_done') ? 'act'
44 : _t === 'report' ? 'mod'
45 : (_t === 'mention' || _priv0) ? 'msgs'
46 : 'conv';
47 // Binnen een draad is dit geen zelfstandige regel: de chip filtert op
48 // de draad, niet op de losse zinnen erin.
49 var _sub = (typeof inThread !== 'undefined' && inThread);
50 var _init = String(_who || '?').replace(/^@/, '').charAt(0).toUpperCase();
51 var _priv = (n.visibility === 'followers' || n.visibility === 'direct');
52 %>
53 <li class="<%= _sub ? 'msg-sub' : 'msg-item' %> msg-<%= _t %><%= _new ? ' is-new' : '' %>"<% if (!_sub) { %> data-kind="<%= _kind %>"<% } %> data-who="<%= String(_who).toLowerCase() %>">
54 <span class="msg-av<%= _t === 'sent' ? ' msg-av-sent' : '' %>" aria-hidden="true">
55 <% 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>
56 <% } 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>
57 <% } else if (n.icon) { %><img src="<%= avatar(n.icon, 96) %>" alt="" loading="lazy">
58 <% } else { %><%= _init %><% } %>
59 <span class="msg-dot msg-dot-<%= _t %>">
60 <% 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>
61 <% } 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>
62 <% } 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>
63 <% } 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>
64 <% } 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>
65 <% } 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>
66 <% } 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>
67 <% } 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><% } %>
68 </span>
69 </span>
70
71 <div class="msg-body">
72 <div class="msg-line">
73 <% if (_t === 'sent') { %>
74 <span class="msg-who"><%= t('msg.you') %></span>
75 <% if (n.to_handle) { %><span class="msg-handle">→ <%= n.to_handle %></span><% } %>
76 <% } else { %>
77 <a class="msg-who" href="<%= n.url %>" target="_blank" rel="nofollow noopener"><%- emojiName(_who, n.actor_emoji_json) %></a>
78 <% if (n.handle && n.name) { %><span class="msg-handle"><%= n.handle %></span><% } %>
79 <% } %>
80 <% if (_new) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
81 <% /* When the post was written, like de Krant and de Guardian PWA
82 show it. created_at (when it reached us) stays the sort key
83 and drives the "new since your last visit" dot above. */ %>
84 <% if (n.published || n.created_at) { %><span class="msg-time"><%= formatDateTime(n.published || n.created_at) %></span><% } %>
85 </div>
86
87 <div class="msg-what">
88 <% if (n.count && n.count > 1) { %>
89 <strong><%= t('msg.and_more', { n: n.count - 1 }) %></strong>
90 <%= _t === 'like' ? t('msg.liked_many') : t('msg.boosted_many') %>
91 <% } else if (_t === 'follow') { %><%= t('notif.followed') %>
92 <% } else if (_t === 'like') { %><%= t('notif.liked') %>
93 <% } else if (_t === 'boost') { %><%= t('notif.boosted') %>
94 <% } else if (_t === 'report') { %><%= t('notif.reported') %>
95 <% } else if (_t === 'mention' && n.help_request) { %>🛟 <%= t('msg.help_request') %>
96 <% } else if (_t === 'mention' && n.wave) { %>👋 <%= t('msg.waved_at_you') %>
97 <% } else if (_t === 'mention') { %><%= t('notif.mentioned') %>
98 <% } else if (_t === 'sent') { %><%= t('msg.sent_reply') %>
99 <% } else if (_t === 'poll_done') { %><strong><%= t('msg.poll_done') %></strong>
100 <% } else { %><%= t('notif.replied') %><% } %>
101 <% if (_priv) { %><span class="msg-priv" title="<%= t('msg.private_hint') %>">🔒 <%= t('msg.private') %></span><% } %>
102 <% if (n.post_slug && !_sub) { %><a class="msg-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
103 <% if (_t === 'mention' && n.note_url) { %><a class="msg-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
104 <% 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><% } %>
105 </div>
106
107 <% if (_t === 'report' && n.content) { %><div class="msg-content"><%= n.content %></div>
108 <% } else if ((_t === 'reply' || _t === 'mention' || _t === 'sent') && n.content) { %><div class="msg-content msg-note"><%- include('../partials/note-body', { nb: n }) %></div>
109 <% if (_t === 'mention' && n.wave && n.actorUri && canMutate) { %>
110 <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">
111 <input type="hidden" name="to" value="<%= n.actorUri %>">
112 <button class="btn small" name="text" value="<%= t('msg.wave_r1') %>"><%= t('msg.wave_r1') %></button>
113 <button class="btn small" name="text" value="<%= t('msg.wave_r2') %>"><%= t('msg.wave_r2') %></button>
114 <button class="btn small" name="text" value="👋"><%= t('msg.wave_back') %></button>
115 </form>
116 <% } %>
117 <% } else if (_t === 'poll_done' && n.poll) { %>
118 <div class="msg-poll" role="group">
119 <% n.poll.options.forEach(function (o) { %>
120 <div class="msg-poll-opt">
121 <div class="msg-poll-top"><span class="msg-poll-name"><%= o.name %></span><span class="msg-poll-pct"><%= o.pct %>%</span></div>
122 <div class="msg-poll-bar"><span style="width:<%= o.pct %>%"></span></div>
123 </div>
124 <% }); %>
125 <div class="msg-poll-total"><%= t('msg.poll_total', { n: n.poll.voters }) %></div>
126 </div>
127 <% } %>
128
129 <% if (_t === 'sent' && n.outboxId) { %>
130 <div class="msg-actions">
131 <details class="msg-edit">
132 <summary><%= t('fedi.edit') %></summary>
133 <div class="msg-edit-form">
134 <%- include('../partials/reply-editor', {
135 action: '/fediverse/' + n.outboxId + '/edit',
136 placeholder: t('fedi.reply_ph'),
137 submitLabel: t('fedi.save_edit'),
138 rows: 2,
139 initialHtml: n.content,
140 initialText: n.editable,
141 defaultLang: n.language,
142 noAttach: true,
143 }) %>
144 </div>
145 </details>
146 <form method="post" action="/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>">
147 <button type="submit" class="msg-del"><%= t('comments.delete') %></button>
148 </form>
149 </div>
150 <% } %>
151 </div>
152 </li>
153<% } %>
Note: See TracBrowser for help on using the repository browser.