248 lines
11 KiB
Markdown
248 lines
11 KiB
Markdown
# 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 `<div contenteditable>` 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 `<input type="file">` 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 `<img>`)
|
|
- 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 `<input type="file" accept="image/*,video/*,audio/*">`
|
|
- 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 `<link rel="stylesheet" href="./css/post-composer.css" />` in `<head>`
|
|
- 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.
|