source: Klonkt/src/views/pages/messages.ejs@ 0202104

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

De gebruikerskant van 3.6: de ward ziet zijn vangnet, de guardian handelt

Drie oppervlakken rondgemaakt op de beschikbaarheid die er sinds vanmorgen
server-side in zit.

Berichten (de ward): de guardians-balk toont per guardian de buddy-list-stip
met het label erbij: beschikbaar, afwezig tot een datum, offline. Het kind ziet
de echte omvang van zijn vangnet, niet alleen de namen. Owner-only omdat de
pagina dat is.

De Guardian-PWA (de guardian): in het paneel per kind staan de mede-guardians
met dezelfde stippen; op een slapende verschijnt de bewuste, zeldzame
vervolgstap "Voorstel: loslaten bij afwezigheid", die langs dezelfde
C2S-pijplijn loopt als de Shaer-apps (Offer van shaer:Lapse, lokaal ward opent
direct, remote ward krijgt het voorstel bezorgd). Lopende lapses staan als
kaart bij de aanvragen, met Eens/Oneens over de bestaande offer-draad en de zin
die het frame bewaakt. En "Even afwezig": een week of een maand, een directe
note met shaer:away naar alle wards, lokaal direct toegepast.

Shaer (beide apps): de lapse-kaart toont stemknoppen alleen aan leden van de
set. De ward kijkt mee naar wat zijn guardians beslissen; het is daar niet de
rechter, om precies de reden uit de editor's note van 3.6.3.

Onderweg gerepareerd: parseStamp kende alleen strings, waardoor een epoch-ms
endTime als lege datum rendde ("unavailable till" zonder datum).

Changed files:
src/routes/posts.js

  • /messages geeft de guardians hun beschikbaarheid mee

src/views/pages/messages.ejs

  • de stip en het label per guardian, met de opmaak erbij

src/routes/guardian.js

  • dashboardState: mede-guardians met status per lokaal kind, plus lapses
  • POST /guardian/api/away en /guardian/api/lapse
  • de nieuwe labels in uiStrings

src/assets/js/guardian.js

  • de guardians-sectie in het paneel, de lapse-kaart, de afwezig-knoppen

src/views/pages/guardian.ejs

  • de "Even afwezig"-sectie

src/assets/css/guardian.css

  • de stippen en de lapse-kaart

src/middleware/render.js

  • parseStamp accepteert epoch ms

src/services/i18n.js

  • de labels en teksten in nl, en, de

remarks: end-to-end in de browser nagelopen op de wegwerp-database: het kind
ziet oma afwezig-tot, opa offline en guard beschikbaar; de guardian opent het
paneel, stelt de lapse voor op de slapende opa (kaart verschijnt, eigen stem
geteld), drukt "A week", en bij het kind staat guard meteen op afwezig tot
5 augustus. 276 tests groen. Niet uitgerold.

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

  • Property mode set to 100644
File size: 14.8 KB
RevLine 
[f1a23b8]1<div class="msg-wrap">
2 <%- include('../partials/fedi-tabs', { active: 'berichten' }) %>
3 <h1 class="msg-title"><%= t('msg.title') %></h1>
[ad6f62a]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><% } %>
[e84ce32]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;">
[780a7c6]14 <strong><%= o['shaer:candidateHandle'] || o['shaer:candidate'] %></strong><br>
[e84ce32]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;">
[780a7c6]18 <input type="hidden" name="offer" value="<%= o.id %>">
[e84ce32]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 <% } %>
[f1a23b8]25
[fcd6964]26 <%# FEP-633c §2: the ward always sees who guards it (committed relations). %>
27 <% if (typeof myGuardians !== 'undefined' && myGuardians.length) { %>
28 <div class="alert" style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-left:3px solid #35a2ff;">
29 <span style="font-size:1.4em;">&#128737;</span>
30 <div style="flex:1;min-width:200px;">
31 <strong><%= t('msg.guardians_label') %></strong><br>
[0202104]32 <%# Availability (FEP-633c 3.6): the ward sees the real size of its
33 safety net, in buddy-list language on the responsibility axis.
34 Labels are UI (Robins besluit); the wire keeps the spec terms.
35 Owner-only by construction: this page is the owner's. %>
36 <span class="msg-guardians">
37 <% myGuardians.forEach(function(g){
38 var _st = g.availability === 'away' ? 'away' : (g.availability === 'dormant' ? 'dormant' : 'active');
39 var _lbl = _st === 'away' ? t('msg.g_away', { date: g.awayUntil ? formatDate(g.awayUntil) : '?' })
40 : (_st === 'dormant' ? t('msg.g_offline') : t('msg.g_available')); %>
41 <span class="msg-guardian">
42 <span class="msg-gdot msg-gdot-<%= _st %>" aria-hidden="true"></span>
43 <%= g.handle %>
44 <span class="msg-gstate"><%= _lbl %></span>
45 </span>
46 <% }); %>
47 </span>
[fcd6964]48 </div>
49 </div>
50 <% } %>
51
[f1a23b8]52 <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
53 <button type="button" class="msg-chip is-on" data-show="all"><%= t('msg.filter_all') %></button>
[8ed65a6]54 <button type="button" class="msg-chip" data-show="msgs"><%= t('msg.filter_msgs') %></button>
[f1a23b8]55 <button type="button" class="msg-chip" data-show="conv"><%= t('msg.filter_conv') %></button>
56 <button type="button" class="msg-chip" data-show="act"><%= t('msg.filter_act') %></button>
[8ed65a6]57 <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
[f1a23b8]58 <button type="button" class="msg-chip" data-show="sent"><%= t('msg.filter_sent') %></button>
59 </div>
[8ed65a6]60 <div class="msg-search">
61 <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>
62 <input type="search" id="msg-q" placeholder="<%= t('msg.search_ph') %>" autocomplete="off" spellcheck="false" aria-label="<%= t('msg.search_ph') %>">
63 </div>
[f1a23b8]64
65 <% var _seen = (typeof seenAt !== 'undefined' && seenAt) ? seenAt : 0; %>
66 <% if (!items || !items.length) { %>
67 <p class="msg-empty"><%= t('msg.empty') %></p>
68 <% } else { %>
[1485933]69 <ul class="msg-list" data-show="all" id="msg-list">
70 <% items.forEach(function(n){ %><%- include('../partials/msg-item', { n: n, seen: _seen }) %><% }); %>
[f1a23b8]71 </ul>
[8ed65a6]72 <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
[1485933]73 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#msg-list', morePath: '/messages' }) %>
[f1a23b8]74 <% } %>
75
76 <%# Interact bookmarklet — moved here from the old Reacties page. %>
77 <details class="msg-bm">
78 <summary><%= t('fedi.bm_label') %></summary>
79 <p class="msg-bm-help"><%= t('fedi.bm_help') %></p>
80 <a class="fedi-bm-btn" id="fedi-bm-btn" href="#" draggable="true" title="<%= t('fedi.bm_help') %>">
81 <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>
82 <%= t('fedi.bm_label') %>
83 </a>
84 </details>
85</div>
86
87<script>
88(function () {
89 if (window.__msgWired) return; window.__msgWired = true;
[8ed65a6]90
91 // Filtering: kind chip AND free-text search, combined in JS (search over the
92 // sender and the rendered message text). Empty search + "all" = show all.
93 var list = document.querySelector('.msg-list');
94 var noMatch = document.querySelector('.msg-nomatch');
95 var q = document.getElementById('msg-q');
96 var kind = 'all';
[1485933]97 var items = [];
98 function indexItem(li) {
99 // Index once: sender + message body + linked post title + poll text.
[8ed65a6]100 var body = li.querySelector('.msg-content');
101 var post = li.querySelector('.msg-post');
[544e9c2]102 var poll = li.querySelector('.msg-poll');
[8ed65a6]103 li._search = ((li.getAttribute('data-who') || '') + ' ' +
[544e9c2]104 (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
105 (poll ? poll.textContent : '')).toLowerCase();
[1485933]106 }
107 // Re-collect + index; called on load and after each "Load more" append so new
108 // items join the filter/search (and inherit the active chip via apply()).
109 function reindex() {
110 items = list ? Array.prototype.slice.call(list.querySelectorAll('.msg-item')) : [];
111 items.forEach(function (li) { if (!li._search) indexItem(li); });
112 }
113 reindex();
[8ed65a6]114 function apply() {
115 if (!list) return;
116 var term = (q && q.value || '').trim().toLowerCase();
117 var shown = 0;
118 items.forEach(function (li) {
119 var ok = (kind === 'all' || li.getAttribute('data-kind') === kind) &&
120 (!term || li._search.indexOf(term) !== -1);
121 li.style.display = ok ? '' : 'none';
122 if (ok) shown++;
123 });
124 if (noMatch) noMatch.hidden = shown !== 0;
125 }
[f1a23b8]126 document.addEventListener('click', function (e) {
127 var chip = e.target.closest('.msg-chip'); if (!chip) return;
[8ed65a6]128 kind = chip.getAttribute('data-show');
[f1a23b8]129 document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
[8ed65a6]130 apply();
[f1a23b8]131 });
[8ed65a6]132 if (q) q.addEventListener('input', apply);
[1485933]133 // After a "Load more" append (htmx), index the new rows and re-apply the filter.
134 document.body.addEventListener('htmx:afterSettle', function (e) {
135 if (e.target && e.target.id === 'msg-list') { reindex(); apply(); }
136 });
[8ed65a6]137
[f1a23b8]138 var a = document.getElementById('fedi-bm-btn');
139 if (a) {
140 a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
141 a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); });
142 }
143})();
144</script>
145
146<style>
[0202104]147 /* Availability of the ward's guardians (FEP-633c 3.6): the buddy-list dot
148 on the responsibility axis. Green available, yellow declared away, grey
149 observed dormant (one answer restores). */
150 .msg-guardians { display: flex; flex-wrap: wrap; gap: .35rem 1rem; }
151 .msg-guardian { display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap; }
152 .msg-gdot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
153 .msg-gdot-active { background: #4caf7d; }
154 .msg-gdot-away { background: #e0a83a; }
155 .msg-gdot-dormant { background: #8a93a3; }
156 .msg-gstate { color: var(--ink-soft, #888); font-size: .82rem; }
[f1a23b8]157 .msg-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
158 .msg-title { margin: 0 0 .9rem; }
159 .msg-empty { color: var(--ink-soft, #888); }
160
161 .msg-filters { display: flex; gap: .4rem; margin: 0 0 1.1rem; flex-wrap: wrap; }
162 .msg-chip { border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: none;
163 color: var(--ink-soft, #888); font: inherit; font-size: .84rem; font-weight: 600;
164 padding: .3rem .85rem; border-radius: 999px; cursor: pointer; }
165 .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
166
[8ed65a6]167 .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
168 padding: .45rem .7rem; border-radius: 10px;
169 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
170 .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
171 .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
172 .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
173 .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
174
[f1a23b8]175 .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
176
177 .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
178 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
179 border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
180 transition: border-color .15s ease, box-shadow .15s ease; }
181 .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
182 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
183 .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
184 background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
185 .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
186
187 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
188 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
189 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
190 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
191 .msg-av-sent svg { width: 17px; height: 17px; }
192 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
193 display: inline-flex; align-items: center; justify-content: center;
194 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
195 .msg-dot svg { width: 10px; height: 10px; }
196 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
197 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
198
199 .msg-body { flex: 1; min-width: 0; }
200 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
201 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
202 a.msg-who:hover { text-decoration: underline; }
203 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
204 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
205 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
206 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
207 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
208 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
209 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
210 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
211 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
212 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
213
[544e9c2]214 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
215 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
216 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
217 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
218 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
219 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
220 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
221 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
222
[f1a23b8]223 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
224 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
225 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
226 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
227 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
228 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
229 color: var(--ink-soft, #888); cursor: pointer; }
230 .msg-del:hover { color: #c0392b; }
231
232 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
233 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
234 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
235</style>
Note: See TracBrowser for help on using the repository browser.