From 2b715a0d8c1a8fb38a9af1c63a55da635dfa0473 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 28 Jul 2023 21:01:08 +0300 Subject: [PATCH] desktop: notifications support (#2754) * desktop: notifications support * adapted external lib for interacting with notificatioins * disabled some functions --- .../java/chat/simplex/app/MainActivity.kt | 40 +---- .../main/java/chat/simplex/app/SimplexApp.kt | 7 +- .../{NtfManager.kt => NtfManager.android.kt} | 52 +----- apps/multiplatform/build.gradle.kts | 1 + apps/multiplatform/common/build.gradle.kts | 1 + .../common/views/helpers/Utils.android.kt | 1 + .../SetNotificationsMode.android.kt | 4 +- .../simplex/common/platform/NtfManager.kt | 106 +++++++++++- .../kotlin/chat/simplex/common/DesktopApp.kt | 3 +- .../common/model/NtfManager.desktop.kt | 153 ++++++++++++++++++ .../common/platform/AppCommon.desktop.kt | 17 +- .../common/platform/Notifications.desktop.kt | 4 +- 12 files changed, 276 insertions(+), 113 deletions(-) rename apps/multiplatform/android/src/main/java/chat/simplex/app/model/{NtfManager.kt => NtfManager.android.kt} (88%) create mode 100644 apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index b8c517ddfa..55d8202f89 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -107,39 +107,18 @@ fun processNotificationIntent(intent: Intent?) { val chatId = intent.getStringExtra("chatId") Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId") if (chatId != null) { - withBGApi { - awaitChatStartedIfNeeded(chatModel) - if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { - chatModel.controller.changeActiveUser(userId, null) - } - val cInfo = chatModel.getChat(chatId)?.chatInfo - chatModel.clearOverlays.value = true - if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel) - } + ntfManager.openChatAction(userId, chatId) } } NtfManager.ShowChatsAction -> { Log.d(TAG, "processNotificationIntent: ShowChatsAction") - withBGApi { - awaitChatStartedIfNeeded(chatModel) - if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { - chatModel.controller.changeActiveUser(userId, null) - } - chatModel.chatId.value = null - chatModel.clearOverlays.value = true - } + ntfManager.showChatsAction(userId) } NtfManager.AcceptCallAction -> { val chatId = intent.getStringExtra("chatId") if (chatId == null || chatId == "") return Log.d(TAG, "processNotificationIntent: AcceptCallAction $chatId") - chatModel.clearOverlays.value = true - val invitation = chatModel.callInvitations[chatId] - if (invitation == null) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) - } else { - chatModel.callManager.acceptIncomingCall(invitation = invitation) - } + ntfManager.acceptCallAction(chatId) } } } @@ -201,19 +180,6 @@ fun processExternalIntent(intent: Intent?) { fun isMediaIntent(intent: Intent): Boolean = intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true -suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) { - // Still decrypting database - if (chatModel.chatRunning.value == null) { - val step = 50L - for (i in 0..(timeout / step)) { - if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) { - break - } - delay(step) - } - } -} - //fun testJson() { // val str: String = """ // """.trimIndent() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 3174e06265..19299d84db 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -139,14 +139,11 @@ class SimplexApp: Application(), LifecycleEventObserver { androidAppContext = this APPLICATION_ID = BuildConfig.APPLICATION_ID ntfManager = object : chat.simplex.common.platform.NtfManager() { - override fun notifyContactConnected(user: User, contact: Contact) = NtfManager.notifyContactConnected(user, contact) - override fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) = NtfManager.notifyContactRequestReceived(user, cInfo) - override fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) = NtfManager.notifyMessageReceived(user, cInfo, cItem) override fun notifyCallInvitation(invitation: RcvCallInvitation) = NtfManager.notifyCallInvitation(invitation) override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId) - override fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions) - override fun createNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() + override fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first }) + override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun cancelCallNotification() = NtfManager.cancelCallNotification() override fun cancelAllNotifications() = NtfManager.cancelAllNotifications() } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt similarity index 88% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.kt rename to apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index 5752d3fd6b..95d2520f56 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -15,7 +15,6 @@ import chat.simplex.app.* import chat.simplex.app.TAG import chat.simplex.app.views.call.IncomingCallActivity import chat.simplex.app.views.call.getKeyguardManager -import chat.simplex.common.views.chatlist.acceptContactRequest import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.* @@ -82,31 +81,6 @@ object NtfManager { } } - fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) { - displayNotification( - user = user, - chatId = cInfo.id, - displayName = cInfo.displayName, - msgText = generalGetString(MR.strings.notification_new_contact_request), - image = cInfo.image, - listOf(NotificationAction.ACCEPT_CONTACT_REQUEST) - ) - } - - fun notifyContactConnected(user: User, contact: Contact) { - displayNotification( - user = user, - chatId = contact.id, - displayName = contact.displayName, - msgText = generalGetString(MR.strings.notification_contact_connected) - ) - } - - fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) { - if (!cInfo.ntfsEnabled) return - displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) - } - fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List = emptyList()) { if (!user.showNotifications) return Log.d(TAG, "notifyMessageReceived $chatId") @@ -243,19 +217,6 @@ object NtfManager { fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() } - private fun hideSecrets(cItem: ChatItem): String { - val md = cItem.formattedText - return if (md != null) { - var res = "" - for (ft in md) { - res += if (ft.format is Format.Secret) "..." else ft.text - } - res - } else { - cItem.text - } - } - private fun chatPendingIntent(intentAction: String, userId: Long?, chatId: String? = null, broadcast: Boolean = false): PendingIntent { Log.d(TAG, "chatPendingIntent for $intentAction") val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt() @@ -299,18 +260,7 @@ object NtfManager { val chatId = intent?.getStringExtra(ChatIdKey) ?: return val m = SimplexApp.context.chatModel when (intent.action) { - NotificationAction.ACCEPT_CONTACT_REQUEST.name -> { - val isCurrentUser = m.currentUser.value?.userId == userId - val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) { - (m.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return - } else { - null - } - val apiId = chatId.replace("<@", "").toLongOrNull() ?: return - acceptContactRequest(apiId, cInfo, isCurrentUser, m) - cancelNotificationsForChat(chatId) - } - + NotificationAction.ACCEPT_CONTACT_REQUEST.name -> ntfManager.acceptContactRequestAction(userId, chatId) RejectCallAction -> { val invitation = m.callInvitations[chatId] if (invitation != null) { diff --git a/apps/multiplatform/build.gradle.kts b/apps/multiplatform/build.gradle.kts index 94cecd1725..bd5da47a14 100644 --- a/apps/multiplatform/build.gradle.kts +++ b/apps/multiplatform/build.gradle.kts @@ -55,6 +55,7 @@ allprojects { google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://oss.sonatype.org/content/repositories/snapshots") maven("https://jitpack.io") } } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 1d7eb6b3c7..45a963b057 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -95,6 +95,7 @@ kotlin { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1") implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6") + implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") implementation("org.slf4j:slf4j-simple:2.0.7") } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index c8ad31f760..67c41c3d79 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -277,6 +277,7 @@ actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? { actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? { return try { val ext = if (asPng) "png" else "jpg" + tmpDir.mkdir() return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply { outputStream().use { out -> image.asAndroidBitmap().compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt index 0944ff4aab..605c40445a 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt @@ -13,14 +13,14 @@ actual fun SetNotificationsModeAdditions() { val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) LaunchedEffect(notificationsPermissionState.hasPermission) { if (notificationsPermissionState.hasPermission) { - ntfManager.createNtfChannelsMaybeShowAlert() + ntfManager.androidCreateNtfChannelsMaybeShowAlert() } else { notificationsPermissionState.launchPermissionRequest() } } } else { LaunchedEffect(Unit) { - ntfManager.createNtfChannelsMaybeShowAlert() + ntfManager.androidCreateNtfChannelsMaybeShowAlert() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 1a722950ce..0e4a67f221 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -2,6 +2,12 @@ package chat.simplex.common.platform import chat.simplex.common.model.* import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.views.chatlist.acceptContactRequest +import chat.simplex.common.views.chatlist.openChat +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.res.MR +import kotlinx.coroutines.delay enum class NotificationAction { ACCEPT_CONTACT_REQUEST @@ -10,14 +16,104 @@ enum class NotificationAction { lateinit var ntfManager: NtfManager abstract class NtfManager { - abstract fun notifyContactConnected(user: User, contact: Contact) - abstract fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) - abstract fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) + fun notifyContactConnected(user: User, contact: Contact) = displayNotification( + user = user, + chatId = contact.id, + displayName = contact.displayName, + msgText = generalGetString(MR.strings.notification_contact_connected) + ) + + fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) = displayNotification( + user = user, + chatId = cInfo.id, + displayName = cInfo.displayName, + msgText = generalGetString(MR.strings.notification_new_contact_request), + image = cInfo.image, + listOf(NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, cInfo.id) }) + ) + + fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) { + if (!cInfo.ntfsEnabled) return + displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + } + + fun acceptContactRequestAction(userId: Long?, chatId: ChatId) { + val isCurrentUser = ChatModel.currentUser.value?.userId == userId + val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) { + (ChatModel.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return + } else { + null + } + val apiId = chatId.replace("<@", "").toLongOrNull() ?: return + acceptContactRequest(apiId, cInfo, isCurrentUser, ChatModel) + cancelNotificationsForChat(chatId) + } + + fun openChatAction(userId: Long?, chatId: ChatId) { + withBGApi { + awaitChatStartedIfNeeded(chatModel) + if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { + chatModel.controller.changeActiveUser(userId, null) + } + val cInfo = chatModel.getChat(chatId)?.chatInfo + chatModel.clearOverlays.value = true + if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel) + } + } + + fun showChatsAction(userId: Long?) { + withBGApi { + awaitChatStartedIfNeeded(chatModel) + if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { + chatModel.controller.changeActiveUser(userId, null) + } + chatModel.chatId.value = null + chatModel.clearOverlays.value = true + } + } + + fun acceptCallAction(chatId: ChatId) { + chatModel.clearOverlays.value = true + val invitation = chatModel.callInvitations[chatId] + if (invitation == null) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) + } else { + chatModel.callManager.acceptIncomingCall(invitation = invitation) + } + } + abstract fun notifyCallInvitation(invitation: RcvCallInvitation) abstract fun hasNotificationsForChat(chatId: String): Boolean abstract fun cancelNotificationsForChat(chatId: String) - abstract fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List = emptyList()) - abstract fun createNtfChannelsMaybeShowAlert() + abstract fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List Unit>> = emptyList()) abstract fun cancelCallNotification() abstract fun cancelAllNotifications() + // Android only + abstract fun androidCreateNtfChannelsMaybeShowAlert() + + private suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) { + // Still decrypting database + if (chatModel.chatRunning.value == null) { + val step = 50L + for (i in 0..(timeout / step)) { + if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) { + break + } + delay(step) + } + } + } + + private fun hideSecrets(cItem: ChatItem): String { + val md = cItem.formattedText + return if (md != null) { + var res = "" + for (ft in md) { + res += if (ft.format is Format.Secret) "..." else ft.text + } + res + } else { + cItem.text + } + } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 416fca3cce..587786518f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -87,7 +87,7 @@ fun showApp() = application { } } } - var windowFocused by remember { mutableStateOf(true) } + var windowFocused by remember { simplexWindowState.windowFocused } LaunchedEffect(windowFocused) { val delay = ChatController.appPrefs.laLockDelay.get() if (!windowFocused && ChatModel.performLA.value && delay > 0) { @@ -119,6 +119,7 @@ class SimplexWindowState { val openMultipleDialog = DialogState>() val saveDialog = DialogState() val toasts = mutableStateListOf>() + var windowFocused = mutableStateOf(true) } data class DialogParams( diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt new file mode 100644 index 0000000000..1660a4974a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -0,0 +1,153 @@ +package chat.simplex.common.model + +import androidx.compose.ui.graphics.* +import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import com.sshtools.twoslices.* +import java.awt.* +import java.awt.TrayIcon.MessageType +import java.io.File +import javax.imageio.ImageIO + +object NtfManager { + private val prevNtfs = arrayListOf>() + + fun notifyCallInvitation(invitation: RcvCallInvitation) { + if (simplexWindowState.windowFocused.value) return + val contactId = invitation.contact.id + Log.d(TAG, "notifyCallInvitation $contactId") + val image = invitation.contact.image + val text = generalGetString( + if (invitation.callType.media == CallMediaType.Video) { + if (invitation.sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call + } else { + if (invitation.sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call + } + ) + val previewMode = appPreferences.notificationPreviewMode.get() + val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) + generalGetString(MR.strings.notification_preview_somebody) + else + invitation.contact.displayName + val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name) + MR.images.icon_foreground_common.image.toComposeImageBitmap() + else + base64ToBitmap(image) + + val actions = listOf( + generalGetString(MR.strings.accept) to { ntfManager.acceptCallAction(invitation.contact.id) }, + generalGetString(MR.strings.reject) to { ChatModel.callManager.endCall(invitation = invitation) } + ) + displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) { + ntfManager.openChatAction(invitation.user.userId, contactId) + } + } + + fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } + + fun cancelNotificationsForChat(chatId: ChatId) { + val ntf = prevNtfs.firstOrNull { it.first == chatId } + if (ntf != null) { + prevNtfs.remove(ntf) + /*try { + ntf.second.close() + } catch (e: Exception) { + // Can be java.lang.UnsupportedOperationException, for example. May do nothing + println("Failed to close notification: ${e.stackTraceToString()}") + }*/ + } + } + + fun cancelAllNotifications() { +// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } } + prevNtfs.clear() + } + + fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List Unit>>) { + if (!user.showNotifications) return + Log.d(TAG, "notifyMessageReceived $chatId") + val previewMode = appPreferences.notificationPreviewMode.get() + val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName + val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText + val largeIcon = when { + actions.isEmpty() -> null + image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> MR.images.icon_foreground_common.image.toComposeImageBitmap() + else -> base64ToBitmap(image) + } + + displayNotificationViaLib(chatId, title, content, prepareIconPath(largeIcon), actions.map { it.first.name to it.second }) { + ntfManager.openChatAction(user.userId, chatId) + } + } + + private fun displayNotificationViaLib( + chatId: String, + title: String, + text: String, + iconPath: String?, + actions: List Unit>>, + defaultAction: (() -> Unit)? + ) { + val builder = Toast.builder() + .title(title) + .content(text) + if (iconPath != null) { + builder.icon(iconPath) + } + if (defaultAction != null) { + builder.defaultAction(defaultAction) + } + actions.forEach { + builder.action(it.first, it.second) + } + prevNtfs.add(chatId to builder.toast()) + } + + private fun prepareIconPath(icon: ImageBitmap?): String? = if (icon != null) { + tmpDir.mkdir() + val newFile = File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", "png")) + try { + ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream()) + newFile.absolutePath + } catch (e: Exception) { + println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}") + null + } + } else null + + private fun displayNotification(title: String, text: String, icon: ImageBitmap?) = when (desktopPlatform) { + DesktopPlatform.LINUX_X86_64, DesktopPlatform.LINUX_AARCH64 -> linuxDisplayNotification(title, text, prepareIconPath(icon)) + DesktopPlatform.WINDOWS_X86_64 -> windowsDisplayNotification(title, text, icon) + DesktopPlatform.MAC_X86_64, DesktopPlatform.MAC_AARCH64 -> macDisplayNotification(title, text, prepareIconPath(icon)) + } + + private fun linuxDisplayNotification(title: String, text: String, iconPath: String?) { + if (iconPath != null) { + Runtime.getRuntime().exec(arrayOf("notify-send", "-i", iconPath, title, text)) + } else { + Toast.toast(ToastType.INFO, title, text) + Runtime.getRuntime().exec(arrayOf("notify-send", title, text)) + } + } + + private fun windowsDisplayNotification(title: String, text: String, icon: ImageBitmap?) { + if (SystemTray.isSupported()) { + val tray = SystemTray.getSystemTray() + tray.remove(tray.trayIcons.firstOrNull { it.toolTip == "SimpleX" }) + val trayIcon = TrayIcon(icon?.toAwtImage(), "SimpleX") + trayIcon.isImageAutoSize = true + tray.add(trayIcon) + trayIcon.displayMessage(title, text, MessageType.INFO) + } else { + Log.e(TAG, "System tray not supported!") + } + } + + private fun macDisplayNotification(title: String, text: String, iconPath: String?) { + Runtime.getRuntime().exec(arrayOf("osascript", "-e", """display notification "${text.replace("\"", "\\\"")}" with title "${title.replace("\"", "\\\"")}"""")) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 54f73c3f52..18ae4a85a9 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -11,17 +11,14 @@ actual val appPlatform = AppPlatform.DESKTOP val defaultLocale: Locale = Locale.getDefault() fun initApp() { - ntfManager = object : NtfManager() { // LALAL - override fun notifyContactConnected(user: User, contact: Contact) {} - override fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) {} - override fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) {} - override fun notifyCallInvitation(invitation: RcvCallInvitation) {} - override fun hasNotificationsForChat(chatId: String): Boolean = false - override fun cancelNotificationsForChat(chatId: String) {} - override fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List) {} - override fun createNtfChannelsMaybeShowAlert() {} + ntfManager = object : NtfManager() { + override fun notifyCallInvitation(invitation: RcvCallInvitation) = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation) + override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId) + override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId) + override fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String?, actions: List Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions) + override fun androidCreateNtfChannelsMaybeShowAlert() {} override fun cancelCallNotification() {} - override fun cancelAllNotifications() {} + override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() } applyAppLocale() withBGApi { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt index 40f89d17f1..397f8a6966 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt @@ -1,5 +1,5 @@ package chat.simplex.common.platform -import chat.simplex.common.model.NotificationsMode +import chat.simplex.common.simplexWindowState -actual fun allowedToShowNotification(): Boolean = true +actual fun allowedToShowNotification(): Boolean = !simplexWindowState.windowFocused.value