diff --git a/src/app/(dashboard)/inbox/page.tsx b/src/app/(dashboard)/inbox/page.tsx index 8488883..61968cd 100644 --- a/src/app/(dashboard)/inbox/page.tsx +++ b/src/app/(dashboard)/inbox/page.tsx @@ -8,7 +8,7 @@ import { ConversationList } from "@/components/inbox/conversation-list"; import { MessageThread } from "@/components/inbox/message-thread"; import { ContactSidebar } from "@/components/inbox/contact-sidebar"; import { toast } from "sonner"; -import { Wifi, WifiOff } from "lucide-react"; +import { WifiOff } from "lucide-react"; export default function InboxPage() { const [conversations, setConversations] = useState([]); @@ -31,11 +31,13 @@ export default function InboxPage() { if (!user) return; + // Table is `whatsapp_config` (singular) — the previous "whatsapp_configs" + // query always returned no rows, so the banner always showed "not connected". const { data } = await supabase - .from("whatsapp_configs") + .from("whatsapp_config") .select("status") .eq("user_id", user.id) - .single(); + .maybeSingle(); setWhatsappConnected(data?.status === "connected"); }; @@ -167,10 +169,11 @@ export default function InboxPage() { ); return ( -
- {/* WhatsApp connection banner */} +
+ {/* WhatsApp connection banner — in the flex column, not absolute, + so it pushes the panels down instead of overlapping them. */} {whatsappConnected === false && ( -
+

WhatsApp is not connected. Go to Settings to connect your account. @@ -178,27 +181,29 @@ export default function InboxPage() {

)} - {/* Left panel: Conversation list */} - +
+ {/* Left panel: Conversation list */} + - {/* Center panel: Message thread */} - + {/* Center panel: Message thread */} + - {/* Right panel: Contact sidebar (hidden on small screens) */} -
- + {/* Right panel: Contact sidebar (hidden on small screens) */} +
+ +
); diff --git a/src/components/inbox/message-thread.tsx b/src/components/inbox/message-thread.tsx index 0465200..439b29f 100644 --- a/src/components/inbox/message-thread.tsx +++ b/src/components/inbox/message-thread.tsx @@ -101,36 +101,61 @@ export function MessageThread({ return { expired, remaining }; }, [messages]); - const fetchMessages = useCallback(async () => { - if (!conversation) return; - setLoading(true); + // Store latest callback in a ref so fetchMessages doesn't need to + // depend on `onMessagesLoaded` — otherwise parent re-renders cause + // fetchMessages to change → useEffect re-fires → refetch → realtime + // UPDATE on conversations.unread_count → parent re-renders → LOOP. + const onMessagesLoadedRef = useRef(onMessagesLoaded); + onMessagesLoadedRef.current = onMessagesLoaded; - const supabase = createClient(); - - const { data, error } = await supabase - .from("messages") - .select("*") - .eq("conversation_id", conversation.id) - .order("created_at", { ascending: true }); - - if (error) { - console.error("Failed to fetch messages:", error); - } else { - onMessagesLoaded(data ?? []); - } - - // Reset unread count - await supabase - .from("conversations") - .update({ unread_count: 0 }) - .eq("id", conversation.id); - - setLoading(false); - }, [conversation, onMessagesLoaded]); + const conversationId = conversation?.id; + const hasUnread = (conversation?.unread_count ?? 0) > 0; useEffect(() => { - fetchMessages(); - }, [fetchMessages]); + if (!conversationId) return; + + const supabase = createClient(); + let cancelled = false; + + (async () => { + setLoading(true); + + const { data, error } = await supabase + .from("messages") + .select("*") + .eq("conversation_id", conversationId) + .order("created_at", { ascending: true }); + + if (cancelled) return; + + if (error) { + console.error("Failed to fetch messages:", error); + } else { + onMessagesLoadedRef.current(data ?? []); + } + + // Only issue the unread_count reset when there's actually something + // to reset. Unconditional updates fire a realtime UPDATE event every + // time, which — combined with the parent's conversation-event handler + // — used to retrigger this effect in a loop. + if (hasUnread) { + await supabase + .from("conversations") + .update({ unread_count: 0 }) + .eq("id", conversationId); + } + + if (!cancelled) setLoading(false); + })(); + + return () => { + cancelled = true; + }; + // Re-fetch only when the selected conversation changes. `hasUnread` + // is used inside but is a boolean derived from conversation; the + // conversationId dep is the real trigger. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [conversationId]); // Auto-scroll to bottom on new messages useEffect(() => {