mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 00:02:28 +00:00
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:
commit
0e06ded808
996 files changed
+48336
-3431
No files matched your search
@@ -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())
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+6
@@ -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()
|
||||
+39
-7
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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()
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+7
-7
@@ -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
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+4
-4
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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)?) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+5
-2
@@ -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 =
|
||||
|
||||
+77
-23
@@ -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
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+6
@@ -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)
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
@@ -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
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+55
-15
@@ -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
|
||||
+3
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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() {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+3
-2
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+29
-15
@@ -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
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+4
-2
@@ -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)
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+13
-9
@@ -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 = {
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+34
-11
@@ -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 = {},
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+13
-9
@@ -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,
|
||||
|
||||
+1
-1
@@ -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.*
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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(
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
|
||||
+5
-1
@@ -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(
|
||||
|
||||
+23
-23
@@ -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
Reference in new issue
Block a user