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

8.3 KiB
Raw Permalink Blame History

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

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

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:

// 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

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)

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

<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 initNDKPage, getPubkey, injectHeaderAvatar, subscribe, ndkFetchEvents, fetchCachedProfile, storeProfile, getUserSettings, patchUserSettings, onUserSettings
profile-cache.mjs createProfileCache for actor profile resolution
post-interactions.mjs formatTimeAgo, registerTimeAgo for relative timestamps
relay-ui.mjs Footer/sidenav relay status
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"
  • Add notifications.html to the sidenav file list or index page app buttons