16 KiB
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:
- Imports and calls
mountMessagingWindow()✓ - Gets the composer via
messagingUi.getComposer()✓ - Links both
messaging-ui.cssandpost-composer.css✓ - Uses
messagingUi.setHeader()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:formatTime()— same as messaging-ui.mjs:9escapeHtml()— same as messaging-ui.mjs:3CASHU_TOKEN_REGEX,isLikelyCashuToken(),findCashuTokens(),renderCashuQrCode()— identical to messaging-ui.mjs:33–95syncMessageCashuBlock()— inline cashu rendering using.msgBubbleCashu*classesrenderMessageContent()— near-identical todefaultRenderMessageContent()
- Bypasses
setMessages()entirely —renderThreadInner()manually builds DOM using.msgBubbleRow,.msgBubble,.msgBubbleContentelements (lines 1953–1988) instead of callingmessagingUi.setMessages() - Custom features not in the component:
- Per-bubble dot menu via
mountBubbleDotMenu()— copy, view-as-markdown/raw/JSON - View mode toggling via
renderBubbleBody()withgetMessageViewMode() - Lazy decryption with in-place DOM updates (lines 2005–2055)
- Progress banner for decryption status
- Per-bubble dot menu via
ai.html — AI Chat
What it does right:
- Imports and calls
mountMessagingWindow()✓ - Uses
aiThreadUi.setMessages()correctly to render conversations ✓ - Uses
aiThreadUi.setHeader()for header updates ✓
What it does wrong:
- Missing
post-composer.css— only linksmessaging-ui.css, notpost-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' - Uses its own custom textarea —
<textarea id="taAiPrompt">with custom buttons instead of the composer - Custom attachment system —
pendingImageAttachments,appendPendingImageFiles(),renderPendingAttachments()duplicate whatpost-composer.mjsprovides 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 }andonSubmit: 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.renderBubbleBodyis 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 |
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()— use from component or exportescapeHtml()— use from component or exportCASHU_TOKEN_REGEX,isLikelyCashuToken(),findCashuTokens(),renderCashuQrCode()— import from messaging-ui.mjssyncMessageCashuBlock()— no longer needed; component handles cashu renderingrenderMessageContent()— pass as option to mountMessagingWindow, or use default
2C. Refactor renderThreadInner to use setMessages
The core change: renderThreadInner() currently builds DOM manually (lines 1953–1988). Refactor to:
- Map
visibleMessagesto the message shape expected bymessagingUi.setMessages() - Call
messagingUi.setMessages(mappedMessages) - Use
onBubbleRenderedhook to mount dot menus - Use
renderBubbleBodyhook 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>:
<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 |
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:
- Hides the reply box from messaging-ui
- Uses
<textarea id="taAiPrompt">with<button id="btnAiSend">and<button id="btnAiAttach"> - Has custom drag-drop, paste, and attachment handling
Refactor to:
- Stop hiding the reply box — remove
aiThreadEls.replyBoxEl.style.display = 'none' - 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 } - Wire onSubmit to call
sendPrompt():onSubmit: async (text) => { return await sendPrompt(text); } - Remove the custom input area HTML — remove
#divAiInputArea,#taAiPrompt,#btnAiSend,#btnAiAttach,#inpAiImageUpload,#divAiPendingAttachments - Remove custom event handlers — remove the
btnAiSend.addEventListener,taAiPrompt.addEventListener, drag-drop handlers ondivAiInputArea, paste handler - 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
attachAsDataUrlmode 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 |