Files
client/plans/post-notifications-mode.md
2026-04-17 16:52:51 -04:00

14 KiB

Notifications Mode for post.html

Overview

Add a notifications=true URL parameter to post.html that transforms the page into a notifications view. Notification events are rendered as full post cards using the existing renderPostItem() from post-interactions.mjs, preserving all interaction capabilities — liking, replying, quoting, zapping, and commenting — on every notification item.

URL Parameter

post.html?notifications=true

Added to the existing parameter set documented in the post.html header comment:

Parameter Values Default Description
notifications true/false false Show notifications directed at the current user

When notifications=true:

  • post box is hidden (implies post=false)
  • follows and viewed are ignored
  • npub is ignored (notifications are always for the logged-in user)
  • Header text changes to NOTIFICATIONS
  • Footer shows notification count instead of post count

Data Flow

flowchart TD
    A[Page Load] --> B[Parse URL: notifications=true]
    B --> C[initNDKPage + getPubkey]
    C --> D[Subscribe: kinds 1,6,7,9735 with #p = currentPubkey]
    C --> E[fetchEventsFromAllRelays for initial batch]
    D --> F[ndkEvent listener]
    E --> F
    F --> G{Is notification for me?}
    G -->|No: own event or not tagged| H[Discard]
    G -->|Yes| I[Fetch referenced post if needed]
    I --> J[Store in notifications array]
    J --> K[Render as full post card via renderPostItem]
    K --> L[Append interaction bar: like, reply, quote, zap, comment]

Rendering Approach — Full Post Cards

The key design decision: notifications render the referenced post as a full interactive card, not a compact notification summary. Each notification shows:

  1. A notification header — small banner above the post card showing who did what
    • Avatar + name of the actor
    • Action type badge: L liked, R reposted, C replied, Z zapped, @ mentioned
    • Relative timestamp
  2. The referenced post — rendered via renderPostItem() with full interaction bar
    • For reactions/reposts/zaps: shows YOUR original post that was interacted with
    • For replies: shows the REPLY itself as the main card, with your original post as a compact reference below
    • For mentions: shows the mentioning post as the main card

This means every notification card supports:

Card Layout

┌─────────────────────────────────────────────────┐
│ 🔔 Alice liked your post                   2m  │  ← notification header
├─────────────────────────────────────────────────┤
│ [Your Avatar] You                               │  ← standard post card
│                                                 │
│ Your original post content here...              │
│                                                 │
│ [Like] [Reply] [Quote] [Zap] [Time]            │  ← full interaction bar
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 🔔 Bob replied to your post                5m  │  ← notification header
├─────────────────────────────────────────────────┤
│ [Bob Avatar] Bob                                │  ← reply post card
│                                                 │
│ Bob reply content here...                       │
│                                                 │
│ [Like] [Reply] [Quote] [Zap] [Time]            │  ← full interaction bar
│ ┌─────────────────────────────────────────────┐ │
│ │ In reply to:                                │ │  ← compact reference
│ │ Your original post snippet...               │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

Implementation Details

1. URL Parameter Parsing

Add to the existing parameter parsing block at line 648:

const notificationsParam = urlParams.get('notifications');
const showNotifications = notificationsParam === 'true';

Mode precedence: notifications overrides viewed, follows, profile, and post:

const showNotifications = notificationsParam === 'true';
const showViewed = showNotifications ? false : (viewedParam === 'true');
const showProfile = showNotifications ? false : (profileParam === 'true');
const showPostBox = showNotifications ? false : (showViewed ? false : (postParam !== 'false'));
const showFollows = showNotifications ? false : (showViewed ? true : (followsParam === 'true'));

2. Notification State Variables

// Notifications mode state
const NOTIF_KINDS = [1, 6, 7, 9735];
const MAX_NOTIFICATIONS = 300;
let notifications = [];
let notificationIds = new Set();
let referencedPosts = new Map();  // eventId -> event (your original posts)
let notifRenderCount = 20;

3. Notification Subscription

In the main() function, when showNotifications is true:

if (showNotifications) {
    document.title = 'NOTIFICATIONS';
    headerText.textContent = 'NOTIFICATIONS';
    
    // Subscribe to events tagging the current user
    subscribe(
        { kinds: NOTIF_KINDS, '#p': [currentPubkey], limit: 50 },
        { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
    );
    
    // Also do a direct fetch for initial population
    fetchEventsFromAllRelays({
        kinds: NOTIF_KINDS, '#p': [currentPubkey], limit: 50
    });
}

4. Event Processing in ndkEvent Handler

Add a new branch in the ndkEvent listener for notifications mode:

if (showNotifications && NOTIF_KINDS.includes(evt.kind)) {
    // Filter: skip own events, skip duplicates
    if (evt.pubkey === currentPubkey) return;
    if (notificationIds.has(evt.id)) return;
    
    // For kind 1: only keep if current user is in p-tags
    if (evt.kind === 1) {
        const pTags = evt.tags?.filter(t => t[0] === 'p').map(t => t[1]) || [];
        if (!pTags.includes(currentPubkey)) return;
    }
    
    // Fetch referenced post for context
    await ensureReferencedPost(evt);
    
    // For reactions/reposts/zaps: only keep if referenced post is ours
    if ([6, 7, 9735].includes(evt.kind)) {
        const refId = getRootEventId(evt);
        const refEvent = referencedPosts.get(refId);
        if (!refEvent || refEvent.pubkey !== currentPubkey) return;
    }
    
    notificationIds.add(evt.id);
    notifications.push(evt);
    pruneNotifications();
    renderNotificationsFeed();
}

5. Rendering Notifications

function createNotificationElement(notifEvent) {
    const wrapper = document.createElement('div');
    wrapper.className = 'divNotifWrapper';
    
    // 1. Notification header (who did what)
    const header = createNotifHeader(notifEvent);
    wrapper.appendChild(header);
    
    // 2. The main post card — determine which event to render
    let mainEvent;
    if (notifEvent.kind === 1) {
        // Reply/mention: show the reply itself
        mainEvent = notifEvent;
    } else {
        // Like/repost/zap: show the referenced (your) post
        const refId = getRootEventId(notifEvent);
        mainEvent = referencedPosts.get(refId) || notifEvent;
    }
    
    // Render as full interactive post card
    const postEl = interactions.renderPostItem(mainEvent, {
        currentPubkey,
        showHeader: true,
        isCompact: false
    });
    wrapper.appendChild(postEl);
    
    // 3. For replies: add compact reference to original post
    if (notifEvent.kind === 1) {
        const refId = getRootEventId(notifEvent);
        const refEvent = referencedPosts.get(refId);
        if (refEvent) {
            const refSnippet = createCompactReference(refEvent);
            wrapper.appendChild(refSnippet);
        }
    }
    
    return wrapper;
}

6. Notification Header CSS

.divNotifWrapper {
    margin-bottom: 15px;
}

.divNotifHeader {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 12px;
    font-size: 80%;
    color: var(--muted-color);
    border: 1px solid var(--muted-color);
    border-bottom: none;
    border-radius: 10px 10px 0 0;
    background: var(--secondary-color);
}

.divNotifHeader .notif-avatar {
    width: 20px;
    height: 20px;
    border-radius: 3px;
    object-fit: cover;
}

.divNotifHeader .notif-type-badge {
    border: 1px solid var(--muted-color);
    color: var(--accent-color);
    border-radius: 999px;
    min-width: 18px;
    height: 18px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 75%;
    font-weight: bold;
    padding: 0 4px;
}

.divNotifWrapper .divPostItem {
    border-radius: 0 0 10px 10px;
    margin-bottom: 0;
}

7. See More Button

Reuse the existing btnSeeMore element. In notifications mode, wire it to:

  1. First expand notifRenderCount if more notifications exist in memory
  2. Then fetch older notifications with until parameter

In the UpdateFooter function, add notifications branch:

if (showNotifications) {
    divFooterRight.innerHTML = `${notifications.length} notifications`;
}

Change the existing notifications link in the sidenav from:

<a href="./notifications.html">NOTIFICATIONS</a>

to:

<a href="./post.html?notifications=true">NOTIFICATIONS</a>

10. Interaction Subscriptions for Notification Posts

After rendering notification cards, subscribe to interactions on the rendered posts so like/reply/quote/zap counts update in real-time — same pattern as the existing renderFeed() function that calls fetchExistingInteractions() and subscribeToInteractions().

11. Inline Reply Composer

The existing handleComposerCommentIntent() and handleComposerQuoteIntent() functions work by finding .divPostItem[data-post-id="..."] in the DOM. Since notification cards contain standard post items rendered by renderPostItem(), the inline composer will work automatically — no changes needed.

However, since showPostBox is false in notifications mode, the inline composers won't mount. We need to override this: in notifications mode, allow inline composers on notification post cards even though the top-level post box is hidden. This requires a small adjustment to getOrCreateInlineComposer() to check showNotifications || showPostBox.

Index Page — LOVE Card

Add a new card to the arrApps array in index.html called LOVE that links to post.html?notifications=true.

The card follows the same pattern as feed and view which have special URL mappings at line 594-598. Add a new condition for love:

{ id: `love`, svg: ``, name: `LOVE` },

And in the URL mapping logic:

const url = Each.id === 'feed'
  ? './post.html?follows=true&post=false'
  : Each.id === 'view'
    ? './post.html?follows=true&post=false&viewed=true'
    : Each.id === 'love'
      ? './post.html?notifications=true'
      : `./${Each.id}.html`;

The SVG is left empty for now as requested.

Files Modified

File Changes
www/post.html Add notifications URL param, notification state, subscription, event handler branch, rendering functions, CSS, sidenav link update
www/index.html Add LOVE card to arrApps array, add URL mapping for love ID

No new files needed. All logic stays inline following the existing patterns.

Implementation Steps

  1. Add notifications=true URL parameter parsing and mode flags in post.html
  2. Add notification-specific CSS styles (notification header, wrapper)
  3. Add notification state variables and helper functions
  4. Add notification subscription in main() initialization
  5. Add notification event processing branch in ndkEvent listener
  6. Add notification rendering functions (createNotificationElement, renderNotificationsFeed, createNotifHeader, createCompactReference)
  7. Wire See More button for notifications mode
  8. Update footer to show notification count
  9. Update sidenav link to use post.html?notifications=true
  10. Allow inline composers in notifications mode (adjust showPostBox guard)
  11. Wire interaction subscriptions for rendered notification post cards
  12. Update the post.html header comment to document the new parameter
  13. Add LOVE card to arrApps array in index.html with empty SVG
  14. Add URL mapping for lovepost.html?notifications=true in index.html