banner.rs

  1use gpui::{Action, Entity, Global, Render};
  2use ui::{prelude::*, ButtonLike, Tooltip};
  3use util::ResultExt;
  4
  5/// Prompts the user to try Zed's features
  6pub struct Banner {
  7    dismissed: bool,
  8    source: String,
  9    details: BannerDetails,
 10}
 11
 12#[derive(Clone)]
 13struct BannerGlobal {
 14    entity: Entity<Banner>,
 15}
 16impl Global for BannerGlobal {}
 17
 18pub struct BannerDetails {
 19    pub action: Box<dyn Action>,
 20    pub banner_label: Box<dyn Fn(&Window, &mut Context<Banner>) -> AnyElement>,
 21}
 22
 23impl Banner {
 24    pub fn new(source: &str, details: BannerDetails, cx: &mut Context<Self>) -> Self {
 25        cx.set_global(BannerGlobal {
 26            entity: cx.entity(),
 27        });
 28        Self {
 29            source: source.to_string(),
 30            details,
 31            dismissed: get_dismissed(source),
 32        }
 33    }
 34
 35    fn should_show(&self, _cx: &mut App) -> bool {
 36        !self.dismissed
 37    }
 38
 39    fn dismiss(&mut self, cx: &mut Context<Self>) {
 40        telemetry::event!("Banner Dismissed", source = self.source);
 41        persist_dismissed(&self.source, cx);
 42        self.dismissed = true;
 43        cx.notify();
 44    }
 45}
 46
 47fn dismissed_at_key(source: &str) -> String {
 48    format!(
 49        "{}_{}",
 50        "_banner_dismissed_at",
 51        source.to_lowercase().trim().replace(" ", "_")
 52    )
 53}
 54
 55fn get_dismissed(source: &str) -> bool {
 56    let dismissed_at = if source == "Git Onboarding" {
 57        "zed_git_banner_dismissed_at".to_string()
 58    } else {
 59        dismissed_at_key(source)
 60    };
 61    db::kvp::KEY_VALUE_STORE
 62        .read_kvp(&dismissed_at)
 63        .log_err()
 64        .map_or(false, |dismissed| dismissed.is_some())
 65}
 66
 67fn persist_dismissed(source: &str, cx: &mut App) {
 68    let dismissed_at = dismissed_at_key(source);
 69    cx.spawn(async |_| {
 70        let time = chrono::Utc::now().to_rfc3339();
 71        db::kvp::KEY_VALUE_STORE.write_kvp(dismissed_at, time).await
 72    })
 73    .detach_and_log_err(cx);
 74}
 75
 76pub fn restore_banner(cx: &mut App) {
 77    cx.defer(|cx| {
 78        cx.global::<BannerGlobal>()
 79            .entity
 80            .clone()
 81            .update(cx, |this, cx| {
 82                this.dismissed = false;
 83                cx.notify();
 84            });
 85    });
 86
 87    let source = &cx.global::<BannerGlobal>().entity.read(cx).source;
 88    let dismissed_at = dismissed_at_key(source);
 89    cx.spawn(async |_| db::kvp::KEY_VALUE_STORE.delete_kvp(dismissed_at).await)
 90        .detach_and_log_err(cx);
 91}
 92
 93impl Render for Banner {
 94    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 95        if !self.should_show(cx) {
 96            return div();
 97        }
 98
 99        let border_color = cx.theme().colors().editor_foreground.opacity(0.3);
100        let banner = h_flex()
101            .rounded_sm()
102            .border_1()
103            .border_color(border_color)
104            .child(
105                ButtonLike::new("try-a-feature")
106                    .child((self.details.banner_label)(window, cx))
107                    .on_click(cx.listener(|this, _, window, cx| {
108                        telemetry::event!("Banner Clicked", source = this.source);
109                        this.dismiss(cx);
110                        window.dispatch_action(this.details.action.boxed_clone(), cx)
111                    })),
112            )
113            .child(
114                div().border_l_1().border_color(border_color).child(
115                    IconButton::new("close", IconName::Close)
116                        .icon_size(IconSize::Indicator)
117                        .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx)))
118                        .tooltip(|window, cx| {
119                            Tooltip::with_meta(
120                                "Close Announcement Banner",
121                                None,
122                                "It won't show again for this feature",
123                                window,
124                                cx,
125                            )
126                        }),
127                ),
128            );
129
130        div().pr_2().child(banner)
131    }
132}