2025-08-26 08:39:09 +00:00
|
|
|
import {
|
|
|
|
|
ClassAttendanceDto,
|
|
|
|
|
ClassChatDto,
|
|
|
|
|
HandRaiseDto,
|
|
|
|
|
SignalingMessageDto,
|
|
|
|
|
} from '@/proxy/classroom/models'
|
2025-08-27 20:55:01 +00:00
|
|
|
import { store } from '@/store/store'
|
2025-08-26 08:39:09 +00:00
|
|
|
import * as signalR from '@microsoft/signalr'
|
|
|
|
|
|
|
|
|
|
export class SignalRService {
|
|
|
|
|
private connection!: signalR.HubConnection
|
|
|
|
|
private isConnected: boolean = false
|
|
|
|
|
private onSignalingMessage?: (message: SignalingMessageDto) => void
|
|
|
|
|
private onAttendanceUpdate?: (record: ClassAttendanceDto) => void
|
|
|
|
|
private onParticipantJoined?: (userId: string, name: string) => void
|
|
|
|
|
private onParticipantLeft?: (userId: string) => void
|
|
|
|
|
private onChatMessage?: (message: ClassChatDto) => void
|
|
|
|
|
private onParticipantMuted?: (userId: string, isMuted: boolean) => void
|
|
|
|
|
private onHandRaiseReceived?: (handRaise: HandRaiseDto) => void
|
|
|
|
|
private onHandRaiseDismissed?: (handRaiseId: string) => void
|
|
|
|
|
|
|
|
|
|
constructor() {
|
2025-08-27 20:55:01 +00:00
|
|
|
const { auth } = store.getState()
|
|
|
|
|
|
2025-08-26 08:39:09 +00:00
|
|
|
// Only initialize connection if not in demo mode
|
2025-08-28 11:53:47 +00:00
|
|
|
// In production, replace with your actual SignalR hub URL
|
|
|
|
|
this.connection = new signalR.HubConnectionBuilder()
|
|
|
|
|
.withUrl(`${import.meta.env.VITE_API_URL}/classroomhub`, {
|
|
|
|
|
accessTokenFactory: () => auth.session.token || '',
|
|
|
|
|
})
|
|
|
|
|
.withAutomaticReconnect()
|
|
|
|
|
.configureLogging(signalR.LogLevel.Information)
|
|
|
|
|
.build()
|
|
|
|
|
|
|
|
|
|
this.setupEventHandlers()
|
2025-08-26 08:39:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupEventHandlers() {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.connection) return
|
2025-08-26 08:39:09 +00:00
|
|
|
|
|
|
|
|
this.connection.on('ReceiveSignalingMessage', (message: SignalingMessageDto) => {
|
|
|
|
|
this.onSignalingMessage?.(message)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.connection.on('AttendanceUpdated', (record: ClassAttendanceDto) => {
|
|
|
|
|
this.onAttendanceUpdate?.(record)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.connection.on('ParticipantJoined', (userId: string, name: string) => {
|
|
|
|
|
this.onParticipantJoined?.(userId, name)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.connection.on('ParticipantLeft', (userId: string) => {
|
|
|
|
|
this.onParticipantLeft?.(userId)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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', (handRaise: HandRaiseDto) => {
|
|
|
|
|
this.onHandRaiseReceived?.(handRaise)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.connection.on('HandRaiseDismissed', (handRaiseId: string) => {
|
|
|
|
|
this.onHandRaiseDismissed?.(handRaiseId)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.connection.onreconnected(() => {
|
|
|
|
|
console.log('SignalR reconnected')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.connection.onclose(() => {
|
|
|
|
|
console.log('SignalR connection closed')
|
|
|
|
|
})
|
2025-08-27 20:55:01 +00:00
|
|
|
|
|
|
|
|
this.connection.on('Error', (message: string) => {
|
|
|
|
|
console.error('Hub error:', message)
|
|
|
|
|
})
|
2025-08-26 08:39:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async start(): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.start()
|
|
|
|
|
this.isConnected = true
|
|
|
|
|
console.log('SignalR connection started')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error starting SignalR connection:', error)
|
|
|
|
|
// Switch to demo mode if connection fails
|
|
|
|
|
this.isConnected = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 20:55:01 +00:00
|
|
|
async joinClass(sessionId: string, userName: string): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection join class for', userName)
|
2025-08-26 08:39:09 +00:00
|
|
|
// Simulate successful join in demo mode
|
|
|
|
|
// Don't auto-add participants in demo mode - let manual simulation handle this
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-27 20:55:01 +00:00
|
|
|
await this.connection.invoke('JoinClass', sessionId, userName)
|
2025-08-26 08:39:09 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error joining class:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-27 20:55:01 +00:00
|
|
|
async leaveClass(sessionId: string): Promise<void> {
|
|
|
|
|
const { auth } = store.getState()
|
|
|
|
|
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating leave class for user', auth.user.id)
|
2025-08-26 08:39:09 +00:00
|
|
|
// Simulate successful leave in demo mode
|
|
|
|
|
setTimeout(() => {
|
2025-08-27 20:55:01 +00:00
|
|
|
this.onParticipantLeft?.(auth.user.id)
|
2025-08-26 08:39:09 +00:00
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-27 20:55:01 +00:00
|
|
|
await this.connection.invoke('LeaveClass', sessionId)
|
2025-08-26 08:39:09 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error leaving class:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendSignalingMessage(message: SignalingMessageDto): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection signaling message', message.type)
|
2025-08-26 08:39:09 +00:00
|
|
|
// In demo mode, we can't send real signaling messages
|
|
|
|
|
// WebRTC will need to work in local-only mode
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('SendSignalingMessage', message)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error sending signaling message:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendChatMessage(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
senderId: string,
|
|
|
|
|
senderName: string,
|
|
|
|
|
message: string,
|
|
|
|
|
isTeacher: boolean,
|
|
|
|
|
): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating chat message from', senderName)
|
2025-08-26 08:39:09 +00:00
|
|
|
const chatMessage: ClassChatDto = {
|
|
|
|
|
id: `msg-${Date.now()}`,
|
|
|
|
|
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 (error) {
|
|
|
|
|
console.error('Error sending chat message:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendPrivateMessage(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
senderId: string,
|
|
|
|
|
senderName: string,
|
|
|
|
|
message: string,
|
|
|
|
|
recipientId: string,
|
|
|
|
|
recipientName: string,
|
|
|
|
|
isTeacher: boolean,
|
|
|
|
|
): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log(
|
|
|
|
|
'Error starting SignalR connection simulating private message from',
|
|
|
|
|
senderName,
|
|
|
|
|
'to',
|
|
|
|
|
recipientName,
|
|
|
|
|
)
|
2025-08-26 08:39:09 +00:00
|
|
|
const chatMessage: ClassChatDto = {
|
|
|
|
|
id: `msg-${Date.now()}`,
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error sending private message:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async sendAnnouncement(
|
|
|
|
|
sessionId: string,
|
|
|
|
|
senderId: string,
|
|
|
|
|
senderName: string,
|
|
|
|
|
message: string,
|
|
|
|
|
): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating announcement from', senderName)
|
2025-08-26 08:39:09 +00:00
|
|
|
const chatMessage: ClassChatDto = {
|
|
|
|
|
id: `msg-${Date.now()}`,
|
|
|
|
|
senderId,
|
|
|
|
|
senderName,
|
|
|
|
|
message,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
isTeacher: true,
|
|
|
|
|
messageType: 'announcement',
|
|
|
|
|
}
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.onChatMessage?.(chatMessage)
|
|
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('SendAnnouncement', sessionId, senderId, senderName, message)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error sending chat message:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async muteParticipant(sessionId: string, userId: string, isMuted: boolean): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating mute participant', userId, isMuted)
|
2025-08-26 08:39:09 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
|
this.onParticipantMuted?.(userId, isMuted)
|
|
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error muting participant:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating hand raise from', studentName)
|
2025-08-26 08:39:09 +00:00
|
|
|
const handRaise: HandRaiseDto = {
|
|
|
|
|
id: `hand-${Date.now()}`,
|
|
|
|
|
studentId,
|
|
|
|
|
studentName,
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
isActive: true,
|
|
|
|
|
}
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.onHandRaiseReceived?.(handRaise)
|
|
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('RaiseHand', sessionId, studentId, studentName)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error raising hand:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async kickParticipant(sessionId: string, participantId: string): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating kick participant', participantId)
|
2025-08-26 08:39:09 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
|
this.onParticipantLeft?.(participantId)
|
|
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('KickParticipant', sessionId, participantId)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error kicking participant:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async approveHandRaise(sessionId: string, handRaiseId: string): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating hand raise approval')
|
2025-08-26 08:39:09 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
|
this.onHandRaiseDismissed?.(handRaiseId)
|
|
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('ApproveHandRaise', sessionId, handRaiseId)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error approving hand raise:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async dismissHandRaise(sessionId: string, handRaiseId: string): Promise<void> {
|
2025-08-28 11:53:47 +00:00
|
|
|
if (!this.isConnected) {
|
|
|
|
|
console.log('Error starting SignalR connection simulating hand raise dismissal')
|
2025-08-26 08:39:09 +00:00
|
|
|
setTimeout(() => {
|
|
|
|
|
this.onHandRaiseDismissed?.(handRaiseId)
|
|
|
|
|
}, 100)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.connection.invoke('DismissHandRaise', sessionId, handRaiseId)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error dismissing hand raise:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSignalingHandler(callback: (message: SignalingMessageDto) => void) {
|
|
|
|
|
this.onSignalingMessage = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setAttendanceUpdatedHandler(callback: (record: ClassAttendanceDto) => void) {
|
|
|
|
|
this.onAttendanceUpdate = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setParticipantJoinHandler(callback: (userId: string, name: string) => void) {
|
|
|
|
|
this.onParticipantJoined = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setParticipantLeaveHandler(callback: (userId: string) => void) {
|
|
|
|
|
this.onParticipantLeft = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setChatMessageReceivedHandler(callback: (message: ClassChatDto) => void) {
|
|
|
|
|
this.onChatMessage = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setParticipantMutedHandler(callback: (userId: string, isMuted: boolean) => void) {
|
|
|
|
|
this.onParticipantMuted = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setHandRaiseReceivedHandler(callback: (handRaise: HandRaiseDto) => void) {
|
|
|
|
|
this.onHandRaiseReceived = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setHandRaiseDismissedHandler(callback: (handRaiseId: string) => void) {
|
|
|
|
|
this.onHandRaiseDismissed = callback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async disconnect(): Promise<void> {
|
|
|
|
|
if (this.isConnected && this.connection) {
|
|
|
|
|
await this.connection.stop()
|
|
|
|
|
this.isConnected = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getConnectionState(): boolean {
|
|
|
|
|
return this.isConnected
|
|
|
|
|
}
|
|
|
|
|
}
|