pull/340/head
Lucas Nogueira 2 years ago
parent fea1d84682
commit eff7041d62
No known key found for this signature in database
GPG Key ID: 7C32FCA95C8C95D7

@ -62,12 +62,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
}
}*/
/**
* Schedule a notification invoke from JavaScript
* Creates local notification in system.
*/
@Command
fun schedule(invoke: Invoke) {
fun batch(invoke: Invoke) {
val localNotifications = Notification.buildNotificationList(invoke)
?: return
val ids = manager!!.schedule(invoke, localNotifications)

@ -45,7 +45,7 @@ class NotificationSchedule(val scheduleObj: JSObject) {
val payload = scheduleObj.getJSObject("data", JSObject())
when (val scheduleKind = scheduleObj.getString("kind", "")) {
"at" -> {
"At" -> {
val dateString = payload.getString("date")
if (dateString.isNotEmpty()) {
val sdf = SimpleDateFormat(JS_DATE_FORMAT)
@ -60,11 +60,11 @@ class NotificationSchedule(val scheduleObj: JSObject) {
throw Exception("`at` date cannot be empty")
}
}
"interval" -> {
"Interval" -> {
val dateMatch = onFromJson(payload)
kind = ScheduleKind.Interval(dateMatch)
}
"every" -> {
"Every" -> {
val interval = NotificationInterval.valueOf(payload.getString("interval"))
kind = ScheduleKind.Every(interval, payload.getInteger("count", 1))
}

@ -72,7 +72,9 @@ interface Options {
*/
actionTypeId?: string
/**
* Identifier used to group multiple notifications on Android.
* Identifier used to group multiple notifications.
*
* https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/1649872-threadidentifier
*/
group?: string
/**
@ -156,20 +158,23 @@ enum ScheduleEvery {
Day = 'Day',
Hour = 'Hour',
Minute = 'Minute',
/**
* Not supported on iOS.
*/
Second = 'Second'
}
type ScheduleData = {
kind: 'at',
kind: 'At',
data: {
date: Date
repeating: boolean
}
} | {
kind: 'interval',
kind: 'Interval',
data: ScheduleInterval
} | {
kind: 'every',
kind: 'Every',
data: {
interval: ScheduleEvery
}
@ -185,15 +190,15 @@ class Schedule {
}
static at(date: Date, repeating = false) {
return new Schedule({ kind: 'at', data: { date, repeating }})
return new Schedule({ kind: 'At', data: { date, repeating }})
}
static interval(interval: ScheduleInterval) {
return new Schedule({ kind: 'interval', data: interval })
return new Schedule({ kind: 'Interval', data: interval })
}
static every(kind: ScheduleEvery) {
return new Schedule({ kind: 'every', data: { interval: kind }})
return new Schedule({ kind: 'Every', data: { interval: kind }})
}
}

@ -4,16 +4,16 @@
import PackageDescription
let package = Package(
name: "tauri-plugin-{{ plugin_name }}",
name: "tauri-plugin-notification",
platforms: [
.iOS(.v13),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "tauri-plugin-{{ plugin_name }}",
name: "tauri-plugin-notification",
type: .static,
targets: ["tauri-plugin-{{ plugin_name }}"]),
targets: ["tauri-plugin-notification"]),
],
dependencies: [
.package(name: "Tauri", path: "../.tauri/tauri-api")
@ -22,7 +22,7 @@ let package = Package(
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "tauri-plugin-{{ plugin_name }}",
name: "tauri-plugin-notification",
dependencies: [
.byName(name: "Tauri")
],

@ -0,0 +1,253 @@
import Tauri
import UserNotifications
enum NotificationError: LocalizedError {
case contentNoId
case contentNoTitle
case contentNoBody
case triggerConstructionFailed
case triggerRepeatIntervalTooShort
case attachmentNoId
case attachmentNoUrl
case attachmentFileNotFound(path: String)
case attachmentUnableToCreate(String)
var errorDescription: String? {
switch self {
case .attachmentFileNotFound(let path):
return "Unable to find file \(path) for attachment"
default:
return ""
}
}
}
func makeNotificationContent(_ notification: JSObject) throws -> UNNotificationContent {
guard let title = notification["title"] as? String else {
throw NotificationError.contentNoTitle
}
guard let body = notification["body"] as? String else {
throw NotificationError.contentNoBody
}
let extra = notification["extra"] as? JSObject ?? [:]
let schedule = notification["schedule"] as? JSObject ?? [:]
let content = UNMutableNotificationContent()
content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil)
content.body = NSString.localizedUserNotificationString(
forKey: body,
arguments: nil)
content.userInfo = [
"__EXTRA__": extra,
"__SCHEDULE__": schedule,
]
if let actionTypeId = notification["actionTypeId"] as? String {
content.categoryIdentifier = actionTypeId
}
if let threadIdentifier = notification["group"] as? String {
content.threadIdentifier = threadIdentifier
}
if let summaryArgument = notification["summary"] as? String {
content.summaryArgument = summaryArgument
}
if let sound = notification["sound"] as? String {
content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
}
if let attachments = notification["attachments"] as? [JSObject] {
content.attachments = try makeAttachments(attachments)
}
return content
}
func makeAttachments(_ attachments: [JSObject]) throws -> [UNNotificationAttachment] {
var createdAttachments = [UNNotificationAttachment]()
for attachment in attachments {
guard let id = attachment["id"] as? String else {
throw NotificationError.attachmentNoId
}
guard let url = attachment["url"] as? String else {
throw NotificationError.attachmentNoUrl
}
guard let urlObject = makeAttachmentUrl(url) else {
throw NotificationError.attachmentFileNotFound(path: url)
}
let options = attachment["options"] as? JSObject ?? [:]
do {
let newAttachment = try UNNotificationAttachment(
identifier: id, url: urlObject, options: makeAttachmentOptions(options))
createdAttachments.append(newAttachment)
} catch {
throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
}
}
return createdAttachments
}
func makeAttachmentUrl(_ path: String) -> URL? {
return URL(string: path)
}
func makeAttachmentOptions(_ options: JSObject) -> JSObject {
var opts: JSObject = [:]
if let iosUNNotificationAttachmentOptionsTypeHintKey = options[
"iosUNNotificationAttachmentOptionsTypeHintKey"] as? String
{
opts[UNNotificationAttachmentOptionsTypeHintKey] = iosUNNotificationAttachmentOptionsTypeHintKey
}
if let iosUNNotificationAttachmentOptionsThumbnailHiddenKey = options[
"iosUNNotificationAttachmentOptionsThumbnailHiddenKey"] as? String
{
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] =
iosUNNotificationAttachmentOptionsThumbnailHiddenKey
}
if let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey = options[
"iosUNNotificationAttachmentOptionsThumbnailClippingRectKey"] as? String
{
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
iosUNNotificationAttachmentOptionsThumbnailClippingRectKey
}
if let iosUNNotificationAttachmentOptionsThumbnailTimeKey = options[
"iosUNNotificationAttachmentOptionsThumbnailTimeKey"] as? String
{
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] =
iosUNNotificationAttachmentOptionsThumbnailTimeKey
}
return opts
}
func handleScheduledNotification(_ invoke: Invoke, _ schedule: JSObject) throws
-> UNNotificationTrigger?
{
let kind = schedule["kind"] as? String ?? ""
let payload = schedule["data"] as? JSObject ?? [:]
switch kind {
case "At":
let date = payload["at"] as? String ?? ""
let dateFormatter = ISO8601DateFormatter()
if let at = dateFormatter.date(from: date) {
let repeats = payload["repeats"] as? Bool ?? false
let dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: at)
if dateInfo.date! < Date() {
invoke.reject("Scheduled time must be *after* current time")
return nil
}
let dateInterval = DateInterval(start: Date(), end: dateInfo.date!)
// Notifications that repeat have to be at least a minute between each other
if repeats && dateInterval.duration < 60 {
throw NotificationError.triggerRepeatIntervalTooShort
}
return UNTimeIntervalNotificationTrigger(
timeInterval: dateInterval.duration, repeats: repeats)
} else {
invoke.reject("could not parse `at` date")
}
case "Interval":
let dateComponents = getDateComponents(payload)
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
case "Every":
let interval = payload["interval"] as? String ?? ""
let count = schedule["count"] as? Int ?? 1
if let repeatDateInterval = getRepeatDateInterval(interval, count) {
// Notifications that repeat have to be at least a minute between each other
if repeatDateInterval.duration < 60 {
throw NotificationError.triggerRepeatIntervalTooShort
}
return UNTimeIntervalNotificationTrigger(
timeInterval: repeatDateInterval.duration, repeats: true)
}
default:
return nil
}
return nil
}
/// Given our schedule format, return a DateComponents object
/// that only contains the components passed in.
func getDateComponents(_ at: JSObject) -> DateComponents {
// var dateInfo = Calendar.current.dateComponents(in: TimeZone.current, from: Date())
// dateInfo.calendar = Calendar.current
var dateInfo = DateComponents()
if let year = at["year"] as? Int {
dateInfo.year = year
}
if let month = at["month"] as? Int {
dateInfo.month = month
}
if let day = at["day"] as? Int {
dateInfo.day = day
}
if let hour = at["hour"] as? Int {
dateInfo.hour = hour
}
if let minute = at["minute"] as? Int {
dateInfo.minute = minute
}
if let second = at["second"] as? Int {
dateInfo.second = second
}
if let weekday = at["weekday"] as? Int {
dateInfo.weekday = weekday
}
return dateInfo
}
/// Compute the difference between the string representation of a date
/// interval and today. For example, if every is "month", then we
/// return the interval between today and a month from today.
func getRepeatDateInterval(_ every: String, _ count: Int) -> DateInterval? {
let cal = Calendar.current
let now = Date()
switch every {
case "Year":
let newDate = cal.date(byAdding: .year, value: count, to: now)!
return DateInterval(start: now, end: newDate)
case "Month":
let newDate = cal.date(byAdding: .month, value: count, to: now)!
return DateInterval(start: now, end: newDate)
case "TwoWeeks":
let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
return DateInterval(start: now, end: newDate)
case "Week":
let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
return DateInterval(start: now, end: newDate)
case "Day":
let newDate = cal.date(byAdding: .day, value: count, to: now)!
return DateInterval(start: now, end: newDate)
case "Hour":
let newDate = cal.date(byAdding: .hour, value: count, to: now)!
return DateInterval(start: now, end: newDate)
case "Minute":
let newDate = cal.date(byAdding: .minute, value: count, to: now)!
return DateInterval(start: now, end: newDate)
case "Second":
let newDate = cal.date(byAdding: .second, value: count, to: now)!
return DateInterval(start: now, end: newDate)
default:
return nil
}
}

@ -0,0 +1,119 @@
import Tauri
import UserNotifications
public func makeCategories(_ actionTypes: [JSObject]) {
var createdCategories = [UNNotificationCategory]()
let generalCategory = UNNotificationCategory(
identifier: "GENERAL",
actions: [],
intentIdentifiers: [],
options: .customDismissAction)
createdCategories.append(generalCategory)
for type in actionTypes {
guard let id = type["id"] as? String else {
Logger.error("Action type must have an id field")
continue
}
let hiddenBodyPlaceholder = type["iosHiddenPreviewsBodyPlaceholder"] as? String ?? ""
let actions = type["actions"] as? [JSObject] ?? []
let newActions = makeActions(actions)
// Create the custom actions for the TIMER_EXPIRED category.
var newCategory: UNNotificationCategory?
newCategory = UNNotificationCategory(
identifier: id,
actions: newActions,
intentIdentifiers: [],
hiddenPreviewsBodyPlaceholder: hiddenBodyPlaceholder,
options: makeCategoryOptions(type))
createdCategories.append(newCategory!)
}
let center = UNUserNotificationCenter.current()
center.setNotificationCategories(Set(createdCategories))
}
func makeActions(_ actions: [JSObject]) -> [UNNotificationAction] {
var createdActions = [UNNotificationAction]()
for action in actions {
guard let id = action["id"] as? String else {
Logger.error("Action must have an id field")
continue
}
let title = action["title"] as? String ?? ""
let input = action["input"] as? Bool ?? false
var newAction: UNNotificationAction
if input {
let inputButtonTitle = action["inputButtonTitle"] as? String
let inputPlaceholder = action["inputPlaceholder"] as? String ?? ""
if inputButtonTitle != nil {
newAction = UNTextInputNotificationAction(
identifier: id,
title: title,
options: makeActionOptions(action),
textInputButtonTitle: inputButtonTitle!,
textInputPlaceholder: inputPlaceholder)
} else {
newAction = UNTextInputNotificationAction(
identifier: id, title: title, options: makeActionOptions(action))
}
} else {
// Create the custom actions for the TIMER_EXPIRED category.
newAction = UNNotificationAction(
identifier: id,
title: title,
options: makeActionOptions(action))
}
createdActions.append(newAction)
}
return createdActions
}
func makeActionOptions(_ action: JSObject) -> UNNotificationActionOptions {
let foreground = action["foreground"] as? Bool ?? false
let destructive = action["destructive"] as? Bool ?? false
let requiresAuthentication = action["requiresAuthentication"] as? Bool ?? false
if foreground {
return .foreground
}
if destructive {
return .destructive
}
if requiresAuthentication {
return .authenticationRequired
}
return UNNotificationActionOptions(rawValue: 0)
}
func makeCategoryOptions(_ type: JSObject) -> UNNotificationCategoryOptions {
let customDismiss = type["iosCustomDismissAction"] as? Bool ?? false
let carPlay = type["iosAllowInCarPlay"] as? Bool ?? false
let hiddenPreviewsShowTitle = type["iosHiddenPreviewsShowTitle"] as? Bool ?? false
let hiddenPreviewsShowSubtitle = type["iosHiddenPreviewsShowSubtitle"] as? Bool ?? false
if customDismiss {
return .customDismissAction
}
if carPlay {
return .allowInCarPlay
}
if hiddenPreviewsShowTitle {
return .hiddenPreviewsShowTitle
}
if hiddenPreviewsShowSubtitle {
return .hiddenPreviewsShowSubtitle
}
return UNNotificationCategoryOptions(rawValue: 0)
}

@ -0,0 +1,112 @@
import Tauri
import UserNotifications
public class NotificationHandler: NSObject, NotificationHandlerProtocol {
public weak var plugin: Plugin?
var notificationsMap = [String: JSObject]()
public func requestPermissions(with completion: ((Bool, Error?) -> Void)? = nil) {
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.badge, .alert, .sound]) { (granted, error) in
completion?(granted, error)
}
}
public func checkPermissions(with completion: ((UNAuthorizationStatus) -> Void)? = nil) {
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { settings in
completion?(settings.authorizationStatus)
}
}
public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
// let notificationData = makeNotificationRequestJSObject(notification.request)
// TODO self.plugin?.trigger("received", data: notificationData)
if let options = notificationsMap[notification.request.identifier] {
let silent = options["silent"] as? Bool ?? false
if silent {
return UNNotificationPresentationOptions.init(rawValue: 0)
}
}
return [
.badge,
.sound,
.alert,
]
}
public func didReceive(response: UNNotificationResponse) {
var data = JSObject()
let originalNotificationRequest = response.notification.request
let actionId = response.actionIdentifier
// We turn the two default actions (open/dismiss) into generic strings
if actionId == UNNotificationDefaultActionIdentifier {
data["actionId"] = "tap"
} else if actionId == UNNotificationDismissActionIdentifier {
data["actionId"] = "dismiss"
} else {
data["actionId"] = actionId
}
// If the type of action was for an input type, get the value
if let inputType = response as? UNTextInputNotificationResponse {
data["inputValue"] = inputType.userText
}
data["notification"] = makeNotificationRequestJSObject(originalNotificationRequest)
// TODO self.plugin?.trigger("localNotificationActionPerformed", data: data, retainUntilConsumed: true)
}
/**
* Turn a UNNotificationRequest into a JSObject to return back to the client.
*/
func makeNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject {
let notificationRequest = notificationsMap[request.identifier] ?? [:]
var notification = makePendingNotificationRequestJSObject(request)
notification["sound"] = notificationRequest["sound"] ?? ""
notification["actionTypeId"] = request.content.categoryIdentifier
notification["attachments"] = notificationRequest["attachments"] ?? []
return notification
}
func makePendingNotificationRequestJSObject(_ request: UNNotificationRequest) -> JSObject {
var notification: JSObject = [
"id": Int(request.identifier) ?? -1,
"title": request.content.title,
"body": request.content.body,
]
if let userInfo = JSTypes.coerceDictionaryToJSObject(request.content.userInfo) {
var extra = userInfo["__EXTRA__"] as? JSObject ?? userInfo
// check for any dates and convert them to strings
for (key, value) in extra {
if let date = value as? Date {
let dateString = ISO8601DateFormatter().string(from: date)
extra[key] = dateString
}
}
notification["extra"] = extra
if var schedule = userInfo["__SCHEDULE__"] as? JSObject {
// convert schedule at date to string
if let date = schedule["at"] as? Date {
let dateString = ISO8601DateFormatter().string(from: date)
schedule["at"] = dateString
}
notification["schedule"] = schedule
}
}
return notification
}
}

@ -0,0 +1,39 @@
import Foundation
import UserNotifications
@objc public protocol NotificationHandlerProtocol {
func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions
func didReceive(response: UNNotificationResponse)
}
@objc public class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
public weak var notificationHandler: NotificationHandlerProtocol?
override init() {
super.init()
let center = UNUserNotificationCenter.current()
center.delegate = self
}
public func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
var presentationOptions: UNNotificationPresentationOptions? = nil
if notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
presentationOptions = notificationHandler?.willPresent(notification: notification)
}
completionHandler(presentationOptions ?? [])
}
public func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
if response.notification.request.trigger?.isKind(of: UNPushNotificationTrigger.self) != true {
notificationHandler?.didReceive(response: response)
}
completionHandler()
}
}

@ -1,24 +1,228 @@
import SwiftRs
import Tauri
import UIKit
import UserNotifications
import WebKit
import Tauri
import SwiftRs
enum ShowNotificationError: LocalizedError {
case noId
case make(Error)
case create(Error)
var errorDescription: String? {
switch self {
case .noId:
return "notification `id` missing"
case .make(let error):
return "Unable to make notification: \(error)"
case .create(let error):
return "Unable to create notification: \(error)"
}
}
}
func showNotification(invoke: Invoke, notification: JSObject)
throws -> UNNotificationRequest
{
guard let identifier = notification["id"] as? Int else {
throw ShowNotificationError.noId
}
var content: UNNotificationContent
do {
content = try makeNotificationContent(notification)
} catch {
throw ShowNotificationError.make(error)
}
var trigger: UNNotificationTrigger?
do {
if let schedule = notification["schedule"] as? JSObject {
try trigger = handleScheduledNotification(invoke, schedule)
} else {
let match = Calendar.current.dateComponents(
[.timeZone, .year, .month, .day, .hour, .minute], from: Date())
trigger = UNCalendarNotificationTrigger(dateMatching: match, repeats: false)
}
} catch {
throw ShowNotificationError.create(error)
}
// Schedule the request.
let request = UNNotificationRequest(
identifier: "\(identifier)", content: content, trigger: trigger
)
let center = UNUserNotificationCenter.current()
center.add(request) { (error: Error?) in
if let theError = error {
invoke.reject(theError.localizedDescription)
}
}
return request
}
class NotificationPlugin: Plugin {
@objc public func requestPermission(_ invoke: Invoke) throws {
invoke.resolve(["permissionState": "granted"])
let notificationHandler = NotificationHandler()
let notificationManager = NotificationManager()
override init() {
super.init()
notificationHandler.plugin = self
notificationManager.notificationHandler = notificationHandler
}
@objc public func show(_ invoke: Invoke) throws {
let request = try showNotification(invoke: invoke, notification: invoke.data)
// TODO self.notificationHandler.notificationsMap[request.identifier] = invoke.data
invoke.resolve([
"id": Int(request.identifier) ?? -1
])
}
@objc public func batch(_ invoke: Invoke) throws {
guard let notifications = invoke.getArray("notifications", JSObject.self) else {
invoke.reject("`notifications` array is required")
return
}
var ids = [String]()
for notification in notifications {
let request = try showNotification(invoke: invoke, notification: notification)
// TODO self.notificationHandler.notificationsMap[request.identifier] = notification
ids.append(request.identifier)
}
let ret = ids.map({ (id) -> JSObject in
return [
"id": Int(id) ?? -1
]
})
invoke.resolve([
"notifications": ret
])
}
@objc public override func requestPermissions(_ invoke: Invoke) {
notificationHandler.requestPermissions { granted, error in
guard error == nil else {
invoke.reject(error!.localizedDescription)
return
}
invoke.resolve(["permissionState": granted ? "granted" : "denied"])
}
}
@objc public override func checkPermissions(_ invoke: Invoke) {
notificationHandler.checkPermissions { status in
let permission: String
switch status {
case .authorized, .ephemeral, .provisional:
permission = "granted"
case .denied:
permission = "denied"
case .notDetermined:
permission = "default"
@unknown default:
permission = "default"
}
invoke.resolve(["permissionState": permission])
}
}
@objc func cancel(_ invoke: Invoke) {
guard let notifications = invoke.getArray("notifications", JSObject.self),
notifications.count > 0
else {
invoke.reject("`notifications` input is required")
return
}
let ids = notifications.map({ (value: JSObject) -> String in
if let idString = value["id"] as? String {
return idString
} else if let idNum = value["id"] as? NSNumber {
return idNum.stringValue
}
return ""
})
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids)
invoke.resolve()
}
@objc func getPending(_ invoke: Invoke) {
UNUserNotificationCenter.current().getPendingNotificationRequests(completionHandler: {
(notifications) in
let ret = notifications.compactMap({ [weak self] (notification) -> JSObject? in
return self?.notificationHandler.makePendingNotificationRequestJSObject(notification)
})
invoke.resolve([
"notifications": ret
])
})
}
@objc func registerActionTypes(_ invoke: Invoke) {
guard let types = invoke.getArray("types", JSObject.self) else {
return
}
makeCategories(types)
invoke.resolve()
}
@objc public func permissionState(_ invoke: Invoke) throws {
invoke.resolve(["permissionState": "granted"])
@objc func removeDeliveredNotifications(_ invoke: Invoke) {
guard let notifications = invoke.getArray("notifications", JSObject.self) else {
invoke.reject("`notifications` input is required")
return
}
@objc public func notify(_ invoke: Invoke) throws {
// TODO
let ids = notifications.map { "\($0["id"] ?? "")" }
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
invoke.resolve()
}
@objc func removeAllDeliveredNotifications(_ invoke: Invoke) {
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
DispatchQueue.main.async(execute: {
UIApplication.shared.applicationIconBadgeNumber = 0
})
invoke.resolve()
}
@objc func getDeliveredNotifications(_ invoke: Invoke) {
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: {
(notifications) in
let ret = notifications.map({ (notification) -> [String: Any] in
return self.notificationHandler.makeNotificationRequestJSObject(
notification.request)
})
invoke.resolve([
"notifications": ret
])
})
}
@objc func createChannel(_ invoke: Invoke) {
invoke.reject("not implemented")
}
@objc func deleteChannel(_ invoke: Invoke) {
invoke.reject("not implemented")
}
@objc func listChannels(_ invoke: Invoke) {
invoke.reject("not implemented")
}
}
@_cdecl("init_plugin_notification")
func initPlugin(name: SRString, webview: WKWebView?) {
Tauri.registerPlugin(webview: webview, name: name.toString(), plugin: NotificationPlugin())
func initPlugin() -> Plugin {
return NotificationPlugin()
}

@ -2,30 +2,21 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
use tauri::{command, AppHandle, Runtime, State};
use crate::{Notification, PermissionState, Result};
/// The options for the notification API.
#[derive(Debug, Clone, Deserialize)]
pub struct NotificationOptions {
/// The notification title.
pub title: String,
/// The notification body.
pub body: Option<String>,
/// The notification icon.
pub icon: Option<String>,
}
use crate::{Notification, NotificationData, PermissionState, Result};
#[command]
pub(crate) async fn is_permission_granted<R: Runtime>(
_app: AppHandle<R>,
notification: State<'_, Notification<R>>,
) -> Result<bool> {
notification
.permission_state()
.map(|s| s == PermissionState::Granted)
) -> Result<Option<bool>> {
let state = notification.permission_state()?;
match state {
PermissionState::Granted => Ok(Some(true)),
PermissionState::Denied => Ok(Some(false)),
PermissionState::Unknown => Ok(None),
}
}
#[command]
@ -40,15 +31,9 @@ pub(crate) async fn request_permission<R: Runtime>(
pub(crate) async fn notify<R: Runtime>(
_app: AppHandle<R>,
notification: State<'_, Notification<R>>,
options: NotificationOptions,
options: NotificationData,
) -> Result<()> {
let mut builder = notification.builder().title(options.title);
if let Some(body) = options.body {
builder = builder.body(body);
}
if let Some(icon) = options.icon {
builder = builder.icon(icon);
}
let mut builder = notification.builder();
builder.data = options;
builder.show()
}

@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Serialize;
use serde::{Deserialize, Serialize};
#[cfg(mobile)]
use tauri::plugin::PluginHandle;
#[cfg(desktop)]
@ -30,8 +30,11 @@ use desktop::Notification;
#[cfg(mobile)]
use mobile::Notification;
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, Serialize, Deserialize)]
struct NotificationData {
/// Notification id.
#[serde(default)]
id: usize,
/// The notification title.
title: Option<String>,
/// The notification body.
@ -47,7 +50,7 @@ pub struct NotificationBuilder<R: Runtime> {
app: AppHandle<R>,
#[cfg(mobile)]
handle: PluginHandle<R>,
data: NotificationData,
pub(crate) data: NotificationData,
}
impl<R: Runtime> NotificationBuilder<R> {

@ -31,7 +31,7 @@ pub fn init<R: Runtime, C: DeserializeOwned>(
impl<R: Runtime> crate::NotificationBuilder<R> {
pub fn show(self) -> crate::Result<()> {
self.handle
.run_mobile_plugin("notify", self.data)
.run_mobile_plugin("show", self.data)
.map_err(Into::into)
}
}

@ -13,6 +13,8 @@ pub enum PermissionState {
Granted,
/// Permission access has been denied.
Denied,
/// Unknown state. Must request permission.
Unknown,
}
impl Display for PermissionState {
@ -20,6 +22,7 @@ impl Display for PermissionState {
match self {
Self::Granted => write!(f, "granted"),
Self::Denied => write!(f, "denied"),
Self::Unknown => write!(f, "Unknown"),
}
}
}
@ -42,6 +45,7 @@ impl<'de> Deserialize<'de> for PermissionState {
match s.to_lowercase().as_str() {
"granted" => Ok(Self::Granted),
"denied" => Ok(Self::Denied),
"default" => Ok(Self::Unknown),
_ => Err(DeError::custom(format!("unknown permission state '{s}'"))),
}
}

Loading…
Cancel
Save