From 8b4b44720eced6eae0fe8aa69feb845d49161d28 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 29 Apr 2023 15:48:13 -0300 Subject: [PATCH] impl rust types --- Cargo.lock | 2 + plugins/notification/Cargo.toml | 2 + plugins/notification/src/lib.rs | 299 +++++++++++++++++++++++++++++++- 3 files changed, 298 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 840d45f6..707ab57f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4933,6 +4933,8 @@ dependencies = [ "tauri", "tauri-build", "thiserror", + "time 0.3.20", + "url", "win7-notifications", ] diff --git a/plugins/notification/Cargo.toml b/plugins/notification/Cargo.toml index cbdb1830..8db456c0 100644 --- a/plugins/notification/Cargo.toml +++ b/plugins/notification/Cargo.toml @@ -17,6 +17,8 @@ tauri.workspace = true log.workspace = true thiserror.workspace = true rand = "0.8" +time = { version = "0.3", features = ["serde", "parsing", "formatting"] } +url = { version = "2", features = ["serde"] } [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] notify-rust = "4.5" diff --git a/plugins/notification/src/lib.rs b/plugins/notification/src/lib.rs index 228578dc..544c83fe 100644 --- a/plugins/notification/src/lib.rs +++ b/plugins/notification/src/lib.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Display}; + +use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer}; #[cfg(mobile)] use tauri::plugin::PluginHandle; #[cfg(desktop)] @@ -29,24 +31,194 @@ pub use error::{Error, Result}; use desktop::Notification; #[cfg(mobile)] use mobile::Notification; +use url::Url; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Attachment { + id: String, + url: Url, +} + +impl Attachment { + pub fn new(id: impl Into, url: Url) -> Self { + Self { id: id.into(), url } + } +} #[derive(Debug, Default, Serialize, Deserialize)] +pub struct ScheduleInterval { + pub year: Option, + pub month: Option, + pub day: Option, + pub weekday: Option, + pub hour: Option, + pub minute: Option, + pub second: Option, +} + +#[derive(Debug)] +pub enum ScheduleEvery { + Year, + Month, + TwoWeeks, + Week, + Day, + Hour, + Minute, + Second, +} + +impl Display for ScheduleEvery { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Year => "Year", + Self::Month => "Month", + Self::TwoWeeks => "TwoWeeks", + Self::Week => "Week", + Self::Day => "Day", + Self::Hour => "Hour", + Self::Minute => "Minute", + Self::Second => "Second", + } + ) + } +} + +impl Serialize for ScheduleEvery { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +impl<'de> Deserialize<'de> for ScheduleEvery { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.to_lowercase().as_str() { + "year" => Ok(Self::Year), + "month" => Ok(Self::Month), + "twoweeks" => Ok(Self::TwoWeeks), + "week" => Ok(Self::Week), + "day" => Ok(Self::Day), + "hour" => Ok(Self::Hour), + "minute" => Ok(Self::Minute), + "second" => Ok(Self::Second), + _ => Err(DeError::custom(format!("unknown every kind '{s}'"))), + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(tag = "kind", content = "data")] +pub enum Schedule { + At { + #[serde( + serialize_with = "iso8601::serialize", + deserialize_with = "time::serde::iso8601::deserialize" + )] + date: time::OffsetDateTime, + #[serde(default)] + repeating: bool, + }, + Interval(ScheduleInterval), + Every { + interval: ScheduleEvery, + }, +} + +// custom ISO-8601 serialization that does not use 6 digits for years. +mod iso8601 { + use serde::{ser::Error as _, Serialize, Serializer}; + use time::{ + format_description::well_known::iso8601::{Config, EncodedConfig}, + format_description::well_known::Iso8601, + OffsetDateTime, + }; + + const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode(); + + pub fn serialize( + datetime: &OffsetDateTime, + serializer: S, + ) -> Result { + datetime + .format(&Iso8601::) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] struct NotificationData { /// Notification id. #[serde(default = "default_id")] id: i32, - /// The notification title. + channel_id: Option, title: Option, - /// The notification body. body: Option, - /// The notification icon. + schedule: Option, + large_body: Option, + summary: Option, + action_type_id: Option, + group: Option, + #[serde(default)] + group_summary: bool, + sound: Option, + #[serde(default)] + inbox_lines: Vec, icon: Option, + large_icon: Option, + icon_color: Option, + #[serde(default)] + attachments: Vec, + #[serde(default)] + extra: HashMap, + #[serde(default)] + ongoing: bool, + #[serde(default)] + auto_cancel: bool, } fn default_id() -> i32 { rand::random() } +impl Default for NotificationData { + fn default() -> Self { + Self { + id: default_id(), + channel_id: None, + title: None, + body: None, + schedule: None, + large_body: None, + summary: None, + action_type_id: None, + group: None, + group_summary: false, + sound: None, + inbox_lines: Vec::new(), + icon: None, + large_icon: None, + icon_color: None, + attachments: Vec::new(), + extra: Default::default(), + ongoing: false, + auto_cancel: false, + } + } +} + /// The notification builder. #[derive(Debug)] pub struct NotificationBuilder { @@ -74,6 +246,21 @@ impl NotificationBuilder { } } + /// Sets the notification identifier. + pub fn id(mut self, id: i32) -> Self { + self.data.id = id; + self + } + + /// Identifier of the {@link Channel} that deliveres this notification. + /// + /// If the channel does not exist, the notification won't fire. + /// Make sure the channel exists with {@link listChannels} and {@link createChannel}. + pub fn channel_id(mut self, id: impl Into) -> Self { + self.data.channel_id.replace(id.into()); + self + } + /// Sets the notification title. pub fn title(mut self, title: impl Into) -> Self { self.data.title.replace(title.into()); @@ -86,11 +273,113 @@ impl NotificationBuilder { self } - /// Sets the notification icon. + /// Schedule this notification to fire on a later time or a fixed interval. + pub fn schedule(mut self, schedule: Schedule) -> Self { + self.data.schedule.replace(schedule); + self + } + + /// Multiline text. + /// Changes the notification style to big text. + /// Cannot be used with `inboxLines`. + pub fn large_body(mut self, large_body: impl Into) -> Self { + self.data.large_body.replace(large_body.into()); + self + } + + /// Detail text for the notification with `largeBody`, `inboxLines` or `groupSummary`. + pub fn summary(mut self, summary: impl Into) -> Self { + self.data.summary.replace(summary.into()); + self + } + + /// Defines an action type for this notification. + pub fn action_type_id(mut self, action_type_id: impl Into) -> Self { + self.data.action_type_id.replace(action_type_id.into()); + self + } + + /// Identifier used to group multiple notifications. + /// + /// https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier + pub fn group(mut self, group: impl Into) -> Self { + self.data.group.replace(group.into()); + self + } + + /// Instructs the system that this notification is the summary of a group on Android. + pub fn group_summary(mut self) -> Self { + self.data.group_summary = true; + self + } + + /// The sound resource name. Only available on mobile. + pub fn sound(mut self, sound: impl Into) -> Self { + self.data.sound.replace(sound.into()); + self + } + + /// Append an inbox line to the notification. + /// Changes the notification style to inbox. + /// Cannot be used with `largeBody`. + /// + /// Only supports up to 5 lines. + pub fn inbox_line(mut self, line: impl Into) -> Self { + self.data.inbox_lines.push(line.into()); + self + } + + /// Notification icon. + /// + /// On Android the icon must be placed in the app's `res/drawable` folder. pub fn icon(mut self, icon: impl Into) -> Self { self.data.icon.replace(icon.into()); self } + + /// Notification large icon (Android). + /// + /// The icon must be placed in the app's `res/drawable` folder. + pub fn large_icon(mut self, large_icon: impl Into) -> Self { + self.data.large_icon.replace(large_icon.into()); + self + } + + /// Icon color on Android. + pub fn icon_color(mut self, icon_color: impl Into) -> Self { + self.data.icon_color.replace(icon_color.into()); + self + } + + /// Append an attachment to the notification. + pub fn attachment(mut self, attachment: Attachment) -> Self { + self.data.attachments.push(attachment); + self + } + + /// Adds an extra payload to store in the notification. + pub fn extra(mut self, key: impl Into, value: impl Serialize) -> Self { + self.data + .extra + .insert(key.into(), serde_json::to_value(value).unwrap()); + self + } + + /// If true, the notification cannot be dismissed by the user on Android. + /// + /// An application service must manage the dismissal of the notification. + /// It is typically used to indicate a background task that is pending (e.g. a file download) + /// or the user is engaged with (e.g. playing music). + pub fn ongoing(mut self) -> Self { + self.data.ongoing = true; + self + } + + /// Automatically cancel the notification when the user clicks on it. + pub fn auto_cancel(mut self) -> Self { + self.data.auto_cancel = true; + self + } } /// Extensions to [`tauri::App`], [`tauri::AppHandle`] and [`tauri::Window`] to access the notification APIs.