295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
import { useSession } from 'next-auth/react';
|
|
import { RocketChatCallListener, CallEvent } from '@/lib/services/rocketchat-call-listener';
|
|
import { useWidgetNotification } from './use-widget-notification';
|
|
import { logger } from '@/lib/logger';
|
|
import { IncomingCall } from '@/components/incoming-call-notification';
|
|
|
|
/**
|
|
* Hook to listen for incoming RocketChat calls and trigger notifications
|
|
*/
|
|
export function useRocketChatCalls() {
|
|
const { data: session } = useSession();
|
|
const { triggerNotification } = useWidgetNotification();
|
|
const listenerRef = useRef<RocketChatCallListener | null>(null);
|
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
const initializedRef = useRef(false);
|
|
const [incomingCall, setIncomingCall] = useState<IncomingCall | null>(null);
|
|
const currentCallRoomIdRef = useRef<string | null>(null);
|
|
const callTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
/**
|
|
* Get RocketChat credentials for the current user
|
|
*/
|
|
const getRocketChatCredentials = useCallback(async () => {
|
|
if (!session?.user?.id) {
|
|
console.log('[useRocketChatCalls] No session user ID');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
console.log('[useRocketChatCalls] Getting RocketChat credentials...');
|
|
|
|
// Extract base URL from environment
|
|
const baseUrl = process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0];
|
|
if (!baseUrl) {
|
|
console.error('[useRocketChatCalls] RocketChat base URL not configured');
|
|
logger.error('[useRocketChatCalls] RocketChat base URL not configured');
|
|
return null;
|
|
}
|
|
|
|
// Get user's RocketChat ID and token
|
|
const tokenResponse = await fetch('/api/rocket-chat/user-token', {
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!tokenResponse.ok) {
|
|
const errorText = await tokenResponse.text();
|
|
console.error('[useRocketChatCalls] Failed to get user token', {
|
|
status: tokenResponse.status,
|
|
error: errorText,
|
|
});
|
|
logger.error('[useRocketChatCalls] Failed to get user token', {
|
|
status: tokenResponse.status,
|
|
});
|
|
return null;
|
|
}
|
|
|
|
const tokenData = await tokenResponse.json();
|
|
console.log('[useRocketChatCalls] ✅ Got credentials', {
|
|
userId: tokenData.userId,
|
|
hasToken: !!tokenData.authToken,
|
|
baseUrl,
|
|
});
|
|
|
|
return {
|
|
userId: tokenData.userId,
|
|
authToken: tokenData.authToken,
|
|
baseUrl,
|
|
};
|
|
} catch (error) {
|
|
console.error('[useRocketChatCalls] Error getting credentials', error);
|
|
logger.error('[useRocketChatCalls] Error getting credentials', { error });
|
|
return null;
|
|
}
|
|
}, [session?.user?.id]);
|
|
|
|
/**
|
|
* Initialize call listener
|
|
*/
|
|
const initializeListener = useCallback(async () => {
|
|
if (initializedRef.current || listenerRef.current) {
|
|
console.log('[useRocketChatCalls] Already initialized, skipping');
|
|
return;
|
|
}
|
|
|
|
console.log('[useRocketChatCalls] Initializing call listener...');
|
|
|
|
const credentials = await getRocketChatCredentials();
|
|
if (!credentials) {
|
|
console.warn('[useRocketChatCalls] Could not get credentials, skipping initialization');
|
|
logger.warn('[useRocketChatCalls] Could not get credentials, skipping initialization');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const listener = RocketChatCallListener.getInstance();
|
|
const success = await listener.initialize(
|
|
credentials.userId,
|
|
credentials.authToken,
|
|
credentials.baseUrl
|
|
);
|
|
|
|
console.log('[useRocketChatCalls] Listener initialization result:', success);
|
|
|
|
if (success) {
|
|
listenerRef.current = listener;
|
|
initializedRef.current = true;
|
|
|
|
// Subscribe to call events
|
|
const unsubscribe = listener.onCall((callEvent: CallEvent) => {
|
|
// Handle call ended events (when caller hangs up)
|
|
if (callEvent.type === 'call-ended') {
|
|
console.log('[useRocketChatCalls] 📞 CALL ENDED - Checking if notification should be removed', {
|
|
endedRoomId: callEvent.roomId,
|
|
currentCallRoomId: currentCallRoomIdRef.current,
|
|
from: callEvent.from.username,
|
|
});
|
|
|
|
logger.info('[useRocketChatCalls] Call ended', {
|
|
roomId: callEvent.roomId,
|
|
currentCallRoomId: currentCallRoomIdRef.current,
|
|
from: callEvent.from.username,
|
|
});
|
|
|
|
// Remove notification if:
|
|
// 1. It matches the current call's roomId, OR
|
|
// 2. The call-ended event has no roomId (generic end event), OR
|
|
// 3. There's no current call tracked (shouldn't happen, but safety check)
|
|
const shouldRemove =
|
|
currentCallRoomIdRef.current === callEvent.roomId ||
|
|
!callEvent.roomId || // Generic call-ended event (no roomId) - match any active call
|
|
!currentCallRoomIdRef.current;
|
|
|
|
if (shouldRemove) {
|
|
console.log('[useRocketChatCalls] ✅ Removing notification for ended call', {
|
|
roomId: callEvent.roomId,
|
|
currentCallRoomId: currentCallRoomIdRef.current,
|
|
isGenericEnd: !callEvent.roomId,
|
|
});
|
|
|
|
// Clear the safety timeout since we received the call-ended event
|
|
if (callTimeoutRef.current) {
|
|
clearTimeout(callTimeoutRef.current);
|
|
callTimeoutRef.current = null;
|
|
}
|
|
|
|
setIncomingCall(null);
|
|
currentCallRoomIdRef.current = null;
|
|
} else {
|
|
console.log('[useRocketChatCalls] ⏭️ Ignoring call ended event - different room', {
|
|
endedRoomId: callEvent.roomId,
|
|
currentCallRoomId: currentCallRoomIdRef.current,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle incoming call events
|
|
if (callEvent.type === 'call-incoming') {
|
|
console.log('[useRocketChatCalls] 🎉 INCOMING CALL DETECTED!', {
|
|
from: callEvent.from.username,
|
|
roomId: callEvent.roomId,
|
|
roomName: callEvent.roomName,
|
|
});
|
|
|
|
logger.info('[useRocketChatCalls] Incoming call detected', {
|
|
from: callEvent.from.username,
|
|
roomId: callEvent.roomId,
|
|
});
|
|
|
|
// Show incoming call notification UI (Outlook-style rectangle)
|
|
setIncomingCall({
|
|
from: {
|
|
userId: callEvent.from.userId,
|
|
username: callEvent.from.username,
|
|
name: callEvent.from.name || callEvent.from.username,
|
|
},
|
|
roomId: callEvent.roomId,
|
|
roomName: callEvent.roomName || callEvent.roomId,
|
|
timestamp: callEvent.timestamp,
|
|
});
|
|
|
|
// Track the current call's roomId so we can match it when the call ends
|
|
currentCallRoomIdRef.current = callEvent.roomId;
|
|
|
|
// Set a safety timeout to clear the notification if no call-ended event is received
|
|
// This handles cases where RocketChat doesn't send the call-ended event
|
|
// Reduced to 45 seconds - if the call hasn't ended by then, it's likely the caller hung up
|
|
if (callTimeoutRef.current) {
|
|
clearTimeout(callTimeoutRef.current);
|
|
}
|
|
callTimeoutRef.current = setTimeout(() => {
|
|
console.log('[useRocketChatCalls] ⏰ Safety timeout: clearing call notification after 45 seconds', {
|
|
roomId: callEvent.roomId,
|
|
});
|
|
if (currentCallRoomIdRef.current === callEvent.roomId) {
|
|
setIncomingCall(null);
|
|
currentCallRoomIdRef.current = null;
|
|
}
|
|
callTimeoutRef.current = null;
|
|
}, 45000); // 45 seconds safety timeout (reduced from 2 minutes)
|
|
|
|
console.log('[useRocketChatCalls] 📞 Incoming call notification UI set', {
|
|
from: callEvent.from.username,
|
|
roomId: callEvent.roomId,
|
|
});
|
|
|
|
// Trigger notification badge
|
|
// For calls, we want to increment the existing count, not replace it
|
|
// So we fetch current count first, then increment
|
|
triggerNotification({
|
|
source: 'rocketchat',
|
|
count: 1, // This will be added to existing count in the registry
|
|
items: [
|
|
{
|
|
id: `call-${callEvent.roomId}-${Date.now()}`,
|
|
title: `📞 Appel entrant de ${callEvent.from.name || callEvent.from.username}`,
|
|
message: `Appel vidéo/audio dans ${callEvent.roomName || 'chat'}`,
|
|
link: `/parole?room=${callEvent.roomId}`,
|
|
timestamp: callEvent.timestamp,
|
|
metadata: {
|
|
type: 'call',
|
|
from: callEvent.from,
|
|
roomId: callEvent.roomId,
|
|
},
|
|
},
|
|
],
|
|
}).then(() => {
|
|
console.log('[useRocketChatCalls] ✅ Notification triggered successfully');
|
|
}).catch((error) => {
|
|
console.error('[useRocketChatCalls] ❌ Error triggering notification', error);
|
|
});
|
|
}
|
|
});
|
|
|
|
unsubscribeRef.current = unsubscribe;
|
|
console.log('[useRocketChatCalls] ✅ Call listener initialized and subscribed');
|
|
logger.debug('[useRocketChatCalls] Call listener initialized');
|
|
} else {
|
|
console.error('[useRocketChatCalls] ❌ Failed to initialize listener');
|
|
}
|
|
} catch (error) {
|
|
console.error('[useRocketChatCalls] Error initializing listener', error);
|
|
logger.error('[useRocketChatCalls] Error initializing listener', { error });
|
|
}
|
|
}, [getRocketChatCredentials, triggerNotification]);
|
|
|
|
/**
|
|
* Cleanup on unmount
|
|
*/
|
|
useEffect(() => {
|
|
if (session?.user?.id) {
|
|
initializeListener();
|
|
}
|
|
|
|
return () => {
|
|
if (unsubscribeRef.current) {
|
|
unsubscribeRef.current();
|
|
unsubscribeRef.current = null;
|
|
}
|
|
|
|
if (listenerRef.current) {
|
|
listenerRef.current.disconnect();
|
|
listenerRef.current = null;
|
|
}
|
|
|
|
if (callTimeoutRef.current) {
|
|
clearTimeout(callTimeoutRef.current);
|
|
callTimeoutRef.current = null;
|
|
}
|
|
|
|
initializedRef.current = false;
|
|
};
|
|
}, [session?.user?.id, initializeListener]);
|
|
|
|
/**
|
|
* Helper function to clear the incoming call notification
|
|
* This ensures we also clear the roomId reference and timeout
|
|
*/
|
|
const clearIncomingCall = useCallback(() => {
|
|
if (callTimeoutRef.current) {
|
|
clearTimeout(callTimeoutRef.current);
|
|
callTimeoutRef.current = null;
|
|
}
|
|
setIncomingCall(null);
|
|
currentCallRoomIdRef.current = null;
|
|
}, []);
|
|
|
|
return {
|
|
incomingCall,
|
|
setIncomingCall,
|
|
clearIncomingCall,
|
|
};
|
|
}
|