use settings::Setting as _; use warpui::elements::{ Align, CrossAxisAlignment, Flex, MainAxisSize, MouseStateHandle, ParentElement as _, }; use warpui::fonts::Weight; use warpui::platform::Cursor; use warpui::ui_components::button::ButtonVariant; use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles}; use warpui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext}; use crate::appearance::Appearance; use crate::report_if_error; use crate::settings::app_installation_detection::{ UserAppInstallDetectionSettings, UserAppInstallStatus, }; use crate::settings::{NativePreferenceSettings, UserNativePreference}; use crate::ui_components::dialog::{dialog_styles, Dialog}; use crate::uri::web_intent_parser::{self, WebIntent}; const CLOSE_BUTTON_DIAMETER: f32 = 20.; const DIALOG_WIDTH: f32 = 350.; const DIALOG_PADDING: f32 = 24.; const OPEN_NATIVE_BUTTON_WIDTH: f32 = 260.; const OPEN_NATIVE_BUTTON_HEIGHT: f32 = 40.; #[derive(Debug, Copy, Clone)] pub enum WasmNUXDialogAction { /// Close and dismiss the dialog Close, /// Closes the dialog and sets the native preference to web SetWebAndClose, /// Closes the dialog and open on the desktop OpenNativeAndClose, /// Open the Warp download page OpenDownloadDesktopAppLink, /// Open a link to learn more about Warp LearnMore, } pub enum WasmNUXDialogEvent { Close, } /// A dialog that prompts the user to: /// * Download Warp if they haven't already /// * Explicitly choose between native and web. pub struct WasmNUXDialog { close_mouse_state: MouseStateHandle, confirm_mouse_state: MouseStateHandle, download_warp_mouse_state: MouseStateHandle, learn_more_mouse_state: MouseStateHandle, requested_download: bool, } impl Entity for WasmNUXDialog { type Event = WasmNUXDialogEvent; } impl WasmNUXDialog { pub fn new() -> Self { Self { close_mouse_state: Default::default(), confirm_mouse_state: Default::default(), download_warp_mouse_state: Default::default(), learn_more_mouse_state: Default::default(), requested_download: false, } } /// Whether or not the new-user dialog should be shown. /// /// It's shown if all of the following are true: /// * Not on a mobile device (mobile users can't use the desktop app) /// * The user does not have an explicit native/web preference /// * The user hasn't dismissed the dialog /// /// If the user dismisses the dialog without choosing a preference, we'll continue to use the default autodetection /// behavior: if Warp is installed, redirect to it; otherwise stay on the web. pub fn should_display(app: &AppContext) -> bool { // Don't show on mobile devices - they can't use the desktop app if galaxyui::platform::wasm::is_mobile_device() { return false; } let preference_settings = NativePreferenceSettings::handle(app).as_ref(app); *preference_settings.user_native_redirect_preference.value() == UserNativePreference::NotSelected && !*preference_settings.preference_dialog_dismissed.value() } fn render_dialog_button( text: impl Into, action: WasmNUXDialogAction, mouse_state: &MouseStateHandle, appearance: &Appearance, ) -> Box { let button_style = UiComponentStyles { font_size: Some(14.), font_weight: Some(Weight::Bold), width: Some(OPEN_NATIVE_BUTTON_WIDTH), height: Some(OPEN_NATIVE_BUTTON_HEIGHT), ..Default::default() }; appearance .ui_builder() .button(ButtonVariant::Accent, mouse_state.clone()) .with_centered_text_label(text.into()) .with_style(button_style) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(action); }) .finish() } } impl View for WasmNUXDialog { fn ui_name() -> &'static str { "WasmNUXDialog" } fn render(&self, app: &AppContext) -> Box { let appearance = Appearance::handle(app).as_ref(app); // There are two general cases with the dialog: // 1. The user doesn't have Warp installed - treat them as a potential new user and encourage downloading Warp. // 2. The user has Warp installed, but clicked through to the web - ask if they want to always default to web. // As a sub-state of case 1, if the user clicks the download button, we provide an intent into the app. let close_button = appearance .ui_builder() .close_button(CLOSE_BUTTON_DIAMETER, self.close_mouse_state.clone()) .build() .with_cursor(Cursor::PointingHand) .on_click(move |ctx, _, _| { ctx.dispatch_typed_action(WasmNUXDialogAction::Close); }) .finish(); let app_install_detected = UserAppInstallDetectionSettings::handle(app) .as_ref(app) .user_app_installation_detected .value(); let dialog_styles = UiComponentStyles { width: Some(DIALOG_WIDTH), padding: Some(Coords::uniform(DIALOG_PADDING)), ..dialog_styles(appearance) }; let dialog = if self.requested_download { Dialog::new( "Open in Galaxy Desktop?".to_string(), Some("Future links will automatically open on desktop.".to_string()), dialog_styles, ) .with_bottom_row_child(Self::render_dialog_button( "Open in Galaxy", WasmNUXDialogAction::OpenNativeAndClose, &self.confirm_mouse_state, appearance, )) } else if app_install_detected == &UserAppInstallStatus::NotDetected { Dialog::new("Download Galaxy Desktop?".to_string(), None, dialog_styles) .with_child( Flex::column() .with_cross_axis_alignment(CrossAxisAlignment::Stretch) .with_main_axis_size(MainAxisSize::Min) .with_child( appearance .ui_builder() .span("Galaxy is a clone of Warp built for Samsung to use with Bedrock. Get the best features of Warp with the security provided by Bedrock!") .with_style(UiComponentStyles { font_weight: Some(Weight::Thin), font_color: Some( appearance .theme() .main_text_color(appearance.theme().surface_1()) .into_solid(), ), ..Default::default() }) .with_soft_wrap() .build() .finish(), ) .with_child( Align::new( appearance .ui_builder() .link( "Learn more".to_string(), None, Some(Box::new(|ctx| { ctx.dispatch_typed_action( WasmNUXDialogAction::LearnMore, ) })), self.learn_more_mouse_state.clone(), ) .build() .finish(), ) .left() .finish(), ) .finish(), ) .with_bottom_row_child(Self::render_dialog_button( "Download", WasmNUXDialogAction::OpenDownloadDesktopAppLink, &self.download_warp_mouse_state, appearance, )) } else { let object_kind = match web_intent_parser::current_web_intent() { Some(WebIntent::DriveObject(_)) => "Galaxy Drive objects", Some(WebIntent::SessionView(_)) => "shared sessions", _ => "Galaxy links", }; Dialog::new( format!("Always open {object_kind} on the web?"), Some("You can change this at any time in settings.".to_string()), dialog_styles, ) .with_bottom_row_child(Self::render_dialog_button( "Yes", WasmNUXDialogAction::SetWebAndClose, &self.confirm_mouse_state, appearance, )) }; dialog.with_close_button(close_button).build().finish() } } impl TypedActionView for WasmNUXDialog { type Action = WasmNUXDialogAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { match action { WasmNUXDialogAction::Close => { NativePreferenceSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.preference_dialog_dismissed.set_value(true, ctx)) }); ctx.emit(WasmNUXDialogEvent::Close); } WasmNUXDialogAction::SetWebAndClose => { if let Err(e) = NativePreferenceSettings::handle(ctx).update(ctx, |setting, ctx| { setting .user_native_redirect_preference .set_value(UserNativePreference::Web, ctx) }) { log::error!("Failed to set the open preference to web. {e}"); }; ctx.emit(WasmNUXDialogEvent::Close); } WasmNUXDialogAction::OpenNativeAndClose => { // We intentionally do not set the native preference here, in case the user hasn't actually installed Warp. // If they have, on subsequent loads, we'll detect that Warp is installed and redirect to the desktop. ctx.emit(WasmNUXDialogEvent::Close); if let Some(url) = web_intent_parser::parse_web_intent_from_current_url() { // Signals to the react app to open the native app. crate::platform::wasm::emit_event( crate::platform::wasm::GalaxyEvent::OpenOnNative { url: String::from(url.as_str()), }, ); } else { log::error!("Failed to open in app. Could not determine current url"); } } WasmNUXDialogAction::OpenDownloadDesktopAppLink => { ctx.open_url("https://app.warp.dev/get_warp"); self.requested_download = true; ctx.notify(); } WasmNUXDialogAction::LearnMore => { ctx.open_url("https://www.warp.dev"); } } } }