# Messaging UI & Composer Unification Plan

## Problem Statement

Both [`www/msg.html`](www/msg.html) and [`www/ai.html`](www/ai.html) import [`mountMessagingWindow()`](www/js/messaging-ui.mjs:143) from the shared component, but **neither page fully delegates to it**. Each page has hundreds of lines of duplicate inline CSS and JS that reimplement what the shared components already provide.

---

## Current State Analysis

### msg.html — Nostr DMs

**What it does right:**
- Imports and calls [`mountMessagingWindow()`](www/msg.html:2392) ✓
- Gets the composer via [`messagingUi.getComposer()`](www/msg.html:2415) ✓
- Links both [`messaging-ui.css`](www/css/messaging-ui.css) and [`post-composer.css`](www/css/post-composer.css) ✓
- Uses [`messagingUi.setHeader()`](www/msg.html:1889) for header updates ✓

**What it does wrong:**
- **~550 lines of duplicate inline CSS** (lines 34–583) that redefine bubble styles using `.msgBubble*` class names that mirror the component's `.msg-bubble*` classes
- **Duplicate JS utility functions** that are identical to [`messaging-ui.mjs`](www/js/messaging-ui.mjs):
  - [`formatTime()`](www/msg.html:1107) — same as [messaging-ui.mjs:9](www/js/messaging-ui.mjs:9)
  - [`escapeHtml()`](www/msg.html:1124) — same as [messaging-ui.mjs:3](www/js/messaging-ui.mjs:3)
  - [`CASHU_TOKEN_REGEX`](www/msg.html:1130), [`isLikelyCashuToken()`](www/msg.html:1133), [`findCashuTokens()`](www/msg.html:1144), [`renderCashuQrCode()`](www/msg.html:1156) — identical to [messaging-ui.mjs:33–95](www/js/messaging-ui.mjs:33)
  - [`syncMessageCashuBlock()`](www/msg.html:1188) — inline cashu rendering using `.msgBubbleCashu*` classes
  - [`renderMessageContent()`](www/msg.html:1259) — near-identical to [`defaultRenderMessageContent()`](www/js/messaging-ui.mjs:97)
- **Bypasses `setMessages()` entirely** — [`renderThreadInner()`](www/msg.html:1866) manually builds DOM using `.msgBubbleRow`, `.msgBubble`, `.msgBubbleContent` elements (lines 1953–1988) instead of calling `messagingUi.setMessages()`
- **Custom features not in the component:**
  - Per-bubble dot menu via [`mountBubbleDotMenu()`](www/msg.html:1323) — copy, view-as-markdown/raw/JSON
  - View mode toggling via [`renderBubbleBody()`](www/msg.html:1298) with [`getMessageViewMode()`](www/msg.html:1220)
  - Lazy decryption with in-place DOM updates (lines 2005–2055)
  - Progress banner for decryption status

### ai.html — AI Chat

**What it does right:**
- Imports and calls [`mountMessagingWindow()`](www/ai.html:3119) ✓
- Uses [`aiThreadUi.setMessages()`](www/ai.html:1659) correctly to render conversations ✓
- Uses [`aiThreadUi.setHeader()`](www/ai.html:1636) for header updates ✓

**What it does wrong:**
- **Missing `post-composer.css`** — only links `messaging-ui.css`, not `post-composer.css`
- **~620 lines of duplicate inline CSS** (lines 30–650) including:
  - `.aiMessageRow`, `.aiMessageBubble`, `.aiMessageContent`, `.aiMessageMeta` — duplicate `.msg-bubble-row`, `.msg-bubble`, `.msg-bubble-content`, `.msg-bubble-meta`
  - `.aiMessageAttachments`, `.aiMessageAttachmentImg` — duplicate `.msg-bubble-attachments`, `.msg-bubble-attachment-img`
- **Hides the messaging-ui reply box** — [`aiThreadEls.replyBoxEl.style.display = 'none'`](www/ai.html:3139)
- **Uses its own custom textarea** — [`<textarea id="taAiPrompt">`](www/ai.html:697) with custom buttons instead of the composer
- **Custom attachment system** — [`pendingImageAttachments`](www/ai.html:938), [`appendPendingImageFiles()`](www/ai.html:1156), [`renderPendingAttachments()`](www/ai.html:1110) duplicate what `post-composer.mjs` provides via drag-drop and file upload
- **Custom drag-drop handling** on `divAiInputArea` (lines 2764–2791) duplicates composer's built-in drag-drop
- **Custom paste handling** for images (lines 2793–2810) duplicates composer's built-in paste
- **Disables the component's composer entirely** — passes `{ disabled: true, showUploadIcon: false, showPreview: false, alwaysShowSendButton: false }` and `onSubmit: async () => false`

---

## Refactoring Strategy

The refactoring has two phases: first extend the shared components to support the features that msg.html and ai.html need, then migrate both pages to use those extended components.

### Phase 1: Extend Shared Components

#### 1A. Add per-bubble customization hooks to messaging-ui.mjs

msg.html needs per-bubble dot menus and view-mode toggling. Rather than keeping that logic inline, add hooks to [`mountMessagingWindow()`](www/js/messaging-ui.mjs:143):

**New options:**
```js
{
  // Called after each bubble is rendered, receives the row element and message data
  onBubbleRendered: (rowEl, bubbleEl, contentEl, msg, index) => void,
  
  // Called to render the bubble body content - allows custom rendering per message
  // If not provided, uses renderMessageContent as before
  renderBubbleBody: (contentEl, msg, index) => string,
}
```

**Changes to [`renderMessages()`](www/js/messaging-ui.mjs:208):**
- After creating each bubble, call `options.onBubbleRendered()` if provided
- If `options.renderBubbleBody` is provided, use it instead of the default content rendering

This lets msg.html:
- Mount its dot menu via `onBubbleRendered`
- Control view-mode rendering via `renderBubbleBody`

#### 1B. Export cashu utilities from messaging-ui.mjs

Currently [`findCashuTokens()`](www/js/messaging-ui.mjs:49), [`renderCashuQrCode()`](www/js/messaging-ui.mjs:62), [`isLikelyCashuToken()`](www/js/messaging-ui.mjs:36) are private. Export them so msg.html can use them without duplicating:

```js
export { findCashuTokens, renderCashuQrCode, isLikelyCashuToken };
```

#### 1C. Add typing indicator support to messaging-ui.css

Move the `.aiTyping` / `.aiTypingDot` / `@keyframes aiPulse` CSS from ai.html inline styles into [`messaging-ui.css`](www/css/messaging-ui.css) with generic class names:

```css
.msg-typing { ... }
.msg-typing-dot { ... }
@keyframes msgTypingPulse { ... }
```

#### 1D. Support image attachments in the composer for AI use case

ai.html currently uses a custom `pendingImageAttachments` array with base64 data URLs. The [`post-composer.mjs`](www/js/post-composer.mjs) uploads files to blossom servers and inserts URLs. For AI chat, we need an option to handle attachments as data URLs instead of uploading:

**New composer option:**
```js
{
  // When true, files are converted to data URLs and stored in an internal array
  // instead of being uploaded to blossom servers
  attachAsDataUrl: true,
  
  // Called when attachments change
  onAttachmentsChange: (attachments) => void,
  
  // Get current pending attachments
  // Added to returned API: getAttachments(), clearAttachments()
}
```

Alternatively, ai.html can keep its own attachment handling outside the composer if the data-URL workflow is too different from the upload workflow. The key change is to use the composer for text input and send button.

### Phase 2: Migrate msg.html

#### 2A. Remove duplicate inline CSS

Remove these CSS blocks from [`www/msg.html`](www/msg.html:34) that duplicate [`messaging-ui.css`](www/css/messaging-ui.css):

| Inline class | Component class | Lines to remove |
|---|---|---|
| `.msgBubbleRow` | `.msg-bubble-row` | 321–326 |
| `.msgBubbleRow.outgoing/incoming` | `.msg-bubble-row.outgoing/incoming` | 334–364 |
| `.msgBubble, .msg-bubble` | `.msg-bubble` | 366–381 |
| `.msgBubbleContent` + children | `.msg-bubble-content` + children | 383–461 |
| `.msgBubble.outgoing/incoming` | `.msg-bubble.outgoing/incoming` | 463–469 |
| `.msgBubbleTime` | `.msg-bubble-time` | 471–476 |
| `.msgBubbleMeta` | `.msg-bubble-meta` | 478–483 |
| `.msgBubbleCashu*` | `.msg-bubble-cashu*` | 485–517 |
| `#divReplyBox` | `.msg-reply-box` | 519–523 |
| `#divReplyInput` | `.msg-reply-input` | 525–547 |
| `.msgEmptyState` | `.msg-empty-state` | 549–554 |
| `#divThreadPane` | `.msg-thread-pane` | 143–153 |
| `#divThreadHeader` | `.msg-thread-header` | 155–163 |
| `.msgThreadHeaderRow` | `.msg-thread-header-row` | 165–171 |
| `.msgThreadHeaderAvatar` | `.msg-thread-header-avatar` | 173–187 |
| `.msgThreadHeaderName` | `.msg-thread-header-name` | 282–289 |
| `#divThreadMessages` | `.msg-thread-messages` | 291–299 |

**Keep** the layout CSS that is page-specific: `#divBody`, `#divMsgLayout`, `#divThreadPaneHost`, `#divConversationsPane`, conversation list styles, conversation starter styles, send mode styles, and the dot-menu host positioning styles.

#### 2B. Remove duplicate inline JS functions

Remove these functions from msg.html that are now available from messaging-ui.mjs:

- [`formatTime()`](www/msg.html:1107) — use from component or export
- [`escapeHtml()`](www/msg.html:1124) — use from component or export  
- [`CASHU_TOKEN_REGEX`](www/msg.html:1130), [`isLikelyCashuToken()`](www/msg.html:1133), [`findCashuTokens()`](www/msg.html:1144), [`renderCashuQrCode()`](www/msg.html:1156) — import from messaging-ui.mjs
- [`syncMessageCashuBlock()`](www/msg.html:1188) — no longer needed; component handles cashu rendering
- [`renderMessageContent()`](www/msg.html:1259) — pass as option to mountMessagingWindow, or use default

#### 2C. Refactor renderThreadInner to use setMessages

The core change: [`renderThreadInner()`](www/msg.html:1866) currently builds DOM manually (lines 1953–1988). Refactor to:

1. Map `visibleMessages` to the message shape expected by `messagingUi.setMessages()`
2. Call `messagingUi.setMessages(mappedMessages)` 
3. Use `onBubbleRendered` hook to mount dot menus
4. Use `renderBubbleBody` hook for view-mode toggling

**Challenge: Lazy decryption** — msg.html does in-place DOM updates after lazy decryption (lines 2005–2055). Options:
- **Option A**: After each lazy decrypt batch, call `setMessages()` again with updated data. The component re-renders all messages. Simple but may cause scroll jumps.
- **Option B**: Add a `updateMessage(id, patch)` method to the component API that updates a single bubble in-place without re-rendering everything.
- **Recommended: Option B** — add `updateMessage(id, patch)` to messaging-ui.mjs that finds the existing bubble by message ID and re-renders just that bubble's content.

### Phase 3: Migrate ai.html

#### 3A. Add post-composer.css link

Add to `<head>`:
```html
<link rel="stylesheet" href="css/post-composer.css" />
```

#### 3B. Remove duplicate inline CSS

Remove these CSS blocks from [`www/ai.html`](www/ai.html:30) that duplicate component styles:

| Inline class | Component class | Lines to remove |
|---|---|---|
| `.aiMessageRow` + variants | `.msg-bubble-row` + variants | 300–312 |
| `.aiMessageBubble` + variants | `.msg-bubble` + variants | 314–335 |
| `.aiMessageContent` + children | `.msg-bubble-content` + children | 337–373 |
| `.aiMessageMeta` | `.msg-bubble-meta` | 375–380 |
| `.aiMessageAttachments` | `.msg-bubble-attachments` | 498–503 |
| `.aiMessageAttachmentImg` | `.msg-bubble-attachment-img` | 505–511 |
| `.aiTyping*` + `@keyframes` | `.msg-typing*` (new) | 382–416 |

**Keep** the layout CSS that is page-specific: `#divBody`, `#divAiLayout`, `#divAiConversationsPane`, conversation list styles, `#divAiChatPane`, `#divAiChatHeader`, dropdown styles, config panel styles, payment panel styles, input area styles (until composer migration), system prompt styles.

#### 3C. Use the messaging-ui composer instead of custom textarea

This is the biggest change for ai.html. Currently it:
1. Hides the reply box from messaging-ui
2. Uses `<textarea id="taAiPrompt">` with `<button id="btnAiSend">` and `<button id="btnAiAttach">`
3. Has custom drag-drop, paste, and attachment handling

**Refactor to:**
1. **Stop hiding the reply box** — remove `aiThreadEls.replyBoxEl.style.display = 'none'`
2. **Enable the composer** — change composerOptions to:
   ```js
   composerOptions: {
     disabled: false,
     showUploadIcon: true,  // for image attachments
     showPreview: false,     // AI chat doesn't need preview
     submitOnEnter: true,    // Enter sends in chat
     alwaysShowSendButton: true
   }
   ```
3. **Wire onSubmit** to call `sendPrompt()`:
   ```js
   onSubmit: async (text) => {
     return await sendPrompt(text);
   }
   ```
4. **Remove the custom input area HTML** — remove `#divAiInputArea`, `#taAiPrompt`, `#btnAiSend`, `#btnAiAttach`, `#inpAiImageUpload`, `#divAiPendingAttachments`
5. **Remove custom event handlers** — remove the `btnAiSend.addEventListener`, `taAiPrompt.addEventListener`, drag-drop handlers on `divAiInputArea`, paste handler
6. **Remove custom attachment code** — remove `pendingImageAttachments`, `appendPendingImageFiles()`, `renderPendingAttachments()`, and related CSS

**Image attachment challenge:** The AI chat sends images as base64 data URLs in the API request, while the composer uploads to blossom and inserts URLs. Two approaches:
- **Approach A (simpler)**: Keep a minimal custom attachment UI outside the composer for image attachments only. The composer handles text + send, but image attachment remains separate.
- **Approach B (full integration)**: Add `attachAsDataUrl` mode to post-composer.mjs. When enabled, dropped/pasted/uploaded images are converted to data URLs and stored internally rather than uploaded.
- **Recommended: Approach A** for initial migration — use the composer for text input and send, keep image attachment as a small separate concern. This minimizes changes to post-composer.mjs.

#### 3D. Update renderCurrentConversation

[`renderCurrentConversation()`](www/ai.html:1626) already correctly maps messages to the component format and calls `aiThreadUi.setMessages()`. The typing indicator rendering via `__AI_TYPING__` sentinel and custom `renderMessageContent` is clean. No changes needed here.

#### 3E. Update typing indicator class names

In the [`renderMessageContent`](www/ai.html:3122) callback, update class names:
```js
// Before
'<span class="aiTyping"><span class="aiTypingDot">...'
// After  
'<span class="msg-typing"><span class="msg-typing-dot">...'
```

---

## Execution Order

```mermaid
graph TD
    A[1A: Add onBubbleRendered + renderBubbleBody hooks to messaging-ui.mjs] --> D
    B[1B: Export cashu utilities from messaging-ui.mjs] --> D
    C[1C: Add typing indicator CSS to messaging-ui.css] --> F
    D[2A: Remove duplicate CSS from msg.html] --> E
    E[2B: Remove duplicate JS from msg.html] --> G
    G[2C: Refactor renderThreadInner to use setMessages] --> H
    F[3A: Add post-composer.css link to ai.html] --> I
    I[3B: Remove duplicate CSS from ai.html] --> J
    J[3C: Use messaging-ui composer in ai.html] --> K
    K[3D: Update typing indicator classes] --> H
    H[Test both pages]
```

## Risk Assessment

| Risk | Impact | Mitigation |
|---|---|---|
| msg.html lazy decryption breaks with setMessages re-render | High — messages flash/scroll jumps | Add updateMessage API to component |
| ai.html image attachments incompatible with composer upload flow | Medium — images stop working | Keep minimal separate attachment UI initially |
| CSS specificity conflicts between inline overrides and component styles | Low — visual glitches | Remove inline styles incrementally, test each removal |
| msg.html dot menu positioning changes | Low — menu appears in wrong spot | Use onBubbleRendered hook with same positioning logic |

## Files Modified

| File | Change Type |
|---|---|
| [`www/js/messaging-ui.mjs`](www/js/messaging-ui.mjs) | Add hooks, exports, updateMessage API |
| [`www/css/messaging-ui.css`](www/css/messaging-ui.css) | Add typing indicator styles, dot-menu host styles |
| [`www/msg.html`](www/msg.html) | Remove ~300 lines CSS, ~170 lines JS, refactor renderThreadInner |
| [`www/ai.html`](www/ai.html) | Remove ~200 lines CSS, ~150 lines JS, remove custom input area, use composer |
