diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index 70f0a8e8..fec4fc53 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -117,6 +117,7 @@ public class ClassroomHub : Hub [HubMethodName("JoinClass")] public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher, bool isActive) { + bool initialMuteState; var classroom = await _classSessionRepository.GetAsync(sessionId); if (classroom == null) { @@ -128,6 +129,8 @@ public class ClassroomHub : Hub ? new ClassroomSettingsDto() : JsonSerializer.Deserialize(classroom.SettingsJson); + initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants ? true : classroomSettings.DefaultMicrophoneState == "muted"; + var participant = await _participantRepository.FirstOrDefaultAsync( x => x.SessionId == sessionId && x.UserId == userId ); @@ -140,7 +143,7 @@ public class ClassroomHub : Hub userId, userName, isTeacher, - classroomSettings.DefaultMicrophoneState == "muted", + initialMuteState, classroomSettings.DefaultCameraState == "off", false, isActive @@ -351,28 +354,49 @@ public class ClassroomHub : Hub [HubMethodName("KickParticipant")] public async Task KickParticipantAsync(Guid sessionId, Guid participantId) { - var attendance = await _attendanceRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null - ); - - if (attendance != null) + try { - await CloseAttendanceAsync(attendance); - await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance); + // 1. Attendance kapat + var attendance = await _attendanceRepository.FirstOrDefaultAsync( + x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null + ); + + if (attendance != null) + { + await CloseAttendanceAsync(attendance); + await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance); + } + + // 2. Participant bul + var participant = await _participantRepository.FirstOrDefaultAsync( + x => x.SessionId == sessionId && x.UserId == participantId + ); + + if (participant != null) + { + // Önce SignalR grubundan çıkar + if (!string.IsNullOrEmpty(participant.ConnectionId)) + { + await Groups.RemoveFromGroupAsync(participant.ConnectionId, sessionId.ToString()); + + // Kullanıcıya "zorunlu çıkış" sinyali gönder + await Clients.Client(participant.ConnectionId) + .SendAsync("ForceDisconnect", "You have been removed from the class."); + } + + // DB’de pasife al + await DeactivateParticipantAsync(participant); + } + + // 3. Diğerlerine duyur + _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId); + await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId); } - - var participant = await _participantRepository.FirstOrDefaultAsync( - x => x.SessionId == sessionId && x.UserId == participantId - ); - - if (participant != null) + catch (Exception ex) { - await DeactivateParticipantAsync(participant); + _logger.LogError(ex, "❌ KickParticipant hata verdi"); + await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu."); } - - _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", participantId, sessionId); - - await Clients.Group(sessionId.ToString()).SendAsync("ParticipantLeft", participantId); } [HubMethodName("ApproveHandRaise")] diff --git a/ui/src/components/classroom/ParticipantGrid.tsx b/ui/src/components/classroom/ParticipantGrid.tsx index 1f52115c..55ab5393 100644 --- a/ui/src/components/classroom/ParticipantGrid.tsx +++ b/ui/src/components/classroom/ParticipantGrid.tsx @@ -264,21 +264,6 @@ export const ParticipantGrid: React.FC = ({ )} - {/* Expand button for non-main participants */} - {!isMain && onParticipantFocus && ( -
- -
- )} ) diff --git a/ui/src/components/classroom/panels/ChatPanel.tsx b/ui/src/components/classroom/panels/ChatPanel.tsx index dd74e161..fae3a009 100644 --- a/ui/src/components/classroom/panels/ChatPanel.tsx +++ b/ui/src/components/classroom/panels/ChatPanel.tsx @@ -1,6 +1,11 @@ import React, { useRef, useEffect } from 'react' import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa' -import { ClassroomChatDto, ClassroomParticipantDto, MessageType } from '@/proxy/classroom/models' +import { + ClassroomChatDto, + ClassroomParticipantDto, + ClassroomSettingsDto, + MessageType, +} from '@/proxy/classroom/models' interface ChatPanelProps { user: { id: string; name: string; role: string } @@ -15,6 +20,7 @@ interface ChatPanelProps { onSendMessage: (e: React.FormEvent) => void onClose: () => void formatTime: (timestamp: string) => string + classSettings: ClassroomSettingsDto } const ChatPanel: React.FC = ({ @@ -30,6 +36,7 @@ const ChatPanel: React.FC = ({ onSendMessage, onClose, formatTime, + classSettings, }) => { const messagesEndRef = useRef(null) @@ -67,18 +74,19 @@ const ChatPanel: React.FC = ({ Herkese - - + {classSettings.allowPrivateMessages && ( + + )} {user.role === 'teacher' && ( - - -
-
- {/* Katılımcı Davranışları */} -
-

Katılımcı İzinleri

-
- - - - -
-
- - {/* Varsayılan Ayarlar */} -
-

Varsayılan Ayarlar

-
-
- - -
-
- - -
-
- - -
-
-
- - {/* Otomatik Ayarlar */} -
-

Otomatik Ayarlar

-
- -
-
- - {user.role !== 'teacher' && ( -
-

⚠️ Ayarları sadece öğretmen değiştirebilir.

-
- )} -
-
- - ) -} - -export default SettingsPanel diff --git a/ui/src/services/classroom/signalr.ts b/ui/src/services/classroom/signalr.ts index 8def144d..0316870a 100644 --- a/ui/src/services/classroom/signalr.ts +++ b/ui/src/services/classroom/signalr.ts @@ -1,4 +1,9 @@ -import { ClassroomAttendanceDto, ClassroomChatDto, HandRaiseDto, MessageType } from '@/proxy/classroom/models' +import { + ClassroomAttendanceDto, + ClassroomChatDto, + HandRaiseDto, + MessageType, +} from '@/proxy/classroom/models' import { store } from '@/store/store' import * as signalR from '@microsoft/signalr' @@ -62,6 +67,7 @@ export class SignalRService { this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => { this.onParticipantMuted?.(userId, isMuted) }) + this.connection.on('HandRaiseReceived', (payload: any) => { // payload = { handRaiseId, studentId, studentName, ... } @@ -101,6 +107,13 @@ export class SignalRService { this.connection.on('Error', (message: string) => { console.error('Hub error:', message) }) + + this.connection.on('ForceDisconnect', async (message: string) => { + console.warn('⚠️ ForceDisconnect received:', message) + + await this.disconnect() + window.location.href = '/classrooms' + }) } async start(): Promise { @@ -211,7 +224,7 @@ export class SignalRService { message: string, recipientId: string, recipientName: string, - isTeacher: boolean + isTeacher: boolean, ): Promise { if (!this.isConnected) { console.log( diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index 2e67f134..545b1ec0 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -56,7 +56,6 @@ import ChatPanel from '@/components/classroom/panels/ChatPanel' import ParticipantsPanel from '@/components/classroom/panels/ParticipantsPanel' import DocumentsPanel from '@/components/classroom/panels/DocumentsPanel' import LayoutPanel from '@/components/classroom/panels/LayoutPanel' -import SettingsPanel from '@/components/classroom/panels/SettingsPanel' import { ScreenSharePanel } from '@/components/classroom/panels/ScreenSharePanel' import { ParticipantGrid } from '@/components/classroom/ParticipantGrid' @@ -110,12 +109,8 @@ const RoomDetail: React.FC = () => { const [isVideoEnabled, setIsVideoEnabled] = useState(true) const [attendanceRecords, setAttendanceRecords] = useState([]) const [chatMessages, setChatMessages] = useState([]) - const [currentLayout, setCurrentLayout] = useState({ - id: 'grid', - name: 'Izgara Görünümü', - type: 'grid', - description: 'Tüm katılımcılar eşit boyutta görünür', - }) + const [currentLayout, setCurrentLayout] = useState(null) + const [focusedParticipant, setFocusedParticipant] = useState() const [hasRaisedHand, setHasRaisedHand] = useState(false) const [isAllMuted, setIsAllMuted] = useState(false) @@ -402,10 +397,16 @@ const RoomDetail: React.FC = () => { setChatMessages((prev) => [...prev, message]) }) - signalRServiceRef.current.setParticipantMutedHandler((userId, isMuted) => { + signalRServiceRef.current.setParticipantMutedHandler(async (userId, isMuted) => { setParticipants((prev) => prev.map((p) => (p.id === userId ? { ...p, isAudioMuted: isMuted } : p)), ) + + // Eğer mute edilen kişi currentUser ise → kendi mikrofonunu kapat + if (userId === user.id) { + await webRTCServiceRef.current?.toggleAudio(!isMuted) + setIsAudioEnabled(!isMuted) + } }) // Hand raise events @@ -737,6 +738,7 @@ const RoomDetail: React.FC = () => { onSendMessage={handleSendMessage} onClose={() => setActiveSidePanel(null)} formatTime={formatTime} + classSettings={classSettings} /> ) @@ -775,22 +777,12 @@ const RoomDetail: React.FC = () => { return ( setActiveSidePanel(null)} /> ) - case 'settings': - return ( - setActiveSidePanel(null)} - /> - ) - default: return null } @@ -857,7 +849,7 @@ const RoomDetail: React.FC = () => { onToggleVideo={user.role === 'observer' ? () => {} : handleToggleVideo} onLeaveCall={handleLeaveCall} onMuteParticipant={handleMuteParticipant} - layout={currentLayout} + layout={currentLayout ?? layouts[0]} focusedParticipant={focusedParticipant} onParticipantFocus={handleParticipantFocus} hasSidePanel={!!activeSidePanel} @@ -1021,13 +1013,9 @@ const RoomDetail: React.FC = () => { > Katılımcılar - {/* Katılımcı adedi badge */} - - {participants.length + 1} - {/* El kaldıran badge */} {raisedHandsCount > 0 && ( - + {raisedHandsCount > 9 ? '9+' : raisedHandsCount} )} @@ -1050,15 +1038,6 @@ const RoomDetail: React.FC = () => { > Görünüm - {user.role === 'teacher' && ( )} - {/* Parmak Kaldır (Öğrenci) */} - {user.role === 'student' && classSettings.allowHandRaise && ( - - )} - {/* Participants */} - - {/* Settings Button */} -