sozsoft-platform/ui/src/services/videoroom/signalr.tsx

574 lines
16 KiB
TypeScript
Raw Normal View History

2026-05-08 05:34:29 +00:00
import { ROUTES_ENUM } from '@/routes/route.constant'
import { store } from '@/store/store'
import * as signalR from '@microsoft/signalr'
import { toast } from '@/components/ui'
import Notification from '@/components/ui/Notification'
import { VideoroomAttendanceDto, VideoroomChatDto } from '@/proxy/videoroom/models'
export class SignalRService {
private connection!: signalR.HubConnection
private isConnected: boolean = false
private currentSessionId?: string
private isKicked: boolean = false
private onAttendanceUpdate?: (record: VideoroomAttendanceDto) => void
private onParticipantJoined?: (
userId: string,
name: string,
isTeacher: boolean,
isActive: boolean,
) => void
private onParticipantLeft?: (payload: {
userId: string
sessionId: string
userName: string
}) => void
private onChatMessage?: (message: VideoroomChatDto) => void
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
private onHandRaiseReceived?: (studentId: string) => void
private onHandRaiseDismissed?: (studentId: string) => void
private onOfferReceived?: (fromUserId: string, offer: RTCSessionDescriptionInit) => void
private onAnswerReceived?: (fromUserId: string, answer: RTCSessionDescriptionInit) => void
private onIceCandidateReceived?: (fromUserId: string, candidate: RTCIceCandidateInit) => void
private onForceCleanup?: () => void
constructor() {
const { auth } = store.getState()
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${import.meta.env.VITE_API_URL}/videoroomhub`, {
accessTokenFactory: () => auth.session.token || '',
})
.configureLogging(signalR.LogLevel.Information)
.build()
this.setupEventHandlers()
}
private setupEventHandlers() {
if (!this.connection) return
this.connection.on('AttendanceUpdated', (record: VideoroomAttendanceDto) => {
this.onAttendanceUpdate?.(record)
})
this.connection.on(
'ParticipantJoined',
(userId: string, name: string, isTeacher: boolean, isActive: boolean) => {
this.onParticipantJoined?.(userId, name, isTeacher, isActive)
},
)
this.connection.on(
'ParticipantLeft',
(payload: { userId: string; sessionId: string; userName: string }) => {
this.onParticipantLeft?.(payload)
},
)
this.connection.on('ChatMessage', (message: any) => {
this.onChatMessage?.(message)
})
this.connection.on('ParticipantMuted', (userId: string, isMuted: boolean) => {
this.onParticipantMuted?.(userId, isMuted)
})
this.connection.on('HandRaiseReceived', (payload: any) => {
this.onHandRaiseReceived?.(payload.studentId)
})
this.connection.on('HandRaiseDismissed', (payload: any) => {
this.onHandRaiseDismissed?.(payload.studentId)
})
this.connection.on('ReceiveOffer', (fromUserId: string, offer: RTCSessionDescriptionInit) => {
this.onOfferReceived?.(fromUserId, offer)
})
this.connection.on('ReceiveAnswer', (fromUserId: string, answer: RTCSessionDescriptionInit) => {
this.onAnswerReceived?.(fromUserId, answer)
})
this.connection.on(
'ReceiveIceCandidate',
(fromUserId: string, candidate: RTCIceCandidateInit) => {
this.onIceCandidateReceived?.(fromUserId, candidate)
},
)
this.connection.onreconnected(async () => {
this.isConnected = true
toast.push(<Notification title="🔄 Bağlantı tekrar kuruldu" type="success" />, {
placement: 'top-end',
})
if (this.currentSessionId && store.getState().auth.user) {
const u = store.getState().auth.user
await this.joinClass(this.currentSessionId, u.id, u.name, u.role === 'teacher', true)
}
})
this.connection.onclose(async () => {
if (this.isKicked) {
toast.push(
<Notification title="⚠️ Bağlantı koptu, yeniden bağlanılıyor..." type="warning" />,
{ placement: 'top-end' },
)
this.isConnected = false
this.currentSessionId = undefined
return
}
this.isConnected = false
try {
if (this.currentSessionId) {
await this.connection.invoke('LeaveClass', this.currentSessionId)
}
} finally {
this.currentSessionId = undefined
}
})
this.connection.on('Error', (message: string) => {
toast.push(<Notification title={`❌ Hata: ${message}`} type="danger" />, {
placement: 'top-end',
})
})
this.connection.on('Warning', (message: string) => {
toast.push(<Notification title={`⚠️ Uyarı: ${message}`} type="warning" />, {
placement: 'top-end',
})
})
this.connection.on('Info', (message: string) => {
toast.push(<Notification title={` Bilgi: ${message}`} type="info" />, {
placement: 'top-end',
})
})
this.connection.onreconnecting(() => {
if (this.isKicked) {
toast.push(
<Notification
title="❌ Sınıftan çıkarıldığınız için yeniden bağlanma engellendi"
type="danger"
/>,
)
this.connection.stop()
throw new Error('Reconnect blocked after kick')
}
})
this.connection.on('ForceDisconnect', async (message: string) => {
this.isKicked = true
toast.push(<Notification title={`❌ Sınıftan çıkarıldınız: ${message}`} type="danger" />, {
placement: 'top-end',
})
if (this.onForceCleanup) {
this.onForceCleanup()
}
try {
await this.connection.stop()
} catch {}
this.isConnected = false
if (this.currentSessionId && store.getState().auth.user) {
this.onParticipantLeft?.({
userId: store.getState().auth.user.id,
sessionId: this.currentSessionId,
userName: store.getState().auth.user.name,
})
}
this.currentSessionId = undefined
window.location.href = ROUTES_ENUM.protected.admin.videoroom.roomList
})
}
async start(): Promise<void> {
try {
const startPromise = this.connection.start()
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Bağlantı zaman aşımına uğradı')), 10000),
)
await Promise.race([startPromise, timeout])
this.isConnected = true
toast.push(<Notification title="✅ Bağlantı kuruldu" type="success" />, {
placement: 'top-end',
})
} catch {
toast.push(
<Notification
title="⚠️ Sunucuya bağlanılamadı. Lütfen sayfayı yenileyin veya internet bağlantınızı kontrol edin."
type="danger"
/>,
{ placement: 'top-end' },
)
this.isConnected = false
}
}
async joinClass(
sessionId: string,
userId: string,
userName: string,
isTeacher: boolean,
isActive: boolean,
): Promise<void> {
if (!this.isConnected) {
toast.push(
<Notification
title="⚠️ Bağlantı yok. Sınıfa katılmadan önce bağlantıyı kontrol edin."
type="warning"
/>,
)
return
}
this.currentSessionId = sessionId
try {
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher, isActive)
} catch {
toast.push(<Notification title="❌ Sınıfa katılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
async leaveClass(sessionId: string): Promise<void> {
const { auth } = store.getState()
if (!this.isConnected) {
this.onParticipantLeft?.({ userId: auth.user.id, sessionId, userName: auth.user.name })
return
}
try {
await this.connection.invoke('LeaveClass', sessionId)
this.currentSessionId = undefined
} catch {
toast.push(<Notification title="⚠️ Çıkış başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async sendChatMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: VideoroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'public',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendChatMessage',
sessionId,
senderId,
senderName,
message,
isTeacher,
'public',
)
} catch {
toast.push(<Notification title="❌ Mesaj gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async sendPrivateMessage(
sessionId: string,
senderId: string,
senderName: string,
message: string,
recipientId: string,
recipientName: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: VideoroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
recipientId,
recipientName,
messageType: 'private',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendPrivateMessage',
sessionId,
senderId,
senderName,
message,
recipientId,
recipientName,
isTeacher,
'private',
)
} catch {
toast.push(<Notification title="❌ Özel mesaj gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async sendAnnouncement(
sessionId: string,
senderId: string,
senderName: string,
message: string,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
const chatMessage: VideoroomChatDto = {
id: crypto.randomUUID(),
sessionId,
senderId,
senderName,
message,
timestamp: new Date().toISOString(),
isTeacher,
messageType: 'announcement',
}
setTimeout(() => {
this.onChatMessage?.(chatMessage)
}, 100)
return
}
try {
await this.connection.invoke(
'SendAnnouncement',
sessionId,
senderId,
senderName,
message,
isTeacher,
)
} catch {
toast.push(<Notification title="❌ Duyuru gönderilemedi" type="danger" />, {
placement: 'top-end',
})
}
}
async muteParticipant(
sessionId: string,
userId: string,
isMuted: boolean,
isTeacher: boolean,
): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onParticipantMuted?.(userId, isMuted)
}, 100)
return
}
try {
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
} catch {
toast.push(<Notification title="⚠️ Katılımcı susturulamadı" type="warning" />, {
placement: 'top-end',
})
}
}
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseReceived?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
} catch {
toast.push(<Notification title="❌ El kaldırma başarısız" type="danger" />, {
placement: 'top-end',
})
}
}
async kickParticipant(sessionId: string, participantId: string, userName: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onParticipantLeft?.({ userId: participantId, sessionId, userName })
}, 100)
return
}
try {
await this.connection.invoke('KickParticipant', sessionId, participantId)
} catch {
toast.push(<Notification title="❌ Katılımcı atılamadı" type="danger" />, {
placement: 'top-end',
})
}
}
async approveHandRaise(sessionId: string, studentId: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseDismissed?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
} catch {
toast.push(<Notification title="⚠️ El kaldırma onayı başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async dismissHandRaise(sessionId: string, studentId: string): Promise<void> {
if (!this.isConnected) {
setTimeout(() => {
this.onHandRaiseDismissed?.(studentId)
}, 100)
return
}
try {
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
} catch {
toast.push(<Notification title="⚠️ El indirme başarısız" type="warning" />, {
placement: 'top-end',
})
}
}
async sendOffer(sessionId: string, targetUserId: string, offer: RTCSessionDescriptionInit) {
if (!this.isConnected) return
await this.connection.invoke('SendOffer', sessionId, targetUserId, offer)
}
async sendAnswer(sessionId: string, targetUserId: string, answer: RTCSessionDescriptionInit) {
if (!this.isConnected) return
await this.connection.invoke('SendAnswer', sessionId, targetUserId, answer)
}
async sendIceCandidate(sessionId: string, targetUserId: string, candidate: RTCIceCandidateInit) {
if (!this.isConnected) return
await this.connection.invoke('SendIceCandidate', sessionId, targetUserId, candidate)
}
setExistingParticipantsHandler(callback: (participants: any[]) => void) {
this.connection.on('ExistingParticipants', callback)
}
setAttendanceUpdatedHandler(callback: (record: VideoroomAttendanceDto) => void) {
this.onAttendanceUpdate = callback
}
setParticipantJoinHandler(
callback: (userId: string, name: string, isTeacher: boolean, isActive: boolean) => void,
) {
this.onParticipantJoined = callback
}
setParticipantLeaveHandler(
callback: (payload: { userId: string; sessionId: string; userName: string }) => void,
) {
this.onParticipantLeft = callback
}
setChatMessageReceivedHandler(callback: (message: VideoroomChatDto) => void) {
this.onChatMessage = callback
}
setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) {
this.onParticipantMuted = callback
}
setHandRaiseReceivedHandler(callback: (studentId: string) => void) {
this.onHandRaiseReceived = callback
}
setHandRaiseDismissedHandler(callback: (studentId: string) => void) {
this.onHandRaiseDismissed = callback
}
setOfferReceivedHandler(
callback: (fromUserId: string, offer: RTCSessionDescriptionInit) => void,
) {
this.onOfferReceived = callback
}
setAnswerReceivedHandler(
callback: (fromUserId: string, answer: RTCSessionDescriptionInit) => void,
) {
this.onAnswerReceived = callback
}
setIceCandidateReceivedHandler(
callback: (fromUserId: string, candidate: RTCIceCandidateInit) => void,
) {
this.onIceCandidateReceived = callback
}
async disconnect(): Promise<void> {
if (this.isConnected && this.currentSessionId) {
try {
await this.connection.invoke('LeaveClass', this.currentSessionId)
} catch {
toast.push(<Notification title="⚠️ Bağlantı koparılırken hata" type="warning" />, {
placement: 'top-end',
})
}
}
if (this.connection) {
await this.connection.stop()
}
this.isConnected = false
this.currentSessionId = undefined
}
getConnectionState(): boolean {
return this.isConnected
}
setForceCleanupHandler(callback: () => void) {
this.onForceCleanup = callback
}
}