From f8478677c5252930393ff0d1b57eef17acf558d9 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 18 Oct 2025 07:13:52 -0700 Subject: [PATCH] Pass the previous app version to the notification endpoint so the update notification can display all missed changelogs, not just the latest one. --- src-tauri/src/history.rs | 78 ++++++++++++------- src-tauri/src/lib.rs | 2 +- src-tauri/src/notifications.rs | 47 ++++++++--- src-tauri/yaak-license/src/license.rs | 4 +- .../yaak-models/src/queries/key_values.rs | 32 +++++++- 5 files changed, 123 insertions(+), 40 deletions(-) diff --git a/src-tauri/src/history.rs b/src-tauri/src/history.rs index b1a2882e..f3f8359a 100644 --- a/src-tauri/src/history.rs +++ b/src-tauri/src/history.rs @@ -1,48 +1,74 @@ +use chrono::{NaiveDateTime, Utc}; +use log::debug; +use std::sync::OnceLock; use tauri::{AppHandle, Runtime}; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; const NAMESPACE: &str = "analytics"; const NUM_LAUNCHES_KEY: &str = "num_launches"; +const LAST_VERSION_KEY: &str = "last_tracked_version"; +const PREV_VERSION_KEY: &str = "last_tracked_version_prev"; +const VERSION_SINCE_KEY: &str = "last_tracked_version_since"; -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct LaunchEventInfo { pub current_version: String, pub previous_version: String, pub launched_after_update: bool, + pub version_since: NaiveDateTime, + pub user_since: NaiveDateTime, pub num_launches: i32, } -pub async fn store_launch_history(app_handle: &AppHandle) -> LaunchEventInfo { - let last_tracked_version_key = "last_tracked_version"; +static LAUNCH_INFO: OnceLock = OnceLock::new(); - let mut info = LaunchEventInfo::default(); +pub fn get_or_upsert_launch_info(app_handle: &AppHandle) -> &LaunchEventInfo { + LAUNCH_INFO.get_or_init(|| { + let now = Utc::now().naive_utc(); + let mut info = LaunchEventInfo { + version_since: app_handle.db().get_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, now), + current_version: app_handle.package_info().version.to_string(), + user_since: app_handle.db().get_settings().created_at, + num_launches: app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0) + 1, - info.num_launches = get_num_launches(app_handle).await + 1; - info.current_version = app_handle.package_info().version.to_string(); + // The rest will be set below + ..Default::default() + }; - app_handle - .with_tx(|tx| { - info.previous_version = - tx.get_key_value_string(NAMESPACE, last_tracked_version_key, ""); + app_handle + .with_tx(|tx| { + // Load the previously tracked version + let curr_db = tx.get_key_value_str(NAMESPACE, LAST_VERSION_KEY, ""); + let prev_db = tx.get_key_value_str(NAMESPACE, PREV_VERSION_KEY, ""); - if !info.previous_version.is_empty() { - info.launched_after_update = info.current_version != info.previous_version; - }; + // We just updated if the app version is different from the last tracked version we stored + if !curr_db.is_empty() && info.current_version != curr_db { + info.launched_after_update = true; + } - // Update key values + // If we just updated, track the previous version as the "previous" current version + if info.launched_after_update { + info.previous_version = curr_db.clone(); + info.version_since = now; + } else { + info.previous_version = prev_db.clone(); + } - let source = &UpdateSource::Background; - let version = info.current_version.as_str(); - tx.set_key_value_string(NAMESPACE, last_tracked_version_key, version, source); - tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source); - Ok(()) - }) - .unwrap(); + // Rotate stored versions: move previous into the "prev" slot before overwriting + let source = &UpdateSource::Background; - info -} - -pub async fn get_num_launches(app_handle: &AppHandle) -> i32 { - app_handle.db().get_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, 0) + tx.set_key_value_str(NAMESPACE, PREV_VERSION_KEY, &info.previous_version, source); + tx.set_key_value_str(NAMESPACE, LAST_VERSION_KEY, &info.current_version, source); + tx.set_key_value_dte(NAMESPACE, VERSION_SINCE_KEY, info.version_since, source); + tx.set_key_value_int(NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches, source); + + Ok(()) + }) + .unwrap(); + + debug!("Initialized launch info"); + + info + }) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fefcb236..f422d2bb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1454,7 +1454,7 @@ pub fn run() { let _ = window::create_main_window(app_handle, "/"); let h = app_handle.clone(); tauri::async_runtime::spawn(async move { - let info = history::store_launch_history(&h).await; + let info = history::get_or_upsert_launch_info(&h); debug!("Launched Yaak {:?}", info); }); diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs index 4a52e4dc..766a819a 100644 --- a/src-tauri/src/notifications.rs +++ b/src-tauri/src/notifications.rs @@ -1,7 +1,7 @@ use std::time::SystemTime; use crate::error::Result; -use crate::history::get_num_launches; +use crate::history::get_or_upsert_launch_info; use chrono::{DateTime, Utc}; use log::debug; use reqwest::Method; @@ -79,7 +79,7 @@ impl YaakNotifier { #[cfg(feature = "license")] let license_check = { - use yaak_license::{LicenseCheckStatus, check_license}; + use yaak_license::{check_license, LicenseCheckStatus}; match check_license(window).await { Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(), Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(), @@ -91,17 +91,17 @@ impl YaakNotifier { #[cfg(not(feature = "license"))] let license_check = "disabled".to_string(); - let settings = window.db().get_settings(); - let num_launches = get_num_launches(app_handle).await; - let info = app_handle.package_info().clone(); + let launch_info = get_or_upsert_launch_info(app_handle); let req = yaak_api_client(app_handle)? .request(Method::GET, "https://notify.yaak.app/notifications") .query(&[ - ("version", info.version.to_string().as_str()), - ("launches", num_launches.to_string().as_str()), - ("installed", settings.created_at.format("%Y-%m-%d").to_string().as_str()), + ("version", &launch_info.current_version), + ("version_prev", &launch_info.previous_version), + ("launches", &launch_info.num_launches.to_string()), + ("installed", &launch_info.user_since.format("%Y-%m-%d").to_string()), ("license", &license_check), - ("platform", get_os()), + ("updates", &get_updater_status(app_handle).to_string()), + ("platform", &get_os().to_string()), ]); let resp = req.send().await?; if resp.status() != 200 { @@ -131,3 +131,32 @@ async fn get_kv(app_handle: &AppHandle) -> Result> { Some(v) => Ok(serde_json::from_str(&v.value)?), } } + +fn get_updater_status(app_handle: &AppHandle) -> &'static str { + #[cfg(not(feature = "updater"))] + { + // Updater is not enabled as a Rust feature + return "missing"; + } + + #[cfg(all(feature = "updater", target_os = "linux"))] + { + let settings = app_handle.db().get_settings(); + if !settings.autoupdate { + // Updates are explicitly disabled + "disabled" + } else if std::env::var("APPIMAGE").is_err() { + // Updates are enabled, but unsupported + "unsupported" + } else { + // Updates are enabled and supported + "enabled" + } + } + + #[cfg(all(feature = "updater", not(target_os = "linux")))] + { + let settings = app_handle.db().get_settings(); + if settings.autoupdate { "enabled" } else { "disabled" } + } +} diff --git a/src-tauri/yaak-license/src/license.rs b/src-tauri/yaak-license/src/license.rs index f71d6282..9f2ab606 100644 --- a/src-tauri/yaak-license/src/license.rs +++ b/src-tauri/yaak-license/src/license.rs @@ -88,7 +88,7 @@ pub async fn activate_license( } let body: ActivateLicenseResponsePayload = response.json().await?; - window.app_handle().db().set_key_value_string( + window.app_handle().db().set_key_value_str( KV_ACTIVATION_ID_KEY, KV_NAMESPACE, body.activation_id.as_str(), @@ -207,5 +207,5 @@ fn build_url(path: &str) -> String { } pub async fn get_activation_id(app_handle: &AppHandle) -> String { - app_handle.db().get_key_value_string(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "") + app_handle.db().get_key_value_str(KV_ACTIVATION_ID_KEY, KV_NAMESPACE, "") } diff --git a/src-tauri/yaak-models/src/queries/key_values.rs b/src-tauri/yaak-models/src/queries/key_values.rs index bfbb05c0..01063fde 100644 --- a/src-tauri/yaak-models/src/queries/key_values.rs +++ b/src-tauri/yaak-models/src/queries/key_values.rs @@ -1,3 +1,4 @@ +use chrono::NaiveDateTime; use crate::db_context::DbContext; use crate::error::Result; use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo}; @@ -22,7 +23,7 @@ impl<'a> DbContext<'a> { Ok(items.map(|v| v.unwrap()).collect()) } - pub fn get_key_value_string(&self, namespace: &str, key: &str, default: &str) -> String { + pub fn get_key_value_str(&self, namespace: &str, key: &str, default: &str) -> String { match self.get_key_value_raw(namespace, key) { None => default.to_string(), Some(v) => { @@ -38,6 +39,22 @@ impl<'a> DbContext<'a> { } } + pub fn get_key_value_dte(&self, namespace: &str, key: &str, default: NaiveDateTime) -> NaiveDateTime { + match self.get_key_value_raw(namespace, key) { + None => default, + Some(v) => { + let result = serde_json::from_str(&v.value); + match result { + Ok(v) => v, + Err(e) => { + error!("Failed to parse date key value: {}", e); + default + } + } + } + } + } + pub fn get_key_value_int(&self, namespace: &str, key: &str, default: i32) -> i32 { match self.get_key_value_raw(namespace, key) { None => default.clone(), @@ -67,7 +84,18 @@ impl<'a> DbContext<'a> { self.conn.resolve().query_row(sql.as_str(), &*params.as_params(), KeyValue::from_row).ok() } - pub fn set_key_value_string( + pub fn set_key_value_dte( + &self, + namespace: &str, + key: &str, + value: NaiveDateTime, + source: &UpdateSource, + ) -> (KeyValue, bool) { + let encoded = serde_json::to_string(&value).unwrap(); + self.set_key_value_raw(namespace, key, &encoded, source) + } + + pub fn set_key_value_str( &self, namespace: &str, key: &str,