NeahStable/hooks/use-rocketchat-calls.ts
2026-01-21 15:56:27 +01:00

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,
};
}