mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-17 19:07:06 +02:00
Add detailed design documents for implementing NIP-10 thread chat feature: - nip10-thread-chat-design.md: Full architecture, data structures, adapter implementation plan, relay selection strategy, UI requirements, and 7-phase implementation checklist - nip10-thread-chat-examples.md: Complete code examples showing identifier parsing, conversation resolution, message loading, reply sending with proper NIP-10 tags, and ChatViewer integration - nip10-thread-chat-summary.md: Quick reference with visual comparisons, architecture diagrams, protocol comparison table, data flow, and FAQ The feature will enable "chat nevent1..." to display kind 1 threaded conversations as chat interfaces, with the root event prominently displayed at the top and all replies shown as chat messages below. Key design decisions: - Use e-tags with NIP-10 markers (root/reply) instead of q-tags - Merge multiple relay sources (seen, hints, outbox) for coverage - Display root event centered with full feed renderer - Reuse existing ChatViewer infrastructure via adapter pattern - Support both nevent (with relay hints) and note (ID only) formats
473 lines
17 KiB
Markdown
473 lines
17 KiB
Markdown
# NIP-10 Thread Chat - Quick Reference
|
|
|
|
## What It Is
|
|
|
|
Turn any kind 1 Nostr thread into a chat interface, with the root event displayed prominently at the top and all replies shown as chat messages below.
|
|
|
|
## Command Format
|
|
|
|
```bash
|
|
chat nevent1qqsxyz... # Full nevent with relay hints
|
|
chat note1abc... # Simple note ID (less reliable)
|
|
```
|
|
|
|
## Visual Comparison
|
|
|
|
### Before (Traditional Feed View)
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ Alice: Check out this cool feature │ ← Root event
|
|
│ [Like] [Repost] [Reply] [Zap] │
|
|
├─────────────────────────────────────┤
|
|
│ Bob: Great idea! │ ← Reply (separate event)
|
|
│ [Like] [Repost] [Reply] [Zap] │
|
|
├─────────────────────────────────────┤
|
|
│ Carol: I agree with Bob │ ← Reply (separate event)
|
|
│ [Like] [Repost] [Reply] [Zap] │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### After (Thread Chat View)
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ 📌 Check out this cool feature... │ ← Header (thread title)
|
|
├─────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────────────┐ │
|
|
│ │ Alice │ │ ← Root event (centered)
|
|
│ │ Check out this │ │ Full feed renderer
|
|
│ │ cool feature I built │ │ (can like, zap, etc.)
|
|
│ │ [Like] [Zap] [Share] │ │
|
|
│ └──────────────────────┘ │
|
|
│ │
|
|
│ ─────────── Replies ────────────── │ ← Visual separator
|
|
│ │
|
|
│ Bob: Great idea! │ ← Replies as chat messages
|
|
│ └─ ↳ thread root │ (simpler, chat-like)
|
|
│ │
|
|
│ Carol: I agree with Bob │
|
|
│ └─ ↳ Bob │
|
|
│ │
|
|
│ ⚡ 1000 Alice │ ← Zaps inline
|
|
│ │
|
|
│ [Type a message...] 📎 [Send] │ ← Chat composer
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ User Input │
|
|
│ chat nevent1qqsxyz... │
|
|
└───────────────────────┬────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ src/lib/chat-parser.ts │
|
|
│ Tries each adapter's parseIdentifier(): │
|
|
│ 1. Nip10Adapter (nevent/note) ← NEW │
|
|
│ 2. Nip29Adapter (relay'group) │
|
|
│ 3. Nip53Adapter (naddr live chat) │
|
|
└───────────────────────┬────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ src/lib/chat/adapters/nip-10-adapter.ts ← NEW │
|
|
│ │
|
|
│ parseIdentifier(input) │
|
|
│ • Match nevent/note format │
|
|
│ • Decode to EventPointer │
|
|
│ • Return ThreadIdentifier │
|
|
│ │
|
|
│ resolveConversation(identifier) │
|
|
│ • Fetch provided event │
|
|
│ • Parse NIP-10 refs → find root │
|
|
│ • Determine relays (merge sources) │
|
|
│ • Extract title from root │
|
|
│ • Return Conversation │
|
|
│ │
|
|
│ loadMessages(conversation) │
|
|
│ • Subscribe: kind 1 replies │
|
|
│ • Subscribe: kind 7 reactions │
|
|
│ • Subscribe: kind 9735 zaps │
|
|
│ • Convert to Message[] │
|
|
│ • Return Observable │
|
|
│ │
|
|
│ sendMessage(conversation, content, options) │
|
|
│ • Build NIP-10 tags (root + reply markers) │
|
|
│ • Add p-tags for participants │
|
|
│ • Create kind 1 event │
|
|
│ • Publish to conversation relays │
|
|
└───────────────────────┬────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────────────────────────────────────────────────┐
|
|
│ src/components/ChatViewer.tsx │
|
|
│ │
|
|
│ Detects: protocol === "nip-10" │
|
|
│ │
|
|
│ Special rendering: │
|
|
│ • Fetch rootEventId from conversation.metadata │
|
|
│ • Render root with KindRenderer (centered) │
|
|
│ • Show visual separator │
|
|
│ • Render replies as chat messages below │
|
|
│ • Chat composer at bottom │
|
|
└────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Key Differences from Other Chat Protocols
|
|
|
|
| Feature | NIP-29 Groups | NIP-53 Live Chat | **NIP-10 Threads (NEW)** |
|
|
|---------|---------------|------------------|---------------------------|
|
|
| **Event Kind** | 9 | 1311 | **1** |
|
|
| **Reply Tag** | q-tag | q-tag | **e-tag with markers** |
|
|
| **Root Display** | ❌ No root | ❌ No root | **✅ Root at top** |
|
|
| **Relay Model** | Single relay | Multiple relays | **Multiple relays** |
|
|
| **Membership** | Admin approval | Open | **Open** |
|
|
| **Protocol** | nip-29 | nip-53 | **nip-10** |
|
|
| **Identifier** | `relay'group` | `naddr1...` | **`nevent1...`** |
|
|
| **Use Case** | Private groups | Live streams | **Twitter threads** |
|
|
|
|
## NIP-10 Tag Structure Comparison
|
|
|
|
### Direct Reply to Root (Kind 1)
|
|
|
|
```javascript
|
|
{
|
|
kind: 1,
|
|
content: "Great point!",
|
|
tags: [
|
|
["e", "<root-id>", "<relay>", "root", "<root-author>"],
|
|
["p", "<root-author>"]
|
|
]
|
|
}
|
|
```
|
|
|
|
### Nested Reply (Kind 1)
|
|
|
|
```javascript
|
|
{
|
|
kind: 1,
|
|
content: "I agree with Alice!",
|
|
tags: [
|
|
["e", "<root-id>", "<relay>", "root", "<root-author>"], // Thread root
|
|
["e", "<alice-msg-id>", "<relay>", "reply", "<alice-pk>"], // Direct parent
|
|
["p", "<root-author>"],
|
|
["p", "<alice-pk>"]
|
|
]
|
|
}
|
|
```
|
|
|
|
### NIP-29 Group Message (Kind 9) - For Comparison
|
|
|
|
```javascript
|
|
{
|
|
kind: 9,
|
|
content: "Hello group!",
|
|
tags: [
|
|
["h", "<group-id>"], // Group identifier
|
|
["q", "<parent-msg-id>"] // Simple reply (if replying)
|
|
]
|
|
}
|
|
```
|
|
|
|
## Relay Selection Strategy
|
|
|
|
NIP-10 threads use **merged relay sources** for maximum reach:
|
|
|
|
```
|
|
1. Root Event Seen Relays (from EventStore)
|
|
└─ Where the root was originally found
|
|
|
|
2. Provided Event Relay Hints (from nevent)
|
|
└─ User-specified relays in the nevent encoding
|
|
|
|
3. Root Author's Outbox (NIP-65 kind 10002)
|
|
└─ Where the root author publishes
|
|
|
|
4. Active User's Outbox (NIP-65 kind 10002)
|
|
└─ Where current user publishes
|
|
|
|
5. Fallback Popular Relays (if < 3 found)
|
|
└─ relay.damus.io, nos.lol, relay.nostr.band
|
|
|
|
Result: Top 5-7 relays (deduplicated, normalized)
|
|
```
|
|
|
|
## Data Flow
|
|
|
|
### Loading a Thread
|
|
|
|
```
|
|
1. User: chat nevent1qqsxyz...
|
|
│
|
|
▼
|
|
2. Parse nevent → EventPointer
|
|
│
|
|
▼
|
|
3. Fetch event xyz
|
|
│
|
|
├─ Is it kind 1? ✓
|
|
│
|
|
▼
|
|
4. Parse NIP-10 references
|
|
│
|
|
├─ Has root marker? → Fetch root event
|
|
└─ No root marker? → xyz IS the root
|
|
│
|
|
▼
|
|
5. Determine relays
|
|
│
|
|
├─ Merge: seen, hints, outbox
|
|
│
|
|
▼
|
|
6. Subscribe to thread events
|
|
│
|
|
├─ kind 1 with #e = root.id
|
|
├─ kind 7 with #e = root.id
|
|
└─ kind 9735 with #e = root.id
|
|
│
|
|
▼
|
|
7. Convert events → Messages
|
|
│
|
|
├─ Parse reply hierarchy
|
|
├─ Extract zap amounts
|
|
└─ Sort chronologically
|
|
│
|
|
▼
|
|
8. Render UI
|
|
│
|
|
├─ Root event (centered, feed renderer)
|
|
├─ Visual separator
|
|
└─ Replies (chat messages)
|
|
```
|
|
|
|
### Sending a Reply
|
|
|
|
```
|
|
1. User types: "Great idea!"
|
|
│
|
|
▼
|
|
2. User clicks Reply on Alice's message
|
|
│
|
|
▼
|
|
3. Adapter.sendMessage(conversation, content, { replyTo: alice.id })
|
|
│
|
|
▼
|
|
4. Build tags:
|
|
│
|
|
├─ ["e", root.id, relay, "root", root.author]
|
|
├─ ["e", alice.id, relay, "reply", alice.pk]
|
|
├─ ["p", root.author]
|
|
└─ ["p", alice.pk]
|
|
│
|
|
▼
|
|
5. Create kind 1 event
|
|
│
|
|
▼
|
|
6. Publish to conversation relays
|
|
│
|
|
├─ relay.damus.io
|
|
├─ nos.lol
|
|
└─ root.author's outbox
|
|
│
|
|
▼
|
|
7. Event propagates
|
|
│
|
|
▼
|
|
8. Subscription receives event
|
|
│
|
|
▼
|
|
9. UI updates (new message appears)
|
|
```
|
|
|
|
## Implementation Files
|
|
|
|
### New Files (to be created)
|
|
|
|
```
|
|
src/lib/chat/adapters/nip-10-adapter.ts
|
|
└─ Full adapter implementation (~600 lines)
|
|
|
|
src/lib/chat/adapters/nip-10-adapter.test.ts
|
|
└─ Unit tests for adapter
|
|
|
|
docs/nip10-thread-chat-design.md
|
|
└─ Detailed design spec
|
|
|
|
docs/nip10-thread-chat-examples.md
|
|
└─ Code examples
|
|
|
|
docs/nip10-thread-chat-summary.md
|
|
└─ This file
|
|
```
|
|
|
|
### Modified Files
|
|
|
|
```
|
|
src/types/chat.ts
|
|
├─ Add ThreadIdentifier type
|
|
├─ Add "nip-10" to ChatProtocol
|
|
└─ Add rootEventId to ConversationMetadata
|
|
|
|
src/lib/chat-parser.ts
|
|
├─ Import Nip10Adapter
|
|
└─ Add to adapter priority list
|
|
|
|
src/components/ChatViewer.tsx
|
|
├─ Detect isThreadChat = protocol === "nip-10"
|
|
├─ Fetch rootEvent from metadata
|
|
├─ Render root with KindRenderer (centered)
|
|
└─ Show visual separator
|
|
|
|
src/components/chat/ReplyPreview.tsx
|
|
└─ Show "thread root" when replying to root
|
|
|
|
CLAUDE.md
|
|
└─ Document NIP-10 thread chat usage
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Example 1: Opening Thread from Twitter-like Feed
|
|
|
|
```bash
|
|
# User sees interesting post in feed (kind 1 note)
|
|
# Clicks "View as Thread" button
|
|
# App extracts event pointer
|
|
chat nevent1qqsrz7x...
|
|
|
|
# Result: Opens thread chat with:
|
|
# - Root post at top (full renderer with actions)
|
|
# - All replies as chat messages below
|
|
# - Reply composer at bottom
|
|
```
|
|
|
|
### Example 2: Opening Thread from Deep Reply
|
|
|
|
```bash
|
|
# User is reading a reply deep in a thread
|
|
# Clicks "View Thread" context menu
|
|
chat nevent1qqsabc... # This is a nested reply, not root
|
|
|
|
# Adapter logic:
|
|
# 1. Fetch event abc
|
|
# 2. Parse NIP-10 refs → finds root XYZ
|
|
# 3. Fetch root event XYZ
|
|
# 4. Resolve conversation with root XYZ
|
|
# 5. Load all replies to XYZ
|
|
# 6. Display full thread (not just from abc onward)
|
|
```
|
|
|
|
### Example 3: Replying in Thread
|
|
|
|
```
|
|
Thread Chat Interface:
|
|
|
|
┌──────────────────────────────────┐
|
|
│ [Root Event by Alice] │ User can interact with root
|
|
│ 42 ⚡ 18 ♥ 3 💬 │ (zap, like, etc.)
|
|
├──────────────────────────────────┤
|
|
│ │
|
|
│ Bob: Great point! │ ← Hover shows Reply button
|
|
│ └─ ↳ thread root │
|
|
│ │
|
|
│ Carol: I agree │ ← Click Reply here
|
|
│ └─ ↳ Bob │
|
|
│ │
|
|
└──────────────────────────────────┘
|
|
↓
|
|
┌──────────────────────────────────┐
|
|
│ [Replying to Carol] │ ← Reply preview shows
|
|
│ Type message... [Send] │
|
|
└──────────────────────────────────┘
|
|
↓ User types "Me too!"
|
|
↓ Clicks Send
|
|
↓
|
|
Publishes kind 1 with:
|
|
["e", rootId, relay, "root", alice] # Thread root
|
|
["e", carolId, relay, "reply", carol] # Direct parent
|
|
["p", alice] # Root author
|
|
["p", carol] # Parent author
|
|
["p", bob] # Mentioned in parent
|
|
```
|
|
|
|
## Benefits
|
|
|
|
### For Users
|
|
- **Focused discussion**: See entire thread in one view
|
|
- **Better context**: Root always visible at top
|
|
- **Faster replies**: Chat-like composer instead of feed actions
|
|
- **Real-time**: New replies appear instantly (subscription)
|
|
- **Cross-client**: Works with any NIP-10 compliant client
|
|
|
|
### For Developers
|
|
- **Reuses infrastructure**: Same ChatViewer, just different adapter
|
|
- **Protocol-agnostic UI**: Adapter pattern abstracts differences
|
|
- **Testable**: Unit tests for adapter, integration tests for UI
|
|
- **Extensible**: Easy to add features (threading UI, export, etc.)
|
|
|
|
## Limitations & Trade-offs
|
|
|
|
### Limitations
|
|
1. **Kind 1 only**: Doesn't work with other event kinds
|
|
2. **No encryption**: All messages public (NIP-10 has no encryption)
|
|
3. **No moderation**: Can't delete/hide replies (not in NIP-10)
|
|
4. **Relay dependent**: Need good relay coverage to see all replies
|
|
5. **No real-time guarantees**: Relies on relay subscriptions
|
|
|
|
### Trade-offs
|
|
- **Simplicity vs Features**: Chat UI sacrifices some feed features (repost, quote, etc.)
|
|
- **Performance vs Completeness**: Limit to 7 relays for speed (might miss some replies)
|
|
- **UX vs Protocol**: Showing root separately breaks chronological order slightly
|
|
|
|
## Future Enhancements
|
|
|
|
1. **Thread Tree View**: Toggle between flat chat and tree structure
|
|
2. **Thread Statistics**: "X replies from Y participants"
|
|
3. **Smart Relay Discovery**: Learn which relays have full thread
|
|
4. **Missing Reply Detection**: "Some replies may be missing" indicator
|
|
5. **Thread Export**: Save thread as markdown/JSON
|
|
6. **Quote Highlighting**: Visual indication of quoted text
|
|
7. **Thread Branching**: Show sub-threads that diverge
|
|
8. **Participant Indicators**: Show who's been most active
|
|
|
|
## FAQ
|
|
|
|
**Q: What happens if I open a nevent for a kind 9 (group message)?**
|
|
A: Nip10Adapter returns null, Nip29Adapter handles it instead.
|
|
|
|
**Q: Can I reply to the root event from the chat?**
|
|
A: Yes! Click the Reply button that appears on hover on the root event display.
|
|
|
|
**Q: What if the root event is deleted?**
|
|
A: Adapter throws error "Thread root not found" - cannot display thread.
|
|
|
|
**Q: Do reactions work?**
|
|
A: Yes! Reactions (kind 7) are subscribed to and shown inline via MessageReactions component.
|
|
|
|
**Q: Can I zap messages in the thread?**
|
|
A: Yes! Zaps (kind 9735) are shown as special chat messages with ⚡ indicator.
|
|
|
|
**Q: What if relays are slow/offline?**
|
|
A: EventStore caches events. If relay is offline, cached events still display. New replies won't arrive until relay reconnects.
|
|
|
|
**Q: How is this different from opening the note in the feed?**
|
|
A: Thread chat provides focused, conversation-centric view with root prominent and chat-like UI. Feed view is more action-oriented (repost, quote, etc.).
|
|
|
|
**Q: Can I use this for group discussions?**
|
|
A: It works, but NIP-29 groups are better for persistent communities. NIP-10 threads are ad-hoc, thread-specific.
|
|
|
|
**Q: Does it support polls/forms/etc?**
|
|
A: No, only text replies (kind 1). Other kinds ignored.
|
|
|
|
## References
|
|
|
|
- [NIP-10: Text Note References](https://github.com/nostr-protocol/nips/blob/master/10.md)
|
|
- [NIP-19: bech32 Encoding](https://github.com/nostr-protocol/nips/blob/master/19.md)
|
|
- [NIP-65: Relay List Metadata](https://github.com/nostr-protocol/nips/blob/master/65.md)
|
|
- [Grimoire Chat System Docs](../CLAUDE.md#chat-system)
|