Files
client/plans/messaging-ui-unification.md
2026-04-17 16:52:51 -04:00

16 KiB
Raw Permalink Blame History

Messaging UI & Composer Unification Plan

Problem Statement

Both www/msg.html and www/ai.html import mountMessagingWindow() 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:

What it does wrong:

ai.html — AI Chat

What it does right:

What it does wrong:

  • Missing post-composer.css — only links messaging-ui.css, not post-composer.css
  • ~620 lines of duplicate inline CSS (lines 30650) 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 boxaiThreadEls.replyBoxEl.style.display = 'none'
  • Uses its own custom textarea<textarea id="taAiPrompt"> with custom buttons instead of the composer
  • Custom attachment systempendingImageAttachments, appendPendingImageFiles(), renderPendingAttachments() duplicate what post-composer.mjs provides via drag-drop and file upload
  • Custom drag-drop handling on divAiInputArea (lines 27642791) duplicates composer's built-in drag-drop
  • Custom paste handling for images (lines 27932810) 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():

New options:

{
  // 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():

  • 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(), renderCashuQrCode(), isLikelyCashuToken() are private. Export them so msg.html can use them without duplicating:

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 with generic class names:

.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 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:

{
  // 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 that duplicate messaging-ui.css:

Inline class Component class Lines to remove
.msgBubbleRow .msg-bubble-row 321326
.msgBubbleRow.outgoing/incoming .msg-bubble-row.outgoing/incoming 334364
.msgBubble, .msg-bubble .msg-bubble 366381
.msgBubbleContent + children .msg-bubble-content + children 383461
.msgBubble.outgoing/incoming .msg-bubble.outgoing/incoming 463469
.msgBubbleTime .msg-bubble-time 471476
.msgBubbleMeta .msg-bubble-meta 478483
.msgBubbleCashu* .msg-bubble-cashu* 485517
#divReplyBox .msg-reply-box 519523
#divReplyInput .msg-reply-input 525547
.msgEmptyState .msg-empty-state 549554
#divThreadPane .msg-thread-pane 143153
#divThreadHeader .msg-thread-header 155163
.msgThreadHeaderRow .msg-thread-header-row 165171
.msgThreadHeaderAvatar .msg-thread-header-avatar 173187
.msgThreadHeaderName .msg-thread-header-name 282289
#divThreadMessages .msg-thread-messages 291299

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:

2C. Refactor renderThreadInner to use setMessages

The core change: renderThreadInner() currently builds DOM manually (lines 19531988). 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 20052055). 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>:

<link rel="stylesheet" href="css/post-composer.css" />

3B. Remove duplicate inline CSS

Remove these CSS blocks from www/ai.html that duplicate component styles:

Inline class Component class Lines to remove
.aiMessageRow + variants .msg-bubble-row + variants 300312
.aiMessageBubble + variants .msg-bubble + variants 314335
.aiMessageContent + children .msg-bubble-content + children 337373
.aiMessageMeta .msg-bubble-meta 375380
.aiMessageAttachments .msg-bubble-attachments 498503
.aiMessageAttachmentImg .msg-bubble-attachment-img 505511
.aiTyping* + @keyframes .msg-typing* (new) 382416

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:
    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():
    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() 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 callback, update class names:

// Before
'<span class="aiTyping"><span class="aiTypingDot">...'
// After  
'<span class="msg-typing"><span class="msg-typing-dot">...'

Execution Order

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 Add hooks, exports, updateMessage API
www/css/messaging-ui.css Add typing indicator styles, dot-menu host styles
www/msg.html Remove ~300 lines CSS, ~170 lines JS, refactor renderThreadInner
www/ai.html Remove ~200 lines CSS, ~150 lines JS, remove custom input area, use composer