npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@samthomson/nostr-messaging

v0.17.1

Published

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

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

  return (
    <DMProvider
      children={children}
      nostr={nostr}
      user={user}
      messagingConfig={{ discoveryRelays: [...], relayMode: 'hybrid' }}
      onNotify={(opts) => toast(opts)}
      getDisplayName={getDisplayName}
      fetchAuthorsBatch={useAuthorsBatch}
      publishEvent={publishEvent}
      uploadFile={uploadFile}
      follows={follows}
      sounds={DEFAULT_NEW_MESSAGE_SOUNDS}
      ui={{ showShorts: true, isMobile }}
    />
  );
}

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. Uses one DB (nostr-dm-cache-v2) with two stores: dm-cache (MessagingState per pubkey) and media-blobs (decrypted 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. Tailwind (when using the UI)
    The UI uses Tailwind classes for layout (e.g. two-panel list + chat). Tailwind only scans your app’s source by default, so it won’t see classes in node_modules and the layout can collapse to one column. Add the package to content in your Tailwind config:

    // tailwind.config.js or tailwind.config.ts
    content: [
      // ... your paths
      "./node_modules/@samthomson/nostr-messaging/dist/**/*.js",
    ],
  3. Vitest configuration (if running tests)
    By default, Vitest doesn't process node_modules through your bundler, so the package's @/ imports won't resolve in tests. Add this to your vite.config.ts:

    test: {
      // ... other test config
      server: {
        deps: {
          inline: ['@samthomson/nostr-messaging'],
        },
      },
    },
  4. 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.

  1. Default message sounds (when using DEFAULT_NEW_MESSAGE_SOUNDS)
    The package ships default sound files in assets/sounds/. To use them, copy them into your app so they are served at /sounds/. Example: add a scripts/postinstall.sh that runs after npm install, and call it from package.json:

    "postinstall": "sh scripts/postinstall.sh"

    In the script, copy the package sounds when present:

    if [ -d "node_modules/@samthomson/nostr-messaging/assets/sounds" ]; then
      mkdir -p public/sounds
      cp node_modules/@samthomson/nostr-messaging/assets/sounds/*.mp3 public/sounds/
    fi

    If you use your own sounds instead, pass config.sounds with your own { id, label, url }[] and skip this.

DMProviderProps Interface

interface DMProviderProps {
  children: ReactNode;
  
  // App dependencies
  nostr: NPool;
  user: { pubkey: string; signer: Signer } | null;
  onNotify: (options: NotifyOptions) => void;
  getDisplayName: (pubkey: string, metadata?: NostrMetadata) => string;
  fetchAuthorsBatch: (pubkeys: string[]) => { data?: Map<...> };
  publishEvent: (event: EventTemplate) => Promise<void>;
  uploadFile: (file: File) => Promise<string>;
  follows: string[];
  onReconnected?: () => void;

  // Messaging config (all settings together)
  messagingConfig: MessagingConfig;

  // UI hints
  ui?: {
    showShorts?: boolean;
    showSearch?: boolean;
    isMobile?: boolean;
  };
}

// MessagingConfig type (defined in types.ts)
interface MessagingConfig {
  discoveryRelays: string[];
  relayMode: RelayMode;
  protocolMode?: ProtocolMode;
  renderInlineMedia?: boolean;
  devMode?: boolean;
  appName?: string;
  appDescription?: string;
  soundPref?: {
    options: NewMessageSoundOption[];
    value: NewMessageSoundPref;
    onChange: (pref: NewMessageSoundPref) => void;
  };
}

Network status is tracked inside the package; use isOnline from useDMContext(). Pass onReconnected if you want to run app logic when the package detects reconnection.

DM provider settings (for app settings UI)

Apps should expose these in their own settings and pass the result into messagingConfig. The package does not ship a settings component.

| Setting | Type | Description | |--------|------|-------------| | discoveryRelays | string[] | Relay URLs used to discover DM inboxes and query messages. Required. | | relayMode | 'discovery' | 'hybrid' | 'strict_outbox' | How to choose relays: discovery only; user's relays + discovery; or only user's relays (NIP-65/10050). Required. | | renderInlineMedia | boolean | Show images/media inline in messages. Default true. | | soundPref | { options: NewMessageSoundOption[]; value: { enabled: boolean; soundId: string }; onChange: (pref) => void } | New-message sound settings: available options, current selection, and change handler. App owns persistence. | | devMode | boolean | Show decryption/dev UI (e.g. seal payload). Default false. | | appName | string | App name shown in conversation list header. | | appDescription | string | App description shown in conversation list header. | | protocolMode | ProtocolMode | Optional. NIP-04 only, NIP-17 only, or both (default). |

Relay list and relay mode affect cache validity; changing them triggers a cold start. Persist discoveryRelays and relayMode (e.g. in app config/localStorage) and pass them into messagingConfig on load.

Default vs custom provider

  • DMProviderDefault: Builds a single props object from its props (user, onNotify, uploadFile, messagingConfig, etc.) and renders <DMProvider {...props} />. If you don't pass sounds/ui/protocolMode, defaults apply (e.g. ui.isMobile: true). Pass ui={{ isMobile: false }} (or your app's value) to override.
  • Custom provider: You pass one props object to DMProvider: messagingConfig, uploadFile, sounds, ui, soundPref, and the rest. No separate "deps" vs "config".

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://gitlab.com/soapbox-pub/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.

License

MIT