Ignore:
Timestamp:
07/18/2026 12:53:15 AM (8 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
05cd954
Parents:
7d19465
git-author:
Robin <roboburr@…> (07/18/2026 12:52:51 AM)
git-committer:
Robin <roboburr@…> (07/18/2026 12:53:15 AM)
Message:

Feature: Messages merges Replies + Notifications into one inbox

/messages replaces /fediverse (manage own replies) and /notifications with a
single stream, per the Robin+Bart decision. getMessages() merges notifications
with your outbound replies ('sent' items, edit/delete via their outbox routes)
and collapses consecutive likes/boosts on the same post into one grouped item.
Design refresh: filter chips (All/Conversations/Activity/Sent, client-side),
avatars with a type-dot overlay, sent items with a direction indicator and
accent border, a lock badge on private replies (visibility now flows through
getNotifications along with actor icons), and unread dots via the existing
seen-watermark (read before marking seen). The two tabs collapse into one
Messages tab carrying the badge; the bookmarklet moved to /messages; old routes
redirect. Also fixes a latent NaN-unsafe date sort in getNotifications that
scrambled ordering when a row had a garbled created_at (seen live). i18n
NL/EN/DE. 4 new tests (67 green); verified in a browser incl. grouping, chips,
private badge, edit/delete forms. Beads: klonkt-demo-pkg. Old
fedi-notifications.ejs and the authorize-interaction manage block are now
unreachable; cleanup tracked in klonkt-demo-bk8.

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

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/services/ActivityPubService.js

    r7d19465 rf1a23b8  
    469469  } catch { return 0; }
    470470}
     471// The seen-watermark itself (ms epoch, 0 = never marked) — the Messages page reads it
     472// BEFORE marking seen, so it can render unread dots on the items newer than last visit.
     473export function notificationsSeenAt(slug) {
     474  try {
     475    const row = db.prepare('SELECT value FROM app_settings WHERE key = ?').get(`fedi_notif_seen:${slug}`);
     476    return row ? (Date.parse(row.value) || 0) : 0;
     477  } catch { return 0; }
     478}
     479
     480// Messages = the unified inbox (Reacties + Meldingen merged, decision Robin+Bart 2026-07-16):
     481// every notification PLUS your own outbound replies ('sent', with edit/delete via their
     482// outboxId), sorted as one stream. Consecutive likes/boosts on the same post collapse into
     483// one grouped item (actors list + count) so activity doesn't drown out conversations.
     484export function getMessages(slug, limit) {
     485  const items = getNotifications(slug, Math.max(120, (limit || 60)));
     486  try {
     487    for (const m of listOutbox(slug).slice(0, 80)) {
     488      items.push({
     489        type: 'sent', outboxId: m.id, to_handle: m.to_handle, in_reply_to: m.in_reply_to,
     490        content: m.content, editable: m.editable, created_at: m.created_at,
     491      });
     492    }
     493  } catch { /* ignore */ }
     494  items.sort((a, b) => _msgTs(b) - _msgTs(a)); // NaN-safe (zie getNotifications)
     495  const out = [];
     496  for (const it of items) {
     497    const prev = out[out.length - 1];
     498    if ((it.type === 'like' || it.type === 'announce') && prev && prev.type === it.type
     499        && prev.post_slug === it.post_slug) {
     500      prev.actors = prev.actors || [prev.name || prev.handle || '?'];
     501      prev.actors.push(it.name || it.handle || '?');
     502      prev.count = (prev.count || 1) + 1;
     503      continue;
     504    }
     505    out.push(it);
     506  }
     507  return out.slice(0, limit || 60);
     508}
    471509
    472510export function buildCreate(base, site, post) {
     
    24052443  try {
    24062444    const rows = db.prepare(`
    2407       SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.content, i.created_at,
     2445      SELECT i.kind, i.actor_name, i.actor_handle, i.actor_url, i.actor_icon, i.content, i.created_at, i.visibility,
    24082446             p.slug AS post_slug, p.title AS post_title
    24092447      FROM ap_interactions i LEFT JOIN posts p ON p.id = i.post_id
     
    24122450    `).all(slug);
    24132451    for (const r of rows) out.push({
    2414       type: r.kind, name: r.actor_name, handle: r.actor_handle, url: r.actor_url,
     2452      type: r.kind, name: r.actor_name, handle: r.actor_handle, url: r.actor_url, icon: r.actor_icon,
    24152453      content: stripLeadingMentions(r.content), post_slug: r.post_slug, post_title: r.post_title, created_at: r.created_at,
     2454      // followers/direct = a private message to the owner (not on the public thread) → 🔒 in Messages
     2455      visibility: r.visibility || 'public',
    24162456    });
    24172457  } catch { /* ignore */ }
     
    24262466    }
    24272467  } catch { /* ignore */ }
    2428   out.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
     2468  // NaN-safe sort: one row with a missing/garbled created_at would otherwise make the
     2469  // comparator return NaN and scramble the WHOLE ordering (seen live: follow rows landing
     2470  // between likes, which also broke Messages' like-grouping).
     2471  out.sort((a, b) => _msgTs(b) - _msgTs(a));
    24292472  return out.slice(0, limit || 60);
    24302473}
     2474function _msgTs(x) { const t = Date.parse((x && x.created_at) || ''); return Number.isFinite(t) ? t : 0; }
    24312475
    24322476// ── Blocking / defederation ───────────────────────────────────────
     
    26072651  linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
    26082652  noteVisibility, isRejectedObject, rejectInteraction, interactionReportTarget,
     2653  getMessages, notificationsSeenAt,
    26092654};
Note: See TracChangeset for help on using the changeset viewer.