# Notifications Mode for `post.html` ## Overview Add a `notifications=true` URL parameter to [`post.html`](../www/post.html) that transforms the page into a notifications view. Notification events are rendered as full post cards using the existing [`renderPostItem()`](../www/js/post-interactions.mjs:549) 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](../www/post.html:3): | 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 ```mermaid 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()`](../www/js/post-interactions.mjs:549) 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: - Liking (via [`publishLike()`](../www/js/post-interactions.mjs:1724)) - Replying (via inline composer + [`handleComposerCommentIntent()`](../www/post.html:1794)) - Quoting (via inline composer + [`handleComposerQuoteIntent()`](../www/post.html:1809)) - Zapping (via interaction bar) - Viewing/toggling comments (via [`renderCommentThread()`](../www/js/post-interactions.mjs:1336)) ### 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](../www/post.html:648): ```javascript const notificationsParam = urlParams.get('notifications'); const showNotifications = notificationsParam === 'true'; ``` Mode precedence: `notifications` overrides `viewed`, `follows`, `profile`, and `post`: ```javascript 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 ```javascript // 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: ```javascript 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](../www/post.html:2098) for notifications mode: ```javascript 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 ```javascript 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 ```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`](../www/post.html:590) 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 ### 8. Footer Update In the [`UpdateFooter`](../www/post.html:1997) function, add notifications branch: ```javascript if (showNotifications) { divFooterRight.innerHTML = `${notifications.length} notifications`; } ``` ### 9. Sidenav Link Update Change the existing [notifications link in the sidenav](../www/post.html:609) from: ```html NOTIFICATIONS ``` to: ```html NOTIFICATIONS ``` ### 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()`](../www/post.html:1641) function that calls [`fetchExistingInteractions()`](../www/js/post-interactions.mjs:1813) and [`subscribeToInteractions()`](../www/js/post-interactions.mjs:1462). ### 11. Inline Reply Composer The existing [`handleComposerCommentIntent()`](../www/post.html:1794) and [`handleComposerQuoteIntent()`](../www/post.html:1809) 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()`](../www/post.html:1755) to check `showNotifications || showPostBox`. ## Index Page — LOVE Card Add a new card to the [`arrApps`](../www/index.html:420) array in [`index.html`](../www/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](../www/index.html:594). Add a new condition for `love`: ```javascript { id: `love`, svg: ``, name: `LOVE` }, ``` And in the URL mapping logic: ```javascript 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`](../www/post.html) | Add `notifications` URL param, notification state, subscription, event handler branch, rendering functions, CSS, sidenav link update | | [`www/index.html`](../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 `love` → `post.html?notifications=true` in `index.html`