Fix desktop launch motion and reaction spacing (#1717)

Co-authored-by: Fizz <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
klopez4212
2026-07-12 08:42:31 -07:00
committed by GitHub
co-authored by Fizz
parent 53c177e341
commit 89e2e2e4ad
6 changed files with 159 additions and 24 deletions
+119 -3
View File
@@ -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<R: tauri::Runtime>(window: &tauri::Window<R>) {
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<R: tauri::Runtime>(window: &tauri::Window<R>) {
// 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<R: tauri::Runtime>(window: &tauri::Window<R>) {
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<R: tauri::Runtime>(window: &tauri::Window<R>) {
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 {
// 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,
};
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 restored geometry asynchronously. Wait
// for several identical outer 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());
+1 -5
View File
@@ -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
+17 -9
View File
@@ -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<boolean | null>(null);
useEffect(() => {
+6 -6
View File
@@ -65,11 +65,11 @@ export function AppTopChrome({
}: AppTopChromeProps) {
const topChromeRef = React.useRef<HTMLDivElement>(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;
@@ -246,7 +246,7 @@ export function MessageReactions({
return (
<div
className={cn(
"group/reactions mt-1.5 flex flex-wrap items-center gap-1.5 pt-1",
"group/reactions mt-1.5 flex flex-wrap items-center gap-1.5",
className,
)}
data-testid="message-reactions"
+15
View File
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { installMockBridge } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";
// Reaction ordering end-to-end guard.
//
@@ -52,6 +53,17 @@ async function getPillOrder(
return labels;
}
async function getBodyToReactionGap(
row: import("@playwright/test").Locator,
): Promise<number> {
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"),
});