From 6ba59a8bca93ea2f6ec01a3808eaad14e9edde43 Mon Sep 17 00:00:00 2001 From: "Mars.P" Date: Tue, 19 May 2026 16:28:55 +0800 Subject: [PATCH] Fix naming popup showing stale state when shown for second pet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # The bug User created pet A, renamed it, switched to pet B. Pet B evolved. The naming popup that appeared for pet B showed pet A's data and neither the input nor buttons would respond. # Root cause The Tauri naming window is created ONCE via WebviewWindowBuilder and reused via .hide()/.show() across hatching events. NamingView (the React component inside that window) is therefore the same instance for every popup. Two stale-state pathologies followed: 1. The `useEffect(() => { invoke('naming_current').then(setPrompt) }, [])` only fires once at initial mount. Subsequent `show()` calls re-display the same component with pet A's old prompt frozen in `useState`. 2. `dismiss()` called `setSubmitting(true)` before invoking `naming_dismiss`, then explicitly DID NOT reset it on success (comment: "the window is about to hide"). That left `submitting=true` permanently. Both input and buttons read `disabled={submitting}`, so on the second show the user couldn't type or click. # Fix Backend (`naming_window_show`): after `.show()`, emit a `naming://refresh` Tauri event. Plumbed through the existing AppHandle.emit. Frontend (`NamingView.tsx`): - Subscribe to `naming://refresh` via `@tauri-apps/api/event`'s `listen`. On every refresh, re-read `naming_current` and reset `name`, `submitting`, and `error` to their initial values. - Also reset `submitting=false` in the success branch of `dismiss()` as defense-in-depth — the listener should normally cover it but a successful dismiss leaving its own state clean means no race window where the user could re-click before the next refresh. Resulting flow: Pet A hatches → show, prompt=A, submit → reset, hide Pet B hatches → show + emit refresh → prompt=B, state clean ✓ # Test plan - [x] cargo test --lib clean (310/310) - [x] pnpm tsc --noEmit clean - [ ] User reproduces original scenario: create 2 pets, name #1, let #2 evolve → popup shows #2's data, input + buttons work --- desktop/src-tauri/src/lib.rs | 12 ++++++++- desktop/src/views/NamingView.tsx | 44 ++++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 14 deletions(-) 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);