mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-11 16:07:15 +02:00
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:
368
docs/compose-dialog.md
Normal file
368
docs/compose-dialog.md
Normal 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
|
||||
Reference in New Issue
Block a user