source: Klonkt/src/views/pages/messages.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: 14.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)) %></div><% } %>
5 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error === 'guardianship' ? t('msg.guard_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<script>
63(function () {
64 if (window.__msgWired) return; window.__msgWired = true;
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';
72 var items = [];
73 function indexItem(li) {
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 }
80 // Index once: sender + message body + linked post title + poll text.
81 var body = li.querySelector('.msg-content');
82 var post = li.querySelector('.msg-post');
83 var poll = li.querySelector('.msg-poll');
84 li._search = ((li.getAttribute('data-who') || '') + ' ' +
85 (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
86 (poll ? poll.textContent : '')).toLowerCase();
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() {
91 items = list ? Array.prototype.slice.call(list.querySelectorAll(':scope > .msg-item')) : [];
92 items.forEach(function (li) { if (!li._search) indexItem(li); });
93 }
94 reindex();
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 }
107 document.addEventListener('click', function (e) {
108 var chip = e.target.closest('.msg-chip'); if (!chip) return;
109 kind = chip.getAttribute('data-show');
110 document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
111 apply();
112 });
113 if (q) q.addEventListener('input', apply);
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 });
118
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
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
146 .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
147
148 /* Een draad is geen regel maar een blok: kop met de context, daaronder de
149 bubbels van oud naar nieuw. De verticale lijn links maakt zichtbaar dat ze
150 bij elkaar horen -- dat was precies wat ontbrak toen verzonden en ontvangen
151 in aparte chips zaten. */
152 .msg-thread { display: block; }
153 .msg-thread-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
154 .msg-thread-who { font-weight: 600; }
155 .msg-thread-count { font-size: .78em; padding: .05rem .4rem; border-radius: 999px;
156 background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
157 .msg-thread-post { display: inline-flex; align-items: center; gap: .3rem; font-size: .85em;
158 margin-inline-start: auto; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
159 .msg-thread-post svg { width: 13px; height: 13px; flex: none; }
160 .msg-thread-msgs { list-style: none; margin: 0; padding: 0 0 0 .85rem; display: flex; flex-direction: column; gap: .45rem;
161 border-inline-start: 2px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
162 .msg-sub { display: flex; gap: .8rem; align-items: flex-start; }
163 /* Jouw eigen bijdrage staat aan de andere kant van de lijn, zodat je de
164 beurten ziet zonder namen te lezen. */
165 .msg-sub.msg-sent { flex-direction: row-reverse; text-align: end; }
166 .msg-sub.msg-sent .msg-line { justify-content: flex-end; }
167
168 .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
169 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
170 border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
171 transition: border-color .15s ease, box-shadow .15s ease; }
172 .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
173 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
174 .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
175 background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
176 .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
177
178 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
179 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
180 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
181 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
182 .msg-av-sent svg { width: 17px; height: 17px; }
183 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
184 display: inline-flex; align-items: center; justify-content: center;
185 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
186 .msg-dot svg { width: 10px; height: 10px; }
187 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
188 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
189
190 .msg-body { flex: 1; min-width: 0; }
191 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
192 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
193 a.msg-who:hover { text-decoration: underline; }
194 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
195 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
196 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
197 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
198 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
199 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
200 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
201 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
202 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
203 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
204
205 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
206 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
207 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
208 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
209 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
210 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
211 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
212 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
213
214 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
215 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
216 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
217 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
218 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
219 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
220 color: var(--ink-soft, #888); cursor: pointer; }
221 .msg-del:hover { color: #c0392b; }
222
223 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
224 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
225 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
226</style>
Note: See TracBrowser for help on using the repository browser.