Empty-cache menu item; catch two more ways an app can notify
Right-click now offers "Empty cache and reload". Caches only - never cookies or local storage: emptying a cache is what you do when a page is serving something stale, and signing you out while doing it would be a different and much less welcome feature. The reload waits for the removal to finish, or it would refill from what was being thrown away. Google Chat could play its own alert sound and still reach none of this, because some apps never construct a Notification - they hand it to their service worker registration instead. ServiceWorkerRegistration.prototype .showNotification now routes to the same place. A worker calling it from its own context is still out of reach; a page calling it is not. Also shims the badge API, which is the precise form of an unread count - a number an app states outright rather than one scraped from its title. WKWebView does not implement it, so an app calling it was talking to nobody. Chat's title carries no count at all, which is why no counter ever appeared for it. The background-apps probe now reports all three signals per app, so which one an app actually uses is a reading rather than a guess.
This commit is contained in:
@@ -25,9 +25,9 @@ uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.6"
|
||||
objc2-web-kit = { version = "0.3.2", features = ["WKWebView", "WKWebViewConfiguration", "WKPreferences", "WKSnapshotConfiguration", "block2"] }
|
||||
objc2-web-kit = { version = "0.3.2", features = ["WKWebView", "WKWebViewConfiguration", "WKPreferences", "WKSnapshotConfiguration", "WKWebsiteDataStore", "WKWebsiteDataRecord", "block2"] }
|
||||
objc2-app-kit = { version = "0.3", features = ["NSImage", "NSBitmapImageRep", "NSImageRep", "NSGraphics"] }
|
||||
objc2-foundation = { version = "0.3", features = ["NSData", "NSString", "NSDictionary", "NSValue", "NSError", "NSGeometry"] }
|
||||
objc2-foundation = { version = "0.3", features = ["NSData", "NSString", "NSDictionary", "NSValue", "NSError", "NSGeometry", "NSSet", "NSDate", "NSArray"] }
|
||||
block2 = "0.6"
|
||||
# Notifications are raised here rather than through the plugin, which offers no
|
||||
# way to learn that one was clicked.
|
||||
|
||||
@@ -732,10 +732,13 @@ pub fn probe_apps(app: AppHandle, state: State<'_, AppState>) -> Result<(), Stri
|
||||
if (!window.__workAppSend) return;
|
||||
var st = window.__workAppState ? window.__workAppState() : {};
|
||||
window.__workAppSend('diag', {
|
||||
title: String(document.title || '').slice(0, 40),
|
||||
title: String(document.title || '').slice(0, 32),
|
||||
active: String(st.active),
|
||||
seen: String(st.seen),
|
||||
queued: String(st.queued) + '/' + String(st.sending)
|
||||
// The three ways an app can say something arrived.
|
||||
notified: String(st.raised),
|
||||
badged: String(st.badged),
|
||||
sw: typeof (window.ServiceWorkerRegistration || {}).prototype
|
||||
});
|
||||
})();"#,
|
||||
);
|
||||
|
||||
+38
-2
@@ -57,7 +57,10 @@
|
||||
/* Readable from the probe, so "it never fired" can be told apart from
|
||||
"it fired and the message never left". */
|
||||
window.__workAppState = function () {
|
||||
return { active: active, queued: queue.length, sending: sending, seen: lastCount };
|
||||
return {
|
||||
active: active, queued: queue.length, sending: sending,
|
||||
seen: lastCount, raised: raised, badged: badged
|
||||
};
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------ routing */
|
||||
@@ -311,7 +314,8 @@
|
||||
[
|
||||
['Hide this element', function () { hide(target); }],
|
||||
['Pick an element to hide…', startPicking],
|
||||
['Manage hidden elements…', function () { send('manage', { x: '1' }); }]
|
||||
['Manage hidden elements…', function () { send('manage', { x: '1' }); }],
|
||||
['Empty cache and reload', function () { send('emptycache', { x: '1' }); }]
|
||||
].forEach(function (item) {
|
||||
var b = document.createElement('div');
|
||||
b.textContent = item[0];
|
||||
@@ -354,6 +358,8 @@
|
||||
notification was about — this app does not, and should not have to. */
|
||||
var notifications = {};
|
||||
var notifySeq = 0;
|
||||
var raised = 0;
|
||||
var badged = 0;
|
||||
|
||||
function WorkNotification(title, options) {
|
||||
options = options || {};
|
||||
@@ -445,6 +451,36 @@
|
||||
});
|
||||
} catch (e) { window.Notification = WorkNotification; }
|
||||
|
||||
/* Some apps do not construct a Notification at all — they hand it to their
|
||||
service worker registration instead. Google Chat is one, which is why it
|
||||
could play its own alert sound and still never reach any of this. Routed
|
||||
to the same place; a worker calling it from its own context is still out
|
||||
of reach, but a page calling it is not. */
|
||||
try {
|
||||
var reg = window.ServiceWorkerRegistration;
|
||||
if (reg && reg.prototype && reg.prototype.showNotification) {
|
||||
reg.prototype.showNotification = function (title, options) {
|
||||
try { new WorkNotification(title, options); } catch (e) {}
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
/* The badge API is the precise version of the unread count: a number the app
|
||||
states outright rather than one scraped from its title. WKWebView does not
|
||||
implement it, so an app calling it is talking to nobody. */
|
||||
try {
|
||||
navigator.setAppBadge = function (n) {
|
||||
badged += 1;
|
||||
send('badge', { n: String(n == null ? 0 : n) });
|
||||
return Promise.resolve();
|
||||
};
|
||||
navigator.clearAppBadge = function () {
|
||||
send('badge', { n: '0' });
|
||||
return Promise.resolve();
|
||||
};
|
||||
} catch (e) {}
|
||||
|
||||
/* ---------------------------------------------------------- the icon */
|
||||
|
||||
/* The site's own icon, read off the page it is on.
|
||||
|
||||
@@ -215,6 +215,42 @@ fn handle_sentinel(
|
||||
}
|
||||
}
|
||||
|
||||
// An app stating its own unread count outright. Absolute, unlike
|
||||
// the title watcher's deltas, so it replaces rather than adds.
|
||||
"badge" => {
|
||||
let Some(total) = params.get("n").and_then(|n| n.parse::<u32>().ok()) else {
|
||||
return;
|
||||
};
|
||||
let state = handle.state::<crate::commands::AppState>();
|
||||
let showing = state.active.lock().unwrap().clone();
|
||||
let previous = {
|
||||
let mut counts = state.unread.lock().unwrap();
|
||||
let previous = counts.get(&from).copied().unwrap_or(0);
|
||||
if total == 0 {
|
||||
counts.remove(&from);
|
||||
} else {
|
||||
counts.insert(from.clone(), total);
|
||||
}
|
||||
previous
|
||||
};
|
||||
let _ = handle.emit("unread-changed", crate::commands::unread_list(&state));
|
||||
|
||||
if total > previous && showing.as_deref() != Some(from.as_str()) {
|
||||
let name = state
|
||||
.cfg()
|
||||
.app(&from)
|
||||
.map(|a| a.name.clone())
|
||||
.unwrap_or_else(|| "Work".into());
|
||||
let n = total - previous;
|
||||
let title = if n == 1 { "1 new".to_string() } else { format!("{n} new") };
|
||||
notify(&handle, &from, &name, &title, &format!("{total} unread"), String::new());
|
||||
}
|
||||
}
|
||||
|
||||
"emptycache" => {
|
||||
empty_cache(&handle, &from);
|
||||
}
|
||||
|
||||
"manage" => {
|
||||
let _ = handle.emit("manage-hidden", from.clone());
|
||||
}
|
||||
@@ -486,6 +522,52 @@ pub fn set_corner_radius(handle: &AppHandle, app_id: &str, radius: f64) {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn set_corner_radius(_: &AppHandle, _: &str, _: f64) {}
|
||||
|
||||
/// Throws away this app's caches and reloads it.
|
||||
///
|
||||
/// Caches only — never cookies or local storage. Emptying a browser's cache is
|
||||
/// a thing you do when a page is serving something stale; signing you out of
|
||||
/// the tool while you do it would be a different and much less welcome feature.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn empty_cache(handle: &AppHandle, app_id: &str) {
|
||||
use block2::RcBlock;
|
||||
use objc2_foundation::{MainThreadMarker, NSDate, NSSet, NSString};
|
||||
use objc2_web_kit::WKWebsiteDataStore;
|
||||
|
||||
let Some(wv) = handle.get_webview(&label_for(app_id)) else { return };
|
||||
let handle = handle.clone();
|
||||
let app_id = app_id.to_string();
|
||||
|
||||
let _ = wv.with_webview(move |_platform| unsafe {
|
||||
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||
|
||||
let types = NSSet::from_retained_slice(&[
|
||||
NSString::from_str("WKWebsiteDataTypeDiskCache"),
|
||||
NSString::from_str("WKWebsiteDataTypeMemoryCache"),
|
||||
NSString::from_str("WKWebsiteDataTypeOfflineWebApplicationCache"),
|
||||
NSString::from_str("WKWebsiteDataTypeFetchCache"),
|
||||
NSString::from_str("WKWebsiteDataTypeServiceWorkerRegistrations"),
|
||||
]);
|
||||
|
||||
let done = RcBlock::new(move || {
|
||||
// Reloaded only once the cache is actually gone, or the reload
|
||||
// would refill it from what we were trying to throw away.
|
||||
if let Some(wv) = handle.get_webview(&label_for(&app_id)) {
|
||||
let _ = wv.eval("location.reload(true)");
|
||||
}
|
||||
});
|
||||
|
||||
WKWebsiteDataStore::defaultDataStore(mtm)
|
||||
.removeDataOfTypes_modifiedSince_completionHandler(
|
||||
&types,
|
||||
&NSDate::distantPast(),
|
||||
&done,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn empty_cache(_: &AppHandle, _: &str) {}
|
||||
|
||||
/// Turns on WKWebView's two-finger back and forward swipes.
|
||||
///
|
||||
/// wry supports it but Tauri does not expose it, so it is set on the native
|
||||
|
||||
Reference in New Issue
Block a user