source: Klonkt/src/views/pages/messages.ejs@ 513ba82

main
Last change on this file since 513ba82 was 1485933, checked in by Robin <roboburr@…>, 7 weeks ago

Feature: Load more on Messages, page 72 (klonkt-demo-r9u, slice 2/4)

Messages now pages in 72s with the shared Load-more button instead of a
hard 80-item cap. getMessages gains an offset arg and pages the merged,
grouped stream by recomputing top-down and slicing [offset, offset+72]
(stable across pages); getNotifications' per-source caps scale with the
requested limit so deep paging can reach older items. The item markup
moved to partials/msg-item.ejs so the page and the append fragment
render identically. The client-side filter/search re-indexes appended
rows on htmx:afterSettle and re-applies the active chip, so search keeps
working across pages. Seen-watermark is only stamped on the first page,
not on appends.

Tests: getMessages offset paging (no overlap, PAGE+1 probe). Browser:
72 -> 144 -> 150 then the button drops; searching a term only present on
page 2 matches after append.

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

  • Property mode set to 100644
File size: 11.4 KB
RevLine 
[f1a23b8]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>
[8ed65a6]9 <button type="button" class="msg-chip" data-show="msgs"><%= t('msg.filter_msgs') %></button>
[f1a23b8]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>
[8ed65a6]12 <button type="button" class="msg-chip" data-show="mod"><%= t('msg.filter_mod') %></button>
[f1a23b8]13 <button type="button" class="msg-chip" data-show="sent"><%= t('msg.filter_sent') %></button>
14 </div>
[8ed65a6]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>
[f1a23b8]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 { %>
[1485933]24 <ul class="msg-list" data-show="all" id="msg-list">
25 <% items.forEach(function(n){ %><%- include('../partials/msg-item', { n: n, seen: _seen }) %><% }); %>
[f1a23b8]26 </ul>
[8ed65a6]27 <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
[1485933]28 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#msg-list', morePath: '/messages' }) %>
[f1a23b8]29 <% } %>
30
31 <%# Interact bookmarklet — moved here from the old Reacties page. %>
32 <details class="msg-bm">
33 <summary><%= t('fedi.bm_label') %></summary>
34 <p class="msg-bm-help"><%= t('fedi.bm_help') %></p>
35 <a class="fedi-bm-btn" id="fedi-bm-btn" href="#" draggable="true" title="<%= t('fedi.bm_help') %>">
36 <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>
37 <%= t('fedi.bm_label') %>
38 </a>
39 </details>
40</div>
41
42<script>
43(function () {
44 if (window.__msgWired) return; window.__msgWired = true;
[8ed65a6]45
46 // Filtering: kind chip AND free-text search, combined in JS (search over the
47 // sender and the rendered message text). Empty search + "all" = show all.
48 var list = document.querySelector('.msg-list');
49 var noMatch = document.querySelector('.msg-nomatch');
50 var q = document.getElementById('msg-q');
51 var kind = 'all';
[1485933]52 var items = [];
53 function indexItem(li) {
54 // Index once: sender + message body + linked post title + poll text.
[8ed65a6]55 var body = li.querySelector('.msg-content');
56 var post = li.querySelector('.msg-post');
[544e9c2]57 var poll = li.querySelector('.msg-poll');
[8ed65a6]58 li._search = ((li.getAttribute('data-who') || '') + ' ' +
[544e9c2]59 (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
60 (poll ? poll.textContent : '')).toLowerCase();
[1485933]61 }
62 // Re-collect + index; called on load and after each "Load more" append so new
63 // items join the filter/search (and inherit the active chip via apply()).
64 function reindex() {
65 items = list ? Array.prototype.slice.call(list.querySelectorAll('.msg-item')) : [];
66 items.forEach(function (li) { if (!li._search) indexItem(li); });
67 }
68 reindex();
[8ed65a6]69 function apply() {
70 if (!list) return;
71 var term = (q && q.value || '').trim().toLowerCase();
72 var shown = 0;
73 items.forEach(function (li) {
74 var ok = (kind === 'all' || li.getAttribute('data-kind') === kind) &&
75 (!term || li._search.indexOf(term) !== -1);
76 li.style.display = ok ? '' : 'none';
77 if (ok) shown++;
78 });
79 if (noMatch) noMatch.hidden = shown !== 0;
80 }
[f1a23b8]81 document.addEventListener('click', function (e) {
82 var chip = e.target.closest('.msg-chip'); if (!chip) return;
[8ed65a6]83 kind = chip.getAttribute('data-show');
[f1a23b8]84 document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
[8ed65a6]85 apply();
[f1a23b8]86 });
[8ed65a6]87 if (q) q.addEventListener('input', apply);
[1485933]88 // After a "Load more" append (htmx), index the new rows and re-apply the filter.
89 document.body.addEventListener('htmx:afterSettle', function (e) {
90 if (e.target && e.target.id === 'msg-list') { reindex(); apply(); }
91 });
[8ed65a6]92
[f1a23b8]93 var a = document.getElementById('fedi-bm-btn');
94 if (a) {
95 a.setAttribute('href', "javascript:void(window.open('" + location.origin + "/authorize_interaction?uri='+encodeURIComponent(window.location.href)))");
96 a.addEventListener('click', function (e) { e.preventDefault(); a.classList.add('nudge'); setTimeout(function(){ a.classList.remove('nudge'); }, 600); });
97 }
98})();
99</script>
100
101<style>
102 .msg-wrap { max-width: 640px; margin: 1rem auto; padding: 0 1rem; }
103 .msg-title { margin: 0 0 .9rem; }
104 .msg-empty { color: var(--ink-soft, #888); }
105
106 .msg-filters { display: flex; gap: .4rem; margin: 0 0 1.1rem; flex-wrap: wrap; }
107 .msg-chip { border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: none;
108 color: var(--ink-soft, #888); font: inherit; font-size: .84rem; font-weight: 600;
109 padding: .3rem .85rem; border-radius: 999px; cursor: pointer; }
110 .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
111
[8ed65a6]112 .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
113 padding: .45rem .7rem; border-radius: 10px;
114 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
115 .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
116 .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
117 .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
118 .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
119
[f1a23b8]120 .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
121
122 .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
123 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent);
124 border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent);
125 transition: border-color .15s ease, box-shadow .15s ease; }
126 .msg-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 30%, transparent);
127 box-shadow: 0 2px 12px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
128 .msg-item.is-new { border-color: color-mix(in srgb, var(--accent, #06c) 45%, transparent);
129 background: color-mix(in srgb, var(--accent, #06c) 4%, transparent); }
130 .msg-sent { border-left: 3px solid color-mix(in srgb, var(--accent, #06c) 55%, transparent); }
131
132 .msg-av { position: relative; flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%;
133 display: inline-flex; align-items: center; justify-content: center; font-weight: 700;
134 background: color-mix(in srgb, var(--accent, #888) 16%, transparent); color: var(--accent, #06c); overflow: visible; }
135 .msg-av img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; background: #fff; }
136 .msg-av-sent svg { width: 17px; height: 17px; }
137 .msg-dot { position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
138 display: inline-flex; align-items: center; justify-content: center;
139 border: 2px solid var(--paper, #fff); background: var(--accent, #06c); color: #fff; }
140 .msg-dot svg { width: 10px; height: 10px; }
141 .msg-dot-like { background: #e8b04b; } .msg-dot-boost { background: #2fa85a; }
142 .msg-dot-report { background: #c0392b; } .msg-dot-mention { background: #8e6cf0; }
143
144 .msg-body { flex: 1; min-width: 0; }
145 .msg-line { display: flex; align-items: baseline; gap: .4rem; }
146 .msg-who { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
147 a.msg-who:hover { text-decoration: underline; }
148 .msg-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
149 .msg-new { width: 8px; height: 8px; border-radius: 50%; background: var(--accent, #06c); flex: 0 0 auto; align-self: center; }
150 .msg-time { color: var(--ink-soft, #999); font-size: .76rem; margin-left: auto; flex: 0 0 auto; white-space: nowrap; }
151 .msg-what { color: var(--ink-soft, #aaa); margin-top: .12rem; line-height: 1.4; }
152 .msg-what .msg-post { color: var(--accent, #06c); font-weight: 600; margin-left: .25rem; }
153 .msg-priv { display: inline-block; margin-left: .3rem; padding: .05rem .5rem; border-radius: 999px; font-size: .72rem; font-weight: 700;
154 background: color-mix(in srgb, #8e6cf0 14%, transparent); color: #8e6cf0; }
155 .msg-content { margin: .45rem 0 0; padding: .55rem .75rem; border-radius: 10px; line-height: 1.5;
156 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); color: var(--ink, inherit); overflow-wrap: anywhere; }
157 .msg-content p { margin: .2rem 0; } .msg-content p:first-child { margin-top: 0; } .msg-content p:last-child { margin-bottom: 0; }
158
[544e9c2]159 .msg-poll { margin: .5rem 0 0; display: flex; flex-direction: column; gap: .4rem; }
160 .msg-poll-opt { display: flex; flex-direction: column; gap: .2rem; }
161 .msg-poll-top { display: flex; justify-content: space-between; gap: .5rem; font-size: .85rem; }
162 .msg-poll-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
163 .msg-poll-pct { font-variant-numeric: tabular-nums; color: var(--ink-soft, #888); flex-shrink: 0; }
164 .msg-poll-bar { height: 7px; border-radius: 999px; overflow: hidden; background: color-mix(in srgb, var(--ink, #000) 10%, transparent); }
165 .msg-poll-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent, #06c); }
166 .msg-poll-total { font-size: .78rem; color: var(--ink-soft, #888); margin-top: .1rem; }
167
[f1a23b8]168 .msg-actions { display: flex; gap: .7rem; align-items: center; margin-top: .45rem; }
169 .msg-edit summary { cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); font-weight: 600; }
170 .msg-edit-form { margin-top: .4rem; display: flex; flex-direction: column; gap: .4rem; }
171 .msg-edit-form textarea { width: 100%; font: inherit; padding: .5rem .65rem; border-radius: 8px;
172 border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); background: transparent; color: var(--ink, inherit); }
173 .msg-del { background: none; border: none; padding: 0; font: inherit; font-size: .8rem; font-weight: 600;
174 color: var(--ink-soft, #888); cursor: pointer; }
175 .msg-del:hover { color: #c0392b; }
176
177 .msg-bm { margin: 1.4rem 0 0; border-top: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); padding-top: .8rem; }
178 .msg-bm summary { cursor: pointer; color: var(--ink-soft, #888); font-weight: 600; font-size: .88rem; }
179 .msg-bm-help { color: var(--ink-soft, #888); font-size: .82rem; line-height: 1.5; margin: .5rem 0 .6rem; }
180</style>
Note: See TracBrowser for help on using the repository browser.