diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index b64dd1a..a3dd4a7 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -743,7 +743,17 @@ async fn naming_window_show( // next time the pet's state is re-evaluated. |main, w, h| notify_window_position(main, w, h), ) - .await + .await?; + // Nudge the naming window's React side to re-read `last_naming` + // and reset its local form state. The window itself is created + // once and reused via .hide()/.show() across hatching events — + // without this event the NamingView component (mounted once) + // keeps the FIRST pet's prompt forever, and worse, its + // `submitting` state stays stuck at `true` from the previous + // dismiss, disabling the input + buttons. See the matching + // listener in NamingView.tsx. + let _ = app.emit("naming://refresh", ()); + Ok(()) } #[tauri::command] diff --git a/desktop/src/views/NamingView.tsx b/desktop/src/views/NamingView.tsx index 711ca23..0a2fc89 100644 --- a/desktop/src/views/NamingView.tsx +++ b/desktop/src/views/NamingView.tsx @@ -17,8 +17,9 @@ * NotifyView. */ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; import "./NamingView.css"; interface NamingPrompt { @@ -40,22 +41,34 @@ export function NamingView() { const inputRef = useRef(null); const lastSizeRef = useRef<{ w: number; h: number } | null>(null); - // Pull the current prompt on mount. Naming windows are one-shot — - // no need to subscribe to live updates (each hatching gets a fresh - // `naming_window_show` call which replaces the stored prompt). - useEffect(() => { - let cancelled = false; + // Pull the current prompt on mount AND on every `naming://refresh` + // emitted by `naming_window_show`. The Tauri naming window is + // created once and reused via .hide()/.show() across hatching + // events — so this React component is the SAME instance for every + // subsequent popup. Without the listener, the prompt + form state + // would freeze at whatever the first hatching set (stale pet name, + // and worse, `submitting=true` carried over from the previous + // dismiss would lock out the input + buttons). + const refreshFromBackend = useCallback(() => { invoke("naming_current") .then((p) => { - if (!cancelled && p) setPrompt(p); + if (p) { + setPrompt(p); + setName(""); // clear stale input value + setSubmitting(false); // unstick the previous dismiss + setError(null); + } }) - .catch((e) => { - if (!cancelled) setError(String(e)); - }); + .catch((e) => setError(String(e))); + }, []); + + useEffect(() => { + refreshFromBackend(); + const unlistenP = listen("naming://refresh", () => refreshFromBackend()); return () => { - cancelled = true; + unlistenP.then((unlisten) => unlisten()).catch(() => {}); }; - }, []); + }, [refreshFromBackend]); // Focus the input once the prompt has rendered. useEffect(() => { @@ -98,7 +111,12 @@ export function NamingView() { name: confirmed && name.trim() !== "" ? name.trim() : null, confirmed, }); - // Don't reset `submitting` — the window is about to hide. + // Reset `submitting` immediately. The window is about to hide, + // but the React instance lives on (Tauri reuses the window) — + // leaving `submitting=true` would lock out the buttons on the + // NEXT hatching event before our naming://refresh listener + // has a chance to fire. Defense in depth. + setSubmitting(false); } catch (e) { setError(String(e)); setSubmitting(false);