@imola-solutions/chat
v0.4.0
Published
Back-office chat module: threads, messages, real-time subscriptions. Consumers inject apiClient + currentUser.
Readme
@imola-solutions/chat
Reusable Chat module for React apps. Extracted from BOPC-ITMP. Provides threads, messages, and real-time subscriptions over a Supabase-compatible API client.
Install
npm install @imola-solutions/chat @imola-solutions/uiPeer dependencies (must be installed in the host app):
react/react-dom^19@mui/material^6 and@emotion/react/@emotion/styled^11@phosphor-icons/react^2dayjs^1sonner^1
Setup
1. Build a chat service
createChatService is a factory that binds the module to the host app's API client, current user, and contacts directory. The API client must expose a Supabase-compatible fluent surface: .from(table).select/.insert/.update/.eq/.in/.order/.maybeSingle() and .channel(...).on(...).subscribe() / .removeChannel(...).
import { createChatService } from "@imola-solutions/chat";
import { createClient } from "@/lib/services/client"; // host app's API client
const chatService = createChatService({
apiClient: createClient(),
currentUser: {
id: "USR-000",
name: "Sofia Rivers",
avatar: "/assets/avatar.png",
},
contacts: [
{ id: "USR-001", name: "Miron Vitold", avatar: "/assets/avatar-1.png" },
// ...
],
});2. Wrap your chat page in the provider
import { ChatProvider } from "@imola-solutions/chat";
<ChatProvider chatService={chatService}>
{/* chat UI */}
</ChatProvider>3. Render ChatLayout
ChatLayout renders the sidebar + a content pane. You control navigation.
import { ChatLayout, ThreadView, ComposeView } from "@imola-solutions/chat";
import { useNavigate, useParams, useLocation } from "react-router-dom";
function ChatPage() {
const navigate = useNavigate();
const { threadId } = useParams();
const goToThread = (type, id) => navigate(`/dashboard/chat/${type}/${id}`);
const goToCompose = () => navigate("/dashboard/chat/compose");
return (
<ChatLayout
chatService={chatService}
currentThreadId={threadId}
onNavigateToThread={goToThread}
onNavigateToCompose={goToCompose}
composeHref="/dashboard/chat/compose"
>
{threadId ? (
<ThreadView threadId={threadId} />
) : (
<ComposeView onNavigateToThread={goToThread} />
)}
</ChatLayout>
);
}API
createChatService({ apiClient, currentUser, contacts?, onAudit?, aiExtensions? })
Returns { fetchThreads, fetchMessages, fetchAllMessages, createThread, sendMessage, subscribeToMessages, subscribeToThreads, resolveUser, summarizeThread, translateMessage, suggestReplies, hasAi, contacts, currentUser }.
apiClient(required) — Supabase-compatible client instance.currentUser(required) —{ id, name, avatar }for the signed-in user.contacts(optional) — array of{ id, name, avatar }used to resolve display info for other users.onAudit(optional) — see Compliance & audit.aiExtensions(optional) — see AI extensions.
Compliance & audit
createChatService accepts an onAudit callback. Every mutation and every AI call emits a structured event you can pipe to your audit log (Splunk, Elastic, DB table, etc.):
const chatService = createChatService({
apiClient,
currentUser,
onAudit: (event) => {
// event: { action, actorId, targetType?, targetId?, timestamp, metadata?, success }
myAuditLogger.record({
...event,
product: "bopc",
surface: "chat",
});
},
});Actions emitted:
thread.create— new thread + participants insertedmessage.send— message persisted (or blocked by AI moderation)ai.summarize,ai.translate,ai.suggest-replies— AI action invoked
Audit callback exceptions are caught silently so a logger outage never breaks the chat flow.
AI extensions
Pass any subset via aiExtensions. Each is optional — the corresponding capability flag on service.hasAi tells your UI whether to render the action.
const chatService = createChatService({
apiClient,
currentUser,
aiExtensions: {
// Pre-send gate. Return { allowed: false, reason } to block a message.
moderateMessage: async (content) => {
const result = await callModerationAPI(content);
return { allowed: result.score < 0.8, reason: result.category };
},
summarizeThread: async (messages) => callLLM(`Summarize: ${JSON.stringify(messages)}`),
translateMessage: async (content, targetLang) => callTranslator(content, targetLang),
suggestReplies: async ({ messages, currentUser }) => callLLM(...),
},
});
// In your UI:
if (chatService.hasAi.summarize) {
return <Button onClick={() => chatService.summarizeThread(threadId)}>Summarize</Button>;
}Moderation flow. When moderateMessage returns { allowed: false }, sendMessage returns { data: null, error } and emits an audit event with metadata.reason === "ai-moderation-blocked". If the moderator itself throws, the send fails open (message is persisted) — set metadata.reason in your logger to catch these.
<ChatProvider chatService={service}>
Loads threads + messages on mount, wires real-time subscriptions, and exposes context state (threads, messages, contacts, currentUser, createThread, createMessage, sidebar toggles).
<ChatLayout>
Composes ChatProvider + ChatView. Props:
chatService— as abovecurrentThreadId— id of the currently open thread (orundefined)onNavigateToThread(type, id)— invoked when a thread/contact is selectedonNavigateToCompose()and/orcomposeHref— for the "Group" compose button
Building blocks
ChatView, ChatSidebar, ThreadView, ComposeView, MessageBox, MessageAdd, ThreadItem, ThreadToolbar, DirectSearch, GroupRecipients, and ChatContext are also exported for custom compositions.
Data model
The service expects three Postgres tables:
chat_threads(id, thread_type, title, created_by, created_at, updated_at)chat_thread_participants(thread_id, user_id)chat_messages(id, thread_id, sender_id, sender_type, message_type, content, created_at)
