user_id; // Find all distinct other parties in conversations $sql = " SELECT u.id as user_id, u.uuid as user_uuid, u.full_name as encrypted_name, u.role as user_role, tp.specialization, m.last_message, m.last_message_type, m.last_message_at, COALESCE(unread.unread_count, 0) as unread_count FROM ( SELECT CASE WHEN sender_id = ? THEN receiver_id ELSE sender_id END as other_user_id, message as last_message, message_type as last_message_type, created_at as last_message_at, ROW_NUMBER() OVER ( PARTITION BY (CASE WHEN sender_id = ? THEN receiver_id ELSE sender_id END) ORDER BY created_at DESC ) as rn FROM chat_messages WHERE sender_id = ? OR receiver_id = ? ) m JOIN users u ON u.id = m.other_user_id LEFT JOIN teacher_profiles tp ON tp.user_id = u.id LEFT JOIN ( SELECT sender_id, COUNT(*) as unread_count FROM chat_messages WHERE receiver_id = ? AND is_read = 0 GROUP BY sender_id ) unread ON unread.sender_id = u.id WHERE m.rn = 1 ORDER BY m.last_message_at DESC "; try { $rows = Database::select($sql, [$userId, $userId, $userId, $userId, $userId]); } catch (\Exception $e) { // Fallback for MySQL setups without window functions $rows = Database::select(" SELECT u.id as user_id, u.uuid as user_uuid, u.full_name as encrypted_name, u.role as user_role, tp.specialization FROM users u LEFT JOIN teacher_profiles tp ON tp.user_id = u.id WHERE u.id IN ( SELECT DISTINCT CASE WHEN sender_id = ? THEN receiver_id ELSE sender_id END FROM chat_messages WHERE sender_id = ? OR receiver_id = ? ) ", [$userId, $userId, $userId]); } $conversations = array_map(function ($row) { $rawName = (string)($row['encrypted_name'] ?? ''); $decryptedName = Security::decrypt($rawName) ?: $rawName; return [ 'user_id' => (int)$row['user_id'], 'user_uuid' => $row['user_uuid'] ?? '', 'full_name' => $decryptedName ?: ($row['user_role'] === 'teacher' ? 'أستاذ المادة' : 'طالب صَقِل'), 'role' => $row['user_role'] ?? 'student', 'specialization' => $row['specialization'] ?? null, 'last_message' => $row['last_message'] ?? '', 'last_message_type' => $row['last_message_type'] ?? 'text', 'last_message_at' => $row['last_message_at'] ?? null, 'unread_count' => (int)($row['unread_count'] ?? 0), ]; }, $rows); $response->json([ 'status' => 'success', 'data' => $conversations ]); } /** * Get chat message history between authenticated user and another user * GET /api/chat/messages?other_user_id=123&course_id=456 */ public function getMessages(Request $request, Response $response): void { $userId = $request->user_id; $queryParams = $request->getQueryParams(); $otherUserId = (int)($queryParams['other_user_id'] ?? $queryParams['user_id'] ?? 0); $courseId = !empty($queryParams['course_id']) ? (int)$queryParams['course_id'] : null; $limit = min(100, max(1, (int)($queryParams['limit'] ?? 50))); if (!$otherUserId) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الطرف الآخر (other_user_id) مطلوب' ]); return; } // Fetch other user info $otherUser = Database::selectOne("SELECT id, uuid, full_name, role FROM users WHERE id = ? LIMIT 1", [$otherUserId]); if (!$otherUser) { $response->status(404)->json(['status' => 'error', 'message' => 'المستخدم الآخر غير موجود']); return; } $rawOtherName = (string)($otherUser['full_name'] ?? ''); $otherUserName = Security::decrypt($rawOtherName) ?: $rawOtherName; // Fetch messages bidirectional $sql = " SELECT id, uuid, sender_id, receiver_id, course_id, lesson_id, message, message_type, media_url, is_read, read_at, created_at FROM chat_messages WHERE ((sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?)) "; $params = [$userId, $otherUserId, $otherUserId, $userId]; if ($courseId) { $sql .= " AND (course_id = ? OR course_id IS NULL)"; $params[] = $courseId; } $sql .= " ORDER BY created_at ASC LIMIT ?"; $params[] = $limit; $messages = Database::select($sql, $params); // Mark unread messages sent by other user to me as read Database::query( "UPDATE chat_messages SET is_read = 1, read_at = NOW() WHERE sender_id = ? AND receiver_id = ? AND is_read = 0", [$otherUserId, $userId] ); $formattedMessages = array_map(function ($msg) use ($userId) { return [ 'id' => (int)$msg['id'], 'uuid' => $msg['uuid'], 'is_mine' => ((int)$msg['sender_id'] === (int)$userId), 'sender_id' => (int)$msg['sender_id'], 'receiver_id' => (int)$msg['receiver_id'], 'course_id' => $msg['course_id'] ? (int)$msg['course_id'] : null, 'lesson_id' => $msg['lesson_id'] ? (int)$msg['lesson_id'] : null, 'message' => $msg['message'], 'message_type' => $msg['message_type'], 'media_url' => $msg['media_url'], 'is_read' => (bool)$msg['is_read'], 'read_at' => $msg['read_at'], 'created_at' => $msg['created_at'], ]; }, $messages); $response->json([ 'status' => 'success', 'data' => [ 'other_user' => [ 'id' => (int)$otherUser['id'], 'uuid' => $otherUser['uuid'], 'full_name' => $otherUserName, 'role' => $otherUser['role'], ], 'messages' => $formattedMessages ] ]); } /** * Send a new chat message (Text, Voice Note, Image, File) * POST /api/chat/messages */ public function sendMessage(Request $request, Response $response): void { $userId = $request->user_id; $body = $request->getBody(); $validator = new Validator(); $isValid = $validator->validate($body, [ 'receiver_id' => 'required', 'message' => 'required' ]); if (!$isValid) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف المستقبل ونص الرسالة مطلوبان', 'errors' => $validator->getErrors() ]); return; } $receiverId = (int)$body['receiver_id']; $messageText = trim((string)$body['message']); $messageType = (string)($body['message_type'] ?? 'text'); $mediaUrl = !empty($body['media_url']) ? trim((string)$body['media_url']) : null; $courseId = !empty($body['course_id']) ? (int)$body['course_id'] : null; $lessonId = !empty($body['lesson_id']) ? (int)$body['lesson_id'] : null; if (!in_array($messageType, ['text', 'voice', 'image', 'file'], true)) { $messageType = 'text'; } // Validate receiver exists $receiver = Database::selectOne("SELECT id, role FROM users WHERE id = ? LIMIT 1", [$receiverId]); if (!$receiver) { $response->status(404)->json([ 'status' => 'error', 'message' => 'المستخدم المستلم غير موجود' ]); return; } $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $messageId = Database::insert( "INSERT INTO chat_messages (uuid, sender_id, receiver_id, course_id, lesson_id, message, message_type, media_url, is_read) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)", [$uuid, $userId, $receiverId, $courseId, $lessonId, $messageText, $messageType, $mediaUrl] ); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم إرسال الرسالة بنجاح', 'data' => [ 'id' => $messageId, 'uuid' => $uuid, 'sender_id' => $userId, 'receiver_id' => $receiverId, 'is_mine' => true, 'course_id' => $courseId, 'lesson_id' => $lessonId, 'message' => $messageText, 'message_type' => $messageType, 'media_url' => $mediaUrl, 'is_read' => false, 'created_at' => date('Y-m-d H:i:s'), ] ]); } /** * Mark all messages in a conversation as read * POST /api/chat/read */ public function markAsRead(Request $request, Response $response): void { $userId = $request->user_id; $body = $request->getBody(); $senderId = (int)($body['sender_id'] ?? 0); if (!$senderId) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف المرسل مطلوب' ]); return; } Database::query( "UPDATE chat_messages SET is_read = 1, read_at = NOW() WHERE sender_id = ? AND receiver_id = ? AND is_read = 0", [$senderId, $userId] ); $response->json([ 'status' => 'success', 'message' => 'تم تحديث حالة القراءة' ]); } /** * Get total unread count for badge notification * GET /api/chat/unread-count */ public function getUnreadCount(Request $request, Response $response): void { $userId = $request->user_id; $row = Database::selectOne( "SELECT COUNT(*) as unread_total FROM chat_messages WHERE receiver_id = ? AND is_read = 0", [$userId] ); $response->json([ 'status' => 'success', 'data' => [ 'unread_count' => (int)($row['unread_total'] ?? 0) ] ]); } }