Merge pull request #70 from ArnasDon/fix/inbox-messages-stale-after-deeplink

fix(inbox): don't drop messages when ?c= URL param updates
This commit is contained in:
Arnas Donauskas 2026-04-20 20:04:32 +03:00 committed by GitHub
commit 7182abe7d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 61 additions and 26 deletions

View File

@ -163,6 +163,16 @@ export default function InboxPage() {
loaded.length > 0 loaded.length > 0
) { ) {
autoSelectedForDeepLinkRef.current = deepLinkConvId; autoSelectedForDeepLinkRef.current = deepLinkConvId;
// If the deep-linked conversation is already the active one
// (e.g. because the user clicked it in the list and we
// router.replace()'d the URL, which made the ConversationList
// refetch and land us back here), do NOT re-apply it. Doing so
// would setMessages([]) on a thread whose messages have
// already been loaded by MessageThread — and because
// conversationId didn't change, MessageThread wouldn't
// refetch. The thread would read "No messages yet" until a
// full page reload rehydrated state from scratch.
if (activeConversation?.id === deepLinkConvId) return;
const match = loaded.find((c) => c.id === deepLinkConvId); const match = loaded.find((c) => c.id === deepLinkConvId);
if (match) { if (match) {
setActiveConversation(match); setActiveConversation(match);
@ -171,7 +181,7 @@ export default function InboxPage() {
} }
} }
}, },
[deepLinkConvId] [deepLinkConvId, activeConversation?.id]
); );
const handleSelectConversation = useCallback( const handleSelectConversation = useCallback(
@ -184,6 +194,14 @@ export default function InboxPage() {
setActiveConversation(conv); setActiveConversation(conv);
setActiveContact(conv.contact ?? null); setActiveContact(conv.contact ?? null);
setMessages([]); setMessages([]);
// Record the selection on the deep-link ref BEFORE we change the
// URL. The router.replace below flips `deepLinkConvId`, which can
// in turn cause ConversationList to refetch and eventually call
// handleConversationsLoaded again. Without this line, the ref
// still points at the previous value, the auto-select block
// sees `ref !== deepLinkConvId`, fires a second time, and
// clobbers the messages MessageThread just fetched.
autoSelectedForDeepLinkRef.current = conv.id;
// Reflect the selection in the URL so a refresh lands the user // Reflect the selection in the URL so a refresh lands the user
// back in the same thread, and so copy-paste links work. Use // back in the same thread, and so copy-paste links work. Use
// replace() to avoid polluting browser history with every click. // replace() to avoid polluting browser history with every click.

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useCallback, useMemo } from "react"; import { useState, useEffect, useCallback, useMemo, useRef } from "react";
import { createClient } from "@/lib/supabase/client"; import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { Conversation, ConversationStatus } from "@/types"; import type { Conversation, ConversationStatus } from "@/types";
@ -46,32 +46,49 @@ export function ConversationList({
const [filter, setFilter] = useState<ConversationStatus | "all">("all"); const [filter, setFilter] = useState<ConversationStatus | "all">("all");
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const fetchConversations = useCallback(async () => { // Keep the latest callback in a ref so the fetch effect below can
const supabase = createClient(); // have a stable, empty-dep identity. Previously the fetch useCallback
const { data, error } = await supabase // depended on `onConversationsLoaded`, which depends on the parent's
.from("conversations") // `deepLinkConvId` — so every URL change (including one the parent
.select("*, contact:contacts(*)") // triggered via router.replace after a click) caused a fresh
.order("last_message_at", { ascending: false }); // conversations fetch. That extra refetch was the trigger for the
// deep-link auto-select running a second time and wiping the active
if (error) { // thread's messages.
// Supabase errors have non-enumerable properties — log fields explicitly const onConversationsLoadedRef = useRef(onConversationsLoaded);
console.error("Failed to fetch conversations:", { onConversationsLoadedRef.current = onConversationsLoaded;
message: error.message,
details: error.details,
hint: error.hint,
code: error.code,
});
setLoading(false);
return;
}
onConversationsLoaded(data ?? []);
setLoading(false);
}, [onConversationsLoaded]);
useEffect(() => { useEffect(() => {
fetchConversations(); const supabase = createClient();
}, [fetchConversations]); let cancelled = false;
(async () => {
const { data, error } = await supabase
.from("conversations")
.select("*, contact:contacts(*)")
.order("last_message_at", { ascending: false });
if (cancelled) return;
if (error) {
// Supabase errors have non-enumerable properties — log fields explicitly
console.error("Failed to fetch conversations:", {
message: error.message,
details: error.details,
hint: error.hint,
code: error.code,
});
setLoading(false);
return;
}
onConversationsLoadedRef.current(data ?? []);
setLoading(false);
})();
return () => {
cancelled = true;
};
}, []);
const filtered = useMemo(() => { const filtered = useMemo(() => {
let result = conversations; let result = conversations;