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 { 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<Conversation[]>([]);
@ -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 (
<div className="-m-6 flex h-[calc(100vh-3.5rem)] overflow-hidden">
{/* WhatsApp connection banner */}
<div className="-m-6 flex h-[calc(100vh-3.5rem)] flex-col overflow-hidden">
{/* WhatsApp connection banner — in the flex column, not absolute,
so it pushes the panels down instead of overlapping them. */}
{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" />
<p className="text-xs text-amber-400">
WhatsApp is not connected. Go to Settings to connect your account.
@ -178,27 +181,29 @@ export default function InboxPage() {
</div>
)}
{/* Left panel: Conversation list */}
<ConversationList
activeConversationId={activeConversation?.id ?? null}
onSelect={handleSelectConversation}
conversations={conversations}
onConversationsLoaded={handleConversationsLoaded}
/>
<div className="flex flex-1 overflow-hidden">
{/* Left panel: Conversation list */}
<ConversationList
activeConversationId={activeConversation?.id ?? null}
onSelect={handleSelectConversation}
conversations={conversations}
onConversationsLoaded={handleConversationsLoaded}
/>
{/* Center panel: Message thread */}
<MessageThread
conversation={activeConversation}
contact={activeContact}
messages={messages}
onMessagesLoaded={handleMessagesLoaded}
onNewMessage={handleNewMessage}
onStatusChange={handleStatusChange}
/>
{/* Center panel: Message thread */}
<MessageThread
conversation={activeConversation}
contact={activeContact}
messages={messages}
onMessagesLoaded={handleMessagesLoaded}
onNewMessage={handleNewMessage}
onStatusChange={handleStatusChange}
/>
{/* Right panel: Contact sidebar (hidden on small screens) */}
<div className="hidden lg:block">
<ContactSidebar contact={activeContact} />
{/* Right panel: Contact sidebar (hidden on small screens) */}
<div className="hidden lg:block">
<ContactSidebar contact={activeContact} />
</div>
</div>
</div>
);

View File

@ -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(() => {