feat: granular WebSocket message.new events

- WebSocket now detects new ContactMessages and broadcasts
  message.new events with contactId, text, channel, direction
- Frontend handles message.new: refreshes timeline for open chat,
  refreshes contacts for sidebar preview update
- dashboard.changed still fires for non-message changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-24 20:04:55 +07:00
parent ac9c50b47d
commit 643d8d02ba
3 changed files with 69 additions and 1 deletions

View File

@@ -1,5 +1,13 @@
import { prisma } from "../../utils/prisma";
function mapChannel(channel: string) {
if (channel === "TELEGRAM") return "Telegram";
if (channel === "WHATSAPP") return "WhatsApp";
if (channel === "INSTAGRAM") return "Instagram";
if (channel === "EMAIL") return "Email";
return "Phone";
}
const COOKIE_USER = "cf_user";
const COOKIE_TEAM = "cf_team";
const COOKIE_CONV = "cf_conv";
@@ -9,6 +17,7 @@ const TEAM_POLL_INTERVAL_MS = 2000;
const peersByTeam = new Map<string, Set<any>>();
const peerTeamById = new Map<string, string>();
const lastSignatureByTeam = new Map<string, string>();
const lastMsgCreatedAtByTeam = new Map<string, Date>();
let pollTimer: ReturnType<typeof setInterval> | null = null;
@@ -45,6 +54,7 @@ function detachPeer(peer: any) {
if (peers.size === 0) {
peersByTeam.delete(teamId);
lastSignatureByTeam.delete(teamId);
lastMsgCreatedAtByTeam.delete(teamId);
}
}
@@ -135,9 +145,46 @@ function sendJson(peer: any, payload: Record<string, unknown>) {
}
}
async function checkNewMessages(teamId: string) {
const lastTs = lastMsgCreatedAtByTeam.get(teamId) ?? new Date(0);
try {
const msgs = await prisma.contactMessage.findMany({
where: { contact: { teamId }, createdAt: { gt: lastTs } },
include: { contact: { select: { id: true, name: true } } },
orderBy: { createdAt: "asc" },
take: 50,
});
if (msgs.length) {
lastMsgCreatedAtByTeam.set(teamId, msgs[msgs.length - 1]!.createdAt);
}
return msgs;
} catch {
return [];
}
}
async function pollAndBroadcast() {
for (const [teamId, peers] of peersByTeam.entries()) {
if (!peers.size) continue;
// Check for new messages → send granular events
const newMessages = await checkNewMessages(teamId);
for (const msg of newMessages) {
const payload = {
type: "message.new",
contactId: msg.contact.id,
contactName: msg.contact.name,
text: msg.content ?? "",
channel: mapChannel(msg.channel),
direction: msg.direction === "OUTBOUND" ? "out" : "in",
at: msg.occurredAt?.toISOString() ?? msg.createdAt.toISOString(),
};
for (const peer of peers) {
sendJson(peer, payload);
}
}
// Check for other dashboard changes (contacts, calendar, deals, etc.)
const signature = await computeTeamSignature(teamId);
const previous = lastSignatureByTeam.get(teamId);
if (signature === previous) continue;