onboarding_banner.rs

  1use chrono::Utc;
  2use feature_flags::{FeatureFlagAppExt as _, PredictEditsFeatureFlag};
  3use gpui::Subscription;
  4use language::language_settings::{all_language_settings, EditPredictionProvider};
  5use settings::SettingsStore;
  6use ui::{prelude::*, ButtonLike, Tooltip};
  7use util::ResultExt;
  8
  9use crate::onboarding_event;
 10
 11/// Prompts the user to try Zed's Edit Prediction feature
 12pub struct ZedPredictBanner {
 13    dismissed: bool,
 14    provider: EditPredictionProvider,
 15    _subscription: Subscription,
 16}
 17
 18impl ZedPredictBanner {
 19    pub fn new(cx: &mut Context<Self>) -> Self {
 20        Self {
 21            dismissed: get_dismissed(),
 22            provider: all_language_settings(None, cx).edit_predictions.provider,
 23            _subscription: cx.observe_global::<SettingsStore>(Self::handle_settings_changed),
 24        }
 25    }
 26
 27    fn should_show(&self, cx: &mut App) -> bool {
 28        cx.has_flag::<PredictEditsFeatureFlag>() && !self.dismissed && !self.provider.is_zed()
 29    }
 30
 31    fn handle_settings_changed(&mut self, cx: &mut Context<Self>) {
 32        let new_provider = all_language_settings(None, cx).edit_predictions.provider;
 33
 34        if new_provider == self.provider {
 35            return;
 36        }
 37
 38        if new_provider.is_zed() {
 39            self.dismiss(cx);
 40        } else {
 41            self.dismissed = get_dismissed();
 42        }
 43
 44        self.provider = new_provider;
 45        cx.notify();
 46    }
 47
 48    fn dismiss(&mut self, cx: &mut Context<Self>) {
 49        onboarding_event!("Banner Dismissed");
 50        persist_dismissed(cx);
 51        self.dismissed = true;
 52        cx.notify();
 53    }
 54}
 55
 56const DISMISSED_AT_KEY: &str = "zed_predict_banner_dismissed_at";
 57
 58fn get_dismissed() -> bool {
 59    db::kvp::KEY_VALUE_STORE
 60        .read_kvp(DISMISSED_AT_KEY)
 61        .log_err()
 62        .map_or(false, |dismissed| dismissed.is_some())
 63}
 64
 65fn persist_dismissed(cx: &mut App) {
 66    cx.spawn(async |_| {
 67        let time = Utc::now().to_rfc3339();
 68        db::kvp::KEY_VALUE_STORE
 69            .write_kvp(DISMISSED_AT_KEY.into(), time)
 70            .await
 71    })
 72    .detach_and_log_err(cx);
 73}
 74
 75pub(crate) fn clear_dismissed(cx: &mut App) {
 76    cx.spawn(async |_| {
 77        db::kvp::KEY_VALUE_STORE
 78            .delete_kvp(DISMISSED_AT_KEY.into())
 79            .await
 80    })
 81    .detach_and_log_err(cx);
 82}
 83
 84impl Render for ZedPredictBanner {
 85    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 86        if !self.should_show(cx) {
 87            return div();
 88        }
 89
 90        let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
 91        let banner = h_flex()
 92            .rounded_sm()
 93            .border_1()
 94            .border_color(border_color)
 95            .child(
 96                ButtonLike::new("try-zed-predict")
 97                    .child(
 98                        h_flex()
 99                            .h_full()
100                            .items_center()
101                            .gap_1p5()
102                            .child(Icon::new(IconName::ZedPredict).size(IconSize::Small))
103                            .child(
104                                h_flex()
105                                    .gap_0p5()
106                                    .child(
107                                        Label::new("Introducing:")
108                                            .size(LabelSize::Small)
109                                            .color(Color::Muted),
110                                    )
111                                    .child(Label::new("Edit Prediction").size(LabelSize::Small)),
112                            ),
113                    )
114                    .on_click(|_, window, cx| {
115                        onboarding_event!("Banner Clicked");
116                        window.dispatch_action(Box::new(zed_actions::OpenZedPredictOnboarding), cx)
117                    }),
118            )
119            .child(
120                div().border_l_1().border_color(border_color).child(
121                    IconButton::new("close", IconName::Close)
122                        .icon_size(IconSize::Indicator)
123                        .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
124                        .tooltip(|window, cx| {
125                            Tooltip::with_meta(
126                                "Close Announcement Banner",
127                                None,
128                                "It won't show again for this feature",
129                                window,
130                                cx,
131                            )
132                        }),
133                ),
134            );
135
136        div().pr_2().child(banner)
137    }
138}