Repository navigation
feat(notification): report actions on desktop - #3671
lucasfernog merged 6 commits into
Conversation
A click on a notification (tap) or on one of the actions of its action type now reaches the JavaScript onAction listeners on desktop, and the new Rust Notification::on_action on every platform. On Linux and the BSDs the click comes from the notification server's ActionInvoked signal; on Windows and macOS from notify-rust's wait_for_response, which needs notify-rust 4.18. register_action_types, remove_active (Linux and the BSDs) and the listener commands are implemented for desktop, so they no longer fail with "command not found". The api example sends a notification with two actions and shows the action it hears. Closes #2150, closes #1903.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Package Changes Through e0a1becThere are 2 changes which include notification with minor, notification-js with minor Planned Package VersionsThe following package releases are the planned based on the context of changes in this pull request.
Add another change file through the GitHub UI by following this link. Read about change files or the docs at github.com/jbolda/covector |
| */ | ||
| async function onAction( | ||
| cb: (notification: Options) => void | ||
| cb: (event: ActionPerformed) => void |
There was a problem hiding this comment.
For anyone wondering like me, no this is not a breaking change but a bugfix. This is the payload we actually sent on mobile all the time, not Options...
|
The windows e2e job is crashed. This seems like a flake. Could you please restart the job? |
|
there's a flake indeed in the windows plugins build.. |
| pub(super) const BUS: (&str, &str, &str) = ( | ||
| "org.freedesktop.Notifications", | ||
| "/org/freedesktop/Notifications", | ||
| "org.freedesktop.Notifications", | ||
| ); | ||
|
|
||
| /// Linux and the BSDs: shows the notification and records it, so the listener thread knows | ||
| /// which payload its signals carry. | ||
| #[cfg(all(unix, not(target_os = "macos")))] | ||
| fn track(notification: notify_rust::Notification, shared: &Arc<Shared>, id: i32, data: Value) { | ||
| let tracker = &shared.tracker; | ||
| let Some(connection) = tracker | ||
| .connection | ||
| .get_or_init(|| listen(Arc::downgrade(shared))) | ||
| else { | ||
| let _ = notification.show(); | ||
| return; | ||
| }; | ||
| let server_id = handle.id(); | ||
| lock(&shared.active).insert(id, Shown(handle)); | ||
| let _ = notify_rust::handle_action(server_id, |response| { | ||
| // A later notification with the same id may have taken the entry meanwhile. | ||
| let mut active = lock(&shared.active); | ||
| if active | ||
| .get(&id) | ||
| .is_some_and(|shown| shown.0.id() == server_id) | ||
| { | ||
| active.remove(&id); | ||
| // Held while the notification is sent, so a signal that arrives before the server id is | ||
| // recorded waits for it instead of being missed. | ||
| let mut shown = lock(&tracker.shown); | ||
| if let Ok(server_id) = notify(connection, ¬ification) { | ||
| shown.insert(server_id, (id, data)); | ||
| } | ||
| } | ||
|
|
||
| /// Linux and the BSDs: sends the notification on the connection the signals arrive on, since | ||
| /// some servers, like dunst, send the signals of a notification only to the connection that | ||
| /// sent it. Returns the server id of the notification. | ||
| #[cfg(all(unix, not(target_os = "macos")))] | ||
| fn notify( | ||
| connection: &zbus::blocking::Connection, | ||
| notification: ¬ify_rust::Notification, | ||
| ) -> zbus::Result<u32> { | ||
| // the only hint the plugin sets | ||
| let hints = notification | ||
| .hints | ||
| .iter() | ||
| .filter_map(|hint| match hint { | ||
| notify_rust::Hint::SoundName(name) => { | ||
| Some(("sound-name", zbus::zvariant::Value::from(name.as_str()))) | ||
| } | ||
| _ => None, | ||
| }) | ||
| .collect::<HashMap<_, _>>(); | ||
| let (destination, path, interface) = BUS; | ||
| connection | ||
| .call_method( | ||
| Some(destination), | ||
| path, | ||
| Some(interface), | ||
| "Notify", | ||
| &( | ||
| ¬ification.appname, | ||
| 0u32, | ||
| ¬ification.icon, | ||
| ¬ification.summary, | ||
| ¬ification.body, | ||
| ¬ification.actions, | ||
| hints, | ||
| -1i32, | ||
| ), | ||
| )? | ||
| .body() | ||
| .deserialize() | ||
| } | ||
|
|
||
| /// Linux and the BSDs: connects to the session bus and subscribes to the signals of the | ||
| /// notification server, then handles them on a thread of its own until the plugin goes away. | ||
| /// The subscription is in place when this returns, so no signal of a later notification is | ||
| /// missed. | ||
| #[cfg(all(unix, not(target_os = "macos")))] | ||
| fn listen(shared: Weak<Shared>) -> Option<zbus::blocking::Connection> { | ||
| let (_, path, interface) = BUS; | ||
| let connection = zbus::blocking::Connection::session().ok()?; | ||
| let rule = zbus::MatchRule::builder() | ||
| .msg_type(zbus::message::Type::Signal) | ||
| .path(path) | ||
| .ok()? | ||
| .interface(interface) | ||
| .ok()? | ||
| .build(); | ||
| let signals = | ||
| zbus::blocking::MessageIterator::for_match_rule(rule, &connection, None).ok()?; | ||
| std::thread::Builder::new() | ||
| .name("notification-actions".into()) | ||
| .spawn(move || { | ||
| for signal in signals.flatten() { | ||
| let Some(shared) = shared.upgrade() else { | ||
| return; | ||
| }; | ||
| on_signal(&shared, &signal); | ||
| } | ||
| }) | ||
| .ok()?; | ||
| Some(connection) | ||
| } | ||
|
|
||
| /// Linux and the BSDs: reports an `ActionInvoked` signal of a tracked notification and forgets | ||
| /// the notification on `NotificationClosed`. Signals of other notifications are ignored. | ||
| #[cfg(all(unix, not(target_os = "macos")))] | ||
| pub(super) fn on_signal(shared: &Shared, signal: &zbus::message::Message) { | ||
| let header = signal.header(); | ||
| let body = signal.body(); | ||
| let (server_id, action) = match header.member().map(|member| member.as_str()) { | ||
| Some("ActionInvoked") => match body.deserialize::<(u32, String)>() { | ||
| Ok((server_id, action)) => (server_id, action), | ||
| Err(_) => return, | ||
| }, | ||
| Some("NotificationClosed") => { | ||
| if let Ok((server_id, _reason)) = body.deserialize::<(u32, u32)>() { | ||
| lock(&shared.tracker.shown).remove(&server_id); | ||
| } | ||
| return; | ||
| } | ||
| drop(active); | ||
| let action = match response { | ||
| notify_rust::ActionResponse::Custom("default") => "tap", | ||
| notify_rust::ActionResponse::Custom(action) => action, | ||
| notify_rust::ActionResponse::Closed(_) => return, | ||
| }; | ||
| _ => return, | ||
| }; | ||
| // The entry stays until the server closes the notification, which it may keep showing. | ||
| let data = lock(&shared.tracker.shown) | ||
| .get(&server_id) | ||
| .map(|(_, data)| data.clone()); | ||
| if let Some(data) = data { | ||
| let action = if action == "default" { "tap" } else { &action }; | ||
| shared.dispatch(performed(action, &data)); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /// Linux and the BSDs: closes the tracked notifications with the given identifiers, or all of | ||
| /// them. They are forgotten when the server reports them closed. | ||
| #[cfg(all(unix, not(target_os = "macos")))] | ||
| pub(super) fn close(shared: &Shared, ids: Option<&[i32]>) { | ||
| let Some(Some(connection)) = shared.tracker.connection.get() else { | ||
| return; | ||
| }; | ||
| let server_ids = lock(&shared.tracker.shown) | ||
| .iter() | ||
| .filter(|(_, (id, _))| ids.is_none_or(|ids| ids.contains(id))) | ||
| .map(|(server_id, _)| *server_id) | ||
| .collect::<Vec<_>>(); | ||
| let (destination, path, interface) = BUS; | ||
| for server_id in server_ids { | ||
| let _ = connection.call_method( | ||
| Some(destination), | ||
| path, | ||
| Some(interface), | ||
| "CloseNotification", | ||
| &(server_id,), | ||
| ); | ||
| } |
There was a problem hiding this comment.
what's going on here? do i understand it correctly that notify-rust isn't working on linux here? shouldn't this be an upstream change then?
Closes #2150
Closes #1903
Problem
On desktop, a click on a notification never reaches the app:
onActionis documented as mobile only, andregisterActionTypes,removeActiveand the listener commands fail withCommand ... not found(#1903) because onlynotify,request_permissionandis_permission_grantedare registered there.Change
onActionon desktop. A click on the notification arrives asactionId: "tap". A click on one of the buttons of its action type arrives as that action'sid. The payload is the same as on mobile ({ actionId, inputValue: null, notification }).ActionInvokedsignal (notify_rust::handle_action).NotificationHandle::wait_for_response. This is whynotify-rustgoes from 4.11 to 4.18.registerActionTypeson desktop. The actions of a type become the buttons of the notifications that reference it viaactionTypeId. Onlyidandtitleare used.removeActiveon desktop. It closes the notification on Linux and the BSDs. It does nothing on Windows and macOS, where notify-rust cannot remove a shown notification. This is documented.Notification::on_action(handler)on every platform (desktop and mobile), so the Rust side can react to clicks without a webview listener.apiexample sends a notification with two actions and shows the action it receives.How to test
examples/api, Notifications tab, "Send notification with actions":Action "tap" on notification <id>.CI of the fork (all 157 checks, Linux, Windows, macOS, Android and iOS) is green: https://github.com/haexmas/plugins-workspace/pull/2