Compare commits

...

4 Commits

Author SHA1 Message Date
Evgeny Poberezkin d50562cfee 5.3-beta.1: Android 137 2023-07-28 20:11:04 +01:00
Stanislav Dmitrenko 15d3d3b11a desktop: encoding utf8 (#2795)
* desktop: encoding utf8

* rename functions

* logs
2023-07-28 19:38:31 +01:00
Stanislav Dmitrenko 2b715a0d8c desktop: notifications support (#2754)
* desktop: notifications support

* adapted external lib for interacting with notificatioins

* disabled some functions
2023-07-28 19:01:08 +01:00
Stanislav Dmitrenko 02d00944ff desktop: fix creating tmpDir and providing name for file to save (#2789)
* desktop: creating tmpDir

* provide name for saved file

* directories only selection on Mac

* specified filename for file savers

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2023-07-28 17:57:48 +01:00
18 changed files with 342 additions and 149 deletions
@@ -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()
@@ -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<NotificationAction>) = 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<Pair<NotificationAction, () -> 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()
}
@@ -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<NotificationAction> = 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) {
+1
View File
@@ -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")
}
}
@@ -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")
}
}
@@ -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)
@@ -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()
}
}
}
@@ -1,4 +1,7 @@
#include <jni.h>
//#include <string.h>
//#include <stdlib.h>
//#include <android/log.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
@@ -69,6 +72,9 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused j
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
//jint length = (jint) (*env)->GetStringUTFLength(env, msg);
//for (int i = 0; i < length; ++i)
// __android_log_print(ANDROID_LOG_ERROR, "simplex", "%d: %02x\n", i, _msg[i]);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
return res;
@@ -1,20 +1,10 @@
#include <jni.h>
#include <string.h>
#include <stdlib.h>
// from the RTS
void hs_init(int * argc, char **argv[]);
//extern void __svfscanf(void){};
//extern void __vfwscanf(void){};
//extern void __memset_chk_fail(void){};
//extern void __strcpy_chk_generic(void){};
//extern void __strcat_chk_generic(void){};
//extern void __libc_globals(void){};
//extern void __rel_iplt_start(void){};
// Android 9 only, not 13
//extern void reallocarray(void){};
JNIEXPORT void JNICALL
Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) {
hs_init(NULL, NULL);
@@ -33,7 +23,7 @@ extern char *chat_password_hash(const char *pwd, const char *salt);
// As a reference: https://stackoverflow.com/a/60002045
jstring correct_string_utf8(JNIEnv *env, char *string) {
jstring decode_to_utf8_string(JNIEnv *env, char *string) {
jobject bb = (*env)->NewDirectByteBuffer(env, (void *)string, strlen(string));
jclass cls_charset = (*env)->FindClass(env, "java/nio/charset/Charset");
jmethodID mid_charset_forName = (*env)->GetStaticMethodID(env, cls_charset, "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;");
@@ -52,14 +42,33 @@ jstring correct_string_utf8(JNIEnv *env, char *string) {
return res;
}
char * encode_to_utf8_chars(JNIEnv *env, jstring string) {
if (!string) return "";
const jclass cls_string = (*env)->FindClass(env, "java/lang/String");
const jmethodID mid_getBytes = (*env)->GetMethodID(env, cls_string, "getBytes", "(Ljava/lang/String;)[B");
const jbyteArray jbyte_array = (jbyteArray) (*env)->CallObjectMethod(env, string, mid_getBytes, (*env)->NewStringUTF(env, "UTF-8"));
jint length = (jint) (*env)->GetArrayLength(env, jbyte_array);
jbyte *jbytes = malloc(length + 1);
(*env)->GetByteArrayRegion(env, jbyte_array, 0, length, jbytes);
// char * should be null terminated but jbyte * isn't. Terminate it with \0. Otherwise, Haskell will not see the end of string
jbytes[length] = '\0';
//for (int i = 0; i < length; ++i)
// fprintf(stderr, "%d: %02x\n", i, jbytes[i]);
(*env)->DeleteLocalRef(env, jbyte_array);
(*env)->DeleteLocalRef(env, cls_string);
return (char *) jbytes;
}
JNIEXPORT jobjectArray JNICALL
Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
jlong _ctrl = (jlong) 0;
jstring res = correct_string_utf8(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
const char *_dbPath = encode_to_utf8_chars(env, dbPath);
const char *_dbKey = encode_to_utf8_chars(env, dbKey);
const char *_confirm = encode_to_utf8_chars(env, confirm);
long int *_ctrl = (long) 0;
jstring res = decode_to_utf8_string(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
@@ -78,43 +87,43 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass cla
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
jstring res = correct_string_utf8(env, chat_send_cmd((void*)controller, _msg));
const char *_msg = encode_to_utf8_chars(env, msg);
jstring res = decode_to_utf8_string(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) {
return correct_string_utf8(env, chat_recv_msg((void*)controller));
return decode_to_utf8_string(env, chat_recv_msg((void*)controller));
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, jclass clazz, jlong controller, jint wait) {
return correct_string_utf8(env, chat_recv_msg_wait((void*)controller, wait));
return decode_to_utf8_string(env, chat_recv_msg_wait((void*)controller, wait));
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = correct_string_utf8(env, chat_parse_markdown(_str));
const char *_str = encode_to_utf8_chars(env, str);
jstring res = decode_to_utf8_string(env, chat_parse_markdown(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = correct_string_utf8(env, chat_parse_server(_str));
const char *_str = encode_to_utf8_chars(env, str);
jstring res = decode_to_utf8_string(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) {
const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE);
const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE);
jstring res = correct_string_utf8(env, chat_password_hash(_pwd, _salt));
const char *_pwd = encode_to_utf8_chars(env, pwd);
const char *_salt = encode_to_utf8_chars(env, salt);
jstring res = decode_to_utf8_string(env, chat_password_hash(_pwd, _salt));
(*env)->ReleaseStringUTFChars(env, pwd, _pwd);
(*env)->ReleaseStringUTFChars(env, salt, _salt);
return res;
@@ -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<NotificationAction> = emptyList())
abstract fun createNtfChannelsMaybeShowAlert()
abstract fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> 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
}
}
}
@@ -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,9 +119,11 @@ class SimplexWindowState {
val openMultipleDialog = DialogState<List<File>>()
val saveDialog = DialogState<File?>()
val toasts = mutableStateListOf<Pair<String, Long>>()
var windowFocused = mutableStateOf(true)
}
data class DialogParams(
val filename: String? = null,
val allowMultiple: Boolean = false,
val fileFilter: ((File?) -> Boolean)? = null,
val fileFilterDescription: String = "",
@@ -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<Pair<ChatId, Slice>>()
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<Pair<NotificationAction, () -> 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<Pair<String, () -> 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("\"", "\\\"")}""""))
}
}
@@ -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<NotificationAction>) {}
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<Pair<NotificationAction, () -> 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 {
@@ -37,15 +37,19 @@ actual class FileChooserLauncher actual constructor() {
}
actual suspend fun launch(input: String) {
val res = if (getContent) {
var res: File?
if (getContent) {
val params = DialogParams(
allowMultiple = false,
fileFilter = fileFilter(input),
fileFilterDescription = fileFilterDescription(input),
)
simplexWindowState.openDialog.awaitResult(params)
res = simplexWindowState.openDialog.awaitResult(params)
} else {
simplexWindowState.saveDialog.awaitResult()
res = simplexWindowState.saveDialog.awaitResult(DialogParams(filename = input))
if (res != null && res.isDirectory) {
res = File(res, input)
}
}
onResult(res?.toURI())
}
@@ -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
@@ -41,9 +41,9 @@ fun FrameWindowScope.FileDialogChooser(
onResult: (result: List<File>) -> Unit
) {
if (isLinux()) {
FileDialogChooserMultiple(title, isLoad, params.allowMultiple, params.fileFilter, params.fileFilterDescription, onResult)
FileDialogChooserMultiple(title, isLoad, params.filename, params.allowMultiple, params.fileFilter, params.fileFilterDescription, onResult)
} else {
FileDialogAwt(title, isLoad, params.allowMultiple, params.fileFilter, onResult)
FileDialogAwt(title, isLoad, params.filename, params.allowMultiple, params.fileFilter, onResult)
}
}
@@ -51,6 +51,7 @@ fun FrameWindowScope.FileDialogChooser(
fun FrameWindowScope.FileDialogChooserMultiple(
title: String,
isLoad: Boolean,
filename: String?,
allowMultiple: Boolean,
fileFilter: ((File?) -> Boolean)? = null,
fileFilterDescription: String? = null,
@@ -73,7 +74,11 @@ fun FrameWindowScope.FileDialogChooserMultiple(
val returned = if (isLoad) {
fileChooser.showOpenDialog(window)
} else {
fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
if (filename != null) {
fileChooser.selectedFile = File(filename)
} else {
fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
}
fileChooser.showSaveDialog(window)
}
val result = when (returned) {
@@ -109,6 +114,7 @@ fun FrameWindowScope.FileDialogChooserMultiple(
private fun FrameWindowScope.FileDialogAwt(
title: String,
isLoad: Boolean,
filename: String?,
allowMultiple: Boolean,
fileFilter: ((File?) -> Boolean)? = null,
onResult: (result: List<File>) -> Unit
@@ -128,6 +134,9 @@ private fun FrameWindowScope.FileDialogAwt(
}.apply {
this.title = title
this.isMultipleMode = allowMultiple && isLoad
if (!isLoad && filename != null) {
this.file = filename
}
if (fileFilter != null) {
this.setFilenameFilter { dir, file ->
fileFilter(File(dir.absolutePath + File.separator + file))
@@ -10,6 +10,7 @@ fun main() {
initHaskell()
initApp()
tmpDir.deleteRecursively()
tmpDir.mkdir()
return showApp()
}
+2 -2
View File
@@ -25,8 +25,8 @@ android.nonTransitiveRClass=true
android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
android.version_name=5.3-beta.0
android.version_code=136
android.version_name=5.3-beta.1
android.version_code=137
desktop.version_name=1.0