Adds per-deal chat threads for both marketplace deals and opportunities (scheduling deals). Buyer and seller can message each other inside the deal context, attach files/images, and get real-time updates via the existing SSE stream.
Data model
Migration f3a4b5c6d7e8 adds deal_messages:
- Polymorphic:
deal_kind VARCHAR(20)(marketplace|scheduling) +deal_id UUID(no FK — points at one of two deal tables) - Workspace-level read state via
read_by JSONB(array of workspace IDs; sub-employees share read state) body TEXT,attachments JSONB,edited_at,is_deleted(soft-delete, body surfaces as[deleted])- Indexes:
(deal_kind, deal_id, created_at DESC),(sender_workspace_id, created_at DESC)
Backend endpoints (prefix `/deal-chat`)
GET /deal-chat/{deal_kind}/{deal_id}/messages?limit&before— paginated history (newest first)POST /deal-chat/{deal_kind}/{deal_id}/messages— send (1–4000 chars, up to 5 attachments); fires a grouped notification to the other party (group_key=chat.{deal_kind}.{deal_id}, 60-minute coalesce window)POST /deal-chat/{deal_kind}/{deal_id}/messages/mark-readGET /deal-chat/{deal_kind}/{deal_id}/unread-countGET /deal-chat/unread-summary— one row per deal with unread messages (used by deals-list unread dots)PATCH /deal-chat/messages/{id}— edit own message (15-minute window)DELETE /deal-chat/messages/{id}— soft delete (sender or SA)POST /deal-chat/{deal_kind}/{deal_id}/attachments/upload— multipart, 10 MB cap
Every endpoint verifies the caller is buyer/seller workspace (or SA).
Real-time push (SSE, extended not replaced)
/notifications/stream now emits two frame types:
{type: "notification", ...}— unchanged from before{type: "chat_message", deal_kind, deal_id, message}— new; only for threads the caller is party to
The NotificationBell component listens for chat frames and dispatches CustomEvent('adnet:chat_message') on window. DealChatPanel subscribes to that event to append messages and auto-mark-read when the panel is open.
Frontend
components/chat/DealChatPanel.tsx— reusable drawer with bubble UI, own-vs-other styling, inline image previews, file chips, 15-minute edit/delete menus, 5-attachment composer (Enter=send, Shift+Enter=newline), optimistic pending messages with spinner + rollback, auto-scroll when near bottom, infinite scroll up viabeforecursor, 12s polling fallback when SSE is unavailable- Deal detail (
/marketplace/deals/[id]): header Chat button with unread badge - Opportunities deal modal (
/opportunities→DealDetailModal): same Chat button (dealKind='scheduling') - Unread dots: orange dot on
/marketplace/dealsrows and on opportunities My Deals / Influencer Deals tables for deals with unread messages
Verified live
- ADV → SRC message delivery, SSE push, unread badges, mark-read, edit/delete, attachments
- Existing notification flows unaffected (bell, SSE, digest email)
- Existing deal flows unaffected (content share, payment, proof, dispute, revisions)
Scope deferred
- Typing indicator
- Per-user fine-grained read receipts (currently workspace-level only)
- Dedicated
CHATNotificationCategory — reusesINFLUENCERto avoid enum migration on existing preference rows
---

