Merge branch 'master' into f/directory-search

Co-authored-by: Claude <noreply@anthropic.com>

# Conflicts:
#	apps/simplex-directory-service/src/Directory/Events.hs
#	apps/simplex-directory-service/src/Directory/Service.hs
#	src/Simplex/Chat/Core.hs
#	src/Simplex/Chat/Library/Internal.hs
#	tests/Bots/DirectoryTests.hs
This commit is contained in:
spaced4ndy committed 2026-10-12 17:34:32 +00:00
commit 0e06ded808
996 files changed
+48336 -3431

No files matched your search

+12 -24
View File
@@ -56,11 +56,6 @@ kotlin {
}
val commonMain by getting {
if (hasSimplexAssets) {
resources.srcDir(simplexAssetsLocal)
} else {
resources.srcDir("src/commonMain/resources/assets/default")
}
dependencies {
api(compose.runtime)
api(compose.foundation)
@@ -78,8 +73,8 @@ kotlin {
// Link Previews
implementation("org.jsoup:jsoup:1.17.2")
// Resources
api("dev.icerock.moko:resources:0.23.0")
api("dev.icerock.moko:resources-compose:0.23.0")
api("dev.icerock.moko:resources:0.27.1")
api("dev.icerock.moko:resources-compose:0.27.1")
// Markdown
implementation("com.mikepenz:multiplatform-markdown-renderer:0.27.0")
@@ -95,7 +90,6 @@ kotlin {
}
}
val androidMain by getting {
kotlin.srcDir("build/generated/moko/androidMain/src")
dependencies {
implementation("androidx.activity:activity-compose:1.9.1")
val workVersion = "2.9.1"
@@ -143,6 +137,7 @@ kotlin {
}
// For jSystemThemeDetector only
implementation("net.java.dev.jna:jna-platform:5.14.0")
implementation("com.github.oshi:oshi-core:6.4.13")
implementation("com.sshtools:two-slices:0.9.1")
implementation("org.slf4j:slf4j-simple:2.0.12")
implementation("uk.co.caprica:vlcj:4.8.3")
@@ -157,27 +152,20 @@ kotlin {
android {
namespace = "chat.simplex.common"
compileSdk = 35
compileSdk = 36
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdk = 26
}
testOptions.targetSdk = 34
lint.targetSdk = 34
val isAndroid = gradle.startParameter.taskNames.find {
val lower = it.lowercase()
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
} != null
if (isAndroid) {
// This is not needed on Android but can't be moved to desktopMain because MR lib don't support this.
// No other ways to exclude a file work, but it's large and should be excluded
kotlin.sourceSets["commonMain"].resources.exclude("/MR/fonts/NotoColorEmoji-Regular.ttf")
}
testOptions.targetSdk = 36
lint.targetSdk = 36
}
multiplatformResources {
multiplatformResourcesPackage = "chat.simplex.res"
// multiplatformResourcesClassName = "MR"
resourcesPackage.set("chat.simplex.res")
resourcesSourceSets {
getByName("commonMain").srcDirs(if (hasSimplexAssets) simplexAssetsLocal.resolve("MR") else file("src/commonMain/resources/assets/default/MR"))
}
}
buildConfig {
@@ -285,8 +273,8 @@ afterEvaluate {
}
return this
}
val fileRegex = Regex("MR/../strings.xml$|MR/..-.../strings.xml$|MR/..-../strings.xml$|MR/base/strings.xml$")
val tree = kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath.replace("\\", "/")) }.asFileTree
val fileRegex = Regex("moko-resources/../strings.xml$|moko-resources/..-.../strings.xml$|moko-resources/..-../strings.xml$|moko-resources/base/strings.xml$")
val tree = fileTree("src/commonMain/moko-resources").filter { fileRegex.containsMatchIn(it.absolutePath.replace("\\", "/")) }.asFileTree
val baseStringsFile = tree.firstOrNull { it.absolutePath.replace("\\", "/").endsWith("base/strings.xml") } ?: throw Exception("No base/strings.xml found")
val lvStringsFile = tree.firstOrNull { it.absolutePath.replace("\\", "/").endsWith("lv/strings.xml") } ?: throw Exception("No base/strings.xml found")
val treeList = ArrayList(tree.toList())
@@ -6,7 +6,7 @@ import android.provider.Settings
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import chat.simplex.res.*
fun Context.openAppSettingsInSystem() {
Intent().apply {
@@ -0,0 +1,6 @@
package chat.simplex.common.platform
import androidx.compose.ui.graphics.drawscope.DrawScope
// Android blurs the bars with a RenderEffect on the layer itself, so this is never called there.
actual fun DrawScope.drawBarsBlurred(radiusPx: Float, barWidth: Float, barHeight: Float, drawBar: DrawScope.() -> Unit) = drawBar()
@@ -1,11 +1,14 @@
package chat.simplex.common.platform
import android.annotation.SuppressLint
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyInfo
import android.security.keystore.KeyProperties
import android.security.keystore.StrongBoxUnavailableException
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import chat.simplex.res.*
import java.security.KeyStore
import javax.crypto.*
import javax.crypto.spec.GCMParameterSpec
@@ -68,15 +71,44 @@ internal class Cryptor: CryptorInterface {
keyStore.deleteEntry(alias)
}
override fun keyStorage(alias: String): String? {
val secretKey = getSecretKey(alias) ?: return null
val keyInfo = SecretKeyFactory.getInstance(secretKey.algorithm, "AndroidKeyStore").getKeySpec(secretKey, KeyInfo::class.java) as KeyInfo
val storage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
when (keyInfo.securityLevel) {
KeyProperties.SECURITY_LEVEL_STRONGBOX -> MR.strings.keystore_key_storage_strongbox
KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> MR.strings.keystore_key_storage_tee
KeyProperties.SECURITY_LEVEL_SOFTWARE -> MR.strings.keystore_key_storage_software
else -> return null
}
} else {
// isInsideSecureHardware does not distinguish StrongBox, which createSecretKey only requests on API 31+
@Suppress("DEPRECATION")
if (keyInfo.isInsideSecureHardware) MR.strings.keystore_key_storage_tee else MR.strings.keystore_key_storage_software
}
return generalGetString(storage)
}
private fun createSecretKey(alias: String): SecretKey? {
if (keyStore.containsAlias(alias)) return getSecretKey(alias)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
return generateKey(keySpec(alias).setIsStrongBoxBacked(true))
} catch (e: StrongBoxUnavailableException) {
Log.i(TAG, "StrongBox is unavailable, using default keystore: ${e.message}")
}
}
return generateKey(keySpec(alias))
}
private fun keySpec(alias: String): KeyGenParameterSpec.Builder =
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
private fun generateKey(spec: KeyGenParameterSpec.Builder): SecretKey {
val keyGenerator: KeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM, "AndroidKeyStore")
keyGenerator.init(
KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
keyGenerator.init(spec.build())
return keyGenerator.generateKey()
}
@@ -2,6 +2,7 @@ package chat.simplex.common.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.graphics.painter.Painter
import java.io.File
@@ -18,3 +19,5 @@ actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this
actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this
actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier
actual fun newPasswordContentType(): ContentType = ContentType.NewPassword + ContentType.Password
@@ -39,7 +39,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.ui.theme.CurrentColors
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
@@ -10,7 +10,7 @@ import androidx.compose.runtime.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.AudioPlayer.duration
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
import java.io.*
@@ -15,7 +15,7 @@ import androidx.core.graphics.drawable.toBitmap
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import java.io.BufferedOutputStream
import java.io.File
import java.net.URI
@@ -17,7 +17,7 @@ import chat.simplex.common.AppScreen
import chat.simplex.common.model.clearAndNotify
import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show()
@@ -6,7 +6,7 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.ImageBitmap
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.*
import com.google.android.exoplayer2.audio.AudioAttributes
@@ -1,15 +1,15 @@
package chat.simplex.common.ui.theme
import androidx.compose.ui.text.font.*
import chat.simplex.res.MR
import chat.simplex.res.*
actual val Inter: FontFamily = FontFamily(
Font(MR.fonts.Inter.regular.fontResourceId),
Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic),
Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold),
Font(MR.fonts.Inter.semibold.fontResourceId, FontWeight.SemiBold),
Font(MR.fonts.Inter.medium.fontResourceId, FontWeight.Medium),
Font(MR.fonts.Inter.light.fontResourceId, FontWeight.Light)
Font(MR.fonts.inter_regular.fontResourceId),
Font(MR.fonts.inter_italic.fontResourceId, style = FontStyle.Italic),
Font(MR.fonts.inter_bold.fontResourceId, FontWeight.Bold),
Font(MR.fonts.inter_semibold.fontResourceId, FontWeight.SemiBold),
Font(MR.fonts.inter_medium.fontResourceId, FontWeight.Medium),
Font(MR.fonts.inter_light.fontResourceId, FontWeight.Light)
)
actual val EmojiFont: FontFamily = FontFamily.Default
@@ -8,7 +8,7 @@ import androidx.annotation.RequiresApi
import androidx.compose.runtime.*
import chat.simplex.common.platform.*
import dev.icerock.moko.resources.ImageResource
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import java.util.concurrent.Executors
@@ -46,7 +46,7 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource
import dev.icerock.moko.resources.compose.painterResource
@@ -132,7 +132,7 @@ actual fun ActiveCallView() {
}
Box(Modifier.fillMaxSize()) {
WebRTCView(chatModel.callCommand) { apiMsg ->
Log.d(TAG, "received from WebRTCView: $apiMsg")
Log.d(TAG, "received from WebRTCView: ${apiMsg.resp.javaClass.simpleName}")
val call = chatModel.activeCall.value
if (call != null) {
val callState = call.androidCallState as ActiveCallState
@@ -711,7 +711,7 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
.collect {
while (callCommand.isNotEmpty()) {
val cmd = callCommand.removeFirstOrNull()
Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd")
Log.d(TAG, "WebRTCView LaunchedEffect executing ${cmd?.javaClass?.simpleName}")
if (cmd != null) {
processCommand(wv, cmd)
}
@@ -785,7 +785,7 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
// onResponse(message)
onResponse(json.decodeFromString(message))
} catch (e: Exception) {
Log.e(TAG, "failed parsing WebView message: $message")
Log.e(TAG, "failed parsing WebView message")
}
}
}
@@ -11,7 +11,7 @@ import androidx.core.content.ContextCompat
import chat.simplex.common.helpers.toURI
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import java.net.URI
@Composable
@@ -5,7 +5,7 @@ import androidx.compose.runtime.*
import chat.simplex.common.model.CryptoFile
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.DefaultDropdownMenu
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import java.net.URI
@@ -14,7 +14,7 @@ import chat.simplex.common.model.MsgContent
import chat.simplex.common.platform.FileChooserLauncher
import chat.simplex.common.platform.saveImage
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import dev.icerock.moko.resources.compose.painterResource
@@ -13,7 +13,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.isVisible
import chat.simplex.common.platform.VideoPlayer
import chat.simplex.common.platform.androidAppContext
import chat.simplex.res.MR
import chat.simplex.res.*
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import coil.decode.GifDecoder
@@ -18,7 +18,7 @@ import chat.simplex.common.model.durationText
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
import kotlinx.datetime.Clock
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
@Composable
@@ -14,7 +14,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.SimplexGreen
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -59,6 +59,7 @@ actual fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
keyStorage: String?,
initialRandomDBPassphrase: MutableState<Boolean>,
migration: Boolean,
) {
@@ -66,7 +67,7 @@ actual fun DatabaseEncryptionFooter(
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
SectionTextFooter(String.format(generalGetString(MR.strings.keychain_is_storing_securely), keyStorage?.let { " ($it)" } ?: ""))
if (initialRandomDBPassphrase.value && !migration) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import chat.simplex.common.views.newchat.ActionButton
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -33,7 +33,7 @@ import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.json
import chat.simplex.common.platform.*
import chat.simplex.common.views.newchat.ActionButton
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.serialization.builtins.*
import java.io.File
import java.net.URI
@@ -24,7 +24,7 @@ import androidx.core.text.HtmlCompat
import chat.simplex.common.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import java.io.*
import java.net.URI
@@ -27,7 +27,7 @@ import boofcv.struct.image.GrayU8
import chat.simplex.common.platform.TAG
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.common.util.concurrent.ListenableFuture
@@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import chat.simplex.common.model.SharedPreference
import chat.simplex.common.model.User
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)?) {
@@ -32,7 +32,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.saveAppLocale
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.fragment.app.FragmentActivity
import chat.simplex.common.model.ChatModel
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -13,7 +13,7 @@ import androidx.compose.ui.text.style.TextAlign
import chat.simplex.common.model.ChatModel
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import com.jakewharton.processphoenix.ProcessPhoenix
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -4,6 +4,9 @@
//#include <stdlib.h>
//#include <android/log.h>
// from libc (stdlib.h is not included because of the reallocarray stub below)
void free(void *ptr);
// from the RTS
void hs_init_with_rtsopts(int * argc, char **argv[]);
@@ -49,6 +52,13 @@ Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused j
// from simplex-chat
typedef long* chat_ctrl;
// Strings returned by chat_* functions are allocated with malloc and owned by the caller.
static jstring decode_and_free(JNIEnv *env, char *string) {
jstring res = (*env)->NewStringUTF(env, string);
free(string);
return res;
}
/*
When you start using any new function from Haskell libraries,
you have to add the function name to the file libsimplex.dll.def in the root directory.
@@ -80,7 +90,7 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused j
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
jlong _ctrl = (jlong) 0;
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
jstring res = decode_and_free(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
(*env)->ReleaseStringUTFChars(env, confirm, _confirm);
@@ -99,7 +109,7 @@ Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused j
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jclass clazz, jlong controller) {
jstring res = (*env)->NewStringUTF(env, chat_close_store((void*)controller));
jstring res = decode_and_free(env, chat_close_store((void*)controller));
return res;
}
@@ -109,7 +119,7 @@ Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused
//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_retry((void*)controller, _msg, retryNum));
jstring res = decode_and_free(env, chat_send_cmd_retry((void*)controller, _msg, retryNum));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
return res;
}
@@ -117,25 +127,25 @@ Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum));
jstring res = decode_and_free(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum));
(*env)->ReleaseStringUTFChars(env, msg, _msg);
return res;
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
return decode_and_free(env, chat_recv_msg((void*)controller));
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) {
return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait));
return decode_and_free(env, chat_recv_msg_wait((void*)controller, wait));
}
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str));
jstring res = decode_and_free(env, chat_parse_markdown(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
@@ -143,7 +153,7 @@ Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, __unused
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused jclass clazz, jstring str) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str));
jstring res = decode_and_free(env, chat_parse_server(_str));
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
@@ -151,7 +161,7 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused j
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, __unused jclass clazz, jstring str, jint safe) {
const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_uri(_str, safe));
jstring res = decode_and_free(env, chat_parse_uri(_str, safe));
(*env)->ReleaseStringUTFChars(env, str, _str);
return res;
}
@@ -160,7 +170,7 @@ JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused 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 = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt));
jstring res = decode_and_free(env, chat_password_hash(_pwd, _salt));
(*env)->ReleaseStringUTFChars(env, pwd, _pwd);
(*env)->ReleaseStringUTFChars(env, salt, _salt);
return res;
@@ -169,7 +179,7 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz, jstring name) {
const char *_name = (*env)->GetStringUTFChars(env, name, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_valid_name(_name));
jstring res = decode_and_free(env, chat_valid_name(_name));
(*env)->ReleaseStringUTFChars(env, name, _name);
return res;
}
@@ -177,7 +187,7 @@ Java_chat_simplex_common_platform_CoreKt_chatValidName(JNIEnv *env, jclass clazz
JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatParseBadgeCode(JNIEnv *env, jclass clazz, jstring code) {
const char *_code = (*env)->GetStringUTFChars(env, code, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_parse_badge_code(_code));
jstring res = decode_and_free(env, chat_parse_badge_code(_code));
(*env)->ReleaseStringUTFChars(env, code, _code);
return res;
}
@@ -195,7 +205,7 @@ Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz
const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE);
jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer);
jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer);
jstring res = (*env)->NewStringUTF(env, chat_write_file((void*)controller, _path, buff, capacity));
jstring res = decode_and_free(env, chat_write_file((void*)controller, _path, buff, capacity));
(*env)->ReleaseStringUTFChars(env, path, _path);
return res;
}
@@ -229,6 +239,7 @@ Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz,
arr = (*env)->NewByteArray(env, len);
(*env)->SetByteArrayRegion(env, arr, 0, len, res + 1);
}
free(res);
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"),
@@ -243,7 +254,7 @@ JNIEXPORT jstring JNICALL
Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jlong controller, jstring from_path, jstring to_path) {
const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE);
const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_encrypt_file((void*)controller, _from_path, _to_path));
jstring res = decode_and_free(env, chat_encrypt_file((void*)controller, _from_path, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, to_path, _to_path);
return res;
@@ -255,7 +266,7 @@ Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass cla
const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE);
const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE);
const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path));
jstring res = decode_and_free(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path));
(*env)->ReleaseStringUTFChars(env, from_path, _from_path);
(*env)->ReleaseStringUTFChars(env, key, _key);
(*env)->ReleaseStringUTFChars(env, nonce, _nonce);
@@ -64,6 +64,7 @@ jstring decode_to_utf8_string(JNIEnv *env, char *string) {
(*env)->DeleteLocalRef(env, bb);
(*env)->DeleteLocalRef(env, charset);
(*env)->DeleteLocalRef(env, cb);
free(string);
return res;
}
@@ -239,6 +240,7 @@ Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz,
arr = (*env)->NewByteArray(env, len);
(*env)->SetByteArrayRegion(env, arr, 0, len, res + 1);
}
free(res);
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"),
@@ -37,7 +37,7 @@ import chat.simplex.common.views.localauth.VerticalDivider
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
@@ -207,7 +207,7 @@ fun MainScreen() {
SwitchingUsersView()
}
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
if (unauthorized.value) {
LaunchedEffect(Unit) {
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
// while the screen moves to a launcher. Detect it and prevent showing the auth
@@ -221,7 +221,8 @@ fun MainScreen() {
SplashView(true)
ModalManager.fullscreen.showPasscodeInView()
}
} else {
}
if (!unauthorized.value || chatModel.activeCallViewIsVisible.value) {
if (chatModel.showCallView.value) {
if (appPlatform.isAndroid) {
LaunchedEffect(Unit) {
@@ -235,6 +236,8 @@ fun MainScreen() {
ActiveCallView()
}
}
}
if (!unauthorized.value) {
ModalManager.fullscreen.showOneTimePasscodeInView()
AlertManager.privacySensitive.showInView()
if (onboarding == OnboardingStage.OnboardingComplete) {
@@ -10,7 +10,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.localauth.SetAppPasscodeView
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
// Spec: spec/client/navigation.md#AppLock
@@ -269,6 +269,11 @@ object AppLock {
fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000
fun recheckAuthState() {
if (ChatModel.showCallView.value) {
// BiometricPrompt drops a prompt requested while MainActivity is stopped (call on lock screen), so request it again
if (userAuthorized.value == false) runAuthenticate()
return
}
val enteredBackgroundVal = enteredBackground.value
val delay = ChatController.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
@@ -18,7 +18,7 @@ import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrationToDeviceState
import chat.simplex.common.views.migration.MigrationToState
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@@ -1129,7 +1129,7 @@ object ChatModel {
fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) {
val current = currentUser.value ?: return
val updated = current.copy(
profile = newProfile.toLocalProfile(current.profile.profileId),
profile = newProfile.toLocalProfile(current.profile.profileId).copy(localBadge = current.profile.localBadge, contactDomainVerified = current.profile.contactDomainVerified),
fullPreferences = preferences ?: current.fullPreferences
)
val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId }
@@ -3569,6 +3569,7 @@ data class ChatItem (
is RcvGroupEvent.MemberCreatedContact -> false
is RcvGroupEvent.MemberProfileUpdated -> false
is RcvGroupEvent.NewMemberPendingReview -> true
is RcvGroupEvent.MsgBadSignature -> false
}
is CIContent.SndGroupEventContent -> false
is CIContent.RcvConnEventContent -> false
@@ -5475,6 +5476,7 @@ sealed class RcvGroupEvent() {
@Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent()
@Serializable @SerialName("memberProfileUpdated") class MemberProfileUpdated(val fromProfile: Profile, val toProfile: Profile): RcvGroupEvent()
@Serializable @SerialName("newMemberPendingReview") class NewMemberPendingReview(): RcvGroupEvent()
@Serializable @SerialName("msgBadSignature") class MsgBadSignature(): RcvGroupEvent()
val text: String get() = text(isChannel = false)
@@ -5499,6 +5501,7 @@ sealed class RcvGroupEvent() {
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
is MemberProfileUpdated -> profileUpdatedText(fromProfile, toProfile)
is NewMemberPendingReview -> generalGetString(MR.strings.rcv_group_event_new_member_pending_review)
is MsgBadSignature -> generalGetString(MR.strings.rcv_group_event_msg_bad_signature)
}
private fun profileUpdatedText(from: Profile, to: Profile): String =
@@ -32,6 +32,7 @@ import chat.simplex.common.views.chat.item.contentModerationPostLink
import chat.simplex.common.views.chat.item.showContentBlockedAlert
import chat.simplex.common.views.chat.item.showQuotedItemDoesNotExistAlert
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.database.deleteDatabaseBackups
import chat.simplex.common.views.migration.MigrationFileLinkData
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.views.usersettings.*
@@ -39,7 +40,7 @@ import chat.simplex.common.views.usersettings.networkAndServers.defaultCondition
import chat.simplex.common.views.usersettings.networkAndServers.serverHostname
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR
import chat.simplex.res.*
import com.russhwolf.settings.Settings
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@@ -215,6 +216,7 @@ class AppPreferences {
val encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE, null)
val initializationVectorSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE, null)
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null)
val shouldDeleteDatabaseBackups = mkBoolPreference(SHARED_PREFS_SHOULD_DELETE_DATABASE_BACKUPS, false)
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
@@ -487,6 +489,7 @@ class AppPreferences {
private const val SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE = "EncryptedSelfDestructPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase"
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
private const val SHARED_PREFS_SHOULD_DELETE_DATABASE_BACKUPS = "ShouldDeleteDatabaseBackups"
private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized"
private const val SHARED_PREFS_SHOULD_IMPORT_APP_SETTINGS = "ShouldImportAppSettings"
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
@@ -686,6 +689,10 @@ object ChatController {
}
apiStartChat()
appPrefs.chatStopped.set(false)
if (appPrefs.shouldDeleteDatabaseBackups.get()) {
deleteDatabaseBackups()
appPrefs.shouldDeleteDatabaseBackups.set(false)
}
} catch (e: Throwable) {
Log.e(TAG, "failed starting chat $e")
throw e
@@ -941,7 +948,7 @@ object ChatController {
val r = json.decodeFromString<API>(rStr)
if (log) {
Log.d(TAG, "sendCmd response type ${r.responseType}")
if (r is API.Result && (r.res is CR.Response || r.res is CR.Invalid)) {
if (r is API.Result && ((r.res is CR.Response && !r.res.type.startsWith("call")) || r.res is CR.Invalid)) {
Log.d(TAG, "sendCmd response json $rStr")
}
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
@@ -958,7 +965,7 @@ object ChatController {
} else {
val r = json.decodeFromString<API>(rStr)
Log.d(TAG, "chatRecvMsg: ${r.responseType}")
if (r is API.Result && (r.res is CR.Response || r.res is CR.Invalid)) Log.d(TAG, "chatRecvMsg json: $rStr")
if (r is API.Result && ((r.res is CR.Response && !r.res.type.startsWith("call")) || r.res is CR.Invalid)) Log.d(TAG, "chatRecvMsg json: $rStr")
r
}
}
@@ -1931,8 +1938,10 @@ object ChatController {
val r = sendCmd(rh, CC.ApiUpdateProfile(userId, profile))
if (r is API.Result && r.res is CR.UserProfileNoChange) return profile to emptyList()
if (r is API.Result && r.res is CR.UserProfileUpdated) return r.res.toProfile to r.res.updateSummary.changedContacts
if (r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.DuplicateName) {
if (r is API.Error && (r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.DuplicateName || r.err is ChatError.ChatErrorChat && r.err.errorType is ChatErrorType.UserExists)) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.failed_to_create_user_duplicate_title), generalGetString(MR.strings.failed_to_create_user_duplicate_desc))
} else if (!(networkErrorAlert(r))) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_saving_profile), "${r.responseType}: ${r.details}")
}
Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}")
return null
@@ -1944,7 +1953,12 @@ object ChatController {
return when {
r is API.Result && r.res is CR.UserProfileNoChange -> null
r is API.Result && r.res is CR.UserProfileUpdated -> r.res.user.updateRemoteHostId(rh)
else -> throw Exception("failed to set profile address: ${r.responseType} ${r.details}")
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiSetProfileAddress", generalGetString(MR.strings.error_saving_profile), r)
}
throw Exception("failed to set profile address: ${r.responseType} ${r.details}")
}
}
}
@@ -3125,6 +3139,13 @@ object ChatController {
// TODO NtfManager.shared.notifyGroupInvitation
}
}
is CR.ReceivedGroupInvitationViaGroup -> {
if (active(r.user)) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
}
}
}
is CR.UserAcceptedGroupSent -> {
if (!active(r.user)) return
@@ -3375,21 +3396,13 @@ object ChatController {
chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId))
}
is CR.CallOffer -> {
// TODO askConfirmation?
// TODO check encryption is compatible
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey)
val useRelay = appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
Log.d(TAG, ".callOffer iceServers $iceServers")
chatModel.callCommand.add(WCallCommand.Offer(
offer = r.offer.rtcSession,
iceCandidates = r.offer.rtcIceCandidates,
media = r.callType.media,
aesKey = r.sharedKey,
iceServers = iceServers,
relay = useRelay
))
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, hasSharedKey = r.sharedKey != null)
if (r.askConfirmation) {
showUnencryptedCallAlert(call) { processCallOffer(r) }
} else {
processCallOffer(r)
}
}
}
is CR.CallAnswer -> {
@@ -3645,6 +3658,39 @@ object ChatController {
}
}
private fun processCallOffer(r: CR.CallOffer) {
val useRelay = appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
chatModel.callCommand.add(WCallCommand.Offer(
offer = r.offer.rtcSession,
iceCandidates = r.offer.rtcIceCandidates,
media = r.callType.media,
aesKey = r.sharedKey,
iceServers = iceServers,
relay = useRelay
))
}
private fun showUnencryptedCallAlert(call: Call, onContinue: () -> Unit) {
// the call may have ended, or a new one started with the same contact, while the alert was shown
fun offerPending(): Boolean {
val c = chatModel.activeCall.value
return c != null && c.remoteHostId == call.remoteHostId && c.contact.id == call.contact.id && c.callState == CallState.OfferReceived
}
val endCall = { if (offerPending()) withBGApi { chatModel.callManager.endCall(call) } }
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.call_not_encrypted_title),
text = generalGetString(MR.strings.call_not_encrypted_desc).format(call.contact.displayName),
confirmText = generalGetString(MR.strings.call_service_notification_end_call),
onConfirm = { endCall() },
dismissText = generalGetString(MR.strings.continue_to_next_step),
onDismiss = { if (offerPending()) onContinue() },
onDismissRequest = { endCall() },
destructive = true,
parseHtml = false
)
}
suspend fun leaveGroup(rh: Long?, groupId: Long) {
val groupInfo = apiLeaveGroup(rh, groupId)
if (groupInfo != null) {
@@ -4871,6 +4917,7 @@ sealed class UserServersError {
@Serializable @SerialName("storageMissing") data class StorageMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
@Serializable @SerialName("proxyMissing") data class ProxyMissing(val protocol: ServerProtocol, val user: UserRef?): UserServersError()
@Serializable @SerialName("duplicateServer") data class DuplicateServer(val protocol: ServerProtocol, val duplicateServer: String, val duplicateHost: String): UserServersError()
@Serializable @SerialName("tooManyHosts") data class TooManyHosts(val protocol: ServerProtocol, val tooManyHostsServer: String): UserServersError()
@Serializable @SerialName("duplicateChatRelayAddress") data class DuplicateChatRelayAddress(val duplicateChatRelay: String, val duplicateAddress: String): UserServersError()
val globalError: String?
@@ -4886,6 +4933,7 @@ sealed class UserServersError {
is StorageMissing -> this.protocol
is ProxyMissing -> this.protocol
is DuplicateServer -> this.protocol
is TooManyHosts -> this.protocol
is DuplicateChatRelayAddress -> null
}
@@ -4901,6 +4949,8 @@ sealed class UserServersError {
is ProxyMissing -> this.user?.let { "${userStr(it)} ${generalGetString(MR.strings.no_message_servers_configured_for_private_routing)}" }
?: generalGetString(MR.strings.no_message_servers_configured_for_private_routing)
is TooManyHosts -> String.format(generalGetString(MR.strings.server_has_too_many_hosts), ServerAddress.parseServerAddress(this.tooManyHostsServer)?.hostnames?.firstOrNull() ?: this.tooManyHostsServer)
else -> null
}
} else {
@@ -6790,6 +6840,7 @@ sealed class CR {
@Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("groupMembers") class GroupMembers(val user: UserRef, val group: Group): CR()
@Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("receivedGroupInvitationViaGroup") class ReceivedGroupInvitationViaGroup(val user: UserRef, val groupInfo: GroupInfo, val viaGroupInfo: GroupInfo, val viaMember: GroupMember, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR()
@Serializable @SerialName("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@@ -6990,6 +7041,7 @@ sealed class CR {
is LeftMemberUser -> "leftMemberUser"
is GroupMembers -> "groupMembers"
is ReceivedGroupInvitation -> "receivedGroupInvitation"
is ReceivedGroupInvitationViaGroup -> "receivedGroupInvitationViaGroup"
is GroupDeletedUser -> "groupDeletedUser"
is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting"
is MemberAccepted -> "memberAccepted"
@@ -7181,6 +7233,7 @@ sealed class CR {
is LeftMemberUser -> withUser(user, json.encodeToString(groupInfo))
is GroupMembers -> withUser(user, json.encodeToString(group))
is ReceivedGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmemberRole: $memberRole")
is ReceivedGroupInvitationViaGroup -> withUser(user, "groupInfo: $groupInfo\nviaGroupInfo: $viaGroupInfo\nviaMember: $viaMember\nmemberRole: $memberRole")
is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo))
is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member")
is MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
@@ -7233,9 +7286,9 @@ sealed class CR {
is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString())
is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}")
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}"
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations.map { it.copy(sharedKey = null) })}"
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: ${callInvitation.callType}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}")
is CallEnded -> withUser(user, "contact: ${contact.id}")
@@ -7555,7 +7608,8 @@ enum class RcvSwitchStatus {
@SerialName("switch_started") SwitchStarted,
@SerialName("sending_qadd") SendingQADD,
@SerialName("sending_quse") SendingQUSE,
@SerialName("received_message") ReceivedMessage
@SerialName("received_message") ReceivedMessage,
@SerialName("received_qend") ReceivedQEND
}
@Serializable
@@ -6,7 +6,7 @@ import chat.simplex.common.BuildConfigCommon
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.DefaultTheme
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import chat.simplex.res.*
import java.util.*
enum class AppPlatform {
@@ -0,0 +1,6 @@
package chat.simplex.common.platform
import androidx.compose.ui.graphics.drawscope.DrawScope
// Draws the bars' background blurred. The drawing to blur is given in bar coordinates, whatever size the blur runs at.
expect fun DrawScope.drawBarsBlurred(radiusPx: Float, barWidth: Float, barHeight: Float, drawBar: DrawScope.() -> Unit)
@@ -8,7 +8,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.helpers.DatabaseUtils.ksDatabasePassword
import chat.simplex.common.views.helpers.DatabaseUtils.randomDatabasePassword
import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
import java.io.File
import java.nio.ByteBuffer
@@ -4,6 +4,7 @@ interface CryptorInterface {
fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String?
fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray>
fun deleteKey(alias: String)
fun keyStorage(alias: String): String?
}
expect val cryptor: CryptorInterface
@@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import com.charleskorn.kaml.*
import kotlinx.serialization.encodeToString
import java.io.*
@@ -1,18 +1,22 @@
package chat.simplex.common.platform
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.autofill.ContentType
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.views.helpers.KeyChangeEffect
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import java.io.File
import kotlin.math.roundToInt
@Composable
expect fun Modifier.desktopOnExternalDrag(
@@ -29,14 +33,14 @@ expect fun Modifier.desktopPointerHoverIconHand(): Modifier
expect fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier
@Composable
fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState<Boolean>, showMenu: State<Boolean>,): Modifier {
fun Modifier.desktopModifyBlurredState(blurred: MutableState<Boolean>, showMenu: State<Boolean>,): Modifier {
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
if (appPlatform.isDesktop) {
KeyChangeEffect(blurRadius.value) {
blurred.value = enabled && blurRadius.value > 0
blurred.value = blurRadius.value > 0
}
}
return if (appPlatform.isDesktop && enabled && blurRadius.value > 0 && !showMenu.value) {
return if (appPlatform.isDesktop && blurRadius.value > 0 && !showMenu.value) {
var job: Job = remember { Job() }
LaunchedEffect(Unit) {
// The approach here is to allow menu to show up and to not blur the view. When menu is shown and mouse is hovering,
@@ -58,27 +62,37 @@ fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState<B
}
}
@Composable
fun blurHidesMedia(enabled: Boolean, blurred: State<Boolean>): Boolean =
enabled && blurred.value && remember { appPrefs.privacyMediaBlurRadius.state }.value > 0
@Composable
fun Modifier.privacyBlur(
enabled: Boolean,
fullSize: Boolean,
preview: ImageBitmap,
blurred: MutableState<Boolean> = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) },
scrollState: State<Boolean>,
onLongClick: () -> Unit = {}
): Modifier {
val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state }
return if (enabled && blurred.value) {
this then Modifier.blur(
radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp,
edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp))
)
return if (blurHidesMedia(true, blurred)) {
this then (if (fullSize) {
val blurredPreview = remember(preview, blurRadius.value) { preview.blurredBy(blurRadius.value) }
Modifier.drawWithContent { drawImage(blurredPreview, dstSize = IntSize(size.width.roundToInt(), size.height.roundToInt())) }
} else Modifier.drawWithCache {
val cropScale = maxOf(size.width / preview.width, size.height / preview.height)
val croppedSize = IntSize((preview.width * cropScale).roundToInt(), (preview.height * cropScale).roundToInt())
val blurredCropped = preview.blurredBy(blurRadius.value, croppedSize.width.toDp().value)
val offset = IntOffset(((size.width - croppedSize.width) / 2).roundToInt(), ((size.height - croppedSize.height) / 2).roundToInt())
onDrawWithContent { clipRect { drawImage(blurredCropped, dstOffset = offset, dstSize = croppedSize) } }
})
.combinedClickable(
onLongClick = onLongClick,
onClick = {
blurred.value = false
}
)
} else if (enabled && blurRadius.value > 0 && appPlatform.isAndroid) {
} else if (blurRadius.value > 0 && appPlatform.isAndroid) {
LaunchedEffect(Unit) {
snapshotFlow { scrollState.value }
.filter { it }
@@ -90,3 +104,29 @@ fun Modifier.privacyBlur(
this
}
}
// Calibrated so the resample blurs as much as Modifier.blur did at each radius; 360 read 5-75% stronger.
private const val BLURRED_MEDIA_WIDTH_DP = 400
// The width in-chat media is assumed to be drawn at; small views are blurred as much on screen.
private const val CHAT_MEDIA_WIDTH_DP = 360
// Bounds the first step: nothing bounds a decoded video frame, and reading every pixel of a 4K one would stall.
private const val RESAMPLE_MEDIA_FROM_SIDE = 512
private fun ImageBitmap.blurredBy(radius: Int, drawnWidthDp: Float = CHAT_MEDIA_WIDTH_DP.toFloat()): ImageBitmap {
if (width <= 0 || height <= 0) return this
val w = (BLURRED_MEDIA_WIDTH_DP * drawnWidthDp / CHAT_MEDIA_WIDTH_DP / radius).toInt().coerceIn(1, width)
val h = (w * height / width).coerceIn(1, BLURRED_MEDIA_WIDTH_DP)
val longest = maxOf(width, height)
var image = if (longest > RESAMPLE_MEDIA_FROM_SIDE) {
val step = RESAMPLE_MEDIA_FROM_SIDE.toFloat() / longest
scale((width * step).roundToInt().coerceAtLeast(1), (height * step).roundToInt().coerceAtLeast(1))
} else this
// One bilinear step from a large image undersamples it.
while (image.width / 2 > w) image = image.scale(image.width / 2, (image.height / 2).coerceAtLeast(1))
image = image.scale(w, h)
// Stretching w px straight to the screen shows their grid; each doubling smooths it, so the last stretch is small.
while (maxOf(image.width, image.height) < BLURRED_MEDIA_WIDTH_DP / 2) image = image.scale(image.width * 2, image.height * 2)
return image
}
expect fun newPasswordContentType(): ContentType
@@ -1,12 +1,13 @@
package chat.simplex.common.platform
import chat.simplex.common.AppLock
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 chat.simplex.res.*
import kotlinx.coroutines.delay
enum class NotificationAction {
@@ -96,6 +97,7 @@ abstract class NtfManager {
}
fun acceptCallAction(chatId: ChatId) {
AppLock.recheckAuthState()
chatModel.clearOverlays.value = true
val invitation = chatModel.callInvitations[chatId]
if (invitation == null) {
@@ -18,7 +18,7 @@ import chat.simplex.common.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.serialization.Transient
import java.util.UUID
@@ -45,7 +45,7 @@ import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.DeleteImageButton
import chat.simplex.common.views.usersettings.EditImageButton
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@@ -15,7 +15,7 @@ import chat.simplex.common.model.LocalBadge
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.NameWithBadge
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.datetime.Instant
@Composable
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.views.onboarding.ReadableTextWithLink
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun BadgesHowItWorksView() {
@@ -39,7 +39,7 @@ import chat.simplex.common.views.helpers.ModalView
import chat.simplex.common.views.helpers.ShareButton
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.views.helpers.withBGApi
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlin.math.abs
@Composable
@@ -23,7 +23,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.res.MR
import chat.simplex.res.*
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
enum class BadgePeriod {
@@ -32,7 +32,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
import chat.simplex.res.MR
import chat.simplex.res.*
private const val badgeCodePrefix = "SB"
private const val badgeCodeBodyLength = 20
@@ -29,7 +29,7 @@ import chat.simplex.common.views.newchat.lightStops
import chat.simplex.common.views.onboarding.HowItWorks
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun BadgesSupportSimplexView(modalManager: ModalManager) {
@@ -34,7 +34,7 @@ import chat.simplex.common.views.helpers.badgeTypeName
import chat.simplex.common.views.helpers.openVerifiedSimplexUri
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.views.usersettings.simplexTeamUri
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun BadgesYourBadgeView(badgeState: BadgeState, modalManager: ModalManager) {
@@ -21,7 +21,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingActionButton
import chat.simplex.common.views.onboarding.TextButtonBelowOnboardingButton
import chat.simplex.res.MR
import chat.simplex.res.*
// TODO [badges]: replace with types produced by the badge purchase API when it lands.
enum class BadgeLevel {
@@ -20,7 +20,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.BannerDismissButton
import chat.simplex.common.views.chatlist.bannerCard
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun SupportSimpleXBanner(
@@ -1,5 +1,6 @@
package chat.simplex.common.views.call
import chat.simplex.common.AppLock
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.withBGApi
@@ -51,13 +52,12 @@ class CallManager(val chatModel: ChatModel) {
callUUID = invitation.callUUID,
callState = CallState.InvitationAccepted,
initialCallType = invitation.callType.media,
sharedKey = invitation.sharedKey,
hasSharedKey = invitation.sharedKey != null,
androidCallState = platform.androidCreateActiveCallState()
)
showCallView.value = true
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
Log.d(TAG, "answerIncomingCall iceServers: $iceServers")
callCommand.add(WCallCommand.Start(
media = invitation.callType.media,
aesKey = invitation.sharedKey,
@@ -79,6 +79,7 @@ class CallManager(val chatModel: ChatModel) {
// Don't destroy WebView if you plan to accept next call right after this one
if (!switchingCall.value) {
AppLock.appWasHidden()
showCallView.value = false
activeCall.value?.androidCallState?.close()
activeCall.value = null
@@ -19,7 +19,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.common.views.usersettings.ProfilePreview
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.datetime.Clock
// Spec: spec/services/calls.md#IncomingCallAlertView
@@ -3,7 +3,7 @@ package chat.simplex.common.views.call
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@@ -21,7 +21,7 @@ data class Call(
val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video),
val localCapabilities: CallCapabilities? = null,
val peerMediaSources: CallMediaSources = CallMediaSources(),
val sharedKey: String? = null,
val hasSharedKey: Boolean = false,
var localCamera: VideoCamera = VideoCamera.User,
val connectionInfo: ConnectionInfo? = null,
var connectedAt: Instant? = null,
@@ -32,14 +32,14 @@ data class Call(
val androidCallState: Closeable
) {
val encrypted: Boolean get() = localEncrypted && sharedKey != null
val encrypted: Boolean get() = localEncrypted && hasSharedKey
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
val encryptionStatus: String get() = when(callState) {
CallState.WaitCapabilities -> ""
CallState.InvitationSent -> generalGetString(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
CallState.InvitationAccepted -> generalGetString(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
CallState.InvitationAccepted -> generalGetString(if (!hasSharedKey) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (!hasSharedKey) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
}
val hasVideo: Boolean
@@ -43,7 +43,7 @@ import chat.simplex.common.views.chat.group.ChatTTLOption
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@@ -1205,12 +1205,13 @@ fun SynchronizeConnectionButtonForce(syncConnectionForce: () -> Unit) {
}
@Composable
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit) {
fun VerifyCodeButton(contactVerified: Boolean, onClick: () -> Unit, disabled: Boolean = false) {
SettingsActionItem(
if (contactVerified) painterResource(MR.images.ic_verified_user) else painterResource(MR.images.ic_shield),
stringResource(if (contactVerified) MR.strings.view_security_code else MR.strings.verify_security_code),
click = onClick,
iconColor = MaterialTheme.colors.secondary,
disabled = disabled,
)
}
@@ -33,7 +33,7 @@ import chat.simplex.common.views.badges.openBadgesView
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.newchat.noShownBadge
import chat.simplex.common.views.usersettings.networkAndServers.serverHostname
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.ImageResource
import kotlinx.serialization.encodeToString
@@ -44,7 +44,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.common.views.newchat.ContactConnectionInfoView
import chat.simplex.common.views.newchat.alertProfileImageSize
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@@ -119,6 +119,20 @@ fun ChatView(
}
chat
} }
if (chatsCtx.secondaryContextFilter == null) {
// cleared when the view is gone rather than on back: Android keeps it on screen while it slides out,
// and by then a channel being created may already be using this state
DisposableEffect(Unit) {
onDispose {
if (chatModel.chatId.value == null && chatModel.creatingChannelId.value == null) {
chatModel.groupMembers.value = emptyList()
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.membersLoaded.value = false
ChannelRelaysModel.reset()
}
}
}
}
val user = chatModel.currentUser.value
val chatInfo = activeChat.value?.chatInfo
if (chat == null || chatInfo == null || user == null) {
@@ -143,7 +157,7 @@ fun ChatView(
)
}
val attachmentOption = rememberSaveable { mutableStateOf<AttachmentOption?>(null) }
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden, skipHalfExpanded = true)
val scope = rememberCoroutineScope()
val selectedChatItems = rememberSaveable { mutableStateOf(null as Set<Long>?) }
val showCommandsMenu = rememberSaveable { mutableStateOf(false) }
@@ -382,10 +396,6 @@ fun ChatView(
hideKeyboard(view)
AudioPlayer.stop()
chatModel.chatId.value = null
chatModel.groupMembers.value = emptyList()
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.membersLoaded.value = false
ChannelRelaysModel.reset()
},
info = {
if (ModalManager.end.hasModalsOpen()) {
@@ -499,6 +509,15 @@ fun ChatView(
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
groupMembersJob.cancel()
val connStats = mutableStateOf<ConnectionStats?>(null)
val connectionCode = mutableStateOf<String?>(null)
val connectionLoaded = mutableStateOf(false)
if (chatsCtx.secondaryContextFilter == null) {
ModalManager.end.closeModals()
}
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
GroupMemberInfoView(chatRh, groupInfo, member, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
}
groupMembersJob = scope.launch(Dispatchers.Default) {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
@@ -513,15 +532,9 @@ fun ChatView(
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertGroupMember(chatRh, groupInfo, updatedMember)
}
if (chatsCtx.secondaryContextFilter == null) {
ModalManager.end.closeModals()
}
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { close ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
GroupMemberInfoView(chatRh, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, close = close, closeAll = close)
}
}
connStats.value = stats
connectionCode.value = code
connectionLoaded.value = true
}
},
loadMessages = { chatId, pagination, visibleItemIndexes ->
@@ -2467,6 +2480,7 @@ fun BoxScope.ChatItemsList(
LaunchedEffect(Unit) {
snapshotFlow { listState.value.isScrollInProgress }
.onCompletion { chatViewScrollState.value = false }
.collect {
chatViewScrollState.value = it
}
@@ -25,7 +25,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.chat.group.*
import chat.simplex.common.views.chat.item.sendCommandMsg
import chat.simplex.common.views.helpers.commandMenuAnimSpec
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.launch
@@ -12,7 +12,7 @@ import chat.simplex.common.model.MsgChatLink
import chat.simplex.common.ui.theme.appColors
import chat.simplex.common.views.helpers.ProfileImage
import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun ComposeChatLinkView(
@@ -18,7 +18,7 @@ import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chatlist.acceptContactRequest
import chat.simplex.common.views.chatlist.rejectContactRequest
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
@@ -16,7 +16,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
@@ -16,7 +16,7 @@ import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.group.removeMember
import chat.simplex.common.views.chat.group.removeMemberDialog
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -21,7 +21,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.IncognitoOptionImage
import chat.simplex.common.views.usersettings.IncognitoView
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -12,7 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.*
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) {
@@ -14,7 +14,7 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.common.platform.base64ToBitmap
import chat.simplex.res.MR
import chat.simplex.res.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.UploadContent
@@ -42,7 +42,7 @@ import chat.simplex.common.views.newchat.RelayProgressIndicator
import chat.simplex.common.views.newchat.RelayStatusIndicator
import chat.simplex.common.views.newchat.noShownBadge
import chat.simplex.common.views.newchat.relayDisplayName
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.ImageResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -2153,7 +2153,8 @@ private fun ownerRelayState(chat: Chat, chatModel: ChatModel): OwnerRelayState?
if (!gInfo.useRelays || gInfo.membership.memberRole != GroupMemberRole.Owner ||
gInfo.membership.memberStatus in listOf(GroupMemberStatus.MemLeft, GroupMemberStatus.MemRemoved, GroupMemberStatus.MemGroupDeleted)
) return null
val relays = if (ChannelRelaysModel.groupId.value == gInfo.groupId) ChannelRelaysModel.groupRelays.toList() else emptyList()
if (ChannelRelaysModel.groupId.value != gInfo.groupId) return null
val relays = ChannelRelaysModel.groupRelays.toList()
if (relays.isEmpty()) return OwnerRelayState(emptyList(), 0, 0, 0, true)
val relayMembers = relays.map { relay ->
relay to chatModel.groupMembers.value.firstOrNull { it.groupMemberId == relay.groupMemberId }
@@ -2186,6 +2187,7 @@ private fun subscriberRelayState(chat: Chat, chatModel: ChatModel): SubscriberRe
val connectedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connStatus == ConnStatus.Ready && it.activeConn?.connFailedErr == null }
val failedCount = relayMembers.count { !relayMemberRemoved(it.memberStatus) && it.activeConn?.connFailedErr != null }
val total = if (relayMembers.isNotEmpty()) relayMembers.size else hostnames.size
if (total == 0 && !chatModel.membersLoaded.value) return null
val noActiveRelays = connectedCount == 0 && (removedCount + failedCount) == total
return SubscriberRelayState(hostnames, relayMembers, connectedCount, removedCount, failedCount, total, noActiveRelays)
}
@@ -22,7 +22,7 @@ import chat.simplex.common.model.durationText
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.platform.AudioPlayer
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.flow.distinctUntilChanged
@Composable
@@ -21,7 +21,7 @@ import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
@Composable
@@ -22,7 +22,7 @@ import chat.simplex.common.views.chat.item.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.getLoadedFilePath
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.ImageResource
import kotlinx.datetime.Clock
@@ -9,7 +9,7 @@ import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCodeScanner
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.stringResource
@Composable
@@ -17,7 +17,7 @@ import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.helpers.*
import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
@Composable
@@ -24,7 +24,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatItem
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.stringResource
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
@@ -15,7 +15,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
@@ -39,7 +39,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.item.itemPrefixText
import chat.simplex.common.views.chat.item.itemSegmentDisplayText
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
@@ -21,7 +21,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.QRCode
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun VerifyCodeView(
@@ -30,7 +30,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@@ -19,7 +19,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.chatRelayDisplayName
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.launch
@@ -21,7 +21,7 @@ import chat.simplex.common.views.chat.ownersContributorsCountStr
import chat.simplex.common.views.chat.subscriberCountStr
import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun ModalData.ChannelMembersView(
@@ -20,7 +20,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.setGroupMembers
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
@Composable
@@ -23,7 +23,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
@@ -49,7 +49,7 @@ import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.database.TtlOptions
import chat.simplex.common.views.newchat.SimpleXLinkQRCode
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@@ -123,6 +123,15 @@ fun ModalData.GroupChatInfoView(
}
},
showMemberInfo = { member, groupRelay ->
val connStats = mutableStateOf<ConnectionStats?>(null)
val connectionCode = mutableStateOf<String?>(null)
val connectionLoaded = mutableStateOf(false)
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
GroupMemberInfoView(rhId, groupInfo, member, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
closeCurrent()
close()
}
}
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
@@ -132,14 +141,9 @@ fun ModalData.GroupChatInfoView(
} else {
member to null
}
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = false, groupRelay = groupRelay, close = closeCurrent) {
closeCurrent()
close()
}
}
}
connStats.value = stats
connectionCode.value = code
connectionLoaded.value = true
}
},
editGroupProfile = {
@@ -24,7 +24,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun GroupLinkView(
@@ -36,7 +36,7 @@ import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.openDirectChat
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.datetime.Clock
import kotlinx.coroutines.*
@@ -45,10 +45,11 @@ import kotlinx.coroutines.*
fun GroupMemberInfoView(
rhId: Long?,
groupInfo: GroupInfo,
member: GroupMember,
groupMember: GroupMember,
scrollToItemId: MutableState<Long?>,
connectionStats: ConnectionStats?,
connectionCode: String?,
connStats: MutableState<ConnectionStats?>,
connectionCode: State<String?>,
connectionLoaded: State<Boolean>,
chatModel: ChatModel,
openedFromSupportChat: Boolean,
groupRelay: GroupRelay? = null,
@@ -60,7 +61,10 @@ fun GroupMemberInfoView(
}
BackHandler(onBack = close)
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId }
val connStats = remember { mutableStateOf(connectionStats) }
// the passed member is shown until the loaded one is added to the model, so that the profile opens without waiting for the core
val member = remember(groupMember.groupMemberId) {
derivedStateOf { chatModel.getGroupMember(groupMember.groupMemberId) ?: groupMember }
}.value
val developerTools = chatModel.controller.appPrefs.developerTools.get()
var progressIndicator by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
@@ -91,7 +95,8 @@ fun GroupMemberInfoView(
connStats,
newRole,
developerTools,
connectionCode,
connectionCode.value,
connectionLoaded.value,
groupRelay = groupRelay,
getContactChat = { chatModel.getContactChat(it) },
openDirectChat = { contactId ->
@@ -101,6 +106,7 @@ fun GroupMemberInfoView(
}
},
createMemberContact = {
val connectionStats = connStats.value
if (member.sendMsgEnabled) {
withBGApi {
progressIndicator = true
@@ -209,7 +215,7 @@ fun GroupMemberInfoView(
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
VerifyCodeView(
mem.displayName,
connectionCode,
connectionCode.value,
mem.verified,
verify = { code ->
chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r ->
@@ -368,6 +374,7 @@ fun GroupMemberInfoLayout(
newRole: MutableState<GroupMemberRole>,
developerTools: Boolean,
connectionCode: String?,
connectionLoaded: Boolean,
groupRelay: GroupRelay? = null,
getContactChat: (Long) -> Chat?,
openDirectChat: (Long) -> Unit,
@@ -538,22 +545,23 @@ fun GroupMemberInfoLayout(
SectionDividerSpaced()
}
val memberConnected = member.memberActive || (groupInfo.useRelays && member.memberCurrent)
val showMemberSupportChat = !openedFromSupportChat &&
groupInfo.membership.memberRole >= GroupMemberRole.Moderator &&
member.memberRole != GroupMemberRole.Relay &&
((groupInfo.fullGroupPreferences.support.on && member.memberRole < GroupMemberRole.Moderator)
|| member.supportChat != null)
val canVerifyCode = connectionCode != null && member.memberRole != GroupMemberRole.Relay
// the same condition decides whether the code is requested, so the row is shown disabled until it arrives
val canVerifyCode = member.memberRole != GroupMemberRole.Relay && (connectionCode != null || !connectionLoaded)
val canSyncConn = cStats != null && cStats.ratchetSyncAllowed
if ((member.memberActive || (groupInfo.useRelays && member.memberCurrent))
&& (showMemberSupportChat || canVerifyCode || canSyncConn)) {
if (memberConnected && (showMemberSupportChat || canVerifyCode || canSyncConn)) {
SectionView {
if (showMemberSupportChat) {
SupportChatButton()
}
if (canVerifyCode) {
VerifyCodeButton(member.verified, verifyClicked)
VerifyCodeButton(member.verified, verifyClicked, disabled = connectionCode == null)
}
if (canSyncConn) {
SynchronizeConnectionButton(syncMemberConnection)
@@ -655,6 +663,20 @@ fun GroupMemberInfoLayout(
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
}
}
} else if (!connectionLoaded && memberConnected) {
SectionDividerSpaced()
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
// the status is set from the same receiving queues as the servers below, so it is reserved with them
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text(stringResource(MR.strings.network_status))
Icon(painterResource(MR.images.ic_info), stringResource(MR.strings.network_status), tint = MaterialTheme.colors.secondary)
}
}
SwitchAddressButton(disabled = true, switchAddress = {})
SimplexServers(stringResource(MR.strings.receiving_via), emptyList())
SimplexServers(stringResource(MR.strings.sending_via), emptyList())
}
}
val connFailedErr = member.activeConn?.connFailedErr
@@ -1117,6 +1139,7 @@ fun PreviewGroupMemberInfoLayout() {
newRole = remember { mutableStateOf(GroupMemberRole.Member) },
developerTools = false,
connectionCode = "123",
connectionLoaded = true,
getContactChat = { Chat.sampleData },
openDirectChat = {},
createMemberContact = {},
@@ -16,7 +16,7 @@ import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.WarningOrange
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
@Composable
@@ -21,7 +21,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.setGroupMembers
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.launch
import kotlin.text.CharCategory.*
@@ -21,7 +21,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
@@ -23,7 +23,7 @@ import chat.simplex.common.views.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
import java.net.URI
@@ -9,7 +9,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.*
@@ -18,7 +18,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -16,7 +16,7 @@ import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@@ -76,6 +76,15 @@ fun MemberSupportChatAppBar(
navigationButton = { NavigationButtonBack(onBackClicked) },
title = { MemberSupportChatToolbarTitle(scopeMember_) },
onTitleClick = {
val connStats = mutableStateOf<ConnectionStats?>(null)
val connectionCode = mutableStateOf<String?>(null)
val connectionLoaded = mutableStateOf(false)
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
GroupMemberInfoView(rhId, groupInfo, scopeMember_, scrollToItemId, connStats, connectionCode, connectionLoaded, chatModel, openedFromSupportChat = true, close = closeCurrent) {
closeCurrent()
close()
}
}
withBGApi {
val r = chatModel.controller.apiGroupMemberInfo(rhId, groupInfo.groupId, scopeMember_.groupMemberId)
val stats = r?.second
@@ -85,14 +94,9 @@ fun MemberSupportChatAppBar(
} else {
null
}
ModalManager.end.showModalCloseable(showClose = true, cardScreen = true) { closeCurrent ->
remember { derivedStateOf { chatModel.getGroupMember(scopeMember_.groupMemberId) } }.value?.let { mem ->
GroupMemberInfoView(rhId, groupInfo, mem, scrollToItemId, stats, code, chatModel, openedFromSupportChat = true, close = closeCurrent) {
closeCurrent()
close()
}
}
}
connStats.value = stats
connectionCode.value = code
connectionLoaded.value = true
}
},
onTop = !oneHandUI.value || !chatBottomBar.value,
@@ -26,7 +26,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.*
@@ -28,7 +28,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.*
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.*
private const val maxByteCount = 1200
@@ -8,7 +8,7 @@ import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun CIBrokenComposableView(alignment: Alignment) {
@@ -12,7 +12,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.model.*
import chat.simplex.common.views.helpers.SimpleButton
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun CICallItemView(
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.ProfileImage
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.stringResource
@Composable
@@ -11,7 +11,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.res.MR
import chat.simplex.res.*
@Composable
fun CIFeaturePreferenceView(
@@ -25,7 +25,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import chat.simplex.res.*
import java.io.File
import java.net.URI
@@ -19,7 +19,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.res.MR
import chat.simplex.res.*
import kotlinx.coroutines.delay
@Composable
@@ -29,6 +29,7 @@ fun CIGroupInvitationView(
memberRole: GroupMemberRole,
showTimestamp: Boolean,
chatIncognito: Boolean = false,
groupChat: Boolean = false,
joinGroup: (Long, () -> Unit) -> Unit,
timedMessagesTTL: Int?
) {
@@ -111,6 +112,9 @@ fun CIGroupInvitationView(
val secondaryColor = MaterialTheme.colors.secondary
Divider(Modifier.fillMaxWidth().padding(top = 8.dp))
Column(Modifier.padding(top = 8.dp, bottom = 4.dp, start = 4.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
if (groupChat) {
Text(if (sent) String.format(stringResource(MR.strings.group_invitation_for_member_only), groupInvitation.localDisplayName) else stringResource(MR.strings.group_invitation_for_you_only), fontSize = 12.sp, color = secondaryColor)
}
if (action) {
Text(groupInvitationStr(), fontSize = 13.sp, lineHeight = 18.sp)
Text(
@@ -26,7 +26,7 @@ import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
import chat.simplex.common.views.chat.chatViewScrollState
import chat.simplex.res.MR
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import kotlin.math.roundToInt
@@ -114,13 +114,13 @@ fun CIImageView(
onClick = onClick
)
.onRightClick { showMenu.value = true }
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
.privacyBlur(!smallView, imageBitmap, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
)
}
@Composable
fun ImageView(painter: Painter, image: String, fileSource: CryptoFile?, onClick: () -> Unit) {
fun ImageView(painter: Painter, image: String, onClick: () -> Unit) {
// On my Android device Compose fails to display 6000x6000 px WebP image with exception:
// IllegalStateException: Recording currently in progress - missing #endRecording() call?
// but can display 5000px image. Using even lower value here just to feel safer.
@@ -138,7 +138,7 @@ fun CIImageView(
onClick = onClick
)
.onRightClick { showMenu.value = true }
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
.privacyBlur(!smallView, previewBitmap, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth,
)
} else {
@@ -149,14 +149,10 @@ fun CIImageView(
onClick = {}
)
.onRightClick { showMenu.value = true }
.privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
.privacyBlur(!smallView, previewBitmap, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }),
contentAlignment = Alignment.Center
) {
imageView(previewBitmap, onClick = {
if (fileSource != null) {
openFile(fileSource)
}
})
imageView(previewBitmap, onClick = onClick)
Icon(
painterResource(MR.images.ic_open_in_new),
contentDescription = stringResource(MR.strings.image_descr),
@@ -191,29 +187,33 @@ fun CIImageView(
}
} else Modifier
)
.desktopModifyBlurredState(!smallView, blurred, showMenu),
.desktopModifyBlurredState(blurred, showMenu),
contentAlignment = Alignment.TopEnd
) {
val res: MutableState<Triple<ImageBitmap, ByteArray, String>?> = remember { mutableStateOf(null) }
if (chatModel.connectedToRemote()) {
LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) {
withBGApi {
// Hidden media is not worth reading, decoding at full size and caching.
val revealed = !blurHidesMedia(!smallView, blurred)
if (revealed) {
if (chatModel.connectedToRemote()) {
LaunchedEffect(file, CIFile.cachedRemoteFileRequests.toList()) {
withBGApi {
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
res.value = imageAndFilePath(file)
}
}
}
} else {
LaunchedEffect(file) {
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
res.value = imageAndFilePath(file)
res.value = withContext(Dispatchers.IO) { imageAndFilePath(file) }
}
}
}
} else {
LaunchedEffect(file) {
if (res.value == null || res.value!!.third != getLoadedFilePath(file)) {
res.value = withContext(Dispatchers.IO) { imageAndFilePath(file) }
}
}
}
val loaded = res.value
val loaded = if (revealed) res.value else null
if (loaded != null && file != null) {
val (imageBitmap, data, _) = loaded
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, blurred, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, blurred, @Composable { painter, onClick -> ImageView(painter, image, onClick) })
} else {
imageView(previewBitmap, onClick = {
if (file != null) {
Loaded 100 of 996 files, more files were not shown because too many files have changed in this diff. Show more