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