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

@mademi_dev/chatemi

v1.0.2

Published

A React messaging kit with API clients, realtime socket support, hooks, provider state, and default Telegram-style UI.

Readme

ChatEmi

ChatEmi is a publish-ready React messaging package for building in-app messenger experiences. It includes:

  • A typed REST API client for conversations, messages, attachments, reactions, read receipts, and search.
  • A typed WebSocket client with reconnects, outbound queuing, typing, presence, receipts, and realtime conversation/message events.
  • Group, channel, direct, and bot conversation models with owner/admin/member roles.
  • Delivered/read receipts, last-seen presence, replies, forwards, avatars, voice messages, images, videos, and files.
  • A modern floating launcher with notification badge, draggable/resizable modal, and compact notification tray.
  • Optional external user-directory API integration.
  • Optional server-side MongoDB connection helpers for API backends.
  • ChatEmiProvider for application layout/state.
  • useChatEmi for product code that needs chat actions and state.
  • ChatEmiMessenger, a default responsive light/dark Telegram-style UI that can be used immediately or customized.

Repository: github.com/mademi-dev/ChatEmi

Requirements

  • React >=18
  • React DOM >=18
  • MongoDB driver >=7 (optional, server-side only)

Install

npm install @mademi_dev/chatemi react react-dom

For MongoDB server helpers in your API backend:

npm install @mademi_dev/chatemi mongodb

Package exports

| Import | Purpose | | --- | --- | | @mademi_dev/chatemi | Provider, hook, API client, socket client, launcher, messenger UI, and types | | @mademi_dev/chatemi/styles.css | Default UI, themes, launcher modal, and notification badge styles | | @mademi_dev/chatemi/server | Node.js MongoDB connection helper and index setup |

Documentation and examples

import { ChatEmiLauncher, ChatEmiMessenger, ChatEmiProvider, useChatEmi } from "@mademi_dev/chatemi";
import "@mademi_dev/chatemi/styles.css";

export function App() {
  return (
    <ChatEmiProvider
      config={{
        apiBaseUrl: "https://api.example.com/chat",
        socketUrl: "wss://api.example.com/chat/socket",
        token: () => localStorage.getItem("access_token") ?? undefined,
        theme: "violet",
        notifications: {
          enabled: true,
          browser: true,
          maxStored: 50
        },
        userDirectory: {
          baseUrl: "https://identity.example.com",
          searchPath: "/users/search",
          headers: () => ({
            Authorization: `Bearer ${localStorage.getItem("identity_token")}`
          })
        }
      }}
    >
      <ChatEmiLauncher theme="violet" />
    </ChatEmiProvider>
  );
}

Next.js app usage

Import package CSS once from app/layout.tsx:

import "@mademi_dev/chatemi/styles.css";
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: "My app"
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Create a client component for the widget:

"use client";

import { ChatEmiLauncher, ChatEmiProvider } from "@mademi_dev/chatemi";

export function ChatWidget() {
  return (
    <ChatEmiProvider
      config={{
        apiBaseUrl: process.env.NEXT_PUBLIC_CHAT_API_URL!,
        socketUrl: process.env.NEXT_PUBLIC_CHAT_SOCKET_URL,
        token: () => localStorage.getItem("access_token") ?? undefined,
        theme: "glass",
        notifications: {
          enabled: true,
          browser: true,
          showWhenOpen: false
        }
      }}
    >
      <ChatEmiLauncher
        defaultOpen={false}
        placement="bottom-right"
        theme="glass"
        title="Support"
        subtitle="Usually replies fast"
      />
    </ChatEmiProvider>
  );
}

Then render <ChatWidget /> from any client component or include it in a page layout. The provider keeps the socket connected while the launcher modal is closed, so incoming notification and message.created events continue updating the badge in the background.

Provider configuration

ChatEmiProvider accepts:

| Prop | Default | Purpose | | --- | --- | --- | | config | required | API, socket, auth, theme, and notification settings | | autoConnect | true | Connect the socket on mount when socketUrl is set | | initialConversations | [] | Seed conversation list before the first REST fetch | | initialActiveConversationId | first conversation | Pre-select a conversation | | initialNotifications | [] | Seed notification tray state |

config fields:

type ChatEmiConfig = {
  apiBaseUrl: string;
  socketUrl?: string;
  token?: string | (() => string | Promise<string | undefined> | undefined);
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
  currentUser?: ChatEmiUser;
  fetchImpl?: typeof fetch;
  websocketFactory?: (url: string) => WebSocket;
  endpoints?: ChatEmiEndpointOverrides;
  userDirectory?: ChatEmiUserDirectoryConfig;
  theme?: ChatEmiTheme;
  notifications?: ChatEmiNotificationConfig;
  reconnect?: {
    enabled?: boolean;
    maxAttempts?: number;
    initialDelayMs?: number;
    maxDelayMs?: number;
  };
};

The socket appends ?token=<token> to socketUrl when a token is configured. Override any REST path with config.endpoints.

Hook usage

useChatEmi() returns provider state, low-level clients, and actions:

import { useChatEmi } from "@mademi_dev/chatemi";

export function SendWelcomeButton({ conversationId }: { conversationId: string }) {
  const { actions, activeMessages, connectionStatus } = useChatEmi();

  return (
    <button
      onClick={() =>
        actions.sendMessage({
          conversationId,
          text: "Welcome to the chat",
          replyToId: activeMessages.at(-1)?.id
        })
      }
    >
      Send ({activeMessages.length} loaded, socket {connectionStatus})
    </button>
  );
}

State highlights:

  • currentUser, conversations, activeConversation, activeMessages
  • notifications, unreadNotificationCount
  • typingByConversation, presenceByUser
  • connectionStatus, loading, error, theme
  • api, socket

Action highlights:

  • Conversations: refreshConversations, openConversation, createConversation
  • Messages: sendMessage, editMessage, deleteMessage, forwardMessage, searchMessages
  • Receipts: markRead, markDelivered
  • Reactions and media: addReaction, removeReaction, uploadAttachment, updateAvatar
  • Members: addMembers, updateMember, removeMember
  • Presence: startTyping, stopTyping, setPresence
  • Notifications: dismissNotification, markNotificationsRead, clearNotifications, requestNotificationPermission
  • Connection: connect, disconnect

Low-level clients

You can use the typed clients outside React when needed:

import { ChatEmiApi, ChatEmiSocket } from "@mademi_dev/chatemi";

const api = new ChatEmiApi({
  apiBaseUrl: "https://api.example.com/chat",
  token: () => getAccessToken()
});

const socket = new ChatEmiSocket({
  apiBaseUrl: "https://api.example.com/chat",
  socketUrl: "wss://api.example.com/chat/socket",
  token: () => getAccessToken()
});

await socket.connect();
socket.on("message.created", (message) => {
  console.log(message);
});

ChatEmiApiError is exported for typed REST error handling.

Expected REST API contract

By default ChatEmi calls these paths under apiBaseUrl:

| Feature | Method/path | | --- | --- | | Current user | GET /me | | Users | GET /users?q=..., GET /users/:userId | | Conversations | GET /conversations, POST /conversations | | Conversation detail | GET /conversations/:conversationId, PATCH /conversations/:conversationId, DELETE /conversations/:conversationId | | Avatar | PATCH /conversations/:conversationId/avatar, PATCH /users/:userId | | Members/admins | POST /conversations/:conversationId/members, PATCH /conversations/:conversationId/members/:userId, DELETE /conversations/:conversationId/members/:userId | | Messages | GET /conversations/:conversationId/messages, POST /conversations/:conversationId/messages | | Message detail | PATCH /conversations/:conversationId/messages/:messageId, DELETE /conversations/:conversationId/messages/:messageId | | Read receipts | POST /conversations/:conversationId/read | | Delivered receipts | POST /conversations/:conversationId/delivered | | Forward | POST /conversations/:conversationId/messages/:messageId/forward | | Reactions | POST /conversations/:conversationId/messages/:messageId/reactions, DELETE /conversations/:conversationId/messages/:messageId/reactions | | Attachment upload | POST /attachments multipart form data | | Search | GET /search/messages?q=... |

If your backend uses different paths, pass config.endpoints to override any route. See docs/BACKEND_CONTRACT.md for request/response shapes.

Socket event contract

The socket sends and receives JSON envelopes:

{
  "type": "message.created",
  "payload": {}
}

Built-in incoming event names include:

  • conversation.created
  • conversation.updated
  • conversation.deleted
  • conversation.member.added
  • conversation.member.updated
  • conversation.member.removed
  • message.created
  • message.updated
  • message.deleted
  • message.receipt
  • message.reaction
  • typing
  • presence
  • notification

Built-in outgoing helper events include:

  • conversation.subscribe
  • conversation.unsubscribe
  • typing
  • message.read
  • message.delivered
  • message.forward
  • conversation.member.update
  • conversation.avatar.update
  • presence

Launcher, themes, and notifications

Use ChatEmiLauncher when you want a floating in-app messenger:

<ChatEmiLauncher
  placement="bottom-right"
  theme="midnight"
  title="Messages"
  subtitle="Team chat"
  defaultOpen={false}
  showNotificationList
  markNotificationsReadOnOpen
  initialSize={{ width: 460, height: 720 }}
  minSize={{ width: 360, height: 520 }}
  maxSize={{ width: 960, height: 860 }}
/>

Launcher props:

| Prop | Default | Purpose | | --- | --- | --- | | placement | bottom-right | bottom-right, bottom-left, top-right, or top-left | | defaultOpen | false | Whether the modal starts open | | showNotificationList | true | Show the compact tray above the chat | | markNotificationsReadOnOpen | true | Mark notifications read when opening | | badgeCount | unread notifications or conversation unread total | Override launcher badge count | | launcherIcon | built-in icon | Custom toggle button content | | initialSize / minSize / maxSize | 420x680 / 340x480 / 920x860 | Desktop modal sizing |

The launcher includes:

  • toggle button with unread notification badge
  • draggable modal header on desktop
  • native CSS resize handle on desktop
  • compact notification tray above the chat
  • mobile-friendly full-width modal behavior

Notification events should use this envelope:

{
  "type": "notification",
  "payload": {
    "id": "notif_1",
    "kind": "message",
    "title": "Ava",
    "body": "Sent a new message",
    "conversationId": "chat_1",
    "messageId": "message_1",
    "createdAt": "2026-06-19T15:43:00.000Z"
  }
}

If the backend only emits message.created, ChatEmi creates a local message notification automatically for messages sent by other users.

Browser notifications are optional and request permission from a user gesture when the launcher opens:

<ChatEmiProvider
  config={{
    apiBaseUrl: "https://api.example.com/chat",
    socketUrl: "wss://api.example.com/chat/socket",
    notifications: {
      enabled: true,
      browser: true,
      showWhenOpen: false,
      maxStored: 100
    }
  }}
>
  <ChatEmiLauncher />
</ChatEmiProvider>

Groups, channels, admins, and members

Create groups and channels by calling the typed API or hook action:

const { actions } = useChatEmi();

await actions.createConversation({
  type: "group",
  title: "Product Team",
  participantIds: ["user_1", "user_2"],
  avatarUrl: "https://cdn.example.com/product.png"
});

await actions.createConversation({
  type: "channel",
  title: "Announcements",
  participantIds: ["owner_1"],
  readOnly: true,
  publicUsername: "company_announcements"
});

Conversation members can include roles and permissions:

{
  user: { id: "user_1", name: "Ava" },
  role: "admin",
  permissions: ["manage_members", "pin_messages", "send_media"],
  joinedAt: "2026-06-19T00:00:00.000Z"
}

The default UI shows a member-management panel to owners/admins/moderators when enableAdminControls is enabled.

Messages, receipts, replies, forwards, and media

Messages support:

  • text and HTML bodies
  • replyToId/replyTo
  • forwardedFrom, forwardedFromConversationId, and forwardedFromMessageId
  • deliveredTo and readBy receipts
  • images, videos, audio, voice messages, generic files, locations, and contacts
await actions.sendMessage({
  conversationId: "chat_1",
  text: "Here is the design",
  replyToId: "message_1",
  attachments: [
    {
      id: "attachment_1",
      type: "image",
      url: "https://cdn.example.com/design.png",
      name: "design.png"
    }
  ]
});

await actions.forwardMessage({
  sourceConversationId: "chat_1",
  targetConversationId: "chat_2",
  messageId: "message_1"
});

Last seen and presence

Users can include presence and lastSeenAt. The default UI renders direct chats as online, last seen 4m ago, or last seen recently.

{
  id: "user_1",
  name: "Ava",
  presence: "offline",
  lastSeenAt: "2026-06-19T14:00:00.000Z"
}

External user API

Use config.userDirectory when users live outside your chat backend. ChatEmi will call that API for user search and user details without leaking the chat API bearer token unless you add it yourself in userDirectory.headers.

<ChatEmiProvider
  config={{
    apiBaseUrl: "https://api.example.com/chat",
    userDirectory: {
      baseUrl: "https://identity.example.com",
      searchPath: "/directory/users",
      userPath: (userId) => `/directory/users/${userId}`,
      headers: async () => ({
        Authorization: `Bearer ${await getIdentityToken()}`
      }),
      mapUser: (raw) => {
        const user = raw as { id: string; displayName: string; photo?: string };
        return {
          id: user.id,
          name: user.displayName,
          avatarUrl: user.photo
        };
      }
    }
  }}
>
  <ChatEmiMessenger />
</ChatEmiProvider>

MongoDB backend integration

MongoDB must be connected from your API server, not from browser React code. Install the optional peer dependency in your backend:

npm install @mademi_dev/chatemi mongodb
import { createChatEmiMongoConnection, ensureChatEmiIndexes } from "@mademi_dev/chatemi/server";

const chatDb = await createChatEmiMongoConnection({
  uri: process.env.MONGODB_URI!,
  databaseName: "chatemi",
  // Pass MongoClient options that match your deployment. ChatEmi intentionally
  // does not guess pool sizes because serverless and long-running servers need
  // different connection strategies.
  clientOptions: {
    appName: "chatemi-api"
  }
});

await chatDb.ensureIndexes();

export const conversations = chatDb.collections.conversations;
export const messages = chatDb.collections.messages;
export const members = chatDb.collections.members;
export const receipts = chatDb.collections.receipts;
export const attachments = chatDb.collections.attachments;

Default collection names:

  • chatemi_conversations
  • chatemi_messages
  • chatemi_members
  • chatemi_receipts
  • chatemi_attachments

Override them with collectionNames when creating the connection. The helper reuses one MongoDB client per process and URI/options pair.

Connection guidance:

  • Create one MongoDB client per server process and reuse it.
  • Do not put MongoDB credentials in React/browser code.
  • For serverless functions, initialize the connection outside the handler so warm invocations reuse it.
  • For long-running servers, pass pool/timeouts through clientOptions based on observed concurrency and MongoDB connection metrics.

Light mode and dark mode

Use theme="light", theme="dark", theme="system", theme="midnight", theme="glass", theme="emerald", or theme="violet":

<ChatEmiLauncher theme="glass" />

Customizing the UI

ChatEmiMessenger props:

| Prop | Default | Purpose | | --- | --- | --- | | showSidebar | true | Conversation list sidebar | | enableAdminControls | true | Member management panel for admins | | enableMessageActions | true | Reply, react, and forward actions | | enableMediaPreview | true | Inline image/video/audio previews | | composerPlaceholder | Write a message... | Composer input placeholder | | renderConversation | built-in row | Custom conversation list item | | renderMessage | built-in bubble | Custom message renderer |

<ChatEmiMessenger
  composerPlaceholder="Message the team"
  renderConversation={(conversation, isActive) => (
    <span style={{ fontWeight: isActive ? 800 : 500 }}>{conversation.title}</span>
  )}
  renderMessage={(message, isMine) => (
    <div className={isMine ? "mine" : "theirs"}>{message.text}</div>
  )}
/>

Development

npm install
npm run typecheck
npm run build

prepublishOnly runs npm run build automatically before publish.

Publishing

This package is published under the @mademi_dev scope. Update the version in package.json, then run:

npm publish --access public