From 420bd8d9527135887e73723eb760d21d936f652f Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Fri, 10 Jul 2026 17:44:20 +0100 Subject: [PATCH 1/2] Fix desktop launch motion and reaction spacing --- desktop/src-tauri/src/lib.rs | 122 +++++++++++++++++- desktop/src-tauri/tauri.conf.json | 6 +- desktop/src/app/App.tsx | 26 ++-- desktop/src/app/AppTopChrome.tsx | 12 +- .../features/messages/ui/MessageReactions.tsx | 2 +- desktop/tests/e2e/reaction-order.spec.ts | 15 +++ 6 files changed, 159 insertions(+), 24 deletions(-) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 1881fbed7e..3114d8a82f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -48,9 +48,11 @@ use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; -use tauri::{Emitter, Manager, RunEvent}; +use tauri::{Emitter, Listener, Manager, RunEvent}; use tauri_plugin_window_state::StateFlags; +const INITIAL_RENDER_READY_EVENT: &str = "initial-render-ready"; + #[tauri::command] fn perform_sidebar_default_haptic() { #[cfg(target_os = "macos")] @@ -152,6 +154,70 @@ fn toggle_maximize(window: &tauri::Window) { } } +fn reveal_initial_window(window: &tauri::Window) { + if let Err(error) = window.show() { + eprintln!("buzz-desktop: failed to reveal main window: {error}"); + return; + } + if let Err(error) = window.set_focus() { + eprintln!("buzz-desktop: failed to focus main window: {error}"); + } +} + +#[cfg(target_os = "macos")] +fn set_initial_window_backing(window: &tauri::Window) { + // The window remains transparent at runtime for vibrancy. Use an opaque + // native backing only across the first visible frames so the previous app + // cannot show through before WebKit has submitted its first surface. + if let Err(error) = window.set_background_color(Some(tauri::window::Color(17, 21, 24, 255))) { + eprintln!("buzz-desktop: failed to set initial window backing: {error}"); + } +} + +#[cfg(target_os = "macos")] +async fn clear_initial_window_backing(window: &tauri::Window) { + tokio::time::sleep(std::time::Duration::from_millis(250)).await; + if let Err(error) = window.set_background_color(None) { + eprintln!("buzz-desktop: failed to clear initial window backing: {error}"); + } +} + +#[cfg(target_os = "macos")] +async fn wait_for_stable_initial_window_geometry(window: &tauri::Window) { + const MAX_POLLS: usize = 120; + const REQUIRED_STABLE_POLLS: usize = 4; + + let mut previous_bounds = None; + let mut stable_polls = 0; + + for _ in 0..MAX_POLLS { + let bounds = match ( + window.is_maximized(), + window.outer_position(), + window.outer_size(), + ) { + (Ok(true), Ok(position), Ok(size)) => { + Some((position.x, position.y, size.width, size.height)) + } + _ => None, + }; + + if bounds.is_some() && bounds == previous_bounds { + stable_polls += 1; + if stable_polls >= REQUIRED_STABLE_POLLS { + return; + } + } else { + stable_polls = 0; + } + previous_bounds = bounds; + + tokio::time::sleep(std::time::Duration::from_millis(16)).await; + } + + eprintln!("buzz-desktop: initial window geometry did not settle before reveal timeout"); +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let builder = tauri::Builder::default() @@ -172,11 +238,61 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin( tauri_plugin_window_state::Builder::default() - // Visibility is excluded: the window starts hidden and the - // frontend shows it once ready. + // Visibility is excluded: the native reveal plugin below + // shows the window after saved geometry has been restored. .with_state_flags(StateFlags::all() & !StateFlags::VISIBLE) .build(), ) + .plugin( + tauri::plugin::Builder::<_, ()>::new("initial-window-reveal") + .on_webview_ready(|webview| { + if webview.label() != "main" { + return; + } + + // macOS applies the initial maximize asynchronously. Wait + // for several identical maximized bounds and for React to + // commit the startup surface before revealing it. + let window = webview.window(); + + #[cfg(target_os = "macos")] + { + set_initial_window_backing(&window); + + let (initial_render_tx, initial_render_rx) = tokio::sync::oneshot::channel(); + window + .app_handle() + .once(INITIAL_RENDER_READY_EVENT, move |_| { + let _ = initial_render_tx.send(()); + }); + + tauri::async_runtime::spawn(async move { + wait_for_stable_initial_window_geometry(&window).await; + + if tokio::time::timeout( + std::time::Duration::from_secs(5), + initial_render_rx, + ) + .await + .is_err() + { + eprintln!( + "buzz-desktop: initial render did not commit before reveal timeout" + ); + } + + reveal_initial_window(&window); + clear_initial_window_backing(&window).await; + }); + } + + #[cfg(not(target_os = "macos"))] + { + reveal_initial_window(&window); + } + }) + .build(), + ) .plugin(tauri_plugin_websocket::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()); diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 5e3cb46bee..ca34c62414 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -20,15 +20,11 @@ "width": 800, "height": 600, "maximized": true, - "visible": true, + "visible": false, "transparent": true, "titleBarStyle": "Overlay", "hiddenTitle": true, "dragDropEnabled": false, - "trafficLightPosition": { - "x": 16, - "y": 24 - }, "backgroundThrottling": "disabled", "minWidth": 800, "minHeight": 500 diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index e978edf920..26bdb2ffbc 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -1,4 +1,5 @@ -import { getCurrentWindow } from "@tauri-apps/api/window"; +import { isTauri } from "@tauri-apps/api/core"; +import { emit } from "@tauri-apps/api/event"; import { QueryClientProvider } from "@tanstack/react-query"; import { RouterProvider } from "@tanstack/react-router"; import { Hexagon } from "lucide-react"; @@ -39,16 +40,26 @@ import { StepProgress } from "@/shared/ui/step-progress"; const LOADING_TEXT = "Setting up your workspace..."; // Minimum time the cold-boot splash stays on screen. A real boot resolves the -// workspace in well under 100ms, and the hidden Tauri window (visible:false -// until getCurrentWindow().show()) takes longer than that to put its first -// frame on screen — without a hold, the bee is unmounted before it is ever -// visible. The hold runs as an overlay above the already-mounted app, so +// workspace in well under 100ms, and the native window setup plus first paint +// can take longer than that — without a hold, the bee is unmounted before it is +// ever visible. The hold runs as an overlay above the already-mounted app, so // time-to-interactive is unchanged; only the reveal waits. const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200; const BOOT_SPLASH_FADE_MS = 200; +const INITIAL_RENDER_READY_EVENT = "initial-render-ready"; type BootSplashPhase = "holding" | "fading" | "done"; +function useInitialRenderReady() { + useLayoutEffect(() => { + if (!isTauri()) { + return; + } + + void emit(INITIAL_RENDER_READY_EVENT); + }, []); +} + // E2E runs skip the hold (it would slow every spec's boot and block pointer // actionability); a spec can opt back in via __BUZZ_E2E__.bootSplashHoldMs. function bootSplashHoldMs(): number { @@ -323,10 +334,7 @@ export function App() { // Mounted at the root so Cmd/Ctrl+R reloads in every app state, // including the loading and first-run setup screens below. useReloadShortcut(); - - useLayoutEffect(() => { - void getCurrentWindow().show(); - }, []); + useInitialRenderReady(); const [sharedIdentity, setSharedIdentity] = useState(null); useEffect(() => { diff --git a/desktop/src/app/AppTopChrome.tsx b/desktop/src/app/AppTopChrome.tsx index 0f00205340..793662c5fa 100644 --- a/desktop/src/app/AppTopChrome.tsx +++ b/desktop/src/app/AppTopChrome.tsx @@ -65,11 +65,11 @@ export function AppTopChrome({ }: AppTopChromeProps) { const topChromeRef = React.useRef(null); const isFullscreen = useIsFullscreen(); - // On macOS the traffic-light buttons overlay the chrome (see - // `trafficLightPosition` in `tauri.conf.json`), so the nav row clears their - // x-position. When the workspace rail is present it already occupies the far - // left, so the nav row only needs to clear the lights past the rail edge - // rather than the full offset. In fullscreen those buttons hide. + // On macOS the native traffic-light buttons overlay the chrome, so the nav + // row clears their x-position. When the workspace rail is present it already + // occupies the far left, so the nav row only needs to clear the lights past + // the rail edge rather than the full offset. In fullscreen those buttons + // hide. // // Fixed px on purpose: the native traffic lights do not scale with the app's // Cmd +/- text zoom (rem), so rem-based clearance shrinks under them when @@ -80,7 +80,7 @@ export function AppTopChrome({ ? "pl-[32px]" : "pl-[80px]" : "pl-3"; - const navRowAlignmentClass = macChrome ? "translate-y-[3px]" : null; + const navRowAlignmentClass = macChrome ? "-translate-y-[4px]" : null; React.useEffect(() => { const topChrome = topChromeRef.current; diff --git a/desktop/src/features/messages/ui/MessageReactions.tsx b/desktop/src/features/messages/ui/MessageReactions.tsx index 0f28bf12f4..a15bcfa430 100644 --- a/desktop/src/features/messages/ui/MessageReactions.tsx +++ b/desktop/src/features/messages/ui/MessageReactions.tsx @@ -246,7 +246,7 @@ export function MessageReactions({ return (
{ + const body = await row.locator(".message-markdown").first().boundingBox(); + const reactions = await row.getByTestId("message-reactions").boundingBox(); + if (!body || !reactions) { + throw new Error("message body or reactions are not laid out"); + } + return Math.round(reactions.y - (body.y + body.height)); +} + /** Click a quick-reaction tray button by emoji (hover → click tray button). */ async function addQuickReaction( row: import("@playwright/test").Locator, @@ -93,11 +105,13 @@ test("reaction pills render left-to-right in the order reactions were added", as const pills = await getPillOrder(row); expect(pills).toEqual(["👍", "❤️", "😂"]); + await expect.poll(() => getBodyToReactionGap(row)).toBe(6); // Screenshot for PR visual proof. const screenshotDir = path.resolve("test-results/reaction-order-screenshots"); fs.mkdirSync(screenshotDir, { recursive: true }); const reactionBar = row.getByTestId("message-reactions"); + await waitForAnimations(page); await reactionBar.screenshot({ path: path.join(screenshotDir, "reaction-order-chronological.png"), }); @@ -154,6 +168,7 @@ test("a later emoji that accrues more reactors stays to the right of an earlier const screenshotDir = path.resolve("test-results/reaction-order-screenshots"); fs.mkdirSync(screenshotDir, { recursive: true }); const reactionBar = row.getByTestId("message-reactions"); + await waitForAnimations(page); await reactionBar.screenshot({ path: path.join(screenshotDir, "reaction-order-count-stable.png"), }); From ed70a3e8f97ae5541194f6542688fda126485d52 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Fri, 10 Jul 2026 18:17:53 +0100 Subject: [PATCH 2/2] fix(desktop): reveal restored non-maximized windows without stalling wait_for_stable_initial_window_geometry only accepted bounds when is_maximized() returned Ok(true). When the window-state plugin restores a normal (non-maximized) saved window, is_maximized() is false, so bounds stayed None forever, no stable state was detected, and the reveal waited the full poll budget (~1.9s) before timing out on every launch. Sample outer position/size regardless of maximized state and rely on consecutive identical bounds to detect that the async restore settled. Signed-off-by: klopez4212 Co-authored-by: Fizz <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co> --- desktop/src-tauri/src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 3114d8a82f..14907bee9e 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -191,14 +191,14 @@ async fn wait_for_stable_initial_window_geometry(window: &tau let mut stable_polls = 0; for _ in 0..MAX_POLLS { - let bounds = match ( - window.is_maximized(), - window.outer_position(), - window.outer_size(), - ) { - (Ok(true), Ok(position), Ok(size)) => { - Some((position.x, position.y, size.width, size.height)) - } + // Accept whatever geometry the window-state plugin restores — maximized + // or a normal saved size. macOS applies the restore asynchronously, so + // we only need consecutive identical outer bounds to know it settled. + // Gating on `is_maximized()` here would leave `bounds` permanently + // `None` for restored non-maximized windows and stall the reveal until + // the poll timeout. + let bounds = match (window.outer_position(), window.outer_size()) { + (Ok(position), Ok(size)) => Some((position.x, position.y, size.width, size.height)), _ => None, }; @@ -250,8 +250,8 @@ pub fn run() { return; } - // macOS applies the initial maximize asynchronously. Wait - // for several identical maximized bounds and for React to + // macOS applies the restored geometry asynchronously. Wait + // for several identical outer bounds and for React to // commit the startup surface before revealing it. let window = webview.window();