diff --git a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs index 1048f3b8..6baea039 100644 --- a/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs +++ b/api/src/Kurs.Platform.HttpApi.Host/Classroom/ClassroomHub.cs @@ -117,7 +117,6 @@ 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) { @@ -129,7 +128,7 @@ public class ClassroomHub : Hub ? new ClassroomSettingsDto() : JsonSerializer.Deserialize(classroom.SettingsJson); - initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants + bool initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants ? true : classroomSettings.DefaultMicrophoneState == "muted"; @@ -137,15 +136,14 @@ public class ClassroomHub : Hub x => x.SessionId == sessionId && x.UserId == userId ); - // 🚨 Kick edilmiş kullanıcı tekrar giriş yapamaz + // ❌ Kicklenmiş kullanıcı tekrar giremez if (participant != null && participant.IsKicked) { await Clients.Caller.SendAsync("Error", "You are not allowed to rejoin this class."); - Context.Abort(); // bağlantıyı anında kapat + Context.Abort(); return; } - if (participant == null) { participant = new ClassroomParticipant( @@ -156,20 +154,25 @@ public class ClassroomHub : Hub isTeacher, initialMuteState, classroomSettings.DefaultCameraState == "off", - false, //isHandRaised - false, //isKicked + false, // isHandRaised + false, // isKicked isActive ); await _participantRepository.InsertAsync(participant, autoSave: true); - await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive); await UpdateParticipantCountAsync(sessionId, classroom); } - else - { - await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive); - } - await CreateAttendanceAsync(sessionId, userId, userName); + await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive); + + // ✅ Attendance sadece yoksa aç + var existingOpenAttendance = await _attendanceRepository.FirstOrDefaultAsync( + x => x.SessionId == sessionId && x.StudentId == userId && x.LeaveTime == null + ); + + if (existingOpenAttendance == null) + { + await CreateAttendanceAsync(sessionId, userId, userName); + } await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); @@ -189,9 +192,11 @@ public class ClassroomHub : Hub .ToList(); await Clients.Caller.SendAsync("ExistingParticipants", others); + await Clients.Group(sessionId.ToString()) .SendAsync("ParticipantJoined", userId, userName, isTeacher, isActive); } + [HubMethodName("LeaveClass")] public async Task LeaveClassAsync(Guid sessionId) { @@ -372,19 +377,20 @@ public class ClassroomHub : Hub { try { - // 1. Attendance kapat + // 1. Attendance kapat (sadece kicklenen için) var attendances = await _attendanceRepository.GetListAsync( - x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null + x => x.SessionId == sessionId && + x.StudentId == participantId && + x.LeaveTime == null ); - if (attendances != null) + foreach (var attendance in attendances) { - foreach (var attendance in attendances) - { - await CloseAttendanceAsync(attendance); - await Clients.Group(sessionId.ToString()) - .SendAsync("AttendanceUpdated", attendance); - } + await CloseAttendanceAsync(attendance); + + // yalnızca diğer katılımcılar görebilsin + await Clients.Group(sessionId.ToString()) + .SendAsync("AttendanceUpdated", attendance); } // 2. Participant bul @@ -394,22 +400,23 @@ public class ClassroomHub : Hub if (participant == null) { - _logger.LogWarning("KickParticipant: Session {SessionId} için participant {ParticipantId} bulunamadı", - sessionId, participantId); + _logger.LogWarning( + "KickParticipant: Session {SessionId} için participant {ParticipantId} bulunamadı", + sessionId, participantId); return; } - // ConnectionId'yi kaydet (DB'ye null yazmadan önce) + // ConnectionId'yi cache et (null yazmadan önce) var connectionId = participant.ConnectionId; - // 3. DB'de kick flag setle + // 3. DB güncelle participant.IsActive = false; participant.IsKicked = true; participant.ConnectionId = null; await _participantRepository.UpdateAsync(participant, autoSave: true); - // 4. Hedef kullanıcıya bildir + // 4. Hedef kullanıcıya bildir (ForceDisconnect) if (!string.IsNullOrEmpty(connectionId)) { await Clients.Client(connectionId) @@ -418,9 +425,13 @@ public class ClassroomHub : Hub await Groups.RemoveFromGroupAsync(connectionId, sessionId.ToString()); } - // 5. Diğer katılımcılara bildir + // 5. Diğer katılımcılara duyur await Clients.Group(sessionId.ToString()) - .SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId }); + .SendAsync("ParticipantLeft", new + { + UserId = participantId, + SessionId = sessionId + }); // 6. Log _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", @@ -428,8 +439,9 @@ public class ClassroomHub : Hub } catch (Exception ex) { - _logger.LogError(ex, "❌ KickParticipant hata verdi (Session={SessionId}, Participant={ParticipantId})", - sessionId, participantId); + _logger.LogError(ex, + "❌ KickParticipant hata verdi (Session={SessionId}, Participant={ParticipantId})", + sessionId, participantId); await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu."); } diff --git a/ui/src/services/classroom/signalr.ts b/ui/src/services/classroom/signalr.ts index 94fd18ab..45d63db8 100644 --- a/ui/src/services/classroom/signalr.ts +++ b/ui/src/services/classroom/signalr.ts @@ -39,7 +39,7 @@ export class SignalRService { .withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, { accessTokenFactory: () => auth.session.token || '', }) - .withAutomaticReconnect() + //.withAutomaticReconnect() .configureLogging(signalR.LogLevel.Information) .build() diff --git a/ui/src/views/classroom/RoomDetail.tsx b/ui/src/views/classroom/RoomDetail.tsx index df9d1c84..c2f63a55 100644 --- a/ui/src/views/classroom/RoomDetail.tsx +++ b/ui/src/views/classroom/RoomDetail.tsx @@ -1,9 +1,7 @@ import React, { useState, useEffect, useRef } from 'react' import { motion } from 'framer-motion' import { - FaUsers, FaComments, - FaUserPlus, FaExpand, FaHandPaper, FaVolumeMute, @@ -19,7 +17,6 @@ import { FaCompress, FaUserFriends, FaLayerGroup, - FaWrench, FaFilePdf, FaFileWord, FaFileImage, @@ -278,40 +275,45 @@ const RoomDetail: React.FC = () => { await webRTCServiceRef.current?.addIceCandidate(fromUserId, candidate) }) + // 🔑 Yeni katılan birini gördüğünde signalRServiceRef.current.setParticipantJoinHandler( - async (userId: string, name: string, isTeacher: boolean, isActive: boolean) => { - if (userId === user.id) return + async (remoteUserId: string, name: string, isTeacher: boolean, isActive: boolean) => { + if (remoteUserId === user.id) return if (!isActive) return console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`) + // State’e ekle setParticipants((prev) => { - const updated = [...prev] - if (!updated.find((p) => p.id === userId)) { - updated.push({ - id: userId, + if (prev.find((p) => p.id === remoteUserId)) return prev + return [ + ...prev, + { + id: remoteUserId, name, sessionId: classSession.id, isTeacher, isAudioMuted: classSettings.defaultMicrophoneState === 'muted', isVideoMuted: classSettings.defaultCameraState === 'off', isActive: true, - }) - } - return updated + }, + ] }) - // 🔑 Mesh: Herkes herkese offer gönderir - if (!webRTCServiceRef.current?.getPeerConnection(userId)) { - await webRTCServiceRef.current?.createPeerConnection(userId) + // PeerConnection hazırla + if (!webRTCServiceRef.current?.getPeerConnection(remoteUserId)) { + await webRTCServiceRef.current?.createPeerConnection(remoteUserId) } - const offer = await webRTCServiceRef.current!.createOffer(userId) - await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) + // 🔑 Çakışmayı önle: sadece id’si küçük olan offer başlatır + if (user.id < remoteUserId) { + const offer = await webRTCServiceRef.current!.createOffer(remoteUserId) + await signalRServiceRef.current?.sendOffer(classSession.id, remoteUserId, offer) + } }, ) - // 🔑 ExistingParticipants handler + // 🔑 Odaya girdiğinde var olan katılımcılar signalRServiceRef.current.setExistingParticipantsHandler( async ( existing: { userId: string; userName: string; isTeacher: boolean; isActive: boolean }[], @@ -320,10 +322,12 @@ const RoomDetail: React.FC = () => { if (!participant.isActive) continue if (participant.userId === user.id) continue + // State’e ekle setParticipants((prev) => { - const updated = [...prev] - if (!updated.find((p) => p.id === participant.userId)) { - updated.push({ + if (prev.find((p) => p.id === participant.userId)) return prev + return [ + ...prev, + { id: participant.userId, name: participant.userName, sessionId: classSession.id, @@ -331,18 +335,20 @@ const RoomDetail: React.FC = () => { isAudioMuted: classSettings.defaultMicrophoneState === 'muted', isVideoMuted: classSettings.defaultCameraState === 'off', isActive: true, - }) - } - return updated + }, + ] }) + // PeerConnection hazırla if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) { await webRTCServiceRef.current?.createPeerConnection(participant.userId) } - // 🔑 Mesh: yeni gelen user da karşıya offer yollar - const offer = await webRTCServiceRef.current!.createOffer(participant.userId) - await signalRServiceRef.current?.sendOffer(classSession.id, participant.userId, offer) + // 🔑 Çakışmayı önle + if (user.id < participant.userId) { + const offer = await webRTCServiceRef.current!.createOffer(participant.userId) + await signalRServiceRef.current?.sendOffer(classSession.id, participant.userId, offer) + } } }, )