Classroom güncellemeleri
This commit is contained in:
parent
277a5314cc
commit
09380b1e63
6 changed files with 93 additions and 281 deletions
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
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")]
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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={() => {
|
||||||
|
|
|
||||||
|
|
@ -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">Açı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">Açı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
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue