From 381d14d195636f518011789706d0d61aa2fc855c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:52:43 +0000 Subject: [PATCH 1/3] Initial plan From e48b5780948bbe9654111f349da4d5989b44a432 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:57:52 +0000 Subject: [PATCH 2/3] Fix realtime updates for EmailsList sidebar and Conversations thread view Co-authored-by: harshithpabbati <43822585+harshithpabbati@users.noreply.github.com> --- .../conversation/Conversations.tsx | 42 +++++++++++++++++-- .../organization/sidebar/EmailsList.tsx | 37 +++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/components/organization/conversation/Conversations.tsx b/components/organization/conversation/Conversations.tsx index 81bf7f2..d8a2e49 100644 --- a/components/organization/conversation/Conversations.tsx +++ b/components/organization/conversation/Conversations.tsx @@ -1,12 +1,14 @@ 'use client'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import { sendEmail } from '@/actions/email'; import { Tables } from '@/database.types'; +import { RealtimeChannel } from '@supabase/supabase-js'; import { useTiptap } from '@/hooks/useTiptap'; +import { createBrowserClient } from '@/lib/supabase/client'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -45,12 +47,14 @@ function confidenceVariant(score: number): 'default' | 'neutral' { export function Conversations({ threadId, - conversations, + conversations: initialConversations, reply, status, }: Props) { const router = useRouter(); const divRef = useRef(null); + const channelRef = useRef(null); + const [conversations, setConversations] = useState(initialConversations); const editor = useTiptap( conversations[conversations.length - 1].role === 'user' @@ -61,7 +65,39 @@ export function Conversations({ useEffect(() => { if (!divRef.current) return; divRef.current.scrollTop = divRef.current.scrollHeight; - }, []); + }, [conversations]); + + useEffect(() => { + if (channelRef.current) return; + + const supabase = createBrowserClient(); + channelRef.current = supabase + .channel(`email:${threadId}`) + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'email', + filter: `thread_id=eq.${threadId}`, + }, + (payload) => { + const newEmail = payload.new as Tables<'email'>; + setConversations((prev) => + prev.some((e) => e.id === newEmail.id) ? prev : [...prev, newEmail] + ); + } + ) + .subscribe(); + + return () => { + if (channelRef.current) { + const channel = channelRef.current; + channelRef.current = null; + channel.unsubscribe(); + } + }; + }, [threadId]); const handleSubmit = async ( status: 'open' | 'closed' | undefined = undefined diff --git a/components/organization/sidebar/EmailsList.tsx b/components/organization/sidebar/EmailsList.tsx index d77a508..32a1117 100644 --- a/components/organization/sidebar/EmailsList.tsx +++ b/components/organization/sidebar/EmailsList.tsx @@ -32,7 +32,8 @@ type ThreadAction = data: Tables<'thread'>[]; status: 'open' | 'closed'; } - | { type: 'INSERT_THREAD'; thread: Tables<'thread'> }; + | { type: 'INSERT_THREAD'; thread: Tables<'thread'> } + | { type: 'UPDATE_THREAD'; thread: Tables<'thread'> }; function threadReducer(state: ThreadState, action: ThreadAction): ThreadState { switch (action.type) { @@ -42,6 +43,25 @@ function threadReducer(state: ThreadState, action: ThreadAction): ThreadState { return { data: action.data, status: action.status, isLoading: false }; case 'INSERT_THREAD': return { ...state, data: [action.thread, ...state.data] }; + case 'UPDATE_THREAD': { + const existsInList = state.data.some((t) => t.id === action.thread.id); + if (action.thread.status !== state.status) { + if (!existsInList) return state; + return { + ...state, + data: state.data.filter((t) => t.id !== action.thread.id), + }; + } + const updated = existsInList + ? state.data.map((t) => (t.id === action.thread.id ? action.thread : t)) + : [action.thread, ...state.data]; + updated.sort( + (a, b) => + new Date(b.last_message_created_at).getTime() - + new Date(a.last_message_created_at).getTime() + ); + return { ...state, data: updated }; + } } } @@ -103,6 +123,21 @@ export function EmailsList({ orgId, name, slug, inboundEmail }: Props) { }); } ) + .on( + 'postgres_changes', + { + event: 'UPDATE', + schema: 'public', + table: 'thread', + filter: `organization_id=eq.${orgId}`, + }, + (payload) => { + dispatch({ + type: 'UPDATE_THREAD', + thread: payload.new as Tables<'thread'>, + }); + } + ) .subscribe(); return () => { From 82d1a8b57747bc40c0d60d91c4c349423cb68b5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:12:57 +0000 Subject: [PATCH 3/3] Fix new thread creation realtime: update last_message_created_at on email insert Co-authored-by: harshithpabbati <43822585+harshithpabbati@users.noreply.github.com> --- app/api/webhooks/inbound-email/route.ts | 28 +++++++++++++------ .../organization/sidebar/EmailsList.tsx | 4 ++- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/api/webhooks/inbound-email/route.ts b/app/api/webhooks/inbound-email/route.ts index 8db73ad..5c45d2c 100644 --- a/app/api/webhooks/inbound-email/route.ts +++ b/app/api/webhooks/inbound-email/route.ts @@ -92,7 +92,7 @@ export async function POST(request: Request) { ); } - const { error: emailError } = await supabase + const { data: emailData, error: emailError } = await supabase .from('email') .insert({ organization_id: orgData.id, @@ -103,20 +103,32 @@ export async function POST(request: Request) { cleaned_body: text, role: 'user', }) + .select('created_at') .single(); - if (thread.status === 'closed') { - await supabase - .from('thread') - .update({ status: 'open' }) - .match({ id: thread.id }); - } - if (emailError) { return new Response(JSON.stringify({ error: 'Failed to create email' }), { status: 200, }); } + const threadUpdate: { last_message_created_at: string; status?: 'open' } = { + last_message_created_at: emailData.created_at, + }; + if (thread.status === 'closed') { + threadUpdate.status = 'open'; + } + const { error: threadUpdateError } = await supabase + .from('thread') + .update(threadUpdate) + .match({ id: thread.id }); + + if (threadUpdateError) { + return new Response( + JSON.stringify({ error: 'Failed to update thread' }), + { status: 200 } + ); + } + return new Response(JSON.stringify({ ok: true }), { status: 200 }); } diff --git a/components/organization/sidebar/EmailsList.tsx b/components/organization/sidebar/EmailsList.tsx index 32a1117..4f07c06 100644 --- a/components/organization/sidebar/EmailsList.tsx +++ b/components/organization/sidebar/EmailsList.tsx @@ -41,8 +41,10 @@ function threadReducer(state: ThreadState, action: ThreadAction): ThreadState { return { ...state, isLoading: true }; case 'FETCH_SUCCESS': return { data: action.data, status: action.status, isLoading: false }; - case 'INSERT_THREAD': + case 'INSERT_THREAD': { + if (state.data.some((t) => t.id === action.thread.id)) return state; return { ...state, data: [action.thread, ...state.data] }; + } case 'UPDATE_THREAD': { const existsInList = state.data.some((t) => t.id === action.thread.id); if (action.thread.status !== state.status) {