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

357 lines
14 KiB
Markdown

# 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
<a href="./notifications.html">NOTIFICATIONS</a>
```
to:
```html
<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()`](../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`