446 lines
13 KiB
TypeScript
446 lines
13 KiB
TypeScript
import { ClassroomAttendanceDto, ClassroomChatDto, HandRaiseDto } from '@/proxy/classroom/models'
|
|
import { store } from '@/store/store'
|
|
import * as signalR from '@microsoft/signalr'
|
|
|
|
export class SignalRService {
|
|
private connection!: signalR.HubConnection
|
|
private isConnected: boolean = false
|
|
private onAttendanceUpdate?: (record: ClassroomAttendanceDto) => void
|
|
private onParticipantJoined?: (userId: string, name: string) => void
|
|
private onParticipantLeft?: (userId: string) => void
|
|
private onChatMessage?: (message: ClassroomChatDto) => 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
|
|
|
|
constructor() {
|
|
const { auth } = store.getState()
|
|
|
|
// Only initialize connection if not in demo mode
|
|
// 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()
|
|
}
|
|
|
|
private setupEventHandlers() {
|
|
if (!this.connection) return
|
|
|
|
this.connection.on('AttendanceUpdated', (record: ClassroomAttendanceDto) => {
|
|
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', (payload: any) => {
|
|
// payload = { handRaiseId, studentId, studentName, ... }
|
|
this.onHandRaiseReceived?.(payload.studentId)
|
|
})
|
|
|
|
this.connection.on('HandRaiseDismissed', (payload: any) => {
|
|
// payload = { handRaiseId, studentId }
|
|
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(() => {
|
|
console.log('SignalR reconnected')
|
|
})
|
|
|
|
this.connection.onclose(() => {
|
|
console.log('SignalR connection closed')
|
|
})
|
|
|
|
this.connection.on('Error', (message: string) => {
|
|
console.error('Hub error:', message)
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
async joinClass(
|
|
sessionId: string,
|
|
userId: string,
|
|
userName: string,
|
|
isTeacher: boolean,
|
|
): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection join class for', userName)
|
|
return
|
|
}
|
|
|
|
console.log('Joining class session:', sessionId, 'as', userName, 'isTeacher:', isTeacher)
|
|
|
|
try {
|
|
await this.connection.invoke('JoinClass', sessionId, userId, userName, isTeacher)
|
|
} catch (error) {
|
|
console.error('Error joining class:', error)
|
|
}
|
|
}
|
|
|
|
async leaveClass(sessionId: string): Promise<void> {
|
|
const { auth } = store.getState()
|
|
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection simulating leave class for user', auth.user.id)
|
|
// Simulate successful leave in demo mode
|
|
setTimeout(() => {
|
|
this.onParticipantLeft?.(auth.user.id)
|
|
}, 100)
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this.connection.invoke('LeaveClass', sessionId)
|
|
} catch (error) {
|
|
console.error('Error leaving class:', error)
|
|
}
|
|
}
|
|
|
|
async sendChatMessage(
|
|
sessionId: string,
|
|
senderId: string,
|
|
senderName: string,
|
|
message: string,
|
|
isTeacher: boolean,
|
|
): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection simulating chat message from', senderName)
|
|
const chatMessage: ClassroomChatDto = {
|
|
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 (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> {
|
|
if (!this.isConnected) {
|
|
console.log(
|
|
'Error starting SignalR connection simulating private message from',
|
|
senderName,
|
|
'to',
|
|
recipientName,
|
|
)
|
|
const chatMessage: ClassroomChatDto = {
|
|
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 (error) {
|
|
console.error('Error sending private message:', error)
|
|
}
|
|
}
|
|
|
|
async sendAnnouncement(
|
|
sessionId: string,
|
|
senderId: string,
|
|
senderName: string,
|
|
message: string,
|
|
isTeacher: boolean,
|
|
): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection simulating announcement from', senderName)
|
|
const chatMessage: ClassroomChatDto = {
|
|
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 (error) {
|
|
console.error('Error sending chat message:', error)
|
|
}
|
|
}
|
|
|
|
async muteParticipant(
|
|
sessionId: string,
|
|
userId: string,
|
|
isMuted: boolean,
|
|
isTeacher: boolean,
|
|
): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection simulating mute participant', userId, isMuted)
|
|
setTimeout(() => {
|
|
this.onParticipantMuted?.(userId, isMuted)
|
|
}, 100)
|
|
return
|
|
}
|
|
|
|
console.log('Muting participant:', userId, 'Muted:', isMuted, 'isTeacher:', isTeacher)
|
|
|
|
try {
|
|
await this.connection.invoke('MuteParticipant', sessionId, userId, isMuted, isTeacher)
|
|
} catch (error) {
|
|
console.error('Error muting participant:', error)
|
|
}
|
|
}
|
|
|
|
async raiseHand(sessionId: string, studentId: string, studentName: string): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection simulating hand raise from', studentName)
|
|
const handRaise: HandRaiseDto = {
|
|
id: crypto.randomUUID(),
|
|
studentId,
|
|
studentName,
|
|
timestamp: new Date().toISOString(),
|
|
isActive: true,
|
|
}
|
|
setTimeout(() => {
|
|
this.onHandRaiseReceived?.(studentId)
|
|
}, 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> {
|
|
if (!this.isConnected) {
|
|
console.log('Error starting SignalR connection simulating kick participant', participantId)
|
|
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, studentId: string): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Simulating hand raise approval for student', studentId)
|
|
setTimeout(() => {
|
|
this.onHandRaiseDismissed?.(studentId)
|
|
}, 100)
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this.connection.invoke('ApproveHandRaise', sessionId, studentId)
|
|
} catch (error) {
|
|
console.error('Error approving hand raise:', error)
|
|
}
|
|
}
|
|
|
|
async dismissHandRaise(sessionId: string, studentId: string): Promise<void> {
|
|
if (!this.isConnected) {
|
|
console.log('Simulating hand raise dismissal for student', studentId)
|
|
setTimeout(() => {
|
|
this.onHandRaiseDismissed?.(studentId)
|
|
}, 100)
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this.connection.invoke('DismissHandRaise', sessionId, studentId)
|
|
} catch (error) {
|
|
console.error('Error dismissing hand raise:', error)
|
|
}
|
|
}
|
|
|
|
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: ClassroomAttendanceDto) => 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: ClassroomChatDto) => 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.connection) {
|
|
await this.connection.stop()
|
|
this.isConnected = false
|
|
}
|
|
}
|
|
|
|
getConnectionState(): boolean {
|
|
return this.isConnected
|
|
}
|
|
}
|