source: Klonkt/src/views/pages/messages.ejs@ 7b04d3b

main
Last change on this file since 7b04d3b was 544e9c2, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: Messages "poll finished" notification (klonkt-demo-93o)

Closed own polls surface a poll_done item in Messages with the final tally (bar+pct per option, voter count), derived read-time from poll_json.closed via ownPollView. i18n msg.poll_done/msg.poll_total (NL/EN/DE). Tests in messages.test.js.

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

  • Property mode set to 100644
File size: 18.9 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 %></div><% } %>
5 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
6
7 <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
8 <button type="button" class="msg-chip is-on" data-show="all"><%= t('msg.filter_all') %></button>
9 <button type="button" class="msg-chip" data-show="msgs"><%= t('msg.filter_msgs') %></button>
10 <button type="button" class="msg-chip" data-show="conv"><%= t('msg.filter_conv') %></button>
11 <button type="button" class="msg-chip" data-show="act"><%= t('msg.filter_act') %></button>
12 <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
13 <button type="button" class="msg-chip" data-show="sent"><%= t('msg.filter_sent') %></button>
14 </div>
15 <div class="msg-search">
16 <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>
17 <input type="search" id="msg-q" placeholder="<%= t('msg.search_ph') %>" autocomplete="off" spellcheck="false" aria-label="<%= t('msg.search_ph') %>">
18 </div>
19
20 <% var _seen = (typeof seenAt !== 'undefined' && seenAt) ? seenAt : 0; %>
21 <% if (!items || !items.length) { %>
22 <p class="msg-empty"><%= t('msg.empty') %></p>
23 <% } else { %>
24 <ul class="msg-list" data-show="all">
25 <% items.forEach(function(n){
26 var _t = n.type === 'announce' ? 'boost' : n.type; // reply|mention|report|like|boost|follow|sent
27 var _new = _seen && n.created_at ? (Date.parse(n.created_at) > _seen) : false;
28 var _who = n.name || n.handle || '';
29 var _priv0 = (n.visibility === 'followers' || n.visibility === 'direct');
30 // Buckets (klonkt-demo-3jf): Messages = @mentions + private (DM) replies;
31 // Activity = likes/boosts/follows; Moderation = reports; Sent = your
32 // replies; Conversations = every other (public) reply.
33 var _kind = _t === 'sent' ? 'sent'
34 : (_t === 'like' || _t === 'boost' || _t === 'follow' || _t === 'poll_done') ? 'act'
35 : _t === 'report' ? 'mod'
36 : (_t === 'mention' || _priv0) ? 'msgs'
37 : 'conv';
38 var _init = String(_who || '?').replace(/^@/, '').charAt(0).toUpperCase();
39 var _priv = (n.visibility === 'followers' || n.visibility === 'direct');
40 %>
41 <li class="msg-item msg-<%= _t %><%= _new ? ' is-new' : '' %>" data-kind="<%= _kind %>" data-who="<%= String(_who).toLowerCase() %>">
42 <span class="msg-av<%= _t === 'sent' ? ' msg-av-sent' : '' %>" aria-hidden="true">
43 <% 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>
44 <% } 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>
45 <% } else if (n.icon) { %><img src="<%= avatar(n.icon, 96) %>" alt="" loading="lazy">
46 <% } else { %><%= _init %><% } %>
47 <span class="msg-dot msg-dot-<%= _t %>">
48 <% 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>
49 <% } 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>
50 <% } 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>
51 <% } 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>
52 <% } 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>
53 <% } 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>
54 <% } 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>
55 <% } 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><% } %>
56 </span>
57 </span>
58
59 <div class="msg-body">
60 <div class="msg-line">
61 <% if (_t === 'sent') { %>
62 <span class="msg-who"><%= t('msg.you') %></span>
63 <% if (n.to_handle) { %><span class="msg-handle">→ <%= n.to_handle %></span><% } %>
64 <% } else { %>
65 <a class="msg-who" href="<%= n.url %>" target="_blank" rel="nofollow noopener"><%= _who %></a>
66 <% if (n.handle && n.name) { %><span class="msg-handle"><%= n.handle %></span><% } %>
67 <% } %>
68 <% if (_new) { %><span class="msg-new" title="<%= t('msg.new') %>"></span><% } %>
69 <% if (n.created_at) { %><span class="msg-time"><%= formatDateTime(n.created_at) %></span><% } %>
70 </div>
71
72 <div class="msg-what">
73 <% if (n.count && n.count > 1) { %>
74 <strong><%= t('msg.and_more', { n: n.count - 1 }) %></strong>
75 <%= _t === 'like' ? t('msg.liked_many') : t('msg.boosted_many') %>
76 <% } else if (_t === 'follow') { %><%= t('notif.followed') %>
77 <% } else if (_t === 'like') { %><%= t('notif.liked') %>
78 <% } else if (_t === 'boost') { %><%= t('notif.boosted') %>
79 <% } else if (_t === 'report') { %><%= t('notif.reported') %>
80 <% } else if (_t === 'mention') { %><%= t('notif.mentioned') %>
81 <% } else if (_t === 'sent') { %><%= t('msg.sent_reply') %>
82 <% } else if (_t === 'poll_done') { %><strong><%= t('msg.poll_done') %></strong>
83 <% } else { %><%= t('notif.replied') %><% } %>
84 <% if (_priv) { %><span class="msg-priv" title="<%= t('msg.private_hint') %>">🔒 <%= t('msg.private') %></span><% } %>
85 <% if (n.post_slug) { %><a class="msg-post" href="/<%= n.post_slug %>"><%= n.post_title || n.post_slug %></a><% } %>
86 <% if (_t === 'mention' && n.note_url) { %><a class="msg-post" href="<%= n.note_url %>" target="_blank" rel="nofollow noopener"><%= t('tl.view_original') %></a><% } %>
87 <% 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><% } %>
88 </div>
89
90 <% if (_t === 'report' && n.content) { %><div class="msg-content"><%= n.content %></div>
91 <% } else if ((_t === 'reply' || _t === 'mention' || _t === 'sent') && n.content) { %><div class="msg-content"><%- n.content %></div>
92 <% } else if (_t === 'poll_done' && n.poll) { %>
93 <div class="msg-poll" role="group">
94 <% n.poll.options.forEach(function (o) { %>
95 <div class="msg-poll-opt">
96 <div class="msg-poll-top"><span class="msg-poll-name"><%= o.name %></span><span class="msg-poll-pct"><%= o.pct %>%</span></div>
97 <div class="msg-poll-bar"><span style="width:<%= o.pct %>%"></span></div>
98 </div>
99 <% }); %>
100 <div class="msg-poll-total"><%= t('msg.poll_total', { n: n.poll.voters }) %></div>
101 </div>
102 <% } %>
103
104 <% if (_t === 'sent' && n.outboxId) { %>
105 <div class="msg-actions">
106 <details class="msg-edit">
107 <summary><%= t('fedi.edit') %></summary>
108 <div class="msg-edit-form">
109 <%- include('../partials/reply-editor', {
110 action: '/fediverse/' + n.outboxId + '/edit',
111 placeholder: t('fedi.reply_ph'),
112 submitLabel: t('fedi.save_edit'),
113 rows: 2,
114 initialHtml: n.content,
115 initialText: n.editable,
116 defaultLang: n.language,
117 noAttach: true,
118 }) %>
119 </div>
120 </details>
121 <form method="post" action="/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>">
122 <button type="submit" class="msg-del"><%= t('comments.delete') %></button>
123 </form>
124 </div>
125 <% } %>
126 </div>
127 </li>
128 <% }); %>
129 </ul>
130 <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
131 <% } %>
132
133 <%# Interact bookmarklet — moved here from the old Reacties page. %>
134 <details class="msg-bm">
135 <summary><%= t('fedi.bm_label') %></summary>
136 <p class="msg-bm-help"><%= t('fedi.bm_help') %></p>
137 <a class="fedi-bm-btn" id="fedi-bm-btn" href="#" draggable="true" title="<%= t('fedi.bm_help') %>">
138 <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>
139 <%= t('fedi.bm_label') %>
140 </a>
141 </details>
142</div>
143
144<script>
145(function () {
146 if (window.__msgWired) return; window.__msgWired = true;
147
148 // Filtering: kind chip AND free-text search, combined in JS (search over the
149 // sender and the rendered message text). Empty search + "all" = show all.
150 var list = document.querySelector('.msg-list');
151 var noMatch = document.querySelector('.msg-nomatch');
152 var q = document.getElementById('msg-q');
153 var kind = 'all';
154 var items = list ? Array.prototype.slice.call(list.querySelectorAll('.msg-item')) : [];
155 items.forEach(function (li) {
156 // Index once: sender + message body + linked post title.
157 var body = li.querySelector('.msg-content');
158 var post = li.querySelector('.msg-post');
159 var poll = li.querySelector('.msg-poll');
160 li._search = ((li.getAttribute('data-who') || '') + ' ' +
161 (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
162 (poll ? poll.textContent : '')).toLowerCase();
163 });
164 function apply() {
165 if (!list) return;
166 var term = (q && q.value || '').trim().toLowerCase();
167 var shown = 0;
168 items.forEach(function (li) {
169 var ok = (kind === 'all' || li.getAttribute('data-kind') === kind) &&
170 (!term || li._search.indexOf(term) !== -1);
171 li.style.display = ok ? '' : 'none';
172 if (ok) shown++;
173 });
174 if (noMatch) noMatch.hidden = shown !== 0;
175 }
176 document.addEventListener('click', function (e) {
177 var chip = e.target.closest('.msg-chip'); if (!chip) return;
178 kind = chip.getAttribute('data-show');
179 document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
180 apply();
181 });
182 if (q) q.addEventListener('input', apply);
183
184 var a = document.getElementById('fedi-bm-btn');
185 if (a) {
186 a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
187 a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); });
188 }
189})();
190</script>
191
192<style>
193 .msg-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
194 .msg-title { margin: 0 0 .9rem; }
195 .msg-empty { color: var(--ink-soft, #888); }
196
197 .msg-filters { display: flex; gap: .4rem; margin: 0 0 1.1rem; flex-wrap: wrap; }
198 .msg-chip { border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: none;
199 color: var(--ink-soft, #888); font: inherit; font-size: .84rem; font-weight: 600;
200 padding: .3rem .85rem; border-radius: 999px; cursor: pointer; }
201 .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
202
203 .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
204 padding: .45rem .7rem; border-radius: 10px;
205 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
206 .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
207 .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
208 .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
209 .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
210
211 .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
212
213 .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
214 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
215 border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
216 transition: border-color .15s ease, box-shadow .15s ease; }
217 .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
218 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
219 .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
220 background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
221 .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
222
223 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
224 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
225 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
226 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
227 .msg-av-sent svg { width: 17px; height: 17px; }
228 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
229 display: inline-flex; align-items: center; justify-content: center;
230 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
231 .msg-dot svg { width: 10px; height: 10px; }
232 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
233 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
234
235 .msg-body { flex: 1; min-width: 0; }
236 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
237 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
238 a.msg-who:hover { text-decoration: underline; }
239 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
240 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
241 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
242 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
243 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
244 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
245 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
246 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
247 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
248 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
249
250 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
251 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
252 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
253 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
254 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
255 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
256 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
257 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
258
259 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
260 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
261 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
262 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
263 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
264 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
265 color: var(--ink-soft, #888); cursor: pointer; }
266 .msg-del:hover { color: #c0392b; }
267
268 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
269 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
270 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
271</style>
Note: See TracBrowser for help on using the repository browser.