In-App Deal Chat

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.

2nd release that day

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-read
  • GET /deal-chat/{deal_kind}/{deal_id}/unread-count
  • GET /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 via before cursor, 12s polling fallback when SSE is unavailable
  • Deal detail (/marketplace/deals/[id]): header Chat button with unread badge
  • Opportunities deal modal (/opportunitiesDealDetailModal): same Chat button (dealKind='scheduling')
  • Unread dots: orange dot on /marketplace/deals rows 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 CHAT NotificationCategory — reuses INFLUENCER to avoid enum migration on existing preference rows

---

Want to see it in your own workspace?

Bring planning, discovery, delivery, and measurement into one platform.