### Direct Messaging on Nostr
This project includes a complete direct messaging system supporting both NIP-04 (legacy) and NIP-17 (modern, - more - private) encrypted messages with real-time subscriptions, optimistic updates, and a persistent - cache first - local storage.
## Quick Start
### 1. Enable Direct Messaging
The `DMProvider` is already added to your app, but **disabled by default**. To enable messaging, pass `enabled: true` in the config:
```tsx
import { DMProvider } from '@/components/DMProvider';
import { PROTOCOL_MODE } from '@/lib/dmConstants';
function App() {
return (
{/* Your app components */}
);
}
```
**Config Options:**
- `enabled` (boolean, default: `false`) - Enable/disable entire DM system. When false, no messages are loaded, stored, or processed.
- `protocolMode` (ProtocolMode, default: `PROTOCOL_MODE.NIP17_ONLY`) - Which protocols to support:
- `PROTOCOL_MODE.NIP04_ONLY` - Legacy encryption only
- `PROTOCOL_MODE.NIP17_ONLY` - Modern private messages (recommended)
- `PROTOCOL_MODE.BOTH` - Support both protocols (for backwards compatibility)
**Note**: The DM system uses domain-based IndexedDB naming (`nostr-dm-store-${hostname}`) to prevent conflicts between multiple apps on the same domain.
### 2. Send Messages
```tsx
import { useDMContext } from '@/hooks/useDMContext';
import { MESSAGE_PROTOCOL } from '@/lib/dmConstants';
function ComposeMessage({ recipientPubkey }: { recipientPubkey: string }) {
const { sendMessage } = useDMContext();
const [content, setContent] = useState('');
const handleSend = async () => {
await sendMessage({
recipientPubkey,
content,
protocol: MESSAGE_PROTOCOL.NIP17, // Uses NIP-44 encryption + gift wrapping
});
setContent('');
};
return (
);
}
```
### 3. Display Conversations
```tsx
import { useDMContext } from '@/hooks/useDMContext';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
function ConversationList({ onSelectConversation }: { onSelectConversation: (pubkey: string) => void }) {
const { conversations, isLoading } = useDMContext();
if (isLoading) {
return
);
}
```
## Using the Complete Messaging Interface
For a fully-featured messaging UI out of the box, use the `DMMessagingInterface` component:
```tsx
import { DMMessagingInterface } from "@/components/dm/DMMessagingInterface";
function MessagesPage() {
return (
);
}
```
The `DMMessagingInterface` component provides a complete messaging UI with:
- Conversation list with Active/Requests tabs
- Message thread view with pagination
- Compose area with file upload support
- Real-time message updates
- Mobile-responsive layout (shows one panel at a time on mobile)
It requires no props and works automatically when wrapped in `DMProvider`.
**For custom layouts**, see the "Building Custom Messaging UIs" section below for individual components (`DMConversationList`, `DMChatArea`, `DMStatusInfo`).
## Sending Files with Messages
```tsx
import { useDMContext } from '@/hooks/useDMContext';
import { useUploadFile } from '@/hooks/useUploadFile';
import { MESSAGE_PROTOCOL } from '@/lib/dmConstants';
import type { FileAttachment } from '@/contexts/DMContext';
function ComposeWithFiles({ recipientPubkey }: { recipientPubkey: string }) {
const { sendMessage } = useDMContext();
const { mutateAsync: uploadFile, isPending: isUploading } = useUploadFile();
const [content, setContent] = useState('');
const [selectedFile, setSelectedFile] = useState(null);
const handleSend = async () => {
let attachments: FileAttachment[] | undefined;
// Upload file if one is selected
if (selectedFile) {
const tags = await uploadFile(selectedFile);
attachments = [{
url: tags[0][1], // URL from first tag
mimeType: selectedFile.type,
size: selectedFile.size,
name: selectedFile.name,
tags: tags
}];
}
await sendMessage({
recipientPubkey,
content,
protocol: MESSAGE_PROTOCOL.NIP17,
attachments,
});
setContent('');
setSelectedFile(null);
};
return (
);
}
```
## Protocol Comparison
### NIP-04 (Legacy)
- **Encryption**: NIP-04 (simpler, older)
- **Metadata**: Sender and recipient visible to relays
- **Event Kind**: Kind 4
- **Use When**: Compatibility with older clients
### NIP-17 (Modern & Private)
- **Encryption**: NIP-44 (stronger)
- **Metadata**: Hidden via gift wrapping (NIP-59)
- **Event Kinds**: Kind 14 (text), Kind 15 (files)
- **Wrapped In**: Kind 1059 (Gift Wrap) with ephemeral keys
- **Use When**: Maximum privacy (recommended)
**Key Privacy Features of NIP-17:**
- Sender identity hidden (uses random ephemeral keys)
- Timestamps randomized (±2 days) to hide send time
- Dual gift wraps (recipient + sender) for message history
## Advanced Features
### Conversation Categorization
The system automatically categorizes conversations:
```tsx
const { conversations } = useDMContext();
// Filter by category
const knownConversations = conversations.filter(c => c.isKnown);
const requestConversations = conversations.filter(c => c.isRequest);
// isKnown = true if user has sent at least one message
// isRequest = true if only received messages, never replied
```
### Loading States
```tsx
const { isLoading, loadingPhase, scanProgress } = useDMContext();
// Check overall loading state
if (isLoading) {
console.log('Current phase:', loadingPhase);
// LOADING_PHASES.CACHE - Loading from local cache
// LOADING_PHASES.RELAYS - Querying relays
// LOADING_PHASES.SUBSCRIPTIONS - Setting up real-time updates
// LOADING_PHASES.READY - Fully loaded
}
// Display scan progress for large message histories
if (scanProgress.nip17) {
console.log(`NIP-17: ${scanProgress.nip17.current} messages - ${scanProgress.nip17.status}`);
}
```
### Clear Cache and Refresh
```tsx
import { useDMContext } from '@/hooks/useDMContext';
function SettingsButton() {
const { clearCacheAndRefetch } = useDMContext();
const handleClearCache = async () => {
await clearCacheAndRefetch();
// Clears IndexedDB cache and reloads all messages from relays
};
return (
);
}
```
## Architecture Notes
### Data Flow
1. **Cache First**: Messages load instantly from encrypted IndexedDB cache
2. **Background Sync**: New messages fetched from relays in parallel
3. **Real-time Updates**: WebSocket subscriptions for live messages
4. **Optimistic UI**: Sent messages appear immediately, confirmed on relay response
### Storage
- **IndexedDB**: All messages stored locally with NIP-44 encryption
- **Per-User Storage**: Separate encrypted store for each logged-in user
- **Automatic Sync**: Debounced writes (15s) + immediate on new messages
### Performance
- **Parallel Queries**: NIP-04 and NIP-17 messages fetched simultaneously
- **Batched Loading**: Messages loaded in batches (1000/batch, 20k limit)
- **Pagination**: Conversation messages paginated (25/page)
- **Deduplication**: Automatic filtering of duplicate messages by ID
### Security
- **NIP-44 Encryption**: Modern authenticated encryption for all NIP-17 messages
- **Local Encryption**: IndexedDB storage encrypted with user's NIP-44 key
- **Ephemeral Keys**: Random keys for NIP-17 gift wraps (sender anonymity)
- **No Plaintext**: Decrypted content never persisted unencrypted
- **Domain Isolation**: IndexedDB databases are namespaced by hostname to prevent data conflicts
## Building Custom Messaging UIs
For advanced use cases, you can use the individual DM components to build custom layouts:
### Available Components
**`DMConversationList`** - Conversation sidebar with tabs
```tsx
import { DMConversationList } from '@/components/dm/DMConversationList';
setSelectedPubkey(pubkey)}
onStatusClick={() => setShowStatus(true)} // optional
className="h-full"
/>
```
**`DMChatArea`** - Message thread and compose area
```tsx
import { DMChatArea } from '@/components/dm/DMChatArea';
setSelectedPubkey(null)} // optional, for mobile back button
className="h-full"
/>
```
**`DMStatusInfo`** - Debug/status panel
```tsx
import { DMStatusInfo } from '@/components/dm/DMStatusInfo';
```
### Custom Layout Example
```tsx
import { useState } from 'react';
import { DMConversationList } from '@/components/dm/DMConversationList';
import { DMChatArea } from '@/components/dm/DMChatArea';
function CustomMessagingLayout() {
const [selectedPubkey, setSelectedPubkey] = useState(null);
return (
{/* Custom sidebar */}
{/* Custom main area */}
{selectedPubkey ? (
) : (