feat: add generic compose/reply dialog with threading support

Implements a comprehensive compose dialog system for creating and replying to Nostr events with automatic protocol-aware threading.

## Features

- **ComposeDialog**: Main dialog with rich text editing, relay selection, and preview mode
- **ThreadBuilder**: Automatic NIP-10 (kind 1) and NIP-22 (all others) thread tag generation
- **RelaySelector**: Visual relay picker with connection status indicators
- **PowerTools**: Quick access toolbar for hashtags, mentions, code blocks, and links
- **MentionEditor**: Enhanced with insertText() method for programmatic insertion

## Threading Support

- NIP-10: Kind 1 notes use e/p tags with root/reply markers
- NIP-22: All other kinds use K/E/A tags for comments
- Automatic mention extraction and p-tag management
- Reply context preview with event metadata

## Components

- src/components/ComposeDialog.tsx (406 lines)
- src/components/RelaySelector.tsx (259 lines)
- src/components/PowerTools.tsx (183 lines)
- src/lib/thread-builder.ts (200 lines)
- docs/compose-dialog.md (comprehensive documentation)

Ready for integration into event viewers and timeline components.
This commit is contained in:
Claude
2026-01-12 14:57:51 +00:00
parent b24810074d
commit 337bc65756
7 changed files with 1503 additions and 0 deletions

368
docs/compose-dialog.md Normal file
View File

@@ -0,0 +1,368 @@
# Compose/Reply Dialog System
A comprehensive, protocol-aware compose dialog for creating and replying to Nostr events.
## Overview
The compose dialog system provides a unified interface for composing notes, replies, and other Nostr events with automatic threading support for both NIP-10 (kind 1 notes) and NIP-22 (all other kinds).
## Architecture
### Core Components
1. **ComposeDialog** (`src/components/ComposeDialog.tsx`)
- Main dialog component
- Rich text editing with MentionEditor
- Tab-based UI (Edit/Preview)
- Relay selection
- Mention management
- Event publishing
2. **RelaySelector** (`src/components/RelaySelector.tsx`)
- Visual relay picker with connection status
- Add/remove relays dynamically
- Shows relay connection state (connected/connecting/disconnected)
- Limits maximum relay count
3. **PowerTools** (`src/components/PowerTools.tsx`)
- Quick access toolbar for formatting
- Hashtag insertion
- Profile mention search and insertion
- Code block insertion
- Link insertion
4. **Thread Builder** (`src/lib/thread-builder.ts`)
- Automatic thread tag generation
- NIP-10 support (kind 1 notes)
- NIP-22 support (all other kinds)
- Mention extraction
### Enhanced MentionEditor
Added `insertText(text: string)` method to MentionEditorHandle for programmatic text insertion from PowerTools.
## Features
### ✅ Implemented
- **Rich Text Editing**: TipTap-based editor with @ mentions and : emoji autocomplete
- **Threading**: Automatic NIP-10 (kind 1) and NIP-22 (all others) threading
- **Relay Selection**: Choose which relays to publish to with connection status
- **Mention Management**: Explicit p-tag control with visual badges
- **Preview Mode**: Preview content and tags before publishing
- **Power Tools**: Quick access to hashtags, mentions, code blocks, links
- **Emoji Support**: NIP-30 emoji tags automatically included
- **Reply Context**: Shows who you're replying to with message preview
### 🔮 Future Enhancements
- Media uploads (NIP-94, NIP-95)
- Quote reposts (NIP-48)
- Draft persistence to Dexie
- Rich text formatting toolbar
- Link preview cards
- Poll creation (NIP-69)
- Content warnings (NIP-36)
## Usage
### Basic Compose
```tsx
import { ComposeDialog } from "@/components/compose";
import { useState } from "react";
function MyComponent() {
const [showCompose, setShowCompose] = useState(false);
return (
<>
<Button onClick={() => setShowCompose(true)}>
Compose Note
</Button>
<ComposeDialog
open={showCompose}
onOpenChange={setShowCompose}
kind={1}
onPublish={(event) => {
console.log("Published event:", event.id);
}}
/>
</>
);
}
```
### Reply to Event
```tsx
import { ComposeDialog } from "@/components/compose";
import type { NostrEvent } from "nostr-tools/core";
function ReplyButton({ event }: { event: NostrEvent }) {
const [showReply, setShowReply] = useState(false);
return (
<>
<Button onClick={() => setShowReply(true)}>
Reply
</Button>
<ComposeDialog
open={showReply}
onOpenChange={setShowReply}
replyTo={event}
kind={1}
onPublish={(newEvent) => {
console.log("Reply published:", newEvent);
}}
/>
</>
);
}
```
### Comment on Non-Note Event (NIP-22)
```tsx
<ComposeDialog
open={showCompose}
onOpenChange={setShowCompose}
replyTo={articleEvent} // kind 30023 article
kind={1111} // Comment kind
onPublish={(comment) => {
console.log("Comment published:", comment);
}}
/>
```
## Threading Behavior
### Kind 1 (Notes) - NIP-10
When replying to a kind 1 note, the dialog automatically adds:
```
["e", "<root-event-id>", "<relay-url>", "root"]
["e", "<reply-to-event-id>", "<relay-url>", "reply"]
["p", "<author-pubkey>"]
["p", "<mentioned-pubkey>", ...]
```
**Thread Structure:**
- Root tag: Points to the thread's first event
- Reply tag: Points to the direct parent event
- P tags: All mentioned users (author + thread participants)
### All Other Kinds - NIP-22
When commenting on other event kinds, the dialog uses NIP-22:
```
["K", "<kind>"]
["E", "<event-id>", "<relay-url>", "<author-pubkey>"] // OR
["A", "<kind:pubkey:d-tag>", "<relay-url>"] // For parameterized replaceable
["p", "<author-pubkey>"]
["k", "<kind>"] // Deprecated, included for compatibility
```
**Behavior:**
- K tag: Kind of the parent event
- E tag: Event pointer (regular/replaceable events)
- A tag: Address pointer (parameterized replaceable events)
- P tags: Mentioned users
- Deprecated k tag: Included for backwards compatibility
## Component Props
### ComposeDialog
```typescript
interface ComposeDialogProps {
open: boolean; // Dialog open state
onOpenChange: (open: boolean) => void; // Open state change callback
replyTo?: NostrEvent; // Event being replied to (optional)
kind?: number; // Event kind to create (default: 1)
initialContent?: string; // Pre-filled content
onPublish?: (event: NostrEvent) => void; // Callback after publish
}
```
### RelaySelector
```typescript
interface RelaySelectorProps {
selectedRelays: string[]; // Currently selected relays
onRelaysChange: (relays: string[]) => void; // Selection change callback
maxRelays?: number; // Max relay limit (default: 10)
}
```
### PowerTools
```typescript
interface PowerToolsProps {
onInsert?: (text: string) => void; // Text insertion callback
onAddMention?: (pubkey: string) => void; // Mention addition callback
}
```
## Thread Tag API
Use the thread builder utilities directly if you need custom tag generation:
```typescript
import { buildThreadTags, buildNip10Tags, buildNip22Tags } from "@/lib/thread-builder";
// Automatic protocol selection
const { tags, relayHint } = buildThreadTags(replyTo, replyKind);
// Explicit NIP-10 (kind 1)
const nip10Tags = buildNip10Tags(replyTo, additionalMentions);
// Explicit NIP-22 (all other kinds)
const nip22Tags = buildNip22Tags(replyTo, additionalMentions);
```
## Keyboard Shortcuts
- **Ctrl/Cmd+Enter**: Submit/publish
- **Shift+Enter**: New line (in editor)
- **Escape**: Close autocomplete suggestions
- **@username**: Trigger profile autocomplete
- **:emoji**: Trigger emoji autocomplete
## Styling
The dialog uses Tailwind CSS with HSL CSS variables from the theme. All components are fully styled and responsive:
- Mobile-friendly layout
- Dark mode support
- Accessible keyboard navigation
- Visual relay connection indicators
- Inline error handling
## Integration Points
### Event Publishing
Events are published using the action system:
```typescript
import { hub, publishEventToRelays } from "@/services/hub";
// Create and sign
const event = await hub.run(async ({ factory }) => {
const unsigned = factory.event(kind, content, tags);
return await factory.sign(unsigned);
});
// Publish to selected relays
await publishEventToRelays(event, selectedRelays);
```
### Relay Management
Relays are loaded from the user's NIP-65 relay list (kind 10002):
```typescript
import { relayListCache } from "@/services/relay-list-cache";
const outboxRelays = await relayListCache.getOutboxRelays(pubkey);
```
### Profile Search
Profile autocomplete uses the ProfileSearchService:
```typescript
import { useProfileSearch } from "@/hooks/useProfileSearch";
const { searchProfiles } = useProfileSearch();
const results = await searchProfiles("alice");
```
### Emoji Search
Emoji autocomplete uses the EmojiSearchService with:
- Unicode emojis (built-in)
- User emoji lists (kind 10030)
- Emoji sets (kind 30030)
```typescript
import { useEmojiSearch } from "@/hooks/useEmojiSearch";
const { searchEmojis } = useEmojiSearch();
const results = await searchEmojis("smile");
```
## File Structure
```
src/
├── components/
│ ├── compose/
│ │ └── index.ts # Public exports
│ ├── ComposeDialog.tsx # Main dialog
│ ├── RelaySelector.tsx # Relay picker
│ ├── PowerTools.tsx # Formatting toolbar
│ └── editor/
│ └── MentionEditor.tsx # Rich text editor (enhanced)
├── lib/
│ └── thread-builder.ts # Threading utilities
└── services/
└── hub.ts # Action runner & publishing
```
## Testing
To test the compose dialog:
1. **Unit tests**: Test thread builder functions
```bash
npm test thread-builder
```
2. **Integration tests**: Test in a real viewer component
```tsx
// Add to an existing viewer component
const [showCompose, setShowCompose] = useState(false);
// In render:
<Button onClick={() => setShowCompose(true)}>Reply</Button>
<ComposeDialog
open={showCompose}
onOpenChange={setShowCompose}
replyTo={event}
/>
```
3. **Manual testing**:
- Compose a new note
- Reply to an existing note
- Comment on an article (kind 30023)
- Test with different relay configurations
- Test mention and emoji autocomplete
- Test preview mode
- Test power tools
## Notes
- The dialog requires an active account with a signer
- At least one relay must be selected to publish
- Thread tags are automatically built based on event kind
- All p-tags (mentions) can be managed explicitly
- Emoji tags (NIP-30) are automatically included for custom emojis
- The editor supports both Unicode and custom emojis
## Related NIPs
- [NIP-10](https://github.com/nostr-protocol/nips/blob/master/10.md): Conventions for clients' use of e and p tags
- [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md): Event `created_at` Limits
- [NIP-30](https://github.com/nostr-protocol/nips/blob/master/30.md): Custom Emoji
- [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md): Relay List Metadata
---
Built with ❤️ for Grimoire