# Streaming Page Plan

## Overview

Add a streaming page (`stream.html`) for users who stream live content. The page implements [NIP-53 Live Activities](reference_repos/nips/53.md) using `kind:30311` (Live Streaming Event) and `kind:1311` (Live Chat Message). The streamer can manage their live event, view their stream, and interact with chat — all using existing components like [`mountComposer()`](www/js/post-composer.mjs:152) and [`post-interactions.mjs`](www/js/post-interactions.mjs:1).

## NIP-53 Summary

### kind:30311 — Live Streaming Event (addressable)
An addressable event the streamer publishes and updates throughout the stream lifecycle:

```
tags:
  ["d", "<unique-id>"]
  ["title", "<stream title>"]
  ["summary", "<description>"]
  ["image", "<preview image url>"]
  ["streaming", "<url>"]           // HLS/m3u8 or other stream URL
  ["status", "planned|live|ended"]
  ["starts", "<unix timestamp>"]
  ["ends", "<unix timestamp>"]
  ["t", "<hashtag>"]
  ["current_participants", "<n>"]
  ["total_participants", "<n>"]
  ["p", "<pubkey>", "<relay>", "<role>"]
  ["relays", "wss://...", ...]
  ["recording", "<url>"]           // post-stream recording
  ["pinned", "<event-id>"]         // pinned chat message
```

### kind:1311 — Live Chat Message
Chat messages scoped to a live event via an `a` tag:

```
tags:
  ["a", "30311:<pubkey>:<d-tag>", "<relay>", "root"]
content: "<chat message text>"
```

## Architecture

```mermaid
graph TD
    A[stream.html] --> B[init-ndk.mjs - auth + NDK]
    A --> C[post-composer.mjs - chat input]
    A --> D[post-interactions.mjs - render chat messages]
    A --> E[Video Player - HLS embed]
    
    subgraph Nostr Events
        F[kind:30311 - Live Event - publish/subscribe]
        G[kind:1311 - Chat Messages - publish/subscribe]
    end
    
    A --> F
    A --> G
    C -->|onSubmit publishes kind:1311| G
    D -->|renders incoming kind:1311| G
```

## CSS Strategy — Reuse client.css

The existing [`client.css`](www/css/client.css:1) and [`post-composer.css`](www/css/post-composer.css:1) provide nearly everything needed. Here is the mapping:

### Existing classes to reuse (NO new CSS needed)

| Element | Existing class/ID | Source |
|---------|-------------------|--------|
| Body flex column layout | `#divBody` override pattern | [`client.css:356`](www/css/client.css:356) — override with `flex-direction: column !important` per rules |
| Buttons | `.btn` | [`client.css:591`](www/css/client.css:591) |
| Post cards for chat messages | `.divPostItem` | [`client.css:900`](www/css/client.css:900) |
| Author header in chat | `.divPostHeader`, `.divPostHeaderMain`, `.divPostProfileLink` | [`client.css:1360`](www/css/client.css:1360) |
| Avatar | `.divPostAvatar`, `.avatar--sm` | [`client.css:1424`](www/css/client.css:1424), [`client.css:1395`](www/css/client.css:1395) |
| Author name | `.divPostAuthorName` | [`client.css:1438`](www/css/client.css:1438) |
| Post content | `.divPostContent` | [`client.css:930`](www/css/client.css:930) |
| Timestamp | `.divPostTime` | [`client.css:973`](www/css/client.css:973) |
| Interaction bar | `.divPostInteractions`, `.interaction-item` | [`client.css:990`](www/css/client.css:990) |
| Comment threads | `.divCommentThread`, `.divCommentsList` | [`client.css:1148`](www/css/client.css:1148) |
| Hidden/visible | `.clsHidden`, `.clsVisible` | [`client.css:878`](www/css/client.css:878) |
| Nostr embeds | `.nostr-embed`, `.nostr-mention`, etc. | [`client.css:215`](www/css/client.css:215) |
| Composer wrapper | `.post-composer-wrapper`, `.post-composer-wrapper--inline` | [`post-composer.css:1`](www/css/post-composer.css:1) |
| Video in posts | `.divPostContent video` | [`client.css:951`](www/css/client.css:951) |

### Minimal page-specific `<style>` additions (inline in stream.html)

Only these styles need to be added in the page `<style>` block, following the [CSS placement rules](www/css/client.css:7):

```css
/* Override divBody to column layout per client.css rules */
#divBody {
  flex-direction: column !important;
  flex-wrap: nowrap !important;
  align-items: center !important;
  justify-content: flex-start !important;
  align-content: flex-start !important;
  gap: 10px;
  overflow-y: auto;
}

/* Width constraint for all main sections — same pattern as post.html */
#divStreamPlayer,
#divStreamInfo,
#divChatFeed,
#divChatComposer {
  width: 80%;
  min-width: 300px;
  max-width: 700px;
}

/* Video player: 16:9 responsive container */
#divStreamPlayer {
  position: relative;
  padding-top: 56.25%; /* 16:9 */
  background: #000;
  border-radius: 10px;
  overflow: hidden;
}

#divStreamPlayer video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Stream info card — uses CSS vars, no hardcoded colors */
#divStreamInfo {
  background: var(--card-background, var(--secondary-color));
  border: 1px solid var(--border-color);
  border-radius: 10px;
  padding: 12px;
}

/* Status badge */
.stream-status-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--border-radius);
  font-size: 80%;
  font-weight: bold;
  border: 1px solid var(--border-color);
}

.stream-status-badge.live {
  color: var(--accent-color);
  border-color: var(--accent-color);
}

/* Chat feed: scrollable, fixed height */
#divChatFeed {
  max-height: 400px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Compact chat message — lighter than full .divPostItem */
.chatMessage {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 5px 10px;
  border-bottom: 1px solid var(--border-color);
  font-size: 90%;
}

.chatMessage .divPostAuthorName {
  font-size: 12px;
}

/* Chat composer override for inline mode */
#divChatComposer .post-composer-wrapper {
  width: 100%;
}
```

These are all page-specific, use only CSS variables, and follow the [CSS placement rules](www/css/client.css:7) documented in client.css.

## Page Layout

```
+--------------------------------------------------+
| HEADER: STREAM                    [avatar]        |
+--------------------------------------------------+
|                                                    |
|  +--------------------------------------------+   |
|  |                                            |   |
|  |          VIDEO PLAYER AREA                 |   |
|  |     embed via streaming tag URL            |   |
|  |     or placeholder if no stream URL        |   |
|  |                                            |   |
|  +--------------------------------------------+   |
|                                                    |
|  +--------------------------------------------+   |
|  | STREAM INFO                                |   |
|  | Title / Summary / Status / Tags            |   |
|  | [Go Live] [End Stream] [Edit]              |   |
|  +--------------------------------------------+   |
|                                                    |
|  +--------------------------------------------+   |
|  | LIVE CHAT                                  |   |
|  | [chat message 1]                           |   |
|  | [chat message 2]                           |   |
|  | [chat message 3]                           |   |
|  | ...                                        |   |
|  +--------------------------------------------+   |
|                                                    |
|  +--------------------------------------------+   |
|  | CHAT COMPOSER (post-composer)               |   |
|  | [type message...] [Send]                   |   |
|  +--------------------------------------------+   |
|                                                    |
+--------------------------------------------------+
| FOOTER                                            |
+--------------------------------------------------+
```

## Implementation Steps

### 1. Update stream.html boilerplate
- Change title from TEMPLATE to STREAM
- Change header text to STREAM
- Add CSS stylesheets: `post-composer.css`
- Add viewport meta tag

### 2. Add page-specific CSS
- `#divBody` flex column layout, centered, scrollable
- Video player container: 16:9 aspect ratio, max-width 700px, responsive
- Stream info card: styled like profile card in post.html
- Chat feed: scrollable container, max-height, auto-scroll to bottom
- Chat messages: compact card style with avatar, name, timestamp, content
- Chat composer: fixed at bottom of chat area

### 3. Add HTML structure in divBody
- `#divStreamPlayer` — video element or placeholder
- `#divStreamInfo` — title, summary, status badge, tags, controls
- `#divStreamControls` — Go Live / End Stream / Edit buttons
- `#divChatFeed` — scrollable chat message list
- `#divChatComposer` — host element for mountComposer

### 4. JavaScript module imports
Add to existing imports:
- [`mountComposer`](www/js/post-composer.mjs:152) from post-composer.mjs
- [`renderPostItem`](www/js/post-interactions.mjs:803), [`renderAuthorHeader`](www/js/post-interactions.mjs:216), [`formatTimeAgo`](www/js/post-interactions.mjs:906) from post-interactions.mjs
- [`initPostCards`](www/js/post-interactions2.mjs:5) from post-interactions2.mjs

### 5. Subscribe to own kind:30311 live event
On auth:
```js
subscribe(
  { kinds: [30311], authors: [currentPubkey] },
  { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
```
Listen for `ndkEvent` and filter for kind 30311 to populate stream info.

### 6. Publish kind:30311 live event
When streamer clicks Go Live:
```js
publishEvent({
  kind: 30311,
  content: '',
  tags: [
    ['d', streamId],
    ['title', title],
    ['summary', summary],
    ['streaming', streamUrl],
    ['status', 'live'],
    ['starts', String(Math.floor(Date.now() / 1000))],
    ['t', ...hashtags],
    ['image', imageUrl]
  ]
});
```

When streamer clicks End Stream, republish with `status: ended` and `ends` timestamp.

### 7. Video player
- Use native `<video>` element for direct URLs
- For HLS streams (.m3u8), use hls.js CDN: `https://cdn.jsdelivr.net/npm/hls.js@latest`
- Detect URL type and initialize accordingly
- Show placeholder when no streaming URL is set

### 8. Subscribe to kind:1311 chat messages
Once the live event `a` tag is known:
```js
subscribe(
  { kinds: [1311], '#a': [`30311:${currentPubkey}:${dTag}`] },
  { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);
```
Render each incoming chat message in the chat feed using a simplified version of renderPostItem or a custom compact renderer.

### 9. Wire post-composer for chat
```js
mountComposer(divChatComposer, {
  currentPubkey,
  showUploadIcon: false,
  showPreview: false,
  onSubmit: async (content) => {
    await publishEvent({
      kind: 1311,
      content: content,
      tags: [
        ['a', `30311:${currentPubkey}:${dTag}`, '', 'root']
      ]
    });
  }
});
```

### 10. Chat message rendering
For each kind:1311 event received:
- Fetch/cache author profile (avatar, display name)
- Render compact chat bubble: `[avatar] [name] [time]: [message]`
- Auto-scroll chat feed to bottom on new messages
- Support NIP-27 nostr: mentions via hydrateNostrEntities

### 11. Stream controls
- **Go Live button**: Prompts for title, streaming URL, optional summary/image/tags. Publishes kind:30311 with status=live.
- **End Stream button**: Updates kind:30311 with status=ended, adds ends timestamp.
- **Edit button**: Allows updating title, summary, image, tags while live.
- Status badge: Shows planned/live/ended with color coding.

### 12. URL parameter support
- `?naddr=...` or `?a=30311:pubkey:dtag` — View someone else's stream
- When viewing another stream, hide stream controls, show chat in read+write mode
- Parse naddr using NostrTools.nip19.decode

## Files Modified

| File | Changes |
|------|---------|
| [`www/stream.html`](www/stream.html:1) | Full page implementation: HTML structure, CSS, JS module |

## No New JS Modules Required
All functionality is achieved by reusing:
- [`post-composer.mjs`](www/js/post-composer.mjs:1) — chat input
- [`post-interactions.mjs`](www/js/post-interactions.mjs:1) — message rendering, entity hydration
- [`init-ndk.mjs`](www/js/init-ndk.mjs) — auth, subscribe, publishEvent
- Existing template boilerplate (sidenav, footer, relay UI, etc.)

## Dependencies
- **hls.js** (CDN) — for HLS stream playback, loaded only when needed
