Changeset 1485933 in Klonkt for src/views/pages


Ignore:
Timestamp:
07/20/2026 07:35:55 AM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
520e477
Parents:
7b04d3b
git-author:
Robin <roboburr@…> (07/20/2026 07:18:02 AM)
git-committer:
Robin <roboburr@…> (07/20/2026 07:35:55 AM)
Message:

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@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/pages/messages.ejs

    r7b04d3b r1485933  
    2222    <p class="msg-empty"><%= t('msg.empty') %></p>
    2323  <% } 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       <% }); %>
     24    <ul class="msg-list" data-show="all" id="msg-list">
     25      <% items.forEach(function(n){ %><%- include('../partials/msg-item', { n: n, seen: _seen }) %><% }); %>
    12926    </ul>
    13027    <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
     28    <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#msg-list', morePath: '/messages' }) %>
    13129  <% } %>
    13230
     
    15250  var q = document.getElementById('msg-q');
    15351  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.
     52  var items = [];
     53  function indexItem(li) {
     54    // Index once: sender + message body + linked post title + poll text.
    15755    var body = li.querySelector('.msg-content');
    15856    var post = li.querySelector('.msg-post');
     
    16159      (body ? body.textContent : '') + ' ' + (post ? post.textContent : '') + ' ' +
    16260      (poll ? poll.textContent : '')).toLowerCase();
    163   });
     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();
    16469  function apply() {
    16570    if (!list) return;
     
    18186  });
    18287  if (q) q.addEventListener('input', apply);
     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  });
    18392
    18493  var a = document.getElementById('fedi-bm-btn');
Note: See TracChangeset for help on using the changeset viewer.