source: Klonkt/src/views/pages/messages.ejs@ 50489c7

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

Draad viel uit de kaart: kop naast de bubbels en avatars buiten de rand

Twee fouten uit de vorige twee commits, allebei zichtbaar op Robins screenshot.

De draadkop stond NAAST de bubbels in plaats van erboven. Oorzaak: ik zette
.msg-thread { display: block } voor .msg-item { display: flex }, en bij gelijke
specificiteit wint de laatste regel. Het blok staat nu na .msg-item, met die
reden erbij zodat niemand het terugverplaatst.

De avatars van eigen bijdragen vielen buiten de kaart. Oorzaak: row-reverse om
je eigen berichten rechts te zetten. Dat duwt de avatar naar de rand zodra de
regel smaller wordt dan zijn inhoud, en dat is op een telefoon altijd. Eigen
bijdragen onderscheiden zich nu met een tint plus de accentlijn die .msg-sent al
had -- dat kan per definitie niet overlopen.

Verder mobiel dichtgezet: min-width:0 op de flexkinderen (anders duwt een lange
handle of URL de bubbel uit de kaart), .msg-line mag afbreken, en onder 560px
verdwijnt de 40px-avatar per bubbel -- de naam staat er al naast en de inhoud
heeft die breedte harder nodig. De kaartpadding krimpt daar mee, en de post-link
gaat van rechts uitgelijnd naar gewoon links, want uitlijnen op een regel die
toch al wrapt levert alleen een gat op.

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

  • Property mode set to 100644
File size: 16.0 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
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. */
163 .msg-thread { display: block; }
164 .msg-thread-head { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: .4rem; }
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. */
168 .msg-thread-who { display: inline-flex; align-items: center; gap: .3rem; min-width: 0;
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);
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;
176 padding-inline-start: .3rem; border-inline-start: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); }
177 .msg-thread-post { display: inline-flex; align-items: center; gap: .3rem; font-size: .85em;
178 margin-inline-start: auto; min-width: 0; max-width: 100%;
179 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
180 .msg-thread-post svg { width: 13px; height: 13px; flex: none; }
181 .msg-thread-msgs { list-style: none; margin: 0; padding: 0 0 0 .7rem; display: flex; flex-direction: column; gap: .5rem;
182 border-inline-start: 2px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); }
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; }
192
193 @media (max-width: 560px) {
194 /* Op een telefoon is 40px avatar per bubbel puur verlies: de naam staat er
195 al naast, en de inhoud heeft de breedte harder nodig. */
196 .msg-sub > .msg-av { display: none; }
197 .msg-thread-msgs { padding-inline-start: .55rem; }
198 .msg-thread-post { margin-inline-start: 0; }
199 .msg-item { padding: .7rem .6rem; gap: .6rem; }
200 }
201
202 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
203 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
204 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
205 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
206 .msg-av-sent svg { width: 17px; height: 17px; }
207 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
208 display: inline-flex; align-items: center; justify-content: center;
209 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
210 .msg-dot svg { width: 10px; height: 10px; }
211 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
212 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
213
214 .msg-body { flex: 1; min-width: 0; }
215 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
216 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
217 a.msg-who:hover { text-decoration: underline; }
218 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
219 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
220 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
221 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
222 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
223 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
224 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
225 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
226 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
227 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
228
229 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
230 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
231 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
232 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
233 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
234 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
235 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
236 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
237
238 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
239 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
240 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
241 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
242 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
243 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
244 color: var(--ink-soft, #888); cursor: pointer; }
245 .msg-del:hover { color: #c0392b; }
246
247 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
248 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
249 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
250</style>
Note: See TracBrowser for help on using the repository browser.