diff --git a/Cargo.lock b/Cargo.lock index 986536aea4..607e0f4976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5636,7 +5636,6 @@ dependencies = [ "askpass", "assistant_settings", "buffer_diff", - "chrono", "collections", "command_palette_hooks", "component", @@ -14195,10 +14194,11 @@ version = "0.1.0" dependencies = [ "auto_update", "call", + "chrono", "client", "collections", + "db", "feature_flags", - "git_ui", "gpui", "http_client", "notifications", @@ -14219,7 +14219,6 @@ dependencies = [ "windows 0.61.1", "workspace", "zed_actions", - "zeta", ] [[package]] @@ -17371,6 +17370,7 @@ dependencies = [ "theme_extension", "theme_selector", "time", + "title_bar", "toolchain_selector", "tree-sitter-md", "tree-sitter-rust", @@ -17629,7 +17629,6 @@ dependencies = [ "anyhow", "arrayvec", "call", - "chrono", "client", "clock", "collections", diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index c6e0746178..5d53376f32 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -21,7 +21,6 @@ anyhow.workspace = true askpass.workspace = true assistant_settings.workspace = true buffer_diff.workspace = true -chrono.workspace = true collections.workspace = true command_palette_hooks.workspace = true component.workspace = true diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 39569218d0..ab10791405 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -9,7 +9,7 @@ use git::{ }; use git_panel_settings::GitPanelSettings; use gpui::{actions, App, FocusHandle}; -use onboarding::{clear_dismissed, GitOnboardingModal}; +use onboarding::GitOnboardingModal; use project_diff::ProjectDiff; use ui::prelude::*; use workspace::Workspace; @@ -103,7 +103,7 @@ pub fn init(cx: &mut App) { }, ); workspace.register_action(move |_, _: &ResetOnboarding, window, cx| { - clear_dismissed(cx); + cx.dispatch_action(&workspace::RestoreBanner); window.refresh(); }); workspace.register_action(|workspace, _action: &git::Init, window, cx| { diff --git a/crates/git_ui/src/onboarding.rs b/crates/git_ui/src/onboarding.rs index 3f3dcd6739..c51de8f5ef 100644 --- a/crates/git_ui/src/onboarding.rs +++ b/crates/git_ui/src/onboarding.rs @@ -1,9 +1,8 @@ use gpui::{ - svg, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, - MouseDownEvent, Render, + svg, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, + Render, }; -use ui::{prelude::*, ButtonLike, TintColor, Tooltip}; -use util::ResultExt; +use ui::{prelude::*, TintColor}; use workspace::{ModalView, Workspace}; use crate::git_panel::GitPanel; @@ -144,130 +143,3 @@ impl Render for GitOnboardingModal { ) } } - -/// Prompts the user to try Zed's git features -pub struct GitBanner { - dismissed: bool, -} - -#[derive(Clone)] -struct GitBannerGlobal(Entity); -impl Global for GitBannerGlobal {} - -impl GitBanner { - pub fn new(cx: &mut Context) -> Self { - cx.set_global(GitBannerGlobal(cx.entity())); - Self { - dismissed: get_dismissed(), - } - } - - fn should_show(&self, _cx: &mut App) -> bool { - !self.dismissed - } - - fn dismiss(&mut self, cx: &mut Context) { - git_onboarding_event!("Banner Dismissed"); - persist_dismissed(cx); - self.dismissed = true; - cx.notify(); - } -} - -const DISMISSED_AT_KEY: &str = "zed_git_banner_dismissed_at"; - -fn get_dismissed() -> bool { - db::kvp::KEY_VALUE_STORE - .read_kvp(DISMISSED_AT_KEY) - .log_err() - .map_or(false, |dismissed| dismissed.is_some()) -} - -fn persist_dismissed(cx: &mut App) { - cx.spawn(async |_| { - let time = chrono::Utc::now().to_rfc3339(); - db::kvp::KEY_VALUE_STORE - .write_kvp(DISMISSED_AT_KEY.into(), time) - .await - }) - .detach_and_log_err(cx); -} - -pub(crate) fn clear_dismissed(cx: &mut App) { - cx.defer(|cx| { - cx.global::() - .clone() - .0 - .update(cx, |this, cx| { - this.dismissed = false; - cx.notify(); - }); - }); - - cx.spawn(async |_| { - db::kvp::KEY_VALUE_STORE - .delete_kvp(DISMISSED_AT_KEY.into()) - .await - }) - .detach_and_log_err(cx); -} - -impl Render for GitBanner { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.should_show(cx) { - return div(); - } - - let border_color = cx.theme().colors().editor_foreground.opacity(0.3); - let banner = h_flex() - .rounded_sm() - .border_1() - .border_color(border_color) - .child( - ButtonLike::new("try-git") - .child( - h_flex() - .h_full() - .items_center() - .gap_1() - .child(Icon::new(IconName::GitBranchSmall).size(IconSize::Small)) - .child( - h_flex() - .gap_0p5() - .child( - Label::new("Introducing:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Label::new("Git Support").size(LabelSize::Small)), - ), - ) - .on_click(cx.listener(|this, _, window, cx| { - git_onboarding_event!("Banner Clicked"); - this.dismiss(cx); - window.dispatch_action( - Box::new(zed_actions::OpenGitIntegrationOnboarding), - cx, - ) - })), - ) - .child( - div().border_l_1().border_color(border_color).child( - IconButton::new("close", IconName::Close) - .icon_size(IconSize::Indicator) - .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx))) - .tooltip(|window, cx| { - Tooltip::with_meta( - "Close Announcement Banner", - None, - "It won't show again for this feature", - window, - cx, - ) - }), - ), - ); - - div().pr_2().child(banner) - } -} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index f5265bdc4f..9852dcdc0e 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -29,7 +29,9 @@ test-support = [ [dependencies] auto_update.workspace = true call.workspace = true +chrono.workspace = true client.workspace = true +db.workspace = true feature_flags.workspace = true gpui.workspace = true notifications.workspace = true @@ -47,8 +49,6 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true -zeta.workspace = true -git_ui.workspace = true [target.'cfg(windows)'.dependencies] windows.workspace = true diff --git a/crates/title_bar/src/banner.rs b/crates/title_bar/src/banner.rs new file mode 100644 index 0000000000..49bcfe520e --- /dev/null +++ b/crates/title_bar/src/banner.rs @@ -0,0 +1,132 @@ +use gpui::{Action, Entity, Global, Render}; +use ui::{prelude::*, ButtonLike, Tooltip}; +use util::ResultExt; + +/// Prompts the user to try Zed's features +pub struct Banner { + dismissed: bool, + source: String, + details: BannerDetails, +} + +#[derive(Clone)] +struct BannerGlobal { + entity: Entity, +} +impl Global for BannerGlobal {} + +pub struct BannerDetails { + pub action: Box, + pub banner_label: Box) -> AnyElement>, +} + +impl Banner { + pub fn new(source: &str, details: BannerDetails, cx: &mut Context) -> Self { + cx.set_global(BannerGlobal { + entity: cx.entity(), + }); + Self { + source: source.to_string(), + details, + dismissed: get_dismissed(source), + } + } + + fn should_show(&self, _cx: &mut App) -> bool { + !self.dismissed + } + + fn dismiss(&mut self, cx: &mut Context) { + telemetry::event!("Banner Dismissed", source = self.source); + persist_dismissed(&self.source, cx); + self.dismissed = true; + cx.notify(); + } +} + +fn dismissed_at_key(source: &str) -> String { + format!( + "{}_{}", + "_banner_dismissed_at", + source.to_lowercase().trim().replace(" ", "_") + ) +} + +fn get_dismissed(source: &str) -> bool { + let dismissed_at = if source == "Git Onboarding" { + "zed_git_banner_dismissed_at".to_string() + } else { + dismissed_at_key(source) + }; + db::kvp::KEY_VALUE_STORE + .read_kvp(&dismissed_at) + .log_err() + .map_or(false, |dismissed| dismissed.is_some()) +} + +fn persist_dismissed(source: &str, cx: &mut App) { + let dismissed_at = dismissed_at_key(source); + cx.spawn(async |_| { + let time = chrono::Utc::now().to_rfc3339(); + db::kvp::KEY_VALUE_STORE.write_kvp(dismissed_at, time).await + }) + .detach_and_log_err(cx); +} + +pub fn restore_banner(cx: &mut App) { + cx.defer(|cx| { + cx.global::() + .entity + .clone() + .update(cx, |this, cx| { + this.dismissed = false; + cx.notify(); + }); + }); + + let source = &cx.global::().entity.read(cx).source; + let dismissed_at = dismissed_at_key(source); + cx.spawn(async |_| db::kvp::KEY_VALUE_STORE.delete_kvp(dismissed_at).await) + .detach_and_log_err(cx); +} + +impl Render for Banner { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + if !self.should_show(cx) { + return div(); + } + + let border_color = cx.theme().colors().editor_foreground.opacity(0.3); + let banner = h_flex() + .rounded_sm() + .border_1() + .border_color(border_color) + .child( + ButtonLike::new("try-a-feature") + .child((self.details.banner_label)(window, cx)) + .on_click(cx.listener(|this, _, window, cx| { + telemetry::event!("Banner Clicked", source = this.source); + this.dismiss(cx); + window.dispatch_action(this.details.action.boxed_clone(), cx) + })), + ) + .child( + div().border_l_1().border_color(border_color).child( + IconButton::new("close", IconName::Close) + .icon_size(IconSize::Indicator) + .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx))) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Close Announcement Banner", + None, + "It won't show again for this feature", + window, + cx, + ) + }), + ), + ); + + div().pr_2().child(banner) + } +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 57dee9e273..294c3fdb4c 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -1,4 +1,5 @@ mod application_menu; +mod banner; mod collab; mod platforms; mod window_controls; @@ -15,10 +16,10 @@ use crate::application_menu::{ use crate::platforms::{platform_linux, platform_mac, platform_windows}; use auto_update::AutoUpdateStatus; +use banner::{Banner, BannerDetails}; use call::ActiveCall; use client::{Client, UserStore}; use feature_flags::{FeatureFlagAppExt, ZedPro}; -use git_ui::onboarding::GitBanner; use gpui::{ actions, div, px, Action, AnyElement, App, Context, Decorations, Element, Entity, InteractiveElement, Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful, @@ -37,7 +38,8 @@ use ui::{ use util::ResultExt; use workspace::{notifications::NotifyResultExt, Workspace}; use zed_actions::{OpenBrowser, OpenRecent, OpenRemote}; -use zeta::ZedPredictBanner; + +pub use banner::restore_banner; #[cfg(feature = "stories")] pub use stories::*; @@ -126,8 +128,7 @@ pub struct TitleBar { should_move: bool, application_menu: Option>, _subscriptions: Vec, - zed_predict_banner: Entity, - git_banner: Entity, + banner: Entity, } impl Render for TitleBar { @@ -211,8 +212,7 @@ impl Render for TitleBar { .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()), ) .child(self.render_collaborator_list(window, cx)) - .child(self.zed_predict_banner.clone()) - .child(self.git_banner.clone()) + .child(self.banner.clone()) .child( h_flex() .gap_1() @@ -315,8 +315,33 @@ impl TitleBar { subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed)); subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify())); - let zed_predict_banner = cx.new(ZedPredictBanner::new); - let git_banner = cx.new(GitBanner::new); + let banner = cx.new(|cx| { + Banner::new( + "Git Onboarding", + BannerDetails { + action: zed_actions::OpenGitIntegrationOnboarding.boxed_clone(), + banner_label: Box::new(|_, _| { + h_flex() + .h_full() + .items_center() + .gap_1() + .child(Icon::new(IconName::GitBranchSmall).size(IconSize::Small)) + .child( + h_flex() + .gap_0p5() + .child( + Label::new("Introducing:") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Label::new("Git Support").size(LabelSize::Small)), + ) + .into_any_element() + }), + }, + cx, + ) + }); Self { platform_style, @@ -329,8 +354,7 @@ impl TitleBar { user_store, client, _subscriptions: subscriptions, - zed_predict_banner, - git_banner, + banner, } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0de1d83a65..67988ae01a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -182,6 +182,7 @@ actions!( ToggleZoom, Unfollow, Welcome, + RestoreBanner, ] ); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c532185494..a81905525c 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -122,6 +122,7 @@ theme.workspace = true theme_extension.workspace = true theme_selector.workspace = true time.workspace = true +title_bar.workspace = true toolchain_selector.workspace = true ui.workspace = true ui_prompt.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 99bfe4992d..4d4284230c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -65,12 +65,12 @@ use uuid::Uuid; use vim_mode_setting::VimModeSetting; use welcome::{BaseKeymap, MultibufferHint}; use workspace::notifications::{dismiss_app_notification, show_app_notification, NotificationId}; -use workspace::CloseIntent; use workspace::{ create_and_open_local_file, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, }; use workspace::{notifications::DetachAndPromptErr, Pane}; +use workspace::{CloseIntent, RestoreBanner}; use zed_actions::{ OpenAccountSettings, OpenBrowser, OpenServerSettings, OpenSettings, OpenZedUrl, Quit, }; @@ -105,6 +105,8 @@ pub fn init(cx: &mut App) { cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps()); cx.on_action(quit); + cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx)); + if ReleaseChannel::global(cx) == ReleaseChannel::Dev { cx.on_action(test_panic); } diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 6621417b18..de4bb36450 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -19,7 +19,6 @@ test-support = [] [dependencies] anyhow.workspace = true arrayvec.workspace = true -chrono.workspace = true client.workspace = true collections.workspace = true command_palette_hooks.workspace = true diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 1e16072c0c..e0e3578af1 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -43,8 +43,6 @@ pub fn init(cx: &mut App) { .edit_prediction_provider = Some(EditPredictionProvider::None) }, ); - - crate::onboarding_banner::clear_dismissed(cx); }); }) .detach(); diff --git a/crates/zeta/src/onboarding_banner.rs b/crates/zeta/src/onboarding_banner.rs deleted file mode 100644 index f38d6fd700..0000000000 --- a/crates/zeta/src/onboarding_banner.rs +++ /dev/null @@ -1,138 +0,0 @@ -use chrono::Utc; -use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag}; -use gpui::Subscription; -use language::language_settings::{all_language_settings, EditPredictionProvider}; -use settings::SettingsStore; -use ui::{prelude::*, ButtonLike, Tooltip}; -use util::ResultExt; - -use crate::onboarding_event; - -/// Prompts the user to try Zed's Edit Prediction feature -pub struct ZedPredictBanner { - dismissed: bool, - provider: EditPredictionProvider, - _subscription: Subscription, -} - -impl ZedPredictBanner { - pub fn new(cx: &mut Context) -> Self { - Self { - dismissed: get_dismissed(), - provider: all_language_settings(None, cx).edit_predictions.provider, - _subscription: cx.observe_global::(Self::handle_settings_changed), - } - } - - fn should_show(&self, cx: &mut App) -> bool { - cx.has_flag::() && !self.dismissed && !self.provider.is_zed() - } - - fn handle_settings_changed(&mut self, cx: &mut Context) { - let new_provider = all_language_settings(None, cx).edit_predictions.provider; - - if new_provider == self.provider { - return; - } - - if new_provider.is_zed() { - self.dismiss(cx); - } else { - self.dismissed = get_dismissed(); - } - - self.provider = new_provider; - cx.notify(); - } - - fn dismiss(&mut self, cx: &mut Context) { - onboarding_event!("Banner Dismissed"); - persist_dismissed(cx); - self.dismissed = true; - cx.notify(); - } -} - -const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at"; - -fn get_dismissed() -> bool { - db::kvp::KEY_VALUE_STORE - .read_kvp(DISMISSED_AT_KEY) - .log_err() - .map_or(false, |dismissed| dismissed.is_some()) -} - -fn persist_dismissed(cx: &mut App) { - cx.spawn(async |_| { - let time = Utc::now().to_rfc3339(); - db::kvp::KEY_VALUE_STORE - .write_kvp(DISMISSED_AT_KEY.into(), time) - .await - }) - .detach_and_log_err(cx); -} - -pub(crate) fn clear_dismissed(cx: &mut App) { - cx.spawn(async |_| { - db::kvp::KEY_VALUE_STORE - .delete_kvp(DISMISSED_AT_KEY.into()) - .await - }) - .detach_and_log_err(cx); -} - -impl Render for ZedPredictBanner { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.should_show(cx) { - return div(); - } - - let border_color = cx.theme().colors().editor_foreground.opacity(0.3); - let banner = h_flex() - .rounded_sm() - .border_1() - .border_color(border_color) - .child( - ButtonLike::new("try-zed-predict") - .child( - h_flex() - .h_full() - .items_center() - .gap_1p5() - .child(Icon::new(IconName::ZedPredict).size(IconSize::Small)) - .child( - h_flex() - .gap_0p5() - .child( - Label::new("Introducing:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Label::new("Edit Prediction").size(LabelSize::Small)), - ), - ) - .on_click(|_, window, cx| { - onboarding_event!("Banner Clicked"); - window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx) - }), - ) - .child( - div().border_l_1().border_color(border_color).child( - IconButton::new("close", IconName::Close) - .icon_size(IconSize::Indicator) - .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx))) - .tooltip(|window, cx| { - Tooltip::with_meta( - "Close Announcement Banner", - None, - "It won't show again for this feature", - window, - cx, - ) - }), - ), - ); - - div().pr_2().child(banner) - } -} diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 58aaffb6a5..78ad51247d 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -2,7 +2,6 @@ mod completion_diff_element; mod init; mod input_excerpt; mod license_detection; -mod onboarding_banner; mod onboarding_modal; mod onboarding_telemetry; mod rate_completion_modal; @@ -13,7 +12,6 @@ pub use init::*; use inline_completion::DataCollectionState; pub use license_detection::is_license_eligible_for_data_collection; use license_detection::LICENSE_FILES_TO_CHECK; -pub use onboarding_banner::*; pub use rate_completion_modal::*; use anyhow::{anyhow, Context as _, Result};