feat(market): add Minha Intenções tab with panel, hook, and badge
- Introduced "Minhas Intenções" tab visible only to logged-in users. - Integrated MyPurchaseIntentionsPanel and useMyPurchaseIntentions hook. - Added tab badge showing count of pending intentions. - Extended session API to include user’s main character for downstream features.
This commit is contained in:
1 parent
f35fee944d
commit
7f42def478
55 files changed
+4906
-95
No files matched your search
@@ -0,0 +1,275 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSocket } from "./use-socket";
|
||||
import { useSession } from "./use-session";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
||||
export interface ChatMessage {
|
||||
_id: string;
|
||||
chat_id: string;
|
||||
sender_id: number;
|
||||
content: string;
|
||||
created_at: string;
|
||||
read_at: string | null;
|
||||
edited_at: string | null;
|
||||
message_type: "text" | "system";
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
export interface Chat {
|
||||
chat_id: string;
|
||||
listing_id: number;
|
||||
buyer_id: number;
|
||||
seller_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_message_at: string | null;
|
||||
last_message_preview: string | null;
|
||||
buyer_unread_count: number;
|
||||
seller_unread_count: number;
|
||||
is_active: boolean;
|
||||
listing_cache: {
|
||||
item_name: string;
|
||||
item_image_url?: string;
|
||||
price_pix?: number;
|
||||
price_tibia_coins?: number;
|
||||
price_gold_coins?: number;
|
||||
server_name: string;
|
||||
listing_status: string;
|
||||
};
|
||||
buyer_cache: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
character_name?: string;
|
||||
};
|
||||
seller_cache: {
|
||||
user_id: number;
|
||||
username: string;
|
||||
character_name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UseChatReturn {
|
||||
chat: Chat | null;
|
||||
messages: ChatMessage[];
|
||||
isLoading: boolean;
|
||||
isSending: boolean;
|
||||
error: string | null;
|
||||
sendMessage: (content: string) => Promise<void>;
|
||||
loadMoreMessages: () => Promise<void>;
|
||||
hasMore: boolean;
|
||||
isTyping: boolean;
|
||||
otherUserTyping: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook para gerenciar um chat específico
|
||||
*/
|
||||
export function useChat(chatId: string | null): UseChatReturn {
|
||||
const { user } = useSession();
|
||||
const { socket, isConnected } = useSocket();
|
||||
const queryClient = useQueryClient();
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [isSending, setIsSending] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [otherUserTyping, setOtherUserTyping] = useState(false);
|
||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Buscar dados do chat
|
||||
const { data: chatData, isLoading: isLoadingChat } = useQuery<{
|
||||
success: boolean;
|
||||
chat: Chat;
|
||||
}>({
|
||||
queryKey: ["chat", chatId],
|
||||
queryFn: async () => {
|
||||
if (!chatId) return null;
|
||||
const res = await fetch(`/api/chats/${chatId}`);
|
||||
if (!res.ok) throw new Error("Erro ao buscar chat");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!chatId && !!user,
|
||||
staleTime: 1000 * 30,
|
||||
});
|
||||
|
||||
// Buscar mensagens
|
||||
const { data: messagesData, isLoading: isLoadingMessages } = useQuery<{
|
||||
success: boolean;
|
||||
messages: ChatMessage[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}>({
|
||||
queryKey: ["chat-messages", chatId, page],
|
||||
queryFn: async () => {
|
||||
if (!chatId) return null;
|
||||
const res = await fetch(`/api/chats/${chatId}/messages?page=${page}&limit=50`);
|
||||
if (!res.ok) throw new Error("Erro ao buscar mensagens");
|
||||
return res.json();
|
||||
},
|
||||
enabled: !!chatId && !!user,
|
||||
staleTime: 1000 * 10,
|
||||
});
|
||||
|
||||
// Atualizar mensagens quando dados mudarem
|
||||
useEffect(() => {
|
||||
if (messagesData?.messages) {
|
||||
if (page === 1) {
|
||||
setMessages(messagesData.messages);
|
||||
} else {
|
||||
// Adicionar mensagens antigas no início
|
||||
setMessages((prev) => [...messagesData.messages, ...prev]);
|
||||
}
|
||||
setHasMore(messagesData.pagination.page < messagesData.pagination.totalPages);
|
||||
}
|
||||
}, [messagesData, page]);
|
||||
|
||||
// Conectar ao Socket.io quando chatId mudar
|
||||
useEffect(() => {
|
||||
if (!socket || !isConnected || !chatId) return;
|
||||
|
||||
// Entrar no chat
|
||||
socket.emit("join_chat", { chat_id: chatId });
|
||||
|
||||
// Listener para novas mensagens
|
||||
const handleNewMessage = (data: { message: ChatMessage }) => {
|
||||
console.log("[CHAT-DEBUG] useChat: new_message recebido", { message: data.message });
|
||||
|
||||
// Verificar se a mensagem já existe para evitar duplicação
|
||||
setMessages((prev) => {
|
||||
const messageExists = prev.some((msg) => msg._id === data.message._id);
|
||||
if (messageExists) {
|
||||
console.log("[CHAT-DEBUG] Mensagem já existe, ignorando duplicação", { messageId: data.message._id });
|
||||
return prev;
|
||||
}
|
||||
return [...prev, data.message];
|
||||
});
|
||||
|
||||
// Invalidar cache
|
||||
queryClient.invalidateQueries({ queryKey: ["chat-messages", chatId] });
|
||||
queryClient.invalidateQueries({ queryKey: ["chat", chatId] });
|
||||
};
|
||||
|
||||
// Listener para indicador de digitação
|
||||
const handleTyping = (data: { user_id: number; username: string; is_typing: boolean }) => {
|
||||
if (data.user_id !== user?.userId) {
|
||||
setOtherUserTyping(data.is_typing);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("new_message", handleNewMessage);
|
||||
socket.on("user_typing", handleTyping);
|
||||
|
||||
return () => {
|
||||
socket.off("new_message", handleNewMessage);
|
||||
socket.off("user_typing", handleTyping);
|
||||
};
|
||||
}, [socket, isConnected, chatId, user, queryClient]);
|
||||
|
||||
// Função para enviar mensagem
|
||||
const sendMessage = useCallback(
|
||||
async (content: string) => {
|
||||
if (!chatId || !socket || !isConnected || !content.trim()) return;
|
||||
|
||||
console.log("[CHAT-DEBUG] sendMessage chamado", { chatId, content, socketConnected: isConnected });
|
||||
|
||||
setIsSending(true);
|
||||
try {
|
||||
// Enviar via Socket.io
|
||||
socket.emit("send_message", {
|
||||
chat_id: chatId,
|
||||
content: content.trim(),
|
||||
});
|
||||
|
||||
// REMOVIDO: Otimistic update que causava duplicação
|
||||
// A mensagem será adicionada automaticamente quando o servidor emitir "new_message"
|
||||
|
||||
// Parar de digitar
|
||||
setIsTyping(false);
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
socket.emit("typing", { chat_id: chatId, is_typing: false });
|
||||
} catch (error) {
|
||||
console.error("[CHAT-DEBUG] Erro ao enviar mensagem:", error);
|
||||
} finally {
|
||||
setIsSending(false);
|
||||
}
|
||||
},
|
||||
[chatId, socket, isConnected, user]
|
||||
);
|
||||
|
||||
// Função para carregar mais mensagens
|
||||
const loadMoreMessages = useCallback(async () => {
|
||||
if (!hasMore || isLoadingMessages) return;
|
||||
setPage((prev) => prev + 1);
|
||||
}, [hasMore, isLoadingMessages]);
|
||||
|
||||
|
||||
return {
|
||||
chat: chatData?.chat || null,
|
||||
messages,
|
||||
isLoading: isLoadingChat || isLoadingMessages,
|
||||
isSending,
|
||||
error: null,
|
||||
sendMessage,
|
||||
loadMoreMessages,
|
||||
hasMore,
|
||||
isTyping,
|
||||
otherUserTyping,
|
||||
};
|
||||
}
|
||||
|
||||
// Exportar handlers de digitação separadamente se necessário
|
||||
export function useChatTyping(
|
||||
socket: Socket | null,
|
||||
isConnected: boolean,
|
||||
chatId: string | null
|
||||
) {
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const handleTypingStart = useCallback(() => {
|
||||
if (!socket || !isConnected || !chatId || isTyping) return;
|
||||
|
||||
setIsTyping(true);
|
||||
socket.emit("typing", { chat_id: chatId, is_typing: true });
|
||||
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
|
||||
typingTimeoutRef.current = setTimeout(() => {
|
||||
setIsTyping(false);
|
||||
socket.emit("typing", { chat_id: chatId, is_typing: false });
|
||||
}, 3000);
|
||||
}, [socket, isConnected, chatId, isTyping]);
|
||||
|
||||
const handleTypingStop = useCallback(() => {
|
||||
if (!socket || !isConnected || !chatId || !isTyping) return;
|
||||
|
||||
setIsTyping(false);
|
||||
socket.emit("typing", { chat_id: chatId, is_typing: false });
|
||||
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
}, [socket, isConnected, chatId, isTyping]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (typingTimeoutRef.current) {
|
||||
clearTimeout(typingTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { handleTypingStart, handleTypingStop };
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user