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
) {
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);
if (match) {
setActiveConversation(match);
@ -171,7 +181,7 @@ export default function InboxPage() {
}
}
},
[deepLinkConvId]
[deepLinkConvId, activeConversation?.id]
);
const handleSelectConversation = useCallback(
@ -184,6 +194,14 @@ export default function InboxPage() {
setActiveConversation(conv);
setActiveContact(conv.contact ?? null);
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
// back in the same thread, and so copy-paste links work. Use
// replace() to avoid polluting browser history with every click.

View File

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