diff --git a/src/app/(admin)/desktop-notifications.tsx b/src/app/(admin)/desktop-notifications.tsx index 7c7f2be..7641316 100644 --- a/src/app/(admin)/desktop-notifications.tsx +++ b/src/app/(admin)/desktop-notifications.tsx @@ -25,7 +25,7 @@ export function DesktopNotifications({ currentUser }: { currentUser: { id: strin window.focus(); router.push(`/tickets/${event.ticket.id}`); }; - } else if (event.type === "message.created" && event.message.authorType === "customer") { + } else if (event.type === "message.created" && event.message.authorType === "customer" && !event.ticketIsNew) { if (!isTicketVisibleTo({ assigneeId: event.assigneeId }, currentUser)) return; const notification = new Notification(`Сообщение от ${event.message.authorName}`, { body: event.message.body.slice(0, 120), diff --git a/src/app/(admin)/ticket-toasts.tsx b/src/app/(admin)/ticket-toasts.tsx index 049c48e..47a8a54 100644 --- a/src/app/(admin)/ticket-toasts.tsx +++ b/src/app/(admin)/ticket-toasts.tsx @@ -92,7 +92,7 @@ export function TicketToasts({ currentUser }: { currentUser: { id: string; role: if (event.type === "ticket.created") { if (!isTicketVisibleTo(event.ticket, currentUser)) return; pushToast("Новая заявка", event.ticket.subject, `/tickets/${event.ticket.id}`); - } else if (event.type === "message.created" && event.message.authorType === "customer") { + } else if (event.type === "message.created" && event.message.authorType === "customer" && !event.ticketIsNew) { if (!isTicketVisibleTo({ assigneeId: event.assigneeId }, currentUser)) return; pushToast(`Сообщение от ${event.message.authorName}`, event.message.body.slice(0, 120), `/tickets/${event.ticketId}`); } diff --git a/src/lib/events/bus.ts b/src/lib/events/bus.ts index 0cdf108..bcbe8c8 100644 --- a/src/lib/events/bus.ts +++ b/src/lib/events/bus.ts @@ -4,7 +4,19 @@ import type { TicketDTO, MessageDTO } from "@/lib/tickets/types"; export type TicketEvent = | { type: "ticket.created"; ticket: TicketDTO } | { type: "ticket.updated"; ticket: TicketDTO } - | { type: "message.created"; ticketId: string; customerId: string; assigneeId: string | null; message: MessageDTO }; + | { + type: "message.created"; + ticketId: string; + customerId: string; + assigneeId: string | null; + message: MessageDTO; + // True when this message is the ticket's very first — subscribers that + // already show a "new ticket" toast/notification off the sibling + // ticket.created event (published for the same message, see + // appendMessage in lib/tickets/service.ts) should skip this one too, + // otherwise a single incoming ticket doubles up as two notifications. + ticketIsNew: boolean; + }; // Stashed on globalThis rather than relying on Node's module cache to give // us a singleton: Next.js dev mode reloads this module on every edit (hot diff --git a/src/lib/tickets/service.ts b/src/lib/tickets/service.ts index 3c302e3..ef23c0f 100644 --- a/src/lib/tickets/service.ts +++ b/src/lib/tickets/service.ts @@ -164,6 +164,7 @@ async function appendMessage(params: { customerId: ticket.customerId, assigneeId: dto.assigneeId, message: messageDto, + ticketIsNew: params.ticketIsNew, }); // Fire-and-forget: push delivery is a best-effort side channel for