mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Merge pull request #105 from ArnasDon/fix/inbox-realtime-hydrate-new-conversations
fix(inbox): hydrate new conversations from realtime so they surface
This commit is contained in:
commit
aecad2e769
@ -37,6 +37,64 @@ export default function InboxPage() {
|
|||||||
// elsewhere.
|
// elsewhere.
|
||||||
const autoSelectedForDeepLinkRef = useRef<string | null>(null);
|
const autoSelectedForDeepLinkRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
// Tracks conversations whose hydrate fetch is currently in flight. The
|
||||||
|
// conv-INSERT and the first-message-INSERT events both call into
|
||||||
|
// hydrateConversation; the dedupe here keeps it at one refetch per
|
||||||
|
// new conversation even when both events arrive within milliseconds.
|
||||||
|
const hydratingConvIdsRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Pull the conversation row with its `contact` joined and merge it
|
||||||
|
// into state. Needed because Supabase Realtime payloads only carry the
|
||||||
|
// row's own columns — a brand-new conversation arrives without a
|
||||||
|
// contact, which surfaced as "Unknown" names, empty avatars, and
|
||||||
|
// (when the conv-INSERT event was delayed past the message-INSERT)
|
||||||
|
// conversations stuck on "No messages yet" until the user reloaded.
|
||||||
|
// Also self-heals if a realtime event was missed: callers can invoke
|
||||||
|
// this whenever they reference a conversation id they don't recognise.
|
||||||
|
const hydrateConversation = useCallback(async (convId: string) => {
|
||||||
|
if (hydratingConvIdsRef.current.has(convId)) return;
|
||||||
|
hydratingConvIdsRef.current.add(convId);
|
||||||
|
try {
|
||||||
|
const supabase = createClient();
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from("conversations")
|
||||||
|
.select("*, contact:contacts(*)")
|
||||||
|
.eq("id", convId)
|
||||||
|
.maybeSingle();
|
||||||
|
if (error) {
|
||||||
|
// Supabase errors have non-enumerable properties — log fields
|
||||||
|
// explicitly so the console message isn't just `{}`.
|
||||||
|
console.error("Failed to hydrate conversation:", {
|
||||||
|
message: error.message,
|
||||||
|
details: error.details,
|
||||||
|
hint: error.hint,
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data) return;
|
||||||
|
const fetched = data as Conversation;
|
||||||
|
setConversations((prev) => {
|
||||||
|
const existing = prev.find((c) => c.id === fetched.id);
|
||||||
|
if (existing) {
|
||||||
|
// Already in state — keep its fields (a realtime UPDATE may
|
||||||
|
// have landed while the fetch was in flight and patched
|
||||||
|
// last_message_text / unread_count to fresher values than
|
||||||
|
// the row we just read). Only backfill `contact`, which the
|
||||||
|
// realtime payloads never carry.
|
||||||
|
return prev.map((c) =>
|
||||||
|
c.id === fetched.id
|
||||||
|
? { ...c, contact: c.contact ?? fetched.contact }
|
||||||
|
: c,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [fetched, ...prev];
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
hydratingConvIdsRef.current.delete(convId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Check WhatsApp connection status on mount
|
// Check WhatsApp connection status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkConnection = async () => {
|
const checkConnection = async () => {
|
||||||
@ -84,9 +142,18 @@ export default function InboxPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update conversation list preview
|
// Update conversation list preview. If the conv isn't in state
|
||||||
setConversations((prev) =>
|
// yet (its INSERT event was missed, delayed, or this is the
|
||||||
prev.map((c) =>
|
// first message for a conversation created out-of-band) the
|
||||||
|
// map() below is a no-op and the preview would never appear —
|
||||||
|
// hydrate the row from the DB instead so the inbox self-heals.
|
||||||
|
let foundInList = false;
|
||||||
|
setConversations((prev) => {
|
||||||
|
if (!prev.some((c) => c.id === newMsg.conversation_id)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
foundInList = true;
|
||||||
|
return prev.map((c) =>
|
||||||
c.id === newMsg.conversation_id
|
c.id === newMsg.conversation_id
|
||||||
? {
|
? {
|
||||||
...c,
|
...c,
|
||||||
@ -97,9 +164,12 @@ export default function InboxPage() {
|
|||||||
? 0
|
? 0
|
||||||
: c.unread_count + 1,
|
: c.unread_count + 1,
|
||||||
}
|
}
|
||||||
: c
|
: c,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
if (!foundInList) {
|
||||||
|
hydrateConversation(newMsg.conversation_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.eventType === "UPDATE") {
|
if (event.eventType === "UPDATE") {
|
||||||
@ -109,7 +179,7 @@ export default function InboxPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeConversation]
|
[activeConversation, hydrateConversation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle realtime conversation events
|
// Handle realtime conversation events
|
||||||
@ -122,13 +192,29 @@ export default function InboxPage() {
|
|||||||
const conv = event.new;
|
const conv = event.new;
|
||||||
|
|
||||||
if (event.eventType === "INSERT") {
|
if (event.eventType === "INSERT") {
|
||||||
setConversations((prev) => [conv, ...prev]);
|
// Prepend immediately for snappy UX, then hydrate to fill in
|
||||||
|
// the `contact` join (realtime payloads omit it). The dedupe
|
||||||
|
// inside hydrateConversation keeps the result idempotent if
|
||||||
|
// the first-message-INSERT handler also triggers a hydrate.
|
||||||
|
setConversations((prev) => {
|
||||||
|
if (prev.some((c) => c.id === conv.id)) return prev;
|
||||||
|
return [conv, ...prev];
|
||||||
|
});
|
||||||
|
hydrateConversation(conv.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.eventType === "UPDATE") {
|
if (event.eventType === "UPDATE") {
|
||||||
setConversations((prev) =>
|
let foundInList = false;
|
||||||
prev.map((c) => (c.id === conv.id ? { ...c, ...conv } : c))
|
setConversations((prev) => {
|
||||||
);
|
if (!prev.some((c) => c.id === conv.id)) return prev;
|
||||||
|
foundInList = true;
|
||||||
|
return prev.map((c) => (c.id === conv.id ? { ...c, ...conv } : c));
|
||||||
|
});
|
||||||
|
if (!foundInList) {
|
||||||
|
// UPDATE arrived before the INSERT (or after a missed
|
||||||
|
// INSERT) — fetch the row so it surfaces with its contact.
|
||||||
|
hydrateConversation(conv.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Update active conversation if it changed
|
// Update active conversation if it changed
|
||||||
if (activeConversation && conv.id === activeConversation.id) {
|
if (activeConversation && conv.id === activeConversation.id) {
|
||||||
@ -138,7 +224,7 @@ export default function InboxPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeConversation]
|
[activeConversation, hydrateConversation]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Subscribe to realtime
|
// Subscribe to realtime
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user