Changeset 8ed65a6 in Klonkt for src/views/pages/messages.ejs


Ignore:
Timestamp:
07/19/2026 11:43:33 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
544e9c2
Parents:
ff3b8ce
git-author:
Robin <roboburr@…> (07/19/2026 11:43:10 PM)
git-committer:
Robin <roboburr@…> (07/19/2026 11:43:33 PM)
Message:

Feature: Messages filters regrouped + search (klonkt-demo-3jf)

The four coarse chips become meaningful buckets and gain a search box.

  • Buckets: Messages (@mentions + private/DM replies), Conversations (public replies), Activity (likes/boosts/follows), Moderation (reports), Sent, All. data-kind is computed from the item type and visibility (a followers/direct reply now lands under Messages, not Conversations).
  • Search box filters by sender + rendered message text + linked post title, indexed once per item; chip and search combine in JS (an item shows only when both match). A 'nothing found' line appears when the result set is empty.
  • Filtering moved from the old CSS data-show rules to JS so search can layer on; the search field matches the framed style used elsewhere.
  • i18n NL/EN/DE for the two new chips, the placeholder and the empty line.

97 tests green (view-only change). Browser-verified: DM + mention under
Messages (2), report under Moderation (1), public reply under Conversations (1);
search 'geheime dm' -> the DM only; a no-hit term -> empty with the message;
Messages chip + a Conversations-only term -> 0.

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

File:
1 edited

Legend:

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

    rff3b8ce r8ed65a6  
    77  <div class="msg-filters" role="tablist" aria-label="<%= t('msg.title') %>">
    88    <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>
    910    <button type="button" class="msg-chip" data-show="conv"><%= t('msg.filter_conv') %></button>
    1011    <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>
    1113    <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') %>">
    1218  </div>
    1319
     
    1925      <% items.forEach(function(n){
    2026           var _t = n.type === 'announce' ? 'boost' : n.type; // reply|mention|report|like|boost|follow|sent
    21            var _kind = (_t === 'like' || _t === 'boost' || _t === 'follow') ? 'act' : (_t === 'sent' ? 'sent' : 'conv');
    2227           var _new = _seen && n.created_at ? (Date.parse(n.created_at) > _seen) : false;
    2328           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') ? 'act'
     35             : _t === 'report' ? 'mod'
     36             : (_t === 'mention' || _priv0) ? 'msgs'
     37             : 'conv';
    2438           var _init = String(_who || '?').replace(/^@/, '').charAt(0).toUpperCase();
    2539           var _priv = (n.visibility === 'followers' || n.visibility === 'direct');
    2640      %>
    27         <li class="msg-item msg-<%= _t %><%= _new ? ' is-new' : '' %>" data-kind="<%= _kind %>">
     41        <li class="msg-item msg-<%= _t %><%= _new ? ' is-new' : '' %>" data-kind="<%= _kind %>" data-who="<%= String(_who).toLowerCase() %>">
    2842          <span class="msg-av<%= _t === 'sent' ? ' msg-av-sent' : '' %>" aria-hidden="true">
    2943            <% 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>
     
    100114      <% }); %>
    101115    </ul>
     116    <p class="msg-nomatch" hidden><%= t('msg.no_match') %></p>
    102117  <% } %>
    103118
     
    116131(function () {
    117132  if (window.__msgWired) return; window.__msgWired = true;
     133
     134  // Filtering: kind chip AND free-text search, combined in JS (search over the
     135  // sender and the rendered message text). Empty search + "all" = show all.
     136  var list = document.querySelector('.msg-list');
     137  var noMatch = document.querySelector('.msg-nomatch');
     138  var q = document.getElementById('msg-q');
     139  var kind = 'all';
     140  var items = list ? Array.prototype.slice.call(list.querySelectorAll('.msg-item')) : [];
     141  items.forEach(function (li) {
     142    // Index once: sender + message body + linked post title.
     143    var body = li.querySelector('.msg-content');
     144    var post = li.querySelector('.msg-post');
     145    li._search = ((li.getAttribute('data-who') || '') + ' ' +
     146      (body ? body.textContent : '') + ' ' + (post ? post.textContent : '')).toLowerCase();
     147  });
     148  function apply() {
     149    if (!list) return;
     150    var term = (q && q.value || '').trim().toLowerCase();
     151    var shown = 0;
     152    items.forEach(function (li) {
     153      var ok = (kind === 'all' || li.getAttribute('data-kind') === kind) &&
     154        (!term || li._search.indexOf(term) !== -1);
     155      li.style.display = ok ? '' : 'none';
     156      if (ok) shown++;
     157    });
     158    if (noMatch) noMatch.hidden = shown !== 0;
     159  }
    118160  document.addEventListener('click', function (e) {
    119161    var chip = e.target.closest('.msg-chip'); if (!chip) return;
    120     var list = document.querySelector('.msg-list'); if (!list) return;
    121     list.setAttribute('data-show', chip.getAttribute('data-show'));
     162    kind = chip.getAttribute('data-show');
    122163    document.querySelectorAll('.msg-chip').forEach(function (c) { c.classList.toggle('is-on', c === chip); });
     164    apply();
    123165  });
     166  if (q) q.addEventListener('input', apply);
     167
    124168  var a = document.getElementById('fedi-bm-btn');
    125169  if (a) {
     
    141185  .msg-chip.is-on { background: var(--accent, #06c); border-color: var(--accent, #06c); color: #fff; }
    142186
     187  .msg-search { display: flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem;
     188    padding: .45rem .7rem; border-radius: 10px;
     189    border: 1px solid color-mix(in srgb, var(--ink, #000) 16%, transparent); }
     190  .msg-search:focus-within { border-color: var(--accent, #06c); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent, #06c) 18%, transparent); }
     191  .msg-search svg { width: 16px; height: 16px; color: var(--ink-soft, #888); flex-shrink: 0; }
     192  .msg-search input { flex: 1; border: none; background: none; color: inherit; font: inherit; outline: none; min-width: 0; }
     193  .msg-nomatch { color: var(--ink-soft, #888); text-align: center; padding: 1.2rem 0; }
     194
    143195  .msg-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .55rem; }
    144   .msg-list[data-show="conv"] li:not([data-kind="conv"]) { display: none; }
    145   .msg-list[data-show="act"]  li:not([data-kind="act"])  { display: none; }
    146   .msg-list[data-show="sent"] li:not([data-kind="sent"]) { display: none; }
    147196
    148197  .msg-item { display: flex; gap: .8rem; padding: .85rem .95rem; border-radius: 14px; align-items: flex-start;
Note: See TracChangeset for help on using the changeset viewer.