# Notifications Page — `notifications.html`

## Overview

A notifications page that shows when someone likes, comments, follows, sends a message, zaps, or otherwise interacts with the current user. Features a **read/unread** system backed by a timestamp in user settings (NIP-78), and a "Mark all as read" text button in the footer that clears the screen.

## Architecture

### Data Flow

```mermaid
flowchart TD
    A[Page Load] --> B[initNDKPage + getPubkey]
    B --> C[getUserSettings - read notificationsReadAt timestamp]
    C --> D[subscribe: kinds 1,3,6,7,9735 with #p = currentPubkey]
    D --> E[ndkEvent listener]
    E --> F{Filter: not own events + dedup}
    F --> G[Determine notification type from event.kind]
    G --> H[Fetch actor profile via profileCache]
    H --> I[Render notification div: avatar + name + description]
    I --> J{event.created_at > notificationsReadAt?}
    J -->|Yes| K[Style as unread]
    J -->|No| L[Style as read]
    K --> M[Insert into divNotifications sorted by created_at desc]
    L --> M
```

### Read/Unread Model

```mermaid
flowchart LR
    A[User Settings NIP-78] -->|notifications.readAt| B[Unix timestamp]
    B --> C{For each notification event}
    C -->|event.created_at > readAt| D[UNREAD - bold/highlighted]
    C -->|event.created_at <= readAt| E[READ - muted/normal]
    F[Mark All As Read button] -->|patchUserSettings| G[Set readAt = now]
    G --> H[Remove all notifications from screen]
```

The `readAt` timestamp lives in the app-wide user settings under the `notifications` namespace:

```javascript
// Reading
const settings = await getUserSettings();
const readAt = settings?.notifications?.readAt || 0;

// Writing (mark all as read)
await patchUserSettings({
  notifications: { readAt: Math.floor(Date.now() / 1000) }
});
```

### Nostr Subscription Filter

```javascript
subscribe({
  kinds: [1, 3, 6, 7, 9735],
  '#p': [currentPubkey],
  limit: 50
}, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });
```

This captures:
- **Kind 7** — Reactions / likes on your posts
- **Kind 6** — Reposts of your posts
- **Kind 1** — Replies to your posts and mentions of you
- **Kind 3** — New followers (contact list events that tag you)
- **Kind 9735** — Zap receipts for your posts

> **Note on Kind 4 (DMs):** NIP-04 DMs are encrypted and won't appear in `#p` tag filters on most relays. The msg.html page handles DMs separately. We exclude kind 4 from this page.

### Notification Item Layout

Each notification is a horizontal flex div:

```
┌──────────────────────────────────────────────────────┐
│  ┌──────┐                                            │
│  │      │  Alice                                     │
│  │avatar│  liked your post                      2m   │
│  │ 48px │                                            │
│  └──────┘                                            │
└──────────────────────────────────────────────────────┘
```

- **Left:** Medium avatar (48×48px, rounded)
- **Middle:** Person's display name (bold) on first line, notification description on second line
- **Right:** Relative time (2m, 5h, 3d)

### Notification Type Descriptions

| Kind | Type | Description Text |
|------|------|-----------------|
| 7 | Reaction | `liked your post` (or shows the reaction content like 🤙) |
| 6 | Repost | `reposted your post` |
| 1 (with e tag) | Reply | `replied to your post` |
| 1 (no e tag) | Mention | `mentioned you` |
| 3 | Follow | `started following you` |
| 9735 | Zap | `zapped you` (with amount if parseable from bolt11) |

### Footer Layout

The footer's second div (`divFooterCenter`) contains the "Mark all as read" text button:

```html
<div id="divFooterCenter" class="divFooterBox">
  <span id="btnMarkAllRead" style="cursor: pointer;">Mark all as read</span>
</div>
```

When clicked:
1. Call `patchUserSettings({ notifications: { readAt: Math.floor(Date.now() / 1000) } })`
2. Clear `divNotifications.innerHTML = ''` — removes all notifications from screen
3. Update local `notificationsReadAt` variable

### Profile Resolution

Uses the same pattern as other pages via `createProfileCache` from `profile-cache.mjs`:

1. Create a profile cache instance configured with `fetchCachedProfile` and `ndkFetchEvents`
2. For each notification event, call `profileCache.getOrFetch(event.pubkey)`
3. Render avatar from `profile.picture` and name from `profile.display_name || profile.name || pubkey.slice(0,8)+'…'`

### Reused Modules

| Module | What we reuse |
|--------|--------------|
| [`init-ndk.mjs`](www/js/init-ndk.mjs) | `initNDKPage`, `getPubkey`, `injectHeaderAvatar`, `subscribe`, `ndkFetchEvents`, `fetchCachedProfile`, `storeProfile`, `getUserSettings`, `patchUserSettings`, `onUserSettings` |
| [`profile-cache.mjs`](www/js/profile-cache.mjs) | `createProfileCache` for actor profile resolution |
| [`post-interactions.mjs`](www/js/post-interactions.mjs) | `formatTimeAgo`, `registerTimeAgo` for relative timestamps |
| [`relay-ui.mjs`](www/js/relay-ui.mjs) | Footer/sidenav relay status |
| [`blossom-ui.mjs`](www/js/blossom-ui.mjs) | Blossom section in sidenav |

### File Structure

```
www/notifications.html    — Single page, all logic inline (same pattern as feed.html)
```

No new JS modules needed.

## Implementation Steps

### 1. Update notifications.html page structure
- Change `<title>` to "NOTIFICATIONS"
- Set header text to "NOTIFICATIONS"
- Add `<meta name="viewport">` tag (missing from template)
- Add `divNotifications` container inside `divBody`
- Add "Mark all as read" text button in `divFooterCenter`

### 2. Add notification-specific CSS (inline `<style>`)
- `#divBody` override: column layout, scroll, centered
- `#divNotifications` — container for notification items, flex column, gap
- `.notif-item` — horizontal flex row, padding, border-bottom separator
- `.notif-item.unread` — slightly highlighted background or left border accent
- `.notif-avatar` — 48×48px rounded image
- `.notif-content` — flex column for name + description
- `.notif-name` — bold, primary color
- `.notif-desc` — muted color, smaller font
- `.notif-time` — right-aligned, muted, small
- `#btnMarkAllRead` — text button style, no border, cursor pointer, accent color on hover

### 3. Import required modules
- Import from `init-ndk.mjs`: `initNDKPage`, `getPubkey`, `injectHeaderAvatar`, `subscribe`, `ndkFetchEvents`, `fetchCachedProfile`, `storeProfile`, `getUserSettings`, `patchUserSettings`, `onUserSettings`, `getVersion`, `updateVersionDisplay`, `disconnect`
- Import from `profile-cache.mjs`: `createProfileCache`
- Import `formatTimeAgo` and `registerTimeAgo` from `post-interactions.mjs` (exported via `initInteractions`)
- Import standard hamburger, relay-ui, blossom-ui modules

### 4. Initialize profile cache
- Create profile cache with `fetchCachedProfile` and `ndkFetchEvents` from init-ndk
- Use it to resolve actor profiles for each notification

### 5. Implement notification subscription and rendering
- After auth, read `notificationsReadAt` from `getUserSettings()`
- Subscribe to `{ kinds: [1, 3, 6, 7, 9735], '#p': [currentPubkey], limit: 50 }`
- Listen for `ndkEvent` window events
- For each event:
  - Skip if `event.pubkey === currentPubkey` (own events)
  - Skip if already seen (dedup by event.id)
  - Determine notification type from kind + tags
  - Fetch actor profile
  - Create notification div with avatar, name, description, time
  - Apply read/unread styling based on `event.created_at` vs `notificationsReadAt`
  - Insert into DOM sorted by `created_at` descending

### 6. Implement "Mark all as read" button
- On click: `patchUserSettings({ notifications: { readAt: Math.floor(Date.now() / 1000) } })`
- Clear all notification items from the DOM
- Update local `notificationsReadAt` variable
- Subscribe to `onUserSettings` for cross-tab sync of readAt changes

### 7. Handle kind 3 (follow) notifications
- Kind 3 events are contact lists — the entire follow list is in the event
- A kind 3 event with `#p` tag matching currentPubkey means someone added you to their follow list
- Display as "started following you"

### 8. Add navigation link
- Add notifications.html to the sidenav file list or index page app buttons
