# Post Composer Component — Plan
## Overview
Create a **reusable post-composer module** (`www/js/post-composer.mjs`) that encapsulates:
1. **File upload** — drag-and-drop onto the compose area + click-to-upload icon
2. **Live preview** — renders the post content as it will appear (images, `nostr:` entities, links)
3. **@ mention autocomplete** — type `@name` to search followed users and insert their `nostr:npub1…` reference
4. **Reusability** — the same module can be mounted inside `post.html`, reply dialogs, or any future compose context
The composer uploads files to **all configured blossom servers** (via existing `uploadToAllServers`) but inserts only the **default server URL** into the post text (via existing `getDefaultBlossomServer` / `getBlobUrl`).
---
## Architecture
```mermaid
graph TD
A[post-composer.mjs] -->|imports| B[blossom-api.mjs]
A -->|imports| C[blossom-ui.mjs]
A -->|imports| D[utilities.mjs - htmlFormatText]
A -->|imports| E[post-interactions.mjs - hydrateNostrEntities]
F[post.html] -->|calls| A
G[future reply dialog] -->|calls| A
A -->|receives| H[contact list - followed pubkeys + profiles]
```
### Module API — `post-composer.mjs`
```js
/**
* Mount a post composer onto an existing contenteditable div.
*
* @param {HTMLElement} hostEl — the contenteditable element, e.g. divPost
* @param {Object} options
* @param {string} options.currentPubkey — logged-in user hex pubkey
* @param {Array} options.followedProfiles — [{pubkey, name, display_name, picture}, ...]
* @param {Function} options.onSubmit — called with final plain-text content string
* @param {Function} options.fetchCachedProfile — profile lookup fn
* @param {boolean} [options.showUploadIcon=true]
* @param {boolean} [options.showPreview=true]
* @returns {{ destroy: Function, refresh: Function }}
*/
export function mountComposer(hostEl, options) { ... }
```
The module does **not** own the `
` element — it wraps around it, adding:
- A **wrapper div** around the host for positioning context
- An **upload icon** (bottom-right of the host)
- A **preview div** (below the host, inside the same wrapper)
- An **@ mention dropdown** (positioned inline near the cursor)
- Drag-and-drop event listeners on the host
- A hidden `` for click-to-upload
---
## Detailed Design
### 1. File Upload
**Drag & Drop on divPost:**
- Listen for `dragover`, `dragleave`, `drop` on the host element
- On `drop`: extract files, show a visual uploading indicator, call `uploadToAllServers(file)` from `blossom-api.mjs`
- On success: construct the URL using `getBlobUrl(sha256, ext)` from `blossom-api.mjs` — this uses the **default** (first healthy upload) server
- Insert the URL at the cursor position in the contenteditable div
- For images: insert the raw URL on its own line (the preview will render it as ``)
- For other files: insert the URL as a link
**Upload Icon (bottom-right):**
- A small clickable icon (SVG paperclip or upload arrow) positioned absolutely in the bottom-right corner of the host element
- On click: trigger a hidden ``
- Same upload flow as drag-and-drop
**Upload Progress:**
- While uploading, show a small spinner or progress text overlay on the host element
- Disable the submit (Enter key) while upload is in progress
### 2. Live Preview
**Trigger:** The preview div appears once the user starts typing (first `input` event with non-empty content).
**Rendering:**
- On each `input` event (debounced ~300ms), take `hostEl.innerText`, run it through `htmlFormatText()` from `utilities.mjs`
- Set the preview div's `innerHTML` to the result
- Call `hydrateNostrEntities(previewEl)` from `post-interactions.mjs` to resolve embedded notes/profiles
- The preview div uses the same CSS classes as `.divPostContent` so it looks identical to rendered posts
**Preview div placement:**
- Sits below the compose area inside the same wrapper
- Has a subtle top border or label like "Preview" in muted text
- Hidden when compose area is empty
### 3. @ Mention Autocomplete
**Trigger:** When the user types `@` followed by at least 1 character, search the `followedProfiles` array.
**Search logic:**
- Filter profiles where `name`, `display_name`, or `nip05` contains the typed substring (case-insensitive)
- Show up to 8 results in a dropdown positioned near the cursor
**Dropdown UI:**
- Absolutely positioned dropdown with avatar thumbnail, display name, and @name
- Keyboard navigation: arrow keys to select, Enter/Tab to confirm, Escape to dismiss
- Click to select
**Insertion:**
- On selection, replace the `@query` text with `nostr:npub1…` (the bech32-encoded npub of the selected user)
- The preview will then render this as a clickable mention link via `htmlFormatText`
**Profile data source:**
- `post.html` already fetches the contact list (kind 3) and has `followedPubkeys`
- The composer receives pre-fetched profile data via `options.followedProfiles`
- Profiles are lazily enriched: if a followed pubkey has no cached profile yet, the dropdown shows the truncated npub
### 4. Integration with post.html
**Current flow in `post.html`:**
1. `divPost` is a `contenteditable` div
2. `handleKeyDown` listens for Enter (without Shift) to call `handlePost()`
3. `handlePost()` reads `divPost.innerText`, publishes kind 1 event
**New flow:**
1. After `divPost` is set up and `interactions` is initialized, call `mountComposer(divPost, { ... })`
2. The composer adds upload, preview, and mention features
3. `handlePost()` remains the same — it reads `divPost.innerText` which now may contain `nostr:npub1…` references and image URLs
4. The composer's `onSubmit` callback is optional — `post.html` can continue using its own Enter key handler
**DOM changes to post.html:**
- Wrap `divPost` in a new `div#divPostWrapper` (or the composer creates this wrapper dynamically)
- The preview div and upload icon are children of this wrapper
- Minimal CSS additions for the wrapper, upload icon positioning, preview styling, and mention dropdown
---
## File Changes Summary
| File | Change |
|------|--------|
| `www/js/post-composer.mjs` | **NEW** — the reusable composer module |
| `www/css/post-composer.css` | **NEW** — styles for upload icon, preview, mention dropdown, drag overlay |
| `www/post.html` | **MODIFY** — import and mount the composer; add CSS link; minor DOM adjustments |
| `www/js/utilities.mjs` | No changes needed — `htmlFormatText` already handles images and nostr entities |
| `www/js/blossom-api.mjs` | No changes needed — `uploadToAllServers`, `getBlobUrl`, `getDefaultBlossomServer` already exist |
| `www/js/post-interactions.mjs` | No changes needed — `hydrateNostrEntities` is already exported |
---
## Implementation Steps
### Step 1: Create `www/css/post-composer.css`
- `.post-composer-wrapper` — relative positioning container
- `.post-composer-upload-icon` — absolute bottom-right, clickable, hover state
- `.post-composer-drag-overlay` — full overlay on dragover with dashed border and upload icon
- `.post-composer-preview` — preview area below compose, uses `.divPostContent` base styles
- `.post-composer-preview-label` — small muted "Preview" label
- `.post-composer-mention-dropdown` — absolutely positioned autocomplete list
- `.post-composer-mention-item` — individual mention result with avatar, name
- `.post-composer-mention-item.active` — highlighted/selected item
- `.post-composer-uploading` — upload-in-progress indicator
### Step 2: Create `www/js/post-composer.mjs`
- Export `mountComposer(hostEl, options)` function
- Implement drag-and-drop upload with visual feedback
- Implement click-to-upload via hidden file input
- Implement debounced live preview using `htmlFormatText` + `hydrateNostrEntities`
- Implement @ mention detection, search, dropdown rendering, and npub insertion
- Return `{ destroy, refresh }` for cleanup and profile list updates
### Step 3: Integrate into `www/post.html`
- Add `` in ``
- Import `mountComposer` from `./js/post-composer.mjs`
- After `interactions` is initialized and contact list is loaded, build `followedProfiles` array
- Call `mountComposer(divPost, { currentPubkey, followedProfiles, fetchCachedProfile })`
- Update `followedProfiles` when new profiles arrive (via `composer.refresh({ followedProfiles })`)
### Step 4: Test all features
- Drag an image file onto divPost — verify upload to all blossom servers, URL inserted with default server
- Click upload icon — verify file picker opens, same upload flow
- Type text with an image URL — verify preview shows the image
- Type `nostr:npub1...` — verify preview shows mention link
- Type `@jack` — verify dropdown appears with matching follows, selection inserts npub
- Press Enter — verify post publishes with correct content including URLs and npub references
---
## Mermaid: Composer Interaction Flow
```mermaid
sequenceDiagram
participant U as User
participant C as Composer
participant B as Blossom API
participant P as Preview
U->>C: Drops image file on divPost
C->>C: Show drag overlay + uploading indicator
C->>B: uploadToAllServers - file
B-->>C: blobDescriptor + sha256
C->>C: getBlobUrl - sha256, ext - default server URL
C->>C: Insert URL at cursor in divPost
C->>P: Debounced preview update
P->>P: htmlFormatText renders img tag
U->>C: Types @jac
C->>C: Detect @ trigger, search followedProfiles
C->>C: Show mention dropdown with matches
U->>C: Selects jack from dropdown
C->>C: Replace @jac with nostr:npub1abc...
C->>P: Debounced preview update
P->>P: htmlFormatText renders mention link
U->>C: Presses Enter
C->>C: onSubmit callback or native handler
```
---
## Mermaid: Module Dependency Graph
```mermaid
graph LR
PC[post-composer.mjs] --> BA[blossom-api.mjs]
PC --> BU[blossom-ui.mjs]
PC --> UT[utilities.mjs]
PC --> PI[post-interactions.mjs]
BA --> BU
PH[post.html] --> PC
PH --> PI
PH --> BA
PH --> BU
PH --> UT
```
---
## Key Design Decisions
1. **Module, not Web Component** — Using a plain ES module with `mountComposer()` rather than a custom element, because the existing codebase uses ES modules throughout and the contenteditable div is already in the HTML. This avoids Shadow DOM complexity and keeps CSS consistent.
2. **Default server for URLs** — `getBlobUrl()` in `blossom-api.mjs` already implements BUD-03 logic (first healthy upload server). We use this for the URL inserted into the post, while `uploadToAllServers()` handles redundant storage.
3. **Preview reuses existing rendering** — By calling `htmlFormatText()` + `hydrateNostrEntities()`, the preview is guaranteed to match how posts look in the feed. No duplicate rendering logic.
4. **@ mentions use npub, not display name** — The inserted text is `nostr:npub1…` which is the Nostr standard. The preview and feed rendering will resolve this to a clickable name via `htmlFormatText`.
5. **Contact list as input** — The composer receives the followed profiles list from the host page rather than fetching it independently. This avoids duplicate subscriptions and lets the host page control when/how profiles are loaded.