mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 01:35:08 +00:00
Merge branch 'master' into f/directory-integration-plan
This commit is contained in:
@@ -16,6 +16,7 @@ android/build
|
||||
android/release
|
||||
common/build
|
||||
desktop/build
|
||||
external/nanohttpd/build
|
||||
release
|
||||
|
||||
# Generated SimpleX assets
|
||||
|
||||
@@ -289,6 +289,7 @@ desktop/src/jvmMain/kotlin/chat/simplex/desktop/ -- Desktop app (1 file)
|
||||
| common/.../common/StoreWindowState.kt (desktopMain) | spec/architecture.md | product/views/settings.md |
|
||||
| common/.../common/model/NtfManager.desktop.kt (desktopMain) | spec/services/notifications.md | product/flows/messaging.md |
|
||||
| common/.../common/views/helpers/AppUpdater.kt (desktopMain) | spec/architecture.md | product/views/settings.md |
|
||||
| common/.../common/platform/AnimatedImage.desktop.kt (desktopMain) | spec/client/chat-view.md | product/views/chat.md |
|
||||
|
||||
### Haskell Core Sources (at `../../src/Simplex/Chat/` relative to `apps/multiplatform/`)
|
||||
|
||||
|
||||
@@ -6,14 +6,31 @@ This is a guide to contributing to the develop of the SimpleX android and deskto
|
||||
|
||||
This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX Chat, sharing code between Android and Desktop (JVM) platforms using Compose Multiplatform for UI.
|
||||
|
||||
## Setup
|
||||
|
||||
The desktop app builds nanohttpd from a submodule, Android does not use it. Before building
|
||||
the desktop app on a fresh checkout:
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Android debug APK
|
||||
./gradlew assembleDebug
|
||||
# Android debug APK, assembleGoogleDebug builds the flavor with the Play Billing dependency
|
||||
./gradlew assembleFossDebug
|
||||
|
||||
# Android release APK
|
||||
./gradlew assembleRelease
|
||||
# Android release APK, distributed via F-Droid and GitHub
|
||||
./gradlew assembleFossRelease
|
||||
|
||||
# Android app bundle, distributed via Google Play, includes Play Billing
|
||||
./gradlew bundleGoogleRelease
|
||||
|
||||
# Always name the flavor for releases. The aggregate tasks (build, assemble, assembleRelease,
|
||||
# bundle, bundleRelease) fail on purpose: they would package a release APK with Play Billing,
|
||||
# or an app bundle without it.
|
||||
# The fdroiddata recipe defaults to assembleRelease and must be changed to assembleFossRelease.
|
||||
|
||||
# Desktop distribution (current OS)
|
||||
./gradlew :desktop:packageDistributionForCurrentOS
|
||||
@@ -22,7 +39,7 @@ This is the **Kotlin Multiplatform (KMP)** mobile and desktop client for SimpleX
|
||||
./gradlew desktopTest
|
||||
|
||||
# Run Android instrumented tests (requires connected device/emulator)
|
||||
./gradlew connectedAndroidTest
|
||||
./gradlew connectedFossDebugAndroidTest
|
||||
|
||||
# Build native libraries for all platforms
|
||||
./gradlew common:cmakeBuild -PcrossCompile
|
||||
|
||||
@@ -35,6 +35,21 @@ android {
|
||||
manifestPlaceholders["extract_native_libs"] = rootProject.extra["compression.level"] as Int != 0
|
||||
}
|
||||
|
||||
// `google` is distributed via Google Play as an app bundle and includes Play Billing.
|
||||
// `foss` is distributed via F-Droid and as APKs on GitHub, without Play dependencies.
|
||||
flavorDimensions += "store"
|
||||
productFlavors {
|
||||
create("google") {
|
||||
dimension = "store"
|
||||
buildConfigField("boolean", "PLAY_STORE", "true")
|
||||
}
|
||||
create("foss") {
|
||||
dimension = "store"
|
||||
isDefault = true
|
||||
buildConfigField("boolean", "PLAY_STORE", "false")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = rootProject.extra["application_id.suffix"] as String
|
||||
@@ -128,8 +143,28 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// The graph is checked rather than the requested task, because every aggregate task
|
||||
// (assemble, assembleRelease, build, bundle, ...) packages these variants too.
|
||||
val projectPath = project.path
|
||||
val apkTasks = setOf("packageFossDebug", "packageGoogleDebug", "packageFossRelease", "packageGoogleRelease")
|
||||
val apkTaskPaths = apkTasks.map { "$projectPath:$it" }.toSet()
|
||||
val bundleTaskPaths = apkTaskPaths.map { it + "Bundle" }.toSet()
|
||||
gradle.taskGraph.whenReady {
|
||||
if (hasTask("$projectPath:packageGoogleRelease")) {
|
||||
throw GradleException("A release apk must not include Play Billing, use assembleFossRelease or bundleGoogleRelease")
|
||||
}
|
||||
if (hasTask("$projectPath:packageFossReleaseBundle")) {
|
||||
throw GradleException("An app bundle must include Play Billing, use bundleGoogleRelease or assembleFossRelease")
|
||||
}
|
||||
// `isBundle` above is derived from the whole invocation, so a bundle in it disables abi splits
|
||||
if (apkTaskPaths.any { hasTask(it) } && bundleTaskPaths.any { hasTask(it) }) {
|
||||
throw GradleException("Build the apks and the bundle in separate invocations, the bundle disables abi splits")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
"googleImplementation"("com.android.billingclient:billing:9.1.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
|
||||
//implementation("androidx.compose.material:material:$compose_version")
|
||||
@@ -160,58 +195,61 @@ dependencies {
|
||||
tasks {
|
||||
val compressApk by creating {
|
||||
doLast {
|
||||
val isRelease = gradle.startParameter.taskNames.find { it.lowercase().contains("release") } != null
|
||||
val buildType: String = if (isRelease) "release" else "debug"
|
||||
val javaHome = System.getProperties()["java.home"] ?: org.gradle.internal.jvm.Jvm.current().javaHome
|
||||
val sdkDir = android.sdkDirectory.absolutePath
|
||||
val keyAlias: String
|
||||
val keyPassword: String
|
||||
val storeFile: String
|
||||
val storePassword: String
|
||||
if (project.properties["android.injected.signing.key.alias"] != null) {
|
||||
keyAlias = project.properties["android.injected.signing.key.alias"] as String
|
||||
keyPassword = project.properties["android.injected.signing.key.password"] as String
|
||||
storeFile = project.properties["android.injected.signing.store.file"] as String
|
||||
storePassword = project.properties["android.injected.signing.store.password"] as String
|
||||
} else {
|
||||
try {
|
||||
val gradleConfig = android.signingConfigs.getByName(buildType)
|
||||
keyAlias = gradleConfig.keyAlias!!
|
||||
keyPassword = gradleConfig.keyPassword!!
|
||||
storeFile = gradleConfig.storeFile!!.absolutePath
|
||||
storePassword = gradleConfig.storePassword!!
|
||||
} catch (e: UnknownDomainObjectException) {
|
||||
// There is no signing config for current build type, can"t sign the apk
|
||||
println("No signing configs for this build type: $buildType")
|
||||
return@doLast
|
||||
// A single invocation can package more than one variant, for example assembleDebug
|
||||
gradle.taskGraph.allTasks.filter { it.path in apkTaskPaths }.forEach { packageTask ->
|
||||
val variant = packageTask.name.removePrefix("package")
|
||||
val buildType: String = if (variant.endsWith("Release")) "release" else "debug"
|
||||
val keyAlias: String
|
||||
val keyPassword: String
|
||||
val storeFile: String
|
||||
val storePassword: String
|
||||
if (project.properties["android.injected.signing.key.alias"] != null) {
|
||||
keyAlias = project.properties["android.injected.signing.key.alias"] as String
|
||||
keyPassword = project.properties["android.injected.signing.key.password"] as String
|
||||
storeFile = project.properties["android.injected.signing.store.file"] as String
|
||||
storePassword = project.properties["android.injected.signing.store.password"] as String
|
||||
} else {
|
||||
try {
|
||||
val gradleConfig = android.signingConfigs.getByName(buildType)
|
||||
keyAlias = gradleConfig.keyAlias!!
|
||||
keyPassword = gradleConfig.keyPassword!!
|
||||
storeFile = gradleConfig.storeFile!!.absolutePath
|
||||
storePassword = gradleConfig.storePassword!!
|
||||
} catch (e: UnknownDomainObjectException) {
|
||||
// There is no signing config for current build type, can"t sign the apk
|
||||
println("No signing configs for this build type: $buildType")
|
||||
return@forEach
|
||||
}
|
||||
}
|
||||
val outputDir = packageTask.outputs.files.files.last()
|
||||
exec {
|
||||
workingDir("../../scripts/android")
|
||||
environment = mapOf(
|
||||
"JAVA_HOME" to "$javaHome",
|
||||
"PATH" to "${System.getenv("PATH")}:$javaHome/bin"
|
||||
)
|
||||
commandLine = listOf(
|
||||
"./compress-and-sign-apk.sh",
|
||||
"${rootProject.extra["compression.level"]}",
|
||||
"$outputDir",
|
||||
sdkDir,
|
||||
storeFile,
|
||||
storePassword,
|
||||
keyAlias,
|
||||
keyPassword
|
||||
)
|
||||
}
|
||||
}
|
||||
lateinit var outputDir: File
|
||||
named(if (isRelease) "packageRelease" else "packageDebug") {
|
||||
outputDir = outputs.files.files.last()
|
||||
}
|
||||
exec {
|
||||
workingDir("../../scripts/android")
|
||||
environment = mapOf(
|
||||
"JAVA_HOME" to "$javaHome",
|
||||
"PATH" to "${System.getenv("PATH")}:$javaHome/bin"
|
||||
)
|
||||
commandLine = listOf(
|
||||
"./compress-and-sign-apk.sh",
|
||||
"${rootProject.extra["compression.level"]}",
|
||||
"$outputDir",
|
||||
sdkDir,
|
||||
storeFile,
|
||||
storePassword,
|
||||
keyAlias,
|
||||
keyPassword
|
||||
)
|
||||
}
|
||||
|
||||
if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") {
|
||||
File(outputDir, "android-release.apk").renameTo(File(outputDir, "simplex.apk"))
|
||||
File(outputDir, "android-armeabi-v7a-release.apk").renameTo(File(outputDir, "simplex-armv7a.apk"))
|
||||
File(outputDir, "android-arm64-v8a-release.apk").renameTo(File(outputDir, "simplex.apk"))
|
||||
if (project.properties["android.injected.signing.key.alias"] != null && buildType == "release") {
|
||||
val flavor = variant.removeSuffix("Release").lowercase()
|
||||
mapOf("arm64-v8a" to "simplex.apk", "armeabi-v7a" to "simplex-armv7a.apk").forEach { (abi, name) ->
|
||||
if (!File(outputDir, "android-$flavor-$abi-release.apk").renameTo(File(outputDir, name))) {
|
||||
logger.warn("No $abi apk to rename to $name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// View all gradle properties set
|
||||
// project.properties.each { k, v -> println "$k -> $v" }
|
||||
@@ -221,9 +259,7 @@ tasks {
|
||||
// Don"t do anything if no compression is needed
|
||||
if (rootProject.extra["compression.level"] as Int != 0) {
|
||||
whenTaskAdded {
|
||||
if (name == "packageDebug") {
|
||||
finalizedBy(compressApk)
|
||||
} else if (name == "packageRelease") {
|
||||
if (name in apkTasks) {
|
||||
finalizedBy(compressApk)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package chat.simplex.app
|
||||
|
||||
// Play Billing is only in the google flavor, so the Play country stays unknown here
|
||||
fun loadPlayStoreCountry() {}
|
||||
@@ -0,0 +1,31 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.platform.androidAppContext
|
||||
import chat.simplex.common.platform.androidPlayStoreCountry
|
||||
import com.android.billingclient.api.*
|
||||
|
||||
// Requests the country of the Google Play account into [androidPlayStoreCountry].
|
||||
// It stays null when Play is unavailable or the user is not signed in.
|
||||
fun loadPlayStoreCountry() {
|
||||
val client = BillingClient.newBuilder(androidAppContext)
|
||||
.setListener { _, _ -> }
|
||||
.enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
|
||||
.build()
|
||||
client.startConnection(object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(result: BillingResult) {
|
||||
if (result.responseCode != BillingClient.BillingResponseCode.OK) {
|
||||
client.endConnection()
|
||||
return
|
||||
}
|
||||
client.getBillingConfigAsync(GetBillingConfigParams.newBuilder().build()) { configResult, config ->
|
||||
if (configResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
androidPlayStoreCountry.value = config?.countryCode
|
||||
}
|
||||
client.endConnection()
|
||||
}
|
||||
}
|
||||
|
||||
// The connection is only used for this one request, it is not retried
|
||||
override fun onBillingServiceDisconnected() = client.endConnection()
|
||||
})
|
||||
}
|
||||
@@ -341,6 +341,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
override fun androidIsXiaomiDevice(): Boolean = setOf("xiaomi", "redmi", "poco").contains(Build.BRAND.lowercase())
|
||||
|
||||
override fun androidLoadPlayStoreCountry() = loadPlayStoreCountry()
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
@Composable
|
||||
override fun androidLockPortraitOrientation() {
|
||||
@@ -370,6 +372,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
override fun androidCreateActiveCallState(): Closeable = ActiveCallState()
|
||||
|
||||
override val androidApiLevel: Int get() = Build.VERSION.SDK_INT
|
||||
|
||||
override val androidIsPlayStoreBuild: Boolean get() = BuildConfig.PLAY_STORE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ kotlin {
|
||||
api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}")
|
||||
implementation("org.jetbrains.compose.material:material-icons-core:1.7.3")
|
||||
implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3")
|
||||
implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}")
|
||||
//Barcode
|
||||
api("org.boofcv:boofcv-core:1.1.3")
|
||||
implementation("com.godaddy.android.colorpicker:compose-color-picker-jvm:0.7.0")
|
||||
@@ -148,8 +147,7 @@ kotlin {
|
||||
implementation("org.slf4j:slf4j-simple:2.0.12")
|
||||
implementation("uk.co.caprica:vlcj:4.8.3")
|
||||
implementation("net.java.dev.jna:jna:5.14.0")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf")
|
||||
implementation(project(":external:nanohttpd"))
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
}
|
||||
}
|
||||
@@ -189,7 +187,6 @@ buildConfig {
|
||||
buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"")
|
||||
buildConfigField("int", "DESKTOP_VERSION_CODE", "${extra["desktop.version_code"]}")
|
||||
buildConfigField("String", "DATABASE_BACKEND", "\"${extra["database.backend"]}\"")
|
||||
buildConfigField("Boolean", "ANDROID_BUNDLE", "${extra["android.bundle"]}")
|
||||
buildConfigField("Boolean", "SIMPLEX_ASSETS", "$hasSimplexAssets")
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.item
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@@ -24,6 +25,7 @@ actual fun SimpleAndAnimatedImageView(
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
smallView: Boolean,
|
||||
blurred: State<Boolean>, // coil drives the animation itself here, so there is nothing to pause
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
+15
-1
@@ -233,7 +233,8 @@ actual fun getFileName(uri: URI): String? {
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
// Can make an exception
|
||||
cursor.getString(nameIndex)
|
||||
// the provider controls this value, and callers use it as a bare file name
|
||||
cursor.getString(nameIndex)?.let { File(it).name }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -341,6 +342,19 @@ actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolea
|
||||
VideoPlayerInterface.PreviewAndDuration(null, 0, 0)
|
||||
}
|
||||
|
||||
actual suspend fun hasVideoTrack(uri: URI): Boolean {
|
||||
val mmr = MediaMetadataRetriever()
|
||||
return try {
|
||||
mmr.setDataSource(androidAppContext, uri.toUri())
|
||||
mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) == "yes"
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Utils.android hasVideoTrack error: ${e.message}")
|
||||
false
|
||||
} finally {
|
||||
mmr.release()
|
||||
}
|
||||
}
|
||||
|
||||
actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encodeToString(this, Base64.DEFAULT)
|
||||
|
||||
actual fun String.toByteArrayFromBase64ForPassphrase(): ByteArray = Base64.decode(this, Base64.DEFAULT)
|
||||
|
||||
+11
-10
@@ -1307,6 +1307,7 @@ data class User(
|
||||
val sendRcptsContacts: Boolean,
|
||||
val sendRcptsSmallGroups: Boolean,
|
||||
val autoAcceptMemberContacts: Boolean,
|
||||
val autoAcceptGroupInvitations: Boolean,
|
||||
val viewPwdHash: UserPwdHash?,
|
||||
val uiThemes: ThemeModeOverrides? = null,
|
||||
val userChatRelay: Boolean,
|
||||
@@ -1339,6 +1340,7 @@ data class User(
|
||||
sendRcptsContacts = true,
|
||||
sendRcptsSmallGroups = false,
|
||||
autoAcceptMemberContacts = false,
|
||||
autoAcceptGroupInvitations = false,
|
||||
viewPwdHash = null,
|
||||
uiThemes = null,
|
||||
userChatRelay = false,
|
||||
@@ -3937,13 +3939,15 @@ enum class MsgDirection {
|
||||
sealed class CIForwardedFrom {
|
||||
@Serializable @SerialName("unknown") object Unknown: CIForwardedFrom()
|
||||
@Serializable @SerialName("contact") class Contact(override val chatName: String, val msgDir: MsgDirection, val contactId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
|
||||
@Serializable @SerialName("group") class Group(override val chatName: String, val msgDir: MsgDirection, val groupId: Long? = null, val chatItemId: Long? = null): CIForwardedFrom()
|
||||
@Serializable @SerialName("group") class Group(override val chatName: String, val msgDir: MsgDirection, val groupId: Long? = null, val chatItemId: Long? = null, val memberId: String? = null, val sharedMsgId_: String? = null, val groupType: GroupType? = null): CIForwardedFrom()
|
||||
@Serializable @SerialName("groupLink") class GroupLink(override val chatName: String, val msgDir: MsgDirection, val groupLink: String, val publicGroupId: String, val memberId: String? = null, val sharedMsgId: String, val groupType: GroupType? = null): CIForwardedFrom()
|
||||
|
||||
open val chatName: String
|
||||
get() = when (this) {
|
||||
Unknown -> ""
|
||||
is Contact -> chatName
|
||||
is Group -> chatName
|
||||
is GroupLink -> chatName
|
||||
}
|
||||
|
||||
val chatTypeApiIdMsgId: Triple<ChatType, Long, Long?>?
|
||||
@@ -3951,18 +3955,15 @@ sealed class CIForwardedFrom {
|
||||
Unknown -> null
|
||||
is Contact -> if (contactId != null) Triple(ChatType.Direct, contactId, chatItemId) else null
|
||||
is Group -> if (groupId != null) Triple(ChatType.Group, groupId, chatItemId) else null
|
||||
is GroupLink -> null
|
||||
}
|
||||
|
||||
val sourceGroupLink: String?
|
||||
get() = if (this is GroupLink) groupLink else null
|
||||
|
||||
fun text(chatType: ChatType): String =
|
||||
if (chatType == ChatType.Local) {
|
||||
if (chatName.isEmpty()) {
|
||||
generalGetString(MR.strings.saved_description)
|
||||
} else {
|
||||
generalGetString(MR.strings.saved_from_description).format(chatName)
|
||||
}
|
||||
} else {
|
||||
generalGetString(MR.strings.forwarded_description)
|
||||
}
|
||||
if (chatType == ChatType.Local) generalGetString(MR.strings.saved_description)
|
||||
else generalGetString(MR.strings.forwarded_description)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
||||
+9
@@ -941,6 +941,12 @@ object ChatController {
|
||||
throw Exception("failed to set auto-accept ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetUserAutoAcceptGroupInvitations(u: User, enable: Boolean) {
|
||||
val r = sendCmd(u.remoteHostId, CC.ApiSetUserAutoAcceptGroupInvitations(u.userId, enable))
|
||||
if (r.result is CR.CmdOk) return
|
||||
throw Exception("failed to set auto-accept group invitations ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiHideUser(u: User, viewPwd: String): User =
|
||||
setUserPrivacy(u.remoteHostId, CC.ApiHideUser(u.userId, viewPwd))
|
||||
|
||||
@@ -3786,6 +3792,7 @@ sealed class CC {
|
||||
class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
|
||||
class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC()
|
||||
class ApiSetUserAutoAcceptMemberContacts(val userId: Long, val enable: Boolean): CC()
|
||||
class ApiSetUserAutoAcceptGroupInvitations(val userId: Long, val enable: Boolean): CC()
|
||||
class ApiHideUser(val userId: Long, val viewPwd: String): CC()
|
||||
class ApiUnhideUser(val userId: Long, val viewPwd: String): CC()
|
||||
class ApiMuteUser(val userId: Long): CC()
|
||||
@@ -3977,6 +3984,7 @@ sealed class CC {
|
||||
"/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}"
|
||||
}
|
||||
is ApiSetUserAutoAcceptMemberContacts -> "/_set accept member contacts $userId ${onOff(enable)}"
|
||||
is ApiSetUserAutoAcceptGroupInvitations -> "/_set accept group invitations $userId ${onOff(enable)}"
|
||||
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}"
|
||||
is ApiMuteUser -> "/_mute user $userId"
|
||||
@@ -4192,6 +4200,7 @@ sealed class CC {
|
||||
is ApiSetUserContactReceipts -> "apiSetUserContactReceipts"
|
||||
is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts"
|
||||
is ApiSetUserAutoAcceptMemberContacts -> "apiSetUserAutoAcceptMemberContacts"
|
||||
is ApiSetUserAutoAcceptGroupInvitations -> "apiSetUserAutoAcceptGroupInvitations"
|
||||
is ApiHideUser -> "apiHideUser"
|
||||
is ApiUnhideUser -> "apiUnhideUser"
|
||||
is ApiMuteUser -> "apiMuteUser"
|
||||
|
||||
+5
@@ -1,5 +1,7 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.ui.theme.DefaultTheme
|
||||
@@ -30,6 +32,9 @@ else
|
||||
|
||||
val databaseBackend: String = if (appPlatform == AppPlatform.ANDROID) "sqlite" else BuildConfigCommon.DATABASE_BACKEND
|
||||
|
||||
// Country of the Google Play account, only set in the google flavor of the Android app
|
||||
val androidPlayStoreCountry: MutableState<String?> = mutableStateOf(null)
|
||||
|
||||
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
|
||||
override fun add(element: E): Boolean {
|
||||
if (size > capacity) removeFirstOrNull()
|
||||
|
||||
+4
@@ -29,7 +29,11 @@ interface PlatformInterface {
|
||||
fun androidRestartNetworkObserver() {}
|
||||
fun androidCreateActiveCallState(): Closeable = Closeable { }
|
||||
fun androidIsXiaomiDevice(): Boolean = false
|
||||
// Requests the Google Play account country into [androidPlayStoreCountry]
|
||||
fun androidLoadPlayStoreCountry() {}
|
||||
val androidApiLevel: Int? get() = null
|
||||
// The build distributed via Google Play, which has to follow its policies
|
||||
val androidIsPlayStoreBuild: Boolean get() = false
|
||||
@Composable fun androidLockPortraitOrientation() {}
|
||||
suspend fun androidAskToAllowBackgroundCalls(): Boolean = true
|
||||
@Composable fun desktopShowAppUpdateNotice() {}
|
||||
|
||||
+8
-5
@@ -502,14 +502,17 @@ fun ChatView(
|
||||
groupMembersJob = scope.launch(Dispatchers.Default) {
|
||||
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
val (updatedMember, code) = if (member.memberActive) {
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
(memCode?.first ?: r?.first ?: member) to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
(r?.first ?: member) to null
|
||||
}
|
||||
if (!isActive || chatModel.chatId.value != groupInfo.id) return@launch
|
||||
// members are not loaded in large groups, so only the opened member is added to the model
|
||||
withContext(Dispatchers.Main) {
|
||||
chatModel.chatsContext.upsertGroupMember(chatRh, groupInfo, updatedMember)
|
||||
}
|
||||
setGroupMembers(chatRh, groupInfo, chatModel)
|
||||
if (!isActive) return@launch
|
||||
|
||||
if (chatsCtx.secondaryContextFilter == null) {
|
||||
ModalManager.end.closeModals()
|
||||
|
||||
+20
-2
@@ -285,7 +285,22 @@ expect fun AttachmentSelection(
|
||||
)
|
||||
|
||||
fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
|
||||
val groups = uris.groupBy { isImage(it) || isVideoUri(it) }
|
||||
// The extension is enough to classify every format except .webm, which is just as commonly an
|
||||
// audio-only container as a video one. An audio-only file has no frame to embed and is sent as a file,
|
||||
// but that can only be told from the content, so reading it is deferred to a background thread.
|
||||
// Only done here, where files arrive without the user saying how to send them (drag & drop, paste) -
|
||||
// an explicitly picked video is still sent as one.
|
||||
if (uris.none { isWebmUri(it) }) {
|
||||
attachFiles(uris, emptySet())
|
||||
} else {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
attachFiles(uris, uris.filter { isWebmUri(it) && hasVideoTrack(it) }.toSet())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<ComposeState>.attachFiles(uris: List<URI>, webmVideos: Set<URI>) {
|
||||
val groups = uris.groupBy { isImage(it) || (isVideoUri(it) && (!isWebmUri(it) || it in webmVideos)) }
|
||||
val media = groups[true] ?: emptyList()
|
||||
val files = groups[false] ?: emptyList()
|
||||
if (media.isNotEmpty()) {
|
||||
@@ -298,9 +313,12 @@ fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
|
||||
private fun isVideoUri(uri: URI): Boolean {
|
||||
val name = getFileName(uri)?.lowercase() ?: return false
|
||||
return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") ||
|
||||
name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv")
|
||||
name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv") ||
|
||||
name.endsWith(".webm")
|
||||
}
|
||||
|
||||
private fun isWebmUri(uri: URI): Boolean = getFileName(uri)?.lowercase()?.endsWith(".webm") == true
|
||||
|
||||
fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
if (uri != null) {
|
||||
val maxFileSize = value.maxFileSize
|
||||
|
||||
+2
-1
@@ -210,7 +210,7 @@ fun CIImageView(
|
||||
val loaded = res.value
|
||||
if (loaded != null && file != null) {
|
||||
val (imageBitmap, data, _) = loaded
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, blurred, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
|
||||
} else {
|
||||
imageView(previewBitmap, onClick = {
|
||||
if (file != null) {
|
||||
@@ -281,5 +281,6 @@ expect fun SimpleAndAnimatedImageView(
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
smallView: Boolean,
|
||||
blurred: State<Boolean>,
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
)
|
||||
|
||||
+65
-17
@@ -23,6 +23,7 @@ import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.chatlist.openChat
|
||||
import chat.simplex.common.views.newchat.planAndConnect
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -101,14 +102,35 @@ fun FramedItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false, iconColor: Color? = null) {
|
||||
fun HeaderText(caption: String, italic: Boolean, fontSize: TextUnit = 12.sp, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontSize = fontSize, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = MaterialTheme.colors.secondary)) {
|
||||
append(caption)
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun headerModifier(pad: Boolean, onClick: (() -> Unit)? = null): Modifier {
|
||||
val sentColor = MaterialTheme.appColors.sentQuote
|
||||
val receivedColor = MaterialTheme.appColors.receivedQuote
|
||||
return Modifier
|
||||
.background(if (sent) sentColor else receivedColor)
|
||||
.fillMaxWidth()
|
||||
.then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier)
|
||||
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HeaderRow(modifier: Modifier, caption: String, italic: Boolean, icon: Painter?, iconColor: Color?) {
|
||||
Row(
|
||||
Modifier
|
||||
.background(if (sent) sentColor else receivedColor)
|
||||
.fillMaxWidth()
|
||||
.padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp),
|
||||
modifier,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -120,19 +142,15 @@ fun FramedItemView(
|
||||
tint = iconColor ?: if (isInDarkTheme()) FileDark else FileLight
|
||||
)
|
||||
}
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontSize = 12.sp, fontStyle = if (italic) FontStyle.Italic else FontStyle.Normal, color = MaterialTheme.colors.secondary)) {
|
||||
append(caption)
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
HeaderText(caption, italic)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false, iconColor: Color? = null) {
|
||||
HeaderRow(headerModifier(pad), caption, italic, icon, iconColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuoteView(qi: CIQuote) {
|
||||
val sentColor = MaterialTheme.appColors.sentQuote
|
||||
@@ -293,8 +311,38 @@ fun FramedItemView(
|
||||
}
|
||||
} else {
|
||||
Header()
|
||||
if (ci.meta.itemForwarded != null) {
|
||||
FramedItemHeader(ci.meta.itemForwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true)
|
||||
val forwarded = ci.meta.itemForwarded
|
||||
if (forwarded != null) {
|
||||
val twoRowHeader = if (chatInfo.chatType == ChatType.Local) {
|
||||
forwarded.chatTypeApiIdMsgId != null || forwarded.sourceGroupLink != null
|
||||
} else {
|
||||
when (forwarded) {
|
||||
is CIForwardedFrom.Group -> forwarded.groupType != null
|
||||
is CIForwardedFrom.GroupLink -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
if (twoRowHeader) {
|
||||
val caption = stringResource(if (chatInfo.chatType == ChatType.Local) MR.strings.saved_from else MR.strings.forwarded_from)
|
||||
Column(
|
||||
headerModifier(pad = true, onClick = {
|
||||
val target = forwarded.chatTypeApiIdMsgId
|
||||
val link = forwarded.sourceGroupLink
|
||||
if (target != null) {
|
||||
val (chatType, apiId, itemId) = target
|
||||
withBGApi { openChat(secondaryChatsCtx = null, chat.remoteHostId, chatType, apiId, itemId) }
|
||||
} else if (link != null) {
|
||||
withBGApi { planAndConnect(chat.remoteHostId, link, close = null) }
|
||||
}
|
||||
}),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
HeaderRow(Modifier, caption, true, painterResource(MR.images.ic_forward), null)
|
||||
HeaderText(forwarded.chatName, italic = false, fontSize = 15.sp, modifier = Modifier.offset(y = (-2).dp))
|
||||
}
|
||||
} else {
|
||||
FramedItemHeader(forwarded.text(chatInfo.chatType), true, painterResource(MR.images.ic_forward), pad = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ci.file == null && ci.formattedText == null && !ci.meta.isLive && isShortEmoji(ci.content.text)) {
|
||||
|
||||
-2
@@ -148,8 +148,6 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
)
|
||||
}
|
||||
.fillMaxSize()
|
||||
// LALAL
|
||||
// https://github.com/JetBrains/compose-multiplatform/pull/2015/files#diff-841b3825c504584012e1d1c834d731bae794cce6acad425d81847c8bbbf239e0R24
|
||||
if (media is ProviderMedia.Image) {
|
||||
val (data: ByteArray, imageBitmap: ImageBitmap) = media
|
||||
FullScreenImageView(modifier, data, imageBitmap)
|
||||
|
||||
+2
@@ -183,6 +183,8 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
|
||||
val showWhatsNew = shouldShowWhatsNew(chatModel)
|
||||
val showUpdatedConditions = chatModel.conditions.value.conditionsAction?.shouldShowNotice ?: false
|
||||
if (showWhatsNew || showUpdatedConditions) {
|
||||
// Requested here, so that the country is known by the time the modal opens
|
||||
platform.androidLoadPlayStoreCountry()
|
||||
delay(1000L)
|
||||
ModalManager.center.showCustomModal { close -> WhatsNewView(close = close, updatedConditions = showUpdatedConditions) }
|
||||
}
|
||||
|
||||
+5
@@ -753,6 +753,11 @@ private fun saveArchiveFromURI(importedArchiveURI: URI): String? {
|
||||
if (inputStream != null && archiveName != null) {
|
||||
val archivePath = "$databaseExportDir${File.separator}$archiveName"
|
||||
val destFile = File(archivePath)
|
||||
// resolves symlinks, so it also catches a final component linking outside the folder
|
||||
if (destFile.canonicalFile.parentFile != databaseExportDir.canonicalFile) {
|
||||
Log.e(TAG, "saveArchiveFromURI path outside of export folder")
|
||||
return null
|
||||
}
|
||||
Files.copy(inputStream, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
|
||||
archivePath
|
||||
} else {
|
||||
|
||||
+2
@@ -294,6 +294,7 @@ class AlertManager {
|
||||
nameCaption: String? = null,
|
||||
subtitle: String? = null,
|
||||
information: String? = null,
|
||||
secondaryInformation: Boolean = false,
|
||||
confirmText: String? = generalGetString(MR.strings.connect_plan_open_chat),
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
connectOtherButton: String? = null,
|
||||
@@ -378,6 +379,7 @@ class AlertManager {
|
||||
information,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = if (secondaryInformation) MaterialTheme.colors.secondary else Color.Unspecified,
|
||||
maxLines = 3,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
+2
-1
@@ -31,7 +31,8 @@ fun AppBarTitle(
|
||||
val connection = if (enableAlphaChanges) handler?.connection else null
|
||||
LaunchedEffect(title) {
|
||||
if (enableAlphaChanges) {
|
||||
handler?.title?.value = title
|
||||
// the app bar shows a single line, so the line breaks of the large title are replaced with spaces
|
||||
handler?.title?.value = title.replace("\n", " ")
|
||||
} else {
|
||||
handler?.connection?.scrollTrackingEnabled = false
|
||||
}
|
||||
|
||||
+3
@@ -495,6 +495,9 @@ fun ciSenderProfile(ci: ChatItem, chatInfo: ChatInfo): LocalProfile? = when (val
|
||||
|
||||
expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true, withAlertOnException: Boolean = true): VideoPlayerInterface.PreviewAndDuration
|
||||
|
||||
// Whether the file really contains a video track. Reads container metadata only, without decoding a frame.
|
||||
expect suspend fun hasVideoTrack(uri: URI): Boolean
|
||||
|
||||
fun showWrongUriAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.non_content_uri_alert_title),
|
||||
|
||||
+16
-3
@@ -656,11 +656,24 @@ private fun showOpenKnownGroupAlert(chatModel: ChatModel, rhId: Long?, close: ((
|
||||
},
|
||||
nameCaption = planSimplexName?.shortStr,
|
||||
subtitle = subscriberCount,
|
||||
information = if (groupInfo.nextConnectPrepared || groupInfo.businessChat != null) {
|
||||
null
|
||||
} else {
|
||||
val isChannel = groupInfo.useRelays
|
||||
generalGetString(when (groupInfo.membership.memberRole) {
|
||||
GroupMemberRole.Observer -> if (isChannel) MR.strings.connect_plan_you_are_subscriber else MR.strings.connect_plan_you_are_observer
|
||||
GroupMemberRole.Moderator -> MR.strings.connect_plan_you_are_moderator
|
||||
GroupMemberRole.Admin -> MR.strings.connect_plan_you_are_admin
|
||||
GroupMemberRole.Owner -> MR.strings.connect_plan_you_are_owner
|
||||
else -> if (isChannel) MR.strings.connect_plan_you_are_contributor else MR.strings.connect_plan_you_are_member
|
||||
})
|
||||
},
|
||||
secondaryInformation = true,
|
||||
confirmText = generalGetString(
|
||||
if (groupInfo.useRelays) {
|
||||
if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_channel
|
||||
MR.strings.connect_plan_open_channel
|
||||
} else if (groupInfo.businessChat == null) {
|
||||
if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_group else MR.strings.connect_plan_open_group
|
||||
MR.strings.connect_plan_open_group
|
||||
} else {
|
||||
if (groupInfo.nextConnectPrepared) MR.strings.connect_plan_open_new_chat else MR.strings.connect_plan_open_chat
|
||||
}
|
||||
@@ -761,7 +774,7 @@ fun showPrepareGroupAlert(
|
||||
nameCaption = planSimplexName?.shortStr,
|
||||
subtitle = subscriberCount,
|
||||
information = ownerVerificationMessage(ownerVerification),
|
||||
confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_new_channel else MR.strings.connect_plan_open_new_group),
|
||||
confirmText = generalGetString(if (isChannel) MR.strings.connect_plan_open_channel else MR.strings.connect_plan_open_group),
|
||||
onConfirm = {
|
||||
AlertManager.privacySensitive.hideAlert()
|
||||
withBGApi {
|
||||
|
||||
+2
-1
@@ -836,7 +836,8 @@ fun strConnectTarget(str: String): ConnectTarget? {
|
||||
val links = parsedMd.filter { it.format?.isSimplexLink ?: false }
|
||||
if (links.size == 1) {
|
||||
val fmt = links[0].format as Format.SimplexLink
|
||||
return ConnectTarget.Link(links[0].text, fmt.linkType, fmt.simplexLinkText)
|
||||
val text = if (fmt.showText != null) fmt.simplexUri else links[0].text
|
||||
return ConnectTarget.Link(text, fmt.linkType, fmt.simplexLinkText)
|
||||
}
|
||||
if (links.isEmpty()) {
|
||||
val nameFt = parsedMd.firstOrNull { it.format is Format.SimplexName }
|
||||
|
||||
+326
-2
@@ -1,6 +1,10 @@
|
||||
package chat.simplex.common.views.onboarding
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.calculatePan
|
||||
import androidx.compose.foundation.gestures.calculateZoom
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -8,17 +12,41 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.toRect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.PointerIcon
|
||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.model.*
|
||||
@@ -34,6 +62,7 @@ import chat.simplex.common.views.usersettings.showAddShortLinkAlert
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.ImageResource
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
@Composable
|
||||
fun ModalData.WhatsNewView(updatedConditions: Boolean = false, viaSettings: Boolean = false, close: () -> Unit) {
|
||||
@@ -913,9 +942,15 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v7.0",
|
||||
post = null,
|
||||
// the trailing space differs from the previously released "v7.0", so that What's new is shown again
|
||||
version = if (isInUs()) "v7.0.1" else "v7.0",
|
||||
post = "https://simplex.chat/blog/20260819-simplex-chat-crowdfunding.html",
|
||||
features = listOf(
|
||||
VersionFeature.FeatureView(
|
||||
icon = null,
|
||||
titleId = MR.strings.v7_0_invest,
|
||||
view = { modalManager -> InvestInSimpleXChatView(modalManager) }
|
||||
),
|
||||
VersionFeature.FeatureDescription(
|
||||
icon = MR.images.ic_alternate_email,
|
||||
titleId = MR.strings.v7_0_simplex_names,
|
||||
@@ -950,6 +985,295 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean {
|
||||
return v != lastVersion
|
||||
}
|
||||
|
||||
private const val WEFUNDER_URL = "https://wefunder.com/simplex.chat"
|
||||
|
||||
private const val CROWDFUNDING_CONTACT_URI = "simplex:/a#JxGcOA1_QhlmVFzYYabloMbvMZk5Y9d9iS3ITDnhzYo?h=smp11.simplex.im"
|
||||
|
||||
// the center modal takes the remaining width of the window, so the image is limited to its design width
|
||||
private val MAX_CROWDFUNDING_IMAGE_WIDTH = DEFAULT_MIN_CENTER_MODAL_WIDTH
|
||||
|
||||
// the width of the page images shipped with the desktop app, so that they are never upscaled
|
||||
private val CROWDFUNDING_PAGE_IMAGE_WIDTH = DEFAULT_MIN_CENTER_MODAL_WIDTH
|
||||
|
||||
// the corner radius the images are designed with, and the same radius as a share of their design width
|
||||
private val CROWDFUNDING_IMAGE_CORNER_RADIUS = 12.dp
|
||||
private const val CROWDFUNDING_IMAGE_CORNER_RADIUS_RATIO = 0.03f
|
||||
|
||||
private class CrowdfundingLayout(
|
||||
val maxImageWidth: Dp,
|
||||
val imageShape: Shape,
|
||||
// the modal manager that shows the page in the center of the window, or null when nothing does
|
||||
private val centerOfWindow: ModalManager?
|
||||
) {
|
||||
fun inCenterOfWindow(modalManager: ModalManager) = modalManager === centerOfWindow
|
||||
}
|
||||
|
||||
// the images are designed for the width of a phone screen, which Android always gives them. On desktop
|
||||
// they are limited to their own width, and their radius is scaled with them, as they are still shown
|
||||
// wider than designed: a fixed radius would not only look almost square, but would also leave the corners
|
||||
// baked into the jpegs visible - they have black behind them, as jpegs have no transparency
|
||||
private val crowdfundingLayout = if (appPlatform.isDesktop)
|
||||
CrowdfundingLayout(CROWDFUNDING_PAGE_IMAGE_WIDTH, object : Shape {
|
||||
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline =
|
||||
Outline.Rounded(RoundRect(size.toRect(), CornerRadius(size.width * CROWDFUNDING_IMAGE_CORNER_RADIUS_RATIO)))
|
||||
}, ModalManager.center)
|
||||
else
|
||||
CrowdfundingLayout(Dp.Unspecified, RoundedCornerShape(CROWDFUNDING_IMAGE_CORNER_RADIUS), null)
|
||||
|
||||
// Google Play policy restricts promoting investments, so Play builds only show it in the US
|
||||
@Composable
|
||||
fun crowdfundingAvailable(): Boolean {
|
||||
if (!platform.androidIsPlayStoreBuild) return true
|
||||
if (androidPlayStoreCountry.value == null) {
|
||||
LaunchedEffect(Unit) {
|
||||
if (androidPlayStoreCountry.value == null) platform.androidLoadPlayStoreCountry()
|
||||
}
|
||||
}
|
||||
return isInUs()
|
||||
}
|
||||
|
||||
fun isInUs(): Boolean =
|
||||
androidPlayStoreCountry.value == "US"
|
||||
|| androidPlayStoreCountry.value == ""
|
||||
|| androidPlayStoreCountry.value == null
|
||||
|
||||
@Composable
|
||||
private fun InvestInSimpleXChatView(modalManager: ModalManager) {
|
||||
if (!crowdfundingAvailable()) return
|
||||
val showGetStake = { modalManager.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = false, inCenterOfWindow = crowdfundingLayout.inCenterOfWindow(modalManager), close = close) } }
|
||||
Column(modifier = Modifier.padding(bottom = 12.dp)) {
|
||||
Text(
|
||||
generalGetString(MR.strings.v7_0_invest),
|
||||
style = MaterialTheme.typography.h4,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(bottom = 6.dp)
|
||||
)
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.v7_0_invest_descr))
|
||||
append(" ")
|
||||
withStyle(SpanStyle(color = MaterialTheme.colors.primary)) {
|
||||
append(generalGetString(MR.strings.learn_more))
|
||||
}
|
||||
},
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier
|
||||
.pointerHoverIcon(PointerIcon.Hand)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = showGetStake
|
||||
)
|
||||
)
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
Image(
|
||||
painterResource(MR.images.crowdfunding_1),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
.widthIn(max = MAX_CROWDFUNDING_IMAGE_WIDTH)
|
||||
.fillMaxWidth()
|
||||
.clip(crowdfundingLayout.imageShape)
|
||||
.pointerHoverIcon(PointerIcon.Hand)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = showGetStake
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CrowdfundingSlide(
|
||||
val image: ImageResource,
|
||||
val heading: String,
|
||||
val info: String?,
|
||||
val text: String,
|
||||
)
|
||||
|
||||
// not localized: the page is only shown to US investors, and the text duplicates the images
|
||||
private val getStakeSlides: List<CrowdfundingSlide> = listOf(
|
||||
CrowdfundingSlide(
|
||||
MR.images.crowdfunding_1,
|
||||
"The first and the only messaging network without any user IDs",
|
||||
null,
|
||||
"By investing, you can benefit from the company growth, and help us build the future of private and secure communications."
|
||||
),
|
||||
CrowdfundingSlide(
|
||||
MR.images.crowdfunding_2,
|
||||
"480,000+ users joined on their own",
|
||||
null,
|
||||
"SimpleX users have been more than doubling every year without any paid marketing, and donated over \$650,000."
|
||||
),
|
||||
CrowdfundingSlide(
|
||||
MR.images.crowdfunding_3,
|
||||
"Developers already bet on SimpleX success",
|
||||
"Independent developers created moderation and AI bots, Telegram bridges, and a public server registry.",
|
||||
"Every service developers build on SimpleX Network may increase its value, and bring new users to SimpleX Chat."
|
||||
),
|
||||
CrowdfundingSlide(
|
||||
MR.images.crowdfunding_4,
|
||||
"Revenue plan: free for users, channels & businesses pay",
|
||||
"SimpleX Chat plans to earn from the infrastructure and services that creators, businesses and large communities need as they grow.",
|
||||
"Read about how we plan to make SimpleX Chat and network profitable, and about all the investment terms on Wefunder."
|
||||
),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun GetStakeView(fromSettings: Boolean, inCenterOfWindow: Boolean = false, close: () -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
@Composable
|
||||
fun slideImage(slide: CrowdfundingSlide) {
|
||||
if (BuildConfigCommon.SIMPLEX_ASSETS) {
|
||||
Image(
|
||||
painterResource(slide.image),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.widthIn(max = crowdfundingLayout.maxImageWidth)
|
||||
.fillMaxWidth()
|
||||
.clip(crowdfundingLayout.imageShape)
|
||||
.fullScreenOnClick(slide.image)
|
||||
)
|
||||
} else {
|
||||
Text(slide.heading, style = MaterialTheme.typography.h4, fontWeight = FontWeight.Medium)
|
||||
if (slide.info != null) {
|
||||
Text(slide.info, Modifier.padding(top = 4.dp), lineHeight = 24.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnWithScrollBar(Modifier.pinchZoom().padding(horizontal = DEFAULT_PADDING)) {
|
||||
// in the center of the window the page is wide enough for the title to fit on one line
|
||||
val title = "Get a stake in\nSimpleX Chat"
|
||||
AppBarTitle(if (inCenterOfWindow) title.replace("\n", " ") else title, withPadding = false)
|
||||
// What's new already shows the image of the first slide, above the link that opens this page
|
||||
if (fromSettings) {
|
||||
slideImage(getStakeSlides[0])
|
||||
}
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(getStakeSlides[0].text)
|
||||
// only the link is clickable, the rest of the paragraph is not
|
||||
withLink(LinkAnnotation.Url(WEFUNDER_URL) { uriHandler.openUriCatching(WEFUNDER_URL) }) {
|
||||
withStyle(SpanStyle(color = MaterialTheme.colors.primary, fontWeight = FontWeight.Bold)) {
|
||||
append(" Learn more and invest on Wefunder.")
|
||||
}
|
||||
}
|
||||
},
|
||||
Modifier.padding(top = if (fromSettings) 8.dp else 0.dp),
|
||||
lineHeight = 24.sp
|
||||
)
|
||||
|
||||
getStakeSlides.drop(1).forEach { slide ->
|
||||
Column(Modifier.padding(top = DEFAULT_PADDING * 1.5f)) {
|
||||
slideImage(slide)
|
||||
Text(slide.text, Modifier.padding(top = 8.dp), lineHeight = 24.sp)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(top = DEFAULT_PADDING * 2),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OnboardingActionButton(
|
||||
if (appPlatform.isAndroid) Modifier.fillMaxWidth() else Modifier.widthIn(min = 300.dp),
|
||||
labelId = MR.strings.v7_0_invest_learn_more,
|
||||
onboarding = null,
|
||||
onclick = { uriHandler.openUriCatching(WEFUNDER_URL) }
|
||||
)
|
||||
if (!chatModel.desktopNoUserNoRemote) {
|
||||
TextButtonBelowOnboardingButton(
|
||||
"or ask SimpleX team",
|
||||
onClick = if (stopped) null else ({
|
||||
close()
|
||||
uriHandler.openVerifiedSimplexUri(CROWDFUNDING_CONTACT_URI)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// there is no pinch gesture with a mouse, so on desktop a slide is opened full screen instead
|
||||
@Composable
|
||||
private fun Modifier.fullScreenOnClick(image: ImageResource): Modifier {
|
||||
if (!appPlatform.isDesktop) return this
|
||||
return pointerHoverIcon(PointerIcon.Hand).clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) {
|
||||
ModalManager.fullscreen.showCustomModal { close ->
|
||||
BackHandler(onBack = close)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = close),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(painterResource(image), contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val MAX_PAGE_ZOOM = 5f
|
||||
|
||||
/**
|
||||
* The slide images contain small text that is unreadable at screen width, so the page can be pinch-zoomed.
|
||||
* Android only: pinch is unavailable with a mouse.
|
||||
*/
|
||||
@Composable
|
||||
private fun Modifier.pinchZoom(): Modifier {
|
||||
if (!appPlatform.isAndroid) return this
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var offsetX by remember { mutableStateOf(0f) }
|
||||
var offsetY by remember { mutableStateOf(0f) }
|
||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||
return this
|
||||
.onGloballyPositioned { size = it.size }
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
translationX = offsetX
|
||||
translationY = offsetY
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
awaitEachGesture {
|
||||
// the initial pass, as the scroll of the same column is applied after this modifier and would take the gesture first
|
||||
awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
|
||||
var taken: Boolean? = null
|
||||
do {
|
||||
val event = awaitPointerEvent(PointerEventPass.Initial)
|
||||
val multiTouch = event.changes.count { it.pressed } > 1
|
||||
if (multiTouch || scale > 1f) {
|
||||
scale = (scale * event.calculateZoom()).coerceIn(1f, MAX_PAGE_ZOOM)
|
||||
val pan = event.calculatePan()
|
||||
// the page is scaled around its center, so it can be panned by half of the overflow in each direction
|
||||
val maxX = size.width * (scale - 1f) / 2
|
||||
val maxY = size.height * (scale - 1f) / 2
|
||||
val pannedY = offsetY + pan.y * scale
|
||||
// the clamp is applied even when the gesture is not taken: at scale 1 both bounds
|
||||
// are 0, which resets the offsets after zooming back out
|
||||
offsetX = (offsetX + pan.x * scale).coerceIn(-maxX, maxX)
|
||||
offsetY = pannedY.coerceIn(-maxY, maxY)
|
||||
// two fingers always mean zoom, taken without a touch slop: waiting for one would let
|
||||
// the scroll reach its own slop first and scroll the page. A one finger drag is left
|
||||
// to the scroll at the edges, decided once so it cannot alternate mid drag
|
||||
if (multiTouch) taken = true
|
||||
else if (taken == null && pan.y != 0f) taken = pannedY.absoluteValue < maxY
|
||||
if (taken == true) event.changes.forEach { if (it.pressed) it.consume() }
|
||||
}
|
||||
} while (event.changes.any { it.pressed })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateUpdateAddressShortLinkView(modalManager: ModalManager) {
|
||||
val clipboard = LocalClipboardManager.current
|
||||
|
||||
+25
-9
@@ -87,13 +87,19 @@ fun PrivacySettingsView(
|
||||
val currentUser = chatModel.currentUser.value
|
||||
if (currentUser != null && !chatModel.desktopNoUserNoRemote) {
|
||||
SectionDividerSpaced()
|
||||
ContacRequestsFromGroupsSection(
|
||||
AutoAcceptSection(
|
||||
currentUser = currentUser,
|
||||
setAutoAcceptGrpDirectInvs = { enable ->
|
||||
setAutoAcceptMemberContacts = { enable ->
|
||||
withApi {
|
||||
chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable)
|
||||
chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable)
|
||||
}
|
||||
},
|
||||
setAutoAcceptGroupInvitations = { enable ->
|
||||
withApi {
|
||||
chatModel.controller.apiSetUserAutoAcceptGroupInvitations(currentUser, enable)
|
||||
chatModel.currentUser.value = currentUser.copy(autoAcceptGroupInvitations = enable)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -333,16 +339,26 @@ expect fun PrivacyDeviceSection(
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ContacRequestsFromGroupsSection(
|
||||
private fun AutoAcceptSection(
|
||||
currentUser: User,
|
||||
setAutoAcceptGrpDirectInvs: (Boolean) -> Unit
|
||||
setAutoAcceptMemberContacts: (Boolean) -> Unit,
|
||||
setAutoAcceptGroupInvitations: (Boolean) -> Unit
|
||||
) {
|
||||
SectionView(stringResource(MR.strings.settings_section_title_contact_requests_from_groups)) {
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_check), stringResource(MR.strings.auto_accept_contact)) {
|
||||
// legacy string key names, reused for their values so this section stays translated
|
||||
SectionView(stringResource(MR.strings.auto_accept_contact)) {
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.settings_section_title_contact_requests_from_groups)) {
|
||||
DefaultSwitch(
|
||||
checked = currentUser.autoAcceptMemberContacts,
|
||||
onCheckedChange = { enable ->
|
||||
setAutoAcceptGrpDirectInvs(enable)
|
||||
setAutoAcceptMemberContacts(enable)
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.group_invitations)) {
|
||||
DefaultSwitch(
|
||||
checked = currentUser.autoAcceptGroupInvitations,
|
||||
onCheckedChange = { enable ->
|
||||
setAutoAcceptGroupInvitations(enable)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -350,7 +366,7 @@ private fun ContacRequestsFromGroupsSection(
|
||||
SectionTextFooter(
|
||||
remember(currentUser.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.this_setting_is_for_your_current_profile) + " ")
|
||||
append(generalGetString(MR.strings.these_settings_are_for_your_current_profile) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser.displayName)
|
||||
}
|
||||
@@ -387,7 +403,7 @@ private fun DeliveryReceiptsSection(
|
||||
SectionTextFooter(
|
||||
remember(currentUser.displayName) {
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.receipts_section_description) + " ")
|
||||
append(generalGetString(MR.strings.these_settings_are_for_your_current_profile) + " ")
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(currentUser.displayName)
|
||||
}
|
||||
|
||||
+14
-2
@@ -22,7 +22,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.BuildConfigCommon
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.platform.*
|
||||
@@ -30,8 +29,10 @@ import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.database.DatabaseView
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.views.migration.MigrateFromDeviceView
|
||||
import chat.simplex.common.views.onboarding.GetStakeView
|
||||
import chat.simplex.common.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.common.views.onboarding.WhatsNewView
|
||||
import chat.simplex.common.views.onboarding.crowdfundingAvailable
|
||||
import chat.simplex.common.views.usersettings.networkAndServers.NetworkAndServersView
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@@ -111,6 +112,17 @@ fun SettingsLayout(
|
||||
AppShutdownItem()
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
|
||||
if (crowdfundingAvailable()) {
|
||||
SectionDividerSpaced()
|
||||
SectionView(stringResource(MR.strings.v7_0_invest)) {
|
||||
SettingsActionItem(
|
||||
painterResource(MR.images.ic_redeem),
|
||||
stringResource(MR.strings.v7_0_crowdfunding),
|
||||
{ ModalManager.start.showModalCloseable(cardScreen = true) { close -> GetStakeView(fromSettings = true, close = close) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionBottomSpacer()
|
||||
}
|
||||
}
|
||||
@@ -143,7 +155,7 @@ fun HelpAndSupportView(
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView(stringResource(MR.strings.settings_section_title_support_project)) {
|
||||
if (!BuildConfigCommon.ANDROID_BUNDLE) {
|
||||
if (!platform.androidIsPlayStoreBuild) {
|
||||
ContributeItem(uriHandler)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<string name="allow_verb">اسمح</string>
|
||||
<string name="smp_servers_preset_add">أضِف خوادم مُعدة مسبقًا</string>
|
||||
<string name="smp_servers_add_to_another_device">أضِف إلى جهاز آخر</string>
|
||||
<string name="users_delete_all_chats_deleted">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="users_delete_all_chats_deleted">ستُحذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="network_enable_socks_info">الوصول إلى الخوادم عبر وسيط SOCKS على المنفذ %d؟ يجب بدء تشغيل الوسيط قبل تفعيل هذا الخيار.</string>
|
||||
<string name="smp_servers_add">أضف خادم</string>
|
||||
<string name="network_settings">إعدادات الشبكة المتقدّمة</string>
|
||||
@@ -44,7 +44,7 @@
|
||||
<string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string>
|
||||
<string name="v4_2_group_links_desc">يمكن للمُدراء إنشاء روابط للانضمام إلى المجموعات.</string>
|
||||
<string name="accept_connection_request__question">قبول طلب الاتصال؟</string>
|
||||
<string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string>
|
||||
<string name="clear_chat_warning">ستُحذف كل الرسائل - لا يمكن التراجع عن هذا! ستُحذف الرسائل فقط من أجلك.</string>
|
||||
<string name="callstatus_accepted">قُبلت المكالمة</string>
|
||||
<string name="allow_calls_only_if">اسمح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</string>
|
||||
<string name="allow_message_reactions_only_if">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string>
|
||||
@@ -686,7 +686,7 @@
|
||||
<string name="v5_2_favourites_filter_descr">تصفية الدردشات غير المقروءة والمفضلة.</string>
|
||||
<string name="v5_2_favourites_filter">البحث عن الدردشات بشكل أسرع</string>
|
||||
<string name="enable_receipts_all">فعّل</string>
|
||||
<string name="v5_2_disappear_one_message_descr">حتى عندما يتم تعطيله في المحادثة.</string>
|
||||
<string name="v5_2_disappear_one_message_descr">حتى عندما تُعطّل في المحادثة.</string>
|
||||
<string name="v5_2_fix_encryption_descr">إصلاح التعمية بعد استعادة النُسخ الاحتياطية.</string>
|
||||
<string name="v5_2_disappear_one_message">اجعل رسالة واحدة تختفي</string>
|
||||
<string name="error_enabling_delivery_receipts">خطأ في تفعيل إيصالات التسليم!</string>
|
||||
@@ -1106,7 +1106,7 @@
|
||||
<string name="tap_to_activate_profile">انقر لتنشيط ملف التعريف.</string>
|
||||
<string name="v4_5_transport_isolation">عزل النقل</string>
|
||||
<string name="this_string_is_not_a_connection_link">هذه السلسلة ليست رابط اتصال!</string>
|
||||
<string name="receipts_section_description">هذه الإعدادات لملف تعريفك الحالي</string>
|
||||
<string name="these_settings_are_for_your_current_profile">هذه الإعدادات لملف تعريفك الحالي</string>
|
||||
<string name="receipts_section_description_1">يمكن تجاوزها في إعدادات الاتصال والمجموعة.</string>
|
||||
<string name="network_option_tcp_connection_timeout">انتهت مهلة اتصال TCP</string>
|
||||
<string name="v4_5_private_filenames_descr">لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC).</string>
|
||||
@@ -1219,6 +1219,13 @@
|
||||
<string name="update_network_settings_question">تحديث إعدادات الشبكة؟</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">سيؤدي تحديث الإعدادات إلى إعادة توصيل العميل بجميع الخوادم.</string>
|
||||
<string name="you_are_observer">أنت المراقب</string>
|
||||
<string name="connect_plan_you_are_observer">أنت المراقب</string>
|
||||
<string name="connect_plan_you_are_member">أنت عضو</string>
|
||||
<string name="connect_plan_you_are_moderator">أنت مُشرف</string>
|
||||
<string name="connect_plan_you_are_admin">أنت المُدير</string>
|
||||
<string name="connect_plan_you_are_owner">أنت المالك</string>
|
||||
<string name="connect_plan_you_are_subscriber">أنت مشترك</string>
|
||||
<string name="connect_plan_you_are_contributor">أنت مساهم</string>
|
||||
<string name="you_are_invited_to_group">أنت مدعو إلى المجموعة</string>
|
||||
<string name="callstate_waiting_for_confirmation">في انتظار التأكيد…</string>
|
||||
<string name="unknown_database_error_with_info">خطأ غير معروف في قاعدة البيانات: %s</string>
|
||||
@@ -1566,7 +1573,7 @@
|
||||
<string name="v5_5_simpler_connect_ui_descr">يقبل شريط البحث روابط الدعوة.</string>
|
||||
<string name="v5_5_message_delivery">تحسّن تسليم الرسائل</string>
|
||||
<string name="v5_5_message_delivery_descr">مع انخفاض استخدام البطارية.</string>
|
||||
<string name="clear_note_folder_warning">سيتم حذف كافة الرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="clear_note_folder_warning">ستُحذف كل الرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="info_row_created_at">أُنشئ في</string>
|
||||
<string name="v5_5_new_interface_languages">واجهة المستخدم المجرية والتركية</string>
|
||||
<string name="v5_5_simpler_connect_ui">الصق الرابط للاتصال!</string>
|
||||
@@ -1710,7 +1717,7 @@
|
||||
<string name="recipients_can_not_see_who_message_from">لا يستطيع المُستلم/ون معرفة مَن أرسل هذه الرسالة.</string>
|
||||
<string name="saved_description">حُفظت</string>
|
||||
<string name="saved_from_chat_item_info_title">حُفظت مِن</string>
|
||||
<string name="saved_from_description">حُفظت مِن %s</string>
|
||||
<string name="saved_from">حُفظت مِن</string>
|
||||
<string name="audio_device_speaker">السماعة</string>
|
||||
<string name="audio_device_earpiece">سماعة الأذن</string>
|
||||
<string name="audio_device_wired_headphones">سماعات الرأس</string>
|
||||
@@ -2184,7 +2191,7 @@
|
||||
<string name="add_your_team_members_to_conversations">أضف أعضاء فريقك إلى المحادثات.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">يُمنع إرسال الرسائل المباشرة بين الأعضاء في هذه الدردشة.</string>
|
||||
<string name="xiaomi_ignore_battery_optimization"><![CDATA[<b>أجهزة Xiaomi</b>: يُرجى تفعيل التشغيل التلقائي (Autostart) في إعدادات النظام لكي تعمل الإشعارات.]]></string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[يتم إرسال جميع الرسائل والملفات <b>مُعمَّاة بين الطرفين</b>، مع أمان ما بعد الكم في الرسائل المباشرة.]]></string>
|
||||
<string name="all_message_and_files_e2e_encrypted"><![CDATA[تُرسل جميع الرسائل والملفات <b>مُعمَّاة بين الطرفين</b>، مع أمان ما بعد الكم في الرسائل المباشرة.]]></string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc_short">تحقق من الرسائل كل 10 دقائق</string>
|
||||
<string name="direct_messages_are_prohibited">يُمنع إرسال الرسائل المباشرة بين الأعضاء.</string>
|
||||
<string name="info_row_chat">الدردشة</string>
|
||||
@@ -2247,7 +2254,7 @@
|
||||
<string name="error_creating_chat_tags">خطأ في إنشاء قائمة الدردشة</string>
|
||||
<string name="chat_list_businesses">الشركات</string>
|
||||
<string name="error_loading_chat_tags">خطأ في تحميل قوائم الدردشة</string>
|
||||
<string name="delete_chat_list_warning">سيتم إزالة جميع المحادثات من القائمة %s، وسيتم حذف القائمة</string>
|
||||
<string name="delete_chat_list_warning">ستُزال جميع المحادثات من القائمة %s، وستُحذف القائمة</string>
|
||||
<string name="create_list">أنشئ قائمة</string>
|
||||
<string name="error_updating_chat_tags">خطأ في تحديث قائمة الدردشة</string>
|
||||
<string name="chat_list_notes">الملحوظات</string>
|
||||
@@ -2255,7 +2262,7 @@
|
||||
<string name="change_order_chat_list_menu_action">تغيير الترتيب</string>
|
||||
<string name="prefs_error_saving_settings">خطأ في حفظ الإعدادات</string>
|
||||
<string name="error_creating_report">خطأ في إنشاء بلاغ</string>
|
||||
<string name="report_item_visibility_submitter">أنت والمشرفون فقط هم من يرون ذلك</string>
|
||||
<string name="report_item_visibility_submitter">أنت والمُشرفون فقط هم من يرون ذلك</string>
|
||||
<string name="report_item_archived">بلاغ مؤرشف</string>
|
||||
<string name="report_item_visibility_moderators">لا يراه إلا المُرسِل والمُشرفين</string>
|
||||
<string name="archive_verb">أرشف</string>
|
||||
@@ -2269,15 +2276,15 @@
|
||||
<string name="group_reports_active_one">1 بلاغ</string>
|
||||
<string name="group_reports_active">%d بلاغات</string>
|
||||
<string name="group_reports_member_reports">بلاغات الأعضاء</string>
|
||||
<string name="report_compose_reason_header_illegal">بلّغ عن المحتوى: سيراه مشرفو المجموعة فقط.</string>
|
||||
<string name="report_compose_reason_header_other">بلّغ عن أُخرى: سيراه مشرفو المجموعة فقط.</string>
|
||||
<string name="group_member_role_moderator">مشرف</string>
|
||||
<string name="report_compose_reason_header_illegal">بلّغ عن المحتوى: سيراه مُشرفو المجموعة فقط.</string>
|
||||
<string name="report_compose_reason_header_other">بلّغ عن أُخرى: سيراه مُشرفو المجموعة فقط.</string>
|
||||
<string name="group_member_role_moderator">مُشرف</string>
|
||||
<string name="report_item_archived_by">بلاغ مؤرشف بواسطة %s</string>
|
||||
<string name="report_compose_reason_header_profile">بلّغ عن ملف تعريف العضو: سيراه مشرفو المجموعة فقط.</string>
|
||||
<string name="report_compose_reason_header_profile">بلّغ عن ملف تعريف العضو: سيراه مُشرفو المجموعة فقط.</string>
|
||||
<string name="report_reason_community">انتهاك إرشادات المجتمع</string>
|
||||
<string name="report_reason_illegal">محتوى غير لائق</string>
|
||||
<string name="report_compose_reason_header_community">بلّغ عن مخالفة: سيراه مشرفو المجموعة فقط.</string>
|
||||
<string name="report_compose_reason_header_spam">بلّغ عن إزعاج (spam): سيراه مشرفو المجموعة فقط.</string>
|
||||
<string name="report_compose_reason_header_community">بلّغ عن مخالفة: سيراه مُشرفو المجموعة فقط.</string>
|
||||
<string name="report_compose_reason_header_spam">بلّغ عن إزعاج (spam): سيراه مُشرفو المجموعة فقط.</string>
|
||||
<string name="report_archive_alert_title">أرشفة البلاغ؟</string>
|
||||
<string name="report_reason_alert_title">سبب الإبلاغ؟</string>
|
||||
<string name="report_archive_alert_desc">سيتم أرشفة البلاغ لك.</string>
|
||||
@@ -2307,14 +2314,14 @@
|
||||
<string name="mute_all_chat">اكتم الكل</string>
|
||||
<string name="unread_mentions">ذّكورات غير مقروءة</string>
|
||||
<string name="max_group_mentions_per_message_reached">يمكنك ذكر ما يصل إلى %1$s من الأعضاء في الرسالة الواحدة!</string>
|
||||
<string name="enable_sending_member_reports">السماح بالإبلاغ عن الرسائل إلى المشرفين.</string>
|
||||
<string name="disable_sending_member_reports">امنع الإبلاغ عن الرسائل للمشرفين.</string>
|
||||
<string name="enable_sending_member_reports">السماح بالإبلاغ عن الرسائل إلى المُشرفين.</string>
|
||||
<string name="disable_sending_member_reports">امنع الإبلاغ عن الرسائل للمُشرفين.</string>
|
||||
<string name="report_archive_alert_title_all">أرشفة كافة البلاغات؟</string>
|
||||
<string name="archive_reports">أرشف البلاغات</string>
|
||||
<string name="report_archive_for_all_moderators">لكل المشرفين</string>
|
||||
<string name="report_archive_for_all_moderators">لكل المُشرفين</string>
|
||||
<string name="report_archive_for_me">لي</string>
|
||||
<string name="notification_group_report">بلاغ: %s</string>
|
||||
<string name="group_members_can_send_reports">يمكن للأعضاء الإبلاغ عن الرسائل إلى المشرفين.</string>
|
||||
<string name="group_members_can_send_reports">يمكن للأعضاء الإبلاغ عن الرسائل إلى المُشرفين.</string>
|
||||
<string name="report_archive_alert_desc_all">سيتم أرشفة كافة البلاغات لك.</string>
|
||||
<string name="report_archive_alert_title_nth">أرشفة %d بلاغ؟</string>
|
||||
<string name="member_reports_are_prohibited">يُمنع الإبلاغ عن الرسائل في هذه المجموعة.</string>
|
||||
@@ -2343,7 +2350,7 @@
|
||||
<string name="unblock_members_desc">سيتم عرض رسائل من هؤلاء الأعضاء!</string>
|
||||
<string name="restore_passphrase_can_not_be_read_enter_manually_desc">لا يمكن قراءة عبارة المرور في Keystore، يُرجى إدخالها يدويًا. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطوِّرين.</string>
|
||||
<string name="members_will_be_removed_from_group_cannot_be_undone">سيتم إزالة الأعضاء من المجموعة - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="feature_roles_moderators">المشرفين</string>
|
||||
<string name="feature_roles_moderators">المُشرفين</string>
|
||||
<string name="restore_passphrase_can_not_be_read_desc">لا يمكن قراءة عبارة المرور في Keystore. قد يكون هذا قد حدث بعد تحديث النظام غير متوافق مع التطبيق. إذا لم يكن الأمر كذلك، فيُرجى التواصل مع المطوِّرين.</string>
|
||||
<string name="group_member_status_pending_approval">موافقة الانتظار</string>
|
||||
<string name="onboarding_conditions_privacy_policy_and_conditions_of_use">سياسة الخصوصية وشروط الاستخدام.</string>
|
||||
@@ -2364,7 +2371,7 @@
|
||||
<string name="group_new_support_chats_short">%d دردشة/ات</string>
|
||||
<string name="group_new_support_chats">%d دردشات مع الأعضاء</string>
|
||||
<string name="group_new_support_messages">%d رسائل</string>
|
||||
<string name="report_sent_alert_title">أُرسِل البلاغ للمشرفين</string>
|
||||
<string name="report_sent_alert_title">أُرسِل البلاغ للمُشرفين</string>
|
||||
<string name="report_sent_alert_msg_view_in_support_chat">يمكنك عرض تقاريرك في \"دردش مع المُدراء\".</string>
|
||||
<string name="accept_pending_member_alert_confirmation_as_observer">اقبل كمراقب</string>
|
||||
<string name="cant_send_message_contact_deleted">حُذفت جهة الاتصال</string>
|
||||
@@ -2375,7 +2382,7 @@
|
||||
<string name="rcv_group_event_member_accepted">قبلت %1$s</string>
|
||||
<string name="rcv_group_event_user_accepted">قبِلك</string>
|
||||
<string name="snd_group_event_member_accepted">لقد قبلت هذا العضو.</string>
|
||||
<string name="snd_group_event_user_pending_review">الرجاء الانتظار ريثما يراجع مشرفو المجموعة طلبك للانضمام إليها.</string>
|
||||
<string name="snd_group_event_user_pending_review">الرجاء الانتظار ريثما يراجع مُشرفو المجموعة طلبك للانضمام إليها.</string>
|
||||
<string name="button_support_chat">دردش مع المُدراء</string>
|
||||
<string name="admission_stage_review">راجع الأعضاء</string>
|
||||
<string name="member_criteria_off">غير مفعّل</string>
|
||||
@@ -2445,7 +2452,7 @@
|
||||
<string name="v6_4_connect_faster">اتصل بشكل أسرع! 🚀</string>
|
||||
<string name="v6_4_message_delivery_descr">تقليل حركة البيانات على شبكات الجوّال.</string>
|
||||
<string name="v6_4_connect_faster_descr">راسل فورًا بمجرد النقر على \"اتصل\".</string>
|
||||
<string name="v6_4_role_moderator">دور جديد للمجموعة: مشرف</string>
|
||||
<string name="v6_4_role_moderator">دور جديد للمجموعة: مُشرف</string>
|
||||
<string name="private_routing_no_session">لا توجد جلسة توجيه خاصة</string>
|
||||
<string name="private_routing_timeout">انتهت مهلة التوجيه الخاص</string>
|
||||
<string name="network_option_protocol_timeout_background">انتهت مهلة خلفية البروتوكول</string>
|
||||
@@ -2746,7 +2753,7 @@
|
||||
<string name="group_members_can_add_message_reactions_channel">يمكن للمشتركين إضافة ردود الفعل على الرسائل.</string>
|
||||
<string name="members_can_chat_with_admins_channel">يمكن للمشتركين الدردشة مع المُدراء.</string>
|
||||
<string name="group_members_can_delete_channel">يمكن للمشتركين حذف الرسائل المُرسلة نهائيًا. (24 ساعة)</string>
|
||||
<string name="group_members_can_send_reports_channel">يمكن للمشتركين الإبلاغ عن الرسائل للمشرفين.</string>
|
||||
<string name="group_members_can_send_reports_channel">يمكن للمشتركين الإبلاغ عن الرسائل للمُشرفين.</string>
|
||||
<string name="group_members_can_send_dms_channel">يمكن للمشتركين إرسال رسائل مباشرة.</string>
|
||||
<string name="group_members_can_send_disappearing_channel">يمكن للمشتركين إرسال رسائل تختفي.</string>
|
||||
<string name="group_members_can_send_files_channel">يمكن للمشتركين إرسال الملفات والوسائط.</string>
|
||||
@@ -2878,7 +2885,7 @@
|
||||
<string name="connect_plan_join_name">انضم للقناة %s</string>
|
||||
<string name="message_signatures_are_not_required">توقيع الرسالة ليس إلزاميًا.</string>
|
||||
<string name="message_signatures_are_required">مطلوب توقيع الرسالة.</string>
|
||||
<string name="register_test_name">سجِّل اسم اختبار</string>
|
||||
<string name="register_test_name">كيفية تسجيل اسم اختبار</string>
|
||||
<string name="remove_name">أزِل الاسم</string>
|
||||
<string name="require_message_signatures">تطلب توقيع الرسائل.</string>
|
||||
<string name="save_simplex_name_question">احفظ اسم SimpleX؟</string>
|
||||
@@ -2902,8 +2909,8 @@
|
||||
<string name="v7_0_channels_previews">أنشئ معاينة الويب.</string>
|
||||
<string name="v7_0_channels_wider_messages">أسهل في القراءة.</string>
|
||||
<string name="v7_0_channels_relays">أدِر مُرحلاتك.</string>
|
||||
<string name="v7_0_simplex_names">أسماء SimpleX (تجريبي)</string>
|
||||
<string name="v7_0_simplex_names_descr">أسماء لقناتك أو لشركتك.</string>
|
||||
<string name="v7_0_simplex_names">أسماء SimpleX العامة (تجريبي)</string>
|
||||
<string name="v7_0_simplex_names_descr">الأسماء العامة لقناتك أو لشركتك.</string>
|
||||
<string name="info_row_file_servers">خوادم الملفات</string>
|
||||
<string name="share_text_file_servers">خوادم الملفات: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
<string name="connect_plan_open_new_chat">Open new chat</string>
|
||||
<string name="connect_plan_open_group">Open group</string>
|
||||
<string name="connect_plan_open_new_group">Open new group</string>
|
||||
<string name="connect_plan_you_are_observer">You are an observer</string>
|
||||
<string name="connect_plan_you_are_member">You are a member</string>
|
||||
<string name="connect_plan_you_are_moderator">You are a moderator</string>
|
||||
<string name="connect_plan_you_are_admin">You are an admin</string>
|
||||
<string name="connect_plan_you_are_owner">You are an owner</string>
|
||||
<string name="error_parsing_uri_title">Invalid link</string>
|
||||
<string name="error_parsing_uri_desc">Please check that SimpleX link is correct.</string>
|
||||
|
||||
@@ -65,8 +70,9 @@
|
||||
<string name="live">LIVE</string>
|
||||
<string name="moderated_description">moderated</string>
|
||||
<string name="forwarded_description">forwarded</string>
|
||||
<string name="forwarded_from">forwarded from</string>
|
||||
<string name="saved_description">saved</string>
|
||||
<string name="saved_from_description">saved from %s</string>
|
||||
<string name="saved_from">saved from</string>
|
||||
<string name="invalid_chat">invalid chat</string>
|
||||
<string name="invalid_data">invalid data</string>
|
||||
<string name="error_showing_message">error showing message</string>
|
||||
@@ -1206,6 +1212,7 @@
|
||||
<string name="stop_sharing_address">Stop sharing address?</string>
|
||||
<string name="stop_sharing">Stop sharing</string>
|
||||
<string name="auto_accept_contact">Auto-accept</string>
|
||||
<string name="group_invitations">Group invitations</string>
|
||||
<string name="sent_to_your_contact_after_connection">Sent to your contact after connection.</string>
|
||||
<string name="address_welcome_message">Welcome message</string>
|
||||
<string name="enter_welcome_message_optional">Enter welcome message… (optional)</string>
|
||||
@@ -1557,7 +1564,7 @@
|
||||
<string name="if_you_enter_passcode_data_removed">If you enter this passcode when opening the app, all app data will be irreversibly removed!</string>
|
||||
<string name="set_passcode">Set passcode</string>
|
||||
<string name="this_setting_is_for_your_current_profile">This setting is for your current profile</string>
|
||||
<string name="receipts_section_description">These settings are for your current profile</string>
|
||||
<string name="these_settings_are_for_your_current_profile">These settings are for your current profile</string>
|
||||
<string name="receipts_section_description_1">They can be overridden in contact and group settings.</string>
|
||||
<string name="receipts_section_contacts">Contacts</string>
|
||||
<string name="receipts_contacts_title_enable">Enable receipts?</string>
|
||||
@@ -1602,7 +1609,7 @@
|
||||
<string name="settings_section_title_chats">Chats</string>
|
||||
<string name="settings_section_title_files">Files</string>
|
||||
<string name="settings_section_title_delivery_receipts">Send delivery receipts to</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Contact requests from groups</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Contact requests in groups</string>
|
||||
<string name="settings_section_title_about">About</string>
|
||||
<string name="settings_section_title_contact">Contact</string>
|
||||
<string name="settings_section_title_support_project">Support the project</string>
|
||||
@@ -2736,6 +2743,10 @@
|
||||
<string name="v6_5_safe_web_links_descr">- opt-in to send link previews.\n- use SOCKS proxy if enabled.\n- prevent hyperlink phishing.\n- remove link tracking.</string>
|
||||
<string name="v6_5_non_profit_governance">Non-profit governance</string>
|
||||
<string name="v6_5_non_profit_governance_descr">To make SimpleX Network last.</string>
|
||||
<string name="v7_0_invest" translatable="false">You can now invest in SimpleX Chat! 🚀</string>
|
||||
<string name="v7_0_invest_descr" translatable="false">Crowdfunding on Wefunder.</string>
|
||||
<string name="v7_0_crowdfunding" translatable="false">Crowdfunding on Wefunder</string>
|
||||
<string name="v7_0_invest_learn_more" translatable="false">Learn more on Wefunder</string>
|
||||
<string name="v7_0_simplex_names">SimpleX public names (BETA)</string>
|
||||
<string name="v7_0_simplex_names_descr">Public names for your channel or business.</string>
|
||||
<string name="v7_0_channels">Better channels 📢</string>
|
||||
@@ -3165,6 +3176,8 @@
|
||||
<string name="relay_address_alert_message">This is a chat relay address, it cannot be used to connect.</string>
|
||||
<string name="connect_plan_open_channel">Open channel</string>
|
||||
<string name="connect_plan_open_new_channel">Open new channel</string>
|
||||
<string name="connect_plan_you_are_subscriber">You are a subscriber</string>
|
||||
<string name="connect_plan_you_are_contributor">You are a contributor</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel">Your channel</string>
|
||||
<string name="connect_plan_this_is_your_link_for_channel_vName"><![CDATA[This is your link for channel <b>%1$s</b>!]]></string>
|
||||
<string name="error_opening_channel">Error opening channel</string>
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
<string name="enter_correct_passphrase">Въведи правилна парола.</string>
|
||||
<string name="feature_enabled_for_you">активирано за вас</string>
|
||||
<string name="enter_password_to_show">Въведи парола в търсенето</string>
|
||||
<string name="receipts_section_description">Тези настройки са за текущия ви профил</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Тези настройки са за текущия ви профил</string>
|
||||
<string name="receipts_section_description_1">Те могат да бъдат променени в настройките за всеки контакт и група.</string>
|
||||
<string name="settings_developer_tools">Инструменти за разработчици</string>
|
||||
<string name="receipts_contacts_disable_for_all">Деактивиране за всички</string>
|
||||
@@ -1301,6 +1301,11 @@
|
||||
<string name="auth_unlock">Отключи</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</string>
|
||||
<string name="you_are_observer">вие сте наблюдател</string>
|
||||
<string name="connect_plan_you_are_observer">Вие сте наблюдател</string>
|
||||
<string name="connect_plan_you_are_member">Вие сте член</string>
|
||||
<string name="connect_plan_you_are_moderator">Вие сте модератор</string>
|
||||
<string name="connect_plan_you_are_admin">Вие сте админ</string>
|
||||
<string name="connect_plan_you_are_owner">Вие сте собственик</string>
|
||||
<string name="gallery_video_button">Видео</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color="#0088ff">се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string>
|
||||
<string name="contact_wants_to_connect_with_you">иска да се свърже с вас!</string>
|
||||
@@ -1723,7 +1728,7 @@
|
||||
<string name="v5_7_forward">Препращане и запазване на съобщения</string>
|
||||
<string name="v5_7_call_sounds">Звуци по време на разговор</string>
|
||||
<string name="saved_description">запазено</string>
|
||||
<string name="saved_from_description">запазено от %s</string>
|
||||
<string name="saved_from">запазено от</string>
|
||||
<string name="saved_chat_item_info_tab">Запазено</string>
|
||||
<string name="saved_from_chat_item_info_title">Запазено от</string>
|
||||
<string name="recipients_can_not_see_who_message_from">Получателят(ите) не могат да видят от кого е това съобщение.</string>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
<string name="callstatus_accepted">কলটি গৃহীত হয়েছে</string>
|
||||
<string name="smp_servers_preset_add">পূর্বনির্ধারিত সার্ভারগুলি যুক্ত করুন</string>
|
||||
<string name="group_member_role_admin">অ্যাডমিন</string>
|
||||
<string name="connect_plan_you_are_admin">আপনি একজন অ্যাডমিন</string>
|
||||
<string name="button_add_welcome_message">স্বাগত বার্তা যুক্ত করুন</string>
|
||||
<string name="users_add">প্রোফাইল যুক্ত করুন</string>
|
||||
<string name="color_secondary_variant">আনুষঙ্গিক রং</string>
|
||||
|
||||
@@ -1573,7 +1573,7 @@
|
||||
<string name="note_folder_local_display_name">Notes privades</string>
|
||||
<string name="receiving_files_not_yet_supported">la recepció de fitxers encara no està suportada</string>
|
||||
<string name="display_name_requested_to_connect">sol·licitada connexió</string>
|
||||
<string name="saved_from_description">desat des de %s</string>
|
||||
<string name="saved_from">desat des de</string>
|
||||
<string name="simplex_link_contact">Adreça de contacte SimpleX</string>
|
||||
<string name="simplex_link_group">Enllaç de grup SimpleX</string>
|
||||
<string name="simplex_link_mode">Enllaços SimpleX</string>
|
||||
@@ -1715,6 +1715,11 @@
|
||||
<string name="image_decoding_exception_desc">La imatge no es pot descodificar. Si us plau, proveu amb una imatge diferent o contacteu amb els desenvolupadors.</string>
|
||||
<string name="video_decoding_exception_desc">El vídeo no es pot descodificar. Si us plau, prova amb un vídeo diferent o contacta amb els desenvolupadors.</string>
|
||||
<string name="you_are_observer">ets observador</string>
|
||||
<string name="connect_plan_you_are_observer">Ets observador</string>
|
||||
<string name="connect_plan_you_are_member">Ets membre</string>
|
||||
<string name="connect_plan_you_are_moderator">Ets moderador</string>
|
||||
<string name="connect_plan_you_are_admin">Ets administrador</string>
|
||||
<string name="connect_plan_you_are_owner">Ets propietari</string>
|
||||
<string name="observer_cant_send_message_title">ets observador(a)</string>
|
||||
<string name="observer_cant_send_message_desc">Poseu-vos en contacte amb l\'administrador del grup.</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Només els propietaris del grup poden activar fitxers i mitjans.</string>
|
||||
@@ -1946,7 +1951,7 @@
|
||||
<string name="if_you_enter_passcode_data_removed">Si introduïu aquesta contrasenya en obrir l\'aplicació, totes les dades de l\'aplicació s\'eliminaran de manera irreversible.</string>
|
||||
<string name="if_you_enter_self_destruct_code">Si introduïu el vostre codi d\'autodestrucció mentre obriu l\'aplicació:</string>
|
||||
<string name="set_passcode">Estableix codi</string>
|
||||
<string name="receipts_section_description">Aquesta configuració és per al vostre perfil actual</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Aquesta configuració és per al vostre perfil actual</string>
|
||||
<string name="receipts_section_description_1">Es pot canviar a la configuració de contacte i grup.</string>
|
||||
<string name="privacy_media_blur_radius_off">No</string>
|
||||
<string name="settings_section_title_settings">Configuració</string>
|
||||
|
||||
@@ -931,6 +931,11 @@
|
||||
<string name="moderate_verb">Moderovat</string>
|
||||
<string name="observer_cant_send_message_desc">Kontaktujte prosím správce skupiny.</string>
|
||||
<string name="you_are_observer">jste pozorovatel</string>
|
||||
<string name="connect_plan_you_are_observer">Jste pozorovatel</string>
|
||||
<string name="connect_plan_you_are_member">Jste člen</string>
|
||||
<string name="connect_plan_you_are_moderator">Jste moderátor</string>
|
||||
<string name="connect_plan_you_are_admin">Jste správce</string>
|
||||
<string name="connect_plan_you_are_owner">Jste vlastník</string>
|
||||
<string name="group_member_role_observer">pozorovatel</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Zpráva bude smazána pro všechny členy.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Zpráva bude pro všechny členy označena jako moderovaná.</string>
|
||||
@@ -1246,7 +1251,7 @@
|
||||
<string name="snd_conn_event_ratchet_sync_required">vyžadováno opětovné vyjednávání šifrování pro %s</string>
|
||||
<string name="receipts_contacts_override_disabled">Odesílání potvrzení o doručení je vypnuto pro %d kontakty.</string>
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Odesílání potvrzení o doručení bude povoleno pro všechny kontakty ve všech viditelných profilech chatu.</string>
|
||||
<string name="receipts_section_description">Toto nastavení je pro váš aktuální profil</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Toto nastavení je pro váš aktuální profil</string>
|
||||
<string name="conn_event_ratchet_sync_allowed">opětovné vyjednávání šifrování povoleno</string>
|
||||
<string name="snd_conn_event_ratchet_sync_allowed">opětovné vyjednávání šifrování povoleno pro %s</string>
|
||||
<string name="conn_event_ratchet_sync_required">vyžadováno opětovné vyjednávání šifrování</string>
|
||||
@@ -1693,7 +1698,7 @@
|
||||
<string name="v5_7_network_descr">Spolehlivější síťové připojení.</string>
|
||||
<string name="allow_to_send_simplex_links">Povolit odesílat SimpleX odkazy.</string>
|
||||
<string name="saved_description">uloženo</string>
|
||||
<string name="saved_from_description">Uloženo z %s</string>
|
||||
<string name="saved_from">Uloženo z</string>
|
||||
<string name="saved_chat_item_info_tab">Uloženo</string>
|
||||
<string name="forwarded_chat_item_info_tab">Přeposláno</string>
|
||||
<string name="saved_from_chat_item_info_title">Uloženo z</string>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<string name="connect_plan_open_group">Åben gruppe</string>
|
||||
<string name="connect_plan_open_new_group">Åbn ny gruppe</string>
|
||||
<string name="error_parsing_uri_title">Ugyldigt link</string>
|
||||
<string name="error_parsing_uri_desc">Kontroller at SimpleX-linket er korrekt.</string>
|
||||
<string name="error_parsing_uri_desc">Kontroller, at SimpleX-linket er korrekt.</string>
|
||||
<string name="opening_database">Åbner databasen…</string>
|
||||
<string name="database_migration_in_progress">Databasemigrering er i gang.\nDet kan tage et par minutter.</string>
|
||||
<string name="non_content_uri_alert_title">Ugyldig filsti</string>
|
||||
@@ -201,7 +201,7 @@
|
||||
<string name="moderated_description">modereret</string>
|
||||
<string name="forwarded_description">videresendt</string>
|
||||
<string name="saved_description">gemt</string>
|
||||
<string name="saved_from_description">gemt fra %s</string>
|
||||
<string name="saved_from">gemt fra</string>
|
||||
<string name="invalid_chat">ugyldig chat</string>
|
||||
<string name="invalid_data">ugyldige data</string>
|
||||
<string name="error_showing_message">fejl ved visning af besked</string>
|
||||
@@ -635,6 +635,8 @@
|
||||
<string name="cant_send_message_you_left">du forlod</string>
|
||||
<string name="cant_send_message_generic">kan ikke sende beskeder</string>
|
||||
<string name="you_are_observer">du er observatør</string>
|
||||
<string name="connect_plan_you_are_observer">Du er observatør</string>
|
||||
<string name="connect_plan_you_are_admin">Du er administrator</string>
|
||||
<string name="reviewed_by_admins">gennemgået af administratorer</string>
|
||||
<string name="cant_send_message_member_has_old_version">medlemmet har en gammel version</string>
|
||||
<string name="image_descr">Billede</string>
|
||||
@@ -882,4 +884,64 @@
|
||||
<string name="another_instance_title">Appen kører allerede</string>
|
||||
<string name="app_update_required">App\'en skal opdateres</string>
|
||||
<string name="chat_link_business_address">Virksomhedsadresse</string>
|
||||
<string name="connect_plan_join_name">Deltag i kanal %s</string>
|
||||
<string name="connect_plan_connect_to_name">Forbind til %s</string>
|
||||
<string name="another_instance_not_responding">En anden instans af appen kører eller blev ikke lukket korrekt. Start alligevel?</string>
|
||||
<string name="channel_owners_contributors_count">%1$d ejere og bidragsydere</string>
|
||||
<string name="relay_bar_relays_failed">%1$d relays fejlede</string>
|
||||
<string name="relay_bar_relays_not_active">%1$d relays ikke aktive</string>
|
||||
<string name="relay_bar_relays_removed">%1$d relays fjernet</string>
|
||||
<string name="channel_subscriber_count_singular">%1$d abonnent</string>
|
||||
<string name="channel_subscriber_count_plural">%1$d abonnenter</string>
|
||||
<string name="badge_supported_simplex">%1$s støttede SimpleX Chat. Mærket udløb den %2$s.</string>
|
||||
<string name="settings_section_title_about">Om</string>
|
||||
<string name="relay_status_accepted">accepteret</string>
|
||||
<string name="v7_0_channels_contributors">Tilføj bidragsydere.</string>
|
||||
<string name="add_description">Tilføj beskrivelse</string>
|
||||
<string name="a_link_for_one_person">Et link til at en enkelt person kan forbinde</string>
|
||||
<string name="content_filter_all_messages">Alle beskeder</string>
|
||||
<string name="messages_section_title">Beskeder</string>
|
||||
<string name="settings_section_title_messages">Beskeder og filer</string>
|
||||
<string name="allow_chat_with_admins">Tillad medlemmer at chatte med admins.</string>
|
||||
<string name="allow_direct_messages_channel">Tillad at sende direkte beskeder til abonnenter.</string>
|
||||
<string name="allow_chat_with_admins_channel">Tillad abonnenter at chatte med admins.</string>
|
||||
<string name="relay_bar_all_relays_failed">Alle relays fejlede</string>
|
||||
<string name="relay_bar_all_relays_removed">Alle relays fjernet</string>
|
||||
<string name="embed_any_webpage_can_show">Enhver hjemmeside kan vise forhåndsvisningen.</string>
|
||||
<string name="badge_unknown_key_title">Mærke kan ikke bekræftes</string>
|
||||
<string name="onboarding_be_free">Vær fri\ni dit netværk</string>
|
||||
<string name="why_built_tagline">Vær fri i dit netværk.</string>
|
||||
<string name="v7_0_channels">Bedre kanaler 📢</string>
|
||||
<string name="block_subscriber_for_all_question">Blokér abonnent for alle?</string>
|
||||
<string name="one_hand_ui_bottom_bar">Værktøjslinje nederst</string>
|
||||
<string name="server_no_sub">intet abonnement</string>
|
||||
<string name="not_connected_to_server_to_receive_messages_no_sub">Du er ikke forbundet til den server, der bruges til at modtage meddelelser fra denne forbindelse (intet abonnement).</string>
|
||||
<string name="voice_recording_not_supported">Stemmeoptagelse er ikke understøttet på din platform</string>
|
||||
<string name="e2ee_info_no_e2ee"><![CDATA[Beskeder i denne kanal er <b>ikke ende-til-ende-krypteret</b>. Et chat-relay kan se disse beskeder.]]></string>
|
||||
<string name="simplex_link_relay">SimpleX relay-adresse</string>
|
||||
<string name="no_chat_relays_enabled">Ingen chat-relays aktiveret.</string>
|
||||
<string name="no_names_servers_enabled">Ingen servere til at slå navne op.</string>
|
||||
<string name="server_warning">Server-advarsel</string>
|
||||
<string name="network_error_unknown_ca">Fingeraftrykket i serveradressen matcher ikke certifikatet: %1$s.</string>
|
||||
<string name="proxy_destination_error_unknown_ca">Fingeraftrykket i destinations-serveradressen matcher ikke certifikatet: %1$s.</string>
|
||||
<string name="error_marking_member_support_chat_read">Fejl ved markering som læst</string>
|
||||
<string name="unsupported_channel_name">Ikke-understøttet navn på kanal</string>
|
||||
<string name="unsupported_contact_name">Ikke-understøttet navn på kontakt</string>
|
||||
<string name="channel_name_requires_newer_app_version">Forbindelse gennem kanal-navnet kræver en nyere version af appen.</string>
|
||||
<string name="contact_name_requires_newer_app_version">Forbindelse gennem kontakt-navnet kræver en nyere version af appen.</string>
|
||||
<string name="please_upgrade_the_app">Opgrader appen.</string>
|
||||
<string name="simplex_name_error">Fejl i SimpleX-navn</string>
|
||||
<string name="simplex_name_no_servers_desc">Ingen af dine servere er sat op til at slå SimpleX-navne op. Konfigurer serverne, eller anvend et forbindelseslink.</string>
|
||||
<string name="simplex_name_not_found">Navn ikke fundet</string>
|
||||
<string name="simplex_name_not_found_desc">Dette SimpleX-navn er ikke registreret. Kontrollér navnet.</string>
|
||||
<string name="simplex_name_no_valid_link">Intet gyldigt link</string>
|
||||
<string name="simplex_name_no_valid_link_desc">SimpleX-navnet %1$s er registreret, men det har ikke et gyldigt link.</string>
|
||||
<string name="simplex_name_unconfirmed">Ubekræftet navn</string>
|
||||
<string name="simplex_name_unconfirmed_desc">SimpleX-navnet %1$s er registreret, men ikke tilføjet til profil. Tilføj det til din adresse eller kanal-profil, hvis du er indehaveren.</string>
|
||||
<string name="channel_temporarily_unavailable">Kanal midlertidigt utilgængelig</string>
|
||||
<string name="channel_no_active_relays_try_later">Kanalen har ingen aktive relays. Vent og prøv igen senere.</string>
|
||||
<string name="group_link_requires_newer_version">Denne gruppe kræver en nyere version af appen. Opdater appen for at kunne deltage.</string>
|
||||
<string name="error_deleting_message">Fejl ved sletning af meddelelse</string>
|
||||
<string name="save_simplex_name_question">Gem SimpleX-navn?</string>
|
||||
<string name="get_simplex_name_beta">Anskaf SimpleX-navn (BETA)</string>
|
||||
</resources>
|
||||
|
||||
@@ -1012,6 +1012,13 @@
|
||||
<string name="moderate_verb">Moderieren</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet.</string>
|
||||
<string name="you_are_observer">Sie sind Beobachter</string>
|
||||
<string name="connect_plan_you_are_observer">Sie sind Beobachter</string>
|
||||
<string name="connect_plan_you_are_member">Sie sind Mitglied</string>
|
||||
<string name="connect_plan_you_are_moderator">Sie sind Moderator</string>
|
||||
<string name="connect_plan_you_are_admin">Sie sind Admin</string>
|
||||
<string name="connect_plan_you_are_owner">Sie sind Eigentümer</string>
|
||||
<string name="connect_plan_you_are_subscriber">Sie sind Abonnent</string>
|
||||
<string name="connect_plan_you_are_contributor">Sie sind Mitwirkender</string>
|
||||
<string name="observer_cant_send_message_title">Sie sind Beobachter</string>
|
||||
<string name="group_member_role_observer">Beobachter</string>
|
||||
<string name="initial_member_role">Anfängliche Rolle</string>
|
||||
@@ -1364,7 +1371,7 @@
|
||||
<string name="v5_2_favourites_filter_descr">Nach ungelesenen und favorisierten Chats filtern.</string>
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert.</string>
|
||||
<string name="receipts_contacts_override_disabled">Das Senden von Bestätigungen an %d Kontakte ist deaktiviert</string>
|
||||
<string name="receipts_section_description">Diese Einstellungen gelten für Ihr aktuelles Chat-Profil</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Diese Einstellungen gelten für Ihr aktuelles Chat-Profil</string>
|
||||
<string name="receipts_section_description_1">Sie können in den Kontakt- und Gruppeneinstellungen überschrieben werden.</string>
|
||||
<string name="receipts_section_contacts">Kontakte</string>
|
||||
<string name="receipts_contacts_title_disable">Bestätigungen deaktivieren\?</string>
|
||||
@@ -1805,7 +1812,7 @@
|
||||
<string name="audio_device_wired_headphones">Kopfhörer</string>
|
||||
<string name="network_option_rcv_concurrency">Gelijktijdige ontvangst</string>
|
||||
<string name="recipients_can_not_see_who_message_from">Empfänger können nicht sehen, von wem die Nachricht stammt.</string>
|
||||
<string name="saved_from_description">abgespeichert von %s</string>
|
||||
<string name="saved_from">abgespeichert von</string>
|
||||
<string name="saved_chat_item_info_tab">Abgespeichert</string>
|
||||
<string name="saved_description">abgespeichert</string>
|
||||
<string name="forwarded_chat_item_info_tab">Weitergeleitet</string>
|
||||
@@ -2812,7 +2819,7 @@
|
||||
<string name="v6_5_invite_friends_descr">Wir haben das Verbinden für neue Nutzer vereinfacht.</string>
|
||||
<string name="your_public_address">Ihre öffentliche Adresse</string>
|
||||
<string name="why_built_heading">Sie wurden ohne ein Benutzerkonto geboren.</string>
|
||||
<string name="why_built_p1">Niemand verfolgte Ihre Gespräche. Niemand erstellte eine Karte, wo Sie sich aufgehalten haben. Privatsphäre war nie ein Feature - sie war selbstverständlich.</string>
|
||||
<string name="why_built_p1">Niemand verfolgte Ihre Gespräche. Niemand hat eine Karte erstellt, wo Sie überall waren. Privatsphäre war nie ein Feature – sie war eine Selbstverständlichkeit.</string>
|
||||
<string name="why_built_p2">Dann sind wir online gegangen, und jede Plattform wollte Etwas von Ihnen - Ihren Namen, Ihre Nummer, Ihre Freunde. Wir akzeptierten, dass es der Preis mit Anderen zu kommunizieren ist, Jemandem preiszugeben, mit wem und wie wir miteinander kommunizieren. Jede Generation, Menschen und Technologien, kannten es nur so - Telefon, E-Mail, Messenger, soziale Medien. Es schien der einzig mögliche Weg zu sein.</string>
|
||||
<string name="why_built_p3">Es gibt einen anderen Weg. Ein Netzwerk ohne Telefonnummern, ohne Benutzerkonten, ohne Benutzerkennungen und ohne jegliche Benutzeridentität. Ein Netzwerk, welches Menschen verbindet und verschlüsselte Nachrichten überträgt, ohne zu wissen, wer mit wem verbunden ist.</string>
|
||||
<string name="why_built_p4">Nicht ein besseres Schloss an der Tür eines Anderen. Kein freundlicher Vermieter, der Ihre Privatsphäre respektiert, aber dennoch jeden Besucher registriert. Sie sind kein Gast. Sie sind zu Hause. Kein Vermieter, kein Fremder kann es betreten - Sie sind souverän.</string>
|
||||
@@ -2959,7 +2966,7 @@
|
||||
<string name="simplex_name_error">Fehler beim SimpleX-Namen</string>
|
||||
<string name="simplex_name_not_verified">SimpleX-Name ist nicht verifiziert</string>
|
||||
<string name="simplex_name_no_valid_link_desc">Der SimpleX-Name %1$s wurde registriert, aber er hat keinen gültigen Link.</string>
|
||||
<string name="simplex_name_unconfirmed_desc">Der SimpleX‑Name %1$s wurde registriert, jedoch nicht in Ihrem Profil hinterlegt. Bitte zu Ihrer Adresse oder zum Kanalprofil hinzufügen, sofern Sie der Besitzer sind.</string>
|
||||
<string name="simplex_name_unconfirmed_desc">Der SimpleX‑Name %1$s wurde registriert, jedoch nicht in Ihrem Profil hinterlegt. Bitte fügen Sie ihn zu Ihrer Adresse oder zum Kanalprofil hinzu, sofern Sie der Besitzer sind.</string>
|
||||
<string name="simplex_name_owner_no_channel_link">Der SimpleX‑Name %1$s wurde ohne Kanal‑Link registriert. Fügen Sie den Kanal‑Link über die Registrierungsseite hinzu.</string>
|
||||
<string name="simplex_name_owner_no_address">Der SimpleX‑Name %1$s wurde ohne SimpleX-Adresse registriert. Fügen Sie die SimpleX-Adresse über die Registrierungsseite hinzu.</string>
|
||||
<string name="simplex_name_not_found_desc">Dieser SimpleX-Name wurde nicht registriert. Bitte überprüfen Sie den Namen.</string>
|
||||
@@ -2984,7 +2991,7 @@
|
||||
<string name="sign_messages">Nachrichten signieren</string>
|
||||
<string name="signature_missing_alert_desc">Der Kanal verlangt für diese Nachricht eine Signatur, welche aber fehlt.</string>
|
||||
<string name="channel_simplex_name">Im Kanal genutzter SimpleX-Name</string>
|
||||
<string name="get_simplex_name_beta">SimpleX-Name erhalten (BETA)</string>
|
||||
<string name="get_simplex_name_beta">Einen SimpleX-Namen erhalten (BETA)</string>
|
||||
<string name="register_test_name">Wie man einen Test-Namen registriert</string>
|
||||
<string name="remove_name">Name entfernen</string>
|
||||
<string name="save_simplex_name_question">SimpleX-Name speichern?</string>
|
||||
|
||||
@@ -1350,7 +1350,7 @@
|
||||
<string name="the_sender_will_not_be_notified">Ο αποστολέας ΔΕΝ θα ειδοποιηθεί.</string>
|
||||
<string name="smp_servers_per_user">Οι διακομιστές για τις νέες συνδέσεις του τρέχοντος προφίλ συνομιλίας σου</string>
|
||||
<string name="xftp_servers_per_user">Οι διακομιστές για τα νέα αρχεία του τρέχοντος προφίλ συνομιλίας σου</string>
|
||||
<string name="receipts_section_description">Αυτές οι ρυθμίσεις ισχύουν για το τρέχον προφίλ σου</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Αυτές οι ρυθμίσεις ισχύουν για το τρέχον προφίλ σου</string>
|
||||
<string name="the_text_you_pasted_is_not_a_link">Το κείμενο που επικόλλησες δεν είναι σύνδεσμος SimpleX.</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Το αρχείο της βάσης δεδομένων που μεταφορτώθηκε, θα διαγραφεί οριστικά από τους διακομιστές.</string>
|
||||
<string name="video_decoding_exception_desc">Το βίντεο δεν μπορεί να αποκωδικοποιηθεί. Δοκίμασε ένα άλλο βίντεο ή επικοινώνησε με τους προγραμματιστές.</string>
|
||||
@@ -2051,7 +2051,7 @@
|
||||
<string name="saved_description">αποθηκευμένο</string>
|
||||
<string name="saved_chat_item_info_tab">Αποθηκευμένο</string>
|
||||
<string name="saved_from_chat_item_info_title">Αποθηκευμένο από</string>
|
||||
<string name="saved_from_description">αποθηκευμένο από %s</string>
|
||||
<string name="saved_from">αποθηκευμένο από</string>
|
||||
<string name="saved_message_title">Αποθηκευμένο μήνυμα</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">Οι αποθηκευμένοι διακομιστές WebRTC ICE θα αφαιρεθούν.</string>
|
||||
<string name="save_group_profile">Αποθήκευση προφίλ ομάδας</string>
|
||||
@@ -2429,6 +2429,11 @@
|
||||
<string name="not_connected_to_server_to_receive_messages_no_sub">Δεν είσαι συνδεδεμένος στον διακομιστή που χρησιμοποιείται για τη λήψη μηνυμάτων από αυτή τη σύνδεση (δεν υπάρχει συνδρομή).</string>
|
||||
<string name="servers_info_proxied_servers_section_footer">Δεν είσαι συνδεδεμένος σε αυτούς τους διακομιστές. Για την παράδοση μηνυμάτων σε αυτούς, χρησιμοποιείται ιδιωτική δρομολόγηση.</string>
|
||||
<string name="you_are_observer">είσαι παρατηρητής</string>
|
||||
<string name="connect_plan_you_are_observer">Είσαι παρατηρητής</string>
|
||||
<string name="connect_plan_you_are_member">Είσαι μέλος</string>
|
||||
<string name="connect_plan_you_are_moderator">Είσαι διαχειριστής</string>
|
||||
<string name="connect_plan_you_are_admin">Είσαι διαχειριστής</string>
|
||||
<string name="connect_plan_you_are_owner">Είσαι ιδιοκτήτης</string>
|
||||
<string name="observer_cant_send_message_title">είσαι παρατηρητής</string>
|
||||
<string name="snd_group_event_member_blocked">μπλόκαρες %s</string>
|
||||
<string name="one_hand_ui_change_instruction">Μπορείς να το αλλάξεις στις ρυθμίσεις Εμφάνισης.</string>
|
||||
|
||||
@@ -1283,7 +1283,7 @@
|
||||
<string name="receipts_contacts_override_disabled">El envío de confirmaciones está desactivado para %d contactos</string>
|
||||
<string name="receipts_contacts_override_enabled">El envío de confirmaciones está activado para %d contactos</string>
|
||||
<string name="send_receipts">Enviar confirmaciones</string>
|
||||
<string name="receipts_section_description">Esta configuración afecta a tu perfil actual</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Esta configuración afecta a tu perfil actual</string>
|
||||
<string name="enable_receipts_all">Activar</string>
|
||||
<string name="receipts_contacts_title_disable">¿Desactivar confirmaciones\?</string>
|
||||
<string name="receipts_contacts_title_enable">¿Activar confirmaciones\?</string>
|
||||
@@ -1724,7 +1724,7 @@
|
||||
<string name="feature_roles_all_members">todos los miembros</string>
|
||||
<string name="allow_to_send_simplex_links">Se permite enviar enlaces SimpleX.</string>
|
||||
<string name="saved_description">guardado</string>
|
||||
<string name="saved_from_description">guardado desde %s</string>
|
||||
<string name="saved_from">guardado desde</string>
|
||||
<string name="saved_chat_item_info_tab">Guardado</string>
|
||||
<string name="saved_from_chat_item_info_title">Guardado desde</string>
|
||||
<string name="forwarded_from_chat_item_info_title">Reenviado por</string>
|
||||
@@ -2665,6 +2665,13 @@
|
||||
<string name="relay_test_step_wait_response">Espera respuesta</string>
|
||||
<string name="channel_member_you">tú</string>
|
||||
<string name="you_are_subscriber">eres suscriptor</string>
|
||||
<string name="connect_plan_you_are_observer">Eres observador</string>
|
||||
<string name="connect_plan_you_are_member">Eres miembro</string>
|
||||
<string name="connect_plan_you_are_moderator">Eres moderador</string>
|
||||
<string name="connect_plan_you_are_admin">Eres administrador</string>
|
||||
<string name="connect_plan_you_are_owner">Eres propietario</string>
|
||||
<string name="connect_plan_you_are_subscriber">Eres suscriptor</string>
|
||||
<string name="connect_plan_you_are_contributor">Eres colaborador</string>
|
||||
<string name="you_can_share_channel_link_anybody_will_be_able_to_connect">Puedes compartir el enlace o código QR. Cualquiera podrá unirse al canal.</string>
|
||||
<string name="relay_section_footer_subscriber">Te conectaste al canal mediante este enlace de servidor.</string>
|
||||
<string name="chat_banner_your_channel">Tu canal</string>
|
||||
|
||||
@@ -156,7 +156,7 @@
|
||||
<string name="turn_off_battery_optimization"><![CDATA[در دیالوگ بعدی <b>اجازه دهید</b> تا اعلانها را فوری دریافت کنید.]]></string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[برای بهبود حریم خصوصی، <b>SimpleX در پسزمینه اجرا میشود</b> و به جای استفاده از پوش نوتیفیکیشن، کار میکند.]]></string>
|
||||
<string name="saved_description">ذخیره شده</string>
|
||||
<string name="saved_from_description">ذخیره شده از %s</string>
|
||||
<string name="saved_from">ذخیره شده از</string>
|
||||
<string name="saved_from_chat_item_info_title">ذخیره شده از</string>
|
||||
<string name="forwarded_description">فرستاده شده</string>
|
||||
<string name="e2ee_info_no_pq"><![CDATA[پیامها، فایلها و تماسها به وسیله <b>رمزنگاری انتها به انتها</b> با محرمانگی پیشرو، مردودسازی و بازیابی ورود غیرمجاز محافظت شدهاند.]]></string>
|
||||
@@ -437,6 +437,11 @@
|
||||
<string name="la_could_not_be_verified">تایید شما ممکن نیست؛ لطفا دوباره امتحان کنید.</string>
|
||||
<string name="choose_file">فایل</string>
|
||||
<string name="you_are_observer">شما ناظر هستید</string>
|
||||
<string name="connect_plan_you_are_observer">شما ناظر هستید</string>
|
||||
<string name="connect_plan_you_are_member">شما عضو هستید</string>
|
||||
<string name="connect_plan_you_are_moderator">شما مدیر هستید</string>
|
||||
<string name="connect_plan_you_are_admin">شما مدیر هستید</string>
|
||||
<string name="connect_plan_you_are_owner">شما صاحب هستید</string>
|
||||
<string name="clear_chat_question">چت پاک شود؟</string>
|
||||
<string name="clear_note_folder_warning">تمام پیامها حذف خواهند شد - این عمل قابل برگشت نیست!</string>
|
||||
<string name="delete_contact_menu_action">حذف</string>
|
||||
@@ -756,7 +761,7 @@
|
||||
<string name="self_destruct_new_display_name">نام نمایشی جدید:</string>
|
||||
<string name="if_you_enter_self_destruct_code">اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید:</string>
|
||||
<string name="all_app_data_will_be_cleared">تمام اطلاعات برنامه حذف میشود.</string>
|
||||
<string name="receipts_section_description">این تنظیمات برای پروفایل فعلی شما هستند</string>
|
||||
<string name="these_settings_are_for_your_current_profile">این تنظیمات برای پروفایل فعلی شما هستند</string>
|
||||
<string name="receipts_contacts_override_enabled">ارسال رسید برای %d مخاطب فعال است</string>
|
||||
<string name="receipts_contacts_disable_for_all">غیرفعال برای همه</string>
|
||||
<string name="receipts_groups_enable_for_all">فعال برای همه گروهها</string>
|
||||
|
||||
@@ -1169,6 +1169,10 @@
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Yksityisyytesi säilyttämiseksi sovelluksessa on push-ilmoitusten sijaan <b>SimpleX-taustapalvelu</b> – se kuluttaa muutaman prosentin akusta päivässä.]]></string>
|
||||
<string name="auth_unlock">Avaa</string>
|
||||
<string name="you_are_observer">olet tarkkailija</string>
|
||||
<string name="connect_plan_you_are_observer">Olet tarkkailija</string>
|
||||
<string name="connect_plan_you_are_member">Olet jäsen</string>
|
||||
<string name="connect_plan_you_are_admin">Olet ylläpitäjä</string>
|
||||
<string name="connect_plan_you_are_owner">Olet omistaja</string>
|
||||
<string name="videos_limit_title">Liikaa videoita!</string>
|
||||
<string name="voice_message">Ääniviesti</string>
|
||||
<string name="waiting_for_video">Odottaa videota</string>
|
||||
@@ -1272,7 +1276,7 @@
|
||||
<string name="sync_connection_force_confirm">Uudelleenneuvottele</string>
|
||||
<string name="sync_connection_force_question">Uudelleenneuvottele salaus\?</string>
|
||||
<string name="sync_connection_force_desc">Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</string>
|
||||
<string name="receipts_section_description">Nämä asetukset koskevat nykyistä profiiliasi</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Nämä asetukset koskevat nykyistä profiiliasi</string>
|
||||
<string name="receipts_section_description_1">Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa.</string>
|
||||
<string name="conn_event_ratchet_sync_ok">salaus ok</string>
|
||||
<string name="conn_event_ratchet_sync_allowed">salauksen uudelleenneuvottelu sallittu</string>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -283,6 +283,9 @@
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">सदस्य को समूह से निकाल दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
|
||||
<string name="member_info_section_title_member">सदस्य</string>
|
||||
<string name="group_member_role_member">सदस्य</string>
|
||||
<string name="connect_plan_you_are_member">आप सदस्य हैं</string>
|
||||
<string name="connect_plan_you_are_admin">आप व्यवस्थापक हैं</string>
|
||||
<string name="connect_plan_you_are_owner">आप स्वामी हैं</string>
|
||||
<string name="search_verb">खोजें</string>
|
||||
<string name="la_mode_off">बंद है</string>
|
||||
<string name="connect_via_contact_link">संपर्क पते के माध्यम से कनेक्ट करें?</string>
|
||||
|
||||
@@ -590,7 +590,7 @@
|
||||
<string name="incognito_info_protects">Anonimni režim štiti Vašu privatnost koristeći novi nasumični profil za svaki kontakt.</string>
|
||||
<string name="custom_time_unit_weeks">nedelje</string>
|
||||
<string name="agent_internal_error_title">Interna greška</string>
|
||||
<string name="saved_from_description">Sačuvano od %s</string>
|
||||
<string name="saved_from">Sačuvano od</string>
|
||||
<string name="saved_description">sačuvano</string>
|
||||
<string name="group_member_status_invited">pozvan</string>
|
||||
<string name="saved_message_title">Sačuvana poruka</string>
|
||||
@@ -1435,6 +1435,11 @@
|
||||
<string name="unable_to_open_browser_desc">Za pozive je potreban podrazumevani veb pretraživač. Molimo vas da konfigurišete podrazumevani pretraživač u sistemu i podelite više informacija sa programerima.</string>
|
||||
<string name="snd_group_event_member_unblocked">odblokirali ste %s</string>
|
||||
<string name="you_are_observer">Vi ste posmatrač.</string>
|
||||
<string name="connect_plan_you_are_observer">Vi ste posmatrač</string>
|
||||
<string name="connect_plan_you_are_member">Vi ste član</string>
|
||||
<string name="connect_plan_you_are_moderator">Vi ste moderator</string>
|
||||
<string name="connect_plan_you_are_admin">Vi ste administrator</string>
|
||||
<string name="connect_plan_you_are_owner">Vi ste vlasnik</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Unapređena privatnost i bezbednost</string>
|
||||
<string name="v5_6_app_data_migration_descr">Migriraj na drugi uređaj pomoću QR koda.</string>
|
||||
<string name="group_preview_rejected">odbijeno</string>
|
||||
|
||||
@@ -1127,6 +1127,13 @@
|
||||
<string name="simplex_service_notification_text">Üzenetek fogadása…</string>
|
||||
<string name="rcv_group_event_2_members_connected">%s és %s kapcsolódott</string>
|
||||
<string name="you_are_observer">Ön megfigyelő</string>
|
||||
<string name="connect_plan_you_are_observer">Ön megfigyelő</string>
|
||||
<string name="connect_plan_you_are_member">Ön tag</string>
|
||||
<string name="connect_plan_you_are_moderator">Ön moderátor</string>
|
||||
<string name="connect_plan_you_are_admin">Ön adminisztrátor</string>
|
||||
<string name="connect_plan_you_are_owner">Ön tulajdonos</string>
|
||||
<string name="connect_plan_you_are_subscriber">Ön feliratkozó</string>
|
||||
<string name="connect_plan_you_are_contributor">Ön közreműködő</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="set_passcode">Jelkód beállítása</string>
|
||||
<string name="whats_new">Újdonságok</string>
|
||||
@@ -1475,7 +1482,7 @@
|
||||
<string name="settings_is_storing_in_clear_text">A jelmondat a beállításokban egyszerű szövegként van tárolva.</string>
|
||||
<string name="terminal_always_visible">Konzol megjelenítése új ablakban</string>
|
||||
<string name="alert_text_msg_bad_hash">Az előző üzenet kivonata különbözik.</string>
|
||||
<string name="receipts_section_description">Ezek a beállítások csak a jelenlegi csevegési profiljára vonatkoznak</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Ezek a beállítások csak a jelenlegi csevegési profiljára vonatkoznak</string>
|
||||
<string name="loading_remote_file_desc">Várjon, amíg a fájl betöltődik a társított hordozható eszközről</string>
|
||||
<string name="read_more_in_github_with_link"><![CDATA[További információkat a <font color="#0088ff">GitHub-tárolónkban</font> talál.]]></string>
|
||||
<string name="error_showing_content">Hiba történt a tartalom megjelenítésekor</string>
|
||||
@@ -1694,7 +1701,7 @@
|
||||
<string name="allow_to_send_simplex_links">A SimpleX-hivatkozások küldése engedélyezve van.</string>
|
||||
<string name="feature_enabled_for">Számukra engedélyezve</string>
|
||||
<string name="saved_description">mentett</string>
|
||||
<string name="saved_from_description">mentve innen: %s</string>
|
||||
<string name="saved_from">mentve innen:</string>
|
||||
<string name="forwarded_from_chat_item_info_title">Továbbítva innen</string>
|
||||
<string name="recipients_can_not_see_who_message_from">A címzett(ek) nem látja(k), hogy kitől származik ez az üzenet.</string>
|
||||
<string name="saved_chat_item_info_tab">Mentett</string>
|
||||
@@ -2881,7 +2888,7 @@
|
||||
<string name="signature_missing_alert_desc">A csatorna megköveteli az üzenet aláírását, de az hiányzik.</string>
|
||||
<string name="channel_simplex_name">Csatorna SimpleX-neve</string>
|
||||
<string name="get_simplex_name_beta">SimpleX-név beszerzése (béta)</string>
|
||||
<string name="register_test_name">Egy név regisztrálása tesztelési céllal</string>
|
||||
<string name="register_test_name">Útmutató egy név regisztrálásához tesztelési céllal</string>
|
||||
<string name="remove_name">Név eltávolítása</string>
|
||||
<string name="save_simplex_name_question">Menti a SimpleX-nevet?</string>
|
||||
<string name="to_verify_channel_member_key">A kulcsok ellenőrzéséhez ezzel a feliratkozóval hasonlítsa össze (vagy olvassa be) az eszközökön található kódot.</string>
|
||||
|
||||
@@ -755,7 +755,7 @@
|
||||
<string name="moderated_description">dimoderasi</string>
|
||||
<string name="invalid_chat">obrolan tidak valid</string>
|
||||
<string name="forwarded_description">diteruskan</string>
|
||||
<string name="saved_from_description">disimpan dari %s</string>
|
||||
<string name="saved_from">disimpan dari</string>
|
||||
<string name="receiving_files_not_yet_supported">terima berkas belum didukung</string>
|
||||
<string name="sender_you_pronoun">anda</string>
|
||||
<string name="unknown_message_format">format pesan tak diketahui</string>
|
||||
@@ -1206,7 +1206,7 @@
|
||||
<string name="receipts_groups_title_enable">Aktifkan tanda terima untuk grup?</string>
|
||||
<string name="empty_chat_profile_is_created">Profil obrolan kosong dengan nama yang disediakan dibuat, dan aplikasi terbuka seperti biasa.</string>
|
||||
<string name="if_you_enter_passcode_data_removed">Jika Anda memasukkan kode sandi saat membuka aplikasi, semua data aplikasi akan dihapus secara permanen!</string>
|
||||
<string name="receipts_section_description">Pengaturan ini untuk profil Anda saat ini</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Pengaturan ini untuk profil Anda saat ini</string>
|
||||
<string name="receipts_contacts_override_enabled">Kirim tanda terima diaktifkan untuk %d kontak</string>
|
||||
<string name="receipts_contacts_override_disabled">Kirim tanda terima dimatikan untuk %d kontak</string>
|
||||
<string name="error_loading_xftp_servers">Gagal memuat server XFTP</string>
|
||||
@@ -2010,6 +2010,13 @@
|
||||
<string name="file_error_auth">Kunci salah atau alamat potongan berkas tidak dikenal - kemungkinan berkas dihapus.</string>
|
||||
<string name="srv_error_version">Versi server tidak kompatibel dengan pengaturan jaringan.</string>
|
||||
<string name="you_are_observer">Anda adalah pengamat</string>
|
||||
<string name="connect_plan_you_are_observer">Anda adalah pengamat</string>
|
||||
<string name="connect_plan_you_are_member">Anda adalah anggota</string>
|
||||
<string name="connect_plan_you_are_moderator">Anda adalah moderator</string>
|
||||
<string name="connect_plan_you_are_admin">Anda adalah admin</string>
|
||||
<string name="connect_plan_you_are_owner">Anda adalah pemilik</string>
|
||||
<string name="connect_plan_you_are_subscriber">Anda adalah pelanggan</string>
|
||||
<string name="connect_plan_you_are_contributor">Anda adalah kontributor</string>
|
||||
<string name="to_start_a_new_chat_help_header">Untuk memulai obrolan baru</string>
|
||||
<string name="gallery_video_button">Video</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">Koneksi yang Anda terima akan dibatalkan!</string>
|
||||
|
||||
@@ -934,6 +934,13 @@
|
||||
<string name="moderate_message_will_be_deleted_warning">Il messaggio verrà eliminato per tutti i membri.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Il messaggio sarà segnato come moderato per tutti i membri.</string>
|
||||
<string name="you_are_observer">sei un osservatore</string>
|
||||
<string name="connect_plan_you_are_observer">Sei un osservatore</string>
|
||||
<string name="connect_plan_you_are_member">Sei un membro</string>
|
||||
<string name="connect_plan_you_are_moderator">Sei un moderatore</string>
|
||||
<string name="connect_plan_you_are_admin">Sei un amministratore</string>
|
||||
<string name="connect_plan_you_are_owner">Sei un proprietario</string>
|
||||
<string name="connect_plan_you_are_subscriber">Sei iscritto/a</string>
|
||||
<string name="connect_plan_you_are_contributor">Sei un collaboratore</string>
|
||||
<string name="initial_member_role">Ruolo iniziale</string>
|
||||
<string name="error_updating_link_for_group">Errore nell\'aggiornamento del link del gruppo</string>
|
||||
<string name="group_member_role_observer">osservatore</string>
|
||||
@@ -1290,7 +1297,7 @@
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">L\'invio delle ricevute di consegna sarà attivo per tutti i contatti in tutti i profili di chat visibili.</string>
|
||||
<string name="receipts_contacts_override_disabled">L\'invio di ricevute è disattivato per %d contatti</string>
|
||||
<string name="sync_connection_force_desc">La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione!</string>
|
||||
<string name="receipts_section_description">Queste impostazioni sono per il tuo profilo attuale</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Queste impostazioni sono per il tuo profilo attuale</string>
|
||||
<string name="receipts_section_description_1">Possono essere sovrascritte nelle impostazioni dei contatti e dei gruppi.</string>
|
||||
<string name="receipts_contacts_enable_for_all">Attiva per tutti</string>
|
||||
<string name="receipts_contacts_enable_keep_overrides">Attiva (mantieni sostituzioni)</string>
|
||||
@@ -1732,7 +1739,7 @@
|
||||
<string name="forward_chat_item">Inoltra</string>
|
||||
<string name="recipients_can_not_see_who_message_from">I destinatari non possono vedere da chi proviene questo messaggio.</string>
|
||||
<string name="saved_chat_item_info_tab">Salvato</string>
|
||||
<string name="saved_from_description">salvato da %s</string>
|
||||
<string name="saved_from">salvato da</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="audio_device_earpiece">Auricolari</string>
|
||||
<string name="audio_device_wired_headphones">Cuffie</string>
|
||||
@@ -2916,7 +2923,7 @@
|
||||
<string name="signature_missing_alert_desc">Il canale ha richiesto di firmare questo messaggio, ma la firma non è presente.</string>
|
||||
<string name="channel_simplex_name">Nome SimpleX per il canale</string>
|
||||
<string name="get_simplex_name_beta">Ottieni nome SimpleX (BETA)</string>
|
||||
<string name="register_test_name">Registra un nome di prova</string>
|
||||
<string name="register_test_name">Come registrare un nome di prova</string>
|
||||
<string name="remove_name">Rimuovi nome</string>
|
||||
<string name="save_simplex_name_question">Salvare il nome SimpleX?</string>
|
||||
<string name="to_verify_channel_member_key">Per verificare le chiavi con questo iscritto, confrontate (o scansionate) il codice sui vostri dispositivi.</string>
|
||||
|
||||
@@ -1128,6 +1128,10 @@
|
||||
<string name="integrity_msg_skipped">%1$d הודעות שדולגו</string>
|
||||
<string name="custom_time_unit_weeks">שבועות</string>
|
||||
<string name="you_are_observer">הינך צופה</string>
|
||||
<string name="connect_plan_you_are_observer">הינך צופה</string>
|
||||
<string name="connect_plan_you_are_member">הינך חבר קבוצה</string>
|
||||
<string name="connect_plan_you_are_admin">הינך מנהל</string>
|
||||
<string name="connect_plan_you_are_owner">הינך בעלים</string>
|
||||
<string name="observer_cant_send_message_title">אין באפשרותך לשלוח הודעות!</string>
|
||||
<string name="icon_descr_video_snd_complete">סרטון נשלח</string>
|
||||
<string name="voice_message">הודעה קולית</string>
|
||||
@@ -1291,7 +1295,7 @@
|
||||
\n- קבוצות קצת יותר טובות.
|
||||
\n- ועוד!</string>
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">שליחת קבלות שליחה תתאפשר עבור כל אנשי הקשר בכל פרופילי הצ\'אט הגלויים.</string>
|
||||
<string name="receipts_section_description">הגדרות אלו מיועדות לפרופיל הנוכחי שלך</string>
|
||||
<string name="these_settings_are_for_your_current_profile">הגדרות אלו מיועדות לפרופיל הנוכחי שלך</string>
|
||||
<string name="receipts_section_description_1">ניתן לעקוף אותם בהגדרות אנשי קשר וקבוצות.</string>
|
||||
<string name="receipts_contacts_override_disabled">שליחת קבלות מושבתת עבור %d אנשי קשר</string>
|
||||
<string name="receipts_contacts_override_enabled">שליחת קבלות מאופשרת עבור %d אנשי קשר</string>
|
||||
@@ -1767,7 +1771,7 @@
|
||||
<string name="network_type_ethernet">חיבור קווי</string>
|
||||
<string name="network_type_cellular">סלולרי</string>
|
||||
<string name="saved_description">נשמר</string>
|
||||
<string name="saved_from_description">נשמר מ%s</string>
|
||||
<string name="saved_from">נשמר מ</string>
|
||||
<string name="forwarded_chat_item_info_tab">הועבר</string>
|
||||
<string name="forwarded_description">הועבר</string>
|
||||
<string name="settings_section_title_network_connection">מחובר לרשת</string>
|
||||
|
||||
@@ -990,6 +990,12 @@
|
||||
<string name="video_descr">ビデオ</string>
|
||||
<string name="alert_title_msg_bad_hash">メッセージのハッシュ値問題</string>
|
||||
<string name="you_are_observer">あなたはオブザーバーです</string>
|
||||
<string name="connect_plan_you_are_observer">あなたはオブザーバーです</string>
|
||||
<string name="connect_plan_you_are_member">あなたはメンバーです</string>
|
||||
<string name="connect_plan_you_are_moderator">あなたはモデレーターです</string>
|
||||
<string name="connect_plan_you_are_admin">あなたは管理者です</string>
|
||||
<string name="connect_plan_you_are_owner">あなたはオーナーです</string>
|
||||
<string name="connect_plan_you_are_subscriber">あなたは購読者です</string>
|
||||
<string name="observer_cant_send_message_desc">グループの管理者に連絡してください。</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">動画は相手がアップロードを完了した時点で受信するができます。</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[SOCKSプロキシがサポートしていない場合、<i>.onion hostを使用する</i>、は「いいえ」に設定します。]]></string>
|
||||
@@ -1278,7 +1284,7 @@
|
||||
<string name="fix_connection_not_supported_by_group_member">グループメンバーによる修正はサポートされていません</string>
|
||||
<string name="receipts_section_contacts">連絡先</string>
|
||||
<string name="receipts_section_description_1">これらは連絡先とグループの設定が優先されます。</string>
|
||||
<string name="receipts_section_description">これらの設定は現在のプロファイル用です</string>
|
||||
<string name="these_settings_are_for_your_current_profile">これらの設定は現在のプロファイル用です</string>
|
||||
<string name="receipts_contacts_title_enable">配信通知を有効?</string>
|
||||
<string name="sender_at_ts">%s : %s</string>
|
||||
<string name="fix_connection">接続を修正</string>
|
||||
@@ -1733,7 +1739,7 @@
|
||||
<string name="v5_7_network_descr">より信頼性の高いネットワーク接続</string>
|
||||
<string name="v5_7_network">ネットワーク管理</string>
|
||||
<string name="saved_description">保存済</string>
|
||||
<string name="saved_from_description">%sから保存</string>
|
||||
<string name="saved_from">から保存</string>
|
||||
<string name="forwarded_chat_item_info_tab">転送済</string>
|
||||
<string name="forwarded_from_chat_item_info_title">転送元</string>
|
||||
<string name="saved_from_chat_item_info_title">保存元</string>
|
||||
|
||||
@@ -468,6 +468,10 @@
|
||||
<string name="group_member_status_left">나감</string>
|
||||
<string name="group_member_role_member">멤버</string>
|
||||
<string name="group_member_role_owner">소유자</string>
|
||||
<string name="connect_plan_you_are_observer">당신은 관찰자입니다</string>
|
||||
<string name="connect_plan_you_are_member">당신은 멤버입니다</string>
|
||||
<string name="connect_plan_you_are_admin">당신은 관리자입니다</string>
|
||||
<string name="connect_plan_you_are_owner">당신은 소유자입니다</string>
|
||||
<string name="group_member_status_group_deleted">그룹 삭제됨</string>
|
||||
<string name="group_member_status_invited">초대됨</string>
|
||||
<string name="group_member_status_removed">강퇴됨</string>
|
||||
|
||||
@@ -1198,7 +1198,7 @@
|
||||
<string name="connect_plan_this_is_your_link_for_group_vName"><![CDATA[Tai yra jūsų nuoroda grupei <b>%1$s</b>!]]></string>
|
||||
<string name="to_connect_via_link_title">Kad prisijungti su nuoroda</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Ši nuoroda nėra tinkama prisijungimo nuoroda!</string>
|
||||
<string name="receipts_section_description">Šie nustatymai yra jūsų dabartiniam profiliui</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Šie nustatymai yra jūsų dabartiniam profiliui</string>
|
||||
<string name="settings_is_storing_in_clear_text">Slaptafrazė saugoma nustatymuose kaip paprastas tekstas.</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Bakstelėkite, kad prisijungti kaip inkognito</string>
|
||||
<string name="send_receipts_disabled_alert_msg">Ši grupė turi daugiau nei %1$d narių, pristatymo kvitai nėra siunčiami.</string>
|
||||
@@ -1547,6 +1547,10 @@
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Jums reikės autentifikuotis kai paleidžiate programėlę arba pratęsiate jos naudojimą po 30 sekundžių fone.</string>
|
||||
<string name="no_history">Nėra istorijos</string>
|
||||
<string name="you_are_observer">esate stebėtojas</string>
|
||||
<string name="connect_plan_you_are_observer">Esate stebėtojas</string>
|
||||
<string name="connect_plan_you_are_member">Esate narys</string>
|
||||
<string name="connect_plan_you_are_admin">Esate administratorius</string>
|
||||
<string name="connect_plan_you_are_owner">Esate savininkas</string>
|
||||
<string name="only_stored_on_members_devices">(saugo tik grupės nariai)</string>
|
||||
<string name="your_simplex_contact_address">Jūsų SimpleX adresas</string>
|
||||
<string name="smp_servers_scan_qr">Nuskanuoti serverio QR kodą</string>
|
||||
@@ -1729,7 +1733,7 @@
|
||||
<string name="audio_device_speaker">Garsiakalbis</string>
|
||||
<string name="v5_7_network">Tinklo valdymas</string>
|
||||
<string name="saved_description">išsaugota</string>
|
||||
<string name="saved_from_description">išsaugota iš %s</string>
|
||||
<string name="saved_from">išsaugota iš</string>
|
||||
<string name="saved_chat_item_info_tab">Išsaugota</string>
|
||||
<string name="voice_messages_not_allowed">Balso žinutės neleidžiamos</string>
|
||||
<string name="network_type_network_wifi">WiFi</string>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<string name="sender_you_pronoun">Jūs</string>
|
||||
<string name="forwarded_description">pārsūtīts</string>
|
||||
<string name="saved_description">saglabāts</string>
|
||||
<string name="saved_from_description">saglabāts no %s</string>
|
||||
<string name="saved_from">saglabāts no</string>
|
||||
<string name="invalid_chat">nederīga tērzēšana</string>
|
||||
<string name="invalid_data">nederīgi dati</string>
|
||||
<string name="error_showing_message">kļūda, rādot ziņojumu</string>
|
||||
@@ -283,6 +283,7 @@
|
||||
<string name="cant_send_message_you_left">Nevar nosūtīt ziņu, jūs esat izgājis</string>
|
||||
<string name="cant_send_message_generic">Nevar nosūtīt ziņu</string>
|
||||
<string name="you_are_observer">Jūs esat vērotājs</string>
|
||||
<string name="connect_plan_you_are_observer">Jūs esat vērotājs</string>
|
||||
<string name="reviewed_by_admins">Pārbaudīts ar administratoriem</string>
|
||||
<string name="cant_send_message_member_has_old_version">Nevar nosūtīt ziņu, dalībniekam ir veca versija</string>
|
||||
<string name="cant_send_commands_alert_text">Nevar Nosūtīt Komandas Brīdinājuma Teksts</string>
|
||||
@@ -1607,7 +1608,7 @@
|
||||
<string name="onboarding_network_operators_continue">Ievada tīkla operatori turpināt</string>
|
||||
<string name="incoming_video_call">Ienākošais video zvans</string>
|
||||
<string name="incoming_audio_call">Ienākošais audio zvans</string>
|
||||
<string name="receipts_section_description">Čeki</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Čeki</string>
|
||||
<string name="receipts_section_description_1">Čeku apraksts 1</string>
|
||||
<string name="receipts_section_contacts">Čeku kontakti</string>
|
||||
<string name="receipts_contacts_title_enable">Čeku kontakti iespējot</string>
|
||||
|
||||
@@ -295,6 +295,9 @@
|
||||
<string name="welcome">സ്വാഗതം!</string>
|
||||
<string name="this_text_is_available_in_settings">ഈ വാചകം ക്രമീകരണങ്ങളിൽ ലഭ്യമാണ്</string>
|
||||
<string name="you_are_observer">നിങ്ങൾ നിരീക്ഷകനാണ്</string>
|
||||
<string name="connect_plan_you_are_observer">നിങ്ങൾ നിരീക്ഷകനാണ്</string>
|
||||
<string name="connect_plan_you_are_member">നിങ്ങൾ അംഗമാണ്</string>
|
||||
<string name="connect_plan_you_are_owner">നിങ്ങൾ ഉടമയാണ്</string>
|
||||
<string name="icon_descr_server_status_pending">തീർപ്പാക്കാത്തത്</string>
|
||||
<string name="icon_descr_send_message">സന്ദേശം അയയ്ക്കുക</string>
|
||||
<string name="send_live_message">തത്സമയ സന്ദേശം അയയ്ക്കുക</string>
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<string name="button_add_welcome_message">Legg til velkomstmelding</string>
|
||||
<string name="add_your_team_members_to_conversations">Legg til dine teammedlemmer i samtalene.</string>
|
||||
<string name="group_member_role_admin">administrator</string>
|
||||
<string name="connect_plan_you_are_admin">Du er administrator</string>
|
||||
<string name="feature_roles_admins">administratorer</string>
|
||||
<string name="v5_6_safer_groups_descr">Administratorer kan blokkere ett medlem for alle.</string>
|
||||
<string name="v4_2_group_links_desc">Administratorer kan lage lenker for å bli med i grupper.</string>
|
||||
|
||||
@@ -936,6 +936,11 @@
|
||||
<string name="group_member_role_observer">Waarnemer</string>
|
||||
<string name="observer_cant_send_message_title">jij bent waarnemer</string>
|
||||
<string name="you_are_observer">je bent waarnemer</string>
|
||||
<string name="connect_plan_you_are_observer">Je bent waarnemer</string>
|
||||
<string name="connect_plan_you_are_member">Je bent lid</string>
|
||||
<string name="connect_plan_you_are_moderator">Je bent moderator</string>
|
||||
<string name="connect_plan_you_are_admin">Je bent beheerder</string>
|
||||
<string name="connect_plan_you_are_owner">Je bent eigenaar</string>
|
||||
<string name="language_system">Systeem</string>
|
||||
<string name="v4_6_audio_video_calls">Audio en video oproepen</string>
|
||||
<string name="confirm_password">Bevestig wachtwoord</string>
|
||||
@@ -1278,7 +1283,7 @@
|
||||
<string name="receipts_contacts_title_disable">Ontvangst bevestiging uitschakelen\?</string>
|
||||
<string name="receipts_contacts_title_enable">Ontvangst bevestiging inschakelen\?</string>
|
||||
<string name="receipts_contacts_override_enabled">Het verzenden van ontvangst bevestiging is ingeschakeld voor %d-contactpersonen</string>
|
||||
<string name="receipts_section_description">Deze instellingen gelden voor uw huidige profiel</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Deze instellingen gelden voor uw huidige profiel</string>
|
||||
<string name="receipts_contacts_disable_keep_overrides">Uitschakelen (overschrijvingen behouden)</string>
|
||||
<string name="receipts_contacts_enable_for_all">Inschakelen voor iedereen</string>
|
||||
<string name="receipts_contacts_enable_keep_overrides">Inschakelen (overschrijvingen behouden)</string>
|
||||
@@ -1722,7 +1727,7 @@
|
||||
<string name="allow_to_send_simplex_links">Sta het verzenden van SimpleX-links toe.</string>
|
||||
<string name="group_members_can_send_simplex_links">Leden kunnen SimpleX-links verzenden.</string>
|
||||
<string name="saved_description">opgeslagen</string>
|
||||
<string name="saved_from_description">opgeslagen van %s</string>
|
||||
<string name="saved_from">opgeslagen van</string>
|
||||
<string name="forward_chat_item">Doorsturen</string>
|
||||
<string name="forwarded_chat_item_info_tab">Doorgestuurd</string>
|
||||
<string name="recipients_can_not_see_who_message_from">Ontvanger(s) kunnen niet zien van wie dit bericht afkomstig is.</string>
|
||||
|
||||
@@ -179,6 +179,11 @@
|
||||
<string name="icon_descr_waiting_for_video">Oczekiwanie na film</string>
|
||||
<string name="waiting_for_video">Oczekiwanie na film</string>
|
||||
<string name="you_are_observer">jesteś obserwatorem</string>
|
||||
<string name="connect_plan_you_are_observer">Jesteś obserwatorem</string>
|
||||
<string name="connect_plan_you_are_member">Jesteś członkiem</string>
|
||||
<string name="connect_plan_you_are_moderator">Jesteś moderatorem</string>
|
||||
<string name="connect_plan_you_are_admin">Jesteś administratorem</string>
|
||||
<string name="connect_plan_you_are_owner">Jesteś właścicielem</string>
|
||||
<string name="observer_cant_send_message_title">Jesteś obserwatorem</string>
|
||||
<string name="icon_descr_server_status_connected">Połączony</string>
|
||||
<string name="maximum_supported_file_size">Obecnie maksymalny obsługiwany rozmiar pliku to %1$s.</string>
|
||||
@@ -1287,7 +1292,7 @@
|
||||
<string name="sync_connection_force_question">Renegocjować szyfrowanie\?</string>
|
||||
<string name="receipts_contacts_override_enabled">Wysyłanie potwierdzeń jest włączone dla %d kontaktów</string>
|
||||
<string name="sync_connection_force_desc">Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu!</string>
|
||||
<string name="receipts_section_description">Te ustawienia dotyczą Twojego bieżącego profilu</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Te ustawienia dotyczą Twojego bieżącego profilu</string>
|
||||
<string name="receipts_section_description_1">Można je nadpisać w ustawieniach kontaktu i grupy.</string>
|
||||
<string name="conn_event_ratchet_sync_ok">szyfrowanie ok</string>
|
||||
<string name="conn_event_ratchet_sync_allowed">renegocjacja szyfrowania dozwolona</string>
|
||||
@@ -1731,7 +1736,7 @@
|
||||
<string name="forward_message">Przekaż wiadomość…</string>
|
||||
<string name="saved_chat_item_info_tab">Zapisane</string>
|
||||
<string name="saved_description">zapisane</string>
|
||||
<string name="saved_from_description">zapisane od %s</string>
|
||||
<string name="saved_from">zapisane od</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="v5_7_forward">Przesyłaj dalej i zapisuj wiadomości</string>
|
||||
<string name="audio_device_earpiece">Słuchawki douszne</string>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -88,6 +88,9 @@
|
||||
<string name="icon_descr_sent_msg_status_sent">enviada</string>
|
||||
<string name="group_preview_you_are_invited">você está convidado para o grupo</string>
|
||||
<string name="you_are_observer">você é observador</string>
|
||||
<string name="connect_plan_you_are_observer">Você é observador</string>
|
||||
<string name="connect_plan_you_are_member">Você é membro</string>
|
||||
<string name="connect_plan_you_are_admin">Você é administrador</string>
|
||||
<string name="notifications">Notificações</string>
|
||||
<string name="icon_descr_server_status_disconnected">Desconectado</string>
|
||||
<string name="text_field_set_contact_placeholder">Definir nome do contato…</string>
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
<string name="connect_plan_repeat_join_request">Repetă cererea de alăturare?</string>
|
||||
<string name="restart_chat_button">Reporniți conversația</string>
|
||||
<string name="saved_description">salvat</string>
|
||||
<string name="saved_from_description">salvat de la %s</string>
|
||||
<string name="saved_from">salvat de la</string>
|
||||
<string name="save_verb">Salvează</string>
|
||||
<string name="saved_chat_item_info_tab">Salvat</string>
|
||||
<string name="saved_from_chat_item_info_title">Salvat din</string>
|
||||
@@ -1999,7 +1999,7 @@
|
||||
<string name="call_desktop_permission_denied_title">Pentru a efectua apeluri, permiteți utilizarea microfonului. Încheiați apelul și încercați să sunați din nou.</string>
|
||||
<string name="onboarding_network_operators_cant_see_who_talks_to_whom">Când sunt activați mai mulți operatori, niciunul dintre ei nu are metadate pentru a afla cine comunică cu cine.</string>
|
||||
<string name="icon_descr_video_on">Video pornit</string>
|
||||
<string name="receipts_section_description">Aceste setări sunt pentru profilul tău actual</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Aceste setări sunt pentru profilul tău actual</string>
|
||||
<string name="privacy_chat_list_open_links_yes">Da</string>
|
||||
<string name="settings_message_shape_tail">Coada</string>
|
||||
<string name="settings_section_title_use_from_desktop">Utilizare de pe desktop</string>
|
||||
@@ -2195,6 +2195,11 @@
|
||||
<string name="migrate_from_device_stopping_chat">Se opresc conversațiile</string>
|
||||
<string name="servers_info_subscriptions_total">Total</string>
|
||||
<string name="you_are_observer">ești observator</string>
|
||||
<string name="connect_plan_you_are_observer">Ești observator</string>
|
||||
<string name="connect_plan_you_are_member">Ești membru</string>
|
||||
<string name="connect_plan_you_are_moderator">Ești moderator</string>
|
||||
<string name="connect_plan_you_are_admin">Ești administrator</string>
|
||||
<string name="connect_plan_you_are_owner">Ești proprietar</string>
|
||||
<string name="video_decoding_exception_desc">Videoclipul nu poate fi decodificat. Vă rugăm să încercați un alt videoclip sau să contactați dezvoltatorii.</string>
|
||||
<string name="maximum_message_size_reached_forwarding">Puteți copia și micșora dimensiunea mesajului pentru a-l trimite.</string>
|
||||
<string name="callstate_waiting_for_answer">aștept răspunsul…</string>
|
||||
|
||||
@@ -1329,7 +1329,7 @@
|
||||
<string name="receipts_contacts_override_enabled">Отправка отчётов о доставке включена для %d контактов</string>
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Отправка отчётов о доставке будет включена для всех контактов во всех видимых профилях чата.</string>
|
||||
<string name="this_setting_is_for_your_current_profile">Установка для Вашего активного профиля</string>
|
||||
<string name="receipts_section_description">Установки для Вашего активного профиля</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Установки для Вашего активного профиля</string>
|
||||
<string name="receipts_contacts_override_disabled">Отправка отчётов о доставке выключена для %d контактов</string>
|
||||
<string name="sync_connection_force_desc">Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!</string>
|
||||
<string name="v5_2_message_delivery_receipts_descr">Вторая галочка - знать, что доставлено! ✅</string>
|
||||
@@ -1799,7 +1799,7 @@
|
||||
<string name="v5_7_network_descr">Более надёжное соединение с сетью.</string>
|
||||
<string name="v5_7_network">Статус сети</string>
|
||||
<string name="saved_description">сохранено</string>
|
||||
<string name="saved_from_description">сохранено из %s</string>
|
||||
<string name="saved_from">сохранено из</string>
|
||||
<string name="forwarded_chat_item_info_tab">Переслано</string>
|
||||
<string name="forwarded_from_chat_item_info_title">Переслано из</string>
|
||||
<string name="recipients_can_not_see_who_message_from">Получатели не видят от кого это сообщение.</string>
|
||||
@@ -2771,6 +2771,13 @@
|
||||
<string name="chat_link_from_owner">(от владельца)</string>
|
||||
<string name="error_sharing_channel">Ошибка при публикации канала</string>
|
||||
<string name="you_are_subscriber">Вы подписчик</string>
|
||||
<string name="connect_plan_you_are_observer">Вы читатель</string>
|
||||
<string name="connect_plan_you_are_member">Вы член группы</string>
|
||||
<string name="connect_plan_you_are_moderator">Вы модератор</string>
|
||||
<string name="connect_plan_you_are_admin">Вы админ</string>
|
||||
<string name="connect_plan_you_are_owner">Вы владелец</string>
|
||||
<string name="connect_plan_you_are_subscriber">Вы подписчик</string>
|
||||
<string name="connect_plan_you_are_contributor">Вы соавтор</string>
|
||||
<string name="new_1_time_link">Новая одноразовая ссылка</string>
|
||||
<string name="onboarding_or_show_qr_code">Или покажите QR лично или через видеозвонок.</string>
|
||||
<string name="onboarding_post_address">Используйте этот адрес в профиле социальных сетей, на сайте или в подписи email.</string>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1078,6 +1078,10 @@
|
||||
<string name="you_have_no_chats">คุณไม่มีการแชท</string>
|
||||
<string name="your_chats">แชท</string>
|
||||
<string name="you_are_observer">คุณเป็นผู้สังเกตการณ์</string>
|
||||
<string name="connect_plan_you_are_observer">คุณเป็นผู้สังเกตการณ์</string>
|
||||
<string name="connect_plan_you_are_member">คุณเป็นสมาชิก</string>
|
||||
<string name="connect_plan_you_are_admin">คุณเป็นผู้ดูแลระบบ</string>
|
||||
<string name="connect_plan_you_are_owner">คุณเป็นเจ้าของ</string>
|
||||
<string name="image_decoding_exception_desc">ภาพไม่สามารถถอดรหัส ได้ โปรดลองใช้รูปภาพอื่นหรือติดต่อนักพัฒนา</string>
|
||||
<string name="observer_cant_send_message_title">คุณไม่สามารถส่งข้อความได้!</string>
|
||||
<string name="icon_descr_waiting_for_image">กําลังรอภาพ</string>
|
||||
@@ -1280,7 +1284,7 @@
|
||||
<string name="in_developing_title">เร็วๆ นี้!</string>
|
||||
<string name="snd_conn_event_ratchet_sync_allowed">อนุญาตให้มีการเจรจา encryption อีกครั้งสําหรับ %s</string>
|
||||
<string name="recipient_colon_delivery_status">%s: %s</string>
|
||||
<string name="receipts_section_description">การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ</string>
|
||||
<string name="these_settings_are_for_your_current_profile">การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ</string>
|
||||
<string name="receipts_section_description_1">สามารถลบล้างได้ในการตั้งค่าผู้ติดต่อและกลุ่ม</string>
|
||||
<string name="receipts_contacts_disable_keep_overrides">ปิดใช้งาน (เก็บการแทนที่)</string>
|
||||
<string name="receipts_contacts_enable_keep_overrides">เปิดใช้งาน (เก็บการแทนที่)</string>
|
||||
|
||||
@@ -842,6 +842,13 @@
|
||||
<string name="group_preview_you_are_invited">Gruba davetlisiniz</string>
|
||||
<string name="you_have_no_chats">Hiç sohbetiniz yok</string>
|
||||
<string name="you_are_observer">Gözlemcisiniz</string>
|
||||
<string name="connect_plan_you_are_observer">Gözlemcisiniz</string>
|
||||
<string name="connect_plan_you_are_member">Üyesiniz</string>
|
||||
<string name="connect_plan_you_are_moderator">Yöneticisiniz</string>
|
||||
<string name="connect_plan_you_are_admin">Yöneticisiniz</string>
|
||||
<string name="connect_plan_you_are_owner">Sahipsiniz</string>
|
||||
<string name="connect_plan_you_are_subscriber">Abonesiniz</string>
|
||||
<string name="connect_plan_you_are_contributor">Katkıda bulunansınız</string>
|
||||
<string name="observer_cant_send_message_title">sen gözlemcisin</string>
|
||||
<string name="view_security_code">Güvenlik kodunu görüntüle</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir.</string>
|
||||
@@ -1204,7 +1211,7 @@
|
||||
<string name="lock_not_enabled">SimpleX Kilit aktif değil!</string>
|
||||
<string name="chat_lock">SimpleX Kilit</string>
|
||||
<string name="connect_via_member_address_alert_title">Doğrudan bağlanılsın mı?</string>
|
||||
<string name="receipts_section_description">Bu ayarlar mevcut profiliniz içindir</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Bu ayarlar mevcut profiliniz içindir</string>
|
||||
<string name="smp_servers_test_failed">Sunucu testi başarısız!</string>
|
||||
<string name="verify_connection">Bağlantıyı onayla</string>
|
||||
<string name="add_contact_or_create_group">Yeni sohbet başlat</string>
|
||||
@@ -1720,7 +1727,7 @@
|
||||
<string name="v5_7_new_interface_languages">Litvanya Kullanıcı Arayüzü</string>
|
||||
<string name="network_type_other">Diğer</string>
|
||||
<string name="saved_description">kaydedildi</string>
|
||||
<string name="saved_from_description">%s tarafından kaydedildi</string>
|
||||
<string name="saved_from">kaydedildi:</string>
|
||||
<string name="forwarded_chat_item_info_tab">İletildi</string>
|
||||
<string name="saved_chat_item_info_tab">Kaydedildi</string>
|
||||
<string name="download_file">İndir</string>
|
||||
|
||||
@@ -1155,6 +1155,12 @@
|
||||
<string name="images_limit_title">Забагато зображень!</string>
|
||||
<string name="videos_limit_title">Забагато відео!</string>
|
||||
<string name="you_are_observer">ви спостерігач</string>
|
||||
<string name="connect_plan_you_are_observer">Ви спостерігач</string>
|
||||
<string name="connect_plan_you_are_member">Ви учасник</string>
|
||||
<string name="connect_plan_you_are_moderator">Ви модератор</string>
|
||||
<string name="connect_plan_you_are_admin">Ви адміністратор</string>
|
||||
<string name="connect_plan_you_are_owner">Ви власник</string>
|
||||
<string name="connect_plan_you_are_contributor">Ви автор</string>
|
||||
<string name="colored_text">кольоровий</string>
|
||||
<string name="callstatus_ended">дзвінок завершено %1$s</string>
|
||||
<string name="callstatus_error">помилка дзвінка</string>
|
||||
@@ -1271,7 +1277,7 @@
|
||||
<string name="abort_switch_receiving_address_confirm">Скасувати</string>
|
||||
<string name="choose_file_title">Виберіть файл</string>
|
||||
<string name="receipts_section_contacts">Контакти</string>
|
||||
<string name="receipts_section_description">Ці налаштування стосуються вашого поточного профілю</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Ці налаштування стосуються вашого поточного профілю</string>
|
||||
<string name="receipts_contacts_title_disable">Вимкнути повідомлення про доставку?</string>
|
||||
<string name="receipts_contacts_title_enable">Увімкнути повідомлення про доставку?</string>
|
||||
<string name="conn_event_ratchet_sync_allowed">можлива перезапис шифрування</string>
|
||||
@@ -1807,7 +1813,7 @@
|
||||
<string name="v5_7_shape_profile_images_descr">Квадрат, коло або щось середнє між ними.</string>
|
||||
<string name="v5_7_quantum_resistant_encryption_descr">Буде ввімкнено в прямих чатах!</string>
|
||||
<string name="saved_description">збережено</string>
|
||||
<string name="saved_from_description">збережено з %s</string>
|
||||
<string name="saved_from">збережено з</string>
|
||||
<string name="network_type_ethernet">Дротова мережа Ethernet</string>
|
||||
<string name="file_not_approved_title">Невідомі сервери!</string>
|
||||
<string name="file_not_approved_descr">Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів:
|
||||
|
||||
@@ -1594,7 +1594,7 @@
|
||||
<string name="cannot_share_message_alert_text">Các tùy chọn của cuộc trò chuyện được chọn không cho phép tin nhắn này.</string>
|
||||
<string name="scan_QR_code">Quét mã QR</string>
|
||||
<string name="saved_description">đã lưu</string>
|
||||
<string name="saved_from_description">đã lưu từ %s</string>
|
||||
<string name="saved_from">đã lưu từ</string>
|
||||
<string name="saved_from_chat_item_info_title">Đã lưu từ</string>
|
||||
<string name="scan_qr_code_from_desktop">Quét mã QR từ máy tính</string>
|
||||
<string name="secured">Đã được bảo mật</string>
|
||||
@@ -1972,7 +1972,7 @@
|
||||
<string name="the_text_you_pasted_is_not_a_link">Văn bản bạn vừa dán không phải là một đường dẫn SimpleX.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Chức vụ sẽ được đổi thành %s. Thành viên sẽ nhận được một lời mời mới.</string>
|
||||
<string name="passphrase_will_be_saved_in_settings">Mật khẩu sẽ được lưu trữ trong cài đặt dưới dạng thuần văn bản sau khi bản đổi nó hoặc khởi động lại ứng dụng.</string>
|
||||
<string name="receipts_section_description">Các cài đặt này là cho hồ sơ trò chuyện hiện tại của bạn</string>
|
||||
<string name="these_settings_are_for_your_current_profile">Các cài đặt này là cho hồ sơ trò chuyện hiện tại của bạn</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Chức vụ sẽ được đổi thành %s. Tất cả mọi người trong nhóm sẽ được thông báo.</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">Bản lưu trữ cơ sở dữ liệu đã được tải lên sẽ bị xóa vĩnh viễn khỏi các máy chủ.</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Việc này không thể được hoàn tác - hồ sơ, các liên hệ, tin nhắn và tệp của bạn sẽ biến mất mà không thể khôi phục.</string>
|
||||
@@ -2213,6 +2213,12 @@
|
||||
<string name="onboarding_network_operators_configure_via_settings">Bạn có thể tùy chỉnh các máy chủ thông qua cài đặt.</string>
|
||||
<string name="you_can_set_connection_name_to_remember">Bạn có thể đặt tên kết nối, để nhớ xem đường dẫn đã được chia sẻ với ai.</string>
|
||||
<string name="you_are_observer">bạn là quan sát viên</string>
|
||||
<string name="connect_plan_you_are_observer">Bạn là quan sát viên</string>
|
||||
<string name="connect_plan_you_are_member">Bạn là thành viên</string>
|
||||
<string name="connect_plan_you_are_moderator">Bạn là kiểm duyệt viên</string>
|
||||
<string name="connect_plan_you_are_admin">Bạn là quản trị viên</string>
|
||||
<string name="connect_plan_you_are_owner">Bạn là chủ sở hữu</string>
|
||||
<string name="connect_plan_you_are_subscriber">Bạn là người theo dõi</string>
|
||||
<string name="maximum_message_size_reached_forwarding">Bạn có thể sao chép và giảm kích thước tin nhắn để gửi nó đi.</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">Bạn có thể bật chúng vào lúc sau thông qua cài đặt Quyền riêng tư & Bảo mật của ứng dụng.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Bạn có thể ẩn hoặc tắt thông báo một hồ sơ người dùng - giữ nó trong phần menu.</string>
|
||||
|
||||
@@ -927,6 +927,13 @@
|
||||
<string name="delete_member_message__question">删除成员消息?</string>
|
||||
<string name="group_member_role_observer">观察员</string>
|
||||
<string name="you_are_observer">你是观察者</string>
|
||||
<string name="connect_plan_you_are_observer">你是观察员</string>
|
||||
<string name="connect_plan_you_are_member">你是成员</string>
|
||||
<string name="connect_plan_you_are_moderator">你是协管</string>
|
||||
<string name="connect_plan_you_are_admin">你是管理员</string>
|
||||
<string name="connect_plan_you_are_owner">你是群主</string>
|
||||
<string name="connect_plan_you_are_subscriber">你是订阅者</string>
|
||||
<string name="connect_plan_you_are_contributor">你是贡献者</string>
|
||||
<string name="error_updating_link_for_group">更新群链接错误</string>
|
||||
<string name="observer_cant_send_message_title">你是观察员</string>
|
||||
<string name="initial_member_role">初始角色</string>
|
||||
@@ -1344,7 +1351,7 @@
|
||||
<string name="connect_via_member_address_alert_desc">连接请求将发送给该群成员。</string>
|
||||
<string name="settings_is_storing_in_clear_text">密码以明文形式存储在设置中。</string>
|
||||
<string name="error_synchronizing_connection">同步连接时出错</string>
|
||||
<string name="receipts_section_description">这些设置适用于你当前的个人资料</string>
|
||||
<string name="these_settings_are_for_your_current_profile">这些设置适用于你当前的个人资料</string>
|
||||
<string name="snd_conn_event_ratchet_sync_allowed">允许为 %s 重新协商加密</string>
|
||||
<string name="receipts_contacts_enable_for_all">为所有人启用</string>
|
||||
<string name="conn_event_ratchet_sync_required">需要重新协商加密</string>
|
||||
@@ -1718,7 +1725,7 @@
|
||||
<string name="saved_description">已保存</string>
|
||||
<string name="saved_chat_item_info_tab">已保存</string>
|
||||
<string name="saved_from_chat_item_info_title">保存自</string>
|
||||
<string name="saved_from_description">保存自%s</string>
|
||||
<string name="saved_from">保存自</string>
|
||||
<string name="forwarded_chat_item_info_tab">已转发</string>
|
||||
<string name="forwarded_from_chat_item_info_title">转发自</string>
|
||||
<string name="audio_device_bluetooth">蓝牙</string>
|
||||
|
||||
@@ -926,6 +926,13 @@
|
||||
<string name="moderate_message_will_be_marked_warning">該訊息將對所有成員標記為已移除。</string>
|
||||
<string name="observer_cant_send_message_title">你是觀察員</string>
|
||||
<string name="you_are_observer">你是觀察員</string>
|
||||
<string name="connect_plan_you_are_observer">你是觀察員</string>
|
||||
<string name="connect_plan_you_are_member">你是成員</string>
|
||||
<string name="connect_plan_you_are_moderator">你是審核員</string>
|
||||
<string name="connect_plan_you_are_admin">你是管理員</string>
|
||||
<string name="connect_plan_you_are_owner">你是擁有者</string>
|
||||
<string name="connect_plan_you_are_subscriber">你是訂閱者</string>
|
||||
<string name="connect_plan_you_are_contributor">你是貢獻者</string>
|
||||
<string name="group_member_role_observer">觀察員</string>
|
||||
<string name="error_updating_link_for_group">更新群組連接時出錯</string>
|
||||
<string name="observer_cant_send_message_desc">請聯絡群組管理員。</string>
|
||||
@@ -1935,7 +1942,7 @@
|
||||
<string name="report_item_archived">已封存的報告</string>
|
||||
<string name="report_item_visibility_submitter">只有你和審核員能夠檢視</string>
|
||||
<string name="report_item_visibility_moderators">只有傳送者和審核員能夠檢視</string>
|
||||
<string name="saved_from_description">已儲存自 %s</string>
|
||||
<string name="saved_from">已儲存自</string>
|
||||
<string name="e2ee_info_no_pq_short">此聊天受到端對端加密保護。</string>
|
||||
<string name="report_reason_other">另一個原因</string>
|
||||
<string name="report_reason_profile">不當的個人檔案</string>
|
||||
@@ -2444,7 +2451,7 @@
|
||||
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">未使用 Tor 或 VPN 時,你的 IP 地址會對檔案伺服器可見。</string>
|
||||
<string name="sanitize_links_toggle">移除連結追蹤</string>
|
||||
<string name="this_setting_is_for_your_current_profile">此設定適用於你目前的個人檔案</string>
|
||||
<string name="receipts_section_description">這些設定適用於你目前的個人檔案</string>
|
||||
<string name="these_settings_are_for_your_current_profile">這些設定適用於你目前的個人檔案</string>
|
||||
<string name="receipts_section_description_1">可在聯絡人和群組設定中覆寫這些設定。</string>
|
||||
<string name="receipts_contacts_override_enabled">已為 %d 個聯絡人啟用送達回條</string>
|
||||
<string name="receipts_contacts_override_disabled">已為 %d 個聯絡人停用送達回條</string>
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect width="24" height="24" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 175 B |
+13
-3
@@ -23,6 +23,7 @@ import javax.swing.SwingUtilities
|
||||
internal class SkiaBitmapVideoSurface : VideoSurface(VideoSurfaceAdapters.getVideoSurfaceAdapter()) {
|
||||
|
||||
private val videoSurface = SkiaBitmapVideoSurface()
|
||||
@Volatile private var mediaPlayer: MediaPlayer? = null
|
||||
private lateinit var imageInfo: ImageInfo
|
||||
private lateinit var frameBytes: ByteArray
|
||||
private val skiaBitmap: Bitmap = Bitmap()
|
||||
@@ -31,6 +32,7 @@ internal class SkiaBitmapVideoSurface : VideoSurface(VideoSurfaceAdapters.getVid
|
||||
val bitmap: State<ImageBitmap?> = composeBitmap
|
||||
|
||||
override fun attach(mediaPlayer: MediaPlayer) {
|
||||
this.mediaPlayer = mediaPlayer
|
||||
videoSurface.attach(mediaPlayer)
|
||||
}
|
||||
|
||||
@@ -39,9 +41,17 @@ internal class SkiaBitmapVideoSurface : VideoSurface(VideoSurfaceAdapters.getVid
|
||||
private var sourceHeight: Int = 0
|
||||
|
||||
override fun getBufferFormat(sourceWidth: Int, sourceHeight: Int): BufferFormat {
|
||||
this.sourceWidth = sourceWidth
|
||||
this.sourceHeight = sourceHeight
|
||||
return RV32BufferFormat(sourceWidth, sourceHeight)
|
||||
// libvlc passes the size the decoder padded the picture to, not the size of the picture (dav1d
|
||||
// pads to a multiple of 128, so 1920x1080 arrives as 1920x1152), and vlc stretches the picture to
|
||||
// fill whatever size is returned. Ask for the size of the track being played instead. The format
|
||||
// is negotiated more than once, and vlc has not selected the track yet on the first calls
|
||||
val player = mediaPlayer
|
||||
val tracks = player?.media()?.info()?.videoTracks()
|
||||
val playingTrack = player?.video()?.track()
|
||||
val track = tracks?.firstOrNull { it.id() == playingTrack } ?: tracks?.singleOrNull()
|
||||
this.sourceWidth = track?.width()?.takeIf { it > 0 } ?: sourceWidth
|
||||
this.sourceHeight = track?.height()?.takeIf { it > 0 } ?: sourceHeight
|
||||
return RV32BufferFormat(this.sourceWidth, this.sourceHeight)
|
||||
}
|
||||
|
||||
override fun allocatedBuffers(buffers: Array<ByteBuffer>) {
|
||||
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asComposeImageBitmap
|
||||
import chat.simplex.common.simplexWindowState
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.jetbrains.skia.Bitmap
|
||||
import org.jetbrains.skia.Codec
|
||||
import org.jetbrains.skia.ColorAlphaType
|
||||
import org.jetbrains.skia.Data
|
||||
|
||||
// Animated images are decoded from data received from other users, which is what the bounds below are for
|
||||
|
||||
// In bytes as the file chooses the color type, 1920x1920 at 4 bytes a pixel is ~15MB
|
||||
private const val MAX_ANIMATED_RASTER_BYTES: Long = 1920L * 1920 * 4
|
||||
// 65535x32 is only 2.1MP, so each side is bounded as well
|
||||
private const val MAX_ANIMATED_SIDE = 4096
|
||||
// Skia copies the encoded bytes into native memory and scans them to count frames
|
||||
private const val MAX_ANIMATED_FILE_SIZE = 32 * 1024 * 1024
|
||||
// Counting frames builds a table the codec holds while it plays, several times the file's size for minimal ones
|
||||
private const val MAX_ANIMATED_FRAMES = 10_000
|
||||
// 10ms or less is how "as fast as possible" is written, and browsers substitute 100ms for it
|
||||
private const val MAX_UNSPECIFIED_FRAME_DURATION_MS = 10
|
||||
private const val DEFAULT_FRAME_DURATION_MS = 100L
|
||||
private const val MIN_FRAME_DURATION_MS = 20L
|
||||
// A frame costing more than this holds most of a core to show under 10 frames a second
|
||||
private const val MAX_FRAME_DECODE_MS = 100L
|
||||
// Far above what a frame within the bounds above can cost, so only a stall reaches it
|
||||
private const val MAX_WAITED_FRAME_COST_MS = 10 * MAX_FRAME_DECODE_MS
|
||||
private const val SLOW_FRAME_COST = 2
|
||||
internal const val MAX_SLOW_FRAME_DEBT = 4
|
||||
private const val NO_PRIOR_FRAME = -1
|
||||
// A frame given no prior frame is rebuilt by recursing down its chain, so a long enough one overflows the
|
||||
// native stack, which no catch can stop. Real animations rebuild nothing.
|
||||
private const val MAX_REBUILT_FRAMES = 64
|
||||
|
||||
// Read once, as asking the codec about a frame allocates and the loop may repeat forever
|
||||
private class Animation(val codec: Codec, val priorFrames: IntArray, val frameDelays: LongArray)
|
||||
|
||||
/**
|
||||
* The current frame of [data], or [still] when it is not an animation, falls outside the bounds above, or
|
||||
* fails before showing a frame; after that it stops on the frame it reached. Decoding runs off the UI thread.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberAnimatedImage(data: ByteArray, still: ImageBitmap, hidden: () -> Boolean = { false }): ImageBitmap {
|
||||
// Keyed as the decoding is, so frames are not written into a replaced state, and hidden is not a key so it
|
||||
// pauses instead of restarting. Every frame is a new wrapper, and only its identity says the image changed.
|
||||
val frame = remember(data, still) { mutableStateOf(still, neverEqualPolicy()) }
|
||||
LaunchedEffect(data, still) {
|
||||
withContext(animationDecoder) {
|
||||
val animation = openAnimation(data) ?: return@withContext
|
||||
try {
|
||||
playFrames(animation, hidden) { frame.value = it }
|
||||
} finally {
|
||||
animation.codec.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
return frame.value
|
||||
}
|
||||
|
||||
// Decoding several large animations must not starve the long running calls that share this pool
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val animationDecoder = Dispatchers.Default.limitedParallelism(2)
|
||||
|
||||
private fun openAnimation(data: ByteArray): Animation? {
|
||||
if (!looksAnimatable(data) || !fileSizeWithinBounds(data.size)) return null
|
||||
var codec: Codec? = null
|
||||
var animation: Animation? = null
|
||||
try {
|
||||
// Skia retains the encoded bytes, so this native buffer is freed as soon as the codec has taken it
|
||||
val encoded = Data.makeFromBytes(data)
|
||||
codec = try {
|
||||
Codec.makeFromData(encoded)
|
||||
} finally {
|
||||
encoded.close()
|
||||
}
|
||||
animation = boundedAnimation(codec)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Unable to read animated image: $e")
|
||||
}
|
||||
// The codec is only left open for an animation that took it, so no bound can return past closing it
|
||||
if (animation == null) codec?.close()
|
||||
return animation
|
||||
}
|
||||
|
||||
private fun boundedAnimation(codec: Codec): Animation? {
|
||||
val info = codec.imageInfo
|
||||
if (!rasterWithinBounds(info.width, info.height, info.bytesPerPixel)) return null
|
||||
// Counting frames scans the file, while dimensions are only read from the header
|
||||
val frameCount = codec.frameCount
|
||||
if (!frameCountWithinBounds(frameCount)) return null
|
||||
val requiredFrames = IntArray(frameCount)
|
||||
val frameDelays = LongArray(frameCount)
|
||||
for (i in 0 until frameCount) {
|
||||
val frameInfo = codec.getFrameInfo(i)
|
||||
requiredFrames[i] = frameInfo.requiredFrame
|
||||
frameDelays[i] = frameDuration(frameInfo.duration)
|
||||
}
|
||||
if (!rebuiltFramesWithinBounds(requiredFrames)) return null
|
||||
return Animation(codec, IntArray(frameCount) { priorFrame(it, requiredFrames[it]) }, frameDelays)
|
||||
}
|
||||
|
||||
internal fun looksAnimatable(data: ByteArray): Boolean =
|
||||
data.startsWith("GIF8") || (data.startsWith("RIFF") && data.startsWith("WEBP", offset = 8))
|
||||
|
||||
private fun ByteArray.startsWith(ascii: String, offset: Int = 0): Boolean {
|
||||
if (size < offset + ascii.length) return false
|
||||
return ascii.indices.all { this[offset + it] == ascii[it].code.toByte() }
|
||||
}
|
||||
|
||||
internal fun rasterWithinBounds(width: Int, height: Int, bytesPerPixel: Int): Boolean {
|
||||
if (width !in 1..MAX_ANIMATED_SIDE || height !in 1..MAX_ANIMATED_SIDE) return false
|
||||
// 0 bytes per pixel would let any raster pass the bound below
|
||||
if (bytesPerPixel < 1) return false
|
||||
// The sides are bounded before they are multiplied, so the product cannot overflow
|
||||
return width.toLong() * height * bytesPerPixel <= MAX_ANIMATED_RASTER_BYTES
|
||||
}
|
||||
|
||||
private suspend fun playFrames(animation: Animation, hidden: () -> Boolean, showFrame: (ImageBitmap) -> Unit) {
|
||||
try {
|
||||
val codec = animation.codec
|
||||
val bitmap = Bitmap()
|
||||
// The codec reports only the first frame's alpha type, and a frame with alpha cannot be read into an
|
||||
// opaque bitmap. allocPixels returns false rather than throwing.
|
||||
if (!bitmap.allocPixels(codec.imageInfo.withColorAlphaType(ColorAlphaType.PREMUL))) return
|
||||
var loopsLeft = codec.repetitionCount // negative repeats forever
|
||||
var debt = 0
|
||||
while (true) {
|
||||
for (i in animation.priorFrames.indices) {
|
||||
awaitFramesAreSeen(hidden)
|
||||
val startedDecoding = System.nanoTime()
|
||||
codec.readPixels(bitmap, i, animation.priorFrames[i])
|
||||
// Wall time, so a frame can overrun by being descheduled rather than by being expensive
|
||||
val decodedIn = System.nanoTime() - startedDecoding
|
||||
debt = slowFrameDebt(debt, decodedIn > MAX_FRAME_DECODE_MS * 1_000_000)
|
||||
// The bitmap is never closed, as the wrapper points at its pixels and a frame may still be drawn
|
||||
showFrame(bitmap.asComposeImageBitmap())
|
||||
if (debt >= MAX_SLOW_FRAME_DEBT) {
|
||||
Log.d(TAG, "Animation too expensive to decode, stopping on this frame")
|
||||
return
|
||||
}
|
||||
delay(frameWait(animation.frameDelays[i], decodedIn / 1_000_000))
|
||||
}
|
||||
if (loopsLeft == 0) return
|
||||
if (loopsLeft > 0) loopsLeft--
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e // the view is gone, not a decoding failure
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, "Unable to play animated image: $e")
|
||||
}
|
||||
}
|
||||
|
||||
// Composition survives the window being minimized or hidden, and the caller knows when its image cannot be seen
|
||||
private suspend fun awaitFramesAreSeen(hidden: () -> Boolean) {
|
||||
if (framesAreSeen(hidden)) return
|
||||
snapshotFlow { framesAreSeen(hidden) }.first { it }
|
||||
}
|
||||
|
||||
private fun framesAreSeen(hidden: () -> Boolean): Boolean =
|
||||
simplexWindowState.windowVisible.value && !simplexWindowState.windowState.isMinimized && !hidden()
|
||||
|
||||
// Waiting out the cost as well as the delay leaves an animation about half a decoder thread. The cost is
|
||||
// wall time, so a stall is only waited out so far.
|
||||
internal fun frameWait(delayMs: Long, costMs: Long): Long =
|
||||
maxOf(delayMs, costMs.coerceAtMost(MAX_WAITED_FRAME_COST_MS))
|
||||
|
||||
internal fun fileSizeWithinBounds(size: Int): Boolean = size <= MAX_ANIMATED_FILE_SIZE
|
||||
|
||||
// A file of no frames would spin the playback loop uncancellably, as it only suspends inside the range
|
||||
internal fun frameCountWithinBounds(frameCount: Int): Boolean = frameCount in 2..MAX_ANIMATED_FRAMES
|
||||
|
||||
// The frame the codec may decode this one from, which is the one before it when the bitmap still holds it.
|
||||
// Rebuilding the chain instead costs 9.10ms a frame against 0.05ms, and Skia refuses a frame it did not ask for.
|
||||
internal fun priorFrame(index: Int, requiredFrame: Int): Int =
|
||||
if (requiredFrame == index - 1) index - 1 else NO_PRIOR_FRAME
|
||||
|
||||
// requiredFrames is what each frame continues; one that continues nothing starts a chain of its own
|
||||
internal fun rebuiltFramesWithinBounds(requiredFrames: IntArray): Boolean {
|
||||
val chain = IntArray(requiredFrames.size)
|
||||
requiredFrames.forEachIndexed { index, required ->
|
||||
val continues = required in 0 until index
|
||||
chain[index] = if (continues) chain[required] + 1 else 1
|
||||
if (continues && priorFrame(index, required) == NO_PRIOR_FRAME && chain[required] > MAX_REBUILT_FRAMES) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Two expensive frames in a row reach the debt, and so do frames that alternate with cheap ones, which a
|
||||
// count that reset would miss
|
||||
internal fun slowFrameDebt(debt: Int, tooSlow: Boolean): Int =
|
||||
(debt + if (tooSlow) SLOW_FRAME_COST else -1).coerceAtLeast(0)
|
||||
|
||||
internal fun frameDuration(declaredMs: Int): Long =
|
||||
if (declaredMs <= MAX_UNSPECIFIED_FRAME_DURATION_MS) DEFAULT_FRAME_DURATION_MS
|
||||
else declaredMs.toLong().coerceAtLeast(MIN_FRAME_DURATION_MS)
|
||||
+41
@@ -7,6 +7,10 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.compose.videoplayer.SkiaBitmapVideoSurface
|
||||
import uk.co.caprica.vlcj.media.Media
|
||||
import uk.co.caprica.vlcj.media.MediaEventAdapter
|
||||
import uk.co.caprica.vlcj.media.MediaParsedStatus
|
||||
import uk.co.caprica.vlcj.media.ParseFlag
|
||||
import uk.co.caprica.vlcj.media.VideoOrientation
|
||||
import uk.co.caprica.vlcj.player.base.*
|
||||
import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent
|
||||
@@ -255,6 +259,43 @@ actual class VideoPlayer actual constructor(
|
||||
return@withContext VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration)
|
||||
}
|
||||
|
||||
// Parsing a local container header takes a few dozen ms, this is only a guard against a stuck parse
|
||||
private const val PARSE_TIMEOUT_MS = 3000L
|
||||
|
||||
// Reads container metadata to tell whether there is a video track at all, without decoding a frame.
|
||||
// libvlc signals the end of parsing with an event, so no polling or frame-decoding budget is needed.
|
||||
suspend fun hasVideoTrack(uri: URI): Boolean = withContext(previewThread.asCoroutineDispatcher()) {
|
||||
if (!uri.toFile().exists()) return@withContext false
|
||||
val media = try {
|
||||
vlcPreviewFactory.media().newMedia(uri.toFile().absolutePath)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "hasVideoTrack unable to create media: ${e.stackTraceToString()}")
|
||||
null
|
||||
} ?: return@withContext false
|
||||
try {
|
||||
val parsed = CompletableDeferred<MediaParsedStatus?>()
|
||||
media.events().addMediaEventListener(object: MediaEventAdapter() {
|
||||
// vlcj maps an unknown status int to null, and a null here would throw on its event thread
|
||||
override fun mediaParsedChanged(parsedMedia: Media?, newStatus: MediaParsedStatus?) {
|
||||
parsed.complete(newStatus)
|
||||
}
|
||||
})
|
||||
if (!media.parsing().parse(PARSE_TIMEOUT_MS.toInt(), ParseFlag.PARSE_LOCAL)) {
|
||||
return@withContext false
|
||||
}
|
||||
if (withTimeoutOrNull(PARSE_TIMEOUT_MS) { parsed.await() } != MediaParsedStatus.DONE) {
|
||||
media.parsing().stop()
|
||||
return@withContext false
|
||||
}
|
||||
media.info().videoTracks().isNotEmpty()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "hasVideoTrack error: ${e.stackTraceToString()}")
|
||||
false
|
||||
} finally {
|
||||
media.release()
|
||||
}
|
||||
}
|
||||
|
||||
val playerThread = Executors.newSingleThreadExecutor()
|
||||
private val previewThread = Executors.newSingleThreadExecutor()
|
||||
private val playersPool: ArrayList<Component> = ArrayList()
|
||||
|
||||
+2
-1
@@ -9,5 +9,6 @@ fun isVideo(uri: URI): Boolean {
|
||||
path.endsWith(".mp4") ||
|
||||
path.endsWith(".mpg") ||
|
||||
path.endsWith(".mpeg") ||
|
||||
path.endsWith(".mkv")
|
||||
path.endsWith(".mkv") ||
|
||||
path.endsWith(".webm")
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,7 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
@@ -15,10 +16,15 @@ actual fun SimpleAndAnimatedImageView(
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
smallView: Boolean,
|
||||
blurred: State<Boolean>,
|
||||
ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit
|
||||
) {
|
||||
// LALAL make it animated too
|
||||
ImageView(BitmapPainter(imageBitmap)) {
|
||||
// The small view is the chat list preview, which the layout keeps on screen without pause, so it stays a
|
||||
// still image. A full screen modal is shown beside the chat rather than in place of it, so this item keeps
|
||||
// composing under one and would otherwise decode where nobody can see it.
|
||||
val frame = if (smallView) imageBitmap
|
||||
else rememberAnimatedImage(data, imageBitmap) { blurred.value || ModalManager.fullscreen.hasModalsOpen() }
|
||||
ImageView(BitmapPainter(frame)) {
|
||||
if (getLoadedFilePath(file) != null) {
|
||||
ModalManager.fullscreen.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
|
||||
+3
-1
@@ -19,8 +19,10 @@ import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) {
|
||||
// Decoded once, as an animation recomposes this on every frame
|
||||
val still = remember(data) { getBitmapFromByteArray(data, false) ?: MR.images.decentralized.image.toComposeImageBitmap() }
|
||||
Image(
|
||||
getBitmapFromByteArray(data, false) ?: MR.images.decentralized.image.toComposeImageBitmap(),
|
||||
rememberAnimatedImage(data, still),
|
||||
contentDescription = stringResource(MR.strings.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier,
|
||||
|
||||
+2
@@ -255,6 +255,8 @@ actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolea
|
||||
return VideoPlayer.getBitmapFromVideo(null, uri, withAlertOnException)
|
||||
}
|
||||
|
||||
actual suspend fun hasVideoTrack(uri: URI): Boolean = VideoPlayer.hasVideoTrack(uri)
|
||||
|
||||
@OptIn(ExperimentalEncodingApi::class)
|
||||
actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encode(this)
|
||||
|
||||
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package chat.simplex.app
|
||||
|
||||
import chat.simplex.common.platform.MAX_SLOW_FRAME_DEBT
|
||||
import chat.simplex.common.platform.frameWait
|
||||
import chat.simplex.common.platform.fileSizeWithinBounds
|
||||
import chat.simplex.common.platform.frameCountWithinBounds
|
||||
import chat.simplex.common.platform.frameDuration
|
||||
import chat.simplex.common.platform.looksAnimatable
|
||||
import chat.simplex.common.platform.priorFrame
|
||||
import chat.simplex.common.platform.rasterWithinBounds
|
||||
import chat.simplex.common.platform.rebuiltFramesWithinBounds
|
||||
import chat.simplex.common.platform.slowFrameDebt
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
// The bounds an animated image must satisfy, checked as arithmetic: skiko's native library is not on the
|
||||
// test runtime classpath, and these numbers are the part that has to be right about someone else's file.
|
||||
class AnimatedImageBoundsTest {
|
||||
private val BYTES_PER_PIXEL = 4 // what a GIF or WebP decodes to
|
||||
|
||||
@Test
|
||||
fun testOrdinaryAnimationIsWithinBounds() {
|
||||
assertTrue(rasterWithinBounds(64, 64, BYTES_PER_PIXEL))
|
||||
assertTrue(rasterWithinBounds(1244, 554, BYTES_PER_PIXEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testHugeDeclaredDimensionsAreRejected() {
|
||||
// A 17GB raster, declared by a GIF of 35 bytes
|
||||
assertFalse(rasterWithinBounds(65535, 65535, BYTES_PER_PIXEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDimensionsOverRasterBudgetAreRejected() {
|
||||
// Plausible-looking, but one raster of this size is ~64MB and a chat shows several at once
|
||||
assertFalse(rasterWithinBounds(4000, 4000, BYTES_PER_PIXEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAspectRatioIsBoundedOnEachSideSeparately() {
|
||||
// Only 2.1MP, so the raster bound alone would animate this with a 65535-pixel scanline
|
||||
assertFalse(rasterWithinBounds(65535, 32, BYTES_PER_PIXEL))
|
||||
assertFalse(rasterWithinBounds(32, 65535, BYTES_PER_PIXEL))
|
||||
assertTrue(rasterWithinBounds(3000, 500, BYTES_PER_PIXEL))
|
||||
assertTrue(rasterWithinBounds(4096, 900, BYTES_PER_PIXEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBudgetBoundariesAreExact() {
|
||||
assertTrue(rasterWithinBounds(1920, 1920, BYTES_PER_PIXEL))
|
||||
assertFalse(rasterWithinBounds(1921, 1920, BYTES_PER_PIXEL))
|
||||
assertFalse(rasterWithinBounds(4097, 100, BYTES_PER_PIXEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmptyDimensionsAreRejected() {
|
||||
assertFalse(rasterWithinBounds(0, 64, BYTES_PER_PIXEL))
|
||||
assertFalse(rasterWithinBounds(64, 0, BYTES_PER_PIXEL))
|
||||
assertFalse(rasterWithinBounds(-1, 64, BYTES_PER_PIXEL))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWiderColorTypesCountAgainstTheSameBudget() {
|
||||
// The file chooses its color type, so the 1920x1920 that fits at four bytes is twice the raster at eight
|
||||
assertFalse(rasterWithinBounds(1920, 1920, 8))
|
||||
assertTrue(rasterWithinBounds(1357, 1357, 8))
|
||||
// A color type claiming no bytes per pixel would otherwise make any raster look free
|
||||
assertFalse(rasterWithinBounds(4096, 4096, 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnimatableContainersAreRecognized() {
|
||||
assertTrue(looksAnimatable("GIF89a...".toByteArray()))
|
||||
assertTrue(looksAnimatable("GIF87a...".toByteArray()))
|
||||
assertTrue(looksAnimatable("RIFF????WEBPVP8X".toByteArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPhotosNeverReachTheAnimationDecoder() {
|
||||
assertFalse(looksAnimatable(bytes(0x89, 'P'.code, 'N'.code, 'G'.code, 0x0D, 0x0A, 0x1A, 0x0A)))
|
||||
assertFalse(looksAnimatable(bytes(0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46)))
|
||||
// A RIFF container that is not WebP, a wave file say
|
||||
assertFalse(looksAnimatable("RIFF????WAVEfmt ".toByteArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShortDataIsRejectedWithoutReadingPastTheEnd() {
|
||||
assertFalse(looksAnimatable(ByteArray(0)))
|
||||
assertFalse(looksAnimatable("GIF".toByteArray()))
|
||||
// Long enough for the RIFF tag, too short for the format that follows it
|
||||
assertFalse(looksAnimatable("RIFF".toByteArray()))
|
||||
assertFalse(looksAnimatable("RIFF1234WEB".toByteArray()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testPriorFrameIsReusedOnlyWhenTheBitmapHoldsIt() {
|
||||
assertEquals(4, priorFrame(5, 4))
|
||||
// An older required frame is no longer in the bitmap, which is also how a predecessor disposed to what
|
||||
// came before it is skipped, as Skia never requires one
|
||||
assertEquals(-1, priorFrame(5, 2))
|
||||
assertEquals(-1, priorFrame(5, -1))
|
||||
// For the first frame, -1 is both its required frame and no prior frame
|
||||
assertEquals(-1, priorFrame(0, -1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnlyFilesSmallEnoughToScanAreWithinBounds() {
|
||||
assertTrue(fileSizeWithinBounds(0))
|
||||
// The largest animation in this repository
|
||||
assertTrue(fileSizeWithinBounds(6_013_354))
|
||||
assertTrue(fileSizeWithinBounds(32 * 1024 * 1024))
|
||||
assertFalse(fileSizeWithinBounds(32 * 1024 * 1024 + 1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnlyAnimationsWorthHoldingFramesForAreWithinBounds() {
|
||||
assertFalse(frameCountWithinBounds(0))
|
||||
assertFalse(frameCountWithinBounds(1))
|
||||
assertFalse(frameCountWithinBounds(-1))
|
||||
assertTrue(frameCountWithinBounds(2))
|
||||
// The longest animation in this repository, and the bound itself
|
||||
assertTrue(frameCountWithinBounds(1041))
|
||||
assertTrue(frameCountWithinBounds(10_000))
|
||||
assertFalse(frameCountWithinBounds(10_001))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAnimationsThatRebuildNothingAreWithinBounds() {
|
||||
// What a real animation looks like: each frame continues the one before it, so nothing is rebuilt
|
||||
assertTrue(rebuiltFramesWithinBounds(IntArray(5000) { it - 1 }))
|
||||
assertTrue(rebuiltFramesWithinBounds(intArrayOf(-1, -1, -1)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testShortRebuiltChainsAreWithinBounds() {
|
||||
// A GIF disposing to what came before it: frame 2 continues frame 0, rebuilding two frames
|
||||
assertTrue(rebuiltFramesWithinBounds(intArrayOf(-1, 0, 0, 2, 2, 4)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLongRebuiltChainsAreRejected() {
|
||||
// Alternating disposal makes every other frame rebuild the chain before it, which Skia recurses through:
|
||||
// 8000 frames of that overflows the native stack and kills the app
|
||||
val alternating = IntArray(8000) { if (it % 2 == 0) it - 2 else it - 1 }
|
||||
assertFalse(rebuiltFramesWithinBounds(alternating))
|
||||
// The bound is on what a rebuild costs, not on how long the animation is
|
||||
assertTrue(rebuiltFramesWithinBounds(IntArray(8000) { it - 1 }))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRebuiltChainBoundIsExact() {
|
||||
fun chainOf(length: Int) = IntArray(length + 2) { if (it == length + 1) it - 2 else it - 1 }
|
||||
assertTrue(rebuiltFramesWithinBounds(chainOf(64)))
|
||||
assertFalse(rebuiltFramesWithinBounds(chainOf(65)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFramesContinuingSomethingImpossibleStartTheirOwnChain() {
|
||||
// A file is not trusted to say a frame continues itself, a later frame, or one that is not there
|
||||
assertTrue(rebuiltFramesWithinBounds(IntArray(5000) { it }))
|
||||
assertTrue(rebuiltFramesWithinBounds(IntArray(5000) { it + 1 }))
|
||||
assertTrue(rebuiltFramesWithinBounds(IntArray(5000) { 9999 }))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAFrameCheaperThanItsDelayWaitsAsItAlwaysDid() {
|
||||
assertEquals(70, frameWait(70, 0))
|
||||
assertEquals(70, frameWait(70, 2))
|
||||
assertEquals(70, frameWait(70, 70))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAFrameDearerThanItsDelayIsWaitedOut() {
|
||||
assertEquals(85, frameWait(20, 85))
|
||||
assertEquals(500, frameWait(20, 500))
|
||||
assertEquals(1000, frameWait(20, 1000))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAStallIsNotWaitedOut() {
|
||||
// Wall time counts a machine that suspended mid-decode, which the frame never spent
|
||||
assertEquals(1000, frameWait(20, 30_000))
|
||||
assertEquals(1000, frameWait(20, 8L * 60 * 60 * 1000))
|
||||
assertEquals(5000, frameWait(5000, 30_000))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTwoExpensiveFramesInARowStopTheAnimation() {
|
||||
var debt = slowFrameDebt(0, tooSlow = true)
|
||||
assertTrue(debt < MAX_SLOW_FRAME_DEBT)
|
||||
debt = slowFrameDebt(debt, tooSlow = true)
|
||||
assertTrue(debt >= MAX_SLOW_FRAME_DEBT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAFrameThatOnlyOverranIsPaidOff() {
|
||||
// One expensive frame among cheap ones is a busy machine, not an expensive animation
|
||||
var debt = slowFrameDebt(0, tooSlow = true)
|
||||
repeat(4) { debt = slowFrameDebt(debt, tooSlow = false) }
|
||||
assertEquals(0, debt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAlternatingExpensiveFramesStillStopTheAnimation() {
|
||||
// Frames that alternate are never expensive twice in a row, which is what a count that resets would miss
|
||||
var debt = 0
|
||||
var frames = 0
|
||||
while (debt < MAX_SLOW_FRAME_DEBT && frames < 100) {
|
||||
debt = slowFrameDebt(debt, tooSlow = frames % 2 == 0)
|
||||
frames++
|
||||
}
|
||||
assertEquals(5, frames)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCheapFramesEarnNoCreditAgainstLaterExpensiveOnes() {
|
||||
var debt = 0
|
||||
repeat(1000) { debt = slowFrameDebt(debt, tooSlow = false) }
|
||||
assertEquals(0, debt)
|
||||
debt = slowFrameDebt(debt, tooSlow = true)
|
||||
debt = slowFrameDebt(debt, tooSlow = true)
|
||||
assertTrue(debt >= MAX_SLOW_FRAME_DEBT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrameDurationSubstitutesTheDefaultForFramesInAHurry() {
|
||||
// Skia reports a GIF delay in milliseconds, so "no delay" and "one centisecond" arrive as 0 and 10
|
||||
assertEquals(100, frameDuration(0))
|
||||
assertEquals(100, frameDuration(10))
|
||||
// Not expected from Skia, but read from the file
|
||||
assertEquals(100, frameDuration(-1))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrameDurationKeepsAuthoredDelays() {
|
||||
assertEquals(70, frameDuration(70))
|
||||
assertEquals(600, frameDuration(600))
|
||||
assertEquals(Int.MAX_VALUE.toLong(), frameDuration(Int.MAX_VALUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFrameDurationRaisesDelaysBelowTheFloor() {
|
||||
assertEquals(20, frameDuration(11))
|
||||
assertEquals(20, frameDuration(19))
|
||||
assertEquals(20, frameDuration(20))
|
||||
assertEquals(21, frameDuration(21))
|
||||
}
|
||||
|
||||
private fun bytes(vararg values: Int): ByteArray = values.map { it.toByte() }.toByteArray()
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
// Built from the upstream submodule pinned at efb2ebf85a2b06f7c508aba9eaad5377e3a01e81, because
|
||||
// upstream never released the org.nanohttpd packages and JitPack no longer serves or builds that
|
||||
// commit. Only the core and websocket modules are used, the samples are not.
|
||||
group = "org.nanohttpd"
|
||||
version = "efb2ebf"
|
||||
|
||||
val upstream = layout.projectDirectory.dir("upstream")
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
setSrcDirs(listOf(upstream.dir("core/src/main/java"), upstream.dir("websocket/src/main/java")))
|
||||
exclude("org/nanohttpd/samples/**")
|
||||
}
|
||||
resources.setSrcDirs(listOf(upstream.dir("core/src/main/resources")))
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
val jvmVersion = JavaVersion.toVersion(providers.gradleProperty("kotlin.jvm.target").get())
|
||||
sourceCompatibility = jvmVersion
|
||||
targetCompatibility = jvmVersion
|
||||
}
|
||||
|
||||
// Without this the jar records the build machine's timestamps, file order and file modes,
|
||||
// which makes the desktop packages unreproducible
|
||||
tasks.jar {
|
||||
// Checked here and not during configuration, so that Android builds, which don't use nanohttpd,
|
||||
// work without the submodule
|
||||
doFirst {
|
||||
if (!upstream.file("core/src/main/java").asFile.isDirectory) {
|
||||
throw GradleException("nanohttpd sources are missing, run: git submodule update --init --recursive")
|
||||
}
|
||||
}
|
||||
isPreserveFileTimestamps = false
|
||||
isReproducibleFileOrder = true
|
||||
filePermissions { unix("644") }
|
||||
dirPermissions { unix("755") }
|
||||
}
|
||||
+1
Submodule apps/multiplatform/external/nanohttpd/upstream added at efb2ebf85a
@@ -24,13 +24,11 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=7.0
|
||||
android.version_code=366
|
||||
android.version_name=7.1-beta.1
|
||||
android.version_code=374
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=7.0
|
||||
desktop.version_code=155
|
||||
desktop.version_name=7.1-beta.1
|
||||
desktop.version_code=158
|
||||
|
||||
kotlin.version=2.1.20
|
||||
gradle.plugin.version=8.7.0
|
||||
|
||||
@@ -222,9 +222,10 @@ Desktop users cannot send voice messages. The record button either does nothing
|
||||
|
||||
Several other Desktop features are also marked with `LALAL` placeholders:
|
||||
- **QR Code Scanner** (`QRCodeScanner.desktop.kt:12`) -- scanning QR codes is not implemented on Desktop
|
||||
- **Animated Drawables** (`Utils.desktop.kt:179`) -- animated image support (e.g., GIF in-line rendering) is not implemented
|
||||
- **Animated Chat Images** (`CIImageView.desktop.kt:19`) -- animated image rendering in chat items
|
||||
- **isImage detection** (`Images.desktop.kt:168`) -- image type detection (implemented but marked as incomplete)
|
||||
- **Animated Drawables** (`Utils.desktop.kt:236`) -- `getDrawableFromUri` returns null, so `isAnimImage` falls back to the file extension
|
||||
- **isImage detection** (`Images.desktop.kt:189`) -- image type detection (implemented but marked as incomplete)
|
||||
|
||||
Desktop cannot decode WebP in chat: `decodeBoundedBufferedImage` (`Utils.desktop.kt:191`) reads through ImageIO, which has no WebP reader, so a received `.webp` renders only as the sender's preview and never opens full screen, and a picked one is skipped. Wallpapers and link previews decode WebP, as they read through Skia instead (`Images.desktop.kt:204`). Received GIFs do animate in chat items and full screen; the animated image decoder accepts WebP but is never reached for it.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ Each type has a dedicated composable in `views/chat/item/`:
|
||||
| Type | Composable | Description |
|
||||
|---|---|---|
|
||||
| Text | `FramedItemView` | Rendered with markdown (bold, italic, code, links, `@mentions`) via `CIMarkdownText` |
|
||||
| Image | `CIImageView` | Thumbnail with tap-to-fullscreen via `ImageFullScreenView` |
|
||||
| Image | `CIImageView` | Thumbnail with tap-to-fullscreen via `ImageFullScreenView`; animated GIFs play inline and full screen |
|
||||
| Video | `CIVideoView` | Video thumbnail with play button; inline playback via `VideoPlayerHolder` |
|
||||
| Voice | `CIVoiceView` | Waveform visualization with playback controls and duration |
|
||||
| File | `CIFileView` | File icon, name, size; download/open actions with progress indicator |
|
||||
|
||||
@@ -18,4 +18,4 @@ pluginManagement {
|
||||
|
||||
rootProject.name = "app"
|
||||
|
||||
include(":android", ":desktop", ":common")
|
||||
include(":android", ":desktop", ":common", ":external:nanohttpd")
|
||||
|
||||
@@ -370,6 +370,8 @@ var platform: PlatformInterface = object : PlatformInterface {}
|
||||
| `androidCreateActiveCallState()` | empty `Closeable` | Create `ActiveCallState` |
|
||||
| `androidIsXiaomiDevice()` | `false` | Check device brand |
|
||||
| `androidApiLevel` | `null` | `Build.VERSION.SDK_INT` |
|
||||
| `androidIsPlayStoreBuild` | `false` | `BuildConfig.PLAY_STORE` |
|
||||
| `androidLoadPlayStoreCountry()` | no-op | Request the Play account country (google flavor only) |
|
||||
| `androidLockPortraitOrientation()` | no-op | Lock to `SCREEN_ORIENTATION_PORTRAIT` |
|
||||
| `androidAskToAllowBackgroundCalls()` | `true` | Show battery restriction dialog |
|
||||
| `desktopShowAppUpdateNotice()` | no-op | Show update notice (Desktop only) |
|
||||
|
||||
@@ -202,6 +202,18 @@ Long-press or right-click opens a dropdown menu with context-sensitive actions (
|
||||
| `InvalidJSON` | -- | `CIInvalidJSONView` | `CIInvalidJSONView.kt` |
|
||||
| `CIMemberCreatedContact` | -- | `CIMemberCreatedContactView` | `CIMemberCreatedContactView.kt` |
|
||||
|
||||
### Animated Images
|
||||
|
||||
`SimpleAndAnimatedImageView` is `expect`/`actual`. Android delegates to coil, which drives the animation
|
||||
itself. Desktop decodes frames with Skia's `Codec` in `platform/AnimatedImage.desktop.kt`, where
|
||||
`rememberAnimatedImage(data, still, hidden)` returns the frame to draw and falls back to the still image when
|
||||
the data is not an animation, exceeds the decode bounds, or fails before showing a frame. Decoding runs off the UI thread
|
||||
on two threads of the shared pool, and pauses while the window is minimized or hidden, while the image is behind the
|
||||
privacy blur, and while a full screen modal covers the chat. An animation whose frames cost too much to
|
||||
decode stops on the frame it reached rather than falling back to the still. The chat list preview (`smallView`) stays a
|
||||
still image. Only GIF reaches this path: desktop decodes stills with ImageIO, which has no WebP reader, so a
|
||||
received `.webp` renders only as the sender's preview and never opens full screen.
|
||||
|
||||
---
|
||||
|
||||
## 6. Context Menu Actions
|
||||
|
||||
@@ -345,7 +345,7 @@ class ArchiveConfig(
|
||||
### Import Flow
|
||||
|
||||
1. User selects an archive file.
|
||||
2. UI copies it to a temp location and constructs an `ArchiveConfig`.
|
||||
2. UI copies it into `databaseExportDir` and constructs an `ArchiveConfig`. The destination is confined to that folder: `getFileName` returns a bare file name on every platform, and `saveArchiveFromURI` checks the canonical destination before copying.
|
||||
3. Calls `apiImportArchive(config)` which sends `CC.ApiImportArchive` to the Haskell core.
|
||||
4. The core extracts and replaces both databases.
|
||||
5. Returns `CR.ArchiveImported` with a list of `ArchiveError` (non-fatal issues during import).
|
||||
|
||||
@@ -424,6 +424,7 @@ Path prefix: `common/src/desktopMain/kotlin/chat/simplex/common/`
|
||||
| `platform/Videos.desktop.kt` | PC10 | Low | Desktop video utilities |
|
||||
| `platform/Notifications.desktop.kt` | PC18 | Low | Desktop notification setup |
|
||||
| `platform/Images.desktop.kt` | PC10 | Low | Desktop image processing |
|
||||
| `platform/AnimatedImage.desktop.kt` | PC10 | Low | Desktop animated image frame decoding (bounded) |
|
||||
| `platform/PlatformTextField.desktop.kt` | PC4 | Low | Desktop text field actual implementation |
|
||||
| `platform/Share.desktop.kt` | PC10 | Low | Desktop clipboard/share |
|
||||
| `platform/Back.desktop.kt` | PC1 | Low | Desktop back navigation |
|
||||
|
||||
Reference in New Issue
Block a user