From 9da624dd0c9eee4c67786aa044ff2fba78864610 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sun, 2 Nov 2025 08:22:55 +0700 Subject: [PATCH] feat: nostr based auto updater (#200) * . * refactor * fix * . * clean up * clean up --- Cargo.lock | 141 +-------- crates/auto_update/Cargo.toml | 5 +- crates/auto_update/src/lib.rs | 521 ++++++++++++++++++++++++++------ crates/coop/src/actions.rs | 1 + crates/coop/src/chatspace.rs | 69 +++-- crates/states/src/constants.rs | 2 - crates/ui/src/input/rope_ext.rs | 1 + rust-toolchain.toml | 2 +- 8 files changed, 471 insertions(+), 271 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5faf3532..faabcf97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -428,7 +428,7 @@ dependencies = [ "libc", "pin-project", "redox_syscall 0.2.16", - "xattr 0.2.3", + "xattr", ] [[package]] @@ -515,14 +515,17 @@ name = "auto_update" version = "0.2.11" dependencies = [ "anyhow", - "cargo-packager-updater", "common", "gpui", + "gpui_tokio", "log", "nostr-sdk", + "reqwest", + "semver", "smallvec", "smol", "states", + "tempfile", ] [[package]] @@ -917,42 +920,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "cargo-packager-updater" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eec09acab5c2227aba2e592d431708305bdeb6d507703f6cd8983fb57b6c5ef7" -dependencies = [ - "base64", - "cargo-packager-utils", - "ctor 0.2.9", - "dirs 5.0.1", - "dunce", - "flate2", - "http", - "log", - "minisign-verify", - "percent-encoding", - "reqwest", - "semver", - "serde", - "serde_json", - "tar", - "tempfile", - "thiserror 1.0.69", - "time", - "url", -] - -[[package]] -name = "cargo-packager-utils" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b43458dd2ee3cdab3f5b105acd80791383b730380c929018701313d7d299d4e8" -dependencies = [ - "ctor 0.2.9", -] - [[package]] name = "cbc" version = "0.1.2" @@ -1607,16 +1574,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctor" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" -dependencies = [ - "quote", - "syn 2.0.108", -] - [[package]] name = "ctor" version = "0.4.3" @@ -1651,15 +1608,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", -] - [[package]] name = "derive_more" version = "0.99.20" @@ -2598,7 +2546,7 @@ dependencies = [ "core-text", "core-video", "cosmic-text", - "ctor 0.4.3", + "ctor", "derive_more", "embed-resource", "etagere", @@ -3747,7 +3695,7 @@ dependencies = [ "bindgen 0.71.1", "core-foundation 0.10.0", "core-video", - "ctor 0.4.3", + "ctor", "foreign-types 0.5.0", "metal", "objc", @@ -3823,12 +3771,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "minisign-verify" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -4175,12 +4117,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-derive" version = "0.4.2" @@ -4816,12 +4752,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -5289,7 +5219,6 @@ dependencies = [ "base64", "bytes", "encoding_rs", - "futures-channel", "futures-core", "futures-util", "h2", @@ -5307,9 +5236,6 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", - "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -5317,7 +5243,6 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -6550,17 +6475,6 @@ dependencies = [ "objc", ] -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr 1.6.1", -] - [[package]] name = "target-lexicon" version = "0.12.16" @@ -6672,37 +6586,6 @@ dependencies = [ "zune-jpeg", ] -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -8471,16 +8354,6 @@ dependencies = [ "libc", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.2", -] - [[package]] name = "xcb" version = "1.6.0" diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 1c3fad98..1e1c7fe8 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -9,10 +9,13 @@ common = { path = "../common" } states = { path = "../states" } gpui.workspace = true +gpui_tokio.workspace = true +reqwest.workspace = true nostr-sdk.workspace = true anyhow.workspace = true smol.workspace = true log.workspace = true smallvec.workspace = true -cargo-packager-updater = "0.2.3" +semver = "1.0.27" +tempfile = "3.23.0" diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index d084c19a..c25123a7 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -1,10 +1,21 @@ -use anyhow::Error; -use cargo_packager_updater::semver::Version; -use cargo_packager_updater::{check_update, Config, Update}; -use gpui::http_client::Url; -use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; +use std::ffi::OsString; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, Context as AnyhowContext, Error}; +use gpui::http_client::{AsyncBody, HttpClient}; +use gpui::{ + App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task, +}; +use nostr_sdk::prelude::*; +use semver::Version; use smallvec::{smallvec, SmallVec}; -use states::{APP_PUBKEY, APP_UPDATER_ENDPOINT}; +use smol::fs::File; +use smol::process::Command; +use states::{app_state, BOOTSTRAP_RELAYS}; + +const APP_PUBKEY: &str = "npub1y9jvl5vznq49eh9f2gj7679v4042kj80lp7p8fte3ql2cr7hty7qsyca8q"; pub fn init(cx: &mut App) { AutoUpdater::set_global(cx.new(AutoUpdater::new), cx); @@ -14,16 +25,101 @@ struct GlobalAutoUpdater(Entity); impl Global for GlobalAutoUpdater {} -#[derive(Debug, Clone)] +#[cfg(not(target_os = "windows"))] +struct InstallerDir(tempfile::TempDir); + +#[cfg(not(target_os = "windows"))] +impl InstallerDir { + async fn new() -> Result { + Ok(Self( + tempfile::Builder::new() + .prefix("coop-auto-update") + .tempdir()?, + )) + } + + fn path(&self) -> &Path { + self.0.path() + } +} + +#[cfg(target_os = "windows")] +struct InstallerDir(PathBuf); + +#[cfg(target_os = "windows")] +impl InstallerDir { + async fn new() -> Result { + let installer_dir = std::env::current_exe()? + .parent() + .context("No parent dir for Coop.exe")? + .join("updates"); + + if smol::fs::metadata(&installer_dir).await.is_ok() { + smol::fs::remove_dir_all(&installer_dir).await?; + } + + smol::fs::create_dir(&installer_dir).await?; + + Ok(Self(installer_dir)) + } + + fn path(&self) -> &Path { + self.0.as_path() + } +} + +struct MacOsUnmounter<'a> { + mount_path: PathBuf, + background_executor: &'a BackgroundExecutor, +} + +impl Drop for MacOsUnmounter<'_> { + fn drop(&mut self) { + let mount_path = std::mem::take(&mut self.mount_path); + + self.background_executor + .spawn(async move { + let unmount_output = Command::new("hdiutil") + .args(["detach", "-force"]) + .arg(&mount_path) + .output() + .await; + + match unmount_output { + Ok(output) if output.status.success() => { + log::info!("Successfully unmounted the disk image"); + } + Ok(output) => { + log::error!( + "Failed to unmount disk image: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + } + Err(error) => { + log::error!("Error while trying to unmount disk image: {:?}", error); + } + } + }) + .detach(); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum AutoUpdateStatus { Idle, Checking, - Checked { update: Box }, + Checked { files: Vec }, Installing, Updated, Errored { msg: Box }, } +impl AsRef for AutoUpdateStatus { + fn as_ref(&self) -> &AutoUpdateStatus { + self + } +} + impl AutoUpdateStatus { pub fn is_updating(&self) -> bool { matches!(self, Self::Checked { .. } | Self::Installing) @@ -33,10 +129,8 @@ impl AutoUpdateStatus { matches!(self, Self::Updated) } - pub fn checked(update: Update) -> Self { - Self::Checked { - update: Box::new(update), - } + pub fn checked(files: Vec) -> Self { + Self::Checked { files } } pub fn error(e: String) -> Self { @@ -44,109 +138,85 @@ impl AutoUpdateStatus { } } +#[derive(Debug)] pub struct AutoUpdater { + /// Current status of the auto updater pub status: AutoUpdateStatus, - config: Config, - version: Version, - #[allow(dead_code)] - subscriptions: SmallVec<[Subscription; 1]>, + + /// Current version of the application + pub version: Version, + + /// Event subscriptions + _subscriptions: SmallVec<[Subscription; 1]>, + + /// Background tasks + _tasks: SmallVec<[Task<()>; 2]>, } impl AutoUpdater { - /// Retrieve the Global Auto Updater instance + /// Retrieve the global auto updater instance pub fn global(cx: &App) -> Entity { cx.global::().0.clone() } - /// Retrieve the Auto Updater instance - pub fn read_global(cx: &App) -> &Self { - cx.global::().0.read(cx) - } - - /// Set the Global Auto Updater instance - pub(crate) fn set_global(state: Entity, cx: &mut App) { + /// Set the global auto updater instance + fn set_global(state: Entity, cx: &mut App) { cx.set_global(GlobalAutoUpdater(state)); } - pub(crate) fn new(cx: &mut Context) -> Self { - let config = cargo_packager_updater::Config { - endpoints: vec![Url::parse(APP_UPDATER_ENDPOINT).expect("Endpoint is not valid")], - pubkey: String::from(APP_PUBKEY), - ..Default::default() - }; - let version = Version::parse(env!("CARGO_PKG_VERSION")).expect("Failed to parse version"); - let mut subscriptions = smallvec![]; + fn new(cx: &mut Context) -> Self { + let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); + let async_version = version.clone(); - subscriptions.push(cx.observe_new::(|this, window, cx| { - if let Some(window) = window { - this.check_for_updates(window, cx); - } - })); + let mut subscriptions = smallvec![]; + let mut tasks = smallvec![]; + + tasks.push( + // Subscribe to get the new update event in the bootstrap relays + Self::subscribe_to_updates(cx), + ); + + tasks.push( + // Subscribe to get the new update event in the bootstrap relays + cx.spawn(async move |this, cx| { + // Check for updates after 2 minutes + cx.background_executor() + .timer(Duration::from_secs(120)) + .await; + + // Update the status to checking + _ = this.update(cx, |this, cx| { + this.set_status(AutoUpdateStatus::Checking, cx); + }); + + match Self::check_for_updates(async_version, cx).await { + Ok(ids) => { + // Update the status to downloading + _ = this.update(cx, |this, cx| { + this.set_status(AutoUpdateStatus::checked(ids), cx); + }); + } + Err(e) => { + log::warn!("{e}") + } + } + }), + ); + + subscriptions.push( + // Observe the status + cx.observe_self(|this, cx| { + if let AutoUpdateStatus::Checked { files } = this.status.clone() { + this.get_latest_release(&files, cx); + } + }), + ); Self { status: AutoUpdateStatus::Idle, version, - config, - subscriptions, - } - } - - pub fn check_for_updates(&mut self, window: &mut Window, cx: &mut Context) { - let config = self.config.clone(); - let current_version = self.version.clone(); - - log::info!("Checking for updates..."); - self.set_status(AutoUpdateStatus::Checking, cx); - - let checking: Task, Error>> = cx.background_spawn(async move { - if let Some(update) = check_update(current_version, config)? { - Ok(Some(update)) - } else { - Ok(None) - } - }); - - cx.spawn_in(window, async move |this, cx| { - if let Ok(Some(update)) = checking.await { - this.update_in(cx, |this, window, cx| { - this.set_status(AutoUpdateStatus::checked(update), cx); - this.install_update(window, cx); - }) - .ok(); - } else { - this.update(cx, |this, cx| { - this.set_status(AutoUpdateStatus::Idle, cx); - }) - .ok(); - } - }) - .detach(); - } - - pub(crate) fn install_update(&mut self, window: &mut Window, cx: &mut Context) { - self.set_status(AutoUpdateStatus::Installing, cx); - - if let AutoUpdateStatus::Checked { update } = self.status.clone() { - let install: Task> = - cx.background_spawn(async move { Ok(update.download_and_install()?) }); - - cx.spawn_in(window, async move |this, cx| { - match install.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_status(AutoUpdateStatus::Updated, cx); - }) - .ok(); - } - Err(e) => { - this.update(cx, |this, cx| { - this.set_status(AutoUpdateStatus::error(e.to_string()), cx); - }) - .ok(); - } - }; - }) - .detach(); + _subscriptions: subscriptions, + _tasks: tasks, } } @@ -154,4 +224,259 @@ impl AutoUpdater { self.status = status; cx.notify(); } + + fn subscribe_to_updates(cx: &App) -> Task<()> { + cx.background_spawn(async move { + let client = app_state().client(); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap(); + + let filter = Filter::new() + .kind(Kind::ReleaseArtifactSet) + .author(app_pubkey) + .limit(1); + + if let Err(e) = client + .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) + .await + { + log::error!("Failed to subscribe to updates: {e}"); + }; + }) + } + + fn check_for_updates(version: Version, cx: &AsyncApp) -> Task, Error>> { + cx.background_spawn(async move { + let client = app_state().client(); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap(); + + let filter = Filter::new() + .kind(Kind::ReleaseArtifactSet) + .author(app_pubkey) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + let new_version: Version = event + .tags + .find(TagKind::d()) + .and_then(|tag| tag.content()) + .and_then(|content| content.split("@").last()) + .and_then(|content| Version::parse(content).ok()) + .context("Failed to parse version")?; + + if new_version > version { + // Get all file metadata event ids + let ids: Vec = event.tags.event_ids().copied().collect(); + + let filter = Filter::new() + .kind(Kind::FileMetadata) + .author(app_pubkey) + .ids(ids.clone()); + + // Get all files for this release + client + .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) + .await?; + + Ok(ids) + } else { + Err(anyhow!("No update available")) + } + } else { + Err(anyhow!("Not found")) + } + }) + } + + fn get_latest_release(&mut self, ids: &[EventId], cx: &mut Context) { + let http_client = cx.http_client(); + let ids = ids.to_vec(); + + let task: Task> = cx.background_spawn(async move { + let client = app_state().client(); + let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap(); + let os = std::env::consts::OS; + + let filter = Filter::new() + .kind(Kind::FileMetadata) + .author(app_pubkey) + .ids(ids); + + // Get all urls for this release + let events = client.database().query(filter).await?; + + for event in events.into_iter() { + // Only process events that match current platform + if event.content != os { + continue; + } + + // Parse the url + let url = event + .tags + .find(TagKind::Url) + .and_then(|tag| tag.content()) + .and_then(|content| Url::parse(content).ok()) + .context("Failed to parse url")?; + + let installer_dir = InstallerDir::new().await?; + let target_path = Self::target_path(&installer_dir).await?; + + // Download the release + download(url.as_str(), &target_path, http_client).await?; + + return Ok((installer_dir, target_path)); + } + + Err(anyhow!("Failed to get latest release")) + }); + + self._tasks.push( + // Install the new release + cx.spawn(async move |this, cx| { + _ = this.update(cx, |this, cx| { + this.set_status(AutoUpdateStatus::Installing, cx); + }); + + match task.await { + Ok((installer_dir, target_path)) => { + if Self::install(installer_dir, target_path, cx).await.is_ok() { + // Update the status to updated + _ = this.update(cx, |this, cx| { + this.set_status(AutoUpdateStatus::Updated, cx); + }); + } + } + Err(e) => { + // Update the status to error including the error message + _ = this.update(cx, |this, cx| { + this.set_status(AutoUpdateStatus::error(e.to_string()), cx); + }); + } + } + }), + ); + } + + async fn target_path(installer_dir: &InstallerDir) -> Result { + let filename = match std::env::consts::OS { + "macos" => anyhow::Ok("Coop.dmg"), + "windows" => Ok("Coop.exe"), + unsupported_os => anyhow::bail!("not supported: {unsupported_os}"), + }?; + + Ok(installer_dir.path().join(filename)) + } + + async fn install( + installer_dir: InstallerDir, + target_path: PathBuf, + cx: &AsyncApp, + ) -> Result<(), Error> { + match std::env::consts::OS { + "macos" => install_release_macos(&installer_dir, target_path, cx).await, + "windows" => install_release_windows(target_path).await, + unsupported_os => anyhow::bail!("Not supported: {unsupported_os}"), + } + } +} + +async fn download( + url: &str, + target_path: &std::path::Path, + client: Arc, +) -> Result<(), Error> { + let body = AsyncBody::default(); + let mut target_file = File::create(&target_path).await?; + let mut response = client.get(url, body, true).await?; + + // Copy the response body to the target file + smol::io::copy(response.body_mut(), &mut target_file).await?; + + Ok(()) +} + +async fn install_release_macos( + temp_dir: &InstallerDir, + downloaded_dmg: PathBuf, + cx: &AsyncApp, +) -> Result<(), Error> { + let running_app_path = cx.update(|cx| cx.app_path())??; + let running_app_filename = running_app_path + .file_name() + .with_context(|| format!("invalid running app path {running_app_path:?}"))?; + + let mount_path = temp_dir.path().join("Coop"); + let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into(); + + mounted_app_path.push("/"); + + let output = Command::new("hdiutil") + .args(["attach", "-nobrowse"]) + .arg(&downloaded_dmg) + .arg("-mountroot") + .arg(temp_dir.path()) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to mount: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + + // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits + let _unmounter = MacOsUnmounter { + mount_path: mount_path.clone(), + background_executor: cx.background_executor(), + }; + + let output = Command::new("rsync") + .args(["-av", "--delete"]) + .arg(&mounted_app_path) + .arg(&running_app_path) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to copy app: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) +} + +async fn install_release_windows(downloaded_installer: PathBuf) -> Result<(), Error> { + //const CREATE_NO_WINDOW: u32 = 0x08000000; + + let system_root = std::env::var("SYSTEMROOT"); + let powershell_path = system_root.as_ref().map_or_else( + |_| "powershell.exe".to_string(), + |p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"), + ); + + let mut installer_path = std::ffi::OsString::new(); + installer_path.push("\""); + installer_path.push(&downloaded_installer); + installer_path.push("\""); + + let output = Command::new(powershell_path) + //.creation_flags(CREATE_NO_WINDOW) + .args(["-NoProfile", "-WindowStyle", "Hidden"]) + .args(["Start-Process"]) + .arg(installer_path) + .arg("-ArgumentList") + .args(["/P", "/R"]) + .output() + .await?; + + anyhow::ensure!( + output.status.success(), + "failed to start installer: {:?}", + String::from_utf8_lossy(&output.stderr) + ); + + Ok(()) } diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index 36a47888..265326e4 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -13,6 +13,7 @@ actions!(sidebar, [Reload, RelayStatus]); pub struct CoopAuthUrlHandler; impl AuthUrlHandler for CoopAuthUrlHandler { + #[allow(mismatched_lifetime_syntaxes)] fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { Box::pin(async move { log::info!("Received Auth URL: {auth_url}"); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 0c59448b..5d1cc42c 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use account::Account; use anyhow::{anyhow, Error}; -use auto_update::AutoUpdater; +use auto_update::{AutoUpdateStatus, AutoUpdater}; use chat::{ChatEvent, ChatRegistry}; use chat_ui::{CopyPublicKey, OpenPublicKey}; use common::display::{shorten_pubkey, RenderedProfile}; @@ -1166,12 +1166,43 @@ impl ChatSpace { fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let file_keystore = KeyStore::global(cx).read(cx).is_using_file_keystore(); let proxy = AppSettings::get_proxy_user_avatars(cx); - let updating = AutoUpdater::read_global(cx).status.is_updating(); - let updated = AutoUpdater::read_global(cx).status.is_updated(); let auth_requests = self.auth_requests.read(cx).len(); h_flex() .gap_1() + .map( + |this| match AutoUpdater::global(cx).read(cx).status.as_ref() { + AutoUpdateStatus::Checking => this.child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Checking for Coop updates...")), + ), + AutoUpdateStatus::Installing => this.child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Installing updates...")), + ), + AutoUpdateStatus::Errored { msg } => this.child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from(msg.as_ref())), + ), + AutoUpdateStatus::Updated => this.child( + div() + .id("restart") + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Updated. Click to restart")) + .on_click(|_ev, _window, cx| { + cx.restart(); + }), + ), + _ => this.child(div()), + }, + ) .when(file_keystore, |this| { this.child( Button::new("keystore-warning") @@ -1185,38 +1216,6 @@ impl ChatSpace { }), ) }) - .when(updating, |this| { - this.child( - h_flex() - .h_6() - .px_2() - .items_center() - .justify_center() - .text_xs() - .rounded_full() - .bg(cx.theme().ghost_element_background_alt) - .child(shared_t!("auto_update.updating")), - ) - }) - .when(updated, |this| { - this.child( - h_flex() - .id("updated") - .h_6() - .px_2() - .items_center() - .justify_center() - .text_xs() - .rounded_full() - .bg(cx.theme().ghost_element_background_alt) - .hover(|this| this.bg(cx.theme().ghost_element_hover)) - .active(|this| this.bg(cx.theme().ghost_element_active)) - .child(shared_t!("auto_update.updated")) - .on_click(|_, _window, cx| { - cx.restart(); - }), - ) - }) .when(auth_requests > 0, |this| { this.child( h_flex() diff --git a/crates/states/src/constants.rs b/crates/states/src/constants.rs index 8f1908bf..9eb6bc74 100644 --- a/crates/states/src/constants.rs +++ b/crates/states/src/constants.rs @@ -1,7 +1,5 @@ pub const CLIENT_NAME: &str = "Coop"; pub const APP_ID: &str = "su.reya.coop"; -pub const APP_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc4MkNFRkQ2RkVGQURGNzUKUldSMTMvcisxdThzZUZraHc4Vno3NVNJek81VkJFUEV3MkJweGFxQXhpekdSU1JIekpqMG4yemMK"; -pub const APP_UPDATER_ENDPOINT: &str = "https://coop-updater.reya.su/"; pub const SETTINGS_IDENTIFIER: &str = "coop:settings"; pub const INBOX_SUB_ID: &str = "inbox"; diff --git a/crates/ui/src/input/rope_ext.rs b/crates/ui/src/input/rope_ext.rs index 96685c59..79629824 100644 --- a/crates/ui/src/input/rope_ext.rs +++ b/crates/ui/src/input/rope_ext.rs @@ -47,6 +47,7 @@ pub trait RopeExt { fn offset_to_position(&self, offset: usize) -> Position; /// Get the word byte range at the given offset (byte). + #[allow(dead_code)] fn word_range(&self, offset: usize) -> Option>; /// Get word at the given offset (byte). diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 806f8019..31719db9 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.88" +channel = "1.90" profile = "minimal" components = ["rustfmt", "clippy"] targets = [