Merge pull request #11 from ArnasDon/fix/inbox-refresh-loop-and-banner

fix: inbox refresh loop, wrong connection status, overlapping banner
This commit is contained in:
Arnas Donauskas 2026-04-16 21:13:40 +03:00 committed by GitHub
commit 2446056842
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 82 additions and 52 deletions

View File

@ -8,7 +8,7 @@ import { ConversationList } from "@/components/inbox/conversation-list";
import { MessageThread } from "@/components/inbox/message-thread"; import { MessageThread } from "@/components/inbox/message-thread";
import { ContactSidebar } from "@/components/inbox/contact-sidebar"; import { ContactSidebar } from "@/components/inbox/contact-sidebar";
import { toast } from "sonner"; import { toast } from "sonner";
import { Wifi, WifiOff } from "lucide-react"; import { WifiOff } from "lucide-react";
export default function InboxPage() { export default function InboxPage() {
const [conversations, setConversations] = useState<Conversation[]>([]); const [conversations, setConversations] = useState<Conversation[]>([]);
@ -31,11 +31,13 @@ export default function InboxPage() {
if (!user) return; 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 const { data } = await supabase
.from("whatsapp_configs") .from("whatsapp_config")
.select("status") .select("status")
.eq("user_id", user.id) .eq("user_id", user.id)
.single(); .maybeSingle();
setWhatsappConnected(data?.status === "connected"); setWhatsappConnected(data?.status === "connected");
}; };
@ -167,10 +169,11 @@ export default function InboxPage() {
); );
return ( return (
<div className="-m-6 flex h-[calc(100vh-3.5rem)] overflow-hidden"> <div className="-m-6 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden">
{/* WhatsApp connection banner */} {/* WhatsApp connection banner — in the flex column, not absolute,
so it pushes the panels down instead of overlapping them. */}
{whatsappConnected === false && ( {whatsappConnected === false && (
<div className="absolute left-60 right-0 top-14 z-10 flex items-center justify-center gap-2 bg-amber-500/10 px-4 py-2"> <div className="flex shrink-0 items-center justify-center gap-2 border-b border-amber-500/20 bg-amber-500/10 px-4 py-2">
<WifiOff className="h-4 w-4 text-amber-400" /> <WifiOff className="h-4 w-4 text-amber-400" />
<p className="text-xs text-amber-400"> <p className="text-xs text-amber-400">
WhatsApp is not connected. Go to Settings to connect your account. WhatsApp is not connected. Go to Settings to connect your account.
@ -178,27 +181,29 @@ export default function InboxPage() {
</div> </div>
)} )}
{/* Left panel: Conversation list */} <div className="flex flex-1 overflow-hidden">
<ConversationList {/* Left panel: Conversation list */}
activeConversationId={activeConversation?.id ?? null} <ConversationList
onSelect={handleSelectConversation} activeConversationId={activeConversation?.id ?? null}
conversations={conversations} onSelect={handleSelectConversation}
onConversationsLoaded={handleConversationsLoaded} conversations={conversations}
/> onConversationsLoaded={handleConversationsLoaded}
/>
{/* Center panel: Message thread */} {/* Center panel: Message thread */}
<MessageThread <MessageThread
conversation={activeConversation} conversation={activeConversation}
contact={activeContact} contact={activeContact}
messages={messages} messages={messages}
onMessagesLoaded={handleMessagesLoaded} onMessagesLoaded={handleMessagesLoaded}
onNewMessage={handleNewMessage} onNewMessage={handleNewMessage}
onStatusChange={handleStatusChange} onStatusChange={handleStatusChange}
/> />
{/* Right panel: Contact sidebar (hidden on small screens) */} {/* Right panel: Contact sidebar (hidden on small screens) */}
<div className="hidden lg:block"> <div className="hidden lg:block">
<ContactSidebar contact={activeContact} /> <ContactSidebar contact={activeContact} />
</div>
</div> </div>
</div> </div>
); );

View File

@ -101,36 +101,61 @@ export function MessageThread({
return { expired, remaining }; return { expired, remaining };
}, [messages]); }, [messages]);
const fetchMessages = useCallback(async () => { // Store latest callback in a ref so fetchMessages doesn't need to
if (!conversation) return; // depend on `onMessagesLoaded` — otherwise parent re-renders cause
setLoading(true); // 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 conversationId = conversation?.id;
const hasUnread = (conversation?.unread_count ?? 0) > 0;
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]);
useEffect(() => { useEffect(() => {
fetchMessages(); if (!conversationId) return;
}, [fetchMessages]);
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 // Auto-scroll to bottom on new messages
useEffect(() => { useEffect(() => {