# Messaging UI Component Plan

## Overview

Extract the thread pane from `www/msg.html` into a reusable `mountMessagingWindow()` component in `www/js/messaging-ui.mjs`, with companion styles in `www/css/messaging-ui.css`. The component follows the same mount-and-return-API pattern as `www/js/post-composer.mjs`.

Additionally, enhance `www/js/post-composer.mjs` with two UX improvements:
1. **Instant clear on send** — clear text + preview immediately, before the async callback resolves.
2. **Input history** — arrow-up/down recalls previous sent messages, like a terminal.

---

## Architecture

```mermaid
graph TD
    A[Host Page] -->|calls| B[mountMessagingWindow hostEl, options]
    B --> C[Thread Header]
    B --> D[Message List - scrollable]
    B --> E[mountComposer - input area]
    E -->|onSubmit| F[Host callback]
    F -->|appendMessage| B
    D -->|scroll top| G[onLoadOlder callback]
```

### Component: `mountMessagingWindow(hostEl, options)`

Creates the full thread pane UI inside `hostEl`. Returns a control API.

#### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `onSubmit` | `async fn(text) => bool` | required | Called when user sends. Return true = success. |
| `onLoadOlder` | `async fn() => void` | null | Called when user scrolls to top. |
| `renderMessageContent` | `fn(rawText) => html` | built-in markdown | Custom renderer for message body. |
| `composerOptions` | `object` | `{}` | Extra options forwarded to `mountComposer`. |
| `scrollThreshold` | `number` | 100 | Pixels from bottom to auto-scroll. |
| `loadOlderThreshold` | `number` | 50 | Pixels from top to trigger load-older. |
| `emptyStateText` | `string` | "No messages yet" | Shown when message list is empty. |

#### Returned API

```js
{
  setHeader({ name, avatarUrl }),     // Update thread header
  setMessages(messages),              // Replace all messages, re-render
  appendMessage(message),             // Add one message, auto-scroll
  prependMessages(messages),          // Prepend older messages, preserve scroll
  scrollToBottom(force),              // Scroll to bottom
  setDisabled(bool),                  // Disable/enable input
  getComposer(),                      // Access the inner post-composer API
  destroy()                           // Tear down all DOM and listeners
}
```

#### Message Shape

```js
{
  id: string,
  content: string,
  outgoing: boolean,          // true = sent by user, false = received
  created_at: number,         // unix seconds
  protocol?: string,          // optional label e.g. "kind4-nip04"
  role?: string,              // optional, for AI: "user" | "assistant" | "system"
  attachments?: Array         // optional image attachments
}
```

### DOM Structure Created

```
hostEl
  div.msg-thread-header
    img.msg-thread-header-avatar
    div.msg-thread-header-name
  div.msg-thread-messages          (scrollable)
    div.msg-bubble-row.outgoing|incoming
      div.msg-bubble.outgoing|incoming
        div.msg-bubble-content     (rendered markdown)
        div.msg-bubble-time
        div.msg-bubble-meta        (optional protocol label)
  div.msg-reply-box
    [mountComposer creates its own DOM here]
```

---

## Post-Composer Enhancements

### 1. Instant Clear on Send

**Problem:** In `onSendClick()`, the composer waits for `await onSubmit(text)` before clearing. For msg.html this means the text lingers while encryption + publish completes.

**Solution:** Add a `clearBeforeSubmit` option (default `true`). When enabled:
1. Capture the text
2. Immediately clear the composer and hide preview
3. Call `onSubmit(text)` asynchronously
4. If `onSubmit` throws or returns false, restore the text

```js
// In onSendClick():
const text = (hostEl.innerText || '').trim();
if (!text) return;

if (clearBeforeSubmit) {
  clearComposerContent();           // instant visual feedback
  pushToHistory(text);              // save to history ring
}

try {
  const result = await onSubmit(text);
  if (result === false && clearBeforeSubmit) {
    // Restore on failure
    hostEl.innerText = text;
    queuePreviewRender();
    updateSendButtonVisibility();
  }
} catch (error) {
  if (clearBeforeSubmit) {
    hostEl.innerText = text;
    queuePreviewRender();
    updateSendButtonVisibility();
  }
  console.error('[post-composer] Submit failed:', error);
}
```

### 2. Input History (Arrow Up/Down)

**Problem:** No way to recall previously sent messages.

**Solution:** Maintain a localStorage-backed ring buffer of sent messages. Arrow-up/down navigates when the cursor is at the start/end of the input.

**Storage key:** `post_composer_history_v1`
**Max entries:** 50 (configurable via `historyMaxEntries` option)

```js
// New option:
//   enableHistory: true (default true)
//   historyKey: string (default 'post_composer_history_v1')
//   historyMaxEntries: number (default 50)

// State:
let history = loadHistory();    // string[]
let historyIndex = -1;          // -1 = not browsing, 0 = most recent
let historyDraft = '';          // saves current unsent text

// On successful send:
function pushToHistory(text) {
  if (!text.trim()) return;
  // Remove duplicate if exists
  history = history.filter(h => h !== text);
  history.unshift(text);
  if (history.length > historyMaxEntries) history.pop();
  saveHistory();
  historyIndex = -1;
  historyDraft = '';
}

// On ArrowUp in keydown handler:
if (event.key === 'ArrowUp' && enableHistory && !mentionState.visible) {
  const atStart = getCaretCharacterOffsetWithin(hostEl) === 0;
  const singleLine = !(hostEl.innerText || '').includes('\n');
  if (atStart || singleLine) {
    event.preventDefault();
    if (historyIndex === -1) {
      historyDraft = hostEl.innerText || '';
    }
    if (historyIndex < history.length - 1) {
      historyIndex++;
      hostEl.innerText = history[historyIndex];
      // Move caret to end
      setCaretByCharacterOffset(hostEl, (hostEl.innerText || '').length);
      queuePreviewRender();
      updateSendButtonVisibility();
    }
    return;
  }
}

// On ArrowDown:
if (event.key === 'ArrowDown' && enableHistory && !mentionState.visible) {
  const text = hostEl.innerText || '';
  const atEnd = getCaretCharacterOffsetWithin(hostEl) >= text.length;
  const singleLine = !text.includes('\n');
  if ((atEnd || singleLine) && historyIndex >= 0) {
    event.preventDefault();
    historyIndex--;
    if (historyIndex < 0) {
      hostEl.innerText = historyDraft;
    } else {
      hostEl.innerText = history[historyIndex];
    }
    setCaretByCharacterOffset(hostEl, (hostEl.innerText || '').length);
    queuePreviewRender();
    updateSendButtonVisibility();
    return;
  }
}
```

---

## CSS: `www/css/messaging-ui.css`

Extract and namespace the thread pane styles from msg.html. Use `.msg-` prefix for all classes. The styles will be a cleaned-up version of the inline styles currently in msg.html lines 43-471.

Key classes:
- `.msg-thread-pane` — the outer container
- `.msg-thread-header` — header bar
- `.msg-thread-messages` — scrollable message area
- `.msg-bubble-row`, `.msg-bubble` — message bubbles
- `.msg-bubble-content` — markdown-rendered content
- `.msg-reply-box` — input area wrapper
- `.msg-empty-state` — empty state text

---

## Migration Plan

### Phase 1: Post-Composer Enhancements

1. Add `clearBeforeSubmit` option to `mountComposer` in `www/js/post-composer.mjs`
2. Add input history ring buffer to `mountComposer`
3. Update `www/css/post-composer.css` if needed for any new elements
4. Test in msg.html — send should now clear instantly, arrow-up should recall

### Phase 2: Create Messaging UI Component

5. Create `www/css/messaging-ui.css` with extracted/namespaced styles
6. Create `www/js/messaging-ui.mjs` with `mountMessagingWindow()`
7. The component internally uses `mountComposer` for the input area

### Phase 3: Refactor msg.html

8. Replace inline `<style>` block with `<link>` to `messaging-ui.css`
9. Replace manual DOM construction with `mountMessagingWindow()` call
10. Keep msg.html-specific logic (Nostr DM encryption, conversation list, send modes) in the page script
11. The page provides `onSubmit` and `onLoadOlder` callbacks to the component

### Phase 4: Integrate into ai.html

12. Add `<link>` to `messaging-ui.css` in ai.html
13. Replace the `divAiChatPane` message area with `mountMessagingWindow()`
14. Keep AI-specific logic (provider selection, streaming, conversation persistence) in the page script
15. Map AI message roles to the component message shape

### Phase 5: Future Pages

16. Any page needing a chat/thread UI can import and mount the component
17. Candidates: `html-tv.html`, `strudel.html`, `didactyl.html`

---

## File Changes Summary

| File | Action |
|------|--------|
| `www/js/post-composer.mjs` | Add `clearBeforeSubmit`, input history |
| `www/css/post-composer.css` | Minor updates if needed |
| `www/js/messaging-ui.mjs` | **New** — `mountMessagingWindow()` |
| `www/css/messaging-ui.css` | **New** — extracted thread pane styles |
| `www/msg.html` | Refactor to use component, remove inline styles |
| `www/ai.html` | Refactor chat pane to use component |

---

## Interaction Flow

```mermaid
sequenceDiagram
    participant User
    participant Composer as post-composer
    participant MsgUI as messaging-ui
    participant Page as Host Page

    User->>Composer: Types message, hits Send
    Composer->>Composer: Save text to history ring
    Composer->>Composer: Clear input + preview immediately
    Composer->>Page: onSubmit text
    Page->>Page: Encrypt / API call async
    Page->>MsgUI: appendMessage sent msg
    MsgUI->>MsgUI: Render bubble, auto-scroll

    Note over Composer: If onSubmit fails...
    Page-->>Composer: throws or returns false
    Composer->>Composer: Restore saved text

    User->>Composer: Presses Arrow Up
    Composer->>Composer: Load previous from history
    Composer->>Composer: Display in input
```

```mermaid
sequenceDiagram
    participant User
    participant MsgUI as messaging-ui
    participant Page as Host Page

    User->>MsgUI: Scrolls to top
    MsgUI->>Page: onLoadOlder callback
    Page->>Page: Fetch older messages
    Page->>MsgUI: prependMessages older
    MsgUI->>MsgUI: Render, preserve scroll position
```
