8.3 KiB
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
#ptag 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:
<div id="divFooterCenter" class="divFooterBox">
<span id="btnMarkAllRead" style="cursor: pointer;">Mark all as read</span>
</div>
When clicked:
- Call
patchUserSettings({ notifications: { readAt: Math.floor(Date.now() / 1000) } }) - Clear
divNotifications.innerHTML = ''— removes all notifications from screen - Update local
notificationsReadAtvariable
Profile Resolution
Uses the same pattern as other pages via createProfileCache from profile-cache.mjs:
- Create a profile cache instance configured with
fetchCachedProfileandndkFetchEvents - For each notification event, call
profileCache.getOrFetch(event.pubkey) - Render avatar from
profile.pictureand name fromprofile.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
divNotificationscontainer insidedivBody - Add "Mark all as read" text button in
divFooterCenter
2. Add notification-specific CSS (inline <style>)
#divBodyoverride: 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
formatTimeAgoandregisterTimeAgofrompost-interactions.mjs(exported viainitInteractions) - Import standard hamburger, relay-ui, blossom-ui modules
4. Initialize profile cache
- Create profile cache with
fetchCachedProfileandndkFetchEventsfrom init-ndk - Use it to resolve actor profiles for each notification
5. Implement notification subscription and rendering
- After auth, read
notificationsReadAtfromgetUserSettings() - Subscribe to
{ kinds: [1, 3, 6, 7, 9735], '#p': [currentPubkey], limit: 50 } - Listen for
ndkEventwindow 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_atvsnotificationsReadAt - Insert into DOM sorted by
created_atdescending
- Skip if
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
notificationsReadAtvariable - Subscribe to
onUserSettingsfor 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
#ptag 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