From b0a754054bda54209694d5c04f789568e910aacc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:35:31 +0000 Subject: [PATCH 1/3] Initial plan From d9bf3d1b3d6c238bcb77cda9f414fc0426db1eba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:36:54 +0000 Subject: [PATCH 2/3] fix: use API_BASE_URL for WebSocket URL in conversation screen Co-authored-by: grillinr <169214325+grillinr@users.noreply.github.com> --- frontend/app/conversation/[username].tsx | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/app/conversation/[username].tsx b/frontend/app/conversation/[username].tsx index 22c73af..72c5ae7 100644 --- a/frontend/app/conversation/[username].tsx +++ b/frontend/app/conversation/[username].tsx @@ -17,7 +17,7 @@ import { MessageBubble } from "@/components/MessageBubble"; import { MessageComposer } from "@/components/MessageComposer"; import { useAppColors } from "@/hooks/useAppColors"; import { useAuth } from "@/contexts/AuthContext"; -import { getDirectMessages, createDirectMessage } from "@/services/api"; +import { API_BASE_URL, getDirectMessages, createDirectMessage } from "@/services/api"; import { ApiDirectMessage } from "@/constants/Types"; type MessageWithState = ApiDirectMessage & { @@ -26,6 +26,19 @@ type MessageWithState = ApiDirectMessage & { tempId?: string; }; +const getWebSocketBaseUrl = (baseUrl?: string) => { + if (!baseUrl) { + return ""; + } + if (baseUrl.startsWith("https://")) { + return `wss://${baseUrl.slice("https://".length)}`; + } + if (baseUrl.startsWith("http://")) { + return `ws://${baseUrl.slice("http://".length)}`; + } + return baseUrl; +}; + export default function ConversationScreen() { const colors = useAppColors(); const insets = useSafeAreaInsets(); @@ -70,10 +83,9 @@ export default function ConversationScreen() { useEffect(() => { if (!user?.username || !token) return; - // Connect to WebSocket (reuse existing connection pattern from terminal) - const protocol = __DEV__ ? "ws" : "wss"; - const host = __DEV__ ? "10.0.2.2:8080" : "devbits.ddns.net"; - const wsUrl = `${protocol}://${host}/messages/${encodeURIComponent(user.username)}/stream?token=${encodeURIComponent(token)}`; + // Connect to WebSocket using API_BASE_URL + const wsBase = getWebSocketBaseUrl(API_BASE_URL); + const wsUrl = `${wsBase}/messages/${encodeURIComponent(user.username)}/stream?token=${encodeURIComponent(token)}`; try { // Close any existing socket from a previous run before creating a new one From 3482dd01fb2ee140e7227f9423061ad4fd9592b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:02:18 +0000 Subject: [PATCH 3/3] fix: merge reconnection logic from base branch while keeping API_BASE_URL approach Co-authored-by: elifouts <116454864+elifouts@users.noreply.github.com> --- frontend/app/conversation/[username].tsx | 175 ++++++++++++++++------- 1 file changed, 121 insertions(+), 54 deletions(-) diff --git a/frontend/app/conversation/[username].tsx b/frontend/app/conversation/[username].tsx index 72c5ae7..cb5d06b 100644 --- a/frontend/app/conversation/[username].tsx +++ b/frontend/app/conversation/[username].tsx @@ -83,70 +83,137 @@ export default function ConversationScreen() { useEffect(() => { if (!user?.username || !token) return; + // Track reconnection state within this effect + let reconnectAttempts = 0; + let reconnectTimeoutId: ReturnType | null = null; + let isActive = true; + // Connect to WebSocket using API_BASE_URL const wsBase = getWebSocketBaseUrl(API_BASE_URL); - const wsUrl = `${wsBase}/messages/${encodeURIComponent(user.username)}/stream?token=${encodeURIComponent(token)}`; + const wsUrl = `${wsBase}/messages/${encodeURIComponent( + user.username + )}/stream?token=${encodeURIComponent(token)}`; - try { - // Close any existing socket from a previous run before creating a new one - if (wsRef.current && (wsRef.current.readyState === WebSocket.OPEN || wsRef.current.readyState === WebSocket.CONNECTING)) { - wsRef.current.close(); + const connect = () => { + if (!isActive) { + return; } - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onopen = () => { - console.log("WebSocket connected for conversation"); - }; - - ws.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - // Handle incoming direct message - if (data.type === "direct_message") { - const newMessage = data.direct_message as ApiDirectMessage; - - // Only add if it's from the current conversation - if ( - newMessage.sender_name === recipientUsername || - newMessage.recipient_name === recipientUsername - ) { - setMessages((prev) => { - // Check if message already exists (avoid duplicates) - const exists = prev.some((m) => m.id === newMessage.id); - if (exists) return prev; - return [...prev, newMessage]; - }); - - // Scroll to bottom - setTimeout(() => { - flatListRef.current?.scrollToEnd({ animated: true }); - }, 100); + try { + // Close any existing socket from a previous run before creating a new one + if ( + wsRef.current && + (wsRef.current.readyState === WebSocket.OPEN || + wsRef.current.readyState === WebSocket.CONNECTING) + ) { + wsRef.current.close(); + } + + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + console.log("WebSocket connected for conversation"); + // Reset reconnect attempts on successful connection + reconnectAttempts = 0; + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + // Handle incoming direct message + if (data.type === "direct_message") { + const newMessage = data.direct_message as ApiDirectMessage; + + // Only add if it's from the current conversation + if ( + newMessage.sender_name === recipientUsername || + newMessage.recipient_name === recipientUsername + ) { + setMessages((prev) => { + // Check if message already exists (avoid duplicates) + const exists = prev.some((m) => m.id === newMessage.id); + if (exists) return prev; + return [...prev, newMessage]; + }); + + // Scroll to bottom + setTimeout(() => { + flatListRef.current?.scrollToEnd({ animated: true }); + }, 100); + } } + } catch (err) { + console.error("Failed to parse WebSocket message:", err); + } + }; + + ws.onerror = (error) => { + console.error("WebSocket error:", error); + }; + + ws.onclose = (event) => { + console.log( + "WebSocket closed", + event?.code, + event?.reason ?? "" + ); + + if (!isActive) { + // Component unmounted or effect cleaned up; do not attempt to reconnect + return; } - } catch (err) { - console.error("Failed to parse WebSocket message:", err); - } - }; - ws.onerror = (error) => { - console.error("WebSocket error:", error); - }; + const maxAttempts = 5; + if (reconnectAttempts >= maxAttempts) { + console.log("Max WebSocket reconnect attempts reached; giving up."); + return; + } - ws.onclose = () => { - console.log("WebSocket closed"); - }; + const baseDelayMs = 1000; + const maxDelayMs = 10000; + const delayMs = Math.min( + baseDelayMs * Math.pow(2, reconnectAttempts), + maxDelayMs + ); + reconnectAttempts += 1; + + console.log( + `Attempting WebSocket reconnect #${reconnectAttempts} in ${delayMs}ms` + ); + + reconnectTimeoutId = setTimeout(() => { + reconnectTimeoutId = null; + connect(); + }, delayMs); + }; + } catch (err) { + console.error("Failed to connect WebSocket:", err); + } + }; - return () => { - if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) { - ws.close(); - } - }; - } catch (err) { - console.error("Failed to connect WebSocket:", err); - } + // Initial connection + connect(); + + return () => { + // Prevent any further reconnect attempts + isActive = false; + + if (reconnectTimeoutId !== null) { + clearTimeout(reconnectTimeoutId); + reconnectTimeoutId = null; + } + + const ws = wsRef.current; + if ( + ws && + (ws.readyState === WebSocket.OPEN || + ws.readyState === WebSocket.CONNECTING) + ) { + ws.close(); + } + }; }, [user?.username, recipientUsername, token]); // Send message with optimistic UI