mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d50562cfee | |||
| 15d3d3b11a | |||
| 2b715a0d8c | |||
| 02d00944ff |
@@ -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()
|
||||
}
|
||||
|
||||
+1
-51
@@ -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) {
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+101
-5
@@ -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 = "",
|
||||
|
||||
+153
@@ -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("\"", "\\\"")}""""))
|
||||
}
|
||||
}
|
||||
+7
-10
@@ -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 {
|
||||
|
||||
+7
-3
@@ -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())
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+12
-3
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user