- Migrated all hooks: useAuth, useChat, useLogin - Migrated all components: Header, ProfileForm, FollowButton, LikeButton, PlaylistFollowButton, ChatMessage, ChatMessages, CommentThread, CommentSection, PlaylistList, ChatSidebar, SettingsPage, DashboardPage - Updated storeSelectors.ts useAuthUser() to use React Query - All production code now uses useUser() hook instead of Zustand store - Action 4.1.1.3 and 4.1.1.4 complete
360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
|
import { useUser } from '@/features/auth/hooks/useUser';
|
|
import { useChatStore } from '../store/chatStore';
|
|
import { apiClient } from '@/services/api/client';
|
|
import { OutgoingMessage, IncomingMessage } from '../types';
|
|
import { v4 as uuidv4 } from 'uuid'; // For message IDs
|
|
import type { UseChatReturn } from '@/hooks/types';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
/**
|
|
* Hook pour gérer le chat WebSocket
|
|
* FE-TYPE-012: Fully typed hook return
|
|
*/
|
|
export const useChat = (): UseChatReturn => {
|
|
const { data: user } = useUser();
|
|
const userId = user?.id;
|
|
// const _username = user?.username;
|
|
const {
|
|
wsToken,
|
|
wsUrl,
|
|
wsStatus,
|
|
setWsStatus,
|
|
addMessage,
|
|
currentConversationId,
|
|
loadMessages,
|
|
addReaction,
|
|
removeReaction,
|
|
setUserTyping,
|
|
} = useChatStore();
|
|
|
|
const ws = useRef<WebSocket | null>(null);
|
|
// CRITIQUE FIX #9: Utiliser un useRef pour stocker le compteur d'erreurs de manière persistante
|
|
// au lieu de le stocker sur ws.current qui peut être réinitialisé
|
|
const errorCountRef = useRef(0);
|
|
// Queue for messages to send (reserved for future use)
|
|
const [_messagesToSend, setMessagesToSend] = useState<OutgoingMessage[]>([]);
|
|
|
|
const connect = useCallback(() => {
|
|
if (!wsToken || !wsUrl || ws.current?.readyState === WebSocket.OPEN) return;
|
|
|
|
// CRITIQUE FIX #9: Vérifier si le serveur WebSocket est disponible avant de tenter la connexion
|
|
// En développement, si le serveur n'est pas démarré, limiter les tentatives
|
|
if (import.meta.env.DEV && wsUrl.includes('127.0.0.1:8081')) {
|
|
// En dev, vérifier si on a déjà eu trop d'erreurs de connexion
|
|
if (errorCountRef.current >= 3) {
|
|
// Trop d'erreurs, ne pas essayer de se connecter pour éviter le spam console
|
|
setWsStatus('disconnected');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// CRITIQUE FIX #39: Nettoyer la connexion précédente si elle existe
|
|
if (ws.current) {
|
|
ws.current.onopen = null;
|
|
ws.current.onmessage = null;
|
|
ws.current.onclose = null;
|
|
ws.current.onerror = null;
|
|
if (
|
|
ws.current.readyState === WebSocket.OPEN ||
|
|
ws.current.readyState === WebSocket.CONNECTING
|
|
) {
|
|
ws.current.close();
|
|
}
|
|
}
|
|
|
|
setWsStatus('connecting');
|
|
const fullWsUrl = `${wsUrl}?token=${wsToken}`; // Assuming WS server is at root of wsUrl
|
|
ws.current = new WebSocket(fullWsUrl);
|
|
|
|
// CRITIQUE FIX #39: Stocker les références aux handlers pour pouvoir les nettoyer
|
|
const handleOpen = () => {
|
|
setWsStatus('connected');
|
|
// CRITIQUE FIX #9: Réinitialiser le compteur d'erreurs en cas de succès
|
|
errorCountRef.current = 0;
|
|
// WebSocket connection successful - no logging needed in production
|
|
// Send any queued messages
|
|
setMessagesToSend((prev) => {
|
|
prev.forEach((msg) => ws.current?.send(JSON.stringify(msg)));
|
|
return [];
|
|
});
|
|
};
|
|
|
|
const handleMessage = (event: MessageEvent) => {
|
|
const data = JSON.parse(event.data);
|
|
if (data.type === 'NewMessage') {
|
|
const message: IncomingMessage = data;
|
|
if (
|
|
message.conversation_id === currentConversationId &&
|
|
message.message_id &&
|
|
message.sender_id &&
|
|
message.content &&
|
|
message.created_at
|
|
) {
|
|
addMessage({
|
|
id: message.message_id,
|
|
conversation_id: message.conversation_id,
|
|
sender_id: message.sender_id,
|
|
sender_username: message.sender_username || 'Unknown',
|
|
content: message.content,
|
|
created_at: message.created_at,
|
|
attachments: message.attachments,
|
|
});
|
|
}
|
|
} else if (data.type === 'ReactionAdded') {
|
|
const reaction: IncomingMessage = data;
|
|
if (reaction.message_id && reaction.user_id && reaction.emoji) {
|
|
addReaction(
|
|
reaction.conversation_id,
|
|
reaction.message_id,
|
|
reaction.user_id,
|
|
reaction.emoji,
|
|
);
|
|
}
|
|
} else if (data.type === 'ReactionRemoved') {
|
|
const reaction: IncomingMessage = data;
|
|
if (reaction.message_id && reaction.user_id) {
|
|
removeReaction(
|
|
reaction.conversation_id,
|
|
reaction.message_id,
|
|
reaction.user_id,
|
|
);
|
|
}
|
|
} else if (data.type === 'UserTyping') {
|
|
const typing: IncomingMessage = data;
|
|
if (typing.user_id) {
|
|
setUserTyping(
|
|
typing.conversation_id,
|
|
typing.user_id,
|
|
typing.is_typing ?? false,
|
|
);
|
|
}
|
|
}
|
|
// Handle other incoming message types (ActionConfirmed, Error, Pong)
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setWsStatus('disconnected');
|
|
// WebSocket disconnected - no logging needed in production
|
|
// Optional: Reconnect logic
|
|
};
|
|
|
|
const handleError = (error: Event) => {
|
|
setWsStatus('error');
|
|
// CRITIQUE FIX #9: Limiter les logs d'erreur pour éviter le spam console
|
|
// En développement, si le serveur n'est pas démarré, limiter à 3 erreurs max
|
|
errorCountRef.current += 1;
|
|
if (errorCountRef.current <= 3) {
|
|
if (import.meta.env.DEV) {
|
|
logger.warn(
|
|
`[WebSocket] Connexion échouée (${errorCountRef.current}/3). Le serveur WebSocket n'est peut-être pas démarré.`,
|
|
{
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
},
|
|
);
|
|
} else {
|
|
logger.error('WebSocket error', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
}
|
|
}
|
|
// Don't close immediately - let onclose handle it
|
|
// ws.current?.close();
|
|
};
|
|
|
|
// CRITIQUE FIX #39: Attacher les handlers
|
|
ws.current.onopen = handleOpen;
|
|
ws.current.onmessage = handleMessage;
|
|
ws.current.onclose = handleClose;
|
|
ws.current.onerror = handleError;
|
|
}, [
|
|
wsToken,
|
|
wsUrl,
|
|
setWsStatus,
|
|
addMessage,
|
|
currentConversationId,
|
|
addReaction,
|
|
removeReaction,
|
|
setUserTyping,
|
|
]);
|
|
|
|
const disconnect = useCallback(() => {
|
|
// CRITIQUE FIX #39: Nettoyer proprement les event handlers avant de fermer
|
|
if (ws.current) {
|
|
ws.current.onopen = null;
|
|
ws.current.onmessage = null;
|
|
ws.current.onclose = null;
|
|
ws.current.onerror = null;
|
|
|
|
if (
|
|
ws.current.readyState === WebSocket.OPEN ||
|
|
ws.current.readyState === WebSocket.CONNECTING
|
|
) {
|
|
ws.current.close();
|
|
}
|
|
|
|
ws.current = null;
|
|
setWsStatus('disconnected');
|
|
}
|
|
}, [setWsStatus]);
|
|
|
|
// FE-BUG-003: Add a ref to track reconnection attempts and avoid infinite loops
|
|
const reconnectCount = useRef(0);
|
|
const maxReconnects = 5;
|
|
|
|
useEffect(() => {
|
|
let timer: NodeJS.Timeout | undefined;
|
|
|
|
if (
|
|
wsToken &&
|
|
wsUrl &&
|
|
wsStatus === 'disconnected' &&
|
|
reconnectCount.current < maxReconnects
|
|
) {
|
|
timer = setTimeout(
|
|
() => {
|
|
reconnectCount.current++;
|
|
connect();
|
|
},
|
|
1000 * Math.pow(2, reconnectCount.current),
|
|
); // Exponential backoff
|
|
}
|
|
|
|
if (wsStatus === 'connected') {
|
|
reconnectCount.current = 0; // Reset on success
|
|
}
|
|
|
|
return () => {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
};
|
|
}, [wsToken, wsUrl, wsStatus, connect]);
|
|
|
|
useEffect(() => {
|
|
// Clean up on unmount
|
|
return () => {
|
|
disconnect();
|
|
};
|
|
}, [disconnect]);
|
|
|
|
const sendMessage = useCallback(
|
|
(content: string, attachments?: import('../types').MessageAttachment[]) => {
|
|
if (
|
|
!ws.current ||
|
|
ws.current.readyState !== WebSocket.OPEN ||
|
|
!currentConversationId ||
|
|
!userId
|
|
) {
|
|
// WebSocket not ready - message will be queued
|
|
logger.warn(
|
|
'WebSocket not open or missing conversation/user ID. Message queued.',
|
|
{
|
|
conversationId: currentConversationId,
|
|
userId,
|
|
},
|
|
);
|
|
setMessagesToSend((prev) => [
|
|
...prev,
|
|
{
|
|
type: 'SendMessage',
|
|
conversation_id: currentConversationId || uuidv4(),
|
|
content,
|
|
parent_message_id: null,
|
|
attachments,
|
|
} as OutgoingMessage,
|
|
]);
|
|
return;
|
|
}
|
|
|
|
const message: OutgoingMessage = {
|
|
type: 'SendMessage',
|
|
conversation_id: currentConversationId,
|
|
content,
|
|
parent_message_id: null,
|
|
attachments,
|
|
};
|
|
ws.current.send(JSON.stringify(message));
|
|
},
|
|
[currentConversationId, userId],
|
|
);
|
|
|
|
// TODO: Add fetchHistory function
|
|
const fetchHistory = useCallback(
|
|
async (conversationId: string) => {
|
|
try {
|
|
const response = await apiClient.get(
|
|
`/conversations/${conversationId}/history`,
|
|
);
|
|
loadMessages(conversationId, response.data.messages);
|
|
} catch (error) {
|
|
logger.error('Failed to fetch chat history', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
conversationId,
|
|
});
|
|
}
|
|
},
|
|
[loadMessages],
|
|
);
|
|
|
|
const addReactionFunc = useCallback(
|
|
(messageId: string, emoji: string) => {
|
|
if (ws.current?.readyState === WebSocket.OPEN && currentConversationId) {
|
|
ws.current.send(
|
|
JSON.stringify({
|
|
type: 'AddReaction',
|
|
conversation_id: currentConversationId,
|
|
message_id: messageId,
|
|
emoji,
|
|
} as OutgoingMessage),
|
|
);
|
|
}
|
|
},
|
|
[currentConversationId],
|
|
);
|
|
|
|
const removeReactionFunc = useCallback(
|
|
(messageId: string) => {
|
|
if (ws.current?.readyState === WebSocket.OPEN && currentConversationId) {
|
|
ws.current.send(
|
|
JSON.stringify({
|
|
type: 'RemoveReaction',
|
|
conversation_id: currentConversationId,
|
|
message_id: messageId,
|
|
} as OutgoingMessage),
|
|
);
|
|
}
|
|
},
|
|
[currentConversationId],
|
|
);
|
|
|
|
const setTyping = useCallback(
|
|
(isTyping: boolean) => {
|
|
if (ws.current?.readyState === WebSocket.OPEN && currentConversationId) {
|
|
ws.current.send(
|
|
JSON.stringify({
|
|
type: 'Typing',
|
|
conversation_id: currentConversationId,
|
|
is_typing: isTyping,
|
|
} as OutgoingMessage),
|
|
);
|
|
}
|
|
},
|
|
[currentConversationId],
|
|
);
|
|
|
|
return {
|
|
wsStatus,
|
|
connect,
|
|
disconnect,
|
|
sendMessage,
|
|
fetchHistory,
|
|
addReaction: addReactionFunc,
|
|
removeReaction: removeReactionFunc,
|
|
setTyping,
|
|
};
|
|
};
|