diff --git a/backend/app/Controllers/ChatController.php b/backend/app/Controllers/ChatController.php
index e217851..a222244 100644
--- a/backend/app/Controllers/ChatController.php
+++ b/backend/app/Controllers/ChatController.php
@@ -312,26 +312,54 @@ class ChatController
[$uuid, $userId, $receiverId, $courseId, $lessonId, $messageText, $messageType, $mediaUrl]
);
+ $msgPayload = [
+ 'id' => $messageId,
+ 'uuid' => $uuid,
+ 'sender_id' => $userId,
+ 'receiver_id' => $receiverId,
+ 'is_mine' => false,
+ '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'),
+ ];
+
+ // Push directly to Workerman WebSocket server for instant 0ms live broadcast to receiver
+ self::pushToWorkerman('push_chat', $receiverId, $msgPayload);
+
$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'),
- ]
+ 'data' => array_merge($msgPayload, ['is_mine' => true])
]);
}
+ /**
+ * Push internal event to Workerman's internal HTTP server (port 4241)
+ */
+ public static function pushToWorkerman(string $action, int $targetUserId, array $payload): bool
+ {
+ try {
+ $ch = curl_init('http://127.0.0.1:4241');
+ curl_setopt($ch, CURLOPT_POST, 1);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
+ 'action' => $action,
+ 'target_user_id' => $targetUserId,
+ 'payload' => json_encode($payload, JSON_UNESCAPED_UNICODE)
+ ]));
+ curl_setopt($ch, CURLOPT_TIMEOUT_MS, 400); // Fast non-blocking timeout
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_exec($ch);
+ curl_close($ch);
+ return true;
+ } catch (\Throwable $e) {
+ return false;
+ }
+ }
+
/**
* Mark all messages in a conversation as read
* POST /api/chat/read
diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php
index 766f215..56f030b 100644
--- a/backend/app/Views/StudentPortal.php
+++ b/backend/app/Views/StudentPortal.php
@@ -946,14 +946,109 @@ class StudentPortal
}
}
+ // ==========================================
+ // Luxury Audio Chime & Real-time Notifications
+ // ==========================================
+ function playChimeNotification() {
+ try {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const now = ctx.currentTime;
+
+ const osc1 = ctx.createOscillator();
+ const gain1 = ctx.createGain();
+ osc1.type = 'sine';
+ osc1.frequency.setValueAtTime(587.33, now);
+ gain1.gain.setValueAtTime(0.2, now);
+ gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
+ osc1.connect(gain1);
+ gain1.connect(ctx.destination);
+ osc1.start(now);
+ osc1.stop(now + 0.35);
+
+ const osc2 = ctx.createOscillator();
+ const gain2 = ctx.createGain();
+ osc2.type = 'sine';
+ osc2.frequency.setValueAtTime(880.00, now + 0.09);
+ gain2.gain.setValueAtTime(0.25, now + 0.09);
+ gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
+ osc2.connect(gain2);
+ gain2.connect(ctx.destination);
+ osc2.start(now + 0.09);
+ osc2.stop(now + 0.55);
+ } catch (e) {
+ console.warn('Audio Context note:', e);
+ }
+ }
+
+ function showLuxuryToast(title, body, icon = '👨🏫') {
+ let container = document.getElementById('saqel_toast_stack');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'saqel_toast_stack';
+ container.style.cssText = 'position: fixed; top: 24px; left: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; direction: rtl;';
+ document.body.appendChild(container);
+ }
+
+ const toast = document.createElement('div');
+ toast.style.cssText = `
+ pointer-events: auto;
+ min-width: 280px;
+ max-width: 380px;
+ background: rgba(22, 27, 34, 0.92);
+ backdrop-filter: blur(24px);
+ -webkit-backdrop-filter: blur(24px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-right: 4px solid var(--accent-color, #38BDF8);
+ border-radius: 16px;
+ padding: 14px 18px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 25px rgba(56, 189, 248, 0.2);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+ cursor: pointer;
+ `;
+
+ toast.innerHTML = `
+
${icon}
+
+
${escapeHtml(title)}
+
${escapeHtml(body)}
+
+ `;
+
+ container.appendChild(toast);
+
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ toast.style.transform = 'translateX(-30px)';
+ toast.style.transition = 'all 0.3s ease';
+ setTimeout(() => toast.remove(), 300);
+ }, 5000);
+ }
+
+ function triggerDesktopNotification(title, body) {
+ if ('Notification' in window && Notification.permission === 'granted') {
+ try {
+ new Notification(title, {
+ body: body,
+ icon: '/favicon.ico'
+ });
+ } catch (e) {}
+ }
+ }
+
function handleIncomingWsEvent(msg) {
const event = msg.event;
const data = msg.data;
if (event === 'authenticated') {
updateWsStatus(true);
- } else if (event === 'chat_message' || event === 'chat_sent') {
- appendChatMessage(data);
+ } else if (event === 'chat_message') {
+ appendChatMessage({ ...data, is_mine: false });
+ playChimeNotification();
+ showLuxuryToast('رسالة جديدة من الأستاذ حمزة الغويري 👨🏫', data.message || 'رد المعلم على استفسارك');
+ triggerDesktopNotification('صَقِل — رد الأستاذ حمزة الغويري', data.message || 'رد المعلم على استفسارك');
} else if (event === 'drm_session_terminated') {
alert('⚠️ تم فتح هذا الحساب من متصفح آخر.');
handleLogout();
diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php
index a39caf4..d130275 100644
--- a/backend/app/Views/TeacherPortal.php
+++ b/backend/app/Views/TeacherPortal.php
@@ -626,15 +626,121 @@ class TeacherPortal
}
}
+ // ==========================================
+ // Luxury Audio Chime & Real-time Notifications
+ // ==========================================
+ function playChimeNotification() {
+ try {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)();
+ const now = ctx.currentTime;
+
+ // Tone 1: High crisp bell (D5)
+ const osc1 = ctx.createOscillator();
+ const gain1 = ctx.createGain();
+ osc1.type = 'sine';
+ osc1.frequency.setValueAtTime(587.33, now);
+ gain1.gain.setValueAtTime(0.2, now);
+ gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
+ osc1.connect(gain1);
+ gain1.connect(ctx.destination);
+ osc1.start(now);
+ osc1.stop(now + 0.35);
+
+ // Tone 2: Harmonic resolution (A5)
+ const osc2 = ctx.createOscillator();
+ const gain2 = ctx.createGain();
+ osc2.type = 'sine';
+ osc2.frequency.setValueAtTime(880.00, now + 0.09);
+ gain2.gain.setValueAtTime(0.25, now + 0.09);
+ gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
+ osc2.connect(gain2);
+ gain2.connect(ctx.destination);
+ osc2.start(now + 0.09);
+ osc2.stop(now + 0.55);
+ } catch (e) {
+ console.warn('Audio Context note:', e);
+ }
+ }
+
+ function showLuxuryToast(title, body, icon = '💬') {
+ let container = document.getElementById('saqel_toast_stack');
+ if (!container) {
+ container = document.createElement('div');
+ container.id = 'saqel_toast_stack';
+ container.style.cssText = 'position: fixed; top: 24px; left: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; direction: rtl;';
+ document.body.appendChild(container);
+ }
+
+ const toast = document.createElement('div');
+ toast.style.cssText = `
+ pointer-events: auto;
+ min-width: 280px;
+ max-width: 380px;
+ background: rgba(22, 27, 34, 0.92);
+ backdrop-filter: blur(24px);
+ -webkit-backdrop-filter: blur(24px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-right: 4px solid var(--accent-gold, #F59E0B);
+ border-radius: 16px;
+ padding: 14px 18px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 25px rgba(245, 158, 11, 0.2);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
+ cursor: pointer;
+ `;
+
+ toast.innerHTML = `
+ ${icon}
+
+
${escapeHtml(title)}
+
${escapeHtml(body)}
+
+ `;
+
+ container.appendChild(toast);
+
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ toast.style.transform = 'translateX(-30px)';
+ toast.style.transition = 'all 0.3s ease';
+ setTimeout(() => toast.remove(), 300);
+ }, 5000);
+ }
+
+ function triggerDesktopNotification(title, body) {
+ if ('Notification' in window && Notification.permission === 'granted') {
+ try {
+ new Notification(title, {
+ body: body,
+ icon: '/favicon.ico'
+ });
+ } catch (e) {}
+ }
+ }
+
function handleIncomingWsEvent(msg) {
const event = msg.event;
const data = msg.data;
if (event === 'authenticated') {
updateWsStatus(true);
- } else if (event === 'chat_message' || event === 'chat_sent') {
- appendChatMessage(data);
- loadConversations(); // refresh sidebar
+ } else if (event === 'chat_message') {
+ const senderId = parseInt(data.sender_id);
+ if (senderId === activeRecipientId) {
+ appendChatMessage({ ...data, is_mine: false });
+ }
+
+ playChimeNotification();
+ const senderEl = document.querySelector(`.conv-item[data-user-id="${senderId}"] .conv-name span`);
+ const senderName = senderEl ? senderEl.textContent : 'طالب';
+ showLuxuryToast(`رسالة جديدة من ${senderName}`, data.message || 'أرسل استفساراً جديداً');
+ triggerDesktopNotification(`صَقِل — رسالة من ${senderName}`, data.message || 'استفسار جديد');
+
+ loadConversations();
+ } else if (event === 'chat_sent') {
+ loadConversations();
} else if (event === 'user_typing') {
const typingSpan = document.getElementById('active_chat_typing');
if (typingSpan && msg.sender_id === activeRecipientId) {