mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-06-06 13:53:54 +02:00
feat: update to alpha.17, typed mobile plugin IPC arguments (#676)
Co-authored-by: Amr Bashir <amr.bashir2015@gmail.com>
This commit is contained in:
committed by
GitHub
parent
76cfdc32b4
commit
e438e0a62d
@@ -2,7 +2,7 @@
|
||||
|
||||
## \[2.0.0-alpha.2]
|
||||
|
||||
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.16.
|
||||
- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9.
|
||||
|
||||
## \[2.0.0-alpha.3]
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ rust-version = { workspace = true }
|
||||
links = "tauri-plugin-notification"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = [ "dox" ]
|
||||
rustc-args = [ "--cfg", "docsrs" ]
|
||||
rustdoc-args = [ "--cfg", "docsrs" ]
|
||||
targets = [ "x86_64-unknown-linux-gnu", "x86_64-linux-android" ]
|
||||
|
||||
[build-dependencies]
|
||||
@@ -34,4 +35,3 @@ win7-notifications = { version = "0.3.1", optional = true }
|
||||
|
||||
[features]
|
||||
windows7-compat = [ "win7-notifications" ]
|
||||
dox = [ "tauri/dox" ]
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies {
|
||||
implementation("androidx.core:core-ktx:1.9.0")
|
||||
implementation("androidx.appcompat:appcompat:1.6.0")
|
||||
implementation("com.google.android.material:material:1.7.0")
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.3")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<receiver android:name="app.tauri.notification.TimedNotificationPublisher" />
|
||||
<receiver android:name="app.tauri.notification.NotificationDismissReceiver" />
|
||||
<receiver
|
||||
android:name="app.tauri.notification.NotificationRestoreReceiver"
|
||||
android:name="app.tauri.notification.LocalNotificationRestoreReceiver"
|
||||
android:directBootAware="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
|
||||
@@ -12,21 +12,42 @@ import android.graphics.Color
|
||||
import android.media.AudioAttributes
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import app.tauri.Logger
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import com.fasterxml.jackson.annotation.JsonValue
|
||||
|
||||
private const val CHANNEL_ID = "id"
|
||||
private const val CHANNEL_NAME = "name"
|
||||
private const val CHANNEL_DESCRIPTION = "description"
|
||||
private const val CHANNEL_IMPORTANCE = "importance"
|
||||
private const val CHANNEL_VISIBILITY = "visibility"
|
||||
private const val CHANNEL_SOUND = "sound"
|
||||
private const val CHANNEL_VIBRATE = "vibration"
|
||||
private const val CHANNEL_USE_LIGHTS = "lights"
|
||||
private const val CHANNEL_LIGHT_COLOR = "lightColor"
|
||||
enum class Importance(@JsonValue val value: Int) {
|
||||
None(0),
|
||||
Min(1),
|
||||
Low(2),
|
||||
Default(3),
|
||||
High(4);
|
||||
}
|
||||
|
||||
enum class Visibility(@JsonValue val value: Int) {
|
||||
Secret(-1),
|
||||
Private(0),
|
||||
Public(1);
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class Channel {
|
||||
lateinit var id: String
|
||||
lateinit var name: String
|
||||
var description: String? = null
|
||||
var sound: String? = null
|
||||
var lights: Boolean? = null
|
||||
var lightsColor: String? = null
|
||||
var vibration: Boolean? = null
|
||||
var importance: Importance? = null
|
||||
var visibility: Visibility? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class DeleteChannelArgs {
|
||||
lateinit var id: String
|
||||
}
|
||||
|
||||
class ChannelManager(private var context: Context) {
|
||||
private var notificationManager: NotificationManager? = null
|
||||
@@ -38,32 +59,7 @@ class ChannelManager(private var context: Context) {
|
||||
|
||||
fun createChannel(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = JSObject()
|
||||
if (invoke.getString(CHANNEL_ID) != null) {
|
||||
channel.put(CHANNEL_ID, invoke.getString(CHANNEL_ID))
|
||||
} else {
|
||||
invoke.reject("Channel missing identifier")
|
||||
return
|
||||
}
|
||||
if (invoke.getString(CHANNEL_NAME) != null) {
|
||||
channel.put(CHANNEL_NAME, invoke.getString(CHANNEL_NAME))
|
||||
} else {
|
||||
invoke.reject("Channel missing name")
|
||||
return
|
||||
}
|
||||
channel.put(
|
||||
CHANNEL_IMPORTANCE,
|
||||
invoke.getInt(CHANNEL_IMPORTANCE, NotificationManager.IMPORTANCE_DEFAULT)
|
||||
)
|
||||
channel.put(CHANNEL_DESCRIPTION, invoke.getString(CHANNEL_DESCRIPTION, ""))
|
||||
channel.put(
|
||||
CHANNEL_VISIBILITY,
|
||||
invoke.getInt(CHANNEL_VISIBILITY, NotificationCompat.VISIBILITY_PUBLIC)
|
||||
)
|
||||
channel.put(CHANNEL_SOUND, invoke.getString(CHANNEL_SOUND))
|
||||
channel.put(CHANNEL_VIBRATE, invoke.getBoolean(CHANNEL_VIBRATE, false))
|
||||
channel.put(CHANNEL_USE_LIGHTS, invoke.getBoolean(CHANNEL_USE_LIGHTS, false))
|
||||
channel.put(CHANNEL_LIGHT_COLOR, invoke.getString(CHANNEL_LIGHT_COLOR))
|
||||
val channel = invoke.parseArgs(Channel::class.java)
|
||||
createChannel(channel)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
@@ -71,18 +67,18 @@ class ChannelManager(private var context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChannel(channel: JSObject) {
|
||||
private fun createChannel(channel: Channel) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationChannel = NotificationChannel(
|
||||
channel.getString(CHANNEL_ID),
|
||||
channel.getString(CHANNEL_NAME),
|
||||
channel.getInteger(CHANNEL_IMPORTANCE)!!
|
||||
channel.id,
|
||||
channel.name,
|
||||
(channel.importance ?: Importance.Default).value
|
||||
)
|
||||
notificationChannel.description = channel.getString(CHANNEL_DESCRIPTION)
|
||||
notificationChannel.lockscreenVisibility = channel.getInteger(CHANNEL_VISIBILITY, android.app.Notification.VISIBILITY_PRIVATE)
|
||||
notificationChannel.enableVibration(channel.getBoolean(CHANNEL_VIBRATE, false))
|
||||
notificationChannel.enableLights(channel.getBoolean(CHANNEL_USE_LIGHTS, false))
|
||||
val lightColor = channel.getString(CHANNEL_LIGHT_COLOR)
|
||||
notificationChannel.description = channel.description
|
||||
notificationChannel.lockscreenVisibility = (channel.visibility ?: Visibility.Private).value
|
||||
notificationChannel.enableVibration(channel.vibration ?: false)
|
||||
notificationChannel.enableLights(channel.lights ?: false)
|
||||
val lightColor = channel.lightsColor ?: ""
|
||||
if (lightColor.isNotEmpty()) {
|
||||
try {
|
||||
notificationChannel.lightColor = Color.parseColor(lightColor)
|
||||
@@ -94,7 +90,7 @@ class ChannelManager(private var context: Context) {
|
||||
)
|
||||
}
|
||||
}
|
||||
var sound = channel.getString(CHANNEL_SOUND)
|
||||
var sound = channel.sound ?: ""
|
||||
if (sound.isNotEmpty()) {
|
||||
if (sound.contains(".")) {
|
||||
sound = sound.substring(0, sound.lastIndexOf('.'))
|
||||
@@ -113,8 +109,8 @@ class ChannelManager(private var context: Context) {
|
||||
|
||||
fun deleteChannel(invoke: Invoke) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = invoke.getString("id")
|
||||
notificationManager?.deleteNotificationChannel(channelId)
|
||||
val args = invoke.parseArgs(DeleteChannelArgs::class.java)
|
||||
notificationManager?.deleteNotificationChannel(args.id)
|
||||
invoke.resolve()
|
||||
} else {
|
||||
invoke.reject("channel not available")
|
||||
@@ -125,28 +121,29 @@ class ChannelManager(private var context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val notificationChannels: List<NotificationChannel> =
|
||||
notificationManager?.notificationChannels ?: listOf()
|
||||
val channels = JSArray()
|
||||
|
||||
val channels = mutableListOf<Channel>()
|
||||
|
||||
for (notificationChannel in notificationChannels) {
|
||||
val channel = JSObject()
|
||||
channel.put(CHANNEL_ID, notificationChannel.id)
|
||||
channel.put(CHANNEL_NAME, notificationChannel.name)
|
||||
channel.put(CHANNEL_DESCRIPTION, notificationChannel.description)
|
||||
channel.put(CHANNEL_IMPORTANCE, notificationChannel.importance)
|
||||
channel.put(CHANNEL_VISIBILITY, notificationChannel.lockscreenVisibility)
|
||||
channel.put(CHANNEL_SOUND, notificationChannel.sound)
|
||||
channel.put(CHANNEL_VIBRATE, notificationChannel.shouldVibrate())
|
||||
channel.put(CHANNEL_USE_LIGHTS, notificationChannel.shouldShowLights())
|
||||
channel.put(
|
||||
CHANNEL_LIGHT_COLOR, String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and notificationChannel.lightColor
|
||||
)
|
||||
val channel = Channel()
|
||||
channel.id = notificationChannel.id
|
||||
channel.name = notificationChannel.name.toString()
|
||||
channel.description = notificationChannel.description
|
||||
channel.sound = notificationChannel.sound.toString()
|
||||
channel.lights = notificationChannel.shouldShowLights()
|
||||
String.format(
|
||||
"#%06X",
|
||||
0xFFFFFF and notificationChannel.lightColor
|
||||
)
|
||||
channels.put(channel)
|
||||
channel.vibration = notificationChannel.shouldVibrate()
|
||||
channel.importance = Importance.values().firstOrNull { it.value == notificationChannel.importance }
|
||||
channel.visibility = Visibility.values().firstOrNull { it.value == notificationChannel.lockscreenVisibility }
|
||||
|
||||
channels.add(channel)
|
||||
}
|
||||
val result = JSObject()
|
||||
result.put("channels", channels)
|
||||
invoke.resolve(result)
|
||||
|
||||
invoke.resolveObject(channels)
|
||||
|
||||
} else {
|
||||
invoke.reject("channel not available")
|
||||
}
|
||||
|
||||
@@ -8,20 +8,22 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
@InvokeArg
|
||||
class Notification {
|
||||
var id: Int = 0
|
||||
var title: String? = null
|
||||
var body: String? = null
|
||||
var largeBody: String? = null
|
||||
var summary: String? = null
|
||||
var id: Int = 0
|
||||
private var sound: String? = null
|
||||
private var smallIcon: String? = null
|
||||
private var largeIcon: String? = null
|
||||
var sound: String? = null
|
||||
var icon: String? = null
|
||||
var largeIcon: String? = null
|
||||
var iconColor: String? = null
|
||||
var actionTypeId: String? = null
|
||||
var group: String? = null
|
||||
@@ -33,7 +35,7 @@ class Notification {
|
||||
var attachments: List<NotificationAttachment>? = null
|
||||
var schedule: NotificationSchedule? = null
|
||||
var channelId: String? = null
|
||||
var source: JSObject? = null
|
||||
var sourceJson: String? = null
|
||||
var visibility: Int? = null
|
||||
var number: Int? = null
|
||||
|
||||
@@ -54,18 +56,6 @@ class Notification {
|
||||
return soundPath
|
||||
}
|
||||
|
||||
fun setSound(sound: String?) {
|
||||
this.sound = sound
|
||||
}
|
||||
|
||||
fun setSmallIcon(smallIcon: String?) {
|
||||
this.smallIcon = AssetUtils.getResourceBaseName(smallIcon)
|
||||
}
|
||||
|
||||
fun setLargeIcon(largeIcon: String?) {
|
||||
this.largeIcon = AssetUtils.getResourceBaseName(largeIcon)
|
||||
}
|
||||
|
||||
fun getIconColor(globalColor: String): String {
|
||||
// use the one defined local before trying for a globally defined color
|
||||
return iconColor ?: globalColor
|
||||
@@ -73,8 +63,8 @@ class Notification {
|
||||
|
||||
fun getSmallIcon(context: Context, defaultIcon: Int): Int {
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
if (smallIcon != null) {
|
||||
resId = AssetUtils.getResourceID(context, smallIcon, "drawable")
|
||||
if (icon != null) {
|
||||
resId = AssetUtils.getResourceID(context, icon, "drawable")
|
||||
}
|
||||
if (resId == AssetUtils.RESOURCE_ID_ZERO_VALUE) {
|
||||
resId = defaultIcon
|
||||
@@ -93,77 +83,15 @@ class Notification {
|
||||
val isScheduled = schedule != null
|
||||
|
||||
companion object {
|
||||
fun fromJson(jsonNotification: JSONObject): Notification {
|
||||
val notification: JSObject = try {
|
||||
val identifier = jsonNotification.getLong("id")
|
||||
if (identifier > Int.MAX_VALUE || identifier < Int.MIN_VALUE) {
|
||||
throw Exception("The notification identifier should be a 32-bit integer")
|
||||
}
|
||||
JSObject.fromJSONObject(jsonNotification)
|
||||
} catch (e: JSONException) {
|
||||
throw Exception("Invalid notification JSON object", e)
|
||||
}
|
||||
return fromJSObject(notification)
|
||||
}
|
||||
|
||||
fun fromJSObject(jsonObject: JSObject): Notification {
|
||||
val notification = Notification()
|
||||
notification.source = jsonObject
|
||||
notification.id = jsonObject.getInteger("id") ?: throw Exception("Missing notification identifier")
|
||||
notification.body = jsonObject.getString("body", null)
|
||||
notification.largeBody = jsonObject.getString("largeBody", null)
|
||||
notification.summary = jsonObject.getString("summary", null)
|
||||
notification.actionTypeId = jsonObject.getString("actionTypeId", null)
|
||||
notification.group = jsonObject.getString("group", null)
|
||||
notification.setSound(jsonObject.getString("sound", null))
|
||||
notification.title = jsonObject.getString("title", null)
|
||||
notification.setSmallIcon(jsonObject.getString("icon", null))
|
||||
notification.setLargeIcon(jsonObject.getString("largeIcon", null))
|
||||
notification.iconColor = jsonObject.getString("iconColor", null)
|
||||
notification.attachments = NotificationAttachment.getAttachments(jsonObject)
|
||||
notification.isGroupSummary = jsonObject.getBoolean("groupSummary", false)
|
||||
notification.channelId = jsonObject.getString("channelId", null)
|
||||
val schedule = jsonObject.getJSObject("schedule")
|
||||
if (schedule != null) {
|
||||
notification.schedule = NotificationSchedule(schedule)
|
||||
}
|
||||
notification.extra = jsonObject.getJSObject("extra")
|
||||
notification.isOngoing = jsonObject.getBoolean("ongoing", false)
|
||||
notification.isAutoCancel = jsonObject.getBoolean("autoCancel", true)
|
||||
notification.visibility = jsonObject.getInteger("visibility")
|
||||
notification.number = jsonObject.getInteger("number")
|
||||
try {
|
||||
val inboxLines = jsonObject.getJSONArray("inboxLines")
|
||||
val inboxStringList: MutableList<String> = ArrayList()
|
||||
for (i in 0 until inboxLines.length()) {
|
||||
inboxStringList.add(inboxLines.getString(i))
|
||||
}
|
||||
notification.inboxLines = inboxStringList
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return notification
|
||||
}
|
||||
|
||||
fun buildNotificationPendingList(notifications: List<Notification>): JSObject {
|
||||
val result = JSObject()
|
||||
val jsArray = JSArray()
|
||||
fun buildNotificationPendingList(notifications: List<Notification>): List<PendingNotification> {
|
||||
val pendingNotifications = mutableListOf<PendingNotification>()
|
||||
for (notification in notifications) {
|
||||
val jsNotification = JSObject()
|
||||
jsNotification.put("id", notification.id)
|
||||
jsNotification.put("title", notification.title)
|
||||
jsNotification.put("body", notification.body)
|
||||
val schedule = notification.schedule
|
||||
if (schedule != null) {
|
||||
val jsSchedule = JSObject()
|
||||
jsSchedule.put("kind", schedule.scheduleObj.getString("kind", null))
|
||||
jsSchedule.put("data", schedule.scheduleObj.getJSObject("data"))
|
||||
jsNotification.put("schedule", jsSchedule)
|
||||
}
|
||||
jsNotification.put("extra", notification.extra)
|
||||
jsArray.put(jsNotification)
|
||||
val pendingNotification = PendingNotification(notification.id, notification.title, notification.body, notification.schedule, notification.extra)
|
||||
pendingNotifications.add(pendingNotification)
|
||||
}
|
||||
result.put("notifications", jsArray)
|
||||
return result
|
||||
return pendingNotifications
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PendingNotification(val id: Int, val title: String?, val body: String?, val schedule: NotificationSchedule?, val extra: JSObject?)
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package app.tauri.notification
|
||||
|
||||
import app.tauri.Logger
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import org.json.JSONObject
|
||||
|
||||
class NotificationAction() {
|
||||
var id: String? = null
|
||||
var title: String? = null
|
||||
var input = false
|
||||
|
||||
constructor(id: String?, title: String?, input: Boolean): this() {
|
||||
this.id = id
|
||||
this.title = title
|
||||
this.input = input
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun buildTypes(types: JSArray): Map<String, List<NotificationAction>> {
|
||||
val actionTypeMap: MutableMap<String, List<NotificationAction>> = HashMap()
|
||||
try {
|
||||
val objects: List<JSONObject> = types.toList()
|
||||
for (obj in objects) {
|
||||
val jsObject = JSObject.fromJSONObject(
|
||||
obj
|
||||
)
|
||||
val actionGroupId = jsObject.getString("id")
|
||||
val actions = jsObject.getJSONArray("actions")
|
||||
val typesArray = mutableListOf<NotificationAction>()
|
||||
for (i in 0 until actions.length()) {
|
||||
val notificationAction = NotificationAction()
|
||||
val action = JSObject.fromJSONObject(actions.getJSONObject(i))
|
||||
notificationAction.id = action.getString("id")
|
||||
notificationAction.title = action.getString("title")
|
||||
notificationAction.input = action.getBoolean("input")
|
||||
typesArray.add(notificationAction)
|
||||
}
|
||||
actionTypeMap[actionGroupId] = typesArray.toList()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.error(Logger.tags("Notification"), "Error when building action types", e)
|
||||
}
|
||||
return actionTypeMap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import app.tauri.PermissionState
|
||||
import app.tauri.annotation.Command
|
||||
import app.tauri.annotation.InvokeArg
|
||||
import app.tauri.annotation.Permission
|
||||
import app.tauri.annotation.PermissionCallback
|
||||
import app.tauri.annotation.TauriPlugin
|
||||
@@ -21,11 +22,55 @@ import app.tauri.plugin.Invoke
|
||||
import app.tauri.plugin.JSArray
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.Plugin
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
const val LOCAL_NOTIFICATIONS = "permissionState"
|
||||
|
||||
@InvokeArg
|
||||
class PluginConfig {
|
||||
var icon: String? = null
|
||||
var sound: String? = null
|
||||
var iconColor: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class BatchArgs {
|
||||
lateinit var notifications: List<Notification>
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class CancelArgs {
|
||||
lateinit var notifications: List<Int>
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class NotificationAction {
|
||||
lateinit var id: String
|
||||
var title: String? = null
|
||||
var input: Boolean? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class ActionType {
|
||||
lateinit var id: String
|
||||
lateinit var actions: List<NotificationAction>
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class RegisterActionTypesArgs {
|
||||
lateinit var types: List<ActionType>
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class ActiveNotification {
|
||||
var id: Int = 0
|
||||
var tag: String? = null
|
||||
}
|
||||
|
||||
@InvokeArg
|
||||
class RemoveActiveArgs {
|
||||
var notifications: List<ActiveNotification> = listOf()
|
||||
}
|
||||
|
||||
@TauriPlugin(
|
||||
permissions = [
|
||||
Permission(strings = [Manifest.permission.POST_NOTIFICATIONS], alias = "permissionState")
|
||||
@@ -41,8 +86,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
companion object {
|
||||
var instance: NotificationPlugin? = null
|
||||
|
||||
fun triggerNotification(notification: JSObject) {
|
||||
instance?.trigger("notification", notification)
|
||||
fun triggerNotification(notification: Notification) {
|
||||
instance?.triggerObject("notification", notification)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,23 +96,32 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
|
||||
super.load(webView)
|
||||
this.webView = webView
|
||||
notificationStorage = NotificationStorage(activity)
|
||||
notificationStorage = NotificationStorage(activity, jsonMapper())
|
||||
|
||||
val manager = TauriNotificationManager(
|
||||
notificationStorage,
|
||||
activity,
|
||||
activity,
|
||||
getConfig()
|
||||
getConfig(PluginConfig::class.java)
|
||||
)
|
||||
manager.createNotificationChannel()
|
||||
|
||||
this.manager = manager
|
||||
|
||||
notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val intent = activity.intent
|
||||
intent?.let {
|
||||
onIntent(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
onIntent(intent)
|
||||
}
|
||||
|
||||
fun onIntent(intent: Intent) {
|
||||
if (Intent.ACTION_MAIN != intent.action) {
|
||||
return
|
||||
}
|
||||
@@ -79,80 +133,43 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
|
||||
@Command
|
||||
fun show(invoke: Invoke) {
|
||||
val notification = Notification.fromJSObject(invoke.data)
|
||||
val notification = invoke.parseArgs(Notification::class.java)
|
||||
val id = manager.schedule(notification)
|
||||
|
||||
val returnVal = JSObject().put("id", id)
|
||||
invoke.resolve(returnVal)
|
||||
invoke.resolveObject(id)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun batch(invoke: Invoke) {
|
||||
val notificationArray = invoke.getArray("notifications")
|
||||
if (notificationArray == null) {
|
||||
invoke.reject("Missing `notifications` argument")
|
||||
return
|
||||
}
|
||||
val args = invoke.parseArgs(BatchArgs::class.java)
|
||||
|
||||
val notifications: MutableList<Notification> =
|
||||
ArrayList(notificationArray.length())
|
||||
val notificationsInput: List<JSONObject> = try {
|
||||
notificationArray.toList()
|
||||
} catch (e: JSONException) {
|
||||
invoke.reject("Provided notification format is invalid")
|
||||
return
|
||||
}
|
||||
val ids = manager.schedule(args.notifications)
|
||||
notificationStorage.appendNotifications(args.notifications)
|
||||
|
||||
for (jsonNotification in notificationsInput) {
|
||||
val notification = Notification.fromJson(jsonNotification)
|
||||
notifications.add(notification)
|
||||
}
|
||||
|
||||
val ids = manager.schedule(notifications)
|
||||
notificationStorage.appendNotifications(notifications)
|
||||
|
||||
val result = JSObject()
|
||||
result.put("notifications", ids)
|
||||
invoke.resolve(result)
|
||||
invoke.resolveObject(ids)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun cancel(invoke: Invoke) {
|
||||
val notifications: List<Int> = invoke.getArray("notifications", JSArray()).toList()
|
||||
if (notifications.isEmpty()) {
|
||||
invoke.reject("Must provide notifications array as notifications option")
|
||||
return
|
||||
}
|
||||
|
||||
manager.cancel(notifications)
|
||||
val args = invoke.parseArgs(CancelArgs::class.java)
|
||||
manager.cancel(args.notifications)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@Command
|
||||
fun removeActive(invoke: Invoke) {
|
||||
val notifications = invoke.getArray("notifications")
|
||||
if (notifications == null) {
|
||||
val args = invoke.parseArgs(RemoveActiveArgs::class.java)
|
||||
|
||||
if (args.notifications.isEmpty()) {
|
||||
notificationManager.cancelAll()
|
||||
invoke.resolve()
|
||||
} else {
|
||||
try {
|
||||
for (o in notifications.toList<Any>()) {
|
||||
if (o is JSONObject) {
|
||||
val notification = JSObject.fromJSONObject((o))
|
||||
val tag = notification.getString("tag", null)
|
||||
val id = notification.getInteger("id", 0)
|
||||
if (tag == null) {
|
||||
notificationManager.cancel(id)
|
||||
} else {
|
||||
notificationManager.cancel(tag, id)
|
||||
}
|
||||
} else {
|
||||
invoke.reject("Unexpected notification type")
|
||||
return
|
||||
}
|
||||
for (notification in args.notifications) {
|
||||
if (notification.tag == null) {
|
||||
notificationManager.cancel(notification.id)
|
||||
} else {
|
||||
notificationManager.cancel(notification.tag, notification.id)
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
invoke.reject(e.message)
|
||||
}
|
||||
invoke.resolve()
|
||||
}
|
||||
@@ -162,14 +179,13 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
fun getPending(invoke: Invoke) {
|
||||
val notifications= notificationStorage.getSavedNotifications()
|
||||
val result = Notification.buildNotificationPendingList(notifications)
|
||||
invoke.resolve(result)
|
||||
invoke.resolveObject(result)
|
||||
}
|
||||
|
||||
@Command
|
||||
fun registerActionTypes(invoke: Invoke) {
|
||||
val types = invoke.getArray("types", JSArray())
|
||||
val typesArray = NotificationAction.buildTypes(types)
|
||||
notificationStorage.writeActionGroup(typesArray)
|
||||
val args = invoke.parseArgs(RegisterActionTypesArgs::class.java)
|
||||
notificationStorage.writeActionGroup(args.types)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@@ -201,9 +217,8 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
|
||||
notifications.put(jsNotification)
|
||||
}
|
||||
}
|
||||
val result = JSObject()
|
||||
result.put("notifications", notifications)
|
||||
invoke.resolve(result)
|
||||
|
||||
invoke.resolveObject(notifications)
|
||||
}
|
||||
|
||||
@Command
|
||||
|
||||
@@ -5,13 +5,26 @@
|
||||
package app.tauri.notification
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ClipData.Item
|
||||
import android.text.format.DateUtils
|
||||
import app.tauri.plugin.JSObject
|
||||
import com.fasterxml.jackson.annotation.JsonFormat
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.SerializerProvider
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
|
||||
const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
|
||||
enum class NotificationInterval {
|
||||
@@ -33,71 +46,89 @@ fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ScheduleKind {
|
||||
@JsonDeserialize(using = NotificationScheduleDeserializer::class)
|
||||
@JsonSerialize(using = NotificationScheduleSerializer::class)
|
||||
sealed class NotificationSchedule {
|
||||
// At specific moment of time (with repeating option)
|
||||
class At(var date: Date, val repeating: Boolean): ScheduleKind()
|
||||
class Interval(val interval: DateMatch): ScheduleKind()
|
||||
class Every(val interval: NotificationInterval, val count: Int): ScheduleKind()
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
class NotificationSchedule(val scheduleObj: JSObject) {
|
||||
val kind: ScheduleKind
|
||||
// Schedule this notification to fire even if app is idled (Doze)
|
||||
var whileIdle: Boolean = false
|
||||
|
||||
init {
|
||||
val payload = scheduleObj.getJSObject("data", JSObject())
|
||||
|
||||
when (val scheduleKind = scheduleObj.getString("kind", "")) {
|
||||
"At" -> {
|
||||
val dateString = payload.getString("date")
|
||||
if (dateString.isNotEmpty()) {
|
||||
val sdf = SimpleDateFormat(JS_DATE_FORMAT)
|
||||
sdf.timeZone = TimeZone.getTimeZone("UTC")
|
||||
val at = sdf.parse(dateString)
|
||||
if (at == null) {
|
||||
throw Exception("could not parse `at` date")
|
||||
} else {
|
||||
kind = ScheduleKind.At(at, payload.getBoolean("repeating"))
|
||||
}
|
||||
} else {
|
||||
throw Exception("`at` date cannot be empty")
|
||||
}
|
||||
}
|
||||
"Interval" -> {
|
||||
val dateMatch = onFromJson(payload)
|
||||
kind = ScheduleKind.Interval(dateMatch)
|
||||
}
|
||||
"Every" -> {
|
||||
val interval = NotificationInterval.valueOf(payload.getString("interval"))
|
||||
kind = ScheduleKind.Every(interval, payload.getInteger("count", 1))
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Unknown schedule kind $scheduleKind")
|
||||
}
|
||||
}
|
||||
whileIdle = scheduleObj.getBoolean("allowWhileIdle", false)
|
||||
}
|
||||
|
||||
private fun onFromJson(onJson: JSObject): DateMatch {
|
||||
val match = DateMatch()
|
||||
match.year = onJson.getInteger("year")
|
||||
match.month = onJson.getInteger("month")
|
||||
match.day = onJson.getInteger("day")
|
||||
match.weekday = onJson.getInteger("weekday")
|
||||
match.hour = onJson.getInteger("hour")
|
||||
match.minute = onJson.getInteger("minute")
|
||||
match.second = onJson.getInteger("second")
|
||||
return match
|
||||
}
|
||||
class At(@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT) var date: Date, val repeating: Boolean = false, val allowWhileIdle: Boolean = false): NotificationSchedule()
|
||||
class Interval(val interval: DateMatch, val allowWhileIdle: Boolean = false): NotificationSchedule()
|
||||
class Every(val interval: NotificationInterval, val count: Int = 0, val allowWhileIdle: Boolean = false): NotificationSchedule()
|
||||
|
||||
fun isRemovable(): Boolean {
|
||||
return when (kind) {
|
||||
is ScheduleKind.At -> !kind.repeating
|
||||
return when (this) {
|
||||
is At -> !repeating
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun allowWhileIdle(): Boolean {
|
||||
return when (this) {
|
||||
is At -> allowWhileIdle
|
||||
is Interval -> allowWhileIdle
|
||||
is Every -> allowWhileIdle
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class NotificationScheduleSerializer @JvmOverloads constructor(t: Class<NotificationSchedule>? = null) :
|
||||
StdSerializer<NotificationSchedule>(t) {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
@Throws(IOException::class, JsonProcessingException::class)
|
||||
override fun serialize(
|
||||
value: NotificationSchedule, jgen: JsonGenerator, provider: SerializerProvider
|
||||
) {
|
||||
jgen.writeStartObject()
|
||||
when (value) {
|
||||
is NotificationSchedule.At -> {
|
||||
jgen.writeObjectFieldStart("at")
|
||||
|
||||
val sdf = SimpleDateFormat(JS_DATE_FORMAT)
|
||||
sdf.timeZone = TimeZone.getTimeZone("UTC")
|
||||
jgen.writeStringField("date", sdf.format(value.date))
|
||||
jgen.writeBooleanField("repeating", value.repeating)
|
||||
|
||||
jgen.writeEndObject()
|
||||
}
|
||||
is NotificationSchedule.Interval -> {
|
||||
jgen.writeObjectFieldStart("interval")
|
||||
|
||||
jgen.writeObjectField("interval", value.interval)
|
||||
|
||||
jgen.writeEndObject()
|
||||
}
|
||||
is NotificationSchedule.Every -> {
|
||||
jgen.writeObjectFieldStart("every")
|
||||
|
||||
jgen.writeObjectField("interval", value.interval)
|
||||
jgen.writeNumberField("count", value.count)
|
||||
|
||||
jgen.writeEndObject()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
jgen.writeEndObject()
|
||||
}
|
||||
}
|
||||
|
||||
internal class NotificationScheduleDeserializer: JsonDeserializer<NotificationSchedule>() {
|
||||
override fun deserialize(
|
||||
jsonParser: JsonParser,
|
||||
deserializationContext: DeserializationContext
|
||||
): NotificationSchedule {
|
||||
val node: JsonNode = jsonParser.codec.readTree(jsonParser)
|
||||
node.get("at")?.let {
|
||||
return jsonParser.codec.treeToValue(it, NotificationSchedule.At::class.java)
|
||||
}
|
||||
node.get("interval")?.let {
|
||||
return jsonParser.codec.treeToValue(it, NotificationSchedule.Interval::class.java)
|
||||
}
|
||||
node.get("every")?.let {
|
||||
return jsonParser.codec.treeToValue(it, NotificationSchedule.Every::class.java)
|
||||
}
|
||||
throw Error("unknown schedule kind $node")
|
||||
}
|
||||
}
|
||||
|
||||
class DateMatch {
|
||||
|
||||
@@ -6,23 +6,23 @@ package app.tauri.notification
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import app.tauri.plugin.JSObject
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.json.JSONException
|
||||
import java.text.ParseException
|
||||
import java.lang.Exception
|
||||
|
||||
// Key for private preferences
|
||||
private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE"
|
||||
// Key used to save action types
|
||||
private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE"
|
||||
|
||||
class NotificationStorage(private val context: Context) {
|
||||
class NotificationStorage(private val context: Context, private val jsonMapper: ObjectMapper) {
|
||||
fun appendNotifications(localNotifications: List<Notification>) {
|
||||
val storage = getStorage(NOTIFICATION_STORE_ID)
|
||||
val editor = storage.edit()
|
||||
for (request in localNotifications) {
|
||||
if (request.isScheduled) {
|
||||
val key: String = request.id.toString()
|
||||
editor.putString(key, request.source.toString())
|
||||
editor.putString(key, request.sourceJson.toString())
|
||||
}
|
||||
}
|
||||
editor.apply()
|
||||
@@ -43,57 +43,29 @@ class NotificationStorage(private val context: Context) {
|
||||
val notifications = ArrayList<Notification>()
|
||||
for (key in all.keys) {
|
||||
val notificationString = all[key] as String?
|
||||
val jsNotification = getNotificationFromJSONString(notificationString)
|
||||
if (jsNotification != null) {
|
||||
try {
|
||||
val notification =
|
||||
Notification.fromJSObject(jsNotification)
|
||||
notifications.add(notification)
|
||||
} catch (_: ParseException) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
val notification = jsonMapper.readValue(notificationString, Notification::class.java)
|
||||
notifications.add(notification)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
return notifications
|
||||
}
|
||||
return ArrayList()
|
||||
}
|
||||
|
||||
private fun getNotificationFromJSONString(notificationString: String?): JSObject? {
|
||||
if (notificationString == null) {
|
||||
return null
|
||||
}
|
||||
val jsNotification = try {
|
||||
JSObject(notificationString)
|
||||
} catch (ex: JSONException) {
|
||||
return null
|
||||
}
|
||||
return jsNotification
|
||||
}
|
||||
|
||||
fun getSavedNotificationAsJSObject(key: String?): JSObject? {
|
||||
fun getSavedNotification(key: String): Notification? {
|
||||
val storage = getStorage(NOTIFICATION_STORE_ID)
|
||||
val notificationString = try {
|
||||
storage.getString(key, null)
|
||||
} catch (ex: ClassCastException) {
|
||||
return null
|
||||
} ?: return null
|
||||
|
||||
val jsNotification = try {
|
||||
JSObject(notificationString)
|
||||
} catch (ex: JSONException) {
|
||||
return null
|
||||
}
|
||||
return jsNotification
|
||||
}
|
||||
|
||||
fun getSavedNotification(key: String?): Notification? {
|
||||
val jsNotification = getSavedNotificationAsJSObject(key) ?: return null
|
||||
val notification = try {
|
||||
Notification.fromJSObject(jsNotification)
|
||||
} catch (ex: ParseException) {
|
||||
return null
|
||||
return try {
|
||||
jsonMapper.readValue(notificationString, Notification::class.java)
|
||||
} catch (ex: JSONException) {
|
||||
null
|
||||
}
|
||||
return notification
|
||||
}
|
||||
|
||||
fun deleteNotification(id: String?) {
|
||||
@@ -106,15 +78,16 @@ class NotificationStorage(private val context: Context) {
|
||||
return context.getSharedPreferences(key, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
fun writeActionGroup(typesMap: Map<String, List<NotificationAction>>) {
|
||||
for ((id, notificationActions) in typesMap) {
|
||||
val editor = getStorage(ACTION_TYPES_ID + id).edit()
|
||||
fun writeActionGroup(actions: List<ActionType>) {
|
||||
for (type in actions) {
|
||||
val i = type.id
|
||||
val editor = getStorage(ACTION_TYPES_ID + type.id).edit()
|
||||
editor.clear()
|
||||
editor.putInt("count", notificationActions.size)
|
||||
for (i in notificationActions.indices) {
|
||||
editor.putString("id$i", notificationActions[i].id)
|
||||
editor.putString("title$i", notificationActions[i].title)
|
||||
editor.putBoolean("input$i", notificationActions[i].input)
|
||||
editor.putInt("count", type.actions.size)
|
||||
for (action in type.actions) {
|
||||
editor.putString("id$i", action.id)
|
||||
editor.putString("title$i", action.title)
|
||||
editor.putBoolean("input$i", action.input ?: false)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
@@ -128,7 +101,12 @@ class NotificationStorage(private val context: Context) {
|
||||
val id = storage.getString("id$i", "")
|
||||
val title = storage.getString("title$i", "")
|
||||
val input = storage.getBoolean("input$i", false)
|
||||
actions[i] = NotificationAction(id, title, input)
|
||||
|
||||
val action = NotificationAction()
|
||||
action.id = id ?: ""
|
||||
action.title = title
|
||||
action.input = input
|
||||
actions[i] = action
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.core.app.RemoteInput
|
||||
import app.tauri.Logger
|
||||
import app.tauri.plugin.JSObject
|
||||
import app.tauri.plugin.PluginManager
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -44,7 +45,7 @@ class TauriNotificationManager(
|
||||
private val storage: NotificationStorage,
|
||||
private val activity: Activity?,
|
||||
private val context: Context,
|
||||
private val config: JSObject
|
||||
private val config: PluginConfig?
|
||||
) {
|
||||
private var defaultSoundID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
private var defaultSmallIconID: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
@@ -200,7 +201,7 @@ class TauriNotificationManager(
|
||||
mBuilder.setOnlyAlertOnce(true)
|
||||
mBuilder.setSmallIcon(notification.getSmallIcon(context, getDefaultSmallIcon(context)))
|
||||
mBuilder.setLargeIcon(notification.getLargeIcon(context))
|
||||
val iconColor = notification.getIconColor(config.getString("iconColor"))
|
||||
val iconColor = notification.getIconColor(config?.iconColor ?: "")
|
||||
if (iconColor.isNotEmpty()) {
|
||||
try {
|
||||
mBuilder.color = Color.parseColor(iconColor)
|
||||
@@ -216,7 +217,7 @@ class TauriNotificationManager(
|
||||
} else {
|
||||
notificationManager.notify(notification.id, buildNotification)
|
||||
try {
|
||||
NotificationPlugin.triggerNotification(notification.source ?: JSObject())
|
||||
NotificationPlugin.triggerNotification(notification)
|
||||
} catch (_: JSONException) {
|
||||
}
|
||||
}
|
||||
@@ -254,7 +255,7 @@ class TauriNotificationManager(
|
||||
notificationAction.title,
|
||||
actionPendingIntent
|
||||
)
|
||||
if (notificationAction.input) {
|
||||
if (notificationAction.input == true) {
|
||||
val remoteInput = RemoteInput.Builder(REMOTE_INPUT_KEY).setLabel(
|
||||
notificationAction.title
|
||||
).build()
|
||||
@@ -298,7 +299,7 @@ class TauriNotificationManager(
|
||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
intent.putExtra(NOTIFICATION_INTENT_KEY, notification.id)
|
||||
intent.putExtra(ACTION_INTENT_KEY, action)
|
||||
intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.source.toString())
|
||||
intent.putExtra(NOTIFICATION_OBJ_INTENT_KEY, notification.sourceJson)
|
||||
val schedule = notification.schedule
|
||||
intent.putExtra(NOTIFICATION_IS_REMOVABLE_KEY, schedule == null || schedule.isRemovable())
|
||||
return intent
|
||||
@@ -326,23 +327,22 @@ class TauriNotificationManager(
|
||||
var pendingIntent =
|
||||
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
|
||||
|
||||
when (val scheduleKind = schedule?.kind) {
|
||||
is ScheduleKind.At -> {
|
||||
val at = scheduleKind.date
|
||||
if (at.time < Date().time) {
|
||||
when (schedule) {
|
||||
is NotificationSchedule.At -> {
|
||||
if (schedule.date.time < Date().time) {
|
||||
Logger.error(Logger.tags("Notification"), "Scheduled time must be *after* current time", null)
|
||||
return
|
||||
}
|
||||
if (scheduleKind.repeating) {
|
||||
val interval: Long = at.time - Date().time
|
||||
alarmManager.setRepeating(AlarmManager.RTC, at.time, interval, pendingIntent)
|
||||
if (schedule.repeating) {
|
||||
val interval: Long = schedule.date.time - Date().time
|
||||
alarmManager.setRepeating(AlarmManager.RTC, schedule.date.time, interval, pendingIntent)
|
||||
} else {
|
||||
setExactIfPossible(alarmManager, schedule, at.time, pendingIntent)
|
||||
setExactIfPossible(alarmManager, schedule, schedule.date.time, pendingIntent)
|
||||
}
|
||||
}
|
||||
is ScheduleKind.Interval -> {
|
||||
val trigger = scheduleKind.interval.nextTrigger(Date())
|
||||
notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, scheduleKind.interval.toMatchString())
|
||||
is NotificationSchedule.Interval -> {
|
||||
val trigger = schedule.interval.nextTrigger(Date())
|
||||
notificationIntent.putExtra(TimedNotificationPublisher.CRON_KEY, schedule.interval.toMatchString())
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(context, request.id, notificationIntent, flags)
|
||||
setExactIfPossible(alarmManager, schedule, trigger, pendingIntent)
|
||||
@@ -352,8 +352,8 @@ class TauriNotificationManager(
|
||||
"notification " + request.id + " will next fire at " + sdf.format(Date(trigger))
|
||||
)
|
||||
}
|
||||
is ScheduleKind.Every -> {
|
||||
val everyInterval = getIntervalTime(scheduleKind.interval, scheduleKind.count)
|
||||
is NotificationSchedule.Every -> {
|
||||
val everyInterval = getIntervalTime(schedule.interval, schedule.count)
|
||||
val startTime: Long = Date().time + everyInterval
|
||||
alarmManager.setRepeating(AlarmManager.RTC, startTime, everyInterval, pendingIntent)
|
||||
}
|
||||
@@ -369,13 +369,13 @@ class TauriNotificationManager(
|
||||
pendingIntent: PendingIntent
|
||||
) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
|
||||
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
|
||||
} else {
|
||||
alarmManager[AlarmManager.RTC, trigger] = pendingIntent
|
||||
}
|
||||
} else {
|
||||
if (SDK_INT >= Build.VERSION_CODES.M && schedule.whileIdle) {
|
||||
if (SDK_INT >= Build.VERSION_CODES.M && schedule.allowWhileIdle()) {
|
||||
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pendingIntent)
|
||||
} else {
|
||||
alarmManager.setExact(AlarmManager.RTC, trigger, pendingIntent)
|
||||
@@ -426,7 +426,7 @@ class TauriNotificationManager(
|
||||
private fun getDefaultSound(context: Context): Int {
|
||||
if (defaultSoundID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSoundID
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
val soundConfigResourceName = AssetUtils.getResourceBaseName(config.getString("sound"))
|
||||
val soundConfigResourceName = AssetUtils.getResourceBaseName(config?.sound)
|
||||
if (soundConfigResourceName != null) {
|
||||
resId = AssetUtils.getResourceID(context, soundConfigResourceName, "raw")
|
||||
}
|
||||
@@ -437,7 +437,7 @@ class TauriNotificationManager(
|
||||
private fun getDefaultSmallIcon(context: Context): Int {
|
||||
if (defaultSmallIconID != AssetUtils.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID
|
||||
var resId: Int = AssetUtils.RESOURCE_ID_ZERO_VALUE
|
||||
val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config.getString("icon"))
|
||||
val smallIconConfigResourceName = AssetUtils.getResourceBaseName(config?.icon)
|
||||
if (smallIconConfigResourceName != null) {
|
||||
resId = AssetUtils.getResourceID(context, smallIconConfigResourceName, "drawable")
|
||||
}
|
||||
@@ -460,7 +460,7 @@ class NotificationDismissReceiver : BroadcastReceiver() {
|
||||
val isRemovable =
|
||||
intent.getBooleanExtra(NOTIFICATION_IS_REMOVABLE_KEY, true)
|
||||
if (isRemovable) {
|
||||
val notificationStorage = NotificationStorage(context)
|
||||
val notificationStorage = NotificationStorage(context, ObjectMapper())
|
||||
notificationStorage.deleteNotification(intExtra.toString())
|
||||
}
|
||||
}
|
||||
@@ -486,11 +486,13 @@ class TimedNotificationPublisher : BroadcastReceiver() {
|
||||
if (id == Int.MIN_VALUE) {
|
||||
Logger.error(Logger.tags("Notification"), "No valid id supplied", null)
|
||||
}
|
||||
val storage = NotificationStorage(context)
|
||||
val notificationJson = storage.getSavedNotificationAsJSObject(id.toString())
|
||||
if (notificationJson != null) {
|
||||
NotificationPlugin.triggerNotification(notificationJson)
|
||||
val storage = NotificationStorage(context, ObjectMapper())
|
||||
|
||||
val savedNotification = storage.getSavedNotification(id.toString())
|
||||
if (savedNotification != null) {
|
||||
NotificationPlugin.triggerNotification(savedNotification)
|
||||
}
|
||||
|
||||
notificationManager.notify(id, notification)
|
||||
if (!rescheduleNotificationIfNeeded(context, intent, id)) {
|
||||
storage.deleteNotification(id.toString())
|
||||
@@ -545,19 +547,19 @@ class LocalNotificationRestoreReceiver : BroadcastReceiver() {
|
||||
)
|
||||
if (um == null || !um.isUserUnlocked) return
|
||||
}
|
||||
val storage = NotificationStorage(context)
|
||||
val storage = NotificationStorage(context, ObjectMapper())
|
||||
val ids = storage.getSavedNotificationIds()
|
||||
val notifications = mutableListOf<Notification>()
|
||||
val updatedNotifications = mutableListOf<Notification>()
|
||||
for (id in ids) {
|
||||
val notification = storage.getSavedNotification(id) ?: continue
|
||||
val schedule = notification.schedule
|
||||
if (schedule != null && schedule.kind is ScheduleKind.At) {
|
||||
val at: Date = schedule.kind.date
|
||||
if (schedule != null && schedule is NotificationSchedule.At) {
|
||||
val at: Date = schedule.date
|
||||
if (at.before(Date())) {
|
||||
// modify the scheduled date in order to show notifications that would have been delivered while device was off.
|
||||
val newDateTime = Date().time + 15 * 1000
|
||||
schedule.kind.date = Date(newDateTime)
|
||||
schedule.date = Date(newDateTime)
|
||||
updatedNotifications.add(notification)
|
||||
}
|
||||
}
|
||||
@@ -567,7 +569,13 @@ class LocalNotificationRestoreReceiver : BroadcastReceiver() {
|
||||
storage.appendNotifications(updatedNotifications)
|
||||
}
|
||||
|
||||
val notificationManager = TauriNotificationManager(storage, null, context, PluginManager.loadConfig(context, "notification"))
|
||||
var config: PluginConfig? = null
|
||||
try {
|
||||
config = PluginManager.loadConfig(context, "notification", PluginConfig::class.java)
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
val notificationManager = TauriNotificationManager(storage, null, context, config)
|
||||
notificationManager.schedule(notifications)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ fn main() {
|
||||
{
|
||||
println!("{error:#}");
|
||||
// when building documentation for Android the plugin build result is irrelevant to the crate itself
|
||||
if !(cfg!(feature = "dox") && std::env::var("TARGET").unwrap().contains("android")) {
|
||||
if !(cfg!(docsrs) && std::env::var("TARGET").unwrap().contains("android")) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,57 +150,62 @@ type ScheduleInterval = {
|
||||
};
|
||||
|
||||
enum ScheduleEvery {
|
||||
Year = "Year",
|
||||
Month = "Month",
|
||||
TwoWeeks = "TwoWeeks",
|
||||
Week = "Week",
|
||||
Day = "Day",
|
||||
Hour = "Hour",
|
||||
Minute = "Minute",
|
||||
Year = "year",
|
||||
Month = "month",
|
||||
TwoWeeks = "twoWeeks",
|
||||
Week = "week",
|
||||
Day = "day",
|
||||
Hour = "hour",
|
||||
Minute = "minute",
|
||||
/**
|
||||
* Not supported on iOS.
|
||||
*/
|
||||
Second = "Second",
|
||||
Second = "second",
|
||||
}
|
||||
|
||||
type ScheduleData =
|
||||
| {
|
||||
kind: "At";
|
||||
data: {
|
||||
at: {
|
||||
date: Date;
|
||||
repeating: boolean;
|
||||
allowWhileIdle: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
kind: "Interval";
|
||||
data: ScheduleInterval;
|
||||
interval: {
|
||||
interval: ScheduleInterval;
|
||||
allowWhileIdle: boolean;
|
||||
};
|
||||
}
|
||||
| {
|
||||
kind: "Every";
|
||||
data: {
|
||||
every: {
|
||||
interval: ScheduleEvery;
|
||||
count: number;
|
||||
allowWhileIdle: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
class Schedule {
|
||||
kind: string;
|
||||
data: unknown;
|
||||
schedule: ScheduleData;
|
||||
|
||||
private constructor(schedule: ScheduleData) {
|
||||
this.kind = schedule.kind;
|
||||
this.data = schedule.data;
|
||||
this.schedule = schedule;
|
||||
}
|
||||
|
||||
static at(date: Date, repeating = false) {
|
||||
return new Schedule({ kind: "At", data: { date, repeating } });
|
||||
toJSON(): string {
|
||||
return JSON.stringify(this.schedule);
|
||||
}
|
||||
|
||||
static interval(interval: ScheduleInterval) {
|
||||
return new Schedule({ kind: "Interval", data: interval });
|
||||
static at(date: Date, repeating = false, allowWhileIdle = false) {
|
||||
return new Schedule({ at: { date, repeating, allowWhileIdle } });
|
||||
}
|
||||
|
||||
static every(kind: ScheduleEvery) {
|
||||
return new Schedule({ kind: "Every", data: { interval: kind } });
|
||||
static interval(interval: ScheduleInterval, allowWhileIdle = false) {
|
||||
return new Schedule({ interval: { interval, allowWhileIdle } });
|
||||
}
|
||||
|
||||
static every(kind: ScheduleEvery, count: number, allowWhileIdle = false) {
|
||||
return new Schedule({ every: { interval: kind, count, allowWhileIdle } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +466,9 @@ async function active(): Promise<ActiveNotification[]> {
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function removeActive(notifications: number[]): Promise<void> {
|
||||
async function removeActive(
|
||||
notifications: { id: number; tag?: string }[],
|
||||
): Promise<void> {
|
||||
return invoke("plugin:notification|remove_active", { notifications });
|
||||
}
|
||||
|
||||
@@ -483,7 +490,7 @@ async function removeAllActive(): Promise<void> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all active notifications.
|
||||
* Creates a notification channel.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -537,7 +544,7 @@ async function removeChannel(id: string): Promise<void> {
|
||||
* @since 2.0.0
|
||||
*/
|
||||
async function channels(): Promise<Channel[]> {
|
||||
return invoke("plugin:notification|getActive");
|
||||
return invoke("plugin:notification|listChannels");
|
||||
}
|
||||
|
||||
async function onNotificationReceived(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// swift-tools-version:5.3
|
||||
// swift-tools-version:5.5
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@@ -6,12 +6,7 @@ import Tauri
|
||||
import UserNotifications
|
||||
|
||||
enum NotificationError: LocalizedError {
|
||||
case contentNoId
|
||||
case contentNoTitle
|
||||
case contentNoBody
|
||||
case triggerRepeatIntervalTooShort
|
||||
case attachmentNoId
|
||||
case attachmentNoUrl
|
||||
case attachmentFileNotFound(path: String)
|
||||
case attachmentUnableToCreate(String)
|
||||
case pastScheduledTime
|
||||
@@ -19,18 +14,8 @@ enum NotificationError: LocalizedError {
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .contentNoId:
|
||||
return "Missing notification identifier"
|
||||
case .contentNoTitle:
|
||||
return "Missing notification title"
|
||||
case .contentNoBody:
|
||||
return "Missing notification body"
|
||||
case .triggerRepeatIntervalTooShort:
|
||||
return "Schedule interval too short, must be a least 1 minute"
|
||||
case .attachmentNoId:
|
||||
return "Missing attachment identifier"
|
||||
case .attachmentNoUrl:
|
||||
return "Missing attachment URL"
|
||||
case .attachmentFileNotFound(let path):
|
||||
return "Unable to find file \(path) for attachment"
|
||||
case .attachmentUnableToCreate(let error):
|
||||
@@ -43,69 +28,56 @@ enum NotificationError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
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 ?? [:]
|
||||
func makeNotificationContent(_ notification: Notification) throws -> UNNotificationContent {
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSString.localizedUserNotificationString(forKey: title, arguments: nil)
|
||||
content.title = NSString.localizedUserNotificationString(
|
||||
forKey: notification.title, arguments: nil)
|
||||
content.body = NSString.localizedUserNotificationString(
|
||||
forKey: body,
|
||||
forKey: notification.body,
|
||||
arguments: nil)
|
||||
|
||||
content.userInfo = [
|
||||
"__EXTRA__": extra,
|
||||
"__SCHEDULE__": schedule,
|
||||
"__EXTRA__": notification.extra as Any,
|
||||
"__SCHEDULE__": notification.schedule as Any,
|
||||
]
|
||||
|
||||
if let actionTypeId = notification["actionTypeId"] as? String {
|
||||
if let actionTypeId = notification.actionTypeId {
|
||||
content.categoryIdentifier = actionTypeId
|
||||
}
|
||||
|
||||
if let threadIdentifier = notification["group"] as? String {
|
||||
if let threadIdentifier = notification.group {
|
||||
content.threadIdentifier = threadIdentifier
|
||||
}
|
||||
|
||||
if let summaryArgument = notification["summary"] as? String {
|
||||
if let summaryArgument = notification.summary {
|
||||
content.summaryArgument = summaryArgument
|
||||
}
|
||||
|
||||
if let sound = notification["sound"] as? String {
|
||||
if let sound = notification.sound {
|
||||
content.sound = UNNotificationSound(named: UNNotificationSoundName(sound))
|
||||
}
|
||||
|
||||
if let attachments = notification["attachments"] as? [JSObject] {
|
||||
if let attachments = notification.attachments {
|
||||
content.attachments = try makeAttachments(attachments)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
func makeAttachments(_ attachments: [JSObject]) throws -> [UNNotificationAttachment] {
|
||||
func makeAttachments(_ attachments: [NotificationAttachment]) 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)
|
||||
|
||||
guard let urlObject = makeAttachmentUrl(attachment.url) else {
|
||||
throw NotificationError.attachmentFileNotFound(path: attachment.url)
|
||||
}
|
||||
|
||||
let options = attachment["options"] as? JSObject ?? [:]
|
||||
let options = attachment.options != nil ? makeAttachmentOptions(attachment.options!) : nil
|
||||
|
||||
do {
|
||||
let newAttachment = try UNNotificationAttachment(
|
||||
identifier: id, url: urlObject, options: makeAttachmentOptions(options))
|
||||
identifier: attachment.id, url: urlObject, options: options)
|
||||
createdAttachments.append(newAttachment)
|
||||
} catch {
|
||||
throw NotificationError.attachmentUnableToCreate(error.localizedDescription)
|
||||
@@ -119,50 +91,37 @@ func makeAttachmentUrl(_ path: String) -> URL? {
|
||||
return URL(string: path)
|
||||
}
|
||||
|
||||
func makeAttachmentOptions(_ options: JSObject) -> JSObject {
|
||||
var opts: JSObject = [:]
|
||||
func makeAttachmentOptions(_ options: NotificationAttachmentOptions) -> [AnyHashable: Any] {
|
||||
var opts: [AnyHashable: Any] = [:]
|
||||
|
||||
if let iosUNNotificationAttachmentOptionsTypeHintKey = options[
|
||||
"iosUNNotificationAttachmentOptionsTypeHintKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsTypeHintKey] = iosUNNotificationAttachmentOptionsTypeHintKey
|
||||
if let value = options.iosUNNotificationAttachmentOptionsTypeHintKey {
|
||||
opts[UNNotificationAttachmentOptionsTypeHintKey] = value
|
||||
}
|
||||
if let iosUNNotificationAttachmentOptionsThumbnailHiddenKey = options[
|
||||
"iosUNNotificationAttachmentOptionsThumbnailHiddenKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] =
|
||||
iosUNNotificationAttachmentOptionsThumbnailHiddenKey
|
||||
if let value = options.iosUNNotificationAttachmentOptionsThumbnailHiddenKey {
|
||||
opts[UNNotificationAttachmentOptionsThumbnailHiddenKey] = value
|
||||
}
|
||||
if let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey = options[
|
||||
"iosUNNotificationAttachmentOptionsThumbnailClippingRectKey"] as? String
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] =
|
||||
iosUNNotificationAttachmentOptionsThumbnailClippingRectKey
|
||||
if let value = options.iosUNNotificationAttachmentOptionsThumbnailClippingRectKey {
|
||||
opts[UNNotificationAttachmentOptionsThumbnailClippingRectKey] = value
|
||||
}
|
||||
if let iosUNNotificationAttachmentOptionsThumbnailTimeKey = options[
|
||||
"iosUNNotificationAttachmentOptionsThumbnailTimeKey"] as? String
|
||||
if let value = options
|
||||
.iosUNNotificationAttachmentOptionsThumbnailTimeKey
|
||||
|
||||
{
|
||||
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] =
|
||||
iosUNNotificationAttachmentOptionsThumbnailTimeKey
|
||||
opts[UNNotificationAttachmentOptionsThumbnailTimeKey] = value
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func handleScheduledNotification(_ schedule: JSObject) throws
|
||||
func handleScheduledNotification(_ schedule: NotificationSchedule) throws
|
||||
-> UNNotificationTrigger?
|
||||
{
|
||||
let kind = schedule["kind"] as? String ?? ""
|
||||
let payload = schedule["data"] as? JSObject ?? [:]
|
||||
switch kind {
|
||||
case "At":
|
||||
let date = payload["date"] as? String ?? ""
|
||||
switch schedule {
|
||||
case .at(let date, let repeating):
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
|
||||
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() {
|
||||
@@ -172,23 +131,20 @@ func handleScheduledNotification(_ schedule: JSObject) throws
|
||||
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 {
|
||||
if repeating && dateInterval.duration < 60 {
|
||||
throw NotificationError.triggerRepeatIntervalTooShort
|
||||
}
|
||||
|
||||
return UNTimeIntervalNotificationTrigger(
|
||||
timeInterval: dateInterval.duration, repeats: repeats)
|
||||
timeInterval: dateInterval.duration, repeats: repeating)
|
||||
|
||||
} else {
|
||||
throw NotificationError.invalidDate(date)
|
||||
}
|
||||
case "Interval":
|
||||
let dateComponents = getDateComponents(payload)
|
||||
case .interval(let interval):
|
||||
let dateComponents = getDateComponents(interval)
|
||||
return UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
|
||||
case "Every":
|
||||
let interval = payload["interval"] as? String ?? ""
|
||||
let count = schedule["count"] as? Int ?? 1
|
||||
|
||||
case .every(let interval, let count):
|
||||
if let repeatDateInterval = getRepeatDateInterval(interval, count) {
|
||||
// Notifications that repeat have to be at least a minute between each other
|
||||
if repeatDateInterval.duration < 60 {
|
||||
@@ -198,9 +154,6 @@ func handleScheduledNotification(_ schedule: JSObject) throws
|
||||
return UNTimeIntervalNotificationTrigger(
|
||||
timeInterval: repeatDateInterval.duration, repeats: true)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -209,30 +162,30 @@ func handleScheduledNotification(_ schedule: JSObject) throws
|
||||
/// Given our schedule format, return a DateComponents object
|
||||
/// that only contains the components passed in.
|
||||
|
||||
func getDateComponents(_ at: JSObject) -> DateComponents {
|
||||
func getDateComponents(_ at: ScheduleInterval) -> 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 {
|
||||
if let year = at.year {
|
||||
dateInfo.year = year
|
||||
}
|
||||
if let month = at["month"] as? Int {
|
||||
if let month = at.month {
|
||||
dateInfo.month = month
|
||||
}
|
||||
if let day = at["day"] as? Int {
|
||||
if let day = at.day {
|
||||
dateInfo.day = day
|
||||
}
|
||||
if let hour = at["hour"] as? Int {
|
||||
if let hour = at.hour {
|
||||
dateInfo.hour = hour
|
||||
}
|
||||
if let minute = at["minute"] as? Int {
|
||||
if let minute = at.minute {
|
||||
dateInfo.minute = minute
|
||||
}
|
||||
if let second = at["second"] as? Int {
|
||||
if let second = at.second {
|
||||
dateInfo.second = second
|
||||
}
|
||||
if let weekday = at["weekday"] as? Int {
|
||||
if let weekday = at.weekday {
|
||||
dateInfo.weekday = weekday
|
||||
}
|
||||
return dateInfo
|
||||
@@ -242,35 +195,33 @@ func getDateComponents(_ at: JSObject) -> DateComponents {
|
||||
/// 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? {
|
||||
func getRepeatDateInterval(_ every: ScheduleEveryKind, _ count: Int) -> DateInterval? {
|
||||
let cal = Calendar.current
|
||||
let now = Date()
|
||||
switch every {
|
||||
case "Year":
|
||||
case .year:
|
||||
let newDate = cal.date(byAdding: .year, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Month":
|
||||
case .month:
|
||||
let newDate = cal.date(byAdding: .month, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "TwoWeeks":
|
||||
case .twoWeeks:
|
||||
let newDate = cal.date(byAdding: .weekOfYear, value: 2 * count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Week":
|
||||
case .week:
|
||||
let newDate = cal.date(byAdding: .weekOfYear, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Day":
|
||||
case .day:
|
||||
let newDate = cal.date(byAdding: .day, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Hour":
|
||||
case .hour:
|
||||
let newDate = cal.date(byAdding: .hour, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Minute":
|
||||
case .minute:
|
||||
let newDate = cal.date(byAdding: .minute, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
case "Second":
|
||||
case .second:
|
||||
let newDate = cal.date(byAdding: .second, value: count, to: now)!
|
||||
return DateInterval(start: now, end: newDate)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,21 +5,7 @@
|
||||
import Tauri
|
||||
import UserNotifications
|
||||
|
||||
enum CategoryError: LocalizedError {
|
||||
case noId
|
||||
case noActionId
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noId:
|
||||
return "Action type `id` missing"
|
||||
case .noActionId:
|
||||
return "Action `id` missing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func makeCategories(_ actionTypes: [JSObject]) throws {
|
||||
internal func makeCategories(_ actionTypes: [ActionType]) {
|
||||
var createdCategories = [UNNotificationCategory]()
|
||||
|
||||
let generalCategory = UNNotificationCategory(
|
||||
@@ -30,22 +16,16 @@ public func makeCategories(_ actionTypes: [JSObject]) throws {
|
||||
|
||||
createdCategories.append(generalCategory)
|
||||
for type in actionTypes {
|
||||
guard let id = type["id"] as? String else {
|
||||
throw CategoryError.noId
|
||||
}
|
||||
let hiddenBodyPlaceholder = type["hiddenPreviewsBodyPlaceholder"] as? String ?? ""
|
||||
let actions = type["actions"] as? [JSObject] ?? []
|
||||
|
||||
let newActions = try makeActions(actions)
|
||||
let newActions = makeActions(type.actions)
|
||||
|
||||
// Create the custom actions for the TIMER_EXPIRED category.
|
||||
var newCategory: UNNotificationCategory?
|
||||
|
||||
newCategory = UNNotificationCategory(
|
||||
identifier: id,
|
||||
identifier: type.id,
|
||||
actions: newActions,
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: hiddenBodyPlaceholder,
|
||||
hiddenPreviewsBodyPlaceholder: type.hiddenBodyPlaceholder ?? "",
|
||||
options: makeCategoryOptions(type))
|
||||
|
||||
createdCategories.append(newCategory!)
|
||||
@@ -55,37 +35,28 @@ public func makeCategories(_ actionTypes: [JSObject]) throws {
|
||||
center.setNotificationCategories(Set(createdCategories))
|
||||
}
|
||||
|
||||
func makeActions(_ actions: [JSObject]) throws -> [UNNotificationAction] {
|
||||
func makeActions(_ actions: [Action]) -> [UNNotificationAction] {
|
||||
var createdActions = [UNNotificationAction]()
|
||||
|
||||
for action in actions {
|
||||
guard let id = action["id"] as? String else {
|
||||
throw CategoryError.noActionId
|
||||
}
|
||||
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 {
|
||||
if action.input {
|
||||
if action.inputButtonTitle != nil {
|
||||
newAction = UNTextInputNotificationAction(
|
||||
identifier: id,
|
||||
title: title,
|
||||
identifier: action.id,
|
||||
title: action.title,
|
||||
options: makeActionOptions(action),
|
||||
textInputButtonTitle: inputButtonTitle!,
|
||||
textInputPlaceholder: inputPlaceholder)
|
||||
textInputButtonTitle: action.inputButtonTitle ?? "",
|
||||
textInputPlaceholder: action.inputPlaceholder ?? "")
|
||||
} else {
|
||||
newAction = UNTextInputNotificationAction(
|
||||
identifier: id, title: title, options: makeActionOptions(action))
|
||||
identifier: action.id, title: action.title, options: makeActionOptions(action))
|
||||
}
|
||||
} else {
|
||||
// Create the custom actions for the TIMER_EXPIRED category.
|
||||
newAction = UNNotificationAction(
|
||||
identifier: id,
|
||||
title: title,
|
||||
identifier: action.id,
|
||||
title: action.title,
|
||||
options: makeActionOptions(action))
|
||||
}
|
||||
createdActions.append(newAction)
|
||||
@@ -94,40 +65,31 @@ func makeActions(_ actions: [JSObject]) throws -> [UNNotificationAction] {
|
||||
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 {
|
||||
func makeActionOptions(_ action: Action) -> UNNotificationActionOptions {
|
||||
if action.foreground {
|
||||
return .foreground
|
||||
}
|
||||
if destructive {
|
||||
if action.destructive {
|
||||
return .destructive
|
||||
}
|
||||
if requiresAuthentication {
|
||||
if action.requiresAuthentication {
|
||||
return .authenticationRequired
|
||||
}
|
||||
return UNNotificationActionOptions(rawValue: 0)
|
||||
}
|
||||
|
||||
func makeCategoryOptions(_ type: JSObject) -> UNNotificationCategoryOptions {
|
||||
let customDismiss = type["customDismissAction"] as? Bool ?? false
|
||||
let carPlay = type["allowInCarPlay"] as? Bool ?? false
|
||||
let hiddenPreviewsShowTitle = type["hiddenPreviewsShowTitle"] as? Bool ?? false
|
||||
let hiddenPreviewsShowSubtitle = type["hiddenPreviewsShowSubtitle"] as? Bool ?? false
|
||||
|
||||
if customDismiss {
|
||||
func makeCategoryOptions(_ type: ActionType) -> UNNotificationCategoryOptions {
|
||||
if type.customDismissAction {
|
||||
return .customDismissAction
|
||||
}
|
||||
if carPlay {
|
||||
if type.allowInCarPlay {
|
||||
return .allowInCarPlay
|
||||
}
|
||||
|
||||
if hiddenPreviewsShowTitle {
|
||||
if type.hiddenPreviewsShowTitle {
|
||||
return .hiddenPreviewsShowTitle
|
||||
}
|
||||
if hiddenPreviewsShowSubtitle {
|
||||
if type.hiddenPreviewsShowSubtitle {
|
||||
return .hiddenPreviewsShowSubtitle
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ public class NotificationHandler: NSObject, NotificationHandlerProtocol {
|
||||
|
||||
public weak var plugin: Plugin?
|
||||
|
||||
private var notificationsMap = [String: JSObject]()
|
||||
private var notificationsMap = [String: Notification]()
|
||||
|
||||
public func saveNotification(_ key: String, _ notification: JSObject) {
|
||||
internal func saveNotification(_ key: String, _ notification: Notification) {
|
||||
notificationsMap.updateValue(notification, forKey: key)
|
||||
}
|
||||
|
||||
@@ -30,12 +30,11 @@ public class NotificationHandler: NSObject, NotificationHandlerProtocol {
|
||||
}
|
||||
|
||||
public func willPresent(notification: UNNotification) -> UNNotificationPresentationOptions {
|
||||
let notificationData = makeNotificationRequestJSObject(notification.request)
|
||||
self.plugin?.trigger("notification", data: notificationData)
|
||||
let notificationData = toActiveNotification(notification.request)
|
||||
try? self.plugin?.trigger("notification", data: notificationData)
|
||||
|
||||
if let options = notificationsMap[notification.request.identifier] {
|
||||
let silent = options["silent"] as? Bool ?? false
|
||||
if silent {
|
||||
if options.silent {
|
||||
return UNNotificationPresentationOptions.init(rawValue: 0)
|
||||
}
|
||||
}
|
||||
@@ -48,73 +47,72 @@ public class NotificationHandler: NSObject, NotificationHandlerProtocol {
|
||||
}
|
||||
|
||||
public func didReceive(response: UNNotificationResponse) {
|
||||
var data = JSObject()
|
||||
|
||||
let originalNotificationRequest = response.notification.request
|
||||
let actionId = response.actionIdentifier
|
||||
|
||||
var actionIdValue: String
|
||||
// We turn the two default actions (open/dismiss) into generic strings
|
||||
if actionId == UNNotificationDefaultActionIdentifier {
|
||||
data["actionId"] = "tap"
|
||||
actionIdValue = "tap"
|
||||
} else if actionId == UNNotificationDismissActionIdentifier {
|
||||
data["actionId"] = "dismiss"
|
||||
actionIdValue = "dismiss"
|
||||
} else {
|
||||
data["actionId"] = actionId
|
||||
actionIdValue = actionId
|
||||
}
|
||||
|
||||
var inputValue: String? = nil
|
||||
// If the type of action was for an input type, get the value
|
||||
if let inputType = response as? UNTextInputNotificationResponse {
|
||||
data["inputValue"] = inputType.userText
|
||||
inputValue = inputType.userText
|
||||
}
|
||||
|
||||
data["notification"] = makeNotificationRequestJSObject(originalNotificationRequest)
|
||||
|
||||
self.plugin?.trigger("actionPerformed", data: data)
|
||||
try? self.plugin?.trigger(
|
||||
"actionPerformed",
|
||||
data: ReceivedNotification(
|
||||
actionId: actionIdValue,
|
||||
inputValue: inputValue,
|
||||
notification: toActiveNotification(originalNotificationRequest)
|
||||
))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"] ?? [JSObject]()
|
||||
return notification
|
||||
func toActiveNotification(_ request: UNNotificationRequest) -> ActiveNotification {
|
||||
let notificationRequest = notificationsMap[request.identifier]!
|
||||
return ActiveNotification(
|
||||
id: Int(request.identifier) ?? -1,
|
||||
title: request.content.title,
|
||||
body: request.content.body,
|
||||
sound: notificationRequest.sound ?? "",
|
||||
actionTypeId: request.content.categoryIdentifier,
|
||||
attachments: notificationRequest.attachments
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
func toPendingNotification(_ request: UNNotificationRequest) -> PendingNotification {
|
||||
return PendingNotification(
|
||||
id: Int(request.identifier) ?? -1,
|
||||
title: request.content.title,
|
||||
body: request.content.body
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingNotification: Encodable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let body: String
|
||||
}
|
||||
|
||||
struct ActiveNotification: Encodable {
|
||||
let id: Int
|
||||
let title: String
|
||||
let body: String
|
||||
let sound: String
|
||||
let actionTypeId: String
|
||||
let attachments: [NotificationAttachment]?
|
||||
}
|
||||
|
||||
struct ReceivedNotification: Encodable {
|
||||
let actionId: String
|
||||
let inputValue: String?
|
||||
let notification: ActiveNotification
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ import UserNotifications
|
||||
center.delegate = self
|
||||
}
|
||||
|
||||
public func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
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 {
|
||||
@@ -31,9 +33,11 @@ import UserNotifications
|
||||
completionHandler(presentationOptions ?? [])
|
||||
}
|
||||
|
||||
public func userNotificationCenter(_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -9,14 +9,11 @@ import UserNotifications
|
||||
import WebKit
|
||||
|
||||
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):
|
||||
@@ -25,13 +22,71 @@ enum ShowNotificationError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
func showNotification(invoke: Invoke, notification: JSObject)
|
||||
enum ScheduleEveryKind: String, Decodable {
|
||||
case year
|
||||
case month
|
||||
case twoWeeks
|
||||
case week
|
||||
case day
|
||||
case hour
|
||||
case minute
|
||||
case second
|
||||
}
|
||||
|
||||
struct ScheduleInterval: Decodable {
|
||||
let year: Int?
|
||||
let month: Int?
|
||||
let day: Int?
|
||||
let weekday: Int?
|
||||
let hour: Int?
|
||||
let minute: Int?
|
||||
let second: Int?
|
||||
}
|
||||
|
||||
enum NotificationSchedule: Decodable {
|
||||
case at(date: String, repeating: Bool)
|
||||
case interval(interval: ScheduleInterval)
|
||||
case every(interval: ScheduleEveryKind, count: Int)
|
||||
}
|
||||
|
||||
struct NotificationAttachmentOptions: Codable {
|
||||
let iosUNNotificationAttachmentOptionsTypeHintKey: String?
|
||||
let iosUNNotificationAttachmentOptionsThumbnailHiddenKey: String?
|
||||
let iosUNNotificationAttachmentOptionsThumbnailClippingRectKey: String?
|
||||
let iosUNNotificationAttachmentOptionsThumbnailTimeKey: String?
|
||||
}
|
||||
|
||||
struct NotificationAttachment: Codable {
|
||||
let id: String
|
||||
let url: String
|
||||
let options: NotificationAttachmentOptions?
|
||||
}
|
||||
|
||||
struct Notification: Decodable {
|
||||
let id: Int
|
||||
var title: String = ""
|
||||
var body: String = ""
|
||||
var extra: [String: String] = [:]
|
||||
let schedule: NotificationSchedule?
|
||||
let attachments: [NotificationAttachment]?
|
||||
let sound: String?
|
||||
let group: String?
|
||||
let actionTypeId: String?
|
||||
let summary: String?
|
||||
var silent = false
|
||||
}
|
||||
|
||||
struct RemoveActiveNotification: Decodable {
|
||||
let id: Int
|
||||
}
|
||||
|
||||
struct RemoveActiveArgs: Decodable {
|
||||
let notifications: [RemoveActiveNotification]
|
||||
}
|
||||
|
||||
func showNotification(invoke: Invoke, notification: Notification)
|
||||
throws -> UNNotificationRequest
|
||||
{
|
||||
guard let identifier = notification["id"] as? Int else {
|
||||
throw ShowNotificationError.noId
|
||||
}
|
||||
|
||||
var content: UNNotificationContent
|
||||
do {
|
||||
content = try makeNotificationContent(notification)
|
||||
@@ -42,7 +97,7 @@ func showNotification(invoke: Invoke, notification: JSObject)
|
||||
var trigger: UNNotificationTrigger?
|
||||
|
||||
do {
|
||||
if let schedule = notification["schedule"] as? JSObject {
|
||||
if let schedule = notification.schedule {
|
||||
try trigger = handleScheduledNotification(schedule)
|
||||
}
|
||||
} catch {
|
||||
@@ -51,7 +106,7 @@ func showNotification(invoke: Invoke, notification: JSObject)
|
||||
|
||||
// Schedule the request.
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "\(identifier)", content: content, trigger: trigger
|
||||
identifier: "\(notification.id)", content: content, trigger: trigger
|
||||
)
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
@@ -64,6 +119,40 @@ func showNotification(invoke: Invoke, notification: JSObject)
|
||||
return request
|
||||
}
|
||||
|
||||
struct CancelArgs: Decodable {
|
||||
let notifications: [Int]
|
||||
}
|
||||
|
||||
struct Action: Decodable {
|
||||
let id: String
|
||||
let title: String
|
||||
var requiresAuthentication: Bool = false
|
||||
var foreground: Bool = false
|
||||
var destructive: Bool = false
|
||||
var input: Bool = false
|
||||
let inputButtonTitle: String?
|
||||
let inputPlaceholder: String?
|
||||
}
|
||||
|
||||
struct ActionType: Decodable {
|
||||
let id: String
|
||||
let actions: [Action]
|
||||
let hiddenPreviewsBodyPlaceholder: String?
|
||||
var customDismissAction = false
|
||||
var allowInCarPlay = false
|
||||
var hiddenPreviewsShowTitle = false
|
||||
var hiddenPreviewsShowSubtitle = false
|
||||
let hiddenBodyPlaceholder: String?
|
||||
}
|
||||
|
||||
struct RegisterActionTypesArgs: Decodable {
|
||||
let types: [ActionType]
|
||||
}
|
||||
|
||||
struct BatchArgs: Decodable {
|
||||
let notifications: [Notification]
|
||||
}
|
||||
|
||||
class NotificationPlugin: Plugin {
|
||||
let notificationHandler = NotificationHandler()
|
||||
let notificationManager = NotificationManager()
|
||||
@@ -75,29 +164,24 @@ class NotificationPlugin: Plugin {
|
||||
}
|
||||
|
||||
@objc public func show(_ invoke: Invoke) throws {
|
||||
let request = try showNotification(invoke: invoke, notification: invoke.data)
|
||||
notificationHandler.saveNotification(request.identifier, invoke.data)
|
||||
invoke.resolve([
|
||||
"id": Int(request.identifier) ?? -1
|
||||
])
|
||||
let notification = try invoke.parseArgs(Notification.self)
|
||||
|
||||
let request = try showNotification(invoke: invoke, notification: notification)
|
||||
notificationHandler.saveNotification(request.identifier, notification)
|
||||
invoke.resolve(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
|
||||
}
|
||||
let args = try invoke.parseArgs(BatchArgs.self)
|
||||
var ids = [Int]()
|
||||
|
||||
for notification in notifications {
|
||||
for notification in args.notifications {
|
||||
let request = try showNotification(invoke: invoke, notification: notification)
|
||||
notificationHandler.saveNotification(request.identifier, notification)
|
||||
ids.append(Int(request.identifier) ?? -1)
|
||||
}
|
||||
|
||||
invoke.resolve([
|
||||
"notifications": ids
|
||||
])
|
||||
invoke.resolve(ids)
|
||||
}
|
||||
|
||||
@objc public override func requestPermissions(_ invoke: Invoke) {
|
||||
@@ -129,18 +213,11 @@ class NotificationPlugin: Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func cancel(_ invoke: Invoke) {
|
||||
guard let notifications = invoke.getArray("notifications", NSNumber.self),
|
||||
notifications.count > 0
|
||||
else {
|
||||
invoke.reject("`notifications` input is required")
|
||||
return
|
||||
}
|
||||
@objc func cancel(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(CancelArgs.self)
|
||||
|
||||
UNUserNotificationCenter.current().removePendingNotificationRequests(
|
||||
withIdentifiers: notifications.map({ (id) -> String in
|
||||
return id.stringValue
|
||||
})
|
||||
withIdentifiers: args.notifications.map { String($0) }
|
||||
)
|
||||
invoke.resolve()
|
||||
}
|
||||
@@ -148,30 +225,27 @@ class NotificationPlugin: Plugin {
|
||||
@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)
|
||||
let ret = notifications.compactMap({ [weak self] (notification) -> PendingNotification? in
|
||||
return self?.notificationHandler.toPendingNotification(notification)
|
||||
})
|
||||
|
||||
invoke.resolve([
|
||||
"notifications": ret
|
||||
])
|
||||
invoke.resolve(ret)
|
||||
})
|
||||
}
|
||||
|
||||
@objc func registerActionTypes(_ invoke: Invoke) throws {
|
||||
guard let types = invoke.getArray("types", JSObject.self) else {
|
||||
return
|
||||
}
|
||||
try makeCategories(types)
|
||||
let args = try invoke.parseArgs(RegisterActionTypesArgs.self)
|
||||
makeCategories(args.types)
|
||||
invoke.resolve()
|
||||
}
|
||||
|
||||
@objc func removeActive(_ invoke: Invoke) {
|
||||
if let notifications = invoke.getArray("notifications", JSObject.self) {
|
||||
let ids = notifications.map { "\($0["id"] ?? "")" }
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids)
|
||||
do {
|
||||
let args = try invoke.parseArgs(RemoveActiveArgs.self)
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(
|
||||
withIdentifiers: args.notifications.map { String($0.id) })
|
||||
invoke.resolve()
|
||||
} else {
|
||||
} catch {
|
||||
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
||||
DispatchQueue.main.async(execute: {
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
@@ -183,13 +257,11 @@ class NotificationPlugin: Plugin {
|
||||
@objc func getActive(_ invoke: Invoke) {
|
||||
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: {
|
||||
(notifications) in
|
||||
let ret = notifications.map({ (notification) -> [String: Any] in
|
||||
return self.notificationHandler.makeNotificationRequestJSObject(
|
||||
let ret = notifications.map({ (notification) -> ActiveNotification in
|
||||
return self.notificationHandler.toActiveNotification(
|
||||
notification.request)
|
||||
})
|
||||
invoke.resolve([
|
||||
"notifications": ret
|
||||
])
|
||||
invoke.resolve(ret)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.0.0-alpha.9"
|
||||
"@tauri-apps/api": "2.0.0-alpha.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_NOTIFICATION__=function(n){"use strict";var i=Object.defineProperty,e=(n,i,e)=>{if(!i.has(n))throw TypeError("Cannot "+e)},t=(n,i,t)=>(e(n,i,"read from private field"),t?t.call(n):i.get(n));function o(n,i=!1){return window.__TAURI_INTERNALS__.transformCallback(n,i)}((n,e)=>{for(var t in e)i(n,t,{get:e[t],enumerable:!0})})({},{Channel:()=>c,PluginListener:()=>l,addPluginListener:()=>f,convertFileSrc:()=>d,invoke:()=>_,transformCallback:()=>o});var r,c=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((n,i,e)=>{if(i.has(n))throw TypeError("Cannot add the same private member more than once");i instanceof WeakSet?i.add(n):i.set(n,e)})(this,r,(()=>{})),this.id=o((n=>{t(this,r).call(this,n)}))}set onmessage(n){var i,t,o,c;o=n,e(i=this,t=r,"write to private field"),c?c.call(i,o):t.set(i,o)}get onmessage(){return t(this,r)}toJSON(){return`__CHANNEL__:${this.id}`}};r=new WeakMap;var a,s,u,l=class{constructor(n,i,e){this.plugin=n,this.event=i,this.channelId=e}async unregister(){return _(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function f(n,i,e){let t=new c;return t.onmessage=e,_(`plugin:${n}|register_listener`,{event:i,handler:t}).then((()=>new l(n,i,t.id)))}async function _(n,i={},e){return window.__TAURI_INTERNALS__.invoke(n,i,e)}function d(n,i="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(n,i)}return function(n){n.Year="Year",n.Month="Month",n.TwoWeeks="TwoWeeks",n.Week="Week",n.Day="Day",n.Hour="Hour",n.Minute="Minute",n.Second="Second"}(a||(a={})),n.Importance=void 0,(s=n.Importance||(n.Importance={}))[s.None=0]="None",s[s.Min=1]="Min",s[s.Low=2]="Low",s[s.Default=3]="Default",s[s.High=4]="High",n.Visibility=void 0,(u=n.Visibility||(n.Visibility={}))[u.Secret=-1]="Secret",u[u.Private=0]="Private",u[u.Public=1]="Public",n.active=async function(){return _("plugin:notification|get_active")},n.cancel=async function(n){return _("plugin:notification|cancel",{notifications:n})},n.cancelAll=async function(){return _("plugin:notification|cancel")},n.channels=async function(){return _("plugin:notification|getActive")},n.createChannel=async function(n){return _("plugin:notification|create_channel",{...n})},n.isPermissionGranted=async function(){return"default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):_("plugin:notification|is_permission_granted")},n.onAction=async function(n){return f("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return f("notification","notification",n)},n.pending=async function(){return _("plugin:notification|get_pending")},n.registerActionTypes=async function(n){return _("plugin:notification|register_action_types",{types:n})},n.removeActive=async function(n){return _("plugin:notification|remove_active",{notifications:n})},n.removeAllActive=async function(){return _("plugin:notification|remove_active")},n.removeChannel=async function(n){return _("plugin:notification|delete_channel",{id:n})},n.requestPermission=async function(){return window.Notification.requestPermission()},n.sendNotification=function(n){"string"==typeof n?new window.Notification(n):new window.Notification(n.title,n)},n}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_NOTIFICATION__})}
|
||||
if("__TAURI__"in window){var __TAURI_NOTIFICATION__=function(n){"use strict";function i(n,i,e,t){if("a"===e&&!t)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof i?n!==i||!t:!i.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===e?t:"a"===e?t.call(n):t?t.value:i.get(n)}var e,t,o,r;"function"==typeof SuppressedError&&SuppressedError;class c{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,e.set(this,(()=>{})),this.id=function(n,i=!1){return window.__TAURI_INTERNALS__.transformCallback(n,i)}((n=>{i(this,e,"f").call(this,n)}))}set onmessage(n){!function(n,i,e,t,o){if("m"===t)throw new TypeError("Private method is not writable");if("a"===t&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof i?n!==i||!o:!i.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===t?o.call(n,e):o?o.value=e:i.set(n,e)}(this,e,n,"f")}get onmessage(){return i(this,e,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}e=new WeakMap;class a{constructor(n,i,e){this.plugin=n,this.event=i,this.channelId=e}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function s(n,i,e){const t=new c;return t.onmessage=e,u(`plugin:${n}|register_listener`,{event:i,handler:t}).then((()=>new a(n,i,t.id)))}async function u(n,i={},e){return window.__TAURI_INTERNALS__.invoke(n,i,e)}return function(n){n.Year="year",n.Month="month",n.TwoWeeks="twoWeeks",n.Week="week",n.Day="day",n.Hour="hour",n.Minute="minute",n.Second="second"}(t||(t={})),n.Importance=void 0,(o=n.Importance||(n.Importance={}))[o.None=0]="None",o[o.Min=1]="Min",o[o.Low=2]="Low",o[o.Default=3]="Default",o[o.High=4]="High",n.Visibility=void 0,(r=n.Visibility||(n.Visibility={}))[r.Secret=-1]="Secret",r[r.Private=0]="Private",r[r.Public=1]="Public",n.active=async function(){return u("plugin:notification|get_active")},n.cancel=async function(n){return u("plugin:notification|cancel",{notifications:n})},n.cancelAll=async function(){return u("plugin:notification|cancel")},n.channels=async function(){return u("plugin:notification|listChannels")},n.createChannel=async function(n){return u("plugin:notification|create_channel",{...n})},n.isPermissionGranted=async function(){return"default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):u("plugin:notification|is_permission_granted")},n.onAction=async function(n){return s("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return s("notification","notification",n)},n.pending=async function(){return u("plugin:notification|get_pending")},n.registerActionTypes=async function(n){return u("plugin:notification|register_action_types",{types:n})},n.removeActive=async function(n){return u("plugin:notification|remove_active",{notifications:n})},n.removeAllActive=async function(){return u("plugin:notification|remove_active")},n.removeChannel=async function(n){return u("plugin:notification|delete_channel",{id:n})},n.requestPermission=async function(){return window.Notification.requestPermission()},n.sendNotification=function(n){"string"==typeof n?new window.Notification(n):new window.Notification(n.title,n)},n}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_NOTIFICATION__})}
|
||||
|
||||
@@ -1 +1 @@
|
||||
!function(){"use strict";var e=Object.defineProperty,n=(e,n,t)=>{if(!n.has(e))throw TypeError("Cannot "+t)},t=(e,t,i)=>(n(e,t,"read from private field"),i?i.call(e):t.get(e));function i(e,n=!1){return window.__TAURI_INTERNALS__.transformCallback(e,n)}((n,t)=>{for(var i in t)e(n,i,{get:t[i],enumerable:!0})})({},{Channel:()=>o,PluginListener:()=>s,addPluginListener:()=>a,convertFileSrc:()=>l,invoke:()=>c,transformCallback:()=>i});var r,o=class{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,((e,n,t)=>{if(n.has(e))throw TypeError("Cannot add the same private member more than once");n instanceof WeakSet?n.add(e):n.set(e,t)})(this,r,(()=>{})),this.id=i((e=>{t(this,r).call(this,e)}))}set onmessage(e){var t,i,o,s;o=e,n(t=this,i=r,"write to private field"),s?s.call(t,o):i.set(t,o)}get onmessage(){return t(this,r)}toJSON(){return`__CHANNEL__:${this.id}`}};r=new WeakMap;var s=class{constructor(e,n,t){this.plugin=e,this.event=n,this.channelId=t}async unregister(){return c(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}};async function a(e,n,t){let i=new o;return i.onmessage=t,c(`plugin:${e}|register_listener`,{event:n,handler:i}).then((()=>new s(e,n,i.id)))}async function c(e,n={},t){return window.__TAURI_INTERNALS__.invoke(e,n,t)}function l(e,n="asset"){return window.__TAURI_INTERNALS__.convertFileSrc(e,n)}!function(){let e=!1,n="default";function t(n){e=!0,window.Notification.permission=n,e=!1}window.Notification=function(e,n){const t=n||{};!function(e){"object"==typeof e&&Object.freeze(e),c("plugin:notification|notify",{options:"string"==typeof e?{title:e}:e})}(Object.assign(t,{title:e}))},window.Notification.requestPermission=function(){return c("plugin:notification|request_permission").then((e=>(t("prompt"===e?"default":e),e)))},Object.defineProperty(window.Notification,"permission",{enumerable:!0,get:()=>n,set:t=>{if(!e)throw new Error("Readonly property");n=t}}),("default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):c("plugin:notification|is_permission_granted")).then((function(e){t(null===e?"default":e?"granted":"denied")}))}()}();
|
||||
!function(){"use strict";async function i(i,n={},t){return window.__TAURI_INTERNALS__.invoke(i,n,t)}"function"==typeof SuppressedError&&SuppressedError,function(){let n=!1,t="default";function o(i){n=!0,window.Notification.permission=i,n=!1}window.Notification=function(n,t){const o=t||{};!function(n){"object"==typeof n&&Object.freeze(n),i("plugin:notification|notify",{options:"string"==typeof n?{title:n}:n})}(Object.assign(o,{title:n}))},window.Notification.requestPermission=function(){return i("plugin:notification|request_permission").then((i=>(o("prompt"===i?"default":i),i)))},Object.defineProperty(window.Notification,"permission",{enumerable:!0,get:()=>t,set:i=>{if(!n)throw new Error("Readonly property");t=i}}),("default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):i("plugin:notification|is_permission_granted")).then((function(i){o(null===i?"default":i?"granted":"denied")}))}()}();
|
||||
|
||||
@@ -33,7 +33,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::<ShowResponse>("show", self.data)
|
||||
.run_mobile_plugin::<i32>("show", self.data)
|
||||
.map(|_| ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
@@ -89,8 +89,7 @@ impl<R: Runtime> Notification<R> {
|
||||
|
||||
pub fn active(&self) -> crate::Result<Vec<ActiveNotification>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<ActiveResponse>("getActive", ())
|
||||
.map(|r| r.notifications)
|
||||
.run_mobile_plugin("getActive", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -102,8 +101,7 @@ impl<R: Runtime> Notification<R> {
|
||||
|
||||
pub fn pending(&self) -> crate::Result<Vec<PendingNotification>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<PendingResponse>("getPending", ())
|
||||
.map(|r| r.notifications)
|
||||
.run_mobile_plugin("getPending", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
@@ -138,34 +136,11 @@ impl<R: Runtime> Notification<R> {
|
||||
#[cfg(target_os = "android")]
|
||||
pub fn list_channels(&self) -> crate::Result<Vec<Channel>> {
|
||||
self.0
|
||||
.run_mobile_plugin::<ListChannelsResult>("listChannels", ())
|
||||
.map(|r| r.channels)
|
||||
.run_mobile_plugin("listChannels", ())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
#[derive(Deserialize)]
|
||||
struct ListChannelsResult {
|
||||
channels: Vec<Channel>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PendingResponse {
|
||||
notifications: Vec<PendingNotification>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveResponse {
|
||||
notifications: Vec<ActiveNotification>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ShowResponse {
|
||||
#[allow(dead_code)]
|
||||
id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PermissionResponse {
|
||||
|
||||
@@ -94,7 +94,7 @@ impl<'de> Deserialize<'de> for ScheduleEvery {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "data")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Schedule {
|
||||
At {
|
||||
#[serde(
|
||||
@@ -104,10 +104,19 @@ pub enum Schedule {
|
||||
date: time::OffsetDateTime,
|
||||
#[serde(default)]
|
||||
repeating: bool,
|
||||
#[serde(default)]
|
||||
allow_while_idle: bool,
|
||||
},
|
||||
Interval {
|
||||
interval: ScheduleInterval,
|
||||
#[serde(default)]
|
||||
allow_while_idle: bool,
|
||||
},
|
||||
Interval(ScheduleInterval),
|
||||
Every {
|
||||
interval: ScheduleEvery,
|
||||
count: u8,
|
||||
#[serde(default)]
|
||||
allow_while_idle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user