import { sql } from "@/lib/db"; import { requireRideParticipant, rideIsActive } from "@/lib/ride-participants"; // In-app chat for a ride. Both the rider and the assigned driver can read and // post, but only while the ride is active (accepted / en_route); a terminal // ride is read-only so the conversation is frozen once the trip ends. type MessageRow = { id: number; ride_id: number; sender_type: "rider" | "driver"; sender_id: string; body: string; created_at: string; sender_name: string; sender_avatar: string | null; }; // GET — messages for the ride. `?since=` returns only rows with id > since // (the polling cursor), oldest-first so the client can append directly. With // no cursor the full history is returned for the initial load. export async function GET(req: Request, { id }: { id: string }) { const rideId = Number(id); if (!Number.isInteger(rideId)) { return Response.json({ error: "Invalid ride id." }, { status: 400 }); } const participant = await requireRideParticipant(req, rideId); if ("error" in participant) return participant.error; const sinceParam = new URL(req.url).searchParams.get("since"); const since = Number(sinceParam); const hasCursor = Number.isInteger(since) && since > 0; try { // The optional `since` cursor can't be a nested sql fragment (sql executes // immediately), so branch into two queries that each take no extra params. const rows = hasCursor ? await sql` SELECT m.id, m.ride_id, m.sender_type, COALESCE(m.sender_user_id::text, m.sender_driver_id::text) AS sender_id, m.body, m.created_at, COALESCE(u.name, CONCAT_WS(' ', d.first_name, d.last_name)) AS sender_name, d.profile_image_url AS sender_avatar FROM messages m LEFT JOIN users u ON u.id = m.sender_user_id LEFT JOIN drivers d ON d.id = m.sender_driver_id WHERE m.ride_id = ${rideId} AND m.id > ${since} ORDER BY m.id ASC ` : await sql` SELECT m.id, m.ride_id, m.sender_type, COALESCE(m.sender_user_id::text, m.sender_driver_id::text) AS sender_id, m.body, m.created_at, COALESCE(u.name, CONCAT_WS(' ', d.first_name, d.last_name)) AS sender_name, d.profile_image_url AS sender_avatar FROM messages m LEFT JOIN users u ON u.id = m.sender_user_id LEFT JOIN drivers d ON d.id = m.sender_driver_id WHERE m.ride_id = ${rideId} ORDER BY m.id ASC `; return Response.json({ data: rows }); } catch (error) { console.error("[GET_MESSAGES]: ", error); return Response.json({ error: "Internal Server Error." }, { status: 500 }); } } // POST — send a message. Rejected (409) if the ride is no longer active, so a // completed/cancelled trip can't receive new messages. export async function POST(req: Request, { id }: { id: string }) { const rideId = Number(id); if (!Number.isInteger(rideId)) { return Response.json({ error: "Invalid ride id." }, { status: 400 }); } const participant = await requireRideParticipant(req, rideId); if ("error" in participant) return participant.error; let body: { body?: string }; try { body = await req.json(); } catch { return Response.json({ error: "Invalid JSON body." }, { status: 400 }); } const text = (body.body ?? "").trim(); if (!text) { return Response.json({ error: "Message body is empty." }, { status: 400 }); } if (text.length > 4000) { return Response.json({ error: "Message is too long." }, { status: 400 }); } try { if (!(await rideIsActive(rideId))) { return Response.json( { error: "This ride is no longer active." }, { status: 409 }, ); } const inserted = await sql` INSERT INTO messages (ride_id, sender_type, sender_user_id, sender_driver_id, body) VALUES ( ${rideId}, ${participant.role}, ${participant.role === "rider" ? participant.userId : null}, ${participant.role === "driver" ? participant.driverId : null}, ${text} ) RETURNING id, ride_id, sender_type, COALESCE(sender_user_id::text, sender_driver_id::text) AS sender_id, body, created_at `; // Join the sender's name/avatar for the returned row so the client can // render the optimistic bubble identically to polled ones. const message = inserted[0]; if (participant.role === "driver") { const driver = await sql<{ name: string; avatar: string | null }>` SELECT CONCAT_WS(' ', first_name, last_name) AS name, profile_image_url AS avatar FROM drivers WHERE id = ${participant.driverId} `; message.sender_name = driver[0]?.name ?? ""; message.sender_avatar = driver[0]?.avatar ?? null; } else { const rider = await sql<{ name: string }>` SELECT name FROM users WHERE id = ${participant.userId} `; message.sender_name = rider[0]?.name ?? ""; message.sender_avatar = null; } return Response.json({ data: message }, { status: 201 }); } catch (error) { console.error("[POST_MESSAGE]: ", error); return Response.json({ error: "Internal Server Error." }, { status: 500 }); } }