diff --git a/.changes/notification-mobile-actions.md b/.changes/notification-mobile-actions.md new file mode 100644 index 000000000..1d43f134c --- /dev/null +++ b/.changes/notification-mobile-actions.md @@ -0,0 +1,7 @@ +--- +notification: minor +notification-js: minor +--- + +- Add Rust builders for mobile notification action types. +- Fix Android action-group entries being stored under the action type ID instead of sequential indices, causing later actions to overwrite earlier ones. diff --git a/plugins/notification/android/src/main/java/NotificationStorage.kt b/plugins/notification/android/src/main/java/NotificationStorage.kt index bceb985db..d4cb96918 100644 --- a/plugins/notification/android/src/main/java/NotificationStorage.kt +++ b/plugins/notification/android/src/main/java/NotificationStorage.kt @@ -80,11 +80,11 @@ class NotificationStorage(private val context: Context, private val jsonMapper: fun writeActionGroup(actions: List) { for (type in actions) { - val i = type.id val editor = getStorage(ACTION_TYPES_ID + type.id).edit() editor.clear() editor.putInt("count", type.actions.size) - for (action in type.actions) { + for (i in 0 until type.actions.size) { + val action = type.actions[i] editor.putString("id$i", action.id) editor.putString("title$i", action.title) editor.putBoolean("input$i", action.input ?: false) diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs index 02134e4db..e93b1e078 100644 --- a/plugins/notification/src/models.rs +++ b/plugins/notification/src/models.rs @@ -320,6 +320,95 @@ pub struct ActionType { hidden_previews_show_subtitle: bool, } +#[cfg(mobile)] +#[derive(Debug)] +pub struct ActionTypeBuilder(ActionType); + +#[cfg(mobile)] +impl ActionType { + pub fn builder(id: impl Into) -> ActionTypeBuilder { + ActionTypeBuilder(Self { + id: id.into(), + actions: Vec::new(), + hidden_previews_body_placeholder: None, + custom_dismiss_action: false, + allow_in_car_play: false, + hidden_previews_show_title: false, + hidden_previews_show_subtitle: false, + }) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn actions(&self) -> &[Action] { + &self.actions + } + + pub fn hidden_previews_body_placeholder(&self) -> Option<&str> { + self.hidden_previews_body_placeholder.as_deref() + } + + pub fn custom_dismiss_action(&self) -> bool { + self.custom_dismiss_action + } + + pub fn allow_in_car_play(&self) -> bool { + self.allow_in_car_play + } + + pub fn hidden_previews_show_title(&self) -> bool { + self.hidden_previews_show_title + } + + pub fn hidden_previews_show_subtitle(&self) -> bool { + self.hidden_previews_show_subtitle + } +} + +#[cfg(mobile)] +impl ActionTypeBuilder { + pub fn actions(mut self, actions: Vec) -> Self { + self.0.actions = actions; + self + } + + pub fn hidden_previews_body_placeholder( + mut self, + hidden_previews_body_placeholder: impl Into, + ) -> Self { + self.0 + .hidden_previews_body_placeholder + .replace(hidden_previews_body_placeholder.into()); + self + } + + pub fn custom_dismiss_action(mut self, custom_dismiss_action: bool) -> Self { + self.0.custom_dismiss_action = custom_dismiss_action; + self + } + + pub fn allow_in_car_play(mut self, allow_in_car_play: bool) -> Self { + self.0.allow_in_car_play = allow_in_car_play; + self + } + + pub fn hidden_previews_show_title(mut self, hidden_previews_show_title: bool) -> Self { + self.0.hidden_previews_show_title = hidden_previews_show_title; + self + } + + pub fn hidden_previews_show_subtitle(mut self, hidden_previews_show_subtitle: bool) -> Self { + self.0.hidden_previews_show_subtitle = hidden_previews_show_subtitle; + self + } + + pub fn build(self) -> ActionType { + self.0 + } +} + #[cfg(mobile)] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -334,6 +423,95 @@ pub struct Action { input_placeholder: Option, } +#[cfg(mobile)] +#[derive(Debug)] +pub struct ActionBuilder(Action); + +#[cfg(mobile)] +impl Action { + pub fn builder(id: impl Into, title: impl Into) -> ActionBuilder { + ActionBuilder(Self { + id: id.into(), + title: title.into(), + requires_authentication: false, + foreground: false, + destructive: false, + input: false, + input_button_title: None, + input_placeholder: None, + }) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn requires_authentication(&self) -> bool { + self.requires_authentication + } + + pub fn foreground(&self) -> bool { + self.foreground + } + + pub fn destructive(&self) -> bool { + self.destructive + } + + pub fn input(&self) -> bool { + self.input + } + + pub fn input_button_title(&self) -> Option<&str> { + self.input_button_title.as_deref() + } + + pub fn input_placeholder(&self) -> Option<&str> { + self.input_placeholder.as_deref() + } +} + +#[cfg(mobile)] +impl ActionBuilder { + pub fn requires_authentication(mut self, requires_authentication: bool) -> Self { + self.0.requires_authentication = requires_authentication; + self + } + + pub fn foreground(mut self, foreground: bool) -> Self { + self.0.foreground = foreground; + self + } + + pub fn destructive(mut self, destructive: bool) -> Self { + self.0.destructive = destructive; + self + } + + pub fn input(mut self, input: bool) -> Self { + self.0.input = input; + self + } + + pub fn input_button_title(mut self, input_button_title: impl Into) -> Self { + self.0.input_button_title.replace(input_button_title.into()); + self + } + + pub fn input_placeholder(mut self, input_placeholder: impl Into) -> Self { + self.0.input_placeholder.replace(input_placeholder.into()); + self + } + + pub fn build(self) -> Action { + self.0 + } +} + #[cfg(target_os = "android")] pub use android::*;