Stop announcing the same email twice
Both notification paths feed one channel, which is how a single arrival came to be announced twice: Gmail names the sender, then the unread count says "1 new" behind it seconds later. A count is now skipped when the app has spoken for itself in the last twenty seconds - generous, because a count is only noticed on a four-second tick, well after the app raised its own. Settings can also turn count notifications off outright, for hearing only what an app says in its own words, at the cost of the tools that never say anything. `npm run ship` now updates only /Applications. The Desktop installer moved behind `--dmg`, for when a build is going to someone else.
This commit is contained in:
@@ -20,6 +20,9 @@ pub struct AppState {
|
||||
/// Title bar height: how far a child webview's origin sits above the
|
||||
/// content the shell measures from.
|
||||
pub chrome: Mutex<f64>,
|
||||
/// When each app last raised a notification in its own words, so a count
|
||||
/// does not immediately say the same thing again in worse words.
|
||||
pub last_spoke: Mutex<std::collections::HashMap<String, std::time::Instant>>,
|
||||
/// A login waiting on an answer: host, account, password. Held only until
|
||||
/// it is saved or declined, and never written anywhere but the Keychain.
|
||||
pub pending_password: Mutex<Option<(String, String, String, String)>>,
|
||||
@@ -80,6 +83,7 @@ pub fn build_state(handle: &AppHandle) -> Result<AppState, String> {
|
||||
dir,
|
||||
radius: Mutex::new(0.0),
|
||||
chrome: Mutex::new(0.0),
|
||||
last_spoke: Mutex::new(std::collections::HashMap::new()),
|
||||
pending_password: Mutex::new(None),
|
||||
config: Mutex::new(config),
|
||||
active: Mutex::new(None),
|
||||
@@ -466,6 +470,16 @@ pub fn set_nav_collapsed(collapsed: bool, state: State<'_, AppState>) -> Result<
|
||||
state.persist()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_count_notifications(
|
||||
enabled: bool,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Config, String> {
|
||||
state.config.lock().unwrap().settings.count_notifications = enabled;
|
||||
state.persist()?;
|
||||
Ok(state.cfg())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_theme(theme: String, app: AppHandle, state: State<'_, AppState>) -> Result<(), String> {
|
||||
state.config.lock().unwrap().settings.theme = theme.clone();
|
||||
|
||||
@@ -88,18 +88,29 @@ pub struct Settings {
|
||||
/// The app that was showing when the window last closed.
|
||||
#[serde(default)]
|
||||
pub last_app: Option<String>,
|
||||
/// Whether an app's unread count may raise a notification of its own.
|
||||
///
|
||||
/// On for the tools that never notify by themselves. Off if you would
|
||||
/// rather hear only what an app says in its own words.
|
||||
#[serde(default = "yes")]
|
||||
pub count_notifications: bool,
|
||||
}
|
||||
|
||||
fn default_theme() -> String {
|
||||
"system".into()
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nav_collapsed: false,
|
||||
theme: default_theme(),
|
||||
last_app: None,
|
||||
count_notifications: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ pub fn run() {
|
||||
commands::delete_group,
|
||||
commands::set_nav_collapsed,
|
||||
commands::set_theme,
|
||||
commands::set_count_notifications,
|
||||
commands::reset_config,
|
||||
commands::set_window_chrome,
|
||||
commands::chrome_height,
|
||||
|
||||
@@ -159,6 +159,16 @@ fn handle_sentinel(
|
||||
let body = params.get("b").cloned().unwrap_or_default();
|
||||
let app_name = params.get("a").cloned().unwrap_or_default();
|
||||
let notification_id = params.get("id").cloned().unwrap_or_default();
|
||||
|
||||
// Noted so a count does not repeat, seconds later and worse,
|
||||
// what the app has just said properly.
|
||||
handle
|
||||
.state::<crate::commands::AppState>()
|
||||
.last_spoke
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(from.clone(), std::time::Instant::now());
|
||||
|
||||
notify(&handle, &from, &app_name, &title, &body, notification_id);
|
||||
}
|
||||
|
||||
@@ -245,7 +255,10 @@ fn handle_sentinel(
|
||||
};
|
||||
let _ = handle.emit("unread-changed", crate::commands::unread_list(&state));
|
||||
|
||||
if total > previous && showing.as_deref() != Some(from.as_str()) {
|
||||
if total > previous
|
||||
&& showing.as_deref() != Some(from.as_str())
|
||||
&& count_may_speak(&handle, &from)
|
||||
{
|
||||
let name = state
|
||||
.cfg()
|
||||
.app(&from)
|
||||
@@ -299,6 +312,25 @@ fn handle_sentinel(
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether an unread count should raise a notification of its own.
|
||||
///
|
||||
/// Two things can suppress it. The setting, for someone who would rather hear
|
||||
/// only what an app says in its own words. And an app having just said it:
|
||||
/// Gmail raises a proper notification naming the sender, and the count arriving
|
||||
/// behind it saying "1 new" is the same news told worse. The window is generous
|
||||
/// because a count is noticed on a four-second tick, well after the app spoke.
|
||||
fn count_may_speak(handle: &AppHandle, app_id: &str) -> bool {
|
||||
let state = handle.state::<crate::commands::AppState>();
|
||||
if !state.cfg().settings.count_notifications {
|
||||
return false;
|
||||
}
|
||||
let spoke = state.last_spoke.lock().unwrap().get(app_id).copied();
|
||||
match spoke {
|
||||
Some(at) => at.elapsed() > std::time::Duration::from_secs(20),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Raises a macOS notification and waits, on its own thread, to see it clicked.
|
||||
///
|
||||
/// Not the notification plugin: that has no way to report a click, and a
|
||||
|
||||
Reference in New Issue
Block a user