Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native Notifications [WIP] #462

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion samples/SkiaMultiplatformSample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile>().configureEach

enum class Target(val simulator: Boolean, val key: String) {
WATCHOS_X86(true, "watchos"), WATCHOS_ARM64(false, "watchos"),
IOS_X64(true, "iosX64"), IOS_ARM64(false, "iosArm64")
IOS_X64(true, "iosX64"), IOS_ARM64(false, "iosArm64"),
MACOS_X64(false, "macosX64")
}


Expand All @@ -229,6 +230,7 @@ if (hostOs == "macos") {

val target = sdkName.orEmpty().let {
when {
it.startsWith("macosx") -> Target.MACOS_X64
it.startsWith("iphoneos") -> Target.IOS_ARM64
it.startsWith("iphonesimulator") -> Target.IOS_X64
it.startsWith("watchos") -> Target.WATCHOS_ARM64
Expand Down
1 change: 1 addition & 0 deletions samples/SkiaMultiplatformSample/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
kotlin.code.style=official
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
kotlin.native.binary.memoryModel=experimental
26 changes: 26 additions & 0 deletions samples/SkiaMultiplatformSample/plists/MacOS/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UILaunchStoryboardName</key>
<string></string>
<key>NSHighResolutionCapable</key>
<string>True</string>
</dict>
</plist>
16 changes: 16 additions & 0 deletions samples/SkiaMultiplatformSample/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,19 @@ targets:
ENABLE_BITCODE: "YES"
ONLY_ACTIVE_ARCH: "NO"
VALID_ARCHS: "arm64"
SkikoSampleMacOS:
type: application
platform: macOS
deploymentTarget: "11.5"
prebuildScripts:
- script: cd "$SRCROOT" && ./gradlew -p . packForXcode
name: GradleCompile
info:
path: plists/MacOS/Info.plist
properties:
UILaunchStoryboardName: ""
sources:
- "src/"
settings:
LIBRARY_SEARCH_PATHS: "$(inherited)"
ENABLE_BITCODE: "YES"
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import kotlin.math.sin
import kotlin.math.PI
import kotlin.math.pow

class Clocks(private val layer: SkiaLayer): SkikoView {
open class Clocks(private val layer: SkiaLayer): SkikoView {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temporarily opened to add custom key binding for macOS

private val platformYOffset = if (hostOs == OS.Ios) 50f else 5f
private var frame = 0
private var xpos = 0.0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.jetbrains.skiko.sample

import kotlinx.browser.document
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.skiko.GenericSkikoView
import org.jetbrains.skiko.SkiaLayer
import org.jetbrains.skiko.notifications.Notification
import org.jetbrains.skiko.onContentScaleChanged
import org.jetbrains.skiko.wasm.onWasmReady
import org.w3c.dom.HTMLCanvasElement
Expand All @@ -17,5 +20,12 @@ fun main() {
canvas.setAttribute("tabindex", "0")
skiaLayer.attachTo(canvas)
skiaLayer.needRedraw()

GlobalScope.launch {
Notification(
title = "Hello",
body = "It works",
).send()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package org.jetbrains.skiko.sample

import kotlinx.coroutines.*
import platform.AppKit.*

import org.jetbrains.skiko.*
import platform.Foundation.NSMakeRect
import platform.Foundation.NSSelectorFromString
import platform.darwin.NSObject
import org.jetbrains.skiko.notifications.Notification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if notifications deserve their own package.

import platform.Foundation.*
import platform.darwin.*

fun makeApp(skiaLayer: SkiaLayer) = Clocks(skiaLayer)
fun makeApp(skiaLayer: SkiaLayer) = object : Clocks(skiaLayer) {
override fun onKeyboardEvent(event: SkikoKeyboardEvent) {
super.onKeyboardEvent(event)
if (event.kind == SkikoKeyboardEventKind.DOWN) when (event.key) {
SkikoKey.KEY_N -> CoroutineScope(SkikoDispatchers.Main).launch {
Notification(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why runBlocking? Cannot we just run it as suspend on SkikoDispatchers.Main dispatcher?

title = "Hello",
body = "It works!"
).send()
println("After notification sent")
}
else -> {}
}
}
}

fun main() {
val app = NSApplication.sharedApplication()
Expand All @@ -20,7 +35,7 @@ fun main() {
appMenuItem.setSubmenu(appMenu)
appMenu.addItemWithTitle("About $appName", NSSelectorFromString("orderFrontStandardAboutPanel:"), "a")
appMenu.addItemWithTitle("Quit $appName", NSSelectorFromString("terminate:"), "q")

app.delegate = object: NSObject(), NSApplicationDelegateProtocol {
override fun applicationShouldTerminateAfterLastWindowClosed(sender: NSApplication): Boolean {
return true
Expand Down
2 changes: 2 additions & 0 deletions skiko/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ dependencies.skia.android-x64=m99-51988d0bc1
dependencies.skia.android-arm64=m99-51988d0bc1

org.gradle.jvmargs=-Xmx3G

kotlin.native.binary.memoryModel=experimental
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jetbrains.skiko.notifications

internal actual suspend fun sendNotification(notification: Notification) {
TODO("Not implemented yet")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.jetbrains.skiko.notifications

sealed class NotificationError(override val message: String) : Throwable(message)

class PermissionNotGrantedError : NotificationError("Permission not granted for notification")
class NotificationsNotSupportedError : NotificationError("Notifications are not supported for this platform")
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.jetbrains.skiko.notifications

internal expect suspend fun sendNotification(notification: Notification)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.jetbrains.skiko.notifications

class Notification(var title: String, var body: String) {
var iconPath: String? = null

suspend fun send() = sendNotification(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.jetbrains.skiko.notifications

import kotlinx.coroutines.withContext
import org.jetbrains.skiko.SkikoDispatchers
import platform.Foundation.NSError
import platform.Foundation.NSUUID
import platform.UserNotifications.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

@Suppress("NAME_SHADOWING")
internal actual suspend fun sendNotification(notification: Notification) = withContext(SkikoDispatchers.IO) {
val nc = UNUserNotificationCenter.currentNotificationCenter()
val options = UNAuthorizationOptionAlert or UNAuthorizationOptionSound or UNAuthorizationOptionBadge

val sent = suspendCoroutine<Boolean> { cont ->
nc.requestAuthorizationWithOptions(options) { granted: Boolean, err: NSError? ->
if (granted) {
with(notification) {
val request = UNNotificationRequest.requestWithIdentifier(id, content, null)
nc.addNotificationRequest(request) { err: NSError? ->
err?.let { println("Sent with error: $it") }
cont.resume(err == null)
}
}
} else {
println("No auth: $err")
cont.resume(false)
}
}
}
println("Sent: $sent")
}

private val Notification.content by lazy {
UNMutableNotificationContent().apply {
setBody(body)
setTitle(title)
}
}

private val Notification.id by lazy {
NSUUID().UUIDString
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.jetbrains.skiko.notifications

import kotlinx.browser.window
import kotlinx.coroutines.await
import org.w3c.notifications.DENIED
import org.w3c.notifications.GRANTED
import org.w3c.notifications.NotificationOptions
import org.w3c.notifications.NotificationPermission
import org.w3c.notifications.Notification as WebNotification

internal actual suspend fun sendNotification(notification: Notification) {
if (window.asDynamic()["Notification"] == undefined) {
throw NotificationsNotSupportedError()
}

val permission = when (WebNotification.permission) {
NotificationPermission.GRANTED -> NotificationPermission.GRANTED
NotificationPermission.DENIED -> NotificationPermission.DENIED
else -> WebNotification.requestPermission().await()
}

if (permission == NotificationPermission.GRANTED) {
WebNotification(notification.title, NotificationOptions(
body = notification.body
))
} else {
throw PermissionNotGrantedError()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jetbrains.skiko.notifications

internal actual suspend fun sendNotification(notification: Notification) {
TODO("Not implemented yet")
}