JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 88
  • Score
    100M100P100Q74420F
  • License MIT

Reusable Nostr messaging system with NIP-04 and NIP-17 support

Package Exports

  • @samthomson/nostr-messaging/core
  • @samthomson/nostr-messaging/package.json
  • @samthomson/nostr-messaging/ui

Readme

@samthomson/nostr-messaging

Reusable Nostr messaging system with NIP-04 and NIP-17 support, IndexedDB caching, and React context for state management.

Built to work with MKStacks.

Installation

npm install @samthomson/nostr-messaging

Quick Start

1. Wrap your app with DMProvider

The package provides a headless React context that manages all messaging state. You need to provide it with dependencies from your app:

import { DMProvider, type DMProviderDeps } from '@samthomson/nostr-messaging/core';
import { useNostr } from '@nostrify/react';
import { useCurrentUser } from '@/hooks/useCurrentUser';
// ... other hooks

function App() {
  const { nostr } = useNostr();
  const { user } = useCurrentUser();
  // ... gather other dependencies

  const deps: DMProviderDeps = {
    nostr,                    // NPool instance from @nostrify/react
    user,                     // { pubkey: string, signer: Signer } | null
    discoveryRelays: [...],   // Array of relay URLs for NIP-17
    relayMode: 'hybrid',      // 'discovery' | 'hybrid' | 'strict_outbox'
    updateConfig: (fn) => {}, // Function to update app config
    isOnline: true,           // Network status
    wasOffline: false,        // Was offline flag
    toast: ({ title, description, variant }) => {}, // Toast function
    getDisplayName: (pubkey, metadata) => string,   // Format user names
    fetchAuthorsBatch: useAuthorsBatch,            // Required: your app's batch-fetch hook (see below)
    publishEvent: async (event) => {},             // Publish events
  };

  return (
    <DMProvider deps={deps}>
      {/* Your app */}
    </DMProvider>
  );
}

Required: fetchAuthorsBatch
The provider needs profile metadata to show display names in the conversation list and chat. You must pass your app's batch-fetch hook (e.g. useAuthorsBatch) as fetchAuthorsBatch. The provider will call it with the relevant pubkeys; your hook should return { data?: Map } where the map is pubkey -> NostrMetadata or pubkey -> { metadata?: NostrMetadata }. If you use Vite, add resolve.dedupe: ["react", "react-dom"] so the package and app share one React instance; otherwise hook updates may not re-render the provider and names can stay unresolved.

2. Use the messaging hooks

import { useDMContext, useConversationMessages } from '@samthomson/nostr-messaging/core';

function MessagingComponent() {
  const {
    messagingState,
    conversations,
    sendMessage,
    searchMessages,
    // ... many other methods
  } = useDMContext();

  // Get messages for a specific conversation
  const {
    messages,
    loadMore,
    hasMore,
    isLoading,
  } = useConversationMessages('conversationId');

  // Send a message
  await sendMessage({
    conversationId: 'pubkey',
    content: 'Hello!',
    protocol: 'nip17', // or 'nip04'
  });

  return (
    <div>
      {/* Build your UI */}
    </div>
  );
}

Dependencies (peer vs dev)

The package declares @nostrify/nostrify and @nostrify/react in peerDependencies (so the consuming app installs and provides them; the package does not bundle them) and again in devDependencies (so we can build and test the package in isolation). That is intentional: peers are required at runtime from the app; devDeps are only for local npm install and npm run build in the package repo.

What's Included

Core (@samthomson/nostr-messaging/core)

Everything you need for messaging:

  • Pure Functions: Message encryption, conversation management, protocol handling
  • Storage: IndexedDB caching for messages and media
  • React Context: DMProvider for state management
  • React Hooks: useDMContext, useConversationMessages
  • Types: Full TypeScript support
  • Constants: DM_PHASES, MESSAGE_PROTOCOL, PROTOCOL_MODE, etc.

Features

  • ✅ NIP-04 (legacy DMs) and NIP-17 (private messages) support
  • ✅ Encrypted file attachments
  • ✅ IndexedDB caching for offline support
  • ✅ Message search (full-text and conversation search)
  • ✅ Unread message tracking
  • ✅ Real-time message sync
  • ✅ Gap filling for message history
  • ✅ Protocol auto-detection and migration

UI export: what the app must provide

If you use @samthomson/nostr-messaging/ui, the package does not bundle its own primitives. Your app must provide them so the package's imports resolve:

  1. Bundler config
    Your app must resolve @/ to your app source (e.g. @/src/). The package's UI code imports from @/components/ui/... and @/lib/utils; those are resolved at build time by your app's bundler.

  2. Components and util
    The package expects these to exist at the paths below. An MKStack/shadcn app already has them; otherwise add them or point @/ so they exist.

Path Exports used
@/lib/utils cn (class-name merge, e.g. clsx + tailwind-merge)
@/components/ui/avatar Avatar, AvatarFallback, AvatarImage
@/components/ui/badge Badge
@/components/ui/button Button
@/components/ui/card Card, CardContent
@/components/ui/dialog Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger
@/components/ui/popover Popover, PopoverContent, PopoverTrigger
@/components/ui/scroll-area ScrollArea
@/components/ui/select Select, SelectContent, SelectItem, SelectTrigger, SelectValue
@/components/ui/separator Separator
@/components/ui/skeleton Skeleton
@/components/ui/tabs Tabs, TabsContent, TabsList, TabsTrigger
@/components/ui/textarea Textarea
@/components/ui/tooltip Tooltip, TooltipContent, TooltipProvider, TooltipTrigger

Install the peer dependencies listed in package.json (React, Radix UI packages, clsx, tailwind-merge, lucide-react, etc.) so your app and the package agree on versions.

DMProviderDeps Interface

interface DMProviderDeps {
  nostr: NPool;                    // Nostr relay pool
  user: { pubkey: string; signer: Signer } | null;
  discoveryRelays: string[];       // Relays for NIP-17 discovery
  relayMode: 'discovery' | 'hybrid' | 'strict_outbox';
  updateConfig: (updater: (config: any) => any) => void;
  isOnline: boolean;
  wasOffline: boolean;
  toast: (options: {
    title?: string;
    description?: string;
    variant?: 'default' | 'destructive';
  }) => void;
  getDisplayName: (pubkey: string, metadata?: NostrMetadata) => string;
  /** Required. Pass your app's useAuthorsBatch (or equivalent). See "Required: fetchAuthorsBatch" above. */
  fetchAuthorsBatch: (pubkeys: string[]) => { data?: Map<string, NostrMetadata | { metadata?: NostrMetadata }> };
  publishEvent: (event: any) => Promise<void>;
}

useDMContext API

The main hook provides:

const {
  // State
  messagingState,           // Current loading phase and status
  conversations,            // Map of all conversations
  
  // Actions
  sendMessage,              // Send a message
  deleteMessage,            // Delete a message
  markAsRead,               // Mark conversation as read
  
  // Search
  searchMessages,           // Full-text message search
  searchConversations,      // Search conversations
  
  // File handling
  uploadAndSendFile,        // Upload and send file
  
  // Sync
  forceSync,                // Force sync messages
  
  // ... many more
} = useDMContext();

Development

Local Development with Another Project

If you're working on both the package and a consuming app:

# In the consuming app's parent directory
git clone https://github.com/yourusername/nostr-messaging.git

# In the consuming app
npm install

# The postinstall script will automatically create a symlink
# if it detects ../nostr-messaging exists

Build

npm install
npm run build

Watch Mode

npm run dev  # Rebuilds on changes

Publishing

Apps depend on this package being on the registry (e.g. ^0.3.0). After version bumps, publish so npm i in consuming apps succeeds.

# Log in if needed (token expired / new machine)
npm login

# From this repo: build and publish (scoped package needs public access)
npm run build
npm publish --access public
# If you use 2FA: npm publish --access public --otp=YOUR_CODE

# Verify
npm view @samthomson/nostr-messaging

Local symlink (Silent, Pathos, etc.): Those apps have a postinstall that, when ../nostr-messaging exists, symlinks it into node_modules/@samthomson/nostr-messaging and runs npm run build in the linked package so dist/ and types are up to date. That only runs after npm i succeeds, so the version in the app’s package.json (e.g. ^0.3.0) must exist on the registry first. Publish this package, then in the app run npm i; the symlink will replace the installed copy and use your local build.

Migration from 0.1.x to 0.2.x

Version 0.2.0 consolidated all exports into /core:

// Before (0.1.x)
import { DMProvider } from '@samthomson/nostr-messaging/react';
import { writeMessagesToDB } from '@samthomson/nostr-messaging/storage';

// After (0.2.x)
import { 
  DMProvider, 
  writeMessagesToDB 
} from '@samthomson/nostr-messaging/core';

License

MIT