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")]
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<ClassroomSettingsDto>(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.");
}
// 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);
}
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")]

View file

@ -264,21 +264,6 @@ export const ParticipantGrid: React.FC<ParticipantGridProps> = ({
</button>
</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>
)

View file

@ -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<ChatPanelProps> = ({
@ -30,6 +36,7 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
onSendMessage,
onClose,
formatTime,
classSettings,
}) => {
const messagesEndRef = useRef<HTMLDivElement>(null)
@ -67,18 +74,19 @@ const ChatPanel: React.FC<ChatPanelProps> = ({
<span>Herkese</span>
</button>
<button
onClick={() => setMessageMode('private')}
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
messageMode === 'private'
? 'bg-green-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<FaUser size={12} />
<span>Özel</span>
</button>
{classSettings.allowPrivateMessages && (
<button
onClick={() => setMessageMode('private')}
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-xs ${
messageMode === 'private'
? 'bg-green-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
<FaUser size={12} />
<span>Özel</span>
</button>
)}
{user.role === 'teacher' && (
<button
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 * 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<void> {
@ -211,7 +224,7 @@ export class SignalRService {
message: string,
recipientId: string,
recipientName: string,
isTeacher: boolean
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
console.log(

View file

@ -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<ClassroomAttendanceDto[]>([])
const [chatMessages, setChatMessages] = useState<ClassroomChatDto[]>([])
const [currentLayout, setCurrentLayout] = useState<VideoLayoutDto>({
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<VideoLayoutDto | null>(null)
const [focusedParticipant, setFocusedParticipant] = useState<string>()
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 (
<LayoutPanel
layouts={layouts}
currentLayout={currentLayout}
currentLayout={currentLayout ?? layouts[0]}
onChangeLayout={handleLayoutChange}
onClose={() => setActiveSidePanel(null)}
/>
)
case 'settings':
return (
<SettingsPanel
user={user}
classSettings={classSettings}
onSettingsChange={handleSettingsChange}
onClose={() => 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 = () => {
>
<FaUserFriends />
<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 */}
{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}
</span>
)}
@ -1050,15 +1038,6 @@ const RoomDetail: React.FC = () => {
>
<FaLayerGroup /> <span>Görünüm</span>
</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' && (
<button
onClick={() => {
@ -1205,22 +1184,6 @@ const RoomDetail: React.FC = () => {
</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 */}
<button
onClick={() => toggleSidePanel('participants')}
@ -1232,13 +1195,9 @@ const RoomDetail: React.FC = () => {
title="Katılımcılar"
>
<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 */}
{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}
</span>
)}
@ -1283,19 +1242,6 @@ const RoomDetail: React.FC = () => {
>
<FaLayerGroup size={14} />
</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>