Classroom güncellemeleri

This commit is contained in:
Sedat Öztürk 2025-08-31 00:46:09 +03:00
parent 277a5314cc
commit 09380b1e63
6 changed files with 93 additions and 281 deletions

View file

@ -117,6 +117,7 @@ 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)
{ {
@ -128,6 +129,8 @@ public class ClassroomHub : Hub
? new ClassroomSettingsDto() ? new ClassroomSettingsDto()
: JsonSerializer.Deserialize<ClassroomSettingsDto>(classroom.SettingsJson); : JsonSerializer.Deserialize<ClassroomSettingsDto>(classroom.SettingsJson);
initialMuteState = !isTeacher && classroomSettings.AutoMuteNewParticipants ? true : classroomSettings.DefaultMicrophoneState == "muted";
var participant = await _participantRepository.FirstOrDefaultAsync( var participant = await _participantRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.UserId == userId x => x.SessionId == sessionId && x.UserId == userId
); );
@ -140,7 +143,7 @@ public class ClassroomHub : Hub
userId, userId,
userName, userName,
isTeacher, isTeacher,
classroomSettings.DefaultMicrophoneState == "muted", initialMuteState,
classroomSettings.DefaultCameraState == "off", classroomSettings.DefaultCameraState == "off",
false, false,
isActive isActive
@ -351,28 +354,49 @@ public class ClassroomHub : Hub
[HubMethodName("KickParticipant")] [HubMethodName("KickParticipant")]
public async Task KickParticipantAsync(Guid sessionId, Guid participantId) public async Task KickParticipantAsync(Guid sessionId, Guid participantId)
{ {
var attendance = await _attendanceRepository.FirstOrDefaultAsync( try
x => x.SessionId == sessionId && x.StudentId == participantId && x.LeaveTime == null
);
if (attendance != null)
{ {
await CloseAttendanceAsync(attendance); // 1. Attendance kapat
await Clients.Group(sessionId.ToString()).SendAsync("AttendanceUpdated", attendance); 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.");
}
// DBde 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);
} }
catch (Exception ex)
var participant = await _participantRepository.FirstOrDefaultAsync(
x => x.SessionId == sessionId && x.UserId == participantId
);
if (participant != null)
{ {
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")] [HubMethodName("ApproveHandRaise")]

View file

@ -264,21 +264,6 @@ export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
</button> </button>
</div> </div>
)} )}
{/* Expand button for non-main participants */}
{!isMain && onParticipantFocus && (
<div className="absolute top-2 right-2 z-10">
<button
onClick={(e) => {
e.stopPropagation()
onParticipantFocus(participant.id)
}}
className="p-1 bg-black bg-opacity-50 text-white rounded-full hover:bg-opacity-70 transition-all"
title="Büyüt"
>
<FaExpand size={12} />
</button>
</div>
)}
</div> </div>
) )

View file

@ -1,6 +1,11 @@
import React, { useRef, useEffect } from 'react' import React, { useRef, useEffect } from 'react'
import { FaTimes, FaUsers, FaUser, FaBullhorn, FaPaperPlane } from 'react-icons/fa' 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 { interface ChatPanelProps {
user: { id: string; name: string; role: string } user: { id: string; name: string; role: string }
@ -15,6 +20,7 @@ interface ChatPanelProps {
onSendMessage: (e: React.FormEvent) => void onSendMessage: (e: React.FormEvent) => void
onClose: () => void onClose: () => void
formatTime: (timestamp: string) => string formatTime: (timestamp: string) => string
classSettings: ClassroomSettingsDto
} }
const ChatPanel: React.FC<ChatPanelProps> = ({ const ChatPanel: React.FC<ChatPanelProps> = ({
@ -30,6 +36,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
onSendMessage, onSendMessage,
onClose, onClose,
formatTime, formatTime,
classSettings,
}) => { }) => {
const messagesEndRef = useRef<HTMLDivElement>(null) const messagesEndRef = useRef<HTMLDivElement>(null)
@ -67,18 +74,19 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
<span>Herkese</span> <span>Herkese</span>
</button> </button>
<button {classSettings.allowPrivateMessages && (
onClick={() => setMessageMode('private')} <button
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${ onClick={() => setMessageMode('private')}
messageMode === 'private' className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
? 'bg-green-600 text-white' messageMode === 'private'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' ? 'bg-green-600 text-white'
}`} : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
> }`}
<FaUser size={12} /> >
<span>Özel</span> <FaUser size={12} />
</button> <span>Özel</span>
</button>
)}
{user.role === 'teacher' && ( {user.role === 'teacher' && (
<button <button
onClick={() => { onClick={() => {

View file

@ -1,164 +0,0 @@
import React from 'react'
import { FaTimes } from 'react-icons/fa'
import { ClassroomSettingsDto } from '@/proxy/classroom/models'
interface SettingsPanelProps {
user: { role: string }
classSettings: ClassroomSettingsDto
onSettingsChange: (newSettings: Partial<ClassroomSettingsDto>) => void
onClose: () => void
}
const SettingsPanel: React.FC<SettingsPanelProps> = ({
user,
classSettings,
onSettingsChange,
onClose,
}) => {
return (
<div className="h-full bg-white flex flex-col text-gray-900">
<div className="p-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-gray-900">Sınıf Ayarları</h3>
<button onClick={onClose}>
<FaTimes className="text-gray-500" />
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className="space-y-6">
{/* Katılımcı Davranışları */}
<div>
<h4 className="font-semibold text-gray-800 mb-3">Katılımcı İzinleri</h4>
<div className="space-y-3">
<label className="flex items-center justify-between">
<span className="text-sm text-gray-700">Parmak kaldırma izni</span>
<input
type="checkbox"
checked={classSettings.allowHandRaise}
onChange={(e) => onSettingsChange({ allowHandRaise: e.target.checked })}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
<label className="flex items-center justify-between">
<span className="text-sm text-gray-700">Öğrenci sohbet izni</span>
<input
type="checkbox"
checked={classSettings.allowStudentChat}
onChange={(e) => onSettingsChange({ allowStudentChat: e.target.checked })}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
<label className="flex items-center justify-between">
<span className="text-sm">Özel mesaj izni</span>
<input
type="checkbox"
checked={classSettings.allowPrivateMessages}
onChange={(e) => onSettingsChange({ allowPrivateMessages: e.target.checked })}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
<label className="flex items-center justify-between">
<span className="text-sm">Öğrenci ekran paylaşımı</span>
<input
type="checkbox"
checked={classSettings.allowStudentScreenShare}
onChange={(e) => onSettingsChange({ allowStudentScreenShare: e.target.checked })}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
</div>
</div>
{/* Varsayılan Ayarlar */}
<div>
<h4 className="font-semibold text-gray-800 mb-3">Varsayılan Ayarlar</h4>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Varsayılan mikrofon durumu
</label>
<select
value={classSettings.defaultMicrophoneState}
onChange={(e) =>
onSettingsChange({
defaultMicrophoneState: e.target.value as 'muted' | 'unmuted',
})
}
className="w-full p-2 border border-gray-300 rounded-md text-sm"
disabled={user.role !== 'teacher'}
>
<option value="muted">Kapalı</option>
<option value="unmuted">ık</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Varsayılan kamera durumu
</label>
<select
value={classSettings.defaultCameraState}
onChange={(e) =>
onSettingsChange({
defaultCameraState: e.target.value as 'on' | 'off',
})
}
className="w-full p-2 border border-gray-300 rounded-md text-sm"
disabled={user.role !== 'teacher'}
>
<option value="on">ık</option>
<option value="off">Kapalı</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Varsayılan layout
</label>
<select
value={classSettings.defaultLayout}
onChange={(e) => onSettingsChange({ defaultLayout: e.target.value })}
className="w-full p-2 border border-gray-300 rounded-md text-sm"
disabled={user.role !== 'teacher'}
>
<option value="grid">Izgara Görünümü</option>
<option value="sidebar">Sunum Modu</option>
<option value="teacher-focus">Öğretmen Odaklı</option>
<option value="interview">Karşılıklı Görüşme</option>
</select>
</div>
</div>
</div>
{/* Otomatik Ayarlar */}
<div>
<h4 className="font-semibold text-gray-800 mb-3">Otomatik Ayarlar</h4>
<div className="space-y-3">
<label className="flex items-center justify-between">
<span className="text-sm text-gray-700">Yeni katılımcıları otomatik sustur</span>
<input
type="checkbox"
checked={classSettings.autoMuteNewParticipants}
onChange={(e) => onSettingsChange({ autoMuteNewParticipants: e.target.checked })}
className="rounded"
disabled={user.role !== 'teacher'}
/>
</label>
</div>
</div>
{user.role !== 'teacher' && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-sm text-yellow-800"> Ayarları sadece öğretmen değiştirebilir.</p>
</div>
)}
</div>
</div>
</div>
)
}
export default SettingsPanel

View file

@ -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 { store } from '@/store/store'
import * as signalR from '@microsoft/signalr' import * as signalR from '@microsoft/signalr'
@ -63,6 +68,7 @@ export class SignalRService {
this.onParticipantMuted?.(userId, isMuted) this.onParticipantMuted?.(userId, isMuted)
}) })
this.connection.on('HandRaiseReceived', (payload: any) => { this.connection.on('HandRaiseReceived', (payload: any) => {
// payload = { handRaiseId, studentId, studentName, ... } // payload = { handRaiseId, studentId, studentName, ... }
this.onHandRaiseReceived?.(payload.studentId) this.onHandRaiseReceived?.(payload.studentId)
@ -101,6 +107,13 @@ export class SignalRService {
this.connection.on('Error', (message: string) => { this.connection.on('Error', (message: string) => {
console.error('Hub error:', message) 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<void> { async start(): Promise<void> {
@ -211,7 +224,7 @@ export class SignalRService {
message: string, message: string,
recipientId: string, recipientId: string,
recipientName: string, recipientName: string,
isTeacher: boolean isTeacher: boolean,
): Promise<void> { ): Promise<void> {
if (!this.isConnected) { if (!this.isConnected) {
console.log( console.log(

View file

@ -56,7 +56,6 @@ import ChatPanel from '@/components/classroom/panels/ChatPanel'
import ParticipantsPanel from '@/components/classroom/panels/ParticipantsPanel' import ParticipantsPanel from '@/components/classroom/panels/ParticipantsPanel'
import DocumentsPanel from '@/components/classroom/panels/DocumentsPanel' import DocumentsPanel from '@/components/classroom/panels/DocumentsPanel'
import LayoutPanel from '@/components/classroom/panels/LayoutPanel' import LayoutPanel from '@/components/classroom/panels/LayoutPanel'
import SettingsPanel from '@/components/classroom/panels/SettingsPanel'
import { ScreenSharePanel } from '@/components/classroom/panels/ScreenSharePanel' import { ScreenSharePanel } from '@/components/classroom/panels/ScreenSharePanel'
import { ParticipantGrid } from '@/components/classroom/ParticipantGrid' import { ParticipantGrid } from '@/components/classroom/ParticipantGrid'
@ -110,12 +109,8 @@ const RoomDetail: React.FC = () => {
const [isVideoEnabled, setIsVideoEnabled] = useState(true) const [isVideoEnabled, setIsVideoEnabled] = useState(true)
const [attendanceRecords, setAttendanceRecords] = useState<ClassroomAttendanceDto[]>([]) const [attendanceRecords, setAttendanceRecords] = useState<ClassroomAttendanceDto[]>([])
const [chatMessages, setChatMessages] = useState<ClassroomChatDto[]>([]) const [chatMessages, setChatMessages] = useState<ClassroomChatDto[]>([])
const [currentLayout, setCurrentLayout] = useState<VideoLayoutDto>({ const [currentLayout, setCurrentLayout] = useState<VideoLayoutDto | null>(null)
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 [focusedParticipant, setFocusedParticipant] = useState<string>() const [focusedParticipant, setFocusedParticipant] = useState<string>()
const [hasRaisedHand, setHasRaisedHand] = useState(false) const [hasRaisedHand, setHasRaisedHand] = useState(false)
const [isAllMuted, setIsAllMuted] = useState(false) const [isAllMuted, setIsAllMuted] = useState(false)
@ -402,10 +397,16 @@ const RoomDetail: React.FC = () => {
setChatMessages((prev) => [...prev, message]) setChatMessages((prev) => [...prev, message])
}) })
signalRServiceRef.current.setParticipantMutedHandler((userId, isMuted) => { signalRServiceRef.current.setParticipantMutedHandler(async (userId, isMuted) => {
setParticipants((prev) => setParticipants((prev) =>
prev.map((p) => (p.id === userId ? { ...p, isAudioMuted: isMuted } : p)), 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 // Hand raise events
@ -737,6 +738,7 @@ const RoomDetail: React.FC = () => {
onSendMessage={handleSendMessage} onSendMessage={handleSendMessage}
onClose={() => setActiveSidePanel(null)} onClose={() => setActiveSidePanel(null)}
formatTime={formatTime} formatTime={formatTime}
classSettings={classSettings}
/> />
) )
@ -775,22 +777,12 @@ const RoomDetail: React.FC = () => {
return ( return (
<LayoutPanel <LayoutPanel
layouts={layouts} layouts={layouts}
currentLayout={currentLayout} currentLayout={currentLayout ?? layouts[0]}
onChangeLayout={handleLayoutChange} onChangeLayout={handleLayoutChange}
onClose={() => setActiveSidePanel(null)} onClose={() => setActiveSidePanel(null)}
/> />
) )
case 'settings':
return (
<SettingsPanel
user={user}
classSettings={classSettings}
onSettingsChange={handleSettingsChange}
onClose={() => setActiveSidePanel(null)}
/>
)
default: default:
return null return null
} }
@ -857,7 +849,7 @@ const RoomDetail: React.FC = () => {
onToggleVideo={user.role === 'observer' ? () => {} : handleToggleVideo} onToggleVideo={user.role === 'observer' ? () => {} : handleToggleVideo}
onLeaveCall={handleLeaveCall} onLeaveCall={handleLeaveCall}
onMuteParticipant={handleMuteParticipant} onMuteParticipant={handleMuteParticipant}
layout={currentLayout} layout={currentLayout ?? layouts[0]}
focusedParticipant={focusedParticipant} focusedParticipant={focusedParticipant}
onParticipantFocus={handleParticipantFocus} onParticipantFocus={handleParticipantFocus}
hasSidePanel={!!activeSidePanel} hasSidePanel={!!activeSidePanel}
@ -1021,13 +1013,9 @@ const RoomDetail: React.FC = () => {
> >
<FaUserFriends /> <FaUserFriends />
<span>Katılımcılar</span> <span>Katılımcılar</span>
{/* Katılımcı adedi badge */}
<span className="absolute top-2 right-3 bg-blue-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
{participants.length + 1}
</span>
{/* El kaldıran badge */} {/* El kaldıran badge */}
{raisedHandsCount > 0 && ( {raisedHandsCount > 0 && (
<span className="absolute top-2 right-8 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]"> <span className="absolute top-2 right-3 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
{raisedHandsCount > 9 ? '9+' : raisedHandsCount} {raisedHandsCount > 9 ? '9+' : raisedHandsCount}
</span> </span>
)} )}
@ -1050,15 +1038,6 @@ const RoomDetail: React.FC = () => {
> >
<FaLayerGroup /> <span>Görünüm</span> <FaLayerGroup /> <span>Görünüm</span>
</button> </button>
<button
onClick={() => {
setMobileMenuOpen(false)
setTimeout(() => toggleSidePanel('settings'), 200)
}}
className={`flex items-center space-x-2 p-3 rounded-lg transition-all text-base ${activeSidePanel === 'settings' ? 'bg-blue-100 text-blue-700' : 'hover:bg-gray-100 text-gray-700'}`}
>
<FaWrench /> <span>Ayarlar</span>
</button>
{user.role === 'teacher' && ( {user.role === 'teacher' && (
<button <button
onClick={() => { onClick={() => {
@ -1205,22 +1184,6 @@ const RoomDetail: React.FC = () => {
</button> </button>
)} )}
{/* Parmak Kaldır (Öğrenci) */}
{user.role === 'student' && classSettings.allowHandRaise && (
<button
onClick={handleRaiseHand}
disabled={hasRaisedHand}
className={`p-2 rounded-lg transition-all ${
hasRaisedHand
? 'bg-yellow-600 text-white cursor-not-allowed'
: 'bg-gray-700 hover:bg-gray-600 text-white hover:bg-yellow-600'
}`}
title={hasRaisedHand ? 'Parmak Kaldırıldı' : 'Parmak Kaldır'}
>
<FaHandPaper size={14} />
</button>
)}
{/* Participants */} {/* Participants */}
<button <button
onClick={() => toggleSidePanel('participants')} onClick={() => toggleSidePanel('participants')}
@ -1232,13 +1195,9 @@ const RoomDetail: React.FC = () => {
title="Katılımcılar" title="Katılımcılar"
> >
<FaUserFriends size={14} /> <FaUserFriends size={14} />
{/* Katılımcı adedi badge */}
<span className="absolute -top-1 -right-1 bg-blue-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
{participants.length + 1}
</span>
{/* El kaldıran badge */} {/* El kaldıran badge */}
{raisedHandsCount > 0 && ( {raisedHandsCount > 0 && (
<span className="absolute -top-1 -right-6 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]"> <span className="absolute -top-1 -right-3 bg-yellow-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center text-[10px]">
{raisedHandsCount > 9 ? '9+' : raisedHandsCount} {raisedHandsCount > 9 ? '9+' : raisedHandsCount}
</span> </span>
)} )}
@ -1283,19 +1242,6 @@ const RoomDetail: React.FC = () => {
> >
<FaLayerGroup size={14} /> <FaLayerGroup size={14} />
</button> </button>
{/* Settings Button */}
<button
onClick={() => toggleSidePanel('settings')}
className={`p-2 rounded-lg transition-all ${
activeSidePanel === 'settings'
? 'bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white'
}`}
title="Ayarlar"
>
<FaWrench size={14} />
</button>
</div> </div>
</div> </div>
</div> </div>