From e9ed5dd023c8362a51c24ca1c3bd9da6c444e1fe Mon Sep 17 00:00:00 2001 From: Jason Tsai Date: Mon, 24 Mar 2025 21:01:36 +0800 Subject: [PATCH] feat(notification): add `EmbedderMsg::ShowNotification` (#36055) Signed-off-by: Jason Tsai --- components/constellation/tracing.rs | 1 + components/script/dom/notification.rs | 98 ++++++++++++++++++++++----- components/servo/lib.rs | 6 ++ components/servo/servo_delegate.rs | 5 ++ components/servo/webview_delegate.rs | 8 ++- components/shared/embedder/lib.rs | 53 +++++++++++++++ 6 files changed, 151 insertions(+), 20 deletions(-) diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 19bf740abb2..08e6fecc099 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -235,6 +235,7 @@ mod from_script { Self::PlayGamepadHapticEffect(..) => target_variant!("PlayGamepadHapticEffect"), Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"), Self::ShutdownComplete => target_variant!("ShutdownComplete"), + Self::ShowNotification(..) => target_variant!("ShowNotification"), } } } diff --git a/components/script/dom/notification.rs b/components/script/dom/notification.rs index 197a79e9bfb..0cdf0a74d69 100644 --- a/components/script/dom/notification.rs +++ b/components/script/dom/notification.rs @@ -9,6 +9,10 @@ use std::time::{SystemTime, UNIX_EPOCH}; use content_security_policy::Destination; use dom_struct::dom_struct; +use embedder_traits::{ + EmbedderMsg, Notification as EmbedderNotification, + NotificationAction as EmbedderNotificationAction, +}; use ipc_channel::ipc; use ipc_channel::router::ROUTER; use js::jsapi::Heap; @@ -253,23 +257,39 @@ impl Notification { /// fn show(&self) { - // TODO: step 3: set shown to false + // step 3: set shown to false + let shown = false; + // TODO: step 4: Let oldNotification be the notification in the list of notifications // whose tag is not the empty string and is notification’s tag, // and whose origin is same origin with notification’s origin, // if any, and null otherwise. + // TODO: step 5: If oldNotification is non-null, then: - // TODO: step 6: If shown is false, then: - // TODO: step 6.1: Append notification to the list of notifications. - // TODO: step 6.2: Display notification on the device - // TODO: Add EmbedderMsg::ShowNotification(...) event + // TODO: step 5.1: Handle close events with oldNotification. + // TODO: step 5.2: If the notification platform supports replacement, then: + // TODO: step 5.2.1: Replace oldNotification with notification, in the list of notifications. + // TODO: step 5.2.2: Set shown to true. + // TODO: step 5.3: Otherwise, remove oldNotification from the list of notifications. + + // step 6: If shown is false, then: + if !shown { + // TODO: step 6.1: Append notification to the list of notifications. + // step 6.2: Display notification on the device + self.global() + .send_to_embedder(EmbedderMsg::ShowNotification( + self.global().webview_id(), + self.to_embedder_notification(), + )); + } + // TODO: step 7: If shown is false or oldNotification is non-null, // and notification’s renotify preference is true, // then run the alert steps for notification. // step 8: If notification is a non-persistent notification, - // then queue a task to fire an event named show on - // the Notification object representing notification. + // then queue a task to fire an event named show on + // the Notification object representing notification. if self.serviceworker_registration.is_none() { self.global() .task_manager() @@ -277,6 +297,46 @@ impl Notification { .queue_simple_event(self.upcast(), atom!("show")); } } + + /// Create an [`embedder_traits::Notification`]. + fn to_embedder_notification(&self) -> EmbedderNotification { + EmbedderNotification { + title: self.title.to_string(), + body: self.body.to_string(), + tag: self.tag.to_string(), + language: self.lang.to_string(), + require_interaction: self.require_interaction, + silent: self.silent, + icon_url: self + .icon + .as_ref() + .and_then(|icon| ServoUrl::parse(icon).ok()), + badge_url: self + .badge + .as_ref() + .and_then(|badge| ServoUrl::parse(badge).ok()), + image_url: self + .image + .as_ref() + .and_then(|image| ServoUrl::parse(image).ok()), + actions: self + .actions + .iter() + .map(|action| EmbedderNotificationAction { + name: action.name.to_string(), + title: action.title.to_string(), + icon_url: action + .icon_url + .as_ref() + .and_then(|icon| ServoUrl::parse(icon).ok()), + icon_resource: action.icon_resource.borrow().clone(), + }) + .collect(), + icon_resource: self.icon_resource.borrow().clone(), + badge_resource: self.badge_resource.borrow().clone(), + image_resource: self.image_resource.borrow().clone(), + } + } } impl NotificationMethods for Notification { @@ -316,12 +376,12 @@ impl NotificationMethods for Notification { .dom_manipulation_task_source() .queue_simple_event(notification.upcast(), atom!("error")); // TODO: abort steps + } else { + // step 5.2: Run the notification show steps for notification + // + // step 1: Run the fetch steps for notification. + notification.fetch_resources_and_show_when_ready(); } - // TODO: step 5.2: Run the notification show steps for notification - // - // step 1: Run the fetch steps for notification. - // following steps are processed in show_steps after all resources are fetched - notification.fetch_resources_and_show_when_ready(); Ok(notification) } @@ -478,10 +538,12 @@ impl NotificationMethods for Notification { // If notification is a non-persistent notification // then queue a task to fire an event named close on the Notification object representing notification. - self.global() - .task_manager() - .dom_manipulation_task_source() - .queue_simple_event(self.upcast(), atom!("close")); + if self.serviceworker_registration.is_none() { + self.global() + .task_manager() + .dom_manipulation_task_source() + .queue_simple_event(self.upcast(), atom!("close")); + } } } @@ -754,7 +816,7 @@ impl Notification { None, url.clone(), Destination::Image, - None, // TODO: check CORS + None, // TODO: check which CORS should be used None, global.get_referrer(), global.insecure_requests_policy(), @@ -809,7 +871,7 @@ impl Notification { let cache_result = global.image_cache().get_cached_image_status( request.url.clone(), global.origin().immutable().clone(), - None, // TODO: check CORS + None, // TODO: check which CORS should be used UsePlaceholder::No, ); match cache_result { diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 72500f83c83..25d12703700 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -985,6 +985,12 @@ impl Servo { ); } }, + EmbedderMsg::ShowNotification(webview_id, notification) => { + match webview_id.and_then(|webview_id| self.get_webview_handle(webview_id)) { + Some(webview) => webview.delegate().show_notification(webview, notification), + None => self.delegate().show_notification(notification), + } + }, } } } diff --git a/components/servo/servo_delegate.rs b/components/servo/servo_delegate.rs index 34d5c50bda5..8ec53a4790b 100644 --- a/components/servo/servo_delegate.rs +++ b/components/servo/servo_delegate.rs @@ -2,6 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use embedder_traits::Notification; + use crate::Servo; use crate::webview_delegate::{AllowOrDenyRequest, WebResourceLoad}; @@ -34,6 +36,9 @@ pub trait ServoDelegate { /// [`WebView`]. For loads associated with a [`WebView`], Servo will call /// [`crate::WebViewDelegate::load_web_resource`]. fn load_web_resource(&self, _load: WebResourceLoad) {} + + /// Request to display a notification. + fn show_notification(&self, _notification: Notification) {} } pub(crate) struct DefaultServoDelegate; diff --git a/components/servo/webview_delegate.rs b/components/servo/webview_delegate.rs index b297b7f925d..9ea75e32af6 100644 --- a/components/servo/webview_delegate.rs +++ b/components/servo/webview_delegate.rs @@ -8,8 +8,9 @@ use base::id::PipelineId; use constellation_traits::ConstellationMsg; use embedder_traits::{ AllowOrDeny, AuthenticationResponse, ContextMenuResult, Cursor, FilterPattern, - GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, PermissionFeature, - SimpleDialog, WebResourceRequest, WebResourceResponse, WebResourceResponseMsg, + GamepadHapticEffectType, InputMethodType, LoadStatus, MediaSessionEvent, Notification, + PermissionFeature, SimpleDialog, WebResourceRequest, WebResourceResponse, + WebResourceResponseMsg, }; use ipc_channel::ipc::IpcSender; use keyboard_types::KeyboardEvent; @@ -462,6 +463,9 @@ pub trait WebViewDelegate { /// For loads not associated with a [`WebView`], such as those for service workers, Servo /// will call [`crate::ServoDelegate::load_web_resource`]. fn load_web_resource(&self, _webview: WebView, _load: WebResourceLoad) {} + + /// Request to display a notification. + fn show_notification(&self, _webview: WebView, _notification: Notification) {} } pub(crate) struct DefaultWebViewDelegate; diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index d2183730df2..638f17edc1c 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -14,6 +14,7 @@ mod webdriver; use std::fmt::{Debug, Error, Formatter}; use std::path::PathBuf; +use std::sync::Arc; use base::id::{PipelineId, WebViewId}; use crossbeam_channel::Sender; @@ -23,6 +24,7 @@ pub use keyboard_types::{KeyboardEvent, Modifiers}; use log::warn; use malloc_size_of_derive::MallocSizeOf; use num_derive::FromPrimitive; +use pixels::Image; use serde::{Deserialize, Serialize}; use servo_url::ServoUrl; use strum_macros::IntoStaticStr; @@ -326,6 +328,8 @@ pub enum EmbedderMsg { /// Required because the constellation can have pending calls to make /// (e.g. SetFrameTree) at the time that we send it an ExitMsg. ShutdownComplete, + /// Request to display a notification. + ShowNotification(Option, Notification), } impl Debug for EmbedderMsg { @@ -582,3 +586,52 @@ pub enum LoadStatus { /// See Complete, } + +/// Data that could be used to display a desktop notification to the end user +/// when the [Notification API]() is called. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Notification { + /// Title of the notification. + pub title: String, + /// Body string of the notification. + pub body: String, + /// An identifier tag for the notification. Notification with the same tag + /// can be replaced by another to avoid users' screen being filled up with similar notifications. + pub tag: String, + /// The tag for the language used in the notification's title, body, and the title of each its actions. [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646) + pub language: String, + /// A boolean value indicates the notification should remain readily available + /// until the end user activates or dismisses the notification. + pub require_interaction: bool, + /// When `true`, indicates no sounds or vibrations should be made. When `None`, + /// the device's default settings should be respected. + pub silent: Option, + /// The URL of an icon. The icon will be displayed as part of the notification. + pub icon_url: Option, + /// Icon's raw image data and metadata. + pub icon_resource: Option>, + /// The URL of a badge. The badge is used when there is no enough space to display the notification, + /// such as on a mobile device's notification bar. + pub badge_url: Option, + /// Badge's raw image data and metadata. + pub badge_resource: Option>, + /// The URL of an image. The image will be displayed as part of the notification. + pub image_url: Option, + /// Image's raw image data and metadata. + pub image_resource: Option>, + /// Actions available for users to choose from for interacting with the notification. + pub actions: Vec, +} + +/// Actions available for users to choose from for interacting with the notification. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct NotificationAction { + /// A string that identifies the action. + pub name: String, + /// The title string of the action to be shown to the user. + pub title: String, + /// The URL of an icon. The icon will be displayed with the action. + pub icon_url: Option, + /// Icon's raw image data and metadata. + pub icon_resource: Option>, +}