classroom düzeltmeler

This commit is contained in:
Sedat Öztürk 2025-08-31 21:35:43 +03:00
parent 8acbd2e257
commit dfd05cbf57
3 changed files with 77 additions and 59 deletions

View file

@ -117,7 +117,6 @@ public class ClassroomHub : Hub
[HubMethodName("JoinClass")] [HubMethodName("JoinClass")]
public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher, bool isActive) public async Task JoinClassAsync(Guid sessionId, Guid userId, string userName, bool isTeacher, bool isActive)
{ {
bool initialMuteState;
var classroom = await _classSessionRepository.GetAsync(sessionId); var classroom = await _classSessionRepository.GetAsync(sessionId);
if (classroom == null) if (classroom == null)
{ {
@ -129,7 +128,7 @@ public class ClassroomHub : Hub
? new ClassroomSettingsDto() ? new ClassroomSettingsDto()
: JsonSerializer.Deserialize<ClassroomSettingsDto>(classroom.SettingsJson); : JsonSerializer.Deserialize<ClassroomSettingsDto>(classroom.SettingsJson);
initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants bool initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants
? true ? true
: classroomSettings.DefaultMicrophoneState == "muted"; : classroomSettings.DefaultMicrophoneState == "muted";
@ -137,15 +136,14 @@ public class ClassroomHub : Hub
x => x.SessionId == sessionId && x.UserId == userId x => x.SessionId == sessionId && x.UserId == userId
); );
// 🚨 Kick edilmiş kullanıcı tekrar giriş yapamaz // ❌ Kicklenmiş kullanıcı tekrar giremez
if (participant != null && participant.IsKicked) if (participant != null && participant.IsKicked)
{ {
await Clients.Caller.SendAsync("Error", "You are not allowed to rejoin this class."); await Clients.Caller.SendAsync("Error", "You are not allowed to rejoin this class.");
Context.Abort(); // bağlantıyı anında kapat Context.Abort();
return; return;
} }
if (participant == null) if (participant == null)
{ {
participant = new ClassroomParticipant( participant = new ClassroomParticipant(
@ -156,20 +154,25 @@ public class ClassroomHub : Hub
isTeacher, isTeacher,
initialMuteState, initialMuteState,
classroomSettings.DefaultCameraState == "off", classroomSettings.DefaultCameraState == "off",
false, //isHandRaised false, // isHandRaised
false, //isKicked false, // isKicked
isActive isActive
); );
await _participantRepository.InsertAsync(participant, autoSave: true); await _participantRepository.InsertAsync(participant, autoSave: true);
await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive);
await UpdateParticipantCountAsync(sessionId, classroom); await UpdateParticipantCountAsync(sessionId, classroom);
} }
else
{
await UpdateParticipantConnectionAsync(participant, Context.ConnectionId, isActive);
}
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 CreateAttendanceAsync(sessionId, userId, userName);
}
await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString()); await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString());
@ -189,9 +192,11 @@ public class ClassroomHub : Hub
.ToList(); .ToList();
await Clients.Caller.SendAsync("ExistingParticipants", others); await Clients.Caller.SendAsync("ExistingParticipants", others);
await Clients.Group(sessionId.ToString()) await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantJoined", userId, userName, isTeacher, isActive); .SendAsync("ParticipantJoined", userId, userName, isTeacher, isActive);
} }
[HubMethodName("LeaveClass")] [HubMethodName("LeaveClass")]
public async Task LeaveClassAsync(Guid sessionId) public async Task LeaveClassAsync(Guid sessionId)
{ {
@ -372,20 +377,21 @@ public class ClassroomHub : Hub
{ {
try try
{ {
// 1. Attendance kapat // 1. Attendance kapat (sadece kicklenen için)
var attendances = await _attendanceRepository.GetListAsync( 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 CloseAttendanceAsync(attendance);
// yalnızca diğer katılımcılar görebilsin
await Clients.Group(sessionId.ToString()) await Clients.Group(sessionId.ToString())
.SendAsync("AttendanceUpdated", attendance); .SendAsync("AttendanceUpdated", attendance);
} }
}
// 2. Participant bul // 2. Participant bul
var participant = await _participantRepository.FirstOrDefaultAsync( var participant = await _participantRepository.FirstOrDefaultAsync(
@ -394,22 +400,23 @@ public class ClassroomHub : Hub
if (participant == null) if (participant == null)
{ {
_logger.LogWarning("KickParticipant: Session {SessionId} için participant {ParticipantId} bulunamadı", _logger.LogWarning(
"KickParticipant: Session {SessionId} için participant {ParticipantId} bulunamadı",
sessionId, participantId); sessionId, participantId);
return; return;
} }
// ConnectionId'yi kaydet (DB'ye null yazmadan önce) // ConnectionId'yi cache et (null yazmadan önce)
var connectionId = participant.ConnectionId; var connectionId = participant.ConnectionId;
// 3. DB'de kick flag setle // 3. DB güncelle
participant.IsActive = false; participant.IsActive = false;
participant.IsKicked = true; participant.IsKicked = true;
participant.ConnectionId = null; participant.ConnectionId = null;
await _participantRepository.UpdateAsync(participant, autoSave: true); await _participantRepository.UpdateAsync(participant, autoSave: true);
// 4. Hedef kullanıcıya bildir // 4. Hedef kullanıcıya bildir (ForceDisconnect)
if (!string.IsNullOrEmpty(connectionId)) if (!string.IsNullOrEmpty(connectionId))
{ {
await Clients.Client(connectionId) await Clients.Client(connectionId)
@ -418,9 +425,13 @@ public class ClassroomHub : Hub
await Groups.RemoveFromGroupAsync(connectionId, sessionId.ToString()); 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()) await Clients.Group(sessionId.ToString())
.SendAsync("ParticipantLeft", new { UserId = participantId, SessionId = sessionId }); .SendAsync("ParticipantLeft", new
{
UserId = participantId,
SessionId = sessionId
});
// 6. Log // 6. Log
_logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}", _logger.LogInformation("👢 Participant {ParticipantId} kicked from session {SessionId}",
@ -428,7 +439,8 @@ public class ClassroomHub : Hub
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "❌ KickParticipant hata verdi (Session={SessionId}, Participant={ParticipantId})", _logger.LogError(ex,
"❌ KickParticipant hata verdi (Session={SessionId}, Participant={ParticipantId})",
sessionId, participantId); sessionId, participantId);
await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu."); await Clients.Caller.SendAsync("Error", "Kick işlemi başarısız oldu.");

View file

@ -39,7 +39,7 @@ export class SignalRService {
.withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, { .withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, {
accessTokenFactory: () => auth.session.token || '', accessTokenFactory: () => auth.session.token || '',
}) })
.withAutomaticReconnect() //.withAutomaticReconnect()
.configureLogging(signalR.LogLevel.Information) .configureLogging(signalR.LogLevel.Information)
.build() .build()

View file

@ -1,9 +1,7 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import { motion } from 'framer-motion' import { motion } from 'framer-motion'
import { import {
FaUsers,
FaComments, FaComments,
FaUserPlus,
FaExpand, FaExpand,
FaHandPaper, FaHandPaper,
FaVolumeMute, FaVolumeMute,
@ -19,7 +17,6 @@ import {
FaCompress, FaCompress,
FaUserFriends, FaUserFriends,
FaLayerGroup, FaLayerGroup,
FaWrench,
FaFilePdf, FaFilePdf,
FaFileWord, FaFileWord,
FaFileImage, FaFileImage,
@ -278,40 +275,45 @@ const RoomDetail: React.FC = () => {
await webRTCServiceRef.current?.addIceCandidate(fromUserId, candidate) await webRTCServiceRef.current?.addIceCandidate(fromUserId, candidate)
}) })
// 🔑 Yeni katılan birini gördüğünde
signalRServiceRef.current.setParticipantJoinHandler( signalRServiceRef.current.setParticipantJoinHandler(
async (userId: string, name: string, isTeacher: boolean, isActive: boolean) => { async (remoteUserId: string, name: string, isTeacher: boolean, isActive: boolean) => {
if (userId === user.id) return if (remoteUserId === user.id) return
if (!isActive) return if (!isActive) return
console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`) console.log(`Participant joined: ${name}, isTeacher: ${isTeacher}`)
// Statee ekle
setParticipants((prev) => { setParticipants((prev) => {
const updated = [...prev] if (prev.find((p) => p.id === remoteUserId)) return prev
if (!updated.find((p) => p.id === userId)) { return [
updated.push({ ...prev,
id: userId, {
id: remoteUserId,
name, name,
sessionId: classSession.id, sessionId: classSession.id,
isTeacher, isTeacher,
isAudioMuted: classSettings.defaultMicrophoneState === 'muted', isAudioMuted: classSettings.defaultMicrophoneState === 'muted',
isVideoMuted: classSettings.defaultCameraState === 'off', isVideoMuted: classSettings.defaultCameraState === 'off',
isActive: true, isActive: true,
}) },
} ]
return updated
}) })
// 🔑 Mesh: Herkes herkese offer gönderir // PeerConnection hazırla
if (!webRTCServiceRef.current?.getPeerConnection(userId)) { if (!webRTCServiceRef.current?.getPeerConnection(remoteUserId)) {
await webRTCServiceRef.current?.createPeerConnection(userId) await webRTCServiceRef.current?.createPeerConnection(remoteUserId)
} }
const offer = await webRTCServiceRef.current!.createOffer(userId) // 🔑 Çakışmayı önle: sadece idsi küçük olan offer başlatır
await signalRServiceRef.current?.sendOffer(classSession.id, userId, offer) 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( signalRServiceRef.current.setExistingParticipantsHandler(
async ( async (
existing: { userId: string; userName: string; isTeacher: boolean; isActive: boolean }[], existing: { userId: string; userName: string; isTeacher: boolean; isActive: boolean }[],
@ -320,10 +322,12 @@ const RoomDetail: React.FC = () => {
if (!participant.isActive) continue if (!participant.isActive) continue
if (participant.userId === user.id) continue if (participant.userId === user.id) continue
// Statee ekle
setParticipants((prev) => { setParticipants((prev) => {
const updated = [...prev] if (prev.find((p) => p.id === participant.userId)) return prev
if (!updated.find((p) => p.id === participant.userId)) { return [
updated.push({ ...prev,
{
id: participant.userId, id: participant.userId,
name: participant.userName, name: participant.userName,
sessionId: classSession.id, sessionId: classSession.id,
@ -331,19 +335,21 @@ const RoomDetail: React.FC = () => {
isAudioMuted: classSettings.defaultMicrophoneState === 'muted', isAudioMuted: classSettings.defaultMicrophoneState === 'muted',
isVideoMuted: classSettings.defaultCameraState === 'off', isVideoMuted: classSettings.defaultCameraState === 'off',
isActive: true, isActive: true,
}) },
} ]
return updated
}) })
// PeerConnection hazırla
if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) { if (!webRTCServiceRef.current?.getPeerConnection(participant.userId)) {
await webRTCServiceRef.current?.createPeerConnection(participant.userId) await webRTCServiceRef.current?.createPeerConnection(participant.userId)
} }
// 🔑 Mesh: yeni gelen user da karşıya offer yollar // 🔑 Çakışmayı önle
if (user.id < participant.userId) {
const offer = await webRTCServiceRef.current!.createOffer(participant.userId) const offer = await webRTCServiceRef.current!.createOffer(participant.userId)
await signalRServiceRef.current?.sendOffer(classSession.id, participant.userId, offer) await signalRServiceRef.current?.sendOffer(classSession.id, participant.userId, offer)
} }
}
}, },
) )