@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.
Maintainers
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.
ChatEmiProviderfor application layout/state.useChatEmifor 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-domFor MongoDB server helpers in your API backend:
npm install @mademi_dev/chatemi mongodbPackage 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
- Full implementation guide:
docs/IMPLEMENTATION_GUIDE.md - Backend REST/WebSocket contract:
docs/BACKEND_CONTRACT.md - Next.js launcher example:
examples/nextjs-chat-widget
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,activeMessagesnotifications,unreadNotificationCounttypingByConversation,presenceByUserconnectionStatus,loading,error,themeapi,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.createdconversation.updatedconversation.deletedconversation.member.addedconversation.member.updatedconversation.member.removedmessage.createdmessage.updatedmessage.deletedmessage.receiptmessage.reactiontypingpresencenotification
Built-in outgoing helper events include:
conversation.subscribeconversation.unsubscribetypingmessage.readmessage.deliveredmessage.forwardconversation.member.updateconversation.avatar.updatepresence
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/replyToforwardedFrom,forwardedFromConversationId, andforwardedFromMessageIddeliveredToandreadByreceipts- 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 mongodbimport { 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_conversationschatemi_messageschatemi_memberschatemi_receiptschatemi_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
clientOptionsbased 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 buildprepublishOnly 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