diff --git a/Cargo.lock b/Cargo.lock index 94fa1d8473..22f1b33b45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,12 +346,15 @@ dependencies = [ "isahc", "lazy_static", "log", + "menu", + "project", "serde", "serde_json", "settings", "smol", "tempdir", "theme", + "util", "workspace", ] diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index dd90fea661..944aa87ee5 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -10,9 +10,12 @@ doctest = false [dependencies] client = { path = "../client" } gpui = { path = "../gpui" } +menu = { path = "../menu" } +project = { path = "../project" } settings = { path = "../settings" } theme = { path = "../theme" } workspace = { path = "../workspace" } +util = { path = "../util" } anyhow = "1.0.38" isahc = "1.7" lazy_static = "1.4" diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 234319bdd6..44eb5fe2e8 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,19 +1,24 @@ +mod update_notification; + use anyhow::{anyhow, Context, Result}; use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; use gpui::{ actions, elements::{Empty, MouseEventHandler, Text}, platform::AppVersion, - AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, - ViewContext, + AppContext, AsyncAppContext, Element, Entity, ModelContext, ModelHandle, MutableAppContext, + Task, View, ViewContext, WeakViewHandle, }; use lazy_static::lazy_static; use serde::Deserialize; use settings::Settings; use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; -use workspace::{ItemHandle, StatusItemView}; +use update_notification::UpdateNotification; +use workspace::{ItemHandle, StatusItemView, Workspace}; +const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &'static str = + "auto-updater-should-show-updated-notification"; const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60); lazy_static! { @@ -23,7 +28,7 @@ lazy_static! { pub static ref ZED_APP_PATH: Option = env::var("ZED_APP_PATH").ok().map(PathBuf::from); } -actions!(auto_update, [Check, DismissErrorMessage]); +actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]); #[derive(Clone, PartialEq, Eq)] pub enum AutoUpdateStatus { @@ -40,6 +45,7 @@ pub struct AutoUpdater { current_version: AppVersion, http_client: Arc, pending_poll: Option>, + db: Arc, server_url: String, } @@ -57,10 +63,15 @@ impl Entity for AutoUpdater { type Event = (); } -pub fn init(http_client: Arc, server_url: String, cx: &mut MutableAppContext) { +pub fn init( + db: Arc, + http_client: Arc, + server_url: String, + cx: &mut MutableAppContext, +) { if let Some(version) = ZED_APP_VERSION.clone().or(cx.platform().app_version().ok()) { let auto_updater = cx.add_model(|cx| { - let updater = AutoUpdater::new(version, http_client, server_url); + let updater = AutoUpdater::new(version, db.clone(), http_client, server_url.clone()); updater.start_polling(cx).detach(); updater }); @@ -70,10 +81,44 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut Mutab updater.update(cx, |updater, cx| updater.poll(cx)); } }); + cx.add_global_action(move |_: &ViewReleaseNotes, cx| { + cx.platform().open_url(&format!("{server_url}/releases")); + }); cx.add_action(AutoUpdateIndicator::dismiss_error_message); + cx.add_action(UpdateNotification::dismiss); } } +pub fn notify_of_any_new_update( + workspace: WeakViewHandle, + cx: &mut MutableAppContext, +) -> Option<()> { + let updater = AutoUpdater::get(cx)?; + let version = updater.read(cx).current_version; + let should_show_notification = updater.read(cx).should_show_update_notification(cx); + + cx.spawn(|mut cx| async move { + let should_show_notification = should_show_notification.await?; + if should_show_notification { + if let Some(workspace) = workspace.upgrade(&cx) { + workspace.update(&mut cx, |workspace, cx| { + workspace.show_notification(0, cx, |cx| { + cx.add_view(|_| UpdateNotification::new(version)) + }); + updater + .read(cx) + .set_should_show_update_notification(false, cx) + .detach_and_log_err(cx); + }); + } + } + anyhow::Ok(()) + }) + .detach(); + + None +} + impl AutoUpdater { fn get(cx: &mut MutableAppContext) -> Option> { cx.default_global::>>().clone() @@ -81,12 +126,14 @@ impl AutoUpdater { fn new( current_version: AppVersion, + db: Arc, http_client: Arc, server_url: String, ) -> Self { Self { status: AutoUpdateStatus::Idle, current_version, + db, http_client, server_url, pending_poll: None, @@ -221,11 +268,36 @@ impl AutoUpdater { } this.update(&mut cx, |this, cx| { + this.set_should_show_update_notification(true, cx) + .detach_and_log_err(cx); this.status = AutoUpdateStatus::Updated; cx.notify(); }); Ok(()) } + + fn set_should_show_update_notification( + &self, + should_show: bool, + cx: &AppContext, + ) -> Task> { + let db = self.db.clone(); + cx.background().spawn(async move { + if should_show { + db.write([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")])?; + } else { + db.delete([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)])?; + } + Ok(()) + }) + } + + fn should_show_update_notification(&self, cx: &AppContext) -> Task> { + let db = self.db.clone(); + cx.background().spawn(async move { + Ok(db.read([(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)])?[0].is_some()) + }) + } } impl Entity for AutoUpdateIndicator { diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs new file mode 100644 index 0000000000..e9c73ef4bc --- /dev/null +++ b/crates/auto_update/src/update_notification.rs @@ -0,0 +1,106 @@ +use crate::ViewReleaseNotes; +use gpui::{ + elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, + platform::{AppVersion, CursorStyle}, + Element, Entity, View, ViewContext, +}; +use menu::Cancel; +use settings::Settings; +use workspace::Notification; + +pub struct UpdateNotification { + version: AppVersion, +} + +pub enum Event { + Dismiss, +} + +impl Entity for UpdateNotification { + type Event = Event; +} + +impl View for UpdateNotification { + fn ui_name() -> &'static str { + "UpdateNotification" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + let theme = cx.global::().theme.clone(); + let theme = &theme.update_notification; + + MouseEventHandler::new::(0, cx, |state, cx| { + Flex::column() + .with_child( + Flex::row() + .with_child( + Text::new( + format!("Updated to Zed {}", self.version), + theme.message.text.clone(), + ) + .contained() + .with_style(theme.message.container) + .aligned() + .top() + .left() + .flex(1., true) + .boxed(), + ) + .with_child( + MouseEventHandler::new::(0, cx, |state, _| { + let style = theme.dismiss_button.style_for(state, false); + Svg::new("icons/decline.svg") + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .boxed() + }) + .with_padding(Padding::uniform(5.)) + .on_click(move |_, _, cx| cx.dispatch_action(Cancel)) + .aligned() + .constrained() + .with_height(cx.font_cache().line_height(theme.message.text.font_size)) + .aligned() + .top() + .flex_float() + .boxed(), + ) + .boxed(), + ) + .with_child({ + let style = theme.action_message.style_for(state, false); + Text::new("View the release notes".to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .contained() + .boxed() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(|_, _, cx| cx.dispatch_action(ViewReleaseNotes)) + .boxed() + } +} + +impl Notification for UpdateNotification { + fn should_dismiss_notification_on_event(&self, event: &::Event) -> bool { + matches!(event, Event::Dismiss) + } +} + +impl UpdateNotification { + pub fn new(version: AppVersion) -> Self { + Self { version } + } + + pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { + cx.emit(Event::Dismiss); + } +} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f469423921..2fa867b02d 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -31,6 +31,7 @@ pub struct Theme { pub project_diagnostics: ProjectDiagnostics, pub breadcrumbs: ContainedText, pub contact_notification: ContactNotification, + pub update_notification: UpdateNotification, pub tooltip: TooltipStyle, } @@ -412,6 +413,13 @@ pub struct ContactNotification { pub dismiss_button: Interactive, } +#[derive(Deserialize, Default)] +pub struct UpdateNotification { + pub message: ContainedText, + pub action_message: Interactive, + pub dismiss_button: Interactive, +} + #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0be86f63e8..8706f1327a 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -158,7 +158,6 @@ fn main() { let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); context_menu::init(cx); - auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); @@ -211,7 +210,7 @@ fn main() { .detach(); cx.set_global(settings); - let project_store = cx.add_model(|_| ProjectStore::new(db)); + let project_store = cx.add_model(|_| ProjectStore::new(db.clone())); let app_state = Arc::new(AppState { languages, themes, @@ -222,6 +221,7 @@ fn main() { build_window_options, initialize_workspace, }); + auto_update::init(db, http, client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); journal::init(app_state.clone(), cx); theme_selector::init(app_state.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index b913e4621d..1ebb1dc446 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -209,6 +209,8 @@ pub fn initialize_workspace( status_bar.add_right_item(cursor_position, cx); status_bar.add_right_item(auto_update, cx); }); + + auto_update::notify_of_any_new_update(cx.weak_handle(), cx); } pub fn build_window_options() -> WindowOptions<'static> { diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 083863f6da..e015895e9c 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -12,6 +12,7 @@ import workspace from "./workspace"; import contextMenu from "./contextMenu"; import projectDiagnostics from "./projectDiagnostics"; import contactNotification from "./contactNotification"; +import updateNotification from "./updateNotification"; import tooltip from "./tooltip"; export const panel = { @@ -38,6 +39,7 @@ export default function app(theme: Theme): Object { }, }, contactNotification: contactNotification(theme), + updateNotification: updateNotification(theme), tooltip: tooltip(theme), }; } diff --git a/styles/src/styleTree/updateNotification.ts b/styles/src/styleTree/updateNotification.ts new file mode 100644 index 0000000000..1c3b705582 --- /dev/null +++ b/styles/src/styleTree/updateNotification.ts @@ -0,0 +1,30 @@ +import Theme from "../themes/common/theme"; +import { iconColor, text } from "./components"; + +const headerPadding = 8; + +export default function updateNotification(theme: Theme): Object { + return { + message: { + ...text(theme, "sans", "primary", { size: "xs" }), + margin: { left: headerPadding, right: headerPadding } + }, + actionMessage: { + ...text(theme, "sans", "secondary", { size: "xs" }), + margin: { left: headerPadding, top: 6, bottom: 6 }, + hover: { + color: theme.textColor["active"].value + } + }, + dismissButton: { + color: iconColor(theme, "secondary"), + iconWidth: 8, + iconHeight: 8, + buttonWidth: 8, + buttonHeight: 8, + hover: { + color: iconColor(theme, "primary") + } + } + } +} \ No newline at end of file