diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d581260e..f27d2071 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,7 +10,7 @@ * - Session management */ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import { SessionProvider, useSession, useServerMessageHandler } from "./contexts/SessionContext"; import { VaultSelect } from "./components/VaultSelect"; import { ModeToggle } from "./components/ModeToggle"; @@ -35,7 +35,6 @@ type DialogType = "changeVault" | null; */ function MainContent(): React.ReactNode { const { mode, vault, clearVault } = useSession(); - const { sendMessage, lastMessage } = useWebSocket(); const handleServerMessage = useServerMessageHandler(); const [activeDialog, setActiveDialog] = useState(null); const [configEditorOpen, setConfigEditorOpen] = useState(false); @@ -50,6 +49,21 @@ function MainContent(): React.ReactNode { const holiday = useHoliday(); // Mobile header collapse state const [isHeaderCollapsed, setIsHeaderCollapsed] = useState(true); + // Session restoration tracking (for page refresh and WebSocket reconnection) + const hasRequestedMeetingStateRef = useRef(false); + const hasSentVaultSelectionRef = useRef(false); + + // Re-establish vault context and meeting state on WebSocket reconnection. + // After reconnect, the server has a fresh connection state and needs to know + // which vault we're using before we can query meeting state. + const handleReconnect = useCallback(() => { + hasSentVaultSelectionRef.current = false; + hasRequestedMeetingStateRef.current = false; + }, []); + + const { sendMessage, lastMessage, connectionStatus } = useWebSocket({ + onReconnect: handleReconnect, + }); // Process server messages through the session handler useEffect(() => { @@ -58,6 +72,33 @@ function MainContent(): React.ReactNode { } }, [lastMessage, handleServerMessage]); + // Re-send vault selection on WebSocket reconnect (server needs vault context) + useEffect(() => { + if ( + connectionStatus === "connected" && + vault && + !hasSentVaultSelectionRef.current + ) { + sendMessage({ type: "select_vault", vaultId: vault.id }); + hasSentVaultSelectionRef.current = true; + } + }, [connectionStatus, vault, sendMessage]); + + // Request meeting state after vault selection to restore any active meeting. + // This runs on initial mount (page refresh) and after WebSocket reconnection. + // Fixes #377 where refreshing with an active meeting orphaned it. + useEffect(() => { + if ( + connectionStatus === "connected" && + vault && + hasSentVaultSelectionRef.current && + !hasRequestedMeetingStateRef.current + ) { + sendMessage({ type: "get_meeting_state" }); + hasRequestedMeetingStateRef.current = true; + } + }, [connectionStatus, vault, sendMessage]); + // Use holiday-specific logo if available const logoSrc = holiday ? `/images/holiday/${holiday}-logo.webp` : "/images/logo.webp"; diff --git a/frontend/src/components/NoteCapture.tsx b/frontend/src/components/NoteCapture.tsx index 8cc5c2e5..a5284cfe 100644 --- a/frontend/src/components/NoteCapture.tsx +++ b/frontend/src/components/NoteCapture.tsx @@ -9,7 +9,7 @@ * - Meeting: Captures go to meeting-specific file */ -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useWebSocket } from "../hooks/useWebSocket"; import { useSession } from "../contexts/SessionContext"; import "./NoteCapture.css"; @@ -60,55 +60,10 @@ export function NoteCapture({ onCaptured }: NoteCaptureProps): React.ReactNode { const meetingTitleRef = useRef(null); const retryCountRef = useRef(0); const retryTimeoutRef = useRef | null>(null); - const hasSentVaultSelectionRef = useRef(false); - const hasRequestedMeetingStateRef = useRef(false); - const { vault, meeting, setMeetingState, clearMeeting, setDiscussionPrefill, setMode } = useSession(); + const { vault, meeting, clearMeeting, setDiscussionPrefill, setMode } = useSession(); - // Callback to re-send vault selection on WebSocket reconnect - const handleReconnect = useCallback(() => { - hasSentVaultSelectionRef.current = false; - hasRequestedMeetingStateRef.current = false; - }, []); - - const { sendMessage, lastMessage, connectionStatus } = useWebSocket({ - onReconnect: handleReconnect, - }); - - // Send vault selection when WebSocket connects (initial or reconnect) - useEffect(() => { - if ( - connectionStatus === "connected" && - vault && - !hasSentVaultSelectionRef.current - ) { - sendMessage({ - type: "select_vault", - vaultId: vault.id, - }); - hasSentVaultSelectionRef.current = true; - } - }, [connectionStatus, vault, sendMessage]); - - // Request meeting state after vault selection (to restore state after reconnect) - useEffect(() => { - if ( - connectionStatus === "connected" && - vault && - hasSentVaultSelectionRef.current && - !hasRequestedMeetingStateRef.current - ) { - sendMessage({ type: "get_meeting_state" }); - hasRequestedMeetingStateRef.current = true; - } - }, [connectionStatus, vault, sendMessage]); - - // Handle meeting_state response (for restoring state after reconnect) - useEffect(() => { - if (lastMessage?.type === "meeting_state") { - setMeetingState(lastMessage.state); - } - }, [lastMessage, setMeetingState]); + const { sendMessage, lastMessage, connectionStatus } = useWebSocket(); // Detect touch-only devices (no hover capability) // On touch devices, Enter adds newlines; send button is the only way to submit @@ -170,25 +125,19 @@ export function NoteCapture({ onCaptured }: NoteCaptureProps): React.ReactNode { } }, [lastMessage, isSubmitting]); - // Handle meeting_started response + // Handle meeting_started response (local UI updates only; session context + // is updated by useServerMessageHandler in MainContent) useEffect(() => { if (lastMessage?.type === "meeting_started" && isStartingMeeting) { setIsStartingMeeting(false); setShowMeetingPrompt(false); setMeetingTitle(""); - // Update session context with meeting state - setMeetingState({ - isActive: true, - title: lastMessage.title, - filePath: lastMessage.filePath, - startedAt: lastMessage.startedAt, - }); showToast("success", `Meeting started: ${lastMessage.title}`); requestAnimationFrame(() => { textareaRef.current?.focus(); }); } - }, [lastMessage, isStartingMeeting, setMeetingState]); + }, [lastMessage, isStartingMeeting]); // Handle meeting_stopped response useEffect(() => {