Skip to content

Spectating Feature

Overview

The Spectating feature allows users to browse another Nostr user's library in read-only mode. This enables discovery of books and annotations without requiring the spectated user to be online or take any action. All data is fetched from Nostr relays and stored locally for offline viewing.

User Journey

1. Initiating Spectate Mode

Entry Point: Eye icon button in the TopBar (right side, next to CypherTap login)

Flow: 1. User clicks the eye icon → popover opens with "Browse a user's library" 2. User enters an npub (Nostr public key in bech32 format) 3. User optionally specifies relay URLs (defaults provided) 4. User clicks "Browse this user's library" 5. System fetches profile, books, and annotations from specified relays 6. On success: page reloads, user enters spectate mode viewing the target's library

Previously Viewed Users: - History of up to 10 previously spectated users is persisted to localStorage - Users can quickly re-spectate from history with one click - History entries store: pubkey, npub, profile info, relays, lastSynced timestamp

2. Spectate Mode Experience

Visual Indicators: - Eye icon button turns blue with blue background tint - TopBar shows "Currently spectating" button (replaces CypherTap login) - Library header shows "{User}'s Library" with binoculars icon - "View Only" badge displayed - Middle-truncated npub shown under username (e.g., npub1abc...xyz)

Restrictions (Read-Only): - Cannot create/edit/delete annotations - Cannot edit book metadata - Cannot delete books - Cannot publish to Nostr - Context menu (three-dot button) is hidden on book cards - Text selection for annotation creation is disabled in reader - SyncStatusButton is hidden (can't sync someone else's data)

What Users CAN Do: - Browse the library - Open and read books (if EPUB data exists locally) - View existing annotations and highlights - Navigate table of contents - Use dark/light mode

3. Managing Spectate Session

Currently Spectating Popover: - Shows profile picture and display name (if available) - Shows middle-truncated npub - Shows relative time since last sync (e.g., "5 minutes ago") - Sync button: Re-fetch latest data from relays - Clear data button: Delete locally stored data for this user - Exit Spectate Mode: Return to own library

History Management: - View previously spectated users - Edit relay configuration per user - Remove users from history - Copy npub to clipboard

4. Exiting Spectate Mode

  • Click "Exit Spectate Mode" in the popover
  • Page reloads, user returns to their own library (if logged in) or welcome screen

Technical Architecture

Key Files

File Purpose
src/lib/stores/spectate.svelte.ts Reactive state store for spectate mode
src/lib/components/SpectateButton.svelte UI component for spectate controls
src/lib/components/TopBar.svelte Integration point in app header
src/lib/services/nostrService.ts fetchRemoteUserData() for fetching from relays
src/routes/+layout.svelte Store initialization based on spectate state
src/routes/+page.svelte Library view with spectate-aware rendering
src/routes/book/[id]/+page.svelte Reader with spectate restrictions

State Management

SpectateStore (spectate.svelte.ts):

interface SpectateTarget {
  pubkey: string;        // hex pubkey
  npub: string;          // bech32 for display
  profile?: NostrProfile;
  relays: string[];
  lastSynced?: number;   // timestamp
}

interface SpectateHistoryEntry {
  pubkey: string;
  npub: string;
  profile?: NostrProfile;
  relays: string[];
  lastSynced?: number;
}

// Reactive state (Svelte 5 runes)
let isSpectating = $state(false);
let target = $state<SpectateTarget | null>(null);
let history = $state<SpectateHistoryEntry[]>([]);

Persistence: - localStorage['sveltereader-spectate'] - Current spectate state (isSpectating, target) - localStorage['sveltereader-spectate-history'] - History array (up to 10 entries)

Data Flow

User enters npub + relays
SpectateButton.validateAndStartSpectating()
nostrService.fetchRemoteUserData(pubkey, relays)
    ┌───────────────────────────────────────┐
    │ Connects to relays via nostr-tools   │
    │ Subscribes to:                        │
    │   - kind 0 (profile)                  │
    │   - kind 30250 (books)                │
    │   - kind 30251 (annotations)          │
    │ Processes events with LWW merge       │
    └───────────────────────────────────────┘
SpectateButton.storeFetchedData()
    ┌───────────────────────────────────────┐
    │ Stores to IndexedDB:                  │
    │   - books (ownerPubkey = target)      │
    │   - annotations (ownerPubkey = target)│
    │ Updates spectate store                │
    └───────────────────────────────────────┘
spectateStore.startSpectating()
Page reload → +layout.svelte initializes stores with target pubkey

Store Initialization

In +layout.svelte, stores are initialized based on spectate state:

$effect(() => {
  if (spectateStore.isSpectating && spectateStore.target) {
    // Initialize with spectated user's pubkey
    books.initialize(spectateStore.target.pubkey);
    annotations.initialize(spectateStore.target.pubkey);
  } else if (cyphertap.pubkey) {
    // Initialize with logged-in user's pubkey
    books.initialize(cyphertap.pubkey);
    annotations.initialize(cyphertap.pubkey);
  }
});

IndexedDB Schema

Books and annotations are scoped by ownerPubkey:

// books store - indexed by 'by-owner' and 'by-sha256'
interface Book {
  id: string;
  ownerPubkey: string;  // ← scoping key
  sha256: string;
  title: string;
  // ...
}

// annotations store - indexed by 'by-owner'
interface Annotation {
  id: string;
  ownerPubkey: string;  // ← scoping key
  bookSha256: string;
  // ...
}

Conditional UI Rendering

Components check spectateStore.isSpectating to adjust behavior:

<!-- Hide context menu when spectating -->
{#if !spectateStore.isSpectating}
  <ContextMenu />
{/if}

<!-- Disable annotation creation in reader -->
epubService.onTextSelected((selection) => {
  if (isSpectating) {
    textSelection = null;
    return;
  }
  // ... handle selection
});

Future Enhancement Ideas

Discovery & Social

  • [ ] Public library listings - Browse a directory of users who've opted-in to public discovery
  • [ ] Follow system - Subscribe to users and get notified of new books/annotations
  • [ ] Recommendation engine - "Users who read X also read Y"
  • [ ] Reading groups - Shared annotations and discussions

Data & Sync

  • [ ] Selective sync - Choose which books to download EPUB data for
  • [ ] Background sync - Periodically refresh spectated users' data
  • [ ] Offline indicators - Show which books have local EPUB data vs. metadata only
  • [ ] Diff view - Show what changed since last sync

Annotations

  • [ ] Public annotations - View-only access to highlights and notes
  • [ ] Annotation replies - Comment on someone's annotations (requires new event kind)
  • [ ] Annotation sharing - Share specific annotations via Nostr

UI/UX

  • [ ] Quick switch - Easily toggle between own library and spectated libraries
  • [ ] Multi-spectate - View multiple users' libraries in tabs
  • [ ] Spectate from book - "See who else is reading this book"
  • [ ] Profile cards - Rich user profiles with reading stats

Privacy & Permissions

  • [ ] Granular visibility - Users control which books/annotations are public
  • [ ] Private spectating - Don't announce that you're viewing someone's library
  • [ ] Block list - Prevent specific users from spectating your library

Nostr Event Kinds

Kind Purpose NIP
0 User profile metadata NIP-01
30250 Book metadata (parameterized replaceable) Custom
30251 Annotation (parameterized replaceable) Custom

Book Event (kind 30250):

{
  "kind": 30250,
  "tags": [
    ["d", "<book-id>"],
    ["title", "Book Title"],
    ["author", "Author Name"],
    ["sha256", "<epub-hash>"],
    ["i", "isbn:<isbn>"]
  ],
  "content": "<optional cover base64>"
}

Annotation Event (kind 30251):

{
  "kind": 30251,
  "tags": [
    ["d", "<annotation-id>"],
    ["a", "30250:<pubkey>:<book-id>"],
    ["book", "<book-sha256>"]
  ],
  "content": "{\"cfiRange\":\"...\",\"selectedText\":\"...\",\"note\":\"...\",\"color\":\"...\"}"
}


Testing Considerations

Manual Testing Checklist

  • [ ] Spectate a user with books and annotations
  • [ ] Spectate a user with no data
  • [ ] Spectate with invalid npub
  • [ ] Spectate with unreachable relays
  • [ ] Sync after initial spectate
  • [ ] Clear data for spectated user
  • [ ] Exit spectate mode
  • [ ] Re-spectate from history
  • [ ] Edit relays for history entry
  • [ ] Verify read-only restrictions in reader
  • [ ] Verify context menu hidden on book cards
  • [ ] Verify lastSynced persists across sessions

Edge Cases

  • User spectates themselves
  • Spectated user has books but no EPUB data available
  • Relay connection timeout
  • Large number of books/annotations
  • Concurrent spectate and login state changes