Compare commits

..

2 Commits

Author SHA1 Message Date
Evgeny Poberezkin
c7f5443920 fix 2023-04-07 11:01:33 +01:00
Evgeny Poberezkin
ccff79aa59 core: allow messages with contact requests 2023-04-07 10:15:19 +01:00
201 changed files with 3193 additions and 23987 deletions

View File

@@ -48,7 +48,7 @@
## Join user groups
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-3](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FeDEgekVhh0zIBYvUupGWZ96kiBEMbXwK%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAS7Z3zQyOhZ9o1qzI9OTZRySpkFTagdLMa6Rc7opuNh4%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22cG3W1qxZGzyrvHTugx16bg%3D%3D%22%7D)
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
@@ -86,11 +86,10 @@ Join our translators to help SimpleX grow!
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
Languages in progress: Arabic, Hindi, Japanese, Spanish and [many others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
## Contribute

View File

@@ -11,8 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 26
targetSdk 32
versionCode 117
versionName "5.0"
versionCode 111
versionName "4.6.1-beta.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -77,18 +77,7 @@ android {
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
// if (isRelease) {
// Comma separated list of languages that will be included in the apk
android.defaultConfig.resConfigs(
"en",
"cs",
"de",
"es",
"fr",
"it",
"nl",
"pl",
"ru",
"zh-rCN"
)
android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "ru", "zh-rCN")
// }
if (isBundle) {
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'

View File

@@ -12,7 +12,8 @@ import androidx.activity.viewModels
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Lock
import androidx.compose.runtime.*
@@ -24,8 +25,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import chat.simplex.app.MainActivity.Companion.enteredBackground
import chat.simplex.app.model.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.ui.theme.SimpleXTheme
@@ -36,11 +37,8 @@ import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chatlist.*
import chat.simplex.app.views.database.DatabaseErrorView
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
import chat.simplex.app.views.usersettings.LAMode
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -95,7 +93,7 @@ class MainActivity: FragmentActivity() {
laFailed,
::runAuthenticate,
::setPerformLA,
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
showLANotice = { m.controller.showLANotice(this) }
)
}
}
@@ -113,8 +111,7 @@ class MainActivity: FragmentActivity() {
override fun onResume() {
super.onResume()
val enteredBackgroundVal = enteredBackground.value
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
runAuthenticate()
}
}
@@ -168,27 +165,16 @@ class MainActivity: FragmentActivity() {
delay(50)
withContext(Dispatchers.Main) {
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_unlock)
else
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_log_in_using_credential)
else
generalGetString(R.string.auth_unlock),
generalGetString(R.string.auth_unlock),
generalGetString(R.string.auth_log_in_using_credential),
this@MainActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success ->
userAuthorized.value = true
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed ->
laFailed.value = true
if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) {
laFailedAlert()
}
}
is LAResult.Unavailable -> {
LAResult.Unavailable -> {
userAuthorized.value = true
m.performLA.value = false
m.controller.appPrefs.performLA.set(false)
@@ -202,116 +188,21 @@ class MainActivity: FragmentActivity() {
}
}
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!laNoticeShown.get()) {
laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title_simplex_lock),
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
confirmText = generalGetString(R.string.la_notice_turn_on),
onConfirm = {
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
showChooseLAMode(laNoticeShown, activity)
}
}
)
private fun setPerformLA(on: Boolean) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA()
} else {
disableLA()
}
}
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
laNoticeShown.set(true)
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.la_lock_mode),
text = null,
confirmText = generalGetString(R.string.la_lock_mode_passcode),
dismissText = generalGetString(R.string.la_lock_mode_system),
onConfirm = {
AlertManager.shared.hideAlert()
setPasscode()
},
onDismiss = {
AlertManager.shared.hideAlert()
initialEnableLA(activity)
}
)
}
private fun initialEnableLA(activity: FragmentActivity) {
private fun enableLA() {
val m = vm.chatModel
val appPrefs = m.controller.appPrefs
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
authenticate(
generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
m.performLA.value = true
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
m.performLA.value = false
appPrefs.performLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
m.performLA.value = false
appPrefs.performLA.set(false)
m.showAdvertiseLAUnavailableAlert.value = true
}
}
}
)
}
private fun setPasscode() {
val chatModel = vm.chatModel
val appPrefs = chatModel.controller.appPrefs
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
chatModel.performLA.value = true
appPrefs.performLA.set(true)
appPrefs.laMode.set(LAMode.PASSCODE)
laTurnedOnAlert()
},
cancel = {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
close)
}
}
}
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
if (on) {
enableLA(activity)
} else {
disableLA(activity)
}
}
private fun enableLA(activity: FragmentActivity) {
val m = vm.chatModel
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_enable_simplex_lock)
else
generalGetString(R.string.new_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential)
else
"",
activity,
this@MainActivity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
@@ -320,13 +211,11 @@ class MainActivity: FragmentActivity() {
prefPerformLA.set(true)
laTurnedOnAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed -> {
m.performLA.value = false
prefPerformLA.set(false)
laFailedAlert()
}
is LAResult.Unavailable -> {
LAResult.Unavailable -> {
m.performLA.value = false
prefPerformLA.set(false)
laUnavailableInstructionAlert()
@@ -336,33 +225,24 @@ class MainActivity: FragmentActivity() {
)
}
private fun disableLA(activity: FragmentActivity) {
private fun disableLA() {
val m = vm.chatModel
authenticate(
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_disable_simplex_lock)
else
generalGetString(R.string.la_enter_app_passcode),
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
generalGetString(R.string.auth_confirm_credential)
else
generalGetString(R.string.auth_disable_simplex_lock),
activity,
generalGetString(R.string.auth_disable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
this@MainActivity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
prefPerformLA.set(false)
ksAppPassword.remove()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
is LAResult.Error, LAResult.Failed -> {
m.performLA.value = true
prefPerformLA.set(true)
laFailedAlert()
}
is LAResult.Unavailable -> {
LAResult.Unavailable -> {
m.performLA.value = false
prefPerformLA.set(false)
laUnavailableTurningOffAlert()
@@ -384,7 +264,7 @@ fun MainPage(
userAuthorized: MutableState<Boolean?>,
laFailed: MutableState<Boolean>,
runAuthenticate: () -> Unit,
setPerformLA: (Boolean, FragmentActivity) -> Unit,
setPerformLA: (Boolean) -> Unit,
showLANotice: () -> Unit
) {
var showChatDatabaseError by rememberSaveable {
@@ -512,14 +392,6 @@ fun MainPage(
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView()
}
DisposableEffectOnRotate {
// When using lock delay = 0 and screen rotates, the app will be locked which is not useful.
// Let's prolong the unlocked period to 3 sec for screen rotation to take place
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
enteredBackground.value = elapsedRealtime() + 3000
}
}
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
@@ -582,23 +454,14 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
when {
intent.type == "text/plain" -> {
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text != null) {
chatModel.sharedContent.value = SharedContent.Text(text)
}
"text/plain" == intent.type -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
chatModel.sharedContent.value = SharedContent.Text(it)
}
isMediaIntent(intent) -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri))
} // All other mime types
}
else -> {
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
if (uri != null) {
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri)
}
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it))
} // All other mime types
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
}
}
}
@@ -606,23 +469,16 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
// Close active chat and show a list of chats
chatModel.chatId.value = null
chatModel.clearOverlays.value = true
Log.e(TAG, "ACTION_SEND_MULTIPLE ${intent.type}")
when {
isMediaIntent(intent) -> {
val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
if (uris != null) {
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris)
} // All other mime types
}
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let {
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
} // All other mime types
else -> {}
}
}
}
}
fun isMediaIntent(intent: Intent): Boolean =
intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
if (chatModel.currentUser.value == null) {

View File

@@ -326,4 +326,4 @@ class SimplexService: Service() {
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
}
}
}

View File

@@ -54,8 +54,10 @@ class ChatModel(val controller: ChatController) {
val terminalItems = mutableStateListOf<TerminalItem>()
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
val userSMPServers = mutableStateOf<(List<ServerCfg>)?>(null)
// Allows to temporary save servers that are being edited on multiple screens
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
val presetSMPServers = mutableStateOf<(List<String>)?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent
@@ -729,7 +731,6 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser
}
override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null }
override val displayName get() = localAlias.ifEmpty { profile.displayName }
@@ -748,14 +749,12 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO
}
fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO
}
companion object {
@@ -885,7 +884,6 @@ data class GroupInfo (
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Voice -> fullGroupPreferences.voice.on
ChatFeature.Calls -> false
}
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
override val displayName get() = groupProfile.displayName
@@ -1312,7 +1310,6 @@ data class ChatItem (
is CIContent.SndCall -> showNtfDir
is CIContent.RcvCall -> false // notification is shown on CallInvitation instead
is CIContent.RcvIntegrityError -> showNtfDir
is CIContent.RcvDecryptionError -> showNtfDir
is CIContent.RcvGroupInvitation -> showNtfDir
is CIContent.SndGroupInvitation -> showNtfDir
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
@@ -1470,10 +1467,10 @@ data class ChatItem (
file = null
)
fun invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String): ChatItem =
fun invalidJSON(json: String): ChatItem =
ChatItem(
chatDir = chatDir ?: CIDirection.DirectSnd(),
meta = meta ?: CIMeta.invalidJSON(),
chatDir = CIDirection.DirectSnd(),
meta = CIMeta.invalidJSON(),
content = CIContent.InvalidJSON(json),
quotedItem = null,
file = null
@@ -1616,7 +1613,6 @@ sealed class CIContent: ItemContent {
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
@@ -1643,7 +1639,6 @@ sealed class CIContent: ItemContent {
is SndCall -> status.text(duration)
is RcvCall -> status.text(duration)
is RcvIntegrityError -> msgError.text
is RcvDecryptionError -> msgDecryptError.text
is RcvGroupInvitation -> groupInvitation.text
is SndGroupInvitation -> groupInvitation.text
is RcvGroupEventContent -> rcvGroupEvent.text
@@ -1682,17 +1677,6 @@ sealed class CIContent: ItemContent {
}
}
@Serializable
enum class MsgDecryptError {
@SerialName("ratchetHeader") RatchetHeader,
@SerialName("tooManySkipped") TooManySkipped;
val text: String get() = when (this) {
RatchetHeader -> generalGetString(R.string.decryption_error)
TooManySkipped -> generalGetString(R.string.decryption_error)
}
}
@Serializable
class CIQuote (
val chatDir: CIDirection? = null,
@@ -1738,32 +1722,23 @@ class CIFile(
is CIFileStatus.SndTransfer -> true
is CIFileStatus.SndComplete -> true
is CIFileStatus.SndCancelled -> true
is CIFileStatus.SndError -> true
is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> false
is CIFileStatus.RcvTransfer -> false
is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> true
is CIFileStatus.RcvError -> false
}
val cancelAction: CancelAction? = when (fileStatus) {
is CIFileStatus.SndStored -> sndCancelAction
is CIFileStatus.SndTransfer -> sndCancelAction
is CIFileStatus.SndComplete ->
if (fileProtocol == FileProtocol.XFTP) {
revokeCancelAction
} else {
null
}
is CIFileStatus.SndCancelled -> null
is CIFileStatus.SndError -> null
is CIFileStatus.RcvInvitation -> null
is CIFileStatus.RcvAccepted -> rcvCancelAction
is CIFileStatus.RcvTransfer -> rcvCancelAction
is CIFileStatus.RcvCancelled -> null
is CIFileStatus.RcvComplete -> null
is CIFileStatus.RcvError -> null
val cancellable: Boolean = when (fileStatus) {
is CIFileStatus.SndStored -> fileProtocol != FileProtocol.XFTP // TODO true - enable when XFTP send supports cancel
is CIFileStatus.SndTransfer -> fileProtocol != FileProtocol.XFTP // TODO true
is CIFileStatus.SndComplete -> false
is CIFileStatus.SndCancelled -> false
is CIFileStatus.RcvInvitation -> false
is CIFileStatus.RcvAccepted -> true
is CIFileStatus.RcvTransfer -> true
is CIFileStatus.RcvCancelled -> false
is CIFileStatus.RcvComplete -> false
}
companion object {
@@ -1778,44 +1753,6 @@ class CIFile(
}
}
@Serializable
class CancelAction(
val uiActionId: Int,
val alert: AlertInfo
)
@Serializable
class AlertInfo(
val titleId: Int,
val messageId: Int,
val confirmId: Int
)
private val sndCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_snd_file__title,
messageId = R.string.stop_snd_file__message,
confirmId = R.string.stop_file__confirm
)
)
private val revokeCancelAction: CancelAction = CancelAction(
uiActionId = R.string.revoke_file__action,
alert = AlertInfo(
titleId = R.string.revoke_file__title,
messageId = R.string.revoke_file__message,
confirmId = R.string.revoke_file__confirm
)
)
private val rcvCancelAction: CancelAction = CancelAction(
uiActionId = R.string.stop_file__action,
alert = AlertInfo(
titleId = R.string.stop_rcv_file__title,
messageId = R.string.stop_rcv_file__message,
confirmId = R.string.stop_file__confirm
)
)
@Serializable
enum class FileProtocol {
@SerialName("smp") SMP,
@@ -1828,13 +1765,11 @@ sealed class CIFileStatus {
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
}
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
@@ -2109,11 +2044,7 @@ enum class CICallStatus {
}
}
fun durationText(sec: Int): String {
val s = sec % 60
val m = sec / 60
return if (m < 60) "%02d:%02d".format(m, s) else "%02d:%02d:%02d".format(m / 60, m % 60, s)
}
fun durationText(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60)
@Serializable
sealed class MsgErrorType() {

View File

@@ -20,6 +20,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
@@ -83,8 +84,6 @@ class AppPreferences(val context: Context) {
set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) }
)
val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false)
val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.SYSTEM) { LAMode.values().firstOrNull { it.name == this } }
val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30)
val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false)
val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null)
val privacyProtectScreen = mkBoolPreference(SHARED_PREFS_PRIVACY_PROTECT_SCREEN, true)
@@ -109,7 +108,6 @@ class AppPreferences(val context: Context) {
val chatLastStart = mkDatePreference(SHARED_PREFS_CHAT_LAST_START, null)
val developerTools = mkBoolPreference(SHARED_PREFS_DEVELOPER_TOOLS, false)
val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false)
val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050")
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
get = fun(): TransportSessionMode {
@@ -143,8 +141,6 @@ class AppPreferences(val context: Context) {
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
val encryptedAppPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE, null)
val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null)
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
@@ -153,6 +149,8 @@ class AppPreferences(val context: Context) {
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val xftpSendEnabled = mkBoolPreference(SHARED_PREFS_XFTP_SEND_ENABLED, false)
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
get = fun() = sharedPreferences.getInt(prefName, default),
@@ -185,12 +183,6 @@ class AppPreferences(val context: Context) {
set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply()
)
private fun <T> mkEnumPreference(prefName: String, default: T, construct: String.() -> T?): SharedPreference<T> =
SharedPreference(
get = fun() = sharedPreferences.getString(prefName, default.toString())?.construct() ?: default,
set = fun(value) = sharedPreferences.edit().putString(prefName, value.toString()).apply()
)
/**
* Provide `[commit] = true` to save preferences right now, not after some unknown period of time.
* So in case of a crash this value will be saved 100%
@@ -217,8 +209,6 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_WEBRTC_POLICY_RELAY = "WebrtcPolicyRelay"
private const val SHARED_PREFS_WEBRTC_CALLS_ON_LOCK_SCREEN = "CallsOnLockScreen"
private const val SHARED_PREFS_PERFORM_LA = "PerformLA"
private const val SHARED_PREFS_LA_MODE = "LocalAuthenticationMode"
private const val SHARED_PREFS_LA_LOCK_DELAY = "LocalAuthenticationLockDelay"
private const val SHARED_PREFS_LA_NOTICE_SHOWN = "LANoticeShown"
private const val SHARED_PREFS_WEBRTC_ICE_SERVERS = "WebrtcICEServers"
private const val SHARED_PREFS_PRIVACY_PROTECT_SCREEN = "PrivacyProtectScreen"
@@ -234,7 +224,6 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort"
private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode"
private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode"
private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode"
@@ -255,13 +244,12 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
private const val SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE = "EncryptedAppPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE = "InitializationVectorAppPassphrase"
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_XFTP_SEND_ENABLED = "XFTPSendEnabled"
}
}
@@ -349,6 +337,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
suspend fun getUserChatData() {
chatModel.userAddress.value = apiGetUserAddress()
val smpServers = getUserSMPServers()
chatModel.userSMPServers.value = smpServers?.first
chatModel.presetSMPServers.value = smpServers?.second
chatModel.chatItemTTL.value = getChatItemTTL()
val chats = apiGetChats()
chatModel.updateChats(chats)
@@ -588,44 +579,38 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun getUserProtoServers(serverProtocol: ServerProtocol): UserProtocolServers? {
val userId = kotlin.runCatching { currentUserId("getUserProtoServers") }.getOrElse { return null }
val r = sendCmd(CC.APIGetUserProtoServers(userId, serverProtocol))
return if (r is CR.UserProtoServers) r.servers
else {
Log.e(TAG, "getUserProtoServers bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.error_loading_smp_servers else R.string.error_loading_xftp_servers),
"${r.responseType}: ${r.details}"
)
null
}
private suspend fun getUserSMPServers(): Pair<List<ServerCfg>, List<String>>? {
val userId = kotlin.runCatching { currentUserId("getUserSMPServers") }.getOrElse { return null }
val r = sendCmd(CC.APIGetUserSMPServers(userId))
if (r is CR.UserSMPServers) return r.smpServers to r.presetSMPServers
Log.e(TAG, "getUserSMPServers bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun setUserProtoServers(serverProtocol: ServerProtocol, servers: List<ServerCfg>): Boolean {
val userId = kotlin.runCatching { currentUserId("setUserProtoServers") }.getOrElse { return false }
val r = sendCmd(CC.APISetUserProtoServers(userId, serverProtocol, servers))
suspend fun setUserSMPServers(smpServers: List<ServerCfg>): Boolean {
val userId = kotlin.runCatching { currentUserId("setUserSMPServers") }.getOrElse { return false }
val r = sendCmd(CC.APISetUserSMPServers(userId, smpServers))
return when (r) {
is CR.CmdOk -> true
else -> {
Log.e(TAG, "setUserProtoServers bad response: ${r.responseType} ${r.details}")
Log.e(TAG, "setUserSMPServers bad response: ${r.responseType} ${r.details}")
AlertManager.shared.showAlertMsg(
generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.error_saving_smp_servers else R.string.error_saving_xftp_servers),
generalGetString(if (serverProtocol == ServerProtocol.SMP) R.string.ensure_smp_server_address_are_correct_format_and_unique else R.string.ensure_xftp_server_address_are_correct_format_and_unique)
generalGetString(R.string.error_saving_smp_servers),
generalGetString(R.string.ensure_smp_server_address_are_correct_format_and_unique)
)
false
}
}
}
suspend fun testProtoServer(server: String): ProtocolTestFailure? {
val userId = currentUserId("testProtoServer")
val r = sendCmd(CC.APITestProtoServer(userId, server))
suspend fun testSMPServer(smpServer: String): SMPTestFailure? {
val userId = currentUserId("testSMPServer")
val r = sendCmd(CC.APITestSMPServer(userId, smpServer))
return when (r) {
is CR.ServerTestResult -> r.testFailure
is CR.SmpTestResult -> r.smpTestFailure
else -> {
Log.e(TAG, "testProtoServer bad response: ${r.responseType} ${r.details}")
throw Exception("testProtoServer bad response: ${r.responseType} ${r.details}")
Log.e(TAG, "testSMPServer bad response: ${r.responseType} ${r.details}")
throw Exception("testSMPServer bad response: ${r.responseType} ${r.details}")
}
}
}
@@ -1017,7 +1002,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
val chatItem = apiCancelFile(fileId)
if (chatItem != null) {
chatItemSimpleUpdate(user, chatItem)
cleanupFile(chatItem)
}
}
@@ -1433,35 +1417,40 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileSndCancelled -> {
is CR.RcvFileSndCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem)
}
is CR.RcvFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileError -> {
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem)
}
is CR.SndFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileComplete -> {
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupDirectFile(r.chatItem)
val cItem = r.chatItem.chatItem
val mc = cItem.content.msgContent
val fileName = cItem.file?.fileName
if (
r.chatItem.chatInfo.chatType == ChatType.Direct
&& mc is MsgContent.MCFile
&& fileName != null
) {
removeFile(appContext, fileName)
}
}
is CR.SndFileRcvCancelled -> {
is CR.SndFileRcvCancelled ->
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupDirectFile(r.chatItem)
}
is CR.SndFileProgressXFTP ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.SndFileCompleteXFTP -> {
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem)
}
is CR.SndFileError -> {
chatItemSimpleUpdate(r.user, r.chatItem)
cleanupFile(r.chatItem)
val cItem = r.chatItem.chatItem
val mc = cItem.content.msgContent
val fileName = cItem.file?.fileName
if (
mc is MsgContent.MCFile
&& fileName != null
) {
removeFile(appContext, fileName)
}
}
is CR.CallInvitation -> {
chatModel.callManager.reportNewIncomingCall(r.callInvitation)
@@ -1513,24 +1502,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
private fun cleanupDirectFile(aChatItem: AChatItem) {
if (aChatItem.chatInfo.chatType == ChatType.Direct) {
cleanupFile(aChatItem)
}
}
private fun cleanupFile(aChatItem: AChatItem) {
val cItem = aChatItem.chatItem
val mc = cItem.content.msgContent
val fileName = cItem.file?.fileName
if (
mc is MsgContent.MCFile
&& fileName != null
) {
removeFile(appContext, fileName)
}
}
private fun active(user: User): Boolean = user.userId == chatModel.currentUser.value?.userId
private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) {
@@ -1725,6 +1696,43 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
)
}
fun showLANotice(activity: FragmentActivity) {
Log.d(TAG, "showLANotice")
if (!appPrefs.laNoticeShown.get()) {
appPrefs.laNoticeShown.set(true)
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.la_notice_title_simplex_lock),
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
confirmText = generalGetString(R.string.la_notice_turn_on),
onConfirm = {
authenticate(
generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
chatModel.performLA.value = true
appPrefs.performLA.set(true)
laTurnedOnAlert()
}
is LAResult.Error, LAResult.Failed -> {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
}
LAResult.Unavailable -> {
chatModel.performLA.value = false
appPrefs.performLA.set(false)
chatModel.showAdvertiseLAUnavailableAlert.value = true
}
}
}
)
}
)
}
}
fun isIgnoringBatteryOptimizations(context: Context): Boolean {
val powerManager = context.getSystemService(Application.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(context.packageName)
@@ -1741,22 +1749,14 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
fun getXFTPCfg(): XFTPFileConfig {
return XFTPFileConfig(minFileSize = 0)
fun getXFTPCfg(): XFTPFileConfig? {
val prefXFTPSendEnabled = appPrefs.xftpSendEnabled.get()
return if (prefXFTPSendEnabled) XFTPFileConfig(minFileSize = 0) else null
}
fun getNetCfg(): NetCfg {
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
val proxyHostPort = appPrefs.networkProxyHostPort.get()
val socksProxy = if (useSocksProxy) {
if (proxyHostPort?.startsWith("localhost:") == true) {
proxyHostPort.removePrefix("localhost")
} else {
proxyHostPort ?: ":9050"
}
} else {
null
}
val socksProxy = if (useSocksProxy) ":9050" else null
val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!)
val requiredHostMode = appPrefs.networkRequiredHostMode.get()
val sessionMode = appPrefs.networkSessionMode.get()
@@ -1859,9 +1859,9 @@ sealed class CC {
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC()
class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC()
class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC()
class APITestProtoServer(val userId: Long, val server: String): CC()
class APIGetUserSMPServers(val userId: Long): CC()
class APISetUserSMPServers(val userId: Long, val smpServers: List<ServerCfg>): CC()
class APITestSMPServer(val userId: Long, val smpServer: String): CC()
class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC()
class APIGetChatItemTTL(val userId: Long): CC()
class APISetNetworkConfig(val networkConfig: NetCfg): CC()
@@ -1915,7 +1915,7 @@ sealed class CC {
is ApiMuteUser -> "/_mute user $userId"
is ApiUnmuteUser -> "/_unmute user $userId"
is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}"
is StartChat -> "/_start subscribe=on expire=${onOff(expire)} xftp=on"
is StartChat -> "/_start subscribe=on expire=${onOff(expire)}"
is ApiStopChat -> "/_stop"
is SetTempFolder -> "/_temp_folder $tempFolder"
is SetFilesFolder -> "/_files_folder $filesFolder"
@@ -1943,9 +1943,9 @@ sealed class CC {
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId"
is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}"
is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}"
is APITestProtoServer -> "/_server test $userId $server"
is APIGetUserSMPServers -> "/_smp $userId"
is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}"
is APITestSMPServer -> "/_smp test $userId $smpServer"
is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}"
is APIGetChatItemTTL -> "/_ttl $userId"
is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}"
@@ -2028,9 +2028,9 @@ sealed class CC {
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink"
is APIGetUserProtoServers -> "apiGetUserProtoServers"
is APISetUserProtoServers -> "apiSetUserProtoServers"
is APITestProtoServer -> "testProtoServer"
is APIGetUserSMPServers -> "apiGetUserSMPServers"
is APISetUserSMPServers -> "apiSetUserSMPServers"
is APITestSMPServer -> "testSMPServer"
is APISetChatItemTTL -> "apiSetChatItemTTL"
is APIGetChatItemTTL -> "apiGetChatItemTTL"
is APISetNetworkConfig -> "/apiSetNetworkConfig"
@@ -2107,7 +2107,7 @@ sealed class CC {
companion object {
fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}"
fun protoServersStr(servers: List<ServerCfg>) = json.encodeToString(ProtoServersConfig(servers))
fun smpServersStr(smpServers: List<ServerCfg>) = if (smpServers.isEmpty()) "default" else json.encodeToString(SMPServersConfig(smpServers))
}
}
@@ -2142,21 +2142,8 @@ class ArchiveConfig(val archivePath: String, val disableCompression: Boolean? =
class DBEncryptionConfig(val currentKey: String, val newKey: String)
@Serializable
enum class ServerProtocol {
@SerialName("smp") SMP,
@SerialName("xftp") XFTP;
}
@Serializable
data class ProtoServersConfig(
val servers: List<ServerCfg>
)
@Serializable
data class UserProtocolServers(
val serverProtocol: ServerProtocol,
val protoServers: List<ServerCfg>,
val presetServers: List<String>,
data class SMPServersConfig(
val smpServers: List<ServerCfg>
)
@Serializable
@@ -2210,39 +2197,29 @@ data class ServerCfg(
}
@Serializable
enum class ProtocolTestStep {
enum class SMPTestStep {
@SerialName("connect") Connect,
@SerialName("disconnect") Disconnect,
@SerialName("createQueue") CreateQueue,
@SerialName("secureQueue") SecureQueue,
@SerialName("deleteQueue") DeleteQueue,
@SerialName("createFile") CreateFile,
@SerialName("uploadFile") UploadFile,
@SerialName("downloadFile") DownloadFile,
@SerialName("compareFile") CompareFile,
@SerialName("deleteFile") DeleteFile;
@SerialName("disconnect") Disconnect;
val text: String get() = when (this) {
Connect -> generalGetString(R.string.smp_server_test_connect)
Disconnect -> generalGetString(R.string.smp_server_test_disconnect)
CreateQueue -> generalGetString(R.string.smp_server_test_create_queue)
SecureQueue -> generalGetString(R.string.smp_server_test_secure_queue)
DeleteQueue -> generalGetString(R.string.smp_server_test_delete_queue)
CreateFile -> generalGetString(R.string.smp_server_test_create_file)
UploadFile -> generalGetString(R.string.smp_server_test_upload_file)
DownloadFile -> generalGetString(R.string.smp_server_test_download_file)
CompareFile -> generalGetString(R.string.smp_server_test_compare_file)
DeleteFile -> generalGetString(R.string.smp_server_test_delete_file)
Disconnect -> generalGetString(R.string.smp_server_test_disconnect)
}
}
@Serializable
data class ProtocolTestFailure(
val testStep: ProtocolTestStep,
data class SMPTestFailure(
val testStep: SMPTestStep,
val testError: AgentErrorType
) {
override fun equals(other: Any?): Boolean {
if (other !is ProtocolTestFailure) return false
if (other !is SMPTestFailure) return false
return other.testStep == this.testStep
}
@@ -2255,8 +2232,6 @@ data class ProtocolTestFailure(
return when {
testError is AgentErrorType.SMP && testError.smpErr is SMPErrorType.AUTH ->
err + " " + generalGetString(R.string.error_smp_test_server_auth)
testError is AgentErrorType.XFTP && testError.xftpErr is XFTPErrorType.AUTH ->
err + " " + generalGetString(R.string.error_xftp_test_server_auth)
testError is AgentErrorType.BROKER && testError.brokerErr is BrokerErrorType.NETWORK ->
err + " " + generalGetString(R.string.error_smp_test_certificate)
else -> err
@@ -2266,7 +2241,6 @@ data class ProtocolTestFailure(
@Serializable
data class ServerAddress(
val serverProtocol: ServerProtocol,
val hostnames: List<String>,
val port: String,
val keyHash: String,
@@ -2274,21 +2248,19 @@ data class ServerAddress(
) {
val uri: String
get() =
"${serverProtocol}://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}"
"smp://${keyHash}${if (basicAuth.isEmpty()) "" else ":$basicAuth"}@${hostnames.joinToString(",")}"
val valid: Boolean
get() = hostnames.isNotEmpty() && hostnames.toSet().size == hostnames.size
companion object {
fun empty(serverProtocol: ServerProtocol) = ServerAddress(
serverProtocol = serverProtocol,
val empty = ServerAddress(
hostnames = emptyList(),
port = "",
keyHash = "",
basicAuth = ""
)
val sampleData = ServerAddress(
serverProtocol = ServerProtocol.SMP,
hostnames = listOf("smp.simplex.im", "1234.onion"),
port = "",
keyHash = "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=",
@@ -2414,16 +2386,14 @@ data class FullChatPreferences(
val timedMessages: TimedMessagesPreference,
val fullDelete: SimpleChatPreference,
val voice: SimpleChatPreference,
val calls: SimpleChatPreference,
) {
fun toPreferences(): ChatPreferences = ChatPreferences(timedMessages = timedMessages, fullDelete = fullDelete, voice = voice, calls = calls)
fun toPreferences(): ChatPreferences = ChatPreferences(timedMessages = timedMessages, fullDelete = fullDelete, voice = voice)
companion object {
val sampleData = FullChatPreferences(
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
voice = SimpleChatPreference(allow = FeatureAllowed.YES),
calls = SimpleChatPreference(allow = FeatureAllowed.YES),
voice = SimpleChatPreference(allow = FeatureAllowed.YES)
)
}
}
@@ -2433,22 +2403,19 @@ data class ChatPreferences(
val timedMessages: TimedMessagesPreference?,
val fullDelete: SimpleChatPreference?,
val voice: SimpleChatPreference?,
val calls: SimpleChatPreference?,
) {
fun setAllowed(feature: ChatFeature, allowed: FeatureAllowed = FeatureAllowed.YES, param: Int? = null): ChatPreferences =
when (feature) {
ChatFeature.TimedMessages -> this.copy(timedMessages = TimedMessagesPreference(allow = allowed, ttl = param ?: this.timedMessages?.ttl))
ChatFeature.FullDelete -> this.copy(fullDelete = SimpleChatPreference(allow = allowed))
ChatFeature.Voice -> this.copy(voice = SimpleChatPreference(allow = allowed))
ChatFeature.Calls -> this.copy(calls = SimpleChatPreference(allow = allowed))
}
companion object {
val sampleData = ChatPreferences(
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
voice = SimpleChatPreference(allow = FeatureAllowed.YES),
calls = SimpleChatPreference(allow = FeatureAllowed.YES),
voice = SimpleChatPreference(allow = FeatureAllowed.YES)
)
}
}
@@ -2469,7 +2436,7 @@ data class TimedMessagesPreference(
): ChatPreference {
companion object {
val ttlValues: List<Int?>
get() = listOf(30, 300, 3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, null)
get() = listOf(30, 300, 3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400)
fun ttlText(ttl: Int?): String {
ttl ?: return generalGetString(R.string.feature_off)
@@ -2520,13 +2487,11 @@ data class ContactUserPreferences(
val timedMessages: ContactUserPreferenceTimed,
val fullDelete: ContactUserPreference,
val voice: ContactUserPreference,
val calls: ContactUserPreference,
) {
fun toPreferences(): ChatPreferences = ChatPreferences(
timedMessages = timedMessages.userPreference.pref,
fullDelete = fullDelete.userPreference.pref,
voice = voice.userPreference.pref,
calls = calls.userPreference.pref
voice = voice.userPreference.pref
)
companion object {
@@ -2545,12 +2510,7 @@ data class ContactUserPreferences(
enabled = FeatureEnabled(forUser = true, forContact = true),
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES)
),
calls = ContactUserPreference(
enabled = FeatureEnabled(forUser = true, forContact = true),
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES)
),
)
)
}
}
@@ -2609,6 +2569,12 @@ sealed class ContactUserPref {
@Serializable @SerialName("user") data class User(val preference: SimpleChatPreference): ContactUserPref() {
override val pref get() = preference
}
val contactOverride: SimpleChatPreference?
get() = when(this) {
is Contact -> pref
is User -> null
}
}
@Serializable
@@ -2623,6 +2589,12 @@ sealed class ContactUserPrefTimed {
@Serializable @SerialName("user") data class User(val preference: TimedMessagesPreference): ContactUserPrefTimed() {
override val pref get() = preference
}
val contactOverride: TimedMessagesPreference?
get() = when(this) {
is Contact -> pref
is User -> null
}
}
interface Feature {
@@ -2636,8 +2608,7 @@ interface Feature {
enum class ChatFeature: Feature {
@SerialName("timedMessages") TimedMessages,
@SerialName("fullDelete") FullDelete,
@SerialName("voice") Voice,
@SerialName("calls") Calls;
@SerialName("voice") Voice;
val asymmetric: Boolean get() = when (this) {
TimedMessages -> false
@@ -2654,7 +2625,6 @@ enum class ChatFeature: Feature {
TimedMessages -> generalGetString(R.string.timed_messages)
FullDelete -> generalGetString(R.string.full_deletion)
Voice -> generalGetString(R.string.voice_messages)
Calls -> generalGetString(R.string.audio_video_calls)
}
val icon: ImageVector
@@ -2662,7 +2632,6 @@ enum class ChatFeature: Feature {
TimedMessages -> Icons.Outlined.Timer
FullDelete -> Icons.Outlined.DeleteForever
Voice -> Icons.Outlined.KeyboardVoice
Calls -> Icons.Outlined.Phone
}
override val iconFilled: ImageVector
@@ -2670,7 +2639,6 @@ enum class ChatFeature: Feature {
TimedMessages -> Icons.Filled.Timer
FullDelete -> Icons.Filled.DeleteForever
Voice -> Icons.Filled.KeyboardVoice
Calls -> Icons.Filled.Phone
}
fun allowDescription(allowed: FeatureAllowed): String =
@@ -2690,11 +2658,6 @@ enum class ChatFeature: Feature {
FeatureAllowed.YES -> generalGetString(R.string.allow_voice_messages_only_if)
FeatureAllowed.NO -> generalGetString(R.string.prohibit_sending_voice_messages)
}
Calls -> when (allowed) {
FeatureAllowed.ALWAYS -> generalGetString(R.string.allow_your_contacts_to_call)
FeatureAllowed.YES -> generalGetString(R.string.allow_calls_only_if)
FeatureAllowed.NO -> generalGetString(R.string.prohibit_calls)
}
}
fun enabledDescription(enabled: FeatureEnabled): String =
@@ -2717,13 +2680,7 @@ enum class ChatFeature: Feature {
enabled.forContact -> generalGetString(R.string.only_your_contact_can_send_voice)
else -> generalGetString(R.string.voice_prohibited_in_this_chat)
}
Calls -> when {
enabled.forUser && enabled.forContact -> generalGetString(R.string.both_you_and_your_contact_can_make_calls)
enabled.forUser -> generalGetString(R.string.only_you_can_make_calls)
enabled.forContact -> generalGetString(R.string.only_your_contact_can_make_calls)
else -> generalGetString(R.string.calls_prohibited_with_this_contact)
}
}
}
}
@Serializable
@@ -2836,29 +2793,26 @@ data class ContactFeaturesAllowed(
val timedMessagesAllowed: Boolean,
val timedMessagesTTL: Int?,
val fullDelete: ContactFeatureAllowed,
val voice: ContactFeatureAllowed,
val calls: ContactFeatureAllowed,
val voice: ContactFeatureAllowed
) {
companion object {
val sampleData = ContactFeaturesAllowed(
timedMessagesAllowed = false,
timedMessagesTTL = null,
fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO),
voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
calls = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES)
)
}
}
fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPreferences): ContactFeaturesAllowed {
val pref = contactUserPreferences.timedMessages.userPreference
val allow = pref.pref.allow
val allow = pref.contactOverride?.allow
return ContactFeaturesAllowed(
timedMessagesAllowed = allow == FeatureAllowed.YES || allow == FeatureAllowed.ALWAYS,
timedMessagesTTL = pref.pref.ttl,
fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete),
voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice),
calls = contactUserPrefToFeatureAllowed(contactUserPreferences.calls),
voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice)
)
}
@@ -2876,8 +2830,7 @@ fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed
ChatPreferences(
timedMessages = TimedMessagesPreference(if (contactFeaturesAllowed.timedMessagesAllowed) FeatureAllowed.YES else FeatureAllowed.NO, contactFeaturesAllowed.timedMessagesTTL),
fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete),
voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice),
calls = contactFeatureAllowedToPref(contactFeaturesAllowed.calls),
voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice)
)
fun contactFeatureAllowedToPref(contactFeatureAllowed: ContactFeatureAllowed): SimpleChatPreference? =
@@ -3034,15 +2987,9 @@ private fun parseChatData(chat: JsonElement): Chat {
?: ChatInfo.InvalidJSON(json.encodeToString(chat.jsonObject["chatInfo"]))
val chatStats = decodeObject(Chat.ChatStats.serializer(), chat.jsonObject["chatStats"])!!
val chatItems: List<ChatItem> = chat.jsonObject["chatItems"]!!.jsonArray.map {
decodeObject(ChatItem.serializer(), it) ?: parseChatItem(it)
decodeObject(ChatItem.serializer(), it) ?: ChatItem.invalidJSON(json.encodeToString(it))
}
return Chat(chatInfo, chatItems, chatStats)
}
private fun parseChatItem(j: JsonElement): ChatItem {
val chatDir: CIDirection? = decodeObject(CIDirection.serializer(), j.jsonObject["chatDir"])
val meta: CIMeta? = decodeObject(CIMeta.serializer(), j.jsonObject["meta"])
return ChatItem.invalidJSON(chatDir, meta, json.encodeToString(j))
return Chat(chatInfo, chatItems, chatStats)
}
private fun <T> decodeObject(deserializer: DeserializationStrategy<T>, obj: JsonElement?): T? =
@@ -3058,8 +3005,8 @@ sealed class CR {
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
@Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List<Chat>): CR()
@Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR()
@Serializable @SerialName("userProtoServers") class UserProtoServers(val user: User, val servers: UserProtocolServers): CR()
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: User, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
@Serializable @SerialName("userSMPServers") class UserSMPServers(val user: User, val smpServers: List<ServerCfg>, val presetSMPServers: List<String>): CR()
@Serializable @SerialName("smpTestResult") class SmpTestResult(val user: User, val smpTestFailure: SMPTestFailure? = null): CR()
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR()
@Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR()
@Serializable @SerialName("contactInfo") class ContactInfo(val user: User, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR()
@@ -3136,7 +3083,6 @@ sealed class CR {
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR()
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: User, val chatItem: AChatItem): CR()
// sending file events
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@@ -3144,8 +3090,6 @@ sealed class CR {
@Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR()
@Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR()
@Serializable @SerialName("sndFileError") class SndFileError(val user: User, val chatItem: AChatItem): CR()
// call events
@Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR()
@Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR()
@Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR()
@@ -3169,8 +3113,8 @@ sealed class CR {
is ChatStopped -> "chatStopped"
is ApiChats -> "apiChats"
is ApiChat -> "apiChat"
is UserProtoServers -> "userProtoServers"
is ServerTestResult -> "serverTestResult"
is UserSMPServers -> "userSMPServers"
is SmpTestResult -> "smpTestResult"
is ChatItemTTL -> "chatItemTTL"
is NetworkConfig -> "networkConfig"
is ContactInfo -> "contactInfo"
@@ -3245,14 +3189,12 @@ sealed class CR {
is RcvFileCancelled -> "rcvFileCancelled"
is RcvFileSndCancelled -> "rcvFileSndCancelled"
is RcvFileProgressXFTP -> "rcvFileProgressXFTP"
is RcvFileError -> "rcvFileError"
is SndFileCancelled -> "sndFileCancelled"
is SndFileComplete -> "sndFileComplete"
is SndFileRcvCancelled -> "sndFileRcvCancelled"
is SndFileStart -> "sndFileStart"
is SndFileProgressXFTP -> "sndFileProgressXFTP"
is SndFileCompleteXFTP -> "sndFileCompleteXFTP"
is SndFileError -> "sndFileError"
is CallInvitation -> "callInvitation"
is CallOffer -> "callOffer"
is CallAnswer -> "callAnswer"
@@ -3277,8 +3219,8 @@ sealed class CR {
is ChatStopped -> noDetails()
is ApiChats -> withUser(user, json.encodeToString(chats))
is ApiChat -> withUser(user, json.encodeToString(chat))
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
is UserSMPServers -> withUser(user, "$smpServers: ${json.encodeToString(smpServers)}\n$presetSMPServers: ${json.encodeToString(presetSMPServers)}")
is SmpTestResult -> withUser(user, json.encodeToString(smpTestFailure))
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
is NetworkConfig -> json.encodeToString(networkConfig)
is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}")
@@ -3354,14 +3296,12 @@ sealed class CR {
is RcvFileCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
is RcvFileError -> withUser(user, json.encodeToString(chatItem))
is SndFileCancelled -> json.encodeToString(chatItem)
is SndFileComplete -> withUser(user, json.encodeToString(chatItem))
is SndFileRcvCancelled -> withUser(user, json.encodeToString(chatItem))
is SndFileStart -> withUser(user, json.encodeToString(chatItem))
is SndFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem)}\nsentSize: $sentSize\ntotalSize: $totalSize")
is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem))
is SndFileError -> withUser(user, json.encodeToString(chatItem))
is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}"
is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}")
is CallAnswer -> withUser(user, "contact: ${contact.id}\nanswer: ${json.encodeToString(answer)}")
@@ -3434,6 +3374,7 @@ class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) {
@Serializable
data class CoreVersionInfo(
val version: String,
val buildTimestamp: String,
val simplexmqVersion: String,
val simplexmqCommit: String
)
@@ -3512,7 +3453,6 @@ sealed class AgentErrorType {
is CMD -> "CMD ${cmdErr.string}"
is CONN -> "CONN ${connErr.string}"
is SMP -> "SMP ${smpErr.string}"
is XFTP -> "XFTP ${xftpErr.string}"
is BROKER -> "BROKER ${brokerErr.string}"
is AGENT -> "AGENT ${agentErr.string}"
is INTERNAL -> "INTERNAL $internalErr"
@@ -3520,7 +3460,6 @@ sealed class AgentErrorType {
@Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType()
@Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType()
@Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType()
@Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType()
@Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType()
@Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType()
@Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType()
@@ -3588,7 +3527,7 @@ sealed class SMPErrorType {
}
@Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType()
@Serializable @SerialName("SESSION") class SESSION: SMPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: SMPCommandError): SMPErrorType()
@Serializable @SerialName("AUTH") class AUTH: SMPErrorType()
@Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType()
@Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType()
@@ -3597,7 +3536,7 @@ sealed class SMPErrorType {
}
@Serializable
sealed class ProtocolCommandError {
sealed class SMPCommandError {
val string: String get() = when (this) {
is UNKNOWN -> "UNKNOWN"
is SYNTAX -> "SYNTAX"
@@ -3605,11 +3544,11 @@ sealed class ProtocolCommandError {
is HAS_AUTH -> "HAS_AUTH"
is NO_QUEUE -> "NO_QUEUE"
}
@Serializable @SerialName("UNKNOWN") class UNKNOWN: ProtocolCommandError()
@Serializable @SerialName("SYNTAX") class SYNTAX: ProtocolCommandError()
@Serializable @SerialName("NO_AUTH") class NO_AUTH: ProtocolCommandError()
@Serializable @SerialName("HAS_AUTH") class HAS_AUTH: ProtocolCommandError()
@Serializable @SerialName("NO_QUEUE") class NO_QUEUE: ProtocolCommandError()
@Serializable @SerialName("UNKNOWN") class UNKNOWN: SMPCommandError()
@Serializable @SerialName("SYNTAX") class SYNTAX: SMPCommandError()
@Serializable @SerialName("NO_AUTH") class NO_AUTH: SMPCommandError()
@Serializable @SerialName("HAS_AUTH") class HAS_AUTH: SMPCommandError()
@Serializable @SerialName("NO_QUEUE") class NO_QUEUE: SMPCommandError()
}
@Serializable
@@ -3651,33 +3590,3 @@ sealed class SMPAgentError {
@Serializable @SerialName("A_VERSION") class A_VERSION: SMPAgentError()
@Serializable @SerialName("A_ENCRYPTION") class A_ENCRYPTION: SMPAgentError()
}
@Serializable
sealed class XFTPErrorType {
val string: String get() = when (this) {
is BLOCK -> "BLOCK"
is SESSION -> "SESSION"
is CMD -> "CMD ${cmdErr.string}"
is AUTH -> "AUTH"
is SIZE -> "SIZE"
is QUOTA -> "QUOTA"
is DIGEST -> "DIGEST"
is CRYPTO -> "CRYPTO"
is NO_FILE -> "NO_FILE"
is HAS_FILE -> "HAS_FILE"
is FILE_IO -> "FILE_IO"
is INTERNAL -> "INTERNAL"
}
@Serializable @SerialName("BLOCK") object BLOCK: XFTPErrorType()
@Serializable @SerialName("SESSION") object SESSION: XFTPErrorType()
@Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): XFTPErrorType()
@Serializable @SerialName("AUTH") object AUTH: XFTPErrorType()
@Serializable @SerialName("SIZE") object SIZE: XFTPErrorType()
@Serializable @SerialName("QUOTA") object QUOTA: XFTPErrorType()
@Serializable @SerialName("DIGEST") object DIGEST: XFTPErrorType()
@Serializable @SerialName("CRYPTO") object CRYPTO: XFTPErrorType()
@Serializable @SerialName("NO_FILE") object NO_FILE: XFTPErrorType()
@Serializable @SerialName("HAS_FILE") object HAS_FILE: XFTPErrorType()
@Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType()
@Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType()
}

View File

@@ -27,4 +27,3 @@ val WarningOrange = Color(255, 127, 0, 255)
val WarningYellow = Color(255, 192, 0, 255)
val FileLight = Color(183, 190, 199, 255)
val FileDark = Color(101, 101, 106, 255)
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)

View File

@@ -8,4 +8,4 @@ val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
)

View File

@@ -18,7 +18,6 @@ val DEFAULT_PADDING = 16.dp
val DEFAULT_SPACE_AFTER_ICON = 4.dp
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
val DEFAULT_BOTTOM_PADDING = 48.dp
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
val DarkColorPalette = darkColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
@@ -28,7 +27,7 @@ val DarkColorPalette = darkColors(
// surface = Color.Black,
// background = Color(0xFF121212),
// surface = Color(0xFF121212),
error = Color.Red,
// error = Color(0xFFCF6679),
onBackground = Color(0xFFFFFBFA),
onSurface = Color(0xFFFFFBFA),
// onError: Color = Color.Black,
@@ -37,7 +36,6 @@ val LightColorPalette = lightColors(
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
primaryVariant = SimplexGreen,
secondary = LightGray,
error = Color.Red,
// background = Color.White,
// surface = Color.White
// onPrimary = Color.White,
@@ -76,4 +74,4 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
shapes = Shapes,
content = content
)
}
}

View File

@@ -6,22 +6,19 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme.colors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBackIosNew
import androidx.compose.material.icons.outlined.ArrowForwardIos
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
@@ -29,72 +26,61 @@ import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.Profile
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.views.helpers.withApi
import chat.simplex.app.views.onboarding.OnboardingStage
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
fun isValidDisplayName(name: String) : Boolean {
return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#")
return (name.firstOrNull { it.isWhitespace() }) == null
}
@Composable
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val displayName = rememberSaveable { mutableStateOf("") }
val fullName = rememberSaveable { mutableStateOf("") }
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
/*CloseSheetBar(close = {
if (chatModel.users.isEmpty()) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
close()
}
})*/
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
AppBarTitleCentered(stringResource(R.string.create_profile))
ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues())
ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center)
Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.display_name),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.full_name_optional__prompt),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName, "", ::isValidDisplayName)
}
AppBarTitle(stringResource(R.string.create_profile), false)
ReadableText(R.string.your_profile_is_stored_on_your_device)
ReadableText(R.string.profile_is_only_shared_with_your_contacts)
Spacer(Modifier.height(10.dp))
Text(
stringResource(R.string.display_name),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.full_name_optional__prompt),
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName)
Spacer(Modifier.fillMaxHeight().weight(1f))
Row {
if (chatModel.users.isEmpty()) {
SimpleButtonDecorated(
SimpleButton(
text = stringResource(R.string.about_simplex),
icon = Icons.Outlined.ArrowBackIosNew,
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
icon = Icons.Outlined.ArrowBackIosNew
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
@@ -107,7 +93,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
}
Surface(shape = RoundedCornerShape(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor)
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = createColor)
}
}
@@ -142,50 +128,24 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
}
@Composable
fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) {
var valid by rememberSaveable { mutableStateOf(true) }
var focused by rememberSaveable { mutableStateOf(false) }
val strokeColor by remember {
derivedStateOf {
if (valid) {
if (focused) {
HighOrLowlight.copy(alpha = 0.6f)
} else {
HighOrLowlight.copy(alpha = 0.3f)
}
} else Color.Red
}
}
fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester? = null) {
val modifier = Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING)
.background(MaterialTheme.colors.secondary)
.height(40.dp)
.clip(RoundedCornerShape(5.dp))
.padding(8.dp)
.navigationBarsWithImePadding()
.onFocusChanged { focused = it.isFocused }
Box(
Modifier
.fillMaxWidth()
.height(52.dp)
.border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(50)),
contentAlignment = Alignment.Center
) {
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true,
cursorBrush = SolidColor(HighOrLowlight)
)
}
LaunchedEffect(Unit) {
snapshotFlow { name.value }
.distinctUntilChanged()
.collect {
valid = isValid(it)
}
}
}
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true,
cursorBrush = SolidColor(HighOrLowlight)
)
}

View File

@@ -106,4 +106,4 @@ class CallManager(val chatModel: ChatModel) {
chatModel.controller.ntfManager.cancelCallNotification()
}
}
}
}

View File

@@ -12,7 +12,6 @@ import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -26,7 +25,6 @@ import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -38,6 +36,7 @@ import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.ProfileImage
import chat.simplex.app.views.onboarding.SimpleXLogo
import kotlinx.datetime.Clock
class IncomingCallActivity: ComponentActivity() {
@@ -187,17 +186,6 @@ fun IncomingCallLockScreenAlertLayout(
}
}
@Composable
private fun SimpleXLogo() {
Image(
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.80f)
)
}
@Composable
private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) {
Surface(

View File

@@ -48,4 +48,4 @@ class SoundPlayer {
companion object {
val shared = SoundPlayer()
}
}
}

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.chat
import android.app.Activity
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
@@ -133,6 +134,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
searchText,
useLinkPreviews = useLinkPreviews,
linkMode = chatModel.simplexLinkMode.value,
allowVideoAttachment = chatModel.controller.appPrefs.xftpSendEnabled.get(),
chatModelIncognito = chatModel.incognito.value,
back = {
hideKeyboard(view)
@@ -306,6 +308,7 @@ fun ChatLayout(
searchValue: State<String>,
useLinkPreviews: Boolean,
linkMode: SimplexLinkMode,
allowVideoAttachment: Boolean,
chatModelIncognito: Boolean,
back: () -> Unit,
info: () -> Unit,
@@ -337,6 +340,7 @@ fun ChatLayout(
sheetContent = {
ChooseAttachmentView(
attachmentOption,
allowVideoAttachment,
hide = { scope.launch { attachmentBottomSheetState.hide() } }
)
},
@@ -378,7 +382,7 @@ fun ChatInfoToolbar(
onSearchValueChanged: (String) -> Unit,
) {
val scope = rememberCoroutineScope()
val showMenu = rememberSaveable { mutableStateOf(false) }
var showMenu by rememberSaveable { mutableStateOf(false) }
var showSearch by rememberSaveable { mutableStateOf(false) }
val onBackClicked = {
if (!showSearch) {
@@ -393,15 +397,15 @@ fun ChatInfoToolbar(
val menuItems = arrayListOf<@Composable () -> Unit>()
menuItems.add {
ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), Icons.Outlined.Search, onClick = {
showMenu.value = false
showMenu = false
showSearch = true
})
}
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
if (chat.chatInfo is ChatInfo.Direct) {
barButtons.add {
IconButton({
showMenu.value = false
showMenu = false
startCall(CallMediaType.Audio)
}) {
Icon(Icons.Outlined.Phone, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
@@ -409,14 +413,14 @@ fun ChatInfoToolbar(
}
menuItems.add {
ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), Icons.Outlined.Videocam, onClick = {
showMenu.value = false
showMenu = false
startCall(CallMediaType.Video)
})
}
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
barButtons.add {
IconButton({
showMenu.value = false
showMenu = false
addMembers(chat.chatInfo.groupInfo)
}) {
Icon(Icons.Outlined.PersonAdd, stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary)
@@ -429,7 +433,7 @@ fun ChatInfoToolbar(
if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
if (ntfsEnabled.value) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications,
onClick = {
showMenu.value = false
showMenu = false
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
scope.launch {
delay(200)
@@ -440,7 +444,7 @@ fun ChatInfoToolbar(
}
barButtons.add {
IconButton({ showMenu.value = true }) {
IconButton({ showMenu = true }) {
Icon(Icons.Default.MoreVert, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
}
}
@@ -457,7 +461,11 @@ fun ChatInfoToolbar(
Divider(Modifier.padding(top = AppBarHeight))
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) {
DefaultDropdownMenu(showMenu) {
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
Modifier.widthIn(min = 220.dp)
) {
menuItems.forEach { it() }
}
}
@@ -784,27 +792,36 @@ fun BoxWithConstraintsScope.FloatingButtons(
}
val showButtonWithCounter = topUnreadCount > 0
val height = with(LocalDensity.current) { maxHeight.toPx() }
val showDropDown = remember { mutableStateOf(false) }
var showDropDown by remember { mutableStateOf(false) }
TopEndFloatingButton(
Modifier.padding(end = 16.dp, top = 24.dp).align(Alignment.TopEnd),
topUnreadCount,
showButtonWithCounter,
onClick = { scope.launch { listState.animateScrollBy(height) } },
onLongClick = { showDropDown.value = true }
onLongClick = { showDropDown = true }
)
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)) {
ItemAction(
generalGetString(R.string.mark_read),
Icons.Outlined.Check,
DropdownMenu(
expanded = showDropDown,
onDismissRequest = { showDropDown = false },
Modifier.width(220.dp),
offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)
) {
DropdownMenuItem(
onClick = {
markRead(
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
bottomUnreadCount
)
showDropDown.value = false
})
showDropDown = false
}
) {
Text(
generalGetString(R.string.mark_read),
maxLines = 1,
)
}
}
}
@@ -1066,6 +1083,7 @@ fun PreviewChatLayout() {
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
allowVideoAttachment = true,
chatModelIncognito = false,
back = {},
info = {},
@@ -1126,6 +1144,7 @@ fun PreviewGroupChatLayout() {
searchValue,
useLinkPreviews = true,
linkMode = SimplexLinkMode.DESCRIPTION,
allowVideoAttachment = true,
chatModelIncognito = false,
back = {},
info = {},

View File

@@ -4,29 +4,22 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.outlined.Close
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.durationText
import chat.simplex.app.ui.theme.DEFAULT_PADDING_HALF
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.chat.item.SentColorLight
import chat.simplex.app.views.helpers.UploadContent
import chat.simplex.app.views.helpers.base64ToBitmap
@Composable
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabled: Boolean) {
Row(
Modifier
.padding(top = 8.dp)
@@ -38,32 +31,13 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
) {
itemsIndexed(media.images) { index, item ->
val content = media.content[index]
if (content is UploadContent.Video) {
Box(contentAlignment = Alignment.Center) {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
Image(
imageBitmap,
"preview video",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
Icon(
Icons.Default.Videocam,
"preview video",
Modifier
.size(20.dp),
tint = Color.White
)
}
} else {
val imageBitmap = base64ToBitmap(item).asImageBitmap()
Image(
imageBitmap,
"preview image",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
}
items(images.size) { index ->
val imageBitmap = base64ToBitmap(images[index]).asImageBitmap()
Image(
imageBitmap,
"preview image",
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
)
}
}
if (cancelEnabled) {

View File

@@ -9,10 +9,10 @@ import android.content.*
import android.content.pm.PackageManager
import android.graphics.*
import android.graphics.drawable.AnimatedImageDrawable
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContract
@@ -49,7 +49,8 @@ import java.nio.file.Files
sealed class ComposePreview {
@Serializable object NoPreview: ComposePreview()
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
@Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable class ImagePreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable class VideoPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
@Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
}
@@ -95,7 +96,8 @@ data class ComposeState(
val sendEnabled: () -> Boolean
get() = {
val hasContent = when (preview) {
is ComposePreview.MediaPreview -> true
is ComposePreview.ImagePreview -> true
is ComposePreview.VideoPreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.isNotEmpty() || liveMessage != null
@@ -108,7 +110,8 @@ data class ComposeState(
val linkPreviewAllowed: Boolean
get() =
when (preview) {
is ComposePreview.MediaPreview -> false
is ComposePreview.ImagePreview -> false
is ComposePreview.VideoPreview -> false
is ComposePreview.VoicePreview -> false
is ComposePreview.FilePreview -> false
else -> useLinkPreviews
@@ -158,8 +161,8 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview {
is MsgContent.MCText -> ComposePreview.NoPreview
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
// TODO: include correct type
is MsgContent.MCImage -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCVideo -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCVideo -> ComposePreview.VideoPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
@@ -180,7 +183,8 @@ fun ComposeView(
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
val xftpSendEnabled = chatModel.controller.appPrefs.xftpSendEnabled.get()
val maxFileSize = getMaxFileSize(fileProtocol = if (xftpSendEnabled) FileProtocol.XFTP else FileProtocol.SMP)
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
val textStyle = remember { mutableStateOf(smallFont) }
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
@@ -188,7 +192,7 @@ fun ComposeView(
val bitmap: Bitmap? = getBitmapFromUri(uri)
if (bitmap != null) {
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
}
}
}
@@ -199,50 +203,52 @@ fun ComposeView(
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
}
}
val processPickedMedia = { uris: List<Uri>, text: String? ->
val processPickedImage = { uris: List<Uri>, text: String? ->
val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
var bitmap: Bitmap? = null
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
when {
isImage -> {
// Image
val drawable = getDrawableFromUri(uri)
bitmap = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
} else {
content.add(UploadContent.SimpleImage(uri))
}
}
else -> {
// Video
val res = getBitmapFromVideo(uri)
bitmap = res.preview
val durationMs = res.duration
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
val drawable = getDrawableFromUri(uri)
var bitmap: Bitmap? = if (drawable != null) getBitmapFromUri(uri) else null
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
if (isAnimNewApi || isAnimOldApi) {
// It's a gif or webp
val fileSize = getFileSize(context, uri)
if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(R.string.large_file),
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
)
}
} else {
content.add(UploadContent.SimpleImage(uri))
}
if (bitmap != null) {
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
}
}
if (imagesPreview.isNotEmpty()) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
}
}
val processPickedVideo = { uris: List<Uri>, text: String? ->
val content = ArrayList<UploadContent>()
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
val (bitmap: Bitmap?, durationMs: Long?) = getBitmapFromVideo(uri)
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
if (bitmap != null) {
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
}
}
if (imagesPreview.isNotEmpty()) {
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.VideoPreview(imagesPreview, content))
}
}
val processPickedFile = { uri: Uri?, text: String? ->
@@ -261,17 +267,16 @@ fun ComposeView(
}
}
}
val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it, null) }
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedImage(it, null) }
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedVideo(it, null) }
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedVideo(it, null) }
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
LaunchedEffect(attachmentOption.value) {
when (attachmentOption.value) {
AttachmentOption.CameraPhoto -> {
AttachmentOption.TakePhoto -> {
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
cameraLauncher.launchWithFallback()
@@ -282,7 +287,7 @@ fun ComposeView(
}
attachmentOption.value = null
}
AttachmentOption.GalleryImage -> {
AttachmentOption.PickImage -> {
try {
galleryImageLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
@@ -290,7 +295,7 @@ fun ComposeView(
}
attachmentOption.value = null
}
AttachmentOption.GalleryVideo -> {
AttachmentOption.PickVideo -> {
try {
galleryVideoLauncher.launch(0)
} catch (e: ActivityNotFoundException) {
@@ -298,7 +303,7 @@ fun ComposeView(
}
attachmentOption.value = null
}
AttachmentOption.File -> {
AttachmentOption.PickFile -> {
filesLauncher.launch("*/*")
attachmentOption.value = null
}
@@ -455,20 +460,28 @@ fun ComposeView(
when (val preview = cs.preview) {
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
is ComposePreview.MediaPreview -> {
is ComposePreview.ImagePreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.SimpleImage -> saveImage(context, it.uri)
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
is UploadContent.Video -> saveFileFromUri(context, it.uri)
else -> return@forEachIndexed
}
if (file != null) {
files.add(file)
if (it is UploadContent.Video) {
msgs.add(MsgContent.MCVideo(if (preview.content.lastIndex == index) msgText else "", preview.images[index], it.duration))
} else {
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
}
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
}
}
}
is ComposePreview.VideoPreview -> {
preview.content.forEachIndexed { index, it ->
val file = when (it) {
is UploadContent.Video -> saveFileFromUri(context, it.uri)
else -> return@forEachIndexed
}
if (file != null) {
files.add(file)
msgs.add(MsgContent.MCVideo(if (preview.content.lastIndex == index) msgText else "", preview.images[index], it.duration))
}
}
}
@@ -503,7 +516,8 @@ fun ComposeView(
)
}
if (sent == null &&
(cs.preview is ComposePreview.MediaPreview ||
(cs.preview is ComposePreview.ImagePreview ||
cs.preview is ComposePreview.VideoPreview ||
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
@@ -628,8 +642,13 @@ fun ComposeView(
when (val preview = composeState.value.preview) {
ComposePreview.NoPreview -> {}
is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
is ComposePreview.MediaPreview -> ComposeImageView(
preview,
is ComposePreview.ImagePreview -> ComposeImageView(
preview.images,
::cancelImages,
cancelEnabled = !composeState.value.editing
)
is ComposePreview.VideoPreview -> ComposeImageView(
preview.images,
::cancelImages,
cancelEnabled = !composeState.value.editing
)
@@ -667,7 +686,7 @@ fun ComposeView(
when (val shared = chatModel.sharedContent.value) {
is SharedContent.Text -> onMessageChange(shared.text)
is SharedContent.Media -> processPickedMedia(shared.uris, shared.text)
is SharedContent.Images -> processPickedImage(shared.uris, shared.text)
is SharedContent.File -> processPickedFile(shared.uri, shared.text)
null -> {}
}

View File

@@ -88,7 +88,7 @@ private fun ContactPreferencesLayout(
AppBarTitle(stringResource(R.string.contact_preferences))
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
val onTTLUpdated = { ttl: Int? ->
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400))
}
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
@@ -104,11 +104,6 @@ private fun ContactPreferencesLayout(
applyPrefs(featuresAllowed.copy(voice = it))
}
SectionSpacer()
val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) }
FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) {
applyPrefs(featuresAllowed.copy(calls = it))
}
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
@@ -143,7 +138,6 @@ private fun FeatureSection(
ContactFeatureAllowed.values(userDefault).map { it to it.text },
allowFeature,
icon = null,
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected
)
}
@@ -153,7 +147,7 @@ private fun FeatureSection(
pref.contactPreference.allow.text
)
}
SectionTextFooter(feature.enabledDescription(enabled) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
SectionTextFooter(feature.enabledDescription(enabled))
}
@Composable

View File

@@ -75,7 +75,7 @@ fun SendMsgView(
) {
Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value
val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.VideoPreview || cs.preview is ComposePreview.FilePreview)
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
@@ -155,18 +155,20 @@ fun SendMsgView(
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
val showDropdown = rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
var showDropdown by rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
DefaultDropdownMenu(
showDropdown,
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false },
Modifier.width(220.dp),
) {
ItemAction(
generalGetString(R.string.send_live_message),
Icons.Filled.Bolt,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown.value = false
showDropdown = false
}
)
}
@@ -225,7 +227,7 @@ private fun NativeKeyboard(
} catch (e: Exception) {
return@OnCommitContentListener false
}
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri))
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Images("", listOf(inputContentInfo.contentUri))
true
}
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)

View File

@@ -134,4 +134,4 @@ private fun splitToParts(s: String, length: Int): String {
return (0..(s.length - 1) / length)
.map { s.drop(it * length).take(length) }
.joinToString(separator = "\n")
}
}

View File

@@ -28,7 +28,6 @@ import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ChatInfoToolbarTitle
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.InfoAboutIncognito
import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
@@ -38,7 +37,6 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
var allowModifyMembers by remember { mutableStateOf(true) }
BackHandler(onBack = close)
AddGroupMembersLayout(
chatModel.incognito.value,
groupInfo = groupInfo,
creatingGroup = creatingGroup,
contactsToAdd = getContactsToAdd(chatModel),
@@ -87,7 +85,6 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
@Composable
fun AddGroupMembersLayout(
chatModelIncognito: Boolean,
groupInfo: GroupInfo,
creatingGroup: Boolean,
contactsToAdd: List<Contact>,
@@ -108,14 +105,6 @@ fun AddGroupMembersLayout(
horizontalAlignment = Alignment.Start,
) {
AppBarTitle(stringResource(R.string.button_add_members))
InfoAboutIncognito(
chatModelIncognito,
false,
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(R.string.group_main_profile_sent),
true
)
Spacer(Modifier.size(DEFAULT_PADDING))
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
@@ -329,7 +318,6 @@ fun showProhibitedToInviteIncognitoAlertDialog() {
fun PreviewAddGroupMembersLayout() {
SimpleXTheme {
AddGroupMembersLayout(
chatModelIncognito = false,
groupInfo = GroupInfo.sampleData,
creatingGroup = false,
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),

View File

@@ -1,6 +1,7 @@
package chat.simplex.app.views.chat.group
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -13,7 +14,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -23,7 +23,6 @@ import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.ProfileNameField
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.usersettings.*
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
@@ -54,32 +53,14 @@ fun GroupProfileLayout(
saveProfile: (GroupProfile) -> Unit,
) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) }
val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) }
val displayName = remember { mutableStateOf(groupProfile.displayName) }
val fullName = remember { mutableStateOf(groupProfile.fullName) }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() }
val dataUnchanged =
displayName.value == groupProfile.displayName &&
fullName.value == groupProfile.fullName &&
chosenImage.value == null
val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
close()
} else {
showUnsavedChangesAlert({
saveProfile(
groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
)
)
}, close)
}
}
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
@@ -95,16 +76,23 @@ fun GroupProfileLayout(
sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
ModalView(close = closeWithAlert) {
ModalView(close = close) {
Column(
Modifier
.verticalScroll(scrollState)
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
Text(
stringResource(R.string.group_profile_is_stored_on_members_devices),
Modifier.padding(bottom = 24.dp),
color = MaterialTheme.colors.onBackground,
lineHeight = 22.sp
)
Column(
Modifier.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING)
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
ReadableText(R.string.group_profile_is_stored_on_members_devices, TextAlign.Center)
Box(
Modifier
.fillMaxWidth()
@@ -113,7 +101,7 @@ fun GroupProfileLayout(
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f))
ProfileImage(192.dp, profileImage.value)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
@@ -121,54 +109,51 @@ fun GroupProfileLayout(
}
}
}
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.group_display_name_field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.group_display_name_field),
Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable {
saveProfile(
groupProfile.copy(
Spacer(Modifier.height(16.dp))
Row {
TextButton(stringResource(R.string.cancel_verb)) {
close.invoke()
}
Spacer(Modifier.padding(horizontal = 8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
Text(
stringResource(R.string.save_group_profile),
modifier = Modifier.clickable {
saveProfile(groupProfile.copy(
displayName = displayName.value,
fullName = fullName.value,
image = profileImage.value
)
)
},
color = MaterialTheme.colors.primary
)
} else {
Text(
stringResource(R.string.save_group_profile),
color = HighOrLowlight
)
))
},
color = MaterialTheme.colors.primary
)
} else {
Text(
stringResource(R.string.save_group_profile),
color = HighOrLowlight
)
}
}
}
Spacer(Modifier.height(DEFAULT_BOTTOM_BUTTON_PADDING))
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
@@ -179,16 +164,6 @@ fun GroupProfileLayout(
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_group_members),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,

View File

@@ -154,4 +154,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
// }
// }
//}
//}

View File

@@ -156,7 +156,6 @@ fun CIFileView(
}
is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.SndError -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvInvitation ->
if (fileSizeValid())
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary)
@@ -171,7 +170,6 @@ fun CIFileView(
}
is CIFileStatus.RcvComplete -> fileIcon()
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
is CIFileStatus.RcvError -> fileIcon(innerIcon = Icons.Outlined.Close)
}
} else {
fileIcon()

View File

@@ -82,12 +82,10 @@ fun CIImageView(
is CIFileStatus.SndTransfer -> progressIndicator()
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_image_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_image)
is CIFileStatus.RcvTransfer -> progressIndicator()
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
else -> {}
}
}

View File

@@ -1,24 +0,0 @@
package chat.simplex.app.views.chat.item
import androidx.compose.runtime.Composable
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
CIMsgError(ci, timedMessagesTTL, showMember) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.decryption_error),
text = when (msgDecryptError) {
MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
}
)
}
}

View File

@@ -300,7 +300,6 @@ private fun loadingIndicator(file: CIFile?) {
}
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_video_snd_complete)
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_video_asked_to_receive)
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_video)
is CIFileStatus.RcvTransfer ->
@@ -310,7 +309,6 @@ private fun loadingIndicator(file: CIFile?) {
progressIndicator()
}
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
else -> {}
}
}

View File

@@ -22,7 +22,8 @@ import androidx.compose.ui.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
@@ -104,7 +105,11 @@ fun ChatItemView(
@Composable
fun MsgContentItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
if (composeState.value.editing) {
@@ -163,8 +168,8 @@ fun ChatItemView(
}
)
}
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancellable) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile)
}
if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
@@ -178,7 +183,11 @@ fun ChatItemView(
@Composable
fun MarkedDeletedItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(R.string.reveal_verb),
@@ -218,7 +227,11 @@ fun ChatItemView(
@Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DefaultDropdownMenu(showMenu) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@@ -234,8 +247,7 @@ fun ChatItemView(
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
@@ -265,15 +277,14 @@ fun ChatItemView(
fun CancelFileItemAction(
fileId: Long,
showMenu: MutableState<Boolean>,
cancelFile: (Long) -> Unit,
cancelAction: CancelAction
cancelFile: (Long) -> Unit
) {
ItemAction(
stringResource(cancelAction.uiActionId),
stringResource(R.string.cancel_verb),
Icons.Outlined.Close,
onClick = {
showMenu.value = false
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
cancelFileAlertDialog(fileId, cancelFile = cancelFile)
},
color = Color.Red
)
@@ -316,30 +327,27 @@ fun ModerateItemAction(
}
@Composable
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
val finalColor = if (color == Color.Unspecified) {
if (isInDarkTheme()) MenuTextColorDark else Color.Black
} else color
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) {
DropdownMenuItem(onClick) {
Row {
Text(
text,
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = finalColor
color = color
)
Icon(icon, text, tint = finalColor)
Icon(icon, text, tint = color)
}
}
}
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(cancelAction.alert.titleId),
text = generalGetString(cancelAction.alert.messageId),
confirmText = generalGetString(cancelAction.alert.confirmId),
title = generalGetString(R.string.cancel_file__question),
text = generalGetString(R.string.file_transfer_will_be_cancelled_warning),
confirmText = generalGetString(R.string.confirm_verb),
destructive = true,
onConfirm = {
cancelFile(fileId)
@@ -356,7 +364,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = {
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)

View File

@@ -17,41 +17,19 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.ChatItem
import chat.simplex.app.model.MsgErrorType
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.AlertManager
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
CIMsgError(ci, timedMessagesTTL, showMember) {
when (msgError) {
is MsgErrorType.MsgSkipped ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
is MsgErrorType.MsgBadHash ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_msg_bad_hash),
text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_msg_bad_id),
text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
)
}
}
}
@Composable
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) {
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
Surface(
Modifier.clickable(onClick = onClick),
Modifier.clickable(onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.alert_title_skipped_messages),
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
)
}),
shape = RoundedCornerShape(18.dp),
color = ReceivedColorLight,
) {
@@ -81,7 +59,6 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false
fun IntegrityErrorItemViewPreview() {
SimpleXTheme {
IntegrityErrorItemView(
MsgErrorType.MsgBadHash(),
ChatItem.getDeletedContentSampleData(),
null
)

View File

@@ -194,14 +194,26 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
@Composable
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
ItemAction(
stringResource(R.string.mark_unread),
Icons.Outlined.MarkChatUnread,
onClick = {
markChatUnread(chat, chatModel)
showMenu.value = false
DropdownMenuItem({
markChatUnread(chat, chatModel)
showMenu.value = false
}) {
Row {
Text(
stringResource(R.string.mark_unread),
modifier = Modifier
.fillMaxWidth()
.weight(1F)
.padding(end = 15.dp),
color = MaterialTheme.colors.onBackground
)
Icon(
Icons.Outlined.MarkChatUnread,
stringResource(R.string.mark_unread),
tint = MaterialTheme.colors.onBackground
)
}
)
}
}
@Composable
@@ -435,7 +447,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 2.dp),
horizontalArrangement = Arrangement.Center,
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = {
AlertManager.shared.hideAlert()
@@ -577,7 +589,15 @@ fun ChatListNavLinkLayout(
chatLinkPreview()
}
if (dropdownMenuItems != null) {
DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems)
Box(Modifier.padding(horizontal = 16.dp)) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier.width(220.dp)
) {
dropdownMenuItems()
}
}
}
}
Divider(Modifier.padding(horizontal = 8.dp))

View File

@@ -21,7 +21,6 @@ import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.*
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -37,7 +36,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@Composable
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit, stopped: Boolean) {
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
val showNewChatSheet = {

View File

@@ -86,7 +86,7 @@ fun ChatPreviewView(
fun attachment(): Pair<ImageVector, String?>? =
when (draft.preview) {
is ComposePreview.FilePreview -> Icons.Filled.InsertDriveFile to draft.preview.fileName
is ComposePreview.MediaPreview -> Icons.Outlined.Image to null
is ComposePreview.ImagePreview -> Icons.Outlined.Image to null
is ComposePreview.VoicePreview -> Icons.Filled.PlayArrow to durationText(draft.preview.durationMs / 1000)
else -> null
}

View File

@@ -114,7 +114,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState
Text(
when (chatModel.sharedContent.value) {
is SharedContent.Text -> stringResource(R.string.share_message)
is SharedContent.Media -> stringResource(R.string.share_image)
is SharedContent.Images -> stringResource(R.string.share_image)
is SharedContent.File -> stringResource(R.string.share_file)
else -> stringResource(R.string.share_message)
},

View File

@@ -1,20 +1,19 @@
package chat.simplex.app.views.chatlist
import SectionItemView
import SectionItemViewSpaceBetween
import android.util.Log
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.draw.*
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
@@ -106,16 +105,15 @@ fun UserPicker(
) {
Column(
Modifier
.widthIn(min = 260.dp)
.widthIn(min = 220.dp)
.width(IntrinsicSize.Min)
.height(IntrinsicSize.Min)
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
.background(if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp)))
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
.background(if (isInDarkTheme()) MaterialTheme.colors.background.darker(-0.7f) else MaterialTheme.colors.background, MaterialTheme.shapes.medium)
) {
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
users.forEach { u ->
UserProfilePickerItem(u.user, u.unreadCount, PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING * 2), openSettings = {
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
settingsClicked()
userPickerState.value = AnimatedViewState.GONE
}) {
@@ -153,7 +151,7 @@ fun UserPicker(
}
@Composable
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues = PaddingValues(start = 8.dp, end = DEFAULT_PADDING), onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
Row(
Modifier
.fillMaxWidth()
@@ -164,7 +162,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues
interactionSource = remember { MutableInteractionSource() },
indication = if (!u.activeUser) LocalIndication.current else null
)
.padding(padding),
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -213,7 +211,6 @@ fun UserProfileRow(u: User) {
u.displayName,
modifier = Modifier
.padding(start = 8.dp, end = 8.dp),
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
)
}
@@ -221,26 +218,24 @@ fun UserProfileRow(u: User) {
@Composable
private fun SettingsPickerItem(onClick: () -> Unit) {
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING * 2.2f, end = DEFAULT_PADDING * 2), minHeight = 68.dp) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING * 1.5f))
Text(
text,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
color = MaterialTheme.colors.onBackground,
)
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}
@Composable
private fun CancelPickerItem(onClick: () -> Unit) {
SectionItemViewSpaceBetween(onClick, padding = PaddingValues(start = DEFAULT_PADDING * 2.2f, end = DEFAULT_PADDING * 2), minHeight = 68.dp) {
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
val text = generalGetString(R.string.cancel_verb)
Icon(Icons.Outlined.Close, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
Spacer(Modifier.width(DEFAULT_PADDING * 1.5f))
Text(
text,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
color = MaterialTheme.colors.onBackground,
)
Icon(Icons.Outlined.Close, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
}
}

View File

@@ -42,9 +42,9 @@ fun DatabaseEncryptionView(m: ChatModel) {
val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") }
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") }
val newKey = rememberSaveable { mutableStateOf("") }
val confirmNewKey = rememberSaveable { mutableStateOf("") }
@@ -89,7 +89,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(newKey.value)
DatabaseUtils.setDatabaseKey(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
@@ -150,7 +150,7 @@ fun DatabaseEncryptionLayout(
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(R.string.remove_passphrase),
onConfirm = {
DatabaseUtils.ksDatabasePassword.remove()
DatabaseUtils.removeDatabaseKey()
setUseKeychain(false, useKeychain, prefs)
storedKey.value = false
},
@@ -366,7 +366,7 @@ fun PassphraseField(
showStrength: Boolean = false,
isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(),
dependsOn: State<Any?>? = null,
dependsOn: MutableState<String>? = null,
) {
var valid by remember { mutableStateOf(validKey(key.value)) }
var showKey by remember { mutableStateOf(false) }
@@ -479,7 +479,7 @@ private fun passphraseEntropy(s: String): Double {
return s.length * log2(poolSize.toDouble())
}
enum class PassphraseStrength(val color: Color) {
private enum class PassphraseStrength(val color: Color) {
VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen);
companion object {
@@ -522,4 +522,4 @@ fun PreviewDatabaseEncryptionLayout() {
onConfirmEncrypt = {},
)
}
}
}

View File

@@ -38,7 +38,7 @@ fun DatabaseErrorView(
) {
val progressIndicator = remember { mutableStateOf(false) }
val dbKey = remember { mutableStateOf("") }
var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) }
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
val context = LocalContext.current
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
@@ -49,7 +49,7 @@ fun DatabaseErrorView(
}
fun saveAndRunChatOnClick() {
DatabaseUtils.ksDatabasePassword.set(dbKey.value)
DatabaseUtils.setDatabaseKey(dbKey.value)
storedDBKey = dbKey.value
appPreferences.storeDBPassphrase.set(true)
useKeychain = true

View File

@@ -52,7 +52,7 @@ fun DatabaseView(
) {
val context = LocalContext.current
val progressIndicator = remember { mutableStateOf(false) }
val runChat = remember { m.chatRunning }
val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) }
val prefs = m.controller.appPrefs
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
@@ -76,7 +76,7 @@ fun DatabaseView(
) {
DatabaseLayout(
progressIndicator.value,
runChat.value != false,
runChat.value,
m.chatDbChanged.value,
useKeychain.value,
m.chatDbEncrypted.value,
@@ -160,7 +160,7 @@ fun DatabaseLayout(
AppBarTitle(stringResource(R.string.your_chat_database))
SectionView(stringResource(R.string.messages_section_title).uppercase()) {
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected) }
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
}
SectionTextFooter(
remember(currentUser?.displayName) {
@@ -388,7 +388,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive)
}
private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
withApi {
try {
if (chatDbChanged.value) {
@@ -417,7 +417,7 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
}
}
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.stop_chat_question),
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
@@ -434,7 +434,7 @@ private fun exportProhibitedAlert() {
)
}
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
if (m.controller.appPrefs.performLA.get()) {
authenticate(
generalGetString(R.string.auth_stop_chat),
@@ -442,13 +442,12 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context:
context as FragmentActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
LAResult.Success, LAResult.Unavailable -> {
stopChat(m, runChat, context)
}
is LAResult.Error -> {
runChat.value = true
}
is LAResult.Failed -> {
LAResult.Failed -> {
runChat.value = true
}
}
@@ -459,7 +458,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context:
}
}
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
withApi {
try {
m.controller.apiStopChat()
@@ -593,7 +592,7 @@ private fun importArchive(
try {
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
m.controller.apiImportArchive(config)
DatabaseUtils.ksDatabasePassword.remove()
DatabaseUtils.removeDatabaseKey()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
@@ -648,7 +647,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
try {
m.controller.apiDeleteStorage()
m.chatDbDeleted.value = true
DatabaseUtils.ksDatabasePassword.remove()
DatabaseUtils.removeDatabaseKey()
m.controller.appPrefs.storeDBPassphrase.set(true)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile))

View File

@@ -3,15 +3,13 @@ package chat.simplex.app.views.helpers
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Dialog
import chat.simplex.app.R
@@ -35,14 +33,13 @@ class AlertManager {
text: String? = null,
buttons: @Composable () -> Unit,
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = buttons,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
title = { Text(title) },
text = alertText,
buttons = buttons
)
}
}
@@ -54,21 +51,22 @@ class AlertManager {
) {
showAlert {
Dialog(onDismissRequest = this::hideAlert) {
Column(
Modifier
.background(if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp)))
.padding(bottom = DEFAULT_PADDING)
) {
Text(
title,
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
textAlign = TextAlign.Center,
fontSize = 20.sp
Column(Modifier.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)) {
Text(title,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = if (text == null) DEFAULT_PADDING else DEFAULT_PADDING_HALF),
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
)
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
if (text != null) {
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = HighOrLowlight)
if (text != null) {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
Text(
text,
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
fontSize = 14.sp,
)
}
}
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
buttons()
}
}
@@ -86,28 +84,24 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
text = alertText(text),
buttons = {
Row (
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
title = { Text(title) },
text = alertText,
confirmButton = {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
},
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
dismissButton = {
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
}) { Text(dismissText) }
}
)
}
}
@@ -122,15 +116,16 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
text = alertText(text),
title = { Text(title) },
text = alertText,
buttons = {
Column(
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp),
horizontalAlignment = Alignment.CenterHorizontally
horizontalAlignment = Alignment.End
) {
TextButton(onClick = {
onDismiss?.invoke()
@@ -139,11 +134,9 @@ class AlertManager {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
}
}
@@ -152,24 +145,18 @@ class AlertManager {
title: String, text: String? = null,
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null
) {
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
showAlert {
AlertDialog(
onDismissRequest = this::hideAlert,
title = alertTitle(title),
text = alertText(text),
buttons = {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.Center
) {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = Color.Unspecified) }
}
},
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
shape = RoundedCornerShape(corner = CornerSize(25.dp))
title = { Text(title) },
text = alertText,
confirmButton = {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText) }
}
)
}
}
@@ -190,30 +177,3 @@ class AlertManager {
val shared = AlertManager()
}
}
private fun alertTitle(title: String): (@Composable () -> Unit)? {
return {
Text(
title,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 20.sp
)
}
}
private fun alertText(text: String?): (@Composable () -> Unit)? {
return if (text == null) {
null
} else {
({
Text(
text,
Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 16.sp,
color = HighOrLowlight
)
})
}
}

View File

@@ -1,31 +1,28 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.views.newchat.ActionButton
sealed class AttachmentOption {
object CameraPhoto: AttachmentOption()
object GalleryImage: AttachmentOption()
object GalleryVideo: AttachmentOption()
object File: AttachmentOption()
object TakePhoto: AttachmentOption()
object PickImage: AttachmentOption()
object PickVideo: AttachmentOption()
object PickFile: AttachmentOption()
}
@Composable
fun ChooseAttachmentView(
attachmentOption: MutableState<AttachmentOption?>,
allowVideoAttachment: Boolean,
hide: () -> Unit
) {
Box(
@@ -42,20 +39,22 @@ fun ChooseAttachmentView(
.padding(horizontal = 8.dp, vertical = 30.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
ActionButton(Modifier.fillMaxWidth(0.25f), null, stringResource(R.string.use_camera_button), icon = painterResource(R.drawable.ic_camera_enhance)) {
attachmentOption.value = AttachmentOption.CameraPhoto
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) {
attachmentOption.value = AttachmentOption.TakePhoto
hide()
}
ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(R.string.gallery_image_button), icon = painterResource(R.drawable.ic_add_photo)) {
attachmentOption.value = AttachmentOption.GalleryImage
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
attachmentOption.value = AttachmentOption.PickImage
hide()
}
ActionButton(Modifier.fillMaxWidth(0.50f), null, stringResource(R.string.gallery_video_button), icon = painterResource(R.drawable.ic_smart_display)) {
attachmentOption.value = AttachmentOption.GalleryVideo
hide()
if (allowVideoAttachment) {
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Videocam) {
attachmentOption.value = AttachmentOption.PickVideo
hide()
}
}
ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(R.string.choose_file), icon = painterResource(R.drawable.ic_note_add)) {
attachmentOption.value = AttachmentOption.File
ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) {
attachmentOption.value = AttachmentOption.PickFile
hide()
}
}

View File

@@ -6,15 +6,13 @@ import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
@Composable
fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) {
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
Column(
Modifier
.fillMaxWidth()
@@ -30,7 +28,7 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
NavigationButtonBack(onButtonClicked = close)
NavigationButtonBack(close)
Row {
endButtons()
}
@@ -56,19 +54,6 @@ fun AppBarTitle(title: String, withPadding: Boolean = true) {
)
}
@Composable
fun ColumnScope.AppBarTitleCentered(title: String, withPadding: Boolean = true) {
Text(
title,
Modifier
.padding(bottom = if (withPadding) DEFAULT_PADDING * 1.5f else 0.dp)
.align(Alignment.CenterHorizontally),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1,
color = MaterialTheme.colors.primary
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,

View File

@@ -3,7 +3,6 @@ package chat.simplex.app.views.helpers
import android.util.Log
import chat.simplex.app.*
import chat.simplex.app.model.AppPreferences
import chat.simplex.app.model.SharedPreference
import chat.simplex.app.views.usersettings.Cryptor
import kotlinx.serialization.*
import java.io.File
@@ -17,36 +16,30 @@ object DatabaseUtils {
}
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
private const val APP_PASSWORD_ALIAS: String = "appPassword"
val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase)
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
fun get(): String? {
return cryptor.decryptData(
passphrase.get()?.toByteArrayFromBase64() ?: return null,
initVector.get()?.toByteArrayFromBase64() ?: return null,
alias,
)
}
fun set(key: String) {
val data = cryptor.encryptText(key, alias)
passphrase.set(data.first.toBase64String())
initVector.set(data.second.toBase64String())
}
fun remove() {
cryptor.deleteKey(alias)
passphrase.set(null)
initVector.set(null)
}
}
private fun hasDatabase(rootDir: String): Boolean =
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
fun getDatabaseKey(): String? {
return cryptor.decryptData(
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
DATABASE_PASSWORD_ALIAS,
)
}
fun setDatabaseKey(key: String) {
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
}
fun removeDatabaseKey() {
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
appPreferences.encryptedDBPassphrase.set(null)
appPreferences.initializationVectorDBPassphrase.set(null)
}
fun useDatabaseKey(): String {
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
var dbKey = ""
@@ -54,10 +47,10 @@ object DatabaseUtils {
if (useKeychain) {
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
setDatabaseKey(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
} else {
dbKey = ksDatabasePassword.get() ?: ""
dbKey = getDatabaseKey() ?: ""
}
}
return dbKey
@@ -108,4 +101,4 @@ data class UpMigration(
sealed class MTRError {
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
}
}

View File

@@ -3,15 +3,10 @@ package chat.simplex.app.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.ZeroCornerSize
import androidx.compose.foundation.text.*
import androidx.compose.material.*
import androidx.compose.material.TextFieldDefaults.indicatorLine
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Error
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@@ -19,18 +14,13 @@ import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.*
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.views.database.PassphraseStrength
import chat.simplex.app.views.database.validKey
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@Composable
@@ -120,109 +110,3 @@ fun DefaultBasicTextField(
}
)
}
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun DefaultConfigurableTextField(
state: MutableState<TextFieldValue>,
placeholder: String,
modifier: Modifier = Modifier,
showPasswordStrength: Boolean = false,
isValid: (String) -> Boolean,
keyboardActions: KeyboardActions = KeyboardActions(),
keyboardType: KeyboardType = KeyboardType.Text,
dependsOn: State<Any?>? = null,
) {
var valid by remember { mutableStateOf(validKey(state.value.text)) }
var showKey by remember { mutableStateOf(false) }
val icon = if (valid) {
if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
} else Icons.Outlined.Error
val iconColor = if (valid) {
if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else HighOrLowlight
} else Color.Red
val keyboard = LocalSoftwareKeyboardController.current
val keyboardOptions = KeyboardOptions(
imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done,
autoCorrect = keyboardType != KeyboardType.Password,
keyboardType = keyboardType
)
val enabled = true
val colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Unspecified,
textColor = MaterialTheme.colors.onBackground,
focusedIndicatorColor = Color.Unspecified,
unfocusedIndicatorColor = Color.Unspecified,
)
val color = MaterialTheme.colors.onBackground
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
value = state.value,
modifier = modifier
.fillMaxWidth()
.background(colors.backgroundColor(enabled).value, shape)
.indicatorLine(enabled, false, interactionSource, colors)
.defaultMinSize(
minWidth = TextFieldDefaults.MinWidth,
minHeight = TextFieldDefaults.MinHeight
),
onValueChange = {
state.value = it
},
cursorBrush = SolidColor(colors.cursorColor(false).value),
visualTransformation = if (showKey || keyboardType != KeyboardType.Password)
VisualTransformation.None
else
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
keyboardOptions = keyboardOptions,
keyboardActions = KeyboardActions(onDone = {
keyboard?.hide()
keyboardActions.onDone?.invoke(this)
}),
singleLine = true,
textStyle = TextStyle.Default.copy(
color = color,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
interactionSource = interactionSource,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = state.value.text,
innerTextField = innerTextField,
placeholder = { Text(placeholder, color = HighOrLowlight) },
singleLine = true,
enabled = enabled,
isError = !valid,
trailingIcon = {
if (keyboardType == KeyboardType.Password || !valid) {
IconButton({ showKey = !showKey }) {
Icon(icon, null, tint = iconColor)
}
}
},
interactionSource = interactionSource,
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
visualTransformation = VisualTransformation.None,
colors = colors
)
}
)
LaunchedEffect(Unit) {
launch {
snapshotFlow { state.value }
.distinctUntilChanged()
.collect {
valid = isValid(it.text)
}
}
launch {
snapshotFlow { dependsOn?.value }
.distinctUntilChanged()
.collect {
valid = isValid(state.value.text)
}
}
}
}

View File

@@ -1,58 +0,0 @@
package chat.simplex.app.views.helpers
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.*
@Composable
fun DefaultDropdownMenu(
showMenu: MutableState<Boolean>,
offset: DpOffset = DpOffset(0.dp, 0.dp),
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background),
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
) {
DropdownMenu(
expanded = showMenu.value,
onDismissRequest = { showMenu.value = false },
Modifier
.widthIn(min = 250.dp)
.padding(vertical = 4.dp),
offset = offset,
) {
dropdownMenuItems?.invoke()
}
}
}
@Composable
fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu(
expanded: MutableState<Boolean>,
modifier: Modifier = Modifier,
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background),
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
) {
ExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp).then(modifier),
expanded = expanded.value,
onDismissRequest = {
expanded.value = false
}
) {
dropdownMenuItems?.invoke()
}
}
}

View File

@@ -45,10 +45,10 @@ fun DefaultTopAppBar(
}
@Composable
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
fun NavigationButtonBack(onButtonClicked: () -> Unit) {
IconButton(onButtonClicked) {
Icon(
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else HighOrLowlight
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = MaterialTheme.colors.primary
)
}
}

View File

@@ -11,7 +11,7 @@ import kotlinx.serialization.encoding.Encoder
sealed class SharedContent {
data class Text(val text: String): SharedContent()
data class Media(val text: String, val uris: List<Uri>): SharedContent()
data class Images(val text: String, val uris: List<Uri>): SharedContent()
data class File(val text: String, val uri: Uri): SharedContent()
}

View File

@@ -15,7 +15,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun <T> ExposedDropDownSettingRow(
@@ -32,7 +33,7 @@ fun <T> ExposedDropDownSettingRow(
Modifier.fillMaxWidth().padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val expanded = remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
if (icon != null) {
Icon(
@@ -45,9 +46,9 @@ fun <T> ExposedDropDownSettingRow(
Text(title, Modifier.weight(1f), color = if (enabled.value) Color.Unspecified else HighOrLowlight)
ExposedDropdownMenuBox(
expanded = expanded.value,
expanded = expanded,
onExpandedChange = {
expanded.value = !expanded.value && enabled.value
expanded = !expanded && enabled.value
}
) {
Row(
@@ -65,28 +66,29 @@ fun <T> ExposedDropDownSettingRow(
)
Spacer(Modifier.size(12.dp))
Icon(
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.icon_descr_more_button),
tint = HighOrLowlight
)
}
DefaultExposedDropdownMenu(
ExposedDropdownMenu(
modifier = Modifier.widthIn(min = 200.dp),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onSelected(selectionOption.first)
expanded.value = false
},
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
expanded = false
}
) {
Text(
selectionOption.second + (if (label != null) " $label" else ""),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
)
}
}

View File

@@ -18,7 +18,8 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.CallSuper
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.PhotoCamera
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
@@ -173,18 +174,7 @@ fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLaunche
@Composable
fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivityResultLauncher<String, List<Uri>> =
rememberLauncherForActivityResult(contract = GetMultipleContentsAndMimeTypes(), cb)
class GetMultipleContentsAndMimeTypes: ActivityResultContracts.GetMultipleContents() {
override fun createIntent(context: Context, input: String): Intent {
val mimeTypes = input.split(";")
return super.createIntent(context, mimeTypes[0]).apply {
if (mimeTypes.isNotEmpty()) {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
}
}
}
}
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetMultipleContents(), cb)
fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
try {
@@ -258,7 +248,7 @@ fun GetImageBottomSheet(
}
}
}
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Image) {
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
try {
galleryLauncher.launch(0)
} catch (e: ActivityNotFoundException) {

View File

@@ -1,66 +1,36 @@
package chat.simplex.app.views.helpers
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.widget.Toast
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.*
import androidx.biometric.BiometricPrompt
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.LocalAuthView
import chat.simplex.app.views.usersettings.LAMode
sealed class LAResult {
object Success: LAResult()
class Error(val errString: CharSequence): LAResult()
class Failed(val errString: CharSequence? = null): LAResult()
class Unavailable(val errString: CharSequence? = null): LAResult()
}
data class LocalAuthRequest (
val title: String?,
val reason: String,
val password: String,
val completed: (LAResult) -> Unit
) {
companion object {
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { }
}
object Failed: LAResult()
object Unavailable: LAResult()
}
fun authenticate(
promptTitle: String,
promptSubtitle: String,
activity: FragmentActivity,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
completed: (LAResult) -> Unit
) {
when (usingLAMode) {
LAMode.SYSTEM -> when {
SDK_INT in 28..29 ->
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
SDK_INT > 29 ->
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
else -> completed(LAResult.Unavailable())
}
LAMode.PASSCODE -> {
val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password)))
ModalManager.shared.showCustomModal(animated = false) { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) {
close()
completed(it)
})
}
}
}
when {
SDK_INT in 28..29 ->
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
SDK_INT > 29 ->
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
else ->
completed(LAResult.Unavailable)
}
}
@@ -96,7 +66,7 @@ private fun authenticateWithBiometricManager(
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
completed(LAResult.Failed())
completed(LAResult.Failed)
}
}
)
@@ -108,7 +78,9 @@ private fun authenticateWithBiometricManager(
.build()
biometricPrompt.authenticate(promptInfo)
}
else -> completed(LAResult.Unavailable())
else -> {
completed(LAResult.Unavailable)
}
}
}
@@ -117,18 +89,6 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
)
fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.lock_not_enabled),
generalGetString(R.string.you_can_turn_on_lock)
)
fun laFailedAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.la_auth_failed),
text = generalGetString(R.string.la_could_not_be_verified)
)
}
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
generalGetString(R.string.auth_unavailable),
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)

View File

@@ -12,8 +12,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -30,21 +28,6 @@ fun SimpleButton(text: String, icon: ImageVector,
}
}
@Composable
fun SimpleButtonDecorated(text: String, icon: ImageVector,
color: Color = MaterialTheme.colors.primary,
textDecoration: TextDecoration = TextDecoration.Underline,
fontWeight: FontWeight = FontWeight.Normal,
click: () -> Unit) {
SimpleButtonFrame(click) {
Icon(
icon, text, tint = color,
modifier = Modifier.padding(end = 8.dp)
)
Text(text, style = MaterialTheme.typography.caption, fontWeight = fontWeight, color = color, textDecoration = textDecoration)
}
}
@Composable
fun SimpleButton(
text: String, icon: ImageVector,
@@ -78,9 +61,9 @@ fun SimpleButtonIconEnded(
}
@Composable
fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled: Boolean = false, content: @Composable () -> Unit) {
fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) {
Surface(shape = RoundedCornerShape(20.dp)) {
val modifier = if (disabled) modifier else modifier.clickable { click() }
val modifier = if (disabled) Modifier else Modifier.clickable { click() }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier.padding(8.dp)

View File

@@ -640,20 +640,4 @@ fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}
}
}
}
}
@Composable
fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) {
val context = LocalContext.current
DisposableEffect(Unit) {
always()
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
val orientation = activity.resources.configuration.orientation
onDispose {
whenDispose()
if (orientation != activity.resources.configuration.orientation) {
whenRotate()
}
}
}
}
}

View File

@@ -1,22 +0,0 @@
package chat.simplex.app.views.localauth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.*
@Composable
fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
val passcode = rememberSaveable { mutableStateOf("") }
PasscodeView(passcode, authRequest.title ?: stringResource(R.string.la_enter_app_passcode), authRequest.reason, stringResource(R.string.submit_passcode),
submit = {
val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(R.string.incorrect_passcode))
authRequest.completed(r)
},
cancel = {
authRequest.completed(LAResult.Error(generalGetString(R.string.authentication_cancelled)))
})
}

View File

@@ -1,100 +0,0 @@
package chat.simplex.app.views.localauth
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleButton
import chat.simplex.app.views.helpers.*
@Composable
fun PasscodeView(
passcode: MutableState<String>,
title: String,
reason: String? = null,
submitLabel: String,
submitEnabled: ((String) -> Boolean)? = null,
submit: () -> Unit,
cancel: () -> Unit,
) {
@Composable
fun VerticalLayout() {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(title, style = MaterialTheme.typography.h1)
if (reason != null) {
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
}
}
PasscodeEntry(passcode, true)
Row {
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
Spacer(Modifier.size(20.dp))
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
}
}
}
@Composable
fun HorizontalLayout() {
Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) {
Column(
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(title, style = MaterialTheme.typography.h1)
if (reason != null) {
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
}
}
PasscodeEntry(passcode, false)
}
Column(
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween
) {
// Just to fill space to correctly calculate the height
Column {
Text("", style = MaterialTheme.typography.h1)
if (reason != null) {
Text("", Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
}
PasscodeView(remember { mutableStateOf("") })
}
BoxWithConstraints {
val s = minOf(maxWidth, maxHeight) / 3.5f
Column(
Modifier.padding(start = 30.dp).height(s * 3),
verticalArrangement = Arrangement.SpaceEvenly
) {
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
}
}
}
}
}
if (LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
VerticalLayout()
} else {
HorizontalLayout()
}
}

View File

@@ -1,183 +0,0 @@
package chat.simplex.app.views.localauth
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.outlined.Backspace
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.ui.theme.HighOrLowlight
@Composable
fun PasscodeEntry(
password: MutableState<String>,
vertical: Boolean,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
PasscodeView(password)
BoxWithConstraints {
if (vertical) {
VerticalPasswordGrid(password)
} else {
HorizontalPasswordGrid(password)
}
}
}
}
@Composable
fun PasscodeView(password: MutableState<String>) {
var showPasscode by rememberSaveable { mutableStateOf(false) }
Text(
if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) },
Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode },
style = MaterialTheme.typography.body1
)
}
@Composable
private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState<String>) {
val s = minOf(maxWidth, maxHeight) / 4 - 1.dp
Column(Modifier.width(IntrinsicSize.Min)) {
DigitsRow(s, 1, 2, 3, password)
Divider()
DigitsRow(s, 4, 5, 6, password)
Divider()
DigitsRow(s, 7, 8, 9, password)
Divider()
Row(Modifier.requiredHeight(s)) {
PasswordEdit(s, Icons.Default.Close) {
password.value = ""
}
VerticalDivider()
PasswordDigit(s, 0, password)
VerticalDivider()
PasswordEdit(s, Icons.Outlined.Backspace) {
password.value = password.value.dropLast(1)
}
}
}
}
@Composable
private fun BoxWithConstraintsScope.HorizontalPasswordGrid(password: MutableState<String>) {
val s = minOf(maxWidth, maxHeight) / 3.5f - 1.dp
Column(Modifier.width(IntrinsicSize.Min)) {
Row(Modifier.height(IntrinsicSize.Min)) {
DigitsRow(s, 1, 2, 3, password);
VerticalDivider()
PasswordEdit(s, Icons.Default.Close) {
password.value = ""
}
}
Divider()
Row(Modifier.height(IntrinsicSize.Min)) {
DigitsRow(s, 4, 5, 6, password)
VerticalDivider()
PasswordDigit(s, 0, password)
}
Divider()
Row(Modifier.height(IntrinsicSize.Min)) {
DigitsRow(s, 7, 8, 9, password)
VerticalDivider()
PasswordEdit(s, Icons.Outlined.Backspace) {
password.value = password.value.dropLast(1)
}
}
}
}
private fun splitPassword(showPassword: Boolean, password: String): String {
val n = if (password.length < 8) 8 else 4
return password.mapIndexed { index, c -> (if (showPassword) c.toString() else "") + (if ((index + 1) % n == 0) " " else "") }.joinToString("")
}
@Composable
private fun DigitsRow(size: Dp, d1: Int, d2: Int, d3: Int, password: MutableState<String>) {
Row(Modifier.height(size)) {
PasswordDigit(size, d1, password)
VerticalDivider()
PasswordDigit(size, d2, password)
VerticalDivider()
PasswordDigit(size, d3, password)
}
}
@Composable
private fun PasswordDigit(size: Dp, d: Int, password: MutableState<String>) {
val s = d.toString()
return PasswordButton(size, action = {
if (password.value.length < 16) {
password.value += s
}
}) {
Text(
s,
style = TextStyle(
fontWeight = FontWeight.Normal,
fontSize = 30.sp,
letterSpacing = (-0.5).sp
),
color = HighOrLowlight
)
}
}
@Composable
private fun PasswordEdit(size: Dp, image: ImageVector, action: () -> Unit) {
PasswordButton(size, action) {
Icon(image, null, tint = HighOrLowlight)
}
}
@Composable
private fun PasswordButton(size: Dp, action: () -> Unit, content: @Composable BoxScope.() -> Unit) {
return Box(
Modifier.size(size)
.background(MaterialTheme.colors.background, RoundedCornerShape(50))
.clickable { action() },
contentAlignment = Alignment.Center
) {
content()
}
}
@Composable
fun VerticalDivider(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha),
thickness: Dp = 1.dp,
startIndent: Dp = 0.dp
) {
val indentMod = if (startIndent.value != 0f) {
Modifier.padding(top = startIndent)
} else {
Modifier
}
val targetThickness = if (thickness == Dp.Hairline) {
(1f / LocalDensity.current.density).dp
} else {
thickness
}
Box(
modifier.then(indentMod)
.fillMaxHeight()
.width(targetThickness)
.background(color = color)
)
}
private const val DividerAlpha = 0.12f

View File

@@ -1,48 +0,0 @@
package chat.simplex.app.views.localauth
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import chat.simplex.app.R
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun SetAppPasscodeView(
submit: () -> Unit,
cancel: () -> Unit,
close: () -> Unit
) {
val passcode = rememberSaveable { mutableStateOf("") }
var enteredPassword by rememberSaveable { mutableStateOf("") }
var confirming by rememberSaveable { mutableStateOf(false) }
@Composable
fun SetPasswordView(title: String, submitLabel: String, submitEnabled: (((String) -> Boolean))? = null, submit: () -> Unit) {
PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) {
close()
cancel()
}
}
if (confirming) {
SetPasswordView(
generalGetString(R.string.confirm_passcode),
generalGetString(R.string.confirm_verb),
submitEnabled = { pwd -> pwd == enteredPassword }
) {
if (passcode.value == enteredPassword) {
ksAppPassword.set(passcode.value)
enteredPassword = ""
passcode.value = ""
close()
submit()
}
}
} else {
SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) {
enteredPassword = passcode.value
passcode.value = ""
confirming = true
}
}
}

View File

@@ -88,14 +88,13 @@ fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit)
}
@Composable
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String, centered: Boolean = false) {
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String) {
if (chatModelIncognito) {
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start
verticalAlignment = Alignment.CenterVertically
) {
Icon(
if (supportedIncognito) Icons.Filled.TheaterComedy else Icons.Outlined.Info,
@@ -103,15 +102,14 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean
tint = if (supportedIncognito) Indigo else WarningOrange,
modifier = Modifier.padding(end = 10.dp).size(20.dp)
)
Text(onText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2)
Text(onText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
}
} else {
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start
verticalAlignment = Alignment.CenterVertically
) {
Icon(
Icons.Outlined.Info,
@@ -119,7 +117,7 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean
tint = HighOrLowlight,
modifier = Modifier.padding(end = 10.dp).size(20.dp)
)
Text(offText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2)
Text(offText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
}
}
}

View File

@@ -1,5 +1,6 @@
package chat.simplex.app.views.newchat
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -14,8 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -27,7 +26,6 @@ import chat.simplex.app.views.chat.group.AddGroupMembersView
import chat.simplex.app.views.chatlist.setGroupMembers
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.usersettings.DeleteImageButton
import chat.simplex.app.views.usersettings.EditImageButton
import com.google.accompanist.insets.ProvideWindowInsets
@@ -38,6 +36,7 @@ import kotlinx.coroutines.launch
@Composable
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
AddGroupLayout(
chatModel.incognito.value,
createGroup = { groupProfile ->
withApi {
val groupInfo = chatModel.controller.apiNewGroup(groupProfile)
@@ -58,11 +57,11 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
}
@Composable
fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
val displayName = rememberSaveable { mutableStateOf("") }
val fullName = rememberSaveable { mutableStateOf("") }
val displayName = remember { mutableStateOf("") }
val fullName = remember { mutableStateOf("") }
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
val focusRequester = remember { FocusRequester() }
@@ -89,8 +88,14 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
.verticalScroll(rememberScrollState())
.padding(horizontal = DEFAULT_PADDING)
) {
AppBarTitleCentered(stringResource(R.string.create_secret_group_title))
ReadableText(R.string.group_is_decentralized, TextAlign.Center)
AppBarTitle(stringResource(R.string.create_secret_group_title), false)
Text(stringResource(R.string.group_is_decentralized))
InfoAboutIncognito(
chatModelIncognito,
false,
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
generalGetString(R.string.group_main_profile_sent)
)
Box(
Modifier
.fillMaxWidth()
@@ -99,7 +104,7 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(108.dp, image = profileImage.value)
ProfileImage(size = 192.dp, image = profileImage.value)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
@@ -107,28 +112,24 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
}
}
}
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.group_display_name_field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.group_display_name_field),
Modifier.padding(bottom = 3.dp)
)
ProfileNameField(displayName, focusRequester)
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
Text(
errorText,
fontSize = 15.sp,
color = MaterialTheme.colors.error
)
Spacer(Modifier.height(3.dp))
Text(
stringResource(R.string.group_full_name_field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
Modifier.padding(bottom = 5.dp)
)
ProfileNameField(fullName, "")
ProfileNameField(fullName)
Spacer(Modifier.height(8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
if (enabled) {
@@ -162,7 +163,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
) {
Surface(shape = RoundedCornerShape(20.dp)) {
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color, fontWeight = FontWeight.Bold)
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color)
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = color)
}
}
@@ -174,6 +175,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
fun PreviewAddGroupLayout() {
SimpleXTheme {
AddGroupLayout(
chatModelIncognito = false,
createGroup = {},
close = {}
)

View File

@@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource
@@ -212,52 +211,6 @@ fun ActionButton(
}
}
@Composable
fun ActionButton(
modifier: Modifier,
text: String?,
comment: String?,
icon: Painter,
tint: Color = MaterialTheme.colors.primary,
disabled: Boolean = false,
click: () -> Unit = {}
) {
Surface(modifier, shape = RoundedCornerShape(18.dp)) {
Column(
Modifier
.fillMaxWidth()
.clickable(onClick = click)
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val tint = if (disabled) HighOrLowlight else tint
Icon(
icon, text,
tint = tint,
modifier = Modifier
.size(40.dp)
.padding(bottom = 8.dp)
)
if (text != null) {
Text(
text,
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold,
color = tint,
modifier = Modifier.padding(bottom = 4.dp)
)
}
if (comment != null) {
Text(
comment,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body2
)
}
}
}
}
@Preview
@Composable
private fun PreviewNewChatSheet() {

View File

@@ -11,7 +11,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -56,13 +55,8 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
}
@Composable
fun ReadableText(@StringRes stringResId: Int, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
}
@Composable
fun ReadableText(text: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
Text(text, modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
fun ReadableText(@StringRes stringResId: Int) {
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(bottom = 12.dp), lineHeight = 22.sp)
}
@Preview(showBackground = true)
@@ -76,4 +70,4 @@ fun PreviewHowItWorks() {
SimpleXTheme {
HowItWorks(user = null)
}
}
}

View File

@@ -12,8 +12,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
@@ -25,25 +23,22 @@ import chat.simplex.app.views.usersettings.changeNotificationsMode
@Composable
fun SetNotificationsMode(m: ChatModel) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(vertical = 14.dp)
) {
//CloseSheetBar(null)
AppBarTitleCentered(stringResource(R.string.onboarding_notifications_mode_title))
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
Text(stringResource(R.string.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
Spacer(Modifier.height(DEFAULT_PADDING * 2f))
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(20.dp)
) {
AppBarTitle(stringResource(R.string.onboarding_notifications_mode_title), false)
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
Text(stringResource(R.string.onboarding_notifications_mode_subtitle))
Spacer(Modifier.padding(DEFAULT_PADDING_HALF))
NotificationButton(currentMode, NotificationsMode.OFF, R.string.onboarding_notifications_mode_off, R.string.onboarding_notifications_mode_off_desc)
NotificationButton(currentMode, NotificationsMode.PERIODIC, R.string.onboarding_notifications_mode_periodic, R.string.onboarding_notifications_mode_periodic_desc)
NotificationButton(currentMode, NotificationsMode.SERVICE, R.string.onboarding_notifications_mode_service, R.string.onboarding_notifications_mode_service_desc)
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage) {
changeNotificationsMode(currentMode.value, m)
}
}
@@ -56,24 +51,18 @@ private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mod
TextButton(
onClick = { currentMode.value = mode },
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)),
shape = RoundedCornerShape(35.dp),
shape = RoundedCornerShape(15.dp),
) {
Column(Modifier.padding(horizontal = 14.dp).padding(top = 4.dp, bottom = 8.dp)) {
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
Text(
stringResource(title),
style = MaterialTheme.typography.h2,
fontWeight = FontWeight.Medium,
color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight,
modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally),
textAlign = TextAlign.Center
)
Text(annotatedStringResource(description),
Modifier.align(Alignment.CenterHorizontally),
color = MaterialTheme.colors.onBackground,
lineHeight = 24.sp,
textAlign = TextAlign.Center
modifier = Modifier.padding(bottom = 4.dp)
)
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
}
}
Spacer(Modifier.height(14.dp))
Spacer(Modifier.height(DEFAULT_PADDING))
}

View File

@@ -4,7 +4,6 @@ import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowForwardIos
@@ -45,13 +44,13 @@ fun SimpleXInfoLayout(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(start = DEFAULT_PADDING * 1.5f, end = DEFAULT_PADDING * 1.5f, top = DEFAULT_PADDING * 4,/* bottom = DEFAULT_PADDING * 4*/),
.padding(horizontal = DEFAULT_PADDING),
) {
Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) {
Box(Modifier.fillMaxWidth().padding(top = 8.dp), contentAlignment = Alignment.Center) {
SimpleXLogo()
}
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 60.dp).padding(horizontal = 48.dp), textAlign = TextAlign.Center)
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 36.dp).padding(horizontal = 48.dp), textAlign = TextAlign.Center)
InfoRow(painterResource(R.drawable.privacy), R.string.privacy_redefined, R.string.first_platform_without_user_ids, width = 80.dp)
InfoRow(painterResource(R.drawable.shield), R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
@@ -69,12 +68,11 @@ fun SimpleXInfoLayout(
Box(
Modifier
.fillMaxWidth()
.padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING), contentAlignment = Alignment.Center
.padding(bottom = 16.dp), contentAlignment = Alignment.Center
) {
SimpleButtonDecorated(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
SimpleButton(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
click = showModal { HowItWorks(user, onboardingStage) })
}
Spacer(Modifier.weight(1f))
}
}
@@ -85,7 +83,7 @@ fun SimpleXLogo() {
contentDescription = stringResource(R.string.image_descr_simplex_logo),
modifier = Modifier
.padding(vertical = DEFAULT_PADDING)
.fillMaxWidth(0.60f)
.fillMaxWidth(0.80f)
)
}
@@ -105,9 +103,9 @@ private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: I
@Composable
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
if (user == null) {
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick)
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
} else {
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick)
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
}
}
@@ -116,30 +114,16 @@ fun OnboardingActionButton(
@StringRes labelId: Int,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
border: Boolean,
onclick: (() -> Unit)?
) {
val modifier = if (border) {
Modifier
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
.padding(
horizontal = DEFAULT_PADDING * 3,
vertical = 4.dp
)
} else {
Modifier
}
SimpleButtonFrame(click = {
onclick?.invoke()
onboardingStage.value = onboarding
}, modifier) {
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
}) {
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
Icon(
Icons.Outlined.ArrowForwardIos, "next stage", tint = MaterialTheme.colors.primary,
modifier = Modifier
.padding(start = 16.dp, top = 5.dp)
.size(15.dp)
modifier = Modifier.padding(end = 8.dp)
)
}
}

View File

@@ -1,7 +1,7 @@
package chat.simplex.app.views.onboarding
import android.content.res.Configuration
import androidx.compose.foundation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -40,13 +40,11 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
}
Column(
horizontalAlignment = Alignment.Start,
modifier = Modifier.padding(bottom = 12.dp)
horizontalAlignment = Alignment.Start
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(bottom = 4.dp)
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
Text(
@@ -109,9 +107,8 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
ModalView(close = close) {
Column(
Modifier
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
.verticalScroll(rememberScrollState()),
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
@@ -310,27 +307,6 @@ private val versionDescriptions: List<VersionDescription> = listOf(
)
)
),
VersionDescription(
version = "v5.0",
features = listOf(
FeatureDescription(
icon = Icons.Outlined.UploadFile,
titleId = R.string.v5_0_large_files_support,
descrId = R.string.v5_0_large_files_support_descr
),
FeatureDescription(
icon = Icons.Outlined.Lock,
titleId = R.string.v5_0_app_passcode,
descrId = R.string.v5_0_app_passcode_descr
),
FeatureDescription(
icon = Icons.Outlined.Translate,
titleId = R.string.v5_0_polish_interface,
descrId = R.string.v5_0_polish_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
)
)
private val lastVersion = versionDescriptions.last().version

View File

@@ -12,7 +12,6 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -103,6 +102,15 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
saveCfg(newCfg)
}
fun updateSettingsDialog(action: () -> Unit) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.update_network_settings_question),
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
confirmText = generalGetString(R.string.update_network_settings_confirmation),
onConfirm = action
)
}
AdvancedNetworkSettingsLayout(
networkTCPConnectTimeout,
networkTCPTimeout,
@@ -113,10 +121,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
networkTCPKeepIntvl,
networkTCPKeepCnt,
resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults,
reset = { showUpdateNetworkSettingsDialog(::reset) },
reset = { updateSettingsDialog(::reset) },
footerDisabled = buildCfg() == currentCfg.value,
revert = { updateView(currentCfg.value) },
save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } }
save = { updateSettingsDialog { saveCfg(buildCfg()) } }
)
}
@@ -254,14 +262,14 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val expanded = rememberSaveable { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
Text(title)
ExposedDropdownMenuBox(
expanded = expanded.value,
expanded = expanded,
onExpandedChange = {
expanded.value = !expanded.value
expanded = !expanded
}
) {
Row(
@@ -277,22 +285,24 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>
)
Spacer(Modifier.size(4.dp))
Icon(
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.invite_to_group_button),
modifier = Modifier.padding(start = 8.dp),
tint = HighOrLowlight
)
}
DefaultExposedDropdownMenu(
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selection.value = selectionOption
expanded.value = false
},
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
expanded = false
}
) {
Text(
"$selectionOption $label",
@@ -313,14 +323,14 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
val expanded = remember { mutableStateOf(false) }
var expanded by remember { mutableStateOf(false) }
Text(title)
ExposedDropdownMenuBox(
expanded = expanded.value,
expanded = expanded,
onExpandedChange = {
expanded.value = !expanded.value
expanded = !expanded
}
) {
val df = DecimalFormat("#.#")
@@ -338,22 +348,24 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
)
Spacer(Modifier.size(4.dp))
Icon(
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
generalGetString(R.string.invite_to_group_button),
modifier = Modifier.padding(start = 8.dp),
tint = HighOrLowlight
)
}
DefaultExposedDropdownMenu(
expanded = expanded
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
values.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
selection.value = selectionOption
expanded.value = false
},
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
expanded = false
}
) {
Text(
"${df.format(selectionOption / 1_000_000.toDouble())} $label",
@@ -403,15 +415,6 @@ fun FooterButton(icon: ImageVector, title: String, action: () -> Unit, disabled:
}
}
fun showUpdateNetworkSettingsDialog(action: () -> Unit) {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.update_network_settings_question),
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
confirmText = generalGetString(R.string.update_network_settings_confirmation),
onConfirm = action
)
}
@Preview(showBackground = true)
@Composable
fun PreviewAdvancedNetworkSettingsLayout() {

View File

@@ -236,7 +236,6 @@ private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
"fr" to "Français",
"it" to "Italiano",
"nl" to "Nederlands",
"pl" to "Polski",
"ru" to "Русский",
"zh-CN" to "简体中文"
)

View File

@@ -43,5 +43,17 @@ fun DeveloperView(
generalGetString(R.string.developer_options)
)
SectionSpacer()
val xftpSendEnabled = m.controller.appPrefs.xftpSendEnabled
val xftpEnabled = remember { mutableStateOf(xftpSendEnabled.get()) }
SectionView(generalGetString(R.string.settings_section_title_experimenta)) {
SettingsPreferenceItem(Icons.Outlined.UploadFile, stringResource(R.string.settings_send_files_via_xftp), xftpSendEnabled, xftpEnabled) {
withApi { m.controller.apiSetXFTPConfig(m.controller.getXFTPCfg()) }
}
}
if (xftpEnabled.value) {
SectionTextFooter(generalGetString(R.string.xftp_requires_v461))
}
}
}

View File

@@ -0,0 +1,37 @@
package chat.simplex.app.views.usersettings
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.UploadFile
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.withApi
@Composable
fun ExperimentalFeaturesView(chatModel: ChatModel) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Text(
stringResource(R.string.settings_experimental_features),
style = MaterialTheme.typography.h1,
modifier = Modifier.padding(start = 16.dp, bottom = 24.dp)
)
SectionView("") {
SettingsPreferenceItem(Icons.Outlined.UploadFile, stringResource(R.string.settings_send_files_via_xftp), chatModel.controller.appPrefs.xftpSendEnabled) {
withApi {
chatModel.controller.apiSetXFTPConfig(chatModel.controller.getXFTPCfg())
}
}
}
}
}

View File

@@ -87,4 +87,4 @@ private fun HiddenProfileLayout(
}
SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password))
}
}
}

View File

@@ -1,26 +1,18 @@
package chat.simplex.app.views.usersettings
import SectionCustomFooter
import SectionDivider
import SectionItemView
import SectionItemWithValue
import SectionSpacer
import SectionTextFooter
import SectionView
import SectionViewSelectable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.*
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
@@ -51,7 +43,6 @@ fun NetworkAndServersView(
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } },
showModal = showModal,
showSettingsModal = showSettingsModal,
showCustomModal = showCustomModal,
@@ -95,7 +86,7 @@ fun NetworkAndServersView(
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
}
showUpdateNetworkSettingsDialog(
updateNetworkSettingsDialog(
title = generalGetString(R.string.update_onion_hosts_settings_question),
startsWith,
onDismiss = {
@@ -122,7 +113,7 @@ fun NetworkAndServersView(
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
}
showUpdateNetworkSettingsDialog(
updateNetworkSettingsDialog(
title = generalGetString(R.string.update_network_session_mode_question),
startsWith,
onDismiss = { sessionMode.value = prevValue }
@@ -147,7 +138,6 @@ fun NetworkAndServersView(
networkUseSocksProxy: MutableState<Boolean>,
onionHosts: MutableState<OnionHosts>,
sessionMode: MutableState<TransportSessionMode>,
proxyPort: State<Int>,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
@@ -162,14 +152,10 @@ fun NetworkAndServersView(
) {
AppBarTitle(stringResource(R.string.network_and_servers))
SectionView(generalGetString(R.string.settings_section_title_messages)) {
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) })
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> SMPServersView(m, close) })
SectionDivider()
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) })
SectionDivider()
SectionItemView {
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
}
SectionDivider()
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
@@ -180,10 +166,7 @@ fun NetworkAndServersView(
}
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
}
if (networkUseSocksProxy.value) {
SectionCustomFooter { Text(annotatedStringResource(R.string.disable_onion_hosts_when_not_supported)) }
}
Spacer(Modifier.height(16.dp))
Spacer(Modifier.height(8.dp))
SectionView(generalGetString(R.string.settings_section_title_calls)) {
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
}
@@ -193,9 +176,7 @@ fun NetworkAndServersView(
@Composable
fun UseSocksProxySwitch(
networkUseSocksProxy: MutableState<Boolean>,
proxyPort: State<Int>,
toggleSocksProxy: (Boolean) -> Unit,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
toggleSocksProxy: (Boolean) -> Unit
) {
Row(
Modifier.fillMaxWidth(),
@@ -212,19 +193,7 @@ fun UseSocksProxySwitch(
stringResource(R.string.network_socks_toggle),
tint = HighOrLowlight
)
if (networkUseSocksProxy.value) {
Row {
Text(generalGetString(R.string.network_socks_toggle_use_socks_proxy) + " (")
Text(
generalGetString(R.string.network_proxy_port).format(proxyPort.value),
Modifier.clickable { showSettingsModal { SockProxySettings(it) }() },
color = MaterialTheme.colors.primary
)
Text(")")
}
} else {
Text(stringResource(R.string.network_socks_toggle))
}
Text(stringResource(R.string.network_socks_toggle))
}
Switch(
checked = networkUseSocksProxy.value,
@@ -237,83 +206,6 @@ fun UseSocksProxySwitch(
}
}
@Composable
fun SockProxySettings(m: ChatModel) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_BOTTOM_PADDING),
) {
val defaultHostPort = remember { "localhost:9050" }
AppBarTitle(generalGetString(R.string.network_socks_proxy_settings))
val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state }
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost"))
}
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050"))
}
val save = {
withBGApi {
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
}
}
SectionView {
SectionItemView {
ResetToDefaultsButton({
showUpdateNetworkSettingsDialog {
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
val newHost = defaultHostPort.split(":").first()
val newPort = defaultHostPort.split(":").last()
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
save()
}
}, disabled = hostPort == defaultHostPort)
}
SectionDivider()
SectionItemView {
DefaultConfigurableTextField(
hostUnsaved,
stringResource(R.string.host_verb),
modifier = Modifier,
isValid = ::validHost,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
keyboardType = KeyboardType.Text,
)
}
SectionDivider()
SectionItemView {
DefaultConfigurableTextField(
portUnsaved,
stringResource(R.string.port_verb),
modifier = Modifier,
isValid = ::validPort,
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
keyboardType = KeyboardType.Number,
)
}
}
SectionCustomFooter {
NetworkSectionFooter(
revert = {
val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost"
val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050"
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
},
save = { showUpdateNetworkSettingsDialog { save() } },
revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
)
}
}
}
@Composable
private fun UseOnionHosts(
onionHosts: MutableState<OnionHosts>,
@@ -382,32 +274,7 @@ private fun SessionModePicker(
)
}
@Composable
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FooterButton(Icons.Outlined.Replay, stringResource(R.string.network_options_revert), revert, revertDisabled)
FooterButton(Icons.Outlined.Check, stringResource(R.string.network_options_save), save, saveDisabled)
}
}
// https://stackoverflow.com/a/106223
private fun validHost(s: String): Boolean {
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
return s.matches(validIp) || s.matches(validHostname)
}
// https://ihateregex.io/expr/port/
private fun validPort(s: String): Boolean {
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
return s.isNotBlank() && s.matches(validPort)
}
private fun showUpdateNetworkSettingsDialog(
private fun updateNetworkSettingsDialog(
title: String,
startsWith: String = "",
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
@@ -431,7 +298,6 @@ fun PreviewNetworkAndServersLayout() {
NetworkAndServersLayout(
developerTools = true,
networkUseSocksProxy = remember { mutableStateOf(true) },
proxyPort = remember { mutableStateOf(9050) },
showModal = { {} },
showSettingsModal = { {} },
showCustomModal = { {} },

View File

@@ -80,11 +80,6 @@ private fun PreferencesLayout(
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
}
SectionSpacer()
val allowCalls = remember(preferences) { mutableStateOf(preferences.calls.allow) }
FeatureSection(ChatFeature.Calls, allowCalls) {
applyPrefs(preferences.copy(calls = SimpleChatPreference(allow = it)))
}
SectionSpacer()
ResetSaveButtons(
reset = reset,
save = savePrefs,
@@ -102,12 +97,11 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
FeatureAllowed.values().map { it to it.text },
allowFeature,
icon = feature.icon,
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected,
onSelected = onSelected
)
}
}
SectionTextFooter(feature.allowDescription(allowFeature.value) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
SectionTextFooter(feature.allowDescription(allowFeature.value))
}
@Composable

View File

@@ -7,41 +7,22 @@ import SectionTextFooter
import SectionView
import android.view.WindowManager
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.HighOrLowlight
import chat.simplex.app.ui.theme.SimplexGreen
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView
enum class LAMode {
SYSTEM,
PASSCODE;
val text: String
get() = when (this) {
SYSTEM -> generalGetString(R.string.la_mode_system)
PASSCODE -> generalGetString(R.string.la_mode_passcode)
}
}
@Composable
fun PrivacySettingsView(
chatModel: ChatModel,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean, FragmentActivity) -> Unit
setPerformLA: (Boolean) -> Unit
) {
Column(
Modifier.fillMaxWidth(),
@@ -50,7 +31,7 @@ fun PrivacySettingsView(
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
AppBarTitle(stringResource(R.string.your_privacy))
SectionView(stringResource(R.string.settings_section_title_device)) {
ChatLockItem(chatModel, showSettingsModal, setPerformLA)
ChatLockItem(chatModel.performLA, setPerformLA)
SectionDivider()
val context = LocalContext.current
SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on ->
@@ -71,12 +52,10 @@ fun PrivacySettingsView(
SectionDivider()
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
SectionDivider()
SectionItemView {
SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it
})
}
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it
}) }
}
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning))
@@ -104,236 +83,3 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
onSelected = onSelected
)
}
private val laDelays = listOf(10, 30, 60, 180, 0)
@Composable
fun SimplexLockView(
chatModel: ChatModel,
currentLAMode: SharedPreference<LAMode>,
setPerformLA: (Boolean, FragmentActivity) -> Unit
) {
val performLA = remember { chatModel.performLA }
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
val activity = LocalContext.current as FragmentActivity
fun resetLAEnabled(onOff: Boolean) {
chatModel.controller.appPrefs.performLA.set(onOff)
chatModel.performLA.value = onOff
}
fun disableUnavailableLA() {
resetLAEnabled(false)
currentLAMode.set(LAMode.SYSTEM)
laUnavailableInstructionAlert()
}
fun toggleLAMode(toLAMode: LAMode) {
authenticate(
if (toLAMode == LAMode.SYSTEM) {
generalGetString(R.string.la_enter_app_passcode)
} else {
generalGetString(R.string.chat_lock)
},
generalGetString(R.string.change_lock_mode), activity
) { laResult ->
when (laResult) {
is LAResult.Error -> {
laFailedAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
LAResult.Success -> {
when (toLAMode) {
LAMode.SYSTEM -> {
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult ->
when (laResult) {
LAResult.Success -> {
currentLAMode.set(toLAMode)
ksAppPassword.remove()
laTurnedOnAlert()
}
is LAResult.Unavailable, is LAResult.Error -> {
laFailedAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
}
}
}
LAMode.PASSCODE -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
laLockDelay.set(30)
currentLAMode.set(toLAMode)
passcodeAlert(generalGetString(R.string.passcode_set))
},
cancel = {},
close
)
}
}
}
}
}
is LAResult.Unavailable -> disableUnavailableLA()
}
}
}
fun changeLAPassword() {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
passcodeAlert(generalGetString(R.string.passcode_changed))
}, cancel = {
passcodeAlert(generalGetString(R.string.passcode_not_changed))
}, close
)
}
}
}
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> {}
is LAResult.Unavailable -> disableUnavailableLA()
}
}
}
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
AppBarTitle(stringResource(R.string.chat_lock))
SectionView {
EnableLock(performLA) { performLAToggle ->
performLA.value = performLAToggle
chatModel.controller.appPrefs.laNoticeShown.set(true)
if (performLAToggle) {
when (currentLAMode.state.value) {
LAMode.SYSTEM -> {
setPerformLA(true, activity)
}
LAMode.PASSCODE -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
submit = {
laLockDelay.set(30)
chatModel.controller.appPrefs.performLA.set(true)
passcodeAlert(generalGetString(R.string.passcode_set))
},
cancel = {
resetLAEnabled(false)
}, close
)
}
}
}
}
} else {
setPerformLA(false, activity)
}
}
SectionDivider()
SectionItemView {
LockModeSelector(laMode) { newLAMode ->
if (laMode.value == newLAMode) return@LockModeSelector
if (chatModel.controller.appPrefs.performLA.get()) {
toggleLAMode(newLAMode)
} else {
currentLAMode.set(newLAMode)
}
}
}
if (performLA.value) {
SectionDivider()
SectionItemView {
LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) }
}
if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) {
SectionDivider()
SectionItemView({ changeLAPassword() }) {
Text(generalGetString(R.string.la_change_app_passcode))
}
}
}
}
}
}
@Composable
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
SectionItemView {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
stringResource(R.string.enable_lock), Modifier
.padding(end = 24.dp)
.fillMaxWidth()
.weight(1F)
)
Switch(
checked = performLA.value,
onCheckedChange = onCheckedChange,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
}
}
}
@Composable
private fun LockModeSelector(state: State<LAMode>, onSelected: (LAMode) -> Unit) {
val values by remember { mutableStateOf(LAMode.values().map { it to it.text }) }
ExposedDropDownSettingRow(
generalGetString(R.string.lock_mode),
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
@Composable
private fun LockDelaySelector(state: State<Int>, onSelected: (Int) -> Unit) {
val delays = remember { if (laDelays.contains(state.value)) laDelays else listOf(state.value) + laDelays }
val values by remember { mutableStateOf(delays.map { it to laDelayText(it) }) }
ExposedDropDownSettingRow(
generalGetString(R.string.lock_after),
values,
state,
icon = null,
enabled = remember { mutableStateOf(true) },
onSelected = onSelected
)
}
private fun laDelayText(t: Int): String {
val m = t / 60
val s = t % 60
return if (t == 0) {
generalGetString(R.string.la_immediately)
} else if (m == 0 || s != 0) {
// there are no options where both minutes and seconds are needed
generalGetString(R.string.la_seconds).format(s)
} else {
generalGetString(R.string.la_minutes).format(m)
}
}
private fun passcodeAlert(title: String) {
AlertManager.shared.showAlertMsg(
title = title,
text = generalGetString(R.string.la_please_remember_to_store_password)
)
}

View File

@@ -32,13 +32,12 @@ import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@Composable
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
var testing by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
ProtocolServerLayout(
SMPServerLayout(
testing,
server,
serverProtocol,
testServer = {
testing = true
scope.launch {
@@ -69,10 +68,9 @@ fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerPr
}
@Composable
private fun ProtocolServerLayout(
private fun SMPServerLayout(
testing: Boolean,
server: ServerCfg,
serverProtocol: ServerProtocol,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
@@ -88,7 +86,7 @@ private fun ProtocolServerLayout(
if (server.preset) {
PresetServer(testing, server, testServer, onUpdate, onDelete)
} else {
CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete)
CustomServer(testing, server, testServer, onUpdate, onDelete)
}
}
}
@@ -121,19 +119,12 @@ private fun PresetServer(
private fun CustomServer(
testing: Boolean,
server: ServerCfg,
serverProtocol: ServerProtocol,
testServer: () -> Unit,
onUpdate: (ServerCfg) -> Unit,
onDelete: () -> Unit,
) {
val serverAddress = remember { mutableStateOf(server.server) }
val valid = remember {
derivedStateOf {
with(parseServerAddress(serverAddress.value)) {
this?.valid == true && this.serverProtocol == serverProtocol
}
}
}
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
SectionView(
stringResource(R.string.smp_servers_your_server_address).uppercase(),
icon = Icons.Outlined.ErrorOutline,
@@ -196,9 +187,9 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent)
}
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, SMPTestFailure?> =
try {
val r = m.controller.testProtoServer(server.server)
val r = m.controller.testSMPServer(server.server)
server.copy(tested = r == null) to r
} catch (e: Exception) {
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")

View File

@@ -18,7 +18,6 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.model.*
@@ -28,19 +27,17 @@ import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.launch
@Composable
fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) {
var presetServers by remember { mutableStateOf(emptyList<String>()) }
fun SMPServersView(m: ChatModel, close: () -> Unit) {
var servers by remember {
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
}
val currServers = remember { mutableStateOf(servers) }
val testing = rememberSaveable { mutableStateOf(false) }
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
val saveDisabled = remember {
derivedStateOf {
servers.isEmpty() ||
servers == currServers.value ||
servers == m.userSMPServers.value ||
testing.value ||
!servers.all { srv ->
val address = parseServerAddress(srv.server)
@@ -50,25 +47,13 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
}
}
LaunchedEffect(Unit) {
val res = m.controller.getUserProtoServers(serverProtocol)
if (res != null) {
currServers.value = res.protoServers
presetServers = res.presetServers
if (servers.isEmpty()) {
servers = currServers.value
}
}
}
fun showServer(server: ServerCfg) {
ModalManager.shared.showModalCloseable(true) { close ->
var old by remember { mutableStateOf(server) }
val index = servers.indexOf(old)
ProtocolServerView(
SMPServerView(
m,
old,
serverProtocol,
onUpdate = { updated ->
val newServers = ArrayList(servers)
newServers.removeAt(index)
@@ -90,12 +75,11 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
ModalView(
close = {
if (saveDisabled.value) close()
else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close)
else showUnsavedChangesAlert({ saveSMPServers(servers, m, close) }, close)
},
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
) {
ProtocolServersLayout(
serverProtocol,
SMPServersLayout(
testing = testing.value,
servers = servers,
serversUnchanged = serversUnchanged.value,
@@ -113,12 +97,12 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
// No saving until something will be changed on the next screen to prevent blank servers on the list
showServer(servers.last())
}) {
Text(stringResource(R.string.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
Text(stringResource(R.string.smp_servers_enter_manually))
}
SectionItemView({
AlertManager.shared.hideAlert()
ModalManager.shared.showModalCloseable { close ->
ScanProtocolServer {
ScanSMPServer {
close()
servers = servers + it
m.userSMPServersUnsaved.value = servers
@@ -126,15 +110,15 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
}
}
) {
Text(stringResource(R.string.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
Text(stringResource(R.string.smp_servers_scan_qr))
}
val hasAllPresets = hasAllPresets(presetServers, servers, m)
val hasAllPresets = hasAllPresets(servers, m)
if (!hasAllPresets) {
SectionItemView({
AlertManager.shared.hideAlert()
servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset }
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
}) {
Text(stringResource(R.string.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground)
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
}
}
}
@@ -150,11 +134,11 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
}
},
resetServers = {
servers = currServers.value ?: emptyList()
servers = m.userSMPServers.value ?: emptyList()
m.userSMPServersUnsaved.value = null
},
saveSMPServers = {
saveServers(serverProtocol, currServers, servers, m)
saveSMPServers(servers, m)
},
showServer = ::showServer,
)
@@ -177,8 +161,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: ()
}
@Composable
private fun ProtocolServersLayout(
serverProtocol: ServerProtocol,
private fun SMPServersLayout(
testing: Boolean,
servers: List<ServerCfg>,
serversUnchanged: Boolean,
@@ -197,12 +180,12 @@ private fun ProtocolServersLayout(
.verticalScroll(rememberScrollState())
.padding(bottom = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.your_SMP_servers else R.string.your_XFTP_servers))
AppBarTitle(stringResource(R.string.your_SMP_servers))
SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.smp_servers else R.string.xftp_servers).uppercase()) {
SectionView(stringResource(R.string.smp_servers).uppercase()) {
for (srv in servers) {
SectionItemView({ showServer(srv) }, disabled = testing) {
ProtocolServerView(serverProtocol, srv, servers, testing)
SmpServerView(srv, servers, testing)
}
SectionDivider()
}
@@ -249,10 +232,10 @@ private fun ProtocolServersLayout(
}
@Composable
private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
val address = parseServerAddress(srv.server)
when {
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
else -> ShowTestStatus(srv)
}
@@ -288,12 +271,12 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<Se
}
}
private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean =
presetServers.all { hasPreset(it, servers) } ?: true
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
private fun addAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
val toAdd = ArrayList<ServerCfg>()
for (srv in presetServers) {
for (srv in m.presetSMPServers.value ?: emptyList()) {
if (!hasPreset(srv, servers)) {
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
}
@@ -330,8 +313,8 @@ private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
return copy
}
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, ProtocolTestFailure> {
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
val updatedServers = ArrayList<ServerCfg>(servers)
for ((index, server) in servers.withIndex()) {
if (server.enabled) {
@@ -348,10 +331,10 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
return fs
}
private fun saveServers(protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
withApi {
if (m.controller.setUserProtoServers(protocol, servers)) {
currServers.value = servers
if (m.controller.setUserSMPServers(servers)) {
m.userSMPServers.value = servers
m.userSMPServersUnsaved.value = null
}
afterSave()

View File

@@ -2,6 +2,7 @@ package chat.simplex.app.views.usersettings
import android.Manifest
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
@@ -16,16 +17,16 @@ import chat.simplex.app.views.newchat.QRCodeScanner
import com.google.accompanist.permissions.rememberPermissionState
@Composable
fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
LaunchedEffect(Unit) {
cameraPermissionState.launchPermissionRequest()
}
ScanProtocolServerLayout(onNext)
ScanSMPServerLayout(onNext)
}
@Composable
private fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
Column(
Modifier
.fillMaxSize()

View File

@@ -42,7 +42,7 @@ import chat.simplex.app.views.onboarding.SimpleXInfo
import chat.simplex.app.views.onboarding.WhatsNewView
@Composable
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit) {
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
val user = chatModel.currentUser.value
val stopped = chatModel.chatRunning.value == false
@@ -126,7 +126,7 @@ fun SettingsLayout(
incognito: MutableState<Boolean>,
incognitoPref: SharedPreference<Boolean>,
userDisplayName: String,
setPerformLA: (Boolean, FragmentActivity) -> Unit,
setPerformLA: (Boolean) -> Unit,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
@@ -174,7 +174,7 @@ fun SettingsLayout(
SectionDivider()
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
SectionDivider()
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped)
SectionDivider()
@@ -207,6 +207,8 @@ fun SettingsLayout(
SectionView(stringResource(R.string.settings_section_title_develop)) {
SettingsActionItem(Icons.Outlined.Code, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
SectionDivider()
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it) })
// SectionDivider()
AppVersionItem(showVersion)
}
}
@@ -292,20 +294,13 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
)
}
@Composable
fun ChatLockItem(
chatModel: ChatModel,
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
setPerformLA: (Boolean, FragmentActivity) -> Unit
) {
val performLA = remember { chatModel.performLA }
val currentLAMode = remember { chatModel.controller.appPrefs.laMode }
SectionItemView(showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }) {
@Composable fun ChatLockItem(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
SectionItemView() {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (performLA.value) Icons.Filled.Lock else Icons.Outlined.Lock,
Icons.Outlined.Lock,
contentDescription = stringResource(R.string.chat_lock),
tint = if (performLA.value) SimplexGreen else HighOrLowlight,
tint = HighOrLowlight,
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
@@ -314,7 +309,14 @@ fun ChatLockItem(
.fillMaxWidth()
.weight(1F)
)
Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = HighOrLowlight)
Switch(
checked = performLA.value,
onCheckedChange = { setPerformLA(it) },
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colors.primary,
uncheckedThumbColor = HighOrLowlight
)
)
}
}
}
@@ -515,7 +517,7 @@ private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
generalGetString(R.string.auth_log_in_using_credential),
context as FragmentActivity,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
}
)
}
@@ -536,7 +538,7 @@ fun PreviewSettingsLayout() {
incognito = remember { mutableStateOf(false) },
incognitoPref = SharedPreference({ false }, {}),
userDisplayName = "Alice",
setPerformLA = { _, _ -> },
setPerformLA = {},
showModal = { {} },
showSettingsModal = { {} },
showSettingsModalWithSearch = { },

View File

@@ -1,10 +1,14 @@
package chat.simplex.app.views.usersettings
import android.content.res.Configuration
import android.graphics.Bitmap
import android.net.Uri
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
@@ -12,20 +16,18 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.ProfileNameField
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.onboarding.ReadableText
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.launch
@@ -34,8 +36,10 @@ import kotlinx.coroutines.launch
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
val user = chatModel.currentUser.value
if (user != null) {
val editProfile = rememberSaveable { mutableStateOf(false) }
var profile by remember { mutableStateOf(user.profile.toProfile()) }
UserProfileLayout(
editProfile = editProfile,
profile = profile,
close,
saveProfile = { displayName, fullName, image ->
@@ -45,7 +49,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
chatModel.updateCurrentUser(newProfile)
profile = newProfile
}
close()
editProfile.value = false
}
}
)
@@ -54,6 +58,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
@Composable
fun UserProfileLayout(
editProfile: MutableState<Boolean>,
profile: Profile,
close: () -> Unit,
saveProfile: (String, String, String?) -> Unit,
@@ -67,7 +72,6 @@ fun UserProfileLayout(
val scrollState = rememberScrollState()
val keyboardState by getKeyboardState()
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
val focusRequester = remember { FocusRequester() }
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout(
scrimColor = Color.Black.copy(alpha = 0.12F),
@@ -83,89 +87,97 @@ fun UserProfileLayout(
sheetState = bottomSheetModalState,
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
) {
val dataUnchanged =
displayName.value == profile.displayName &&
fullName.value == profile.fullName &&
chosenImage.value == null
val closeWithAlert = {
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
close()
} else {
showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close)
}
}
ModalView(close = closeWithAlert) {
ModalView(close = close) {
Column(
Modifier
.verticalScroll(scrollState)
.padding(horizontal = DEFAULT_PADDING),
horizontalAlignment = Alignment.Start
) {
AppBarTitleCentered(stringResource(R.string.your_current_profile))
ReadableText(generalGetString(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center)
Column(
Modifier
.fillMaxWidth()
) {
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
AppBarTitle(stringResource(R.string.your_current_profile), false)
Text(
stringResource(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it),
Modifier.padding(bottom = 24.dp),
color = MaterialTheme.colors.onBackground,
lineHeight = 22.sp
)
if (editProfile.value) {
Column(
Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f))
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
DeleteImageButton { profileImage.value = null }
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
contentAlignment = Alignment.Center
) {
Box(contentAlignment = Alignment.TopEnd) {
Box(contentAlignment = Alignment.Center) {
ProfileImage(192.dp, profileImage.value)
EditImageButton { scope.launch { bottomSheetModalState.show() } }
}
if (profileImage.value != null) {
DeleteImageButton { profileImage.value = null }
}
}
}
}
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.display_name__field),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
Box {
if (!isValidDisplayName(displayName.value)) {
Icon(Icons.Outlined.Info, tint = Color.Red, contentDescription = stringResource(R.string.display_name_cannot_contain_whitespace))
}
ProfileNameTextField(displayName)
}
ProfileNameTextField(fullName)
Row {
TextButton(stringResource(R.string.cancel_verb)) {
displayName.value = profile.displayName
fullName.value = profile.fullName
profileImage.value = profile.image
editProfile.value = false
}
Spacer(Modifier.padding(horizontal = 8.dp))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val saveModifier: Modifier
val saveColor: Color
if (enabled) {
saveModifier = Modifier
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
saveColor = MaterialTheme.colors.primary
} else {
saveModifier = Modifier
saveColor = HighOrLowlight
}
Text(
stringResource(R.string.no_spaces),
fontSize = 16.sp,
color = Color.Red
stringResource(R.string.save_and_notify_contacts),
modifier = saveModifier,
color = saveColor
)
}
}
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
Spacer(Modifier.height(DEFAULT_PADDING))
Text(
stringResource(R.string.full_name__field),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(fullName)
Spacer(Modifier.height(DEFAULT_PADDING))
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val saveModifier: Modifier
val saveColor: Color
if (enabled) {
saveModifier = Modifier
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
saveColor = MaterialTheme.colors.primary
} else {
saveModifier = Modifier
saveColor = HighOrLowlight
} else {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.Start
) {
Box(
Modifier
.fillMaxWidth()
.padding(bottom = 24.dp), contentAlignment = Alignment.Center
) {
ProfileImage(192.dp, profile.image)
if (profile.image == null) {
EditImageButton {
editProfile.value = true
scope.launch { bottomSheetModalState.show() }
}
}
}
ProfileNameRow(stringResource(R.string.display_name__field), profile.displayName)
ProfileNameRow(stringResource(R.string.full_name__field), profile.fullName)
TextButton(stringResource(R.string.edit_verb)) { editProfile.value = true }
}
Text(
stringResource(R.string.save_and_notify_contacts),
modifier = saveModifier,
color = saveColor
)
}
Spacer(Modifier.height(DEFAULT_BOTTOM_BUTTON_PADDING))
if (savedKeyboardState != keyboardState) {
LaunchedEffect(keyboardState) {
scope.launch {
@@ -180,17 +192,60 @@ fun UserProfileLayout(
}
}
@Composable
fun ProfileNameTextField(name: MutableState<String>) {
BasicTextField(
value = name.value,
onValueChange = { name.value = it },
modifier = Modifier
.padding(bottom = 24.dp)
.padding(start = 28.dp)
.fillMaxWidth(),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = true
)
}
@Composable
fun ProfileNameRow(label: String, text: String) {
Row(Modifier.padding(bottom = 24.dp)) {
Text(
label,
color = MaterialTheme.colors.onBackground
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
text,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.onBackground
)
}
}
@Composable
fun TextButton(text: String, click: () -> Unit) {
Text(
text,
color = MaterialTheme.colors.primary,
modifier = Modifier.clickable(onClick = click),
)
}
@Composable
fun EditImageButton(click: () -> Unit) {
IconButton(
onClick = click,
modifier = Modifier.size(30.dp)
modifier = Modifier.background(Color(1f, 1f, 1f, 0.2f), shape = CircleShape)
) {
Icon(
Icons.Outlined.PhotoCamera,
contentDescription = stringResource(R.string.edit_image),
tint = MaterialTheme.colors.primary,
modifier = Modifier.size(30.dp)
modifier = Modifier.size(36.dp)
)
}
}
@@ -206,16 +261,6 @@ fun DeleteImageButton(click: () -> Unit) {
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),
confirmText = generalGetString(R.string.save_and_notify_contacts),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -228,6 +273,7 @@ fun PreviewUserProfileLayoutEditOff() {
UserProfileLayout(
profile = Profile.sampleData,
close = {},
editProfile = remember { mutableStateOf(false) },
saveProfile = { _, _, _ -> }
)
}
@@ -245,6 +291,7 @@ fun PreviewUserProfileLayoutEditOn() {
UserProfileLayout(
profile = Profile.sampleData,
close = {},
editProfile = remember { mutableStateOf(true) },
saveProfile = { _, _, _ -> }
)
}

View File

@@ -19,7 +19,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.chatPasswordHash
@@ -32,6 +31,7 @@ import chat.simplex.app.views.database.PassphraseField
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.CreateProfile
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) {
@@ -45,7 +45,6 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
searchTextOrPassword = searchTextOrPassword,
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
visibleUsersCount = visibleUsersCount(m),
prefPerformLA = m.controller.appPrefs.performLA.get(),
addUser = {
ModalManager.shared.showModalCloseable { close ->
CreateProfile(m, close)
@@ -74,14 +73,14 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
AlertManager.shared.hideAlert()
removeUser(m, user, users, true, searchTextOrPassword.value.trim())
}) {
Text(stringResource(R.string.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeUser(m, user, users, false, searchTextOrPassword.value.trim())
}
) {
Text(stringResource(R.string.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
}
}
}
@@ -143,7 +142,6 @@ private fun UserProfilesView(
searchTextOrPassword: MutableState<String>,
profileHidden: MutableState<Boolean>,
visibleUsersCount: Int,
prefPerformLA: Boolean,
showHiddenProfilesNotice: SharedPreference<Boolean>,
addUser: () -> Unit,
activateUser: (User) -> Unit,
@@ -172,7 +170,7 @@ private fun UserProfilesView(
SectionView {
for (user in filteredUsers) {
UserView(user, users, visibleUsersCount, prefPerformLA, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
UserView(user, users, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
SectionDivider()
}
if (searchTextOrPassword.value.trim().isEmpty()) {
@@ -205,7 +203,6 @@ private fun UserView(
user: User,
users: List<User>,
visibleUsersCount: Int,
prefPerformLA: Boolean,
activateUser: (User) -> Unit,
removeUser: (User) -> Unit,
unhideUser: (User) -> Unit,
@@ -213,39 +210,43 @@ private fun UserView(
unmuteUser: (User) -> Unit,
showHiddenProfile: (User) -> Unit,
) {
val showMenu = remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showMenu.value = true }) {
var showDropdownMenu by remember { mutableStateOf(false) }
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
activateUser(user)
}
Box(Modifier.padding(horizontal = 16.dp)) {
DefaultDropdownMenu(showMenu) {
DropdownMenu(
expanded = showDropdownMenu,
onDismissRequest = { showDropdownMenu = false },
Modifier.width(220.dp)
) {
if (user.hidden) {
ItemAction(stringResource(R.string.user_unhide), Icons.Outlined.LockOpen, onClick = {
showMenu.value = false
showDropdownMenu = false
unhideUser(user)
})
} else {
if (visibleUsersCount > 1 && prefPerformLA) {
if (visibleUsersCount > 1) {
ItemAction(stringResource(R.string.user_hide), Icons.Outlined.Lock, onClick = {
showMenu.value = false
showDropdownMenu = false
showHiddenProfile(user)
})
}
if (user.showNtfs) {
ItemAction(stringResource(R.string.user_mute), Icons.Outlined.NotificationsOff, onClick = {
showMenu.value = false
showDropdownMenu = false
muteUser(user)
})
} else {
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.Notifications, onClick = {
showMenu.value = false
showDropdownMenu = false
unmuteUser(user)
})
}
}
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
removeUser(user)
showMenu.value = false
showDropdownMenu = false
})
}
}
@@ -387,4 +388,4 @@ private fun showMuteProfileAlert(showMuteProfileAlert: SharedPreference<Boolean>
showMuteProfileAlert.set(false)
},
)
}
}

View File

@@ -23,6 +23,7 @@ fun VersionInfoView(info: CoreVersionInfo) {
Text(String.format(stringResource(R.string.app_version_name), BuildConfig.VERSION_NAME))
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
Text(String.format(stringResource(R.string.core_version), info.version))
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
}

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M181.5,835q-22.97,0 -40.23,-17.27Q124,800.47 124,777.5L124,182q0,-22.97 17.27,-40.23Q158.53,124.5 181.5,124.5L561,124.5q12.25,0 20.63,8.46T590,153.18q0,12.32 -8.38,20.58T561,182L181.5,182v595.5L777,777.5L777,399q0,-12.25 8.43,-20.63 8.43,-8.38 20.5,-8.38 12.07,0 20.33,8.38T834.5,399v378.5q0,22.97 -17.27,40.23Q799.97,835 777,835L181.5,835ZM727.83,341.5q-12.32,0 -20.58,-8.38T699,312.5L699,261h-51.5q-12.25,0 -20.63,-8.43 -8.38,-8.43 -8.38,-20.5 0,-12.07 8.38,-20.33t20.63,-8.25L699,203.5v-52q0,-11.68 8.43,-20.09 8.43,-8.41 20.5,-8.41 12.07,0 20.33,8.41 8.25,8.41 8.25,20.09v52h52q11.68,0 20.09,8.46Q837,220.43 837,232.17q0,12.32 -8.41,20.58Q820.17,261 808.5,261h-52v51.5q0,12.25 -8.46,20.63t-20.21,8.38ZM273,676.5h413.17q9.82,0 13.82,-7.75T698,653L585.58,503.6q-4.7,-6.1 -11.46,-6.1 -6.76,0 -11.61,6L448,653.5l-81.46,-106.39q-4.69,-5.61 -11.5,-5.61 -6.81,0 -11.72,5.58L261.57,653.02q-5.07,7.98 -0.95,15.73T273,676.5ZM181.5,399v378.5L181.5,182v217Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480.03,697Q551,697 602.75,645.94t51.75,-122.5Q654.5,452 602.72,400q-51.78,-52 -122.75,-52Q408,348 356.75,400.06t-51.25,123.5Q305.5,595 356.78,646q51.28,51 123.25,51ZM435.5,482.5l31,-71.42Q470,402.5 480,403t13.5,9.08l30.16,70.42 65.92,27.58q9.42,4.16 9.42,13.79t-9.42,13.05L523.5,564l-30,69.92Q490,643 480,643.5t-13.5,-8.58L435.5,564l-65.58,-27.08Q360,533.5 360,523.87t9.92,-13.79L435.5,482.5ZM142.5,835.5q-22.97,0 -40.23,-17.27Q85,800.97 85,778L85,268.5q0,-21.97 17.27,-39.73Q119.53,211 142.5,211h147l54.91,-66.5q7.59,-10 19.11,-15 11.52,-5 24.98,-5h183q13.47,0 24.98,5 11.52,5 19.52,15l54.5,66.5h147q21.97,0 39.73,17.77Q875,246.53 875,268.5L875,778q0,22.97 -17.77,40.23Q839.47,835.5 817.5,835.5h-675ZM817.5,778L817.5,268.5h-675L142.5,778h675ZM480,522.5Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M255.69,838.85q-22.26,0 -38.46,-16.2 -16.2,-16.2 -16.2,-38.44L201.04,175.79q0,-22.24 16.2,-38.44 16.2,-16.2 38.61,-16.2h333.61l169.5,169v493.89q0,22.41 -16.2,38.61 -16.2,16.2 -38.46,16.2L255.69,838.85ZM574.42,304.23L574.42,151.35L255.85,151.35q-9.23,0 -16.92,7.69 -7.69,7.69 -7.69,16.92v608.08q0,9.23 7.69,16.92 7.69,7.69 16.92,7.69h448.31q9.23,0 16.92,-7.69 7.69,-7.69 7.69,-16.92L728.77,304.23L574.42,304.23ZM231.23,151.35v152.88,-152.88 657.31,-657.31Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M451.5,595v99q0,12.25 8.43,20.63 8.43,8.38 20.5,8.38 12.07,0 20.33,-8.38T509,694v-99h100.5q11.68,0 20.09,-8.43 8.41,-8.43 8.41,-20.5 0,-12.07 -8.41,-20.33 -8.41,-8.25 -20.09,-8.25L509,537.5L509,437q0,-11.68 -8.46,-20.09 -8.46,-8.41 -20.21,-8.41 -12.32,0 -20.58,8.41 -8.25,8.41 -8.25,20.09v100.5h-100q-12.25,0 -20.63,8.46t-8.38,20.21q0,12.32 8.38,20.58T351.5,595h100ZM222,875q-22.97,0 -40.23,-17.27Q164.5,840.47 164.5,817.5v-675q0,-22.97 17.27,-40.23Q199.03,85 222,85h335q11.91,0 22.71,4.75 10.79,4.75 18.91,12.34l179.26,179.31Q786,289.5 790.75,300.29q4.75,10.8 4.75,22.71v494.5q0,22.97 -17.27,40.23Q760.97,875 738,875L222,875ZM552.5,296L552.5,142.5L222,142.5v675h516L738,325L581.5,325q-12.25,0 -20.63,-8.38T552.5,296ZM222,142.5L222,325 222,142.5v675,-675Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="m436.5,615.5 l175.5,-114q13,-8.5 13,-23.51T612,454L436.5,340q-14.5,-9.5 -29,-1.27Q393,346.96 393,364v227.5q0,17.76 14.5,25.88 14.5,8.12 29,-1.88ZM142.5,795.5q-22.97,0 -40.23,-17.27Q85,760.97 85,738L85,222q0,-22.97 17.27,-40.23Q119.53,164.5 142.5,164.5h675q23.72,0 40.61,17.27Q875,199.03 875,222v516q0,22.97 -16.89,40.23Q841.22,795.5 817.5,795.5h-675ZM142.5,738L142.5,222v516ZM142.5,738h675L817.5,222h-675v516Z"/>
</vector>

View File

@@ -11,7 +11,7 @@
<string name="smp_servers_add">Přidat server…</string>
<string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu 9050\? Před povolením této možnosti musí být spuštěna proxy.</string>
<string name="accept_feature">Přijmout</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Povolte svým kontaktům odesílat mizící zprávy.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Umožněte svým kontaktům odesílat mizící zprávy.</string>
<string name="about_simplex_chat">O <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="smp_servers_add_to_another_device">Přidat do jiného zařízení</string>
<string name="accept_requests">Přijímat žádosti</string>
@@ -186,11 +186,9 @@
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
<string name="toast_permission_denied">Oprávnění zamítnuto!</string>
<string name="use_camera_button">Fotoaparát</string>
<string name="use_camera_button">Použít fotoaparát</string>
<string name="from_gallery_button">Z Galerie</string>
<string name="choose_file">Soubor</string>
<string name="gallery_image_button">Obrázek</string>
<string name="gallery_video_button">Video</string>
<string name="choose_file">Vybrat soubor</string>
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
<string name="above_then_preposition_continuation">potom:</string>
@@ -279,7 +277,7 @@
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_icon">IKONA APLIKACE</string>
<string name="settings_section_title_themes">TÉMATA</string>
<string name="settings_section_title_messages">ZPRÁVY A SOUBORY</string>
<string name="settings_section_title_messages">ZPRÁVY</string>
<string name="settings_section_title_calls">VOLÁNÍ</string>
<string name="export_database">Export databáze</string>
<string name="import_database">Import databáze</string>
@@ -657,11 +655,14 @@
<string name="app_version_title">Verze aplikace</string>
<string name="app_version_name">Verze aplikace: v%s</string>
<string name="core_version">Verze jádra: v%s</string>
<string name="core_build_timestamp">Jádro sestaveno: %s</string>
<string name="delete_address__question">Smazat adresu\?</string>
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
<string name="contact_requests">Žádosti o kontakt</string>
<string name="display_name__field">Zobrazované jméno:</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery váš profil vidět nemohou.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty.
\n
\n<xliff:g id="appName">SimpleX</xliff:g> servery váš profil vidět nemohou.</string>
<string name="save_preferences_question">Uložit předvolby\?</string>
<string name="save_and_notify_contact">Uložit a upozornit kontakt</string>
<string name="save_and_notify_contacts">Uložit a upozornit kontakty</string>
@@ -725,9 +726,11 @@
<string name="answer_call">Přijmout hovor</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> přeskočená zpráva (zprávy)</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Může se to stát, když:
\n1. Zprávy vypršely v odesílajícím klientovi po 2 dnech nebo na serveru po 30 dnech.
\n2. Dešifrování zprávy se nezdařilo, protože vy nebo váš kontakt jste použili starou zálohu databáze.
\n3. Spojení je kompromitováno.</string>
\n1. Zprávy na serveru vyprší, pokud nebyly přijaty po dobu 30 dnů,
\n2. Server, který používáte pro příjem zpráv od tohoto kontaktu, byl aktualizován a restartován.
\n3. Spojení je narušeno.
\nPřipojte se k vývojářům prostřednictvím Nastavení, abyste mohli dostávat aktualizace o serverech.
\nBudeme přidávat redundantní servery, abychom zabránili ztrátě zpráv.</string>
<string name="settings_section_title_you">VY</string>
<string name="settings_section_title_support">PODPOŘIT SIMPLEX CHAT</string>
<string name="settings_section_title_develop">VÝVOJ</string>
@@ -905,7 +908,7 @@
<string name="theme">Téma</string>
<string name="chat_preferences_contact_allows">Kontakt povolil</string>
<string name="chat_preferences_on">zapnuto</string>
<string name="chat_preferences_off">vypnout</string>
<string name="chat_preferences_off">vypnuto</string>
<string name="chat_preferences">Chat předvolby</string>
<string name="contact_preferences">Předvolby kontaktu</string>
<string name="group_preferences">Předvolby skupiny</string>
@@ -1019,6 +1022,7 @@
<string name="you_will_still_receive_calls_and_ntfs">Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní.</string>
<string name="you_can_hide_or_mute_user_profile">Můžete skrýt nebo ztlumit uživatelský profil - Podržte pro menu.</string>
<string name="user_unhide">Odkrýt</string>
<string name="settings_send_files_via_xftp">Poslat videa a soubory přes XFTP</string>
<string name="database_upgrade">Aktualizace databáze</string>
<string name="database_downgrade_warning">Upozornění: můžete ztratit nějaká data!</string>
<string name="confirm_database_upgrades">Potvrdit aktualizaci databáze</string>
@@ -1033,6 +1037,7 @@
<string name="hide_dev_options">Skrýt:</string>
<string name="show_developer_options">Zobrazit možnosti vývojáře</string>
<string name="settings_section_title_experimenta">POKUSNÝ</string>
<string name="xftp_requires_v461">Pro příjem přes XFTP je vyžadována verze 4.6.1+.</string>
<string name="image_will_be_received_when_contact_completes_uploading">Obrázek bude přijat, až kontakt dokončí jeho nahrání.</string>
<string name="show_dev_options">Zobrazit:</string>
<string name="developer_options">ID databáze a možnost Izolace přenosu.</string>
@@ -1051,92 +1056,6 @@
<string name="icon_descr_waiting_for_video">Čekám na video</string>
<string name="icon_descr_video_snd_complete">Video odesláno</string>
<string name="video_will_be_received_when_contact_completes_uploading">Video bude přijato, až kontakt dokončí jeho nahrávání.</string>
<string name="video_will_be_received_when_contact_is_online">Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo zkontrolujte později!</string>
<string name="video_will_be_received_when_contact_is_online">Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo se podívejte později!</string>
<string name="waiting_for_video">Čekám na video</string>
<string name="error_loading_smp_servers">Chyba načítání serverů SMP</string>
<string name="error_loading_xftp_servers">Chyba načítání serverů XFTP</string>
<string name="error_saving_xftp_servers">Chyba ukládání XFTP serverů</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy XFTP serverů jsou ve správném formátu s oddělenými řádky a nejsou duplicitní.</string>
<string name="error_xftp_test_server_auth">Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo</string>
<string name="smp_server_test_compare_file">Porovnat soubor</string>
<string name="smp_server_test_create_file">Vytvořit soubor</string>
<string name="smp_server_test_delete_file">Smazat soubor</string>
<string name="smp_server_test_download_file">Stáhnout soubor</string>
<string name="smp_server_test_upload_file">Nahrát soubor</string>
<string name="xftp_servers">XFTP servery</string>
<string name="network_socks_toggle_use_socks_proxy">Použít SOCKS proxy</string>
<string name="host_verb">Host</string>
<string name="network_proxy_port">port %d</string>
<string name="network_socks_proxy_settings">Nastavení SOCKS proxy</string>
<string name="la_lock_mode_passcode">Zadat heslo</string>
<string name="la_lock_mode">SimpleX zámek</string>
<string name="la_lock_mode_system">Ověření systému</string>
<string name="la_authenticate">Ověřit</string>
<string name="la_auth_failed">Ověření selhalo</string>
<string name="la_change_app_passcode">Změnit heslo</string>
<string name="la_current_app_passcode">Aktuální heslo</string>
<string name="la_enter_app_passcode">Zadat heslo</string>
<string name="la_no_app_password">Bez hesla aplikace</string>
<string name="la_could_not_be_verified">Nemohli jste být ověřeni; Zkuste to prosím znovu.</string>
<string name="la_minutes">%d minut</string>
<string name="la_seconds">%d vteřin</string>
<string name="la_immediately">Ihned</string>
<string name="la_please_remember_to_store_password">Zapamatujte si jej nebo bezpečně uložte - neexistuje způsob, jak obnovit ztracené heslo!</string>
<string name="lock_not_enabled">Zámek SimpleX není povolen!</string>
<string name="you_can_turn_on_lock">Zámek SimpleX můžete zapnout v Nastavení.</string>
<string name="port_verb">Port</string>
<string name="disable_onion_hosts_when_not_supported">Nastavte <i>Použít .onion hostitele</i> na Ne, pokud je SOCKS proxy nepodporuje.</string>
<string name="your_XFTP_servers">Vaše XFTP servery</string>
<string name="enable_lock">Povolit zámek</string>
<string name="lock_after">Zamknout po</string>
<string name="lock_mode">Režim zámku</string>
<string name="authentication_cancelled">Ověření zrušeno</string>
<string name="confirm_passcode">Potvrdit heslo</string>
<string name="incorrect_passcode">Nesprávné heslo</string>
<string name="new_passcode">Nové heslo</string>
<string name="submit_passcode">Odeslat</string>
<string name="la_mode_system">Systém</string>
<string name="change_lock_mode">Změnit zamykání</string>
<string name="la_mode_passcode">Heslo</string>
<string name="passcode_changed">Heslo změněno!</string>
<string name="passcode_not_changed">Heslo nezměněno!</string>
<string name="passcode_set">Heslo nastaveno!</string>
<string name="decryption_error">Chyba dešifrování</string>
<string name="alert_title_msg_bad_hash">Špatný hash zprávy</string>
<string name="alert_title_msg_bad_id">Špatné ID zprávy</string>
<string name="alert_text_msg_bad_hash">Hash předchozí zprávy se liší.</string>
<string name="alert_text_msg_bad_id">ID další zprávy je nesprávné (menší nebo rovno předchozí).
\nMůže se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitováno.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> zprávy se nepodařilo dešifrovat.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> zprývy přeskočeny.</string>
<string name="alert_text_fragment_please_report_to_developers">Nahlaste to prosím vývojářům.</string>
<string name="alert_text_fragment_permanent_error_reconnect">Tato chyba je pro toto připojení trvalá, připojte se znovu.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Může se to stát, když vy nebo vaše připojení použijete starou zálohu databáze.</string>
<string name="no_spaces">Žádné mezery!</string>
<string name="revoke_file__message">Soubor bude smazán ze serverů.</string>
<string name="stop_rcv_file__message">Příjem souboru bude zastaven.</string>
<string name="allow_calls_only_if">Povolte hovory, pouze pokud je váš kontakt povolí.</string>
<string name="allow_your_contacts_to_call">Povolte svým kontaktům vám volat.</string>
<string name="audio_video_calls">Audio/video hovory</string>
<string name="available_in_v51">"
\nDostupné ve verzi 5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Volat můžete vy i váš kontakt.</string>
<string name="only_you_can_make_calls">Volat můžete pouze vy.</string>
<string name="prohibit_calls">Zákaz audio/video hovorů.</string>
<string name="calls_prohibited_with_this_contact">Audio/video hovory jsou zakázány.</string>
<string name="only_your_contact_can_make_calls">Volat může pouze váš kontakt.</string>
<string name="v5_0_app_passcode">Heslo aplikace</string>
<string name="v5_0_large_files_support_descr">Rychle a bez čekání, než bude odesílatel online!</string>
<string name="v5_0_polish_interface">Polské rozhraní</string>
<string name="v5_0_app_passcode_descr">Nastavte jej namísto ověřování systému.</string>
<string name="v5_0_large_files_support">Videa a soubory až do velikosti 1 gb</string>
<string name="v5_0_polish_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
<string name="revoke_file__title">Odvolat soubor\?</string>
<string name="revoke_file__confirm">Odvolat</string>
<string name="revoke_file__action">Odvolat soubor</string>
<string name="stop_snd_file__message">Odesílání souboru bude zastaveno.</string>
<string name="stop_file__confirm">Zastavit</string>
<string name="stop_file__action">Zastavit soubor</string>
<string name="stop_snd_file__title">Zastavit odesílání souboru\?</string>
<string name="stop_rcv_file__title">Zastavit příjem souboru\?</string>
</resources>

View File

@@ -180,7 +180,7 @@
<string name="you_have_no_chats">Sie haben keine Chats</string>
<!-- ShareListView.kt -->
<string name="share_message">Nachricht teilen…</string>
<string name="share_image">Medien teilen…</string>
<string name="share_image">Bild teilen…</string>
<string name="share_file">Datei teilen…</string>
<!-- ComposeView.kt, helpers -->
<string name="attach">Anhängen</string>
@@ -254,11 +254,9 @@
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
<!-- GetImageView -->
<string name="toast_permission_denied">Berechtigung verweigert!</string>
<string name="use_camera_button">Kamera</string>
<string name="use_camera_button">Kamera\nverwenden</string>
<string name="from_gallery_button">Aus dem\nFotoalbum</string>
<string name="choose_file">Datei</string>
<string name="gallery_image_button">Bild</string>
<string name="gallery_video_button">Video</string>
<string name="choose_file">Datei\nauswählen</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Danke, dass Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> installiert haben!</string>
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
@@ -430,7 +428,7 @@
<string name="display_name__field">Angezeigter Name:</string>
<string name="full_name__field">"Vollständiger Name:</string>
<string name="your_current_profile">Mein aktuelles Chat-Profil</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. <xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
<string name="edit_image">Bild bearbeiten</string>
<string name="delete_image">Bild löschen</string>
<string name="save_preferences_question">Präferenzen speichern?</string>
@@ -557,10 +555,7 @@
<string name="integrity_msg_bad_id">Falsche Nachrichten-ID</string>
<string name="integrity_msg_duplicate">Doppelte Nachricht</string>
<string name="alert_title_skipped_messages">Übersprungene Nachrichten</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:
\n1. Die Nachrichten verfallen auf dem sendenden Client-System nach 2 Tagen oder auf dem Server nach 30 Tagen.
\n2. Die Nachrichten-Entschlüsselung ist fehlgeschlagen, da von Ihnen oder Ihrem Kontakt ein altes Datenbank-Backup genutzt wurde.
\n3. Die Verbindung wurde kompromittiert.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:\n1. Die Nachrichten verfallen auf dem Server, wenn sie 30 Tage lang nicht empfangen wurden.\n2. Der Server, den Sie zum Empfangen der Nachrichten von diesem Kontakt verwenden, wurde aktualisiert und neu gestartet.\n3. Die Verbindung ist kompromittiert.\nBitte nehmen Sie über die Einstellungen Verbindung mit den Entwicklern auf, um Updates zu den Servern zu erhalten.\nWir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu verhindern.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Datenschutz &amp; Sicherheit</string>
<string name="your_privacy">Meine Privatsphäre</string>
@@ -581,7 +576,7 @@
<string name="settings_section_title_socks">SOCKS-PROXY</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">DESIGN</string>
<string name="settings_section_title_messages">NACHRICHTEN und DATEIEN</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_incognito">Inkognito Modus</string>
<!-- DatabaseView.kt -->
@@ -1005,6 +1000,7 @@
<string name="app_version_code">App Build: %s</string>
<string name="app_version_title">App Version</string>
<string name="app_version_name">App Version: v%s</string>
<string name="core_build_timestamp">Core übersetzt am: %s</string>
<string name="core_version">Core Version: v%s</string>
<string name="users_add">Profil hinzufügen</string>
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string>
@@ -1077,7 +1073,7 @@
<string name="enter_password_to_show">Für die Anzeige das Passwort im Suchfeld eingeben</string>
<string name="make_profile_private">Privates Profil erzeugen!</string>
<string name="user_mute">Stummschalten</string>
<string name="tap_to_activate_profile">Tippen Sie auf das Profil um es zu aktivieren.</string>
<string name="tap_to_activate_profile">Tippen Sie, um das Profil zu aktivieren.</string>
<string name="should_be_at_least_one_profile">Es muss mindestens ein Benutzer-Profil vorhanden sein.</string>
<string name="should_be_at_least_one_visible_profile">Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.</string>
<string name="user_unmute">Stummschaltung aufheben</string>
@@ -1100,6 +1096,7 @@
<string name="group_welcome_title">Begrüßungsmeldung</string>
<string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten - für das Menü gedrückt halten.</string>
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Meine Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string>
<string name="settings_send_files_via_xftp">Videos und Dateien per XFTP versenden</string>
<string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string>
<string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string>
<string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string>
@@ -1108,14 +1105,15 @@
<string name="settings_section_title_experimenta">EXPERIMENTELL</string>
<string name="database_upgrade">Datenbank-Aktualisierung</string>
<string name="mtr_error_different">Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s</string>
<string name="downgrade_and_open_chat">Datenbank herabstufen und den Chat öffnen</string>
<string name="downgrade_and_open_chat">Herabstufen und den Chat öffnen</string>
<string name="incompatible_database_version">Inkompatible Datenbank-Version</string>
<string name="database_downgrade_warning">Warnung: Sie könnten einige Daten verlieren!</string>
<string name="database_downgrade">Datenbank auf alte Version herabstufen</string>
<string name="database_downgrade">Datenbank-Herabstufung</string>
<string name="developer_options">Datenbank-IDs und Transport-Isolationsoption.</string>
<string name="mtr_error_no_down_migration">Die Datenbank-Version ist neuer als die App, keine Abwärts-Migration für: %s</string>
<string name="hide_dev_options">Verberge:</string>
<string name="database_migrations">Migrationen: %s</string>
<string name="xftp_requires_v461">Für den Empfang per XFTP wird v4.6.1 oder neuer benötigt.</string>
<string name="image_will_be_received_when_contact_completes_uploading">Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string>
<string name="file_will_be_received_when_contact_completes_uploading">Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string>
<string name="cancel_file__question">Dateitransfer abbrechen\?</string>
@@ -1134,91 +1132,4 @@
<string name="icon_descr_waiting_for_video">Auf das Video warten</string>
<string name="waiting_for_video">Auf das Video warten</string>
<string name="video_will_be_received_when_contact_is_online">Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</string>
<string name="your_XFTP_servers">Ihre XFTP-Server</string>
<string name="host_verb">Host</string>
<string name="error_saving_xftp_servers">Fehler beim Speichern der XFTP-Server</string>
<string name="error_loading_smp_servers">Fehler beim Laden der SMP-Server</string>
<string name="error_xftp_test_server_auth">Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung</string>
<string name="smp_server_test_download_file">Datei herunterladen</string>
<string name="smp_server_test_compare_file">Datei vergleichen</string>
<string name="smp_server_test_delete_file">Datei löschen</string>
<string name="lock_mode">Sperr-Modus</string>
<string name="authentication_cancelled">Authentifizierung abgebrochen</string>
<string name="confirm_passcode">Passwort bestätigen</string>
<string name="enable_lock">Sperre aktivieren</string>
<string name="incorrect_passcode">Passwort falsch</string>
<string name="lock_after">Sperre nach</string>
<string name="new_passcode">Neues Passwort</string>
<string name="la_mode_passcode">Passwort</string>
<string name="passcode_changed">Passwort geändert!</string>
<string name="passcode_set">Passwort wurde geändert!</string>
<string name="change_lock_mode">Sperr-Modus ändern</string>
<string name="passcode_not_changed">Passwort wurde nicht geändert!</string>
<string name="decryption_error">Entschlüsselungsfehler</string>
<string name="decryption_error_permanent">Dauerhafter Entschlüsselungsfehler</string>
<string name="error_loading_xftp_servers">Fehler beim Laden der XFTP-Server</string>
<string name="la_lock_mode_passcode">Passworteingabe</string>
<string name="la_enter_app_passcode">Passwort eingeben</string>
<string name="la_no_app_password">Kein App-Passwort</string>
<string name="la_authenticate">Authentifizieren</string>
<string name="la_minutes">%d Minuten</string>
<string name="la_seconds">%d Sekunden</string>
<string name="la_immediately">Sofort</string>
<string name="port_verb">Port</string>
<string name="network_proxy_port">Port %d</string>
<string name="smp_server_test_create_file">Datei erstellen</string>
<string name="la_current_app_passcode">Aktuelles Passwort</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Dies kann passieren, falls Sie oder Ihre Verbindung ein altes Datenbank-Backup genutzt haben.</string>
<string name="la_please_remember_to_store_password">Es gibt keine Möglichkeit ein vergessenes Passwort wiederherzustellen - bitte erinnern Sie sich gut daran oder speichern Sie es sicher ab!</string>
<string name="alert_text_fragment_please_report_to_developers">Bitte melden Sie es den Entwicklern.</string>
<string name="la_change_app_passcode">Passwort ändern</string>
<string name="alert_title_msg_bad_hash">Ungültiger Nachrichten-Hash</string>
<string name="la_auth_failed">Authentifizierung fehlgeschlagen</string>
<string name="alert_title_msg_bad_id">Falsche Nachrichten-ID</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
<string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string>
<string name="la_lock_mode">SimpleX Sperr-Modus</string>
<string name="lock_not_enabled">SimpleX Sperre ist nicht aktiviert!</string>
<string name="disable_onion_hosts_when_not_supported">Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.</string>
<string name="submit_passcode">Übermitteln</string>
<string name="la_mode_system">System</string>
<string name="la_could_not_be_verified">Sie können nicht überprüft werden - bitte versuchen Sie es nochmal.</string>
<string name="xftp_servers">XFTP-Server</string>
<string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen).
\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> Nachrichten konnten nicht entschlüsselt werden.</string>
<string name="alert_text_msg_bad_hash">Der Hash der vorherigen Nachricht ist unterschiedlich.</string>
<string name="you_can_turn_on_lock">Sie können die SimpleX Sperre über die Einstellungen aktivieren.</string>
<string name="network_socks_proxy_settings">SOCKS-Proxy Einstellungen</string>
<string name="la_lock_mode_system">System-Authentifizierung</string>
<string name="alert_text_fragment_permanent_error_reconnect">Es handelt sich um einen permanenten Fehler für diese Verbindung - bitte verbinden Sie sich neu.</string>
<string name="smp_server_test_upload_file">Datei hochladen</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> Nachrichten übersprungen.</string>
<string name="allow_calls_only_if">Anrufe sind nur erlaubt, wenn Ihr Kontakt das ebenfalls erlaubt.</string>
<string name="allow_your_contacts_to_call">Erlaubt Ihren Kontakten Sie anzurufen.</string>
<string name="audio_video_calls">Audio/Video Anrufe</string>
<string name="available_in_v51">"
\nVerfügbar in v5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Sowohl Sie, als auch Ihr Kontakt können Anrufe tätigen.</string>
<string name="only_you_can_make_calls">Nur Sie können Anrufe tätigen.</string>
<string name="prohibit_calls">Audio/Video Anrufe nicht erlauben.</string>
<string name="stop_rcv_file__title">Den Empfang der Datei beenden\?</string>
<string name="stop_snd_file__title">Das Senden der Datei beenden\?</string>
<string name="stop_rcv_file__message">Der Empfang der Datei wird beendet.</string>
<string name="stop_snd_file__message">Das Senden der Datei wird beendet.</string>
<string name="stop_file__action">Datei beenden</string>
<string name="revoke_file__message">Die Datei wird von den Servern gelöscht.</string>
<string name="no_spaces">Keine Leerzeichen!</string>
<string name="revoke_file__confirm">Widerrufen</string>
<string name="revoke_file__action">Datei widerrufen</string>
<string name="revoke_file__title">Datei widerrufen\?</string>
<string name="stop_file__confirm">Beenden</string>
<string name="v5_0_large_files_support">Videos und Dateien bis zu 1GB</string>
<string name="v5_0_large_files_support_descr">Schnell und ohne warten auf den Absender, bis er online ist!</string>
<string name="v5_0_polish_interface">Polnische Bedienoberfläche</string>
<string name="v5_0_app_passcode_descr">Anstelle der System-Authentifizierung festlegen.</string>
<string name="v5_0_polish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="only_your_contact_can_make_calls">Nur Ihr Kontakt kann Anrufe tätigen.</string>
<string name="v5_0_app_passcode">App Passwort</string>
<string name="calls_prohibited_with_this_contact">Audio/Video Anrufe sind nicht erlaubt.</string>
</resources>

View File

@@ -10,20 +10,20 @@
<string name="chat_item_ttl_day">un dia</string>
<string name="chat_item_ttl_month">un mes</string>
<string name="chat_item_ttl_week">una semana</string>
<string name="allow_disappearing_messages_only_if">Se permiten mensajes temporales sólo si tu contacto también los permite.</string>
<string name="allow_disappearing_messages_only_if">Permitir mensajes temporales sólo si tu contacto los permite.</string>
<string name="v4_3_improved_server_configuration_desc">Añadir servidores escaneando códigos QR.</string>
<string name="smp_servers_preset_add">Añadir servidores predefinidos</string>
<string name="all_group_members_will_remain_connected">Todos los miembros del grupo permanecerán conectados.</string>
<string name="allow_irreversible_message_deletion_only_if">Se permite la eliminación irreversible de mensajes sólo si tu contacto también lo permite para tí.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Permites a tus contactos enviar mensajes temporales</string>
<string name="allow_your_contacts_to_send_voice_messages">Permites a tus contactos enviar mensajes de voz.</string>
<string name="allow_irreversible_message_deletion_only_if">Permitir la eliminación irreversible de mensajes sólo si tu contacto también lo permite.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la frase de contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Permitir a tus contactos enviar mensajes temporales</string>
<string name="allow_your_contacts_to_send_voice_messages">Permitir a tus contactos enviar mensajes de voz.</string>
<string name="chat_preferences_always">siempre</string>
<string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.</string>
<string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando, no se iniciará ningún servicio en segundo plano.</string>
<string name="settings_section_title_icon">ICONO DE LA APLICACIÓN</string>
<string name="incognito_random_profile_from_contact_description">Se enviará un perfil aleatorio al contacto del que recibió este enlace</string>
<string name="turning_off_service_and_periodic">La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.</string>
<string name="notifications_mode_service_desc">El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.</string>
<string name="notifications_mode_service_desc">El servicio en segundo plano está siempre en funcionamiento las notificaciones se muestran en cuanto los mensajes estén disponibles.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Se puede desactivar en Configuración</b> las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.</string>
<string name="notifications_mode_service">Siempre activo</string>
<string name="allow_verb">Permitir</string>
@@ -47,7 +47,7 @@
<string name="accept">Aceptar</string>
<string name="audio_call_no_encryption">llamada de audio (sin cifrado e2e)</string>
<string name="icon_descr_audio_call">llamada de audio</string>
<string name="settings_audio_video_calls">Llamadas y Videollamadas</string>
<string name="settings_audio_video_calls">Llamadas y videollamadas</string>
<string name="icon_descr_audio_off">Audio desactivado</string>
<string name="icon_descr_audio_on">Audio activado</string>
<string name="integrity_msg_bad_id">ID de mensaje erróneo</string>
@@ -55,12 +55,12 @@
<string name="users_delete_all_chats_deleted">Se eliminarán todos los chats y mensajes. ¡No puede deshacerse!</string>
<string name="accept_feature">Aceptar</string>
<string name="allow_to_send_disappearing">Permitir enviar mensajes temporales.</string>
<string name="keychain_is_storing_securely">Android Keystore se utiliza para almacenar de forma segura la contraseña - permite que el servicio de notificación funcione.</string>
<string name="keychain_is_storing_securely">Android Keystore se utiliza para almacenar de forma segura la frase de contraseña - permite que el servicio de notificación funcione.</string>
<string name="users_add">Añadir perfil</string>
<string name="incognito_random_profile_description">Se enviará un perfil aleatorio a tu contacto</string>
<string name="color_primary">Color</string>
<string name="allow_your_contacts_irreversibly_delete">Permites a tus contactos eliminar irreversiblemente los mensajes enviados.</string>
<string name="allow_voice_messages_only_if">Se permiten mensajes de voz sólo si tu contacto también los permite.</string>
<string name="color_primary">Acento</string>
<string name="allow_your_contacts_irreversibly_delete">Permitir a tus contactos eliminar irreversiblemente los mensajes enviados.</string>
<string name="allow_voice_messages_only_if">Permitir mensajes de voz sólo si tu contacto los permite.</string>
<string name="allow_direct_messages">Permitir el envío de mensajes directos a los miembros.</string>
<string name="allow_to_delete_messages">Permitir la eliminación irreversible de los mensajes enviados.</string>
<string name="allow_to_send_voice">Permitir enviar mensajes de voz.</string>
@@ -70,7 +70,7 @@
<string name="integrity_msg_bad_hash">hash de mensaje erróneo</string>
<string name="answer_call">Responder llamada</string>
<string name="group_member_role_admin">administrador</string>
<string name="allow_voice_messages_question">¿Permites los mensajes de voz\?</string>
<string name="allow_voice_messages_question">¿Permitir mensajes de voz\?</string>
<string name="back">Volver</string>
<string name="about_simplex_chat">Sobre <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="smp_servers_add_to_another_device">Añadir a otro dispositivo</string>
@@ -85,7 +85,7 @@
<string name="both_you_and_your_contact_can_send_disappearing">Tanto tú como tu contacto podéis enviar mensajes temporales.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Escanear código QR</b>: para conectar con tu contacto que te muestre código QR.</string>
<string name="create_profile_button">Crear</string>
<string name="create_one_time_link">Crear enlace de invitación de un solo uso.</string>
<string name="create_one_time_link">Crear enlace de invitación de un uso.</string>
<string name="create_group">Crear grupo secreto</string>
<string name="database_passphrase_will_be_updated">La contraseña de cifrado de la base de datos será actualizada.</string>
<string name="info_row_database_id">ID de la base de datos</string>
@@ -98,7 +98,7 @@
<string name="rcv_group_event_group_deleted">grupo eliminado</string>
<string name="delete_group_question">¿Eliminar grupo\?</string>
<string name="delete_messages_after">Eliminar después de</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar SimpleX Lock en Configuración, después de activar la autenticación de dispositivo.</string>
<string name="no_call_on_lock_screen">Desactivar</string>
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales no están permitidos en este chat.</string>
<string name="disappearing_messages_are_prohibited">Los mensajes temporales no están permitidos en este grupo.</string>
@@ -108,8 +108,8 @@
<string name="simplex_link_mode_description">Descripción</string>
<string name="smp_server_test_connect">Conectar</string>
<string name="notification_contact_connected">Conectado</string>
<string name="auth_disable_simplex_lock">Desactivar Bloqueo SimpleX</string>
<string name="auth_device_authentication_is_disabled_turning_off">Autenticación de dispositivo desactivada. Bloqueo SimpleX deshabilitado.</string>
<string name="auth_disable_simplex_lock">Desactivar SimpleX Lock</string>
<string name="auth_device_authentication_is_disabled_turning_off">Autenticación de dispositivo desactivada. SimpleX Lock deshabilitado.</string>
<string name="maximum_supported_file_size">El tamaño máximo de archivo admitido es <xliff:g id="maxFileSize">%1$s</xliff:g></string>
<string name="clear_verification">Eliminar verificación</string>
<string name="create_profile">Crear perfil</string>
@@ -166,7 +166,7 @@
<string name="icon_descr_server_status_disconnected">Desconectado</string>
<string name="icon_descr_server_status_connected">Conectado</string>
<string name="copied">Copiado en portapapeles</string>
<string name="share_one_time_link">Crear enlace de invitación de un solo uso.</string>
<string name="share_one_time_link">Crear enlace de invitación de un uso.</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 PC: escanéa el código QR desde la app mediante <b>Escanéo de código QR </b></string>
<string name="delete_contact_menu_action">Eliminar</string>
<string name="delete_group_menu_action">Eliminar</string>
@@ -231,7 +231,7 @@
<string name="chat_preferences_default">predefinido (%s)</string>
<string name="full_deletion">Eliminar para todos</string>
<string name="feature_enabled">activado</string>
<string name="contacts_can_mark_messages_for_deletion">El contacto solo puede marcar los mensajes para eliminar. Tu podrás verlos.</string>
<string name="contacts_can_mark_messages_for_deletion">El contacto puede marcar los mensajes para eliminar; tu podrás verlos.</string>
<string name="ttl_w">%ds</string>
<string name="deleted_description">eliminado</string>
<string name="connect_via_contact_link">¿Conectar mediante enlace de contacto\?</string>
@@ -250,20 +250,21 @@
<string name="icon_descr_email">Email</string>
<string name="connect_button">Conectar</string>
<string name="connect_via_link">Conectar mediante enlace</string>
<string name="database_passphrase_and_export">Base de Datos y
\nContraseña</string>
<string name="database_passphrase_and_export">Base de datos
\ny frase de contraseña</string>
<string name="contribute">Contribuye</string>
<string name="core_build_timestamp">Core compilado: %s</string>
<string name="core_version">Core versión: v%s</string>
<string name="contact_requests">Solicitud del contacto</string>
<string name="delete_image">Eliminar imagen</string>
<string name="edit_image">Editar imagen</string>
<string name="settings_section_title_chats">CHATS</string>
<string name="change_verb">Cambiar</string>
<string name="notifications_mode_periodic_desc">Se realizan comprobaciones de mensajes nuevos periódicas de hasta un minuto de duración cada 10 minutos</string>
<string name="notifications_mode_periodic_desc">Comprueba mensajes nuevos de hasta un minuto cada 10 minutos</string>
<string name="clear_contacts_selection_button">Limpiar</string>
<string name="change_member_role_question">¿Cambiar rol de grupo\?</string>
<string name="v4_4_verify_connection_security_desc">Compara los códigos de seguridad con tus contactos</string>
<string name="choose_file">Archivo</string>
<string name="choose_file">Elije archivo</string>
<string name="clear_verb">Limpiar</string>
<string name="clear_chat_button">Limpiar chat</string>
<string name="configure_ICE_servers">Configurar servidores ICE</string>
@@ -336,7 +337,7 @@
<string name="error_setting_network_config">Error al actualizar la configuración de red</string>
<string name="error_creating_address">Error creando dirección</string>
<string name="error_deleting_user">Error eliminando perfil de usuario</string>
<string name="auth_enable_simplex_lock">Activar Bloqueo SimpleX</string>
<string name="auth_enable_simplex_lock">Activar SimpleX Lock</string>
<string name="one_time_link">Enlace de invitación de un uso</string>
<string name="smp_servers">Servidores SMP</string>
<string name="settings_experimental_features">Características experimentales</string>
@@ -359,8 +360,6 @@
<string name="error_saving_file">Error al guardar archivo</string>
<string name="icon_descr_server_status_error">Error</string>
<string name="from_gallery_button">De la Galería</string>
<string name="gallery_image_button">Imagen</string>
<string name="gallery_video_button">Vídeo</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si has recibido el enlace de invitación a <xliff:g id="appName">SimpleX Chat</xliff:g>, puedes abrirlo en tu navegador:</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si eliges rechazar, el remitente NO será notificado.</string>
<string name="invalid_contact_link">¡Enlace no válido!</string>
@@ -385,8 +384,8 @@
<string name="error_deleting_group">Error al eliminar grupo</string>
<string name="error_deleting_pending_contact_connection">Error al eliminar conexión de contacto pendiente</string>
<string name="hide_notification">Ocultar</string>
<string name="notification_preview_mode_hidden">Oculta</string>
<string name="notification_display_mode_hidden_desc">Se ocultan tanto el contacto como el mensaje</string>
<string name="notification_preview_mode_hidden">Oculto</string>
<string name="notification_display_mode_hidden_desc">Ocultar contacto y mensaje</string>
<string name="hide_verb">Ocultar</string>
<string name="for_everybody">Para todos</string>
<string name="icon_descr_file">Archivo</string>
@@ -408,7 +407,7 @@
<string name="v4_4_french_interface">Interfaz en francés</string>
<string name="image_descr">Imagen</string>
<string name="file_not_found">Archivo no encontrado</string>
<string name="how_to_use_simplex_chat">Guía de uso</string>
<string name="how_to_use_simplex_chat">Guia de uso</string>
<string name="full_name_optional__prompt">Nombre completo (opcional)</string>
<string name="callstate_ended">finalizado</string>
<string name="settings_section_title_help">AYUDA</string>
@@ -446,16 +445,16 @@
<string name="share_link">Compartir enlace</string>
<string name="how_it_works">Cómo funciona</string>
<string name="delete_message_cannot_be_undone_warning">El mensaje se eliminará. ¡No puede deshacerse!</string>
<string name="incognito_info_protects">La función del modo incógnito es proteger la identidad del perfil principal: por cada contacto nuevo se genera un perfil aleatorio.</string>
<string name="incognito_info_protects">El modo incógnito protege la identidad del perfil principal, por cada contacto nuevo se genera un perfil nuevo aleatorio.</string>
<string name="turn_off_battery_optimization">Para poder usarse <b>deshabilita la optimización de batería</b> para <xliff:g id="appName">SimpleX</xliff:g> en el siguiente cuadro de diálogo. De lo contrario las notificaciones estarán desactivadas.</string>
<string name="install_simplex_chat_for_terminal">Instalar <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para terminal</string>
<string name="group_invitation_item_description">invitación al grupo <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="rcv_group_event_member_added">invitado <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas dentro del mismo perfil.</string>
<string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas en un único perfil de chat.</string>
<string name="invite_to_group_button">Invitar al grupo</string>
<string name="to_verify_compare">Para comprobar el cifrado de extremo a extremo con su contacto compare (o escanee) el código en sus dispositivos.</string>
<string name="database_is_not_encrypted">La base de datos no está cifrada. Escribe una contraseña para protegerla.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no duplicadas.</string>
<string name="icon_descr_instant_notifications">Notificación instantánea</string>
<string name="network_settings_title">Configuración de red</string>
<string name="network_use_onion_hosts_no_desc_in_alert">No se usarán hosts .onion</string>
@@ -471,9 +470,11 @@
<string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes no está permitida en este grupo.</string>
<string name="v4_3_improved_server_configuration">Configuración del servidor mejorada</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Esto puede suceder cuando:
\n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días.
\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.
\n3. La conexión ha sido comprometida.</string>
\n1. Los mensajes caducan en el servidor si no se han recibido durante 30 días.
\n2. El servidor que utiliza para recibir los mensajes de este contacto fue actualizado y reiniciado.
\n3. La conexión está comprometida.
\nPor favor, contacta con los desarrolladores a través del menú Configuración para recibir actualizaciones sobre los servidores.
\nAñadiremos redundancia de servidores para evitar la pérdida de mensajes.</string>
<string name="notification_preview_mode_message">Texto del mensaje</string>
<string name="member_info_section_title_member">MIEMBRO</string>
<string name="chat_item_ttl_none">nunca</string>
@@ -506,7 +507,7 @@
<string name="invalid_data">datos no válidos</string>
<string name="invalid_message_format">formato de mensaje no válido</string>
<string name="live">EN VIVO</string>
<string name="moderated_description">eliminado por el moderador</string>
<string name="moderated_description">moderado</string>
<string name="display_name_invited_to_connect">invitado a conectarse</string>
<string name="service_notifications_disabled">¡Las notificaciones instantáneas están desactivadas!</string>
<string name="notification_preview_new_message">mensaje nuevo</string>
@@ -539,14 +540,14 @@
<string name="ok">OK</string>
<string name="only_stored_on_members_devices">(sólo almacenado por miembros del grupo)</string>
<string name="markdown_help">Ayuda sintaxis markdown</string>
<string name="network_and_servers">Redes y Servidores</string>
<string name="network_and_servers">Redes y servidores</string>
<string name="network_use_onion_hosts_prefer_desc">Se usarán hosts .onion cuando estén disponibles.</string>
<string name="italic">cursiva</string>
<string name="incoming_audio_call">Llamada entrante</string>
<string name="video_call_no_encryption">videollamada (sin cifrado e2e)</string>
<string name="status_no_e2e_encryption">sin cifrado e2e</string>
<string name="import_database">Importar base de datos</string>
<string name="settings_section_title_messages">MENSAJES Y ARCHIVOS</string>
<string name="settings_section_title_messages">MENSAJES</string>
<string name="import_database_question">¿Importar base de datos\?</string>
<string name="no_received_app_files">Sin archivos recibidos o enviados</string>
<string name="messages_section_title">Mensajes</string>
@@ -602,8 +603,8 @@
<string name="reset_color">Restablecer colores</string>
<string name="only_you_can_send_disappearing">Sólo tú puedes enviar mensajes temporales</string>
<string name="only_your_contact_can_send_disappearing">Sólo tu contacto puede enviar mensajes temporales.</string>
<string name="prohibit_sending_voice">Prohibes el envío de mensajes de voz.</string>
<string name="notifications_mode_off">Se ejecuta sólo cuando la aplicación está abierta</string>
<string name="prohibit_sending_voice">Prohibir el envío de mensajes de voz.</string>
<string name="notifications_mode_off">Se ejecuta cuando la aplicación está abierta</string>
<string name="auth_open_chat_console">Abrir la consola de chat</string>
<string name="save_verb">Guardar</string>
<string name="restore_database_alert_confirm">Restaurar</string>
@@ -626,7 +627,7 @@
<string name="restore_database_alert_title">¿Restaurar copia de seguridad de la base de datos\?</string>
<string name="onboarding_notifications_mode_title">Notificaciones privadas</string>
<string name="image_descr_profile_image">imagen del perfil</string>
<string name="prohibit_sending_voice_messages">Prohibes el envío de mensajes de voz.</string>
<string name="prohibit_sending_voice_messages">Prohibir el envío de mensajes de voz.</string>
<string name="protect_app_screen">Proteger la pantalla de la aplicación</string>
<string name="read_more_in_github_with_link">Más información en nuestro <font color="#0088ff">repositorio GitHub</font> .</string>
<string name="icon_descr_record_voice_message">Grabar mensaje de voz</string>
@@ -651,7 +652,7 @@
<string name="network_options_revert">Revertir</string>
<string name="network_option_ping_interval">Intervalo PING</string>
<string name="network_option_ping_count">Contador PING</string>
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar).</string>
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcar para eliminar).</string>
<string name="v4_5_message_draft_descr">Conserva el último borrador del mensaje con los datos adjuntos.</string>
<string name="v4_5_private_filenames">Nombres de archivos privados</string>
<string name="v4_5_reduced_battery_usage">Reducción del uso de la batería</string>
@@ -676,7 +677,7 @@
<string name="reject">Rechazar</string>
<string name="open_verb">Abrir</string>
<string name="icon_descr_call_pending_sent">Llamada pendiente</string>
<string name="privacy_and_security">Privacidad y Seguridad</string>
<string name="privacy_and_security">Privacidad y seguridad</string>
<string name="store_passphrase_securely_without_recover">Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes.</string>
<string name="save_archive">Guardar archivo</string>
<string name="restore_database_alert_desc">Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer.</string>
@@ -685,16 +686,16 @@
<string name="network_option_protocol_timeout">Tiempo de espera del protocolo</string>
<string name="network_option_seconds_label">seg</string>
<string name="users_delete_with_connections">Perfil y conexiones de servidor</string>
<string name="prohibit_sending_disappearing_messages">Prohibes el envío de mensajes temporales</string>
<string name="prohibit_sending_disappearing_messages">Prohibir el envío de mensajes temporales</string>
<string name="only_you_can_send_voice">Sólo tú puedes enviar mensajes de voz.</string>
<string name="only_your_contact_can_send_voice">Sólo tu contacto puede enviar mensajes de voz.</string>
<string name="run_chat_section">EJECUTAR CHAT</string>
<string name="restart_the_app_to_use_imported_chat_database">Reinicia la aplicación para poder usar la base de datos importada.</string>
<string name="enter_correct_current_passphrase">Introduce la contraseña actual correcta.</string>
<string name="feature_received_prohibited">recepción prohibida</string>
<string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar).</string>
<string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcar para eliminar).</string>
<string name="prohibit_direct_messages">Prohibir el envío de mensajes directos a los miembros.</string>
<string name="prohibit_sending_disappearing">Prohibes el envío de mensajes temporales</string>
<string name="prohibit_sending_disappearing">Prohibir el envío de mensajes temporales</string>
<string name="v4_2_security_assessment">Evaluación de la seguridad</string>
<string name="receiving_files_not_yet_supported">la recepción de archivos aún no está disponible</string>
<string name="sending_files_not_yet_supported">el envío de archivos aún no está disponible</string>
@@ -736,11 +737,11 @@
<string name="stop_chat_confirmation">Detener</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</string>
<string name="skip_inviting_button">Omitir invitación a miembros</string>
<string name="settings_notification_preview_mode_title">Vista previa</string>
<string name="settings_notification_preview_mode_title">Mostrar vista previa</string>
<string name="la_notice_turn_on">Activar</string>
<string name="share_verb">Compartir</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
<string name="set_contact_name">Escribe un nombre para el contacto</string>
<string name="set_contact_name">Escribe el nombre del contacto</string>
<string name="network_socks_toggle">Usar proxy SOCKS (puerto 9050)</string>
<string name="unknown_error">Error desconocido</string>
<string name="member_role_will_be_changed_with_notification">El rol cambiará a \"%s\". Se notificará a todos los miembros del grupo.</string>
@@ -748,9 +749,9 @@
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
<string name="icon_descr_received_msg_status_unread">no leído</string>
<string name="text_field_set_contact_placeholder">Escribe un nombre para el contacto…</string>
<string name="text_field_set_contact_placeholder">Escribe el nombre del contacto…</string>
<string name="switch_receiving_address_question">¿Cambiar dirección de recepción\?</string>
<string name="use_camera_button">Cámara</string>
<string name="use_camera_button">Usar cámara</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">¡El contacto con el que has compartido este enlace NO podrá conectarse!</string>
<string name="show_QR_code">Mostrar código QR</string>
<string name="this_link_is_not_a_valid_connection_link">¡El enlace no es un enlace de conexión válido!</string>
@@ -774,7 +775,7 @@
<string name="stop_chat_to_export_import_or_delete_chat_database">Detén Chat para poder exportar, importar o eliminar la base de datos. No puedes recibir ni enviar mensajes mientras Chat esté detenido.</string>
<string name="thank_you_for_installing_simplex">Gracias por instalar <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger la privacidad, en lugar de los identificadores de usuario que utilizan el resto de plataformas, <xliff:g id="appName">SimpleX</xliff:g> dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos.</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa Bloqueo SimpleX.
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa SimpleX Lock.
\nSe te pedirá que completes la autenticación antes de activar esta función.</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">Al actualizar la configuración, el cliente se reconectará a todos los servidores.</string>
<string name="use_simplex_chat_servers__question">¿Usar servidores <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\?</string>
@@ -784,9 +785,9 @@
<string name="error_smp_test_server_auth">El servidor requiere autorización para crear colas, comprueba la contraseña</string>
<string name="enter_passphrase_notification_desc">Para recibir notificaciones, introduce la contraseña de la base de datos</string>
<string name="ntf_channel_calls">Llamadas de chat SimpleX</string>
<string name="notifications_mode_periodic">Se ejecuta periódicamente</string>
<string name="notification_preview_mode_message_desc">Se muestran el nombre del contacto y el mensaje</string>
<string name="notification_preview_mode_contact_desc">Se muestra solo el nombre del contacto</string>
<string name="notifications_mode_periodic">Se inicia periódicamente</string>
<string name="notification_preview_mode_message_desc">Mostrar contacto y mensaje</string>
<string name="notification_preview_mode_contact_desc">Mostrar sólo contacto</string>
<string name="auth_simplex_lock_turned_on">Bloqueo SimpleX activado</string>
<string name="auth_stop_chat">Detener chat</string>
<string name="moderate_message_will_be_deleted_warning">El mensaje se eliminará para todos los miembros.</string>
@@ -799,7 +800,7 @@
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad.</string>
<string name="first_platform_without_user_ids">La primera plataforma sin identificadores de usuario: diseñada para la privacidad.</string>
<string name="alert_message_no_group">Este grupo ya no existe.</string>
<string name="incognito_info_find">Para conocer el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string>
<string name="incognito_info_find">Para encontrar el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string>
<string name="accept_feature_set_1_day">Establecer 1 día</string>
<string name="v4_4_french_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
<string name="v4_5_italian_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
@@ -811,7 +812,7 @@
<string name="is_not_verified">%s no está verificado</string>
<string name="smp_servers_test_server">Probar servidor</string>
<string name="smp_servers_test_servers">Probar servidores</string>
<string name="star_on_github">Estrella en GitHub</string>
<string name="star_on_github">Comienza en GitHub</string>
<string name="smp_servers_per_user">Los servidores para nuevas conexiones de tu perfil de Chat actual</string>
<string name="network_disable_socks">¿Usar conexión directa a Internet\?</string>
<string name="update_onion_hosts_settings_question">¿Actualizar la configuración de los hosts .onion\?</string>
@@ -825,7 +826,7 @@
<string name="rcv_group_event_updated_group_profile">perfil de grupo actualizado</string>
<string name="network_option_tcp_connection_timeout">Tiempo de espera de la conexión TCP agotado</string>
<string name="theme">Tema</string>
<string name="set_group_preferences">Establece preferencias de grupo</string>
<string name="set_group_preferences">Establecer preferencias de grupo</string>
<string name="settings_section_title_support">SOPORTE SIMPLEX CHAT</string>
<string name="set_password_to_export">Escribe la contraseña para exportar</string>
<string name="update_database">Actualizar</string>
@@ -841,7 +842,7 @@
<string name="error_smp_test_failed_at_step">Prueba fallida en el paso %s.</string>
<string name="tap_to_start_new_chat">Pulsa para iniciar chat nuevo</string>
<string name="share_message">Compartir mensaje…</string>
<string name="share_image">Compartir medios</string>
<string name="share_image">Compartir imagen</string>
<string name="show_call_on_lock_screen">Mostrar</string>
<string name="unknown_database_error_with_info">Error desconocido en la base de datos: %s</string>
<string name="database_backup_can_be_restored">El intento de cambiar la contraseña de la base de datos no se ha completado.</string>
@@ -858,13 +859,13 @@
<string name="description_via_one_time_link">mediante enlace de un uso</string>
<string name="your_chats">Tus chats</string>
<string name="voice_message_send_text">Mensaje de voz…</string>
<string name="your_contact_address">Mi dirección de contacto</string>
<string name="your_contact_address">Tu dirección de contacto</string>
<string name="icon_descr_video_off">Desactivar vídeo</string>
<string name="icon_descr_video_on">Activar vídeo</string>
<string name="wrong_passphrase">Contraseña de base de datos incorrecta</string>
<string name="wrong_passphrase_title">¡Contraseña incorrecta!</string>
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Te has unido a este grupo. Conectando con miembro del grupo invitado.</string>
<string name="alert_title_cant_invite_contacts_descr">Estás usando un perfil incógnito para este grupo, por tanto para evitar compartir tu perfil principal no se permite invitar contactos</string>
<string name="alert_title_cant_invite_contacts_descr">Estás utilizando un perfil incógnito para este grupo. Para evitar compartir tu perfil principal, invitar contactos no está permitido</string>
<string name="you_are_invited_to_group">Has sido invitado al grupo</string>
<string name="v4_3_voice_messages">Mensajes de voz</string>
<string name="v4_3_irreversible_message_deletion_desc">Tus contactos pueden permitir la eliminación completa de mensajes.</string>
@@ -900,7 +901,7 @@
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando se acepte tu solicitud de conexión, por favor espere o compruébalo más tarde.</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde.</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</string>
<string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal</string>
<string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que has compartido un perfil incógnito, al grupo en el que utilizas tu perfil principal</string>
<string name="simplex_link_mode_browser">mediante navegador</string>
<string name="simplex_link_connection">mediante <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_service_notification_title">Servicio <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
@@ -915,7 +916,7 @@
<string name="snd_group_event_member_deleted">has eliminado <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="group_info_member_you">Tú: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puedes compartir un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán.</string>
<string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten.</string>
<string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil se usará para los grupos a los que te inviten.</string>
<string name="your_preferences">Tus preferencias</string>
<string name="v4_2_auto_accept_contact_requests_desc">Con mensaje de bienvenida opcional.</string>
<string name="you_are_observer">Tu rol es observador</string>
@@ -924,7 +925,7 @@
<string name="you_invited_your_contact">Has invitado a tu contacto</string>
<string name="you_will_be_connected_when_group_host_device_is_online">Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde.</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Tu contacto puede escanear el código QR desde la aplicación.</string>
<string name="your_settings">Mi configuración</string>
<string name="your_settings">Tu configuración</string>
<string name="your_SMP_servers">Tus servidores SMP</string>
<string name="you_control_your_chat">¡Tú controlas tu chat!</string>
<string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</string>
@@ -934,8 +935,8 @@
<string name="icon_descr_video_call">Videollamada</string>
<string name="your_calls">Tus llamadas</string>
<string name="your_ice_servers">Tus servidores ICE</string>
<string name="your_privacy">Privacidad</string>
<string name="settings_section_title_you">MIS DATOS</string>
<string name="your_privacy">Tu privacidad</string>
<string name="settings_section_title_you">TU</string>
<string name="your_chat_database">Base de datos Chat</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.</string>
<string name="you_sent_group_invitation">Has enviado una invitación de grupo</string>
@@ -961,20 +962,22 @@
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> mensaje(s) omitido(s)</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Dejarás de recibir mensajes de este grupo. El historial del chat se conservará.</string>
<string name="view_security_code">Ver código de seguridad</string>
<string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos.</string>
<string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz debes permitir que tu contacto pueda enviarlos.</string>
<string name="voice_messages_prohibited">¡Mensajes de voz prohibidos!</string>
<string name="group_main_profile_sent">Tu perfil Chat será enviado a los miembros del grupo</string>
<string name="icon_descr_address">Dirección <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="image_descr_simplex_logo">Logo <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="icon_descr_simplex_team">Equipo <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="your_profile_will_be_sent">Tu perfil Chat se enviará a tu contacto</string>
<string name="your_chat_profiles">Mis perfiles</string>
<string name="your_simplex_contact_address">Mi dirección de contacto <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="your_chat_profiles">Tus perfiles Chat</string>
<string name="your_simplex_contact_address">Tu dirección de contacto <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="smp_servers_your_server">Tu servidor</string>
<string name="smp_servers_your_server_address">Dirección de tu servidor</string>
<string name="section_title_welcome_message">MENSAJE DE BIENVENIDA</string>
<string name="your_current_profile">Tu perfil actual</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. Los servidores <xliff:g id="appName">SimpleX</xliff:g> no pueden ver tu perfil.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.
\n
\nLos servidores <xliff:g id="appName">SimpleX</xliff:g> no pueden ver tu perfil.</string>
<string name="language_system">Sistema</string>
<string name="button_add_welcome_message">Agregar mensaje de bienvenida</string>
<string name="v4_6_audio_video_calls">Llamadas y videollamadas</string>
@@ -990,7 +993,7 @@
<string name="user_hide">Ocultar</string>
<string name="user_mute">Silenciar</string>
<string name="save_and_update_group_profile">Guardar y actualizar perfil del grupo</string>
<string name="tap_to_activate_profile">Pulsa sobre un perfil para activarlo.</string>
<string name="tap_to_activate_profile">Pulsa para activar el perfil.</string>
<string name="should_be_at_least_one_visible_profile">Debe haber al menos un perfil de usuario visible.</string>
<string name="user_unhide">Mostrar</string>
<string name="button_welcome_message">Mensaje de bienvenida</string>
@@ -1001,7 +1004,7 @@
<string name="muted_when_inactive">¡Silenciado cuando está inactivo!</string>
<string name="v4_6_group_moderation">Moderación de grupos</string>
<string name="v4_6_hidden_chat_profiles">Perfiles Chat ocultos</string>
<string name="v4_6_hidden_chat_profiles_descr">¡Protege tus perfiles con contraseña!</string>
<string name="v4_6_hidden_chat_profiles_descr">¡Protege tus perfiles Chat con contraseña!</string>
<string name="v4_6_audio_video_calls_descr">Soporte bluetooth y otras mejoras.</string>
<string name="v4_6_group_welcome_message_descr">¡Establece el mensaje mostrado a los miembros nuevos!</string>
<string name="v4_6_chinese_spanish_interface">Interfaz en chino y español</string>
@@ -1020,21 +1023,23 @@
\n- borrar mensajes de los miembros.
\n- desactivar el rol a miembros (a rol \"observador\")</string>
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Tus perfiles Chat.</string>
<string name="settings_send_files_via_xftp">Enviar archivos mediante XFTP</string>
<string name="database_upgrade">Actualización de la base de datos</string>
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
<string name="database_downgrade">Degradación de la base de datos</string>
<string name="invalid_migration_confirmation">Confirmación de migración no válida</string>
<string name="upgrade_and_open_chat">Actualizar y abrir Chat</string>
<string name="database_migrations">Migraciones: %s</string>
<string name="mtr_error_different">migración diferente en la aplicación/base de datos: %s / %s</string>
<string name="downgrade_and_open_chat">Volver a versión anterior y abrir Chat</string>
<string name="downgrade_and_open_chat">Degradar y abrir Chat</string>
<string name="database_downgrade_warning">Atención: ¡puedes perder algunos datos!</string>
<string name="incompatible_database_version">Versión de base de datos incompatible</string>
<string name="confirm_database_upgrades">Confirmar actualizaciones de la bases de datos</string>
<string name="mtr_error_no_down_migration">la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s</string>
<string name="mtr_error_no_down_migration">la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia abajo para: %s</string>
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
<string name="developer_options">ID de base de datos y opción de aislamiento de transporte.</string>
<string name="file_will_be_received_when_contact_completes_uploading">El archivo se recibirá cuando tu contacto termine de subirlo.</string>
<string name="image_will_be_received_when_contact_completes_uploading">La imagen se recibirá cuando tu contacto termine de subirla.</string>
<string name="xftp_requires_v461">Se requiere v4.6.1+ para recibir vía XFTP.</string>
<string name="show_developer_options">Mostrar opciones de desarrollador</string>
<string name="hide_dev_options">Ocultar:</string>
<string name="show_dev_options">Mostrar:</string>
@@ -1045,99 +1050,4 @@
<string name="unhide_chat_profile">Mostrar perfil de chat</string>
<string name="unhide_profile">Mostrar perfil</string>
<string name="delete_profile">Eliminar perfil</string>
<string name="video_descr">Vídeo</string>
<string name="video_will_be_received_when_contact_is_online">El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</string>
<string name="waiting_for_video">Esperando el vídeo</string>
<string name="icon_descr_video_asked_to_receive">Ha pedido recibir el video</string>
<string name="videos_limit_title">¡Demasiados vídeos!</string>
<string name="icon_descr_video_snd_complete">Vídeo enviado</string>
<string name="video_will_be_received_when_contact_completes_uploading">El vídeo se recibirá cuando tu contacto termine de subirlo.</string>
<string name="videos_limit_desc">Solo se pueden enviar 10 vídeos de forma simultánea</string>
<string name="icon_descr_waiting_for_video">Esperando el vídeo</string>
<string name="error_saving_xftp_servers">Error guardando servidores SMP</string>
<string name="error_loading_xftp_servers">Error cargando servidores XFTP</string>
<string name="error_loading_smp_servers">Error cargando servidores SMP</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor XFTP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
<string name="error_xftp_test_server_auth">El servidor requiere autorización para subir, comprueba la contraseña</string>
<string name="smp_server_test_compare_file">Comparar archivo</string>
<string name="smp_server_test_create_file">Crear archivo</string>
<string name="smp_server_test_delete_file">Eliminar archivo</string>
<string name="smp_server_test_upload_file">Subir archivo</string>
<string name="xftp_servers">Servidores XFTP</string>
<string name="your_XFTP_servers">Tus servidores XFTP</string>
<string name="port_verb">Puerto</string>
<string name="network_proxy_port">puerto %d</string>
<string name="disable_onion_hosts_when_not_supported">Establece <i>Usar hosts .onion</i> en No si el proxy SOCKS no los admite.</string>
<string name="smp_server_test_download_file">Descargar archivo</string>
<string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string>
<string name="host_verb">Host</string>
<string name="network_socks_proxy_settings">Configuración proxy SOCKS</string>
<string name="la_authenticate">Autenticar</string>
<string name="la_current_app_passcode">Código de acceso actual</string>
<string name="authentication_cancelled">Autenticación cancelada</string>
<string name="change_lock_mode">Cambiar el modo de bloqueo</string>
<string name="la_seconds">%d segundos</string>
<string name="la_auth_failed">Error de autenticación</string>
<string name="la_change_app_passcode">Cambiar el código de acceso</string>
<string name="passcode_not_changed">¡Código de acceso no cambiado!</string>
<string name="la_lock_mode_system">Autenticación del sistema</string>
<string name="la_no_app_password">Sin código de acceso de la aplicación</string>
<string name="la_lock_mode_passcode">Campo de código de acceso</string>
<string name="la_lock_mode">Modo Bloqueo SimpleX</string>
<string name="la_could_not_be_verified">No has podido ser verificado. Inténtelo de nuevo.</string>
<string name="la_minutes">%d minutos</string>
<string name="la_enter_app_passcode">Introducir el código de acceso</string>
<string name="la_immediately">Inmediatamente</string>
<string name="la_please_remember_to_store_password">Por favor, recuerda y guarda la contraseña en un lugar seguro. ¡No hay ninguna manera de recuperar una contraseña perdida!</string>
<string name="lock_not_enabled">¡Bloqueo SimpleX no activado!</string>
<string name="you_can_turn_on_lock">Puedes activar el Bloqueo SimpleX a través de Configuración.</string>
<string name="confirm_passcode">Confirmar el código de acceso</string>
<string name="enable_lock">Activar bloqueo</string>
<string name="lock_after">Bloquear después de</string>
<string name="lock_mode">Modo de bloqueo</string>
<string name="submit_passcode">Enviar</string>
<string name="incorrect_passcode">Código de acceso incorrecto</string>
<string name="new_passcode">Nuevo código de acceso</string>
<string name="la_mode_passcode">Código de acceso</string>
<string name="passcode_changed">¡Código de acceso cambiado!</string>
<string name="passcode_set">¡Código de acceso guardado!</string>
<string name="la_mode_system">Sistema</string>
<string name="decryption_error">Error de descifrado</string>
<string name="alert_text_msg_bad_id">El ID del siguiente mensaje es incorrecto (menor o igual que el anterior).
\nPuede ocurrir por algún bug o cuando la conexión está comprometida.</string>
<string name="alert_text_fragment_please_report_to_developers">Por favor, informa a los desarrolladores.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> mensajes omitidos.</string>
<string name="alert_title_msg_bad_hash">Hash de mensaje incorrecto</string>
<string name="alert_title_msg_bad_id">ID de mensaje incorrecto</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.</string>
<string name="alert_text_msg_bad_hash">El hash del mensaje anterior es diferente.</string>
<string name="alert_text_fragment_permanent_error_reconnect">El error es permanente para esta conexión, por favor vuelve a conectarte.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> mensajes no pudieron ser descifrados.</string>
<string name="no_spaces">¡Sin espacios!</string>
<string name="stop_file__action">Detener archivo</string>
<string name="revoke_file__message">El archivo será eliminado de los servidores.</string>
<string name="stop_rcv_file__message">Se detendrá la recepción del archivo.</string>
<string name="revoke_file__confirm">Revocar</string>
<string name="revoke_file__action">Revocar archivo</string>
<string name="revoke_file__title">¿Revocar archivo\?</string>
<string name="stop_snd_file__message">Se detendrá el envío del archivo.</string>
<string name="stop_file__confirm">Detener</string>
<string name="stop_rcv_file__title">¿Dejar de recibir el archivo\?</string>
<string name="stop_snd_file__title">¿Dejar de enviar el archivo\?</string>
<string name="allow_calls_only_if">Se permiten llamadas sólo si tu contacto también las permite.</string>
<string name="allow_your_contacts_to_call">Permites que tus contactos puedan llamarte.</string>
<string name="audio_video_calls">Llamadas/Videollamadas</string>
<string name="calls_prohibited_with_this_contact">Las llamadas/videollamadas no están permitidas.</string>
<string name="available_in_v51">"
\nDisponible en v5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Tanto tú como tu contacto podéis realizar llamadas.</string>
<string name="only_you_can_make_calls">Solo tú puedes realizar llamadas.</string>
<string name="only_your_contact_can_make_calls">Sólo tu contacto puede realizar llamadas.</string>
<string name="prohibit_calls">Prohibir las llamadas/videollamadas.</string>
<string name="v5_0_app_passcode">Código de acceso a la aplicación</string>
<string name="v5_0_polish_interface">Interfaz polaco</string>
<string name="v5_0_app_passcode_descr">Úsalo en lugar de la autenticación del sistema.</string>
<string name="v5_0_polish_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
<string name="v5_0_large_files_support">Vídeos y archivos de hasta 1Gb</string>
<string name="v5_0_large_files_support_descr">¡Rápido y sin tener que esperar a que el remitente esté en línea!</string>
</resources>

View File

@@ -156,7 +156,7 @@
<string name="contact_connection_pending">connexion…</string>
<string name="group_connection_pending">connexion…</string>
<string name="share_message">Partager le message…</string>
<string name="share_image">Partager le média</string>
<string name="share_image">Partager limage</string>
<string name="images_limit_desc">Envoi de 10 images en même temps maximum</string>
<string name="personal_welcome">Bienvenue <xliff:g>%1$s</xliff:g> !</string>
<string name="notifications_mode_periodic_desc">Vérifie les nouveaux messages toutes les 10 minutes pendant 1 minute au maximum.</string>
@@ -201,9 +201,7 @@
<string name="to_share_with_your_contact">(à partager avec votre contact)</string>
<string name="create_group">Créer un groupe secret</string>
<string name="from_gallery_button">Depuis la Phototèque</string>
<string name="choose_file">Fichier</string>
<string name="gallery_image_button">Image</string>
<string name="gallery_video_button">Vidéo</string>
<string name="choose_file">Choisir le fichier</string>
<string name="to_start_a_new_chat_help_header">Pour démarrer une nouvelle discussion</string>
<string name="chat_help_tap_button">Appuyez sur le bouton</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scanner un code QR</b> : pour vous connecter à votre contact qui vous montre un code QR.</string>
@@ -296,7 +294,7 @@
<string name="waiting_for_file">En attente du fichier</string>
<string name="voice_message">Message vocal</string>
<string name="toast_permission_denied">Autorisation refusée !</string>
<string name="use_camera_button">Appareil\nphoto</string>
<string name="use_camera_button">Utiliser l\'Appareil photo</string>
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé <xliff:g id="appNameFull">SimpleX Chat</xliff:g> !</string>
<string name="you_can_connect_to_simplex_chat_founder">Vous pouvez <font color="#0088ff">vous connecter aux développeurs de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour leur poser des questions et recevoir des réponses :</font>.</string>
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
@@ -439,7 +437,9 @@
<string name="section_title_welcome_message">MESSAGE DE BIENVENUE</string>
<string name="display_name__field">Nom affiché :</string>
<string name="full_name__field">Nom complet :</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts. Les serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts.
\n
\nLes serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
<string name="delete_image">Supprimer l\'image</string>
<string name="save_preferences_question">Sauvegarder les préférences \?</string>
<string name="you_control_your_chat">Vous maîtrisez vos discussions !</string>
@@ -475,10 +475,12 @@
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> message⸱s manqué⸱s</string>
<string name="integrity_msg_bad_id">ID de message incorrecte</string>
<string name="settings_section_title_settings">PARAMÈTRES</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Cela peut arriver quand :
\n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours.
\n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.
\n3. La connexion a été compromise.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">C\'est possible quand :
\n1. Les messages expirent du serveur (après 30 jours si ils ne sont pas reçu).
\n2. Le serveur que vous utilisez pour recevoir les messages de ce contact a été mise à jour ou redémarré.
\n3. La connection est compromise.
\nVeuillez vous connecter aux développeurs via les Paramètres pour recevoir les mises à jour concernant les serveurs.
\nNous allons ajouter une redondance des serveurs pour éviter la perte de messages.</string>
<string name="icon_descr_call_rejected">Appel rejeté</string>
<string name="rcv_group_event_member_deleted">a retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="snd_group_event_member_deleted">vous avez retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
@@ -547,7 +549,7 @@
<string name="settings_experimental_features">Fonctionnalités expérimentales</string>
<string name="settings_section_title_socks">SOCKS PROXY</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES ET FICHIERS</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">APPELS</string>
<string name="import_database">Importer la base de données</string>
<string name="new_database_archive">Nouvelle archive de base de données</string>
@@ -852,7 +854,7 @@
<string name="group_is_decentralized">Le groupe est entièrement décentralisé il n\'est visible que par ses membres.</string>
<string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string>
<string name="network_options_revert">Revenir en arrière</string>
<string name="prohibit_sending_disappearing_messages">Interdire lenvoi de messages éphémères.</string>
<string name="prohibit_sending_disappearing_messages">Interdit lenvoi de messages éphémères.</string>
<string name="incognito_info_protects">Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé.</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string>
<string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</string>
@@ -885,7 +887,7 @@
<string name="allow_to_send_voice">Autoriser l\'envoi de messages vocaux.</string>
<string name="prohibit_sending_voice">Interdire l\'envoi de messages vocaux.</string>
<string name="allow_to_send_disappearing">Autorise lenvoi de messages éphémères.</string>
<string name="prohibit_sending_disappearing">Interdire lenvoi de messages éphémères.</string>
<string name="prohibit_sending_disappearing">Interdit lenvoi de messages éphémères.</string>
<string name="prohibit_message_deletion">Interdire la suppression irréversible des messages.</string>
<string name="group_members_can_send_dms">Les membres du groupe peuvent envoyer des messages directs.</string>
<string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans ce groupe.</string>
@@ -923,6 +925,7 @@
<string name="core_simplexmq_version">simplexmq : v%s (%2s)</string>
<string name="app_version_code">Build de l\'app : %s</string>
<string name="app_version_name">Version de l\'app : v%s</string>
<string name="core_build_timestamp">Cœur compilé le : %s</string>
<string name="core_version">Version du cœur : v%s</string>
<string name="network_option_ping_count">Nombre de PING</string>
<string name="users_delete_all_chats_deleted">Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !</string>
@@ -1019,6 +1022,7 @@
<string name="group_welcome_title">Message d\'accueil</string>
<string name="you_can_hide_or_mute_user_profile">Vous pouvez masquer ou mettre en sourdine un profil d\'utilisateur - maintenez-le enfoncé pour accéder au menu.</string>
<string name="you_will_still_receive_calls_and_ntfs">Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu\'ils sont actifs.</string>
<string name="settings_send_files_via_xftp">Envoi de fichiers via XFTP</string>
<string name="database_downgrade">Rétrogradation de la base de données</string>
<string name="database_upgrade">Mise à niveau de la base de données</string>
<string name="incompatible_database_version">Version de la base de données incompatible</string>
@@ -1034,6 +1038,7 @@
<string name="show_dev_options">Afficher :</string>
<string name="show_developer_options">Afficher les options pour les développeurs</string>
<string name="file_will_be_received_when_contact_completes_uploading">Le fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.</string>
<string name="xftp_requires_v461">v4.6.1+ nécessaire pour la réception via XFTP.</string>
<string name="developer_options">IDs de base de données et option d\'isolation du transport.</string>
<string name="settings_section_title_experimenta">EXPÉRIMENTALE</string>
<string name="hide_dev_options">Cacher :</string>
@@ -1044,99 +1049,4 @@
<string name="cancel_file__question">Annuler le transfert de fichiers \?</string>
<string name="file_transfer_will_be_cancelled_warning">Le transfert de fichiers sera annulé. S\'il est en cours, il sera interrompu.</string>
<string name="profile_password">Mot de passe de profil</string>
<string name="videos_limit_title">Trop de vidéos !</string>
<string name="video_descr">Vidéo</string>
<string name="icon_descr_video_snd_complete">Vidéo envoyée</string>
<string name="video_will_be_received_when_contact_completes_uploading">La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer.</string>
<string name="icon_descr_waiting_for_video">En attente de la vidéo</string>
<string name="waiting_for_video">En attente de la vidéo</string>
<string name="video_will_be_received_when_contact_is_online">La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !</string>
<string name="icon_descr_video_asked_to_receive">Requête de réception de la vidéo</string>
<string name="videos_limit_desc">Seulement 10 vidéos peuvent être envoyées en même temps</string>
<string name="smp_server_test_create_file">Créer un fichier</string>
<string name="smp_server_test_delete_file">Supprimer le fichier</string>
<string name="error_saving_xftp_servers">Erreur lors de la sauvegarde des serveurs XFTP</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées.</string>
<string name="error_xftp_test_server_auth">Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe</string>
<string name="smp_server_test_upload_file">Transférer le fichier</string>
<string name="xftp_servers">Serveurs XFTP</string>
<string name="your_XFTP_servers">Vos serveurs XFTP</string>
<string name="smp_server_test_compare_file">Comparer le fichier</string>
<string name="smp_server_test_download_file">Télécharger le fichier</string>
<string name="error_loading_smp_servers">Erreur lors du chargement des serveurs SMP</string>
<string name="error_loading_xftp_servers">Erreur lors du chargement des serveurs XFTP</string>
<string name="host_verb">Héberger</string>
<string name="port_verb">Port</string>
<string name="network_proxy_port">port %d</string>
<string name="network_socks_proxy_settings">Paramètres de proxy SOCKS</string>
<string name="network_socks_toggle_use_socks_proxy">Utiliser un proxy SOCKS</string>
<string name="disable_onion_hosts_when_not_supported">Définissez <i>Utiliser les hôtes .onion</i> sur Non si le proxy SOCKS ne les prend pas en charge.</string>
<string name="la_lock_mode">Mode de SimpleX Lock</string>
<string name="lock_not_enabled">SimpleX Lock n\'est pas activé !</string>
<string name="la_lock_mode_system">Authentification du système</string>
<string name="la_authenticate">Authentification</string>
<string name="la_auth_failed">Echec de l\'authentification</string>
<string name="la_change_app_passcode">Modifier le code d\'accès</string>
<string name="la_current_app_passcode">Code d\'accès actuel</string>
<string name="la_minutes">%d minutes</string>
<string name="la_seconds">%d secondes</string>
<string name="la_enter_app_passcode">Entrer le code d\'accès</string>
<string name="la_immediately">Immédiatement</string>
<string name="la_no_app_password">Pas de code d\'accès à l\'app</string>
<string name="la_lock_mode_passcode">Saisie du code</string>
<string name="la_please_remember_to_store_password">Veuillez le mémoriser ou le conserver en toute sécurité - il n\'y a aucun moyen de récupérer un mot de passe perdu !</string>
<string name="enable_lock">Activer le verrouillage</string>
<string name="change_lock_mode">Modifier le mode de verrouillage</string>
<string name="incorrect_passcode">Code d\'accès erroné</string>
<string name="new_passcode">Nouveau code d\'accès</string>
<string name="la_mode_passcode">Code d\'accès</string>
<string name="passcode_changed">Code d\'accès modifié !</string>
<string name="passcode_not_changed">Le code d\'accès n\'a pas été modifié !</string>
<string name="passcode_set">Code d\'accès défini !</string>
<string name="la_mode_system">Système</string>
<string name="authentication_cancelled">Authentification annulée</string>
<string name="alert_title_msg_bad_id">Mauvais ID de message</string>
<string name="alert_text_msg_bad_hash">Le hash du message précédent est différent.</string>
<string name="alert_text_msg_bad_id">L\'ID du message suivant est incorrect (inférieur ou égal au précédent).
\nCela peut se produire en raison d\'un bug ou lorsque la connexion est compromise.</string>
<string name="decryption_error">Erreur de déchiffrement</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Cela peut se produire lorsque vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.</string>
<string name="alert_title_msg_bad_hash">Mauvais hash de message</string>
<string name="no_spaces">Pas d\'espace !</string>
<string name="allow_calls_only_if">Autoriser les appels que si votre contact les autorise.</string>
<string name="allow_your_contacts_to_call">Autorise vos contacts à vous appeler.</string>
<string name="audio_video_calls">Appels audio/vidéo</string>
<string name="available_in_v51">"
\nDisponible dans la v5.1"</string>
<string name="prohibit_calls">Interdire les appels audio/vidéo.</string>
<string name="revoke_file__message">Le fichier sera supprimé des serveurs.</string>
<string name="revoke_file__confirm">Révoquer</string>
<string name="revoke_file__action">Révoquer le fichier</string>
<string name="revoke_file__title">Révoquer le fichier \?</string>
<string name="stop_snd_file__message">L\'envoi du fichier sera interrompu.</string>
<string name="alert_text_fragment_please_report_to_developers">Veuillez le signaler aux développeurs.</string>
<string name="submit_passcode">Soumettre</string>
<string name="both_you_and_your_contact_can_make_calls">Vous et votre contact pouvez tous deux passer des appels.</string>
<string name="only_you_can_make_calls">Vous seul pouvez passer des appels.</string>
<string name="calls_prohibited_with_this_contact">Interdire les appels audio/vidéo.</string>
<string name="confirm_passcode">Confirmer le code d\'accès</string>
<string name="lock_after">Verrouillage après</string>
<string name="lock_mode">Mode de verrouillage</string>
<string name="only_your_contact_can_make_calls">Seul votre contact peut passer des appels.</string>
<string name="stop_rcv_file__message">La réception du fichier sera interrompue.</string>
<string name="stop_file__confirm">Arrêter</string>
<string name="stop_snd_file__title">Arrêter l\'envoi du fichier \?</string>
<string name="stop_file__action">Arrêter le fichier</string>
<string name="stop_rcv_file__title">Arrêter de recevoir le fichier \?</string>
<string name="alert_text_fragment_permanent_error_reconnect">Cette erreur est persistante pour cette connexion, veuillez vous reconnecter.</string>
<string name="la_could_not_be_verified">Vous n\'avez pas pu être vérifié·e; veuillez réessayer.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messages n\'ont pas pu être déchiffrés.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messages sautés.</string>
<string name="you_can_turn_on_lock">Vous pouvez activer SimpleX Lock dans les Paramètres.</string>
<string name="v5_0_polish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v5_0_large_files_support">Vidéos et fichiers jusqu\'à 1Go</string>
<string name="v5_0_app_passcode">Code d\'accès à l\'app</string>
<string name="v5_0_large_files_support_descr">Rapide et ne nécessitant pas d\'attendre que l\'expéditeur soit en ligne !</string>
<string name="v5_0_polish_interface">Interface en polonais</string>
<string name="v5_0_app_passcode_descr">Il permet de remplacer l\'authentification du système.</string>
</resources>

View File

@@ -180,120 +180,4 @@
<string name="restore_database_alert_confirm">वापस लौटाना</string>
<string name="a_plus_b">ए + बी</string>
<string name="incognito_random_profile">आपका यादृच्छिक प्रोफ़ाइल</string>
<string name="chat_preferences">चैट वरीयताएँ</string>
<string name="ttl_days">%d दिनों</string>
<string name="allow_voice_messages_question">ध्वनि संदेशों की अनुमति दें\?</string>
<string name="clear_chat_menu_action">साफ़</string>
<string name="icon_descr_audio_off">ऑडियो बंद</string>
<string name="icon_descr_audio_on">ऑडियो चालू</string>
<string name="v4_4_disappearing_messages">गायब होने वाले संदेश</string>
<string name="v4_5_reduced_battery_usage_descr">अधिक सुधार जल्द ही आ रहे हैं!</string>
<string name="copy_verb">नकलना</string>
<string name="change_role">भूमिका बदलें</string>
<string name="ttl_hour">%d घंटा</string>
<string name="auth_unavailable">प्रमाणीकरण अनुपलब्ध</string>
<string name="icon_descr_edited">संपादित</string>
<string name="delete_group_question">समूह मिटाएं\?</string>
<string name="contact_already_exists">संपर्क पहले से मौजूद है</string>
<string name="notification_preview_mode_contact">संपर्क नाम</string>
<string name="notification_preview_somebody">संपर्क छुपाया गया:</string>
<string name="delete_verb">मिटाना</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">जब आप पृष्ठभूमि में 30 सेकंड के बाद ऐप को प्रारंभ या फिर से शुरू करते हैं तो आपको प्रमाणित करने की आवश्यकता होगी।</string>
<string name="icon_descr_asked_to_receive">प्रतिमा प्राप्त करने को कहा</string>
<string name="back">वापस</string>
<string name="icon_descr_close_button">बंद करें बटन</string>
<string name="create_address">पता बनाना</string>
<string name="invite_prohibited">संपर्क आमंत्रित नहीं कर सकते!</string>
<string name="icon_descr_contact_checked">संपर्क जांचा गया</string>
<string name="contact_preferences">संपर्क वरीयताएँ</string>
<string name="rcv_group_event_changed_your_role">आपकी भूमिका को %s में बदल दिया</string>
<string name="clear_contacts_selection_button">साफ़</string>
<string name="timed_messages">गायब होने वाले संदेश</string>
<string name="display_name__field">प्रदर्शित होने वाला नाम:</string>
<string name="theme_dark">अँधेरा</string>
<string name="allow_your_contacts_to_send_voice_messages">अपने संपर्कों को ध्वनि संदेश भेजने की अनुमति दें।</string>
<string name="ttl_day">%d दिन</string>
<string name="ttl_hours">%d घंटे</string>
<string name="v4_6_chinese_spanish_interface">चीनी और स्पेनिश इंटरफ़ेस</string>
<string name="simplex_link_mode_description">विवरण</string>
<string name="notifications_mode_off_desc">ऐप केवल तभी सूचनाएं प्राप्त कर सकता है जब वह चल रहा हो, कोई पृष्ठभूमि सेवा प्रारंभ नहीं की जाएगी</string>
<string name="edit_verb">संपादन करना</string>
<string name="decentralized">विकेन्द्रीकृत</string>
<string name="icon_descr_call_ended">कॉल समाप्त</string>
<string name="icon_descr_call_progress">कॉल चल रहा है</string>
<string name="auto_accept_images">छवियों को स्वत: स्वीकार करें</string>
<string name="chat_archive_header">चैट संग्रह</string>
<string name="chat_is_stopped_indication">चैट रोक दी गई है</string>
<string name="chat_archive_section">चैट संग्रह</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">आप इस समूह से संदेश प्राप्त करना बंद कर देंगे। चैट इतिहास संरक्षित किया जाएगा।</string>
<string name="rcv_group_event_changed_member_role">%s की भूमिका को %s में बदला</string>
<string name="group_member_status_complete">पूर्ण</string>
<string name="disappearing_prohibited_in_this_chat">इस चैट में गायब होने वाले संदेश प्रतिबंधित हैं।</string>
<string name="disappearing_messages_are_prohibited">इस चैट में गायब होने वाले संदेश प्रतिबंधित हैं।</string>
<string name="chat_with_developers">डेवलपर्स के साथ चैट करें</string>
<string name="alert_title_contact_connection_pending">संपर्क अभी तक जुड़ा नहीं है!</string>
<string name="chat_console">चैट कंसोल</string>
<string name="all_your_contacts_will_remain_connected">आपके सभी संपर्क जुड़े रहेंगे।</string>
<string name="network_session_mode_user">चैट प्रोफ़ाइल</string>
<string name="contact_requests">संपर्क अनुरोध</string>
<string name="create_profile_button">बनाएं</string>
<string name="callstatus_error">कॉल त्रुटि</string>
<string name="callstatus_in_progress">कॉल चल रहा है</string>
<string name="always_use_relay">हमेशा रिले का प्रयोग करें</string>
<string name="icon_descr_audio_call">ऑडियो कॉल</string>
<string name="settings_audio_video_calls">ऑडियो और वीडियो कॉल</string>
<string name="call_already_ended">कॉल पहले ही खत्म हो चुकी है!</string>
<string name="no_call_on_lock_screen">अक्षम करना</string>
<string name="status_contact_has_e2e_encryption">संपर्क में ई2ई एन्क्रिप्शन है</string>
<string name="status_contact_has_no_e2e_encryption">संपर्क में कोई ई2ई एन्क्रिप्शन नहीं है</string>
<string name="chat_is_running">चैट चल रही है</string>
<string name="chat_is_stopped">चैट रोक दी गई है</string>
<string name="rcv_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string>
<string name="ttl_min">%d मिनट</string>
<string name="settings_section_title_device">उपकरण</string>
<string name="group_member_status_creator">रचनाकार</string>
<string name="change_member_role_question">समूह भूमिका बदलें\?</string>
<string name="ttl_week">%d सप्ताह</string>
<string name="v4_2_auto_accept_contact_requests">संपर्क अनुरोधों को स्वत: स्वीकार करें</string>
<string name="integrity_msg_bad_hash">खराब संदेश हैश</string>
<string name="allow_voice_messages_only_if">ध्वनि संदेशों को केवल तभी अनुमति दें यदि आपका संपर्क उन्हें अनुमति देता है।</string>
<string name="incognito_random_profile_from_contact_description">जिस संपर्क से आपने यह लिंक प्राप्त किया है, उसे एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string>
<string name="callstatus_calling">कॉल कर रहा है…</string>
<string name="feature_cancelled_item">रद्द %s</string>
<string name="rcv_conn_event_switch_queue_phase_completed">आपके लिए पता बदल दिया</string>
<string name="notifications_mode_periodic_desc">प्रत्येक 10 मिनट में 1 मिनट तक नए संदेशों की जाँच करता है</string>
<string name="ttl_sec">%d सेकंड</string>
<string name="ttl_month">%d महीना</string>
<string name="ttl_months">%d महीने</string>
<string name="settings_developer_tools">डेवलपर उपकरण</string>
<string name="incognito_random_profile_description">आपके संपर्क को एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string>
<string name="snd_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string>
<string name="button_add_welcome_message">स्वागत संदेश जोड़ें</string>
<string name="allow_your_contacts_irreversibly_delete">अपने संपर्कों को भेजे गए संदेशों को अपरिवर्तनीय रूप से हटाने की अनुमति दें।</string>
<string name="allow_to_send_voice">ध्वनि संदेश भेजने की अनुमति दें।</string>
<string name="alert_title_cant_invite_contacts">संपर्कों को आमंत्रित नहीं कर सकते!</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s के लिए पता बदल रहा है…</string>
<string name="dont_show_again">दोबारा मत दिखाओ</string>
<string name="both_you_and_your_contact_can_send_voice">आप और आपका संपर्क दोनों ध्वनि संदेश भेज सकते हैं।</string>
<string name="settings_section_title_develop">विकास करना</string>
<string name="user_mute">आवाज़ बंद करना</string>
<string name="allow_your_contacts_to_send_disappearing_messages">अपने संपर्कों को गायब होने वाले संदेश भेजने की अनुमति दें।</string>
<string name="cancel_file__question">फ़ाइल स्थानांतरण रद्द करें\?</string>
<string name="v4_6_audio_video_calls">ऑडियो और वीडियो कॉल</string>
<string name="v4_6_reduced_battery_usage_descr">अधिक सुधार जल्द ही आ रहे हैं!</string>
<string name="icon_descr_video_asked_to_receive">वीडियो प्राप्त करने के लिए कहा</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">संपर्क और सभी संदेशों को हटा दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
<string name="ok">ठीक है</string>
<string name="save_servers_button">बचाना</string>
<string name="language_system">प्रणाली</string>
<string name="theme_system">प्रणाली</string>
<string name="network_options_save">बचाना</string>
<string name="chat_preferences_on">चालू</string>
<string name="la_lock_mode_system">प्रणाली प्रमाणीकरण</string>
<string name="la_mode_system">प्रणाली</string>
<string name="your_SMP_servers">आपका एसएमपी सर्वर</string>
<string name="messages_section_title">संदेशों</string>
<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>
</resources>

View File

@@ -152,7 +152,7 @@
<string name="tap_to_start_new_chat">Tocca per iniziare una conversazione</string>
<string name="chat_with_developers">Scrivi agli sviluppatori</string>
<string name="you_have_no_chats">Non hai chat</string>
<string name="share_image">Condividi multimediale…</string>
<string name="share_image">Condividi immagine…</string>
<string name="share_file">Condividi file…</string>
<string name="icon_descr_context">Icona contestuale</string>
<string name="icon_descr_cancel_file_preview">Annulla anteprima file</string>
@@ -298,7 +298,7 @@
<string name="display_name">Nome da mostrare</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>Aggiungi un contatto</b>: per creare il tuo codice QR una tantum per il tuo contatto.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scansiona codice QR</b>: per connetterti al contatto che ti mostra il codice QR.</string>
<string name="choose_file">File</string>
<string name="choose_file">Scegli file</string>
<string name="clear_chat_button">Svuota chat</string>
<string name="clear_chat_question">Svuotare la chat\?</string>
<string name="clear_verb">Svuota</string>
@@ -308,8 +308,6 @@
<string name="create_group">Crea gruppo segreto</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scansiona dall\'app il codice QR mostrato, tramite <b>Scansiona codice QR</b>.</string>
<string name="from_gallery_button">Dalla Galleria</string>
<string name="gallery_image_button">Immagine</string>
<string name="gallery_video_button">Video</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Se scegli di rifiutare, il mittente NON verrà avvisato.</string>
<string name="clear_chat_menu_action">Svuota</string>
<string name="icon_descr_close_button">Pulsante di chiusura</string>
@@ -547,7 +545,7 @@
<string name="to_connect_via_link_title">Per connettersi via link</string>
<string name="to_share_with_your_contact">(da condividere con il tuo contatto)</string>
<string name="to_start_a_new_chat_help_header">Per iniziare una nuova chat</string>
<string name="use_camera_button">Fotocamera</string>
<string name="use_camera_button">Usa la fotocamera</string>
<string name="you_can_connect_to_simplex_chat_founder">Puoi <font color="#0088ff">connetterti con gli sviluppatori di <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per porre domande e ricevere aggiornamenti</font>.</string>
<string name="invalid_contact_link">Link non valido!</string>
<string name="invalid_QR_code">Codice QR non valido</string>
@@ -667,7 +665,9 @@
<string name="you_control_your_chat">Sei tu a controllare la tua chat!</string>
<string name="your_current_profile">Il tuo profilo attuale</string>
<string name="your_profile_is_stored_on_your_device">Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di <xliff:g id="appName">SimpleX</xliff:g> non possono vedere il tuo profilo.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.
\n
\nI server di <xliff:g id="appName">SimpleX</xliff:g> non possono vedere il tuo profilo.</string>
<string name="ignore">Ignora</string>
<string name="immune_to_spam_and_abuse">Immune a spam e abusi</string>
<string name="incoming_audio_call">Chiamata in arrivo</string>
@@ -696,9 +696,11 @@
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> vuole connettersi con te via</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">Puoi controllare attraverso quale/i server <b>ricevere</b> i messaggi, i tuoi contatti i server che usi per inviare loro i messaggi.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Può accadere quando:
\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.
\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio.
\n3. La connessione è stata compromessa.</string>
\n1. I messaggi scadono sul server se non sono stati ricevuti per 30 giorni,
\n2. Il server usato per ricevere i messaggi da questo contatto è stato aggiornato e riavviato.
\n3. La connessione è compromessa.
\nConnettiti agli sviluppatori tramite Impostazioni per ricevere aggiornamenti riguardo i server.
\nAggiungeremo la ridondanza del server per prevenire la perdita di messaggi.</string>
<string name="icon_descr_call_rejected">Chiamata rifiutata</string>
<string name="icon_descr_call_missed">Chiamata persa</string>
<string name="status_no_e2e_encryption">nessuna crittografia e2e</string>
@@ -726,7 +728,7 @@
<string name="import_database_question">Importare il database della chat\?</string>
<string name="import_database">Importa database</string>
<string name="settings_section_title_incognito">Modalità incognito</string>
<string name="settings_section_title_messages">MESSAGGI E FILE</string>
<string name="settings_section_title_messages">MESSAGGI</string>
<string name="new_database_archive">Nuovo archivio database</string>
<string name="old_database_archive">Vecchio archivio del database</string>
<string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string>
@@ -925,6 +927,7 @@
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="app_version_code">Build dell\'app: %s</string>
<string name="app_version_name">Versione app: v%s</string>
<string name="core_build_timestamp">Core compilato il: %s</string>
<string name="smp_servers_per_user">I server per le nuove connessioni del profilo di chat attuale</string>
<string name="users_add">Aggiungi profilo</string>
<string name="users_delete_question">Eliminare il profilo di chat\?</string>
@@ -1019,6 +1022,7 @@
<string name="user_hide">Nascondi</string>
<string name="v4_6_group_welcome_message_descr">Imposta il messaggio mostrato ai nuovi membri!</string>
<string name="user_unmute">Riattiva audio</string>
<string name="settings_send_files_via_xftp">Invia file via XFTP</string>
<string name="database_downgrade">Downgrade del database</string>
<string name="database_upgrade">Aggiornamento del database</string>
<string name="incompatible_database_version">Versione del database incompatibile</string>
@@ -1027,6 +1031,7 @@
<string name="hide_dev_options">Nascondi:</string>
<string name="show_dev_options">Mostra:</string>
<string name="show_developer_options">Mostra opzioni sviluppatore</string>
<string name="xftp_requires_v461">v4.6.1+ necessaria per ricevere via XFTP.</string>
<string name="downgrade_and_open_chat">Esegui downgrade e apri chat</string>
<string name="database_migrations">Migrazioni: %s</string>
<string name="database_downgrade_warning">Attenzione: potresti perdere alcuni dati!</string>
@@ -1044,99 +1049,4 @@
<string name="delete_chat_profile">Elimina il profilo di chat</string>
<string name="delete_profile">Elimina profilo</string>
<string name="profile_password">Password del profilo</string>
<string name="videos_limit_desc">È possibile inviare solo 10 video contemporaneamente</string>
<string name="videos_limit_title">Troppi video!</string>
<string name="icon_descr_video_asked_to_receive">Richiesta di ricevere il video</string>
<string name="video_descr">Video</string>
<string name="icon_descr_video_snd_complete">Video inviato</string>
<string name="video_will_be_received_when_contact_completes_uploading">Il video verrà ricevuto quando il tuo contatto completerà l\'invio.</string>
<string name="video_will_be_received_when_contact_is_online">Il video verrà ricevuto quando il tuo contatto sarà in linea, attendi o controlla più tardi!</string>
<string name="icon_descr_waiting_for_video">In attesa del video</string>
<string name="waiting_for_video">In attesa del video</string>
<string name="error_loading_xftp_servers">Errore nel caricamento dei server XFTP</string>
<string name="error_saving_xftp_servers">Errore nel salvataggio dei server XFTP</string>
<string name="error_xftp_test_server_auth">Il server richiede l\'autorizzazione per l\'invio, controlla la password</string>
<string name="smp_server_test_compare_file">Confronta file</string>
<string name="smp_server_test_create_file">Crea file</string>
<string name="smp_server_test_download_file">Scarica file</string>
<string name="smp_server_test_upload_file">Invia file</string>
<string name="xftp_servers">Server XFTP</string>
<string name="your_XFTP_servers">I tuoi server XFTP</string>
<string name="network_socks_proxy_settings">Impostazioni proxy SOCKS</string>
<string name="network_socks_toggle_use_socks_proxy">Usa proxy SOCKS</string>
<string name="host_verb">Host</string>
<string name="port_verb">Porta</string>
<string name="network_proxy_port">porta %d</string>
<string name="disable_onion_hosts_when_not_supported">Imposta <i>Usa gli host .onion</i> su No se il proxy SOCKS non li supporta.</string>
<string name="smp_server_test_delete_file">Elimina file</string>
<string name="error_loading_smp_servers">Errore nel caricamento dei server SMP</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi del server XFTP siano nel formato corretto, uno per riga e non doppi.</string>
<string name="la_auth_failed">Autenticazione fallita</string>
<string name="la_lock_mode_passcode">Inserimento del codice di accesso</string>
<string name="la_lock_mode">Modalità di SimpleX Lock</string>
<string name="la_lock_mode_system">Autenticazione di sistema</string>
<string name="la_authenticate">Autentica</string>
<string name="la_change_app_passcode">Cambia codice di accesso</string>
<string name="la_current_app_passcode">Codice di accesso attuale</string>
<string name="la_minutes">%d minuti</string>
<string name="la_seconds">%d secondi</string>
<string name="la_enter_app_passcode">Inserisci il codice di accesso</string>
<string name="la_immediately">Immediatamente</string>
<string name="la_no_app_password">Nessun codice di accesso dell\'app</string>
<string name="la_please_remember_to_store_password">Ricordala o conservala in modo sicuro: non c\'è modo di recuperare una password persa!</string>
<string name="lock_not_enabled">SimpleX Lock non attivato!</string>
<string name="you_can_turn_on_lock">Puoi attivare SimpleX Lock tramite le impostazioni.</string>
<string name="la_could_not_be_verified">Non è stato possibile verificarti, riprova.</string>
<string name="confirm_passcode">Conferma il codice di accesso</string>
<string name="la_mode_passcode">Codice di accesso</string>
<string name="passcode_changed">Codice di accesso cambiato!</string>
<string name="passcode_not_changed">Codice di accesso non cambiato!</string>
<string name="passcode_set">Codice di accesso impostato!</string>
<string name="submit_passcode">Invia</string>
<string name="la_mode_system">Sistema</string>
<string name="change_lock_mode">Cambia modalità di blocco</string>
<string name="authentication_cancelled">Autenticazione annullata</string>
<string name="lock_after">Blocca dopo</string>
<string name="enable_lock">Attiva blocco</string>
<string name="incorrect_passcode">Codice di accesso errato</string>
<string name="new_passcode">Nuovo codice di accesso</string>
<string name="lock_mode">Modalità di blocco</string>
<string name="decryption_error">Errore di decifrazione</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messaggi saltati.</string>
<string name="alert_title_msg_bad_id">ID del messaggio errato</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Può accadere quando tu o il tuo contatto avete usato il backup del database vecchio.</string>
<string name="alert_text_msg_bad_id">L\'ID del messaggio successivo non è corretto (inferiore o uguale al precedente).
\nPuò accadere a causa di qualche bug o quando la connessione è compromessa.</string>
<string name="alert_text_fragment_permanent_error_reconnect">L\'errore è permanente per questa connessione, riconnettiti.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messaggi non decifrati.</string>
<string name="alert_title_msg_bad_hash">Hash del messaggio errato</string>
<string name="alert_text_msg_bad_hash">L\'hash del messaggio precedente è diverso.</string>
<string name="alert_text_fragment_please_report_to_developers">Si prega di segnalarlo agli sviluppatori.</string>
<string name="stop_snd_file__message">L\'invio del file verrà interrotto.</string>
<string name="stop_file__action">Ferma file</string>
<string name="stop_snd_file__title">Fermare l\'invio del file\?</string>
<string name="revoke_file__message">Il file verrà eliminato dai server.</string>
<string name="stop_rcv_file__message">La ricezione del file verrà interrotta.</string>
<string name="revoke_file__confirm">Revoca</string>
<string name="revoke_file__action">Revoca file</string>
<string name="revoke_file__title">Revocare il file\?</string>
<string name="stop_file__confirm">Ferma</string>
<string name="stop_rcv_file__title">Fermare la ricezione del file\?</string>
<string name="no_spaces">Niente spazi!</string>
<string name="allow_calls_only_if">Consenti le chiamate solo se il contatto le consente.</string>
<string name="calls_prohibited_with_this_contact">Le chiamate audio/video sono vietate.</string>
<string name="both_you_and_your_contact_can_make_calls">Sia tu che il tuo contatto potete effettuare chiamate.</string>
<string name="only_you_can_make_calls">Solo tu puoi effettuare chiamate.</string>
<string name="only_your_contact_can_make_calls">Solo il tuo contatto può effettuare chiamate.</string>
<string name="prohibit_calls">Proibisci le chiamate audio/video.</string>
<string name="audio_video_calls">Chiamate audio/video</string>
<string name="available_in_v51">"
\nDisponibile nella v5.1"</string>
<string name="allow_your_contacts_to_call">Consenti ai tuoi contatti di chiamarti.</string>
<string name="v5_0_app_passcode">Codice di accesso dell\'app</string>
<string name="v5_0_large_files_support_descr">Veloce e senza aspettare che il mittente sia in linea!</string>
<string name="v5_0_polish_interface">Interfaccia polacca</string>
<string name="v5_0_app_passcode_descr">Impostala al posto dell\'autenticazione di sistema.</string>
<string name="v5_0_polish_interface_descr">Grazie agli utenti contribuite via Weblate!</string>
<string name="v5_0_large_files_support">Video e file fino a 1 GB</string>
</resources>

View File

@@ -540,6 +540,7 @@
<string name="markdown_help">マークダウン (書式編集) ガイド</string>
<string name="smp_servers_enter_manually">サーバを手動で入力</string>
<string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。</string>
<string name="core_build_timestamp">コアのビルド@: %s</string>
<string name="create_address">アドレスを作成</string>
<string name="delete_address__question">アドレスを削除しますか?</string>
<string name="display_name__field">表示の名前:</string>
@@ -842,7 +843,9 @@
<string name="this_string_is_not_a_connection_link">このストリングは接続リンクではありません!</string>
<string name="chat_with_the_founder">質問やアイデアを送る</string>
<string name="star_on_github">GithubでStar</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。 <xliff:g id="appName">SimpleX</xliff:g>のサーバには開示されません。</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。
\n
\n<xliff:g id="appName">SimpleX</xliff:g>のサーバには開示されません。</string>
<string name="your_calls">あなたの通話</string>
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g>飛ばしたメッセージ</string>
<string name="send_link_previews">リンクのプレビューを送信</string>

View File

@@ -10,9 +10,9 @@
<string name="contact_already_exists">이미 추가된 연락처에요.</string>
<string name="smp_server_test_connect">연결</string>
<string name="connection_error_auth">연결 오류 (인증)</string>
<string name="smp_server_test_create_queue">대기열 만들기</string>
<string name="smp_server_test_create_queue">대기열 생성</string>
<string name="database_initialization_error_title">데이터베이스를 초기화할 수 없어요</string>
<string name="notifications_mode_service_desc">앱이 백그라운드에서 항상 실행되어요. 대신 메시지가 도착하자마자 바로 알림이 요.</string>
<string name="notifications_mode_service_desc">앱이 백그라운드에서 항상 실행요. 대신 메시지가 도착하자마자 바로 알림이 요.</string>
<string name="notifications_mode_periodic_desc">10분마다 최대 1분간 새 메시지 확인</string>
<string name="notification_contact_connected">연결됨</string>
<string name="notification_preview_somebody">숨긴 대화 상대 :</string>
@@ -43,10 +43,10 @@
<string name="clear_chat_question">채팅을 지울까요\?</string>
<string name="connection_request_sent">연결 요청 완료</string>
<string name="connect_via_link">링크를 통해 연결</string>
<string name="smp_servers_preset_add">프리셋 서버 추가하기</string>
<string name="smp_servers_preset_add">프리셋 서버 추가</string>
<string name="smp_servers_add">서버 추가…</string>
<string name="chat_console">채팅 콘솔</string>
<string name="smp_servers_check_address">서버 주소를 확인 후 다시 시도해 주세요.</string>
<string name="smp_servers_check_address">서버 주소를 확인 후 다시 시도하십시오.</string>
<string name="configure_ICE_servers">ICE 서버 설정</string>
<string name="contribute">기여</string>
<string name="network_settings">고급 네트워크 설정</string>
@@ -95,10 +95,10 @@
<string name="change_database_passphrase_question">데이터베이스 암호를 바꾸겠습니까\?</string>
<string name="confirm_new_passphrase">새로운 암호 확인…</string>
<string name="chat_archive_section">채팅 기록 보관함</string>
<string name="rcv_group_event_changed_your_role">내 역할이 %s 역할로 변경.</string>
<string name="rcv_conn_event_switch_queue_phase_changing">주소 바꾸는 중</string>
<string name="snd_conn_event_switch_queue_phase_changing">주소 바꾸는 중</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s의 주소 바꾸는 중</string>
<string name="rcv_group_event_changed_your_role">내 역할이 %s 역할로 변경되었습니다.</string>
<string name="rcv_conn_event_switch_queue_phase_changing">주소 바꾸</string>
<string name="snd_conn_event_switch_queue_phase_changing">주소 바꾸</string>
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s의 주소 바꾸</string>
<string name="rcv_group_event_member_connected">연결됨</string>
<string name="group_member_status_complete">완료</string>
<string name="group_member_status_connected">연결됨</string>
@@ -119,7 +119,7 @@
<string name="cant_delete_user_profile">사용자 프로필을 삭제할 수 없습니다</string>
<string name="chat_preferences_always">항상</string>
<string name="chat_preferences_contact_allows">대화 상대가 허용했어요.</string>
<string name="contact_preferences">연락처 개별 설정</string>
<string name="contact_preferences">연락처 설정</string>
<string name="allow_voice_messages_only_if">대화 상대도 허용한 경우에만 음성 메시지를 보낼 수 있습니다.</string>
<string name="allow_your_contacts_irreversibly_delete">모두에게서 메시지 영구 삭제 허용하기.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">대화 상대에게 자동 삭제되는 메시지 허용하기.</string>
@@ -135,7 +135,7 @@
<string name="v4_6_chinese_spanish_interface">중국어 및 스페인어 인터페이스</string>
<string name="about_simplex">SimpleX에 대하여</string>
<string name="accept">수락</string>
<string name="share_one_time_link">일회용 초대 링크 만들기</string>
<string name="share_one_time_link">일회용 초대 링크 생성</string>
<string name="create_address">주소 생성</string>
<string name="chat_item_ttl_day">1일</string>
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX</xliff:g>에 대하여</string>
@@ -166,7 +166,7 @@
<string name="settings_section_title_icon">앱 아이콘</string>
<string name="incognito_random_profile_from_contact_description">링크를 보낸 사람한테 랜덤으로 만들어진 익명 프로필이 보내져요</string>
<string name="network_session_mode_user_description">별도로 분리된 TCP 연결(그리고 SOCKS 자격 증명)이 <b>각각의 채팅 프로필</b>에 사용될 거예요.</string>
<string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 대화 상대 및 그룹 구성원</b>에게 사용될 거예요.
<string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 연락처 및 그룹 구성원</b>에게 사용될 거예요.
\n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 엄청 높을 수 있고 일부 연결이 실패할 수 있어요.</string>
<string name="icon_descr_asked_to_receive">이미지 수신 요청됨</string>
<string name="v4_6_audio_video_calls">음성 및 영상 전화</string>
@@ -176,7 +176,7 @@
<string name="onboarding_notifications_mode_off_desc"><b>배터리에 가장 좋음</b>. 앱이 실행 중일 때만 알림을 받게 되며 백그라운드에서 실행되지 않습니다.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>설정을 통해 비활성화할 수 있어요.</b> 앱이 실행되는 동안 알림이 표시되요.</string>
<string name="both_you_and_your_contact_can_send_disappearing">나와 대화 상대 모두 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 연결할 수 있어요.</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 연결요.</string>
<string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 암호키 저장소에 접근할 수 없습니다</string>
<string name="onboarding_notifications_mode_service_desc"><b>배터리 많이 사용</b>! 백그라운드에서 항상 실행돼요. 메시지를 수신하자마자 알림이 떠요.</string>
<string name="callstatus_ended">통화 종료됨 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
@@ -184,7 +184,7 @@
<string name="icon_descr_call_progress">전화 연결 중</string>
<string name="icon_descr_cancel_link_preview">링크 미리보기 취소</string>
<string name="icon_descr_cancel_image_preview">이미지 미리보기 취소</string>
<string name="rcv_group_event_changed_member_role">%s 에서 %s 역할 변경</string>
<string name="rcv_group_event_changed_member_role">%s 역할에서 %s 역할 변경되었습니다</string>
<string name="chat_database_section">채팅 데이터베이스</string>
<string name="alert_title_cant_invite_contacts">대화 상대를 초대할 수 없습니다!</string>
<string name="change_verb">변경</string>
@@ -206,6 +206,7 @@
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">대화 상대와 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
<string name="status_contact_has_e2e_encryption">대화 상대와 종단간 암호화됨</string>
<string name="alert_title_contact_connection_pending">대화 상대와 아직 연결되지 않았어요!</string>
<string name="core_build_timestamp">코어 빌드: %s</string>
<string name="archive_created_on_ts"><xliff:g id="archive_ts">%1$s</xliff:g>에 생성 완료</string>
<string name="create_one_time_link">일회용 초대 링크 생성</string>
<string name="create_secret_group_title">비밀 그룹 생성</string>
@@ -220,7 +221,7 @@
<string name="delete_message__question">메시지를 삭제할까요\?</string>
<string name="for_me_only">나에게서만 삭제</string>
<string name="delete_member_message__question">멤버의 메시지를 삭제할까요\?</string>
<string name="maximum_supported_file_size">현재 지원되는 최대 파일 크기는 <xliff:g id="maxFileSize">%1$s</xliff:g> 에요.</string>
<string name="maximum_supported_file_size">현재 지원되는 최대 파일 크기는 <xliff:g id="maxFileSize">%1$s</xliff:g>입니다.</string>
<string name="image_decoding_exception_title">디코딩 오류</string>
<string name="button_delete_contact">대화 상대 삭제</string>
<string name="delete_contact_question">연락처를 삭제할까요\?</string>
@@ -241,18 +242,18 @@
<string name="database_passphrase">데이터베이스 비밀번호</string>
<string name="delete_files_and_media_for_all_users">모든 채팅 프로필 파일 삭제</string>
<string name="database_error">데이터베이스 에러</string>
<string name="passphrase_is_different">데이터베이스 비밀번호가 암호 저장소에 저장된 것과 일치하지 않아요.</string>
<string name="passphrase_is_different">데이터베이스 비밀번호가 암호 저장소에 저장된 것과 일치하지 않습니다.</string>
<string name="database_passphrase_is_required">채팅을 열려면 데이터베이스 비밀번호가 필요해요.</string>
<string name="delete_archive">보관된 채팅 삭제</string>
<string name="delete_chat_archive_question">보관된 채팅을 삭제할까요\?</string>
<string name="num_contacts_selected">%d 개의 연락처가 선택되었어요.</string>
<string name="num_contacts_selected">%d 개의 연락처가 선택되었습니다.</string>
<string name="info_row_database_id">데이터베이스 아이디</string>
<string name="users_delete_profile_for">다음 채팅 프로필 삭제</string>
<string name="theme_dark">어둡게</string>
<string name="delete_after">다음 기간 이후 자동 삭제</string>
<string name="above_then_preposition_continuation">위 다음 :</string>
<string name="delete_database">데이터베이스 삭제</string>
<string name="set_password_to_export_desc">데이터베이스는 임의의 비밀번호로 암호화되었어요. 내보내기 기능 사용 전 비밀번호를 변경해 주세요.</string>
<string name="set_password_to_export_desc">데이터베이스는 임의의 비밀번호로 암호화되었습니다. 내보내기 기능 사용 전 비밀번호를 변경해 주세요.</string>
<string name="delete_files_and_media_question">파일과 미디어를 삭제할까요\?</string>
<string name="current_passphrase">현재 비밀번호…</string>
<string name="database_encrypted">데이터베이스 암호화 완료!</string>
@@ -269,7 +270,7 @@
<string name="ttl_d">%d일</string>
<string name="ttl_days">%d일</string>
<string name="button_delete_group">그룹 삭제</string>
<string name="rcv_conn_event_switch_queue_phase_completed">주소 변경</string>
<string name="rcv_conn_event_switch_queue_phase_completed">주소 변경되었습니다.</string>
<string name="database_encryption_will_be_updated">데이터베이스 비밀번호가 업데이트되고 암호 저장소에 보관됩니다.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">데이터베이스는 암호화되고, 비밀번호는 암호 저장소에 보관될 거에요.</string>
<string name="users_delete_question">채팅 프로필을 삭제할까요\?</string>
@@ -328,7 +329,7 @@
<string name="auth_enable_simplex_lock">SimpleX 잠금 활성화</string>
<string name="auth_log_in_using_credential">자격 증명으로 로그인</string>
<string name="auth_open_chat_console">채팅 콘솔 열기</string>
<string name="auth_stop_chat">채팅 기능 중지하기</string>
<string name="auth_stop_chat">채팅 중지하기</string>
<string name="auth_unlock">잠금 해제하기</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">앱을 사용하지 않는 지 30초가 지나면 다시 인증해야 해요.</string>
<string name="contact_wants_to_connect_with_you">님이 연결하고 싶어해요!</string>
@@ -336,7 +337,7 @@
<string name="callstate_waiting_for_answer">응답 대기 중…</string>
<string name="callstate_waiting_for_confirmation">확인 대기 중…</string>
<string name="alert_title_skipped_messages">읽지 않는 메시지</string>
<string name="alert_title_cant_invite_contacts_descr">이 그룹에서 익명 프로필을 사용하고 있어요. 원래의 내 프로필이 노출되는 걸 방지하기 위해 대화 상대 초대가 허용되지 않아요.</string>
<string name="alert_title_cant_invite_contacts_descr">이 그룹에서 익명 프로필을 사용하고 있어요. 원래 프로필이 노출되는 걸 방지하기 위해 대화 상대 초대가 허용되지 않아요.</string>
<string name="button_remove_member">멤버 삭제하기</string>
<string name="chat_item_ttl_seconds">%s 초</string>
<string name="alert_message_group_invitation_expired">이 링크로 참여할 수 없어요. 이미 삭제된 링크에요.</string>
@@ -371,7 +372,7 @@
\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우
\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우
\n3. 침해된 연결의 경우
\n서버 업데이트를 받으려면 설정에서 개발자에게 연락해 주세요.
\n서버 업데이트를 받으려면 설정을 통해 개발자에게 연락해 주세요.
\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요.</string>
<string name="auth_simplex_lock_turned_on">SimpleX 잠금 켜짐</string>
<string name="callstate_received_answer">응답됨…</string>
@@ -386,11 +387,11 @@
<string name="description_via_group_link_incognito">그룹 링크로 익명 채팅</string>
<string name="description_via_group_link">그룹 링크로 채팅</string>
<string name="description_via_one_time_link">일회용 링크로 채팅</string>
<string name="description_you_shared_one_time_link_incognito">일회용 익명 링크를 공유했어요.</string>
<string name="description_you_shared_one_time_link">일회용 링크를 공유했어요.</string>
<string name="description_via_contact_address_link_incognito">상대의 연락처 링크로 익명 연결</string>
<string name="description_via_contact_address_link">상대의 연락처 링크로 연결</string>
<string name="description_via_one_time_link_incognito">일회용 연락처로 익명 연결</string>
<string name="description_you_shared_one_time_link_incognito">일회용 익명 연락처를 공유했어요.</string>
<string name="description_you_shared_one_time_link">일회용 프로필 연락처를 공유했어요.</string>
<string name="description_via_contact_address_link_incognito">상대의 연락처 링크로 익명 채팅</string>
<string name="description_via_contact_address_link">상대의 연락처 링크로 채팅</string>
<string name="description_via_one_time_link_incognito">일회용 연락처로 익명 채팅</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요.</string>
<string name="error_saving_smp_servers">SMP 서버 저장 오류</string>
<string name="error_setting_network_config">네트워크 설정 업데이트 오류</string>
@@ -475,7 +476,7 @@
<string name="feature_offered_item">%s 제안</string>
<string name="feature_offered_item_with_param">%s 제안: %2s</string>
<string name="icon_descr_instant_notifications">즉시 알림</string>
<string name="hide_notification">기기</string>
<string name="hide_notification"></string>
<string name="hide_verb">숨기기</string>
<string name="for_everybody">모두에게</string>
<string name="icon_descr_edited">수정됨</string>
@@ -492,7 +493,7 @@
<string name="group_member_role_owner">소유자</string>
<string name="group_member_status_group_deleted">그룹 삭제됨</string>
<string name="group_member_status_invited">초대됨</string>
<string name="group_member_status_removed">강퇴</string>
<string name="group_member_status_removed">삭제</string>
<string name="icon_descr_expand_role">역할 선택지 펼치기</string>
<string name="files_and_media_section">파일 &amp; 미디어</string>
<string name="group_invitation_item_description">그룹으로 초대 <xliff:g id="group_name">%1$s</xliff:g></string>
@@ -567,7 +568,7 @@
<string name="import_database">데이터베이스 가져오기</string>
<string name="import_database_confirmation">가져오기</string>
<string name="incognito">익명 모드</string>
<string name="incognito_info_find">익명 채팅에 사용되는 프로필을 확인하려면 채팅 상단에 있는 연락처 또는 그룹 이름을 탭하세요.</string>
<string name="incognito_info_find">익명 채팅에 사용되는 프로필을 찾으려면 채팅 상단에 있는 연락처 또는 그룹 이름을 탭하세요.</string>
<string name="image_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 이미지가 수신될 거예요.</string>
<string name="image_descr_profile_image">프로필 이미지</string>
<string name="incognito_info_allows">하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요.</string>
@@ -615,282 +616,4 @@
<string name="invalid_QR_code">잘못된 QR 코드</string>
<string name="incorrect_code">잘못된 보안 코드!</string>
<string name="invalid_contact_link">잘못된 링크!</string>
<string name="marked_deleted_description">삭제됨으로 표시됨</string>
<string name="moderated_item_description">%s에 의해 조정됨</string>
<string name="live">라이브</string>
<string name="moderated_description">조정됨</string>
<string name="network_error_desc"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> 에서 네트워크 연결 상태를 확인한 후 다시 시도하세요.</string>
<string name="la_notice_title_simplex_lock">SimpleX 잠금</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">정보를 보호하려면 SimpleX 잠금을 켜세요.
\n이 기능이 활성화하기 전에 인증을 완료하라는 메시지가 표시될 거예요.</string>
<string name="notification_new_contact_request">새로운 대화 요청</string>
<string name="notification_preview_mode_hidden">숨겨짐</string>
<string name="notification_preview_mode_contact_desc">대화 상대 이름만 표시</string>
<string name="notification_preview_new_message">새로운 메시지</string>
<string name="notifications_mode_off">앱이 열릴 때 실행</string>
<string name="notifications_mode_periodic">주기적으로 실행됨</string>
<string name="notification_display_mode_hidden_desc">연락처 이름 및 메시지 숨기기</string>
<string name="la_notice_turn_on">켜기</string>
<string name="message_delivery_error_desc">대화 상대가 나와의 연결을 삭제했을 가능성이 커요.</string>
<string name="message_delivery_error_title">메시지 전달 오류</string>
<string name="moderate_verb">조정</string>
<string name="moderate_message_will_be_deleted_warning">모든 멤버에게서 메시지가 삭제될 거예요.</string>
<string name="moderate_message_will_be_marked_warning">이 메시지는 모든 멤버에게 조정됨으로 표시될 거예요.</string>
<string name="no_details">세부 정보 없음</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 모바일: <b>모바일 앱에서 열기</b>를 누른 다음 앱에서 <b>연결</b>을 누르세요.</string>
<string name="mark_code_verified">확인됨으로 표시</string>
<string name="network_and_servers">네트워크 및 서버</string>
<string name="new_database_archive">새 데이터베이스 보관함</string>
<string name="new_member_role">새 멤버 역할</string>
<string name="no_contacts_to_add">추가할 연락처 없음</string>
<string name="no_contacts_selected">선택한 연락처 없음</string>
<string name="network_status">네트워크 상태</string>
<string name="language_system">시스템</string>
<string name="messages_section_title">메시지</string>
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
<string name="member_info_section_title_member">멤버</string>
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
<string name="network_options_revert">되돌리기</string>
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
<string name="leave_group_button">나가기</string>
<string name="large_file">큰 파일!</string>
<string name="network_settings_title">네트워크 설정</string>
<string name="network_use_onion_hosts_required_desc">연결하려면 Onion 호스트가 필요해요.</string>
<string name="network_option_ping_count">핑 횟수</string>
<string name="network_option_ping_interval">핑 간격</string>
<string name="network_option_protocol_timeout">프로토콜 타임아웃</string>
<string name="network_option_seconds_label"></string>
<string name="network_option_tcp_connection_timeout">TCP 연결 시간 초과</string>
<string name="muted_when_inactive">비활성 시 음소거!</string>
<string name="message_deletion_prohibited_in_chat">이 채팅에서는 메시지 영구 삭제가 허용되지 않아요.</string>
<string name="make_private_connection">비공개 연결하기</string>
<string name="no_received_app_files">수신 또는 전송된 파일 없음</string>
<string name="network_options_save">저장하기</string>
<string name="make_profile_private">프로필을 비공개로 설정하세요!</string>
<string name="live_message">라이브 메시지!</string>
<string name="mark_read">읽음으로 표시</string>
<string name="mark_unread">읽지 않음으로 표시</string>
<string name="mute_chat">음소거</string>
<string name="markdown_in_messages">메시지에 사용된 마크다운</string>
<string name="network_socks_toggle">SOCKS 프록시 사용 (포트 9050)</string>
<string name="network_disable_socks">직접적인 인터넷 연결을 사용할까요\?</string>
<string name="network_enable_socks">SOCKS 프록시를 사용할까요\?</string>
<string name="network_disable_socks_info">설정하면 메시징 서버에서 내 IP 주소와 내가 연결하려는 서버를 볼 수 있어요.</string>
<string name="network_use_onion_hosts">.onion 호스트 사용</string>
<string name="network_use_onion_hosts_no">아니요</string>
<string name="network_use_onion_hosts_prefer">사용 가능한 경우</string>
<string name="network_use_onion_hosts_required">필요함</string>
<string name="network_use_onion_hosts_prefer_desc">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
<string name="network_use_onion_hosts_no_desc">Onion 호스트가 사용되지 않을 거예요.</string>
<string name="network_session_mode_transport_isolation">전송 격리</string>
<string name="network_use_onion_hosts_no_desc_in_alert">Onion 호스트가 사용되지 않을 거예요.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">연결하려면 Onion 호스트가 필요해요.</string>
<string name="next_generation_of_private_messaging">차세대 사생활 보호 메시징</string>
<string name="new_passphrase">새 비밀번호…</string>
<string name="network_option_enable_tcp_keep_alive">TCP 연결 유지 활성화</string>
<string name="new_in_version">%s의 새로운 기능</string>
<string name="markdown_help">마크다운 도움말</string>
<string name="many_people_asked_how_can_it_deliver">많은 사람들의 질문 : <i><xliff:g id="appName">SimpleX</xliff:g>에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?</i></string>
<string name="leave_group_question">그룹에서 나갈까요\?</string>
<string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만 다음에 대한 다운 마이그레이션 없음: %s</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string>
<string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
<string name="network_options_reset_to_defaults">기본값으로 재설정</string>
<string name="notification_preview_mode_message">메시지 내용</string>
<string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string>
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string>
<string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string>
<string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string>
<string name="periodic_notifications">주기적 알림</string>
<string name="periodic_notifications_desc">주기적으로 새 메시지를 확인해요 — 하루에 몇 퍼센트의 배터리를 사용할 거예요. 푸시 알림을 사용하지 않아요 — 기기의 데이터가 서버로 전송되지 않아요.</string>
<string name="periodic_notifications_disabled">주기적 알림이 비활성화되었어요.</string>
<string name="ntf_channel_calls">SimpleX Chat 통화</string>
<string name="ntf_channel_messages">SimpleX Chat 메시지</string>
<string name="observer_cant_send_message_desc">그룹 관리자에게 문의해 주세요.</string>
<string name="observer_cant_send_message_title">메시지를 보낼 수 없습니다!</string>
<string name="ok"></string>
<string name="reject_contact_button">거절</string>
<string name="password_to_show">비밀번호 표시</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">사용자 디바이스에만 <b>2계층 종단 간 암호화</b> 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.</string>
<string name="read_more_in_github">자세한 내용은 GitHub에서 확인해 주세요.</string>
<string name="privacy_and_security">개인 정보 및 보안</string>
<string name="notifications_will_be_hidden">알림은 앱이 중지되기 전까지만 전달될 거예요!</string>
<string name="only_you_can_send_disappearing">자동 삭제되는 메시지는 나만 보낼 수 있어요.</string>
<string name="prohibit_sending_disappearing_messages">자동 삭제되는 메시지 허용되지 않음.</string>
<string name="prohibit_sending_voice_messages">음성 메시지 허용되지 않음.</string>
<string name="prohibit_sending_disappearing">자동 삭제되는 메시지 허용되지 않음.</string>
<string name="old_database_archive">이전 데이터베이스 기록</string>
<string name="rcv_group_event_member_added"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> 초대됨</string>
<string name="rcv_group_event_member_left">나감</string>
<string name="rcv_group_event_user_deleted">강퇴됨</string>
<string name="rcv_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> 강퇴됨</string>
<string name="only_group_owners_can_change_prefs">그룹 소유자만 그룹 설정을 변경할 수 있어요.</string>
<string name="receiving_via">다음을 통해 수신</string>
<string name="only_your_contact_can_send_disappearing">대화 상대만 자동 삭제되는 메시지를 보낼 수 있어요.</string>
<string name="prohibit_direct_messages">멤버들 간의 1:1 채팅이 허용되지 않음.</string>
<string name="only_you_can_send_voice">나만 음성 메시지를 보낼 수 있어요.</string>
<string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string>
<string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string>
<string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string>
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string>
<string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string>
<string name="one_time_link">일회성 초대 링크</string>
<string name="paste_button">붙여넣기</string>
<string name="profile_is_only_shared_with_your_contacts">프로필은 대화 상대들하고만 공유됩니다.</string>
<string name="privacy_redefined">프라이버시의 재정의</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">오픈 소스 프로토콜과 코드 - 누구나 자신만의 서버를 구축할 수 있어요.</string>
<string name="onboarding_notifications_mode_off">앱이 실행 중일 때</string>
<string name="read_more_in_github_with_link">ㅍ자세한 내용은 \u0020<font color="#0088ff">GitHub</font> 에서 확인해 주세요.</string>
<string name="relay_server_protects_ip">릴레이 서버는 IP 주소를 숨겨주지만, 통화 시간을 관찰 할 수 있어요.</string>
<string name="rcv_group_event_invited_via_your_group_link">그룹 링크로 초대</string>
<string name="onboarding_notifications_mode_subtitle">설정을 통해 나중에 변경할 수 있어요.</string>
<string name="onboarding_notifications_mode_title">비공개 알림</string>
<string name="reject">거절</string>
<string name="open_simplex_chat_to_accept_call"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>을 열어 전화 받기</string>
<string name="open_verb">열기</string>
<string name="protect_app_screen">앱 잠금</string>
<string name="personal_welcome"><xliff:g>%1$s</xliff:g>님, 환영합니다!</string>
<string name="only_stored_on_members_devices">(그룹 구성원에게만 저장됨)</string>
<string name="paste_connection_link_below_to_connect">아래 칸에 받은 링크를 붙여넣기하여 대화 상대와 연결해 주세요.</string>
<string name="rate_the_app">앱 평가하기</string>
<string name="onboarding_notifications_mode_periodic">주기적</string>
<string name="onboarding_notifications_mode_service">즉시</string>
<string name="paste_the_link_you_received">받은 링크 붙여넣기</string>
<string name="relay_server_if_necessary">릴레이 서버는 필요한 경우에만 사용되어요. 릴레이 서버가 사용되지 않으면 제3자가 내 IP 주소를 관찰할 수 있어요.</string>
<string name="open_chat">채팅 열기</string>
<string name="people_can_connect_only_via_links_you_share">공유한 링크를 통해서만 나에게 연결할 수 있어요.</string>
<string name="rcv_group_event_updated_group_profile">그룹 프로필 업데이트됨</string>
<string name="profile_password">프로필 비밀번호</string>
<string name="reveal_verb">보이기</string>
<string name="save_verb">저장하기</string>
<string name="scan_QR_code">QR 코드 스캔하기</string>
<string name="reply_verb">답장</string>
<string name="reset_verb">초기화</string>
<string name="save_servers_button">저장하기</string>
<string name="save_and_notify_contacts">저장하고 모든 대화 상대에게 알리기</string>
<string name="save_and_notify_group_members">저장하고 그룹 멤버들에게 알리기</string>
<string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string>
<string name="remove_passphrase">지우기</string>
<string name="save_archive">아카이브 저장하기</string>
<string name="save_color">색상 저장하기</string>
<string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string>
<string name="restore_database">데이터베이스 백업 복원하기</string>
<string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string>
<string name="restore_database_alert_title">데이터베이스 백업을 복원할까요\?</string>
<string name="restore_passphrase_not_found_desc">키스토어에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요.</string>
<string name="save_and_update_group_profile">저장하고 그룹 프로필 업데이트하기</string>
<string name="save_welcome_message_question">환영 메시지를 저장할까요\?</string>
<string name="save_group_profile">그룹 프로필 저장하기</string>
<string name="reset_color">색상 초기화</string>
<string name="remove_member_confirmation">강퇴하기</string>
<string name="scan_code">코드 스캔하기</string>
<string name="scan_code_from_contacts_app">대화 상대의 앱에서 보안 코드를 스캔해 주세요.</string>
<string name="saved_ICE_servers_will_be_removed">저장된 WebRTC ICE 서버가 제거될 거예요.</string>
<string name="save_passphrase_and_open_chat">비밀번호 저장하고 채팅 열기</string>
<string name="role_in_group">역할</string>
<string name="save_preferences_question">설정을 저장할까요\?</string>
<string name="save_profile_password">프로필 비밀번호 저장하기</string>
<string name="restart_the_app_to_use_imported_chat_database">가져온 채팅 데이터베이스를 사용하려면 앱을 다시 실행해 주세요.</string>
<string name="restart_the_app_to_create_a_new_chat_profile">새 프로필을 만드려면 앱을 다시 실행해 주세요.</string>
<string name="remove_passphrase_from_keychain">암호 저장소에서 비밀번호를 삭제할까요\?</string>
<string name="run_chat_section">채팅 기능 실행하기</string>
<string name="restore_database_alert_confirm">복원하기</string>
<string name="sender_cancelled_file_transfer">대화 상대가 파일 전송을 취소했어요.</string>
<string name="smp_server_test_secure_queue">보안 대기열</string>
<string name="smp_server_test_upload_file">파일 업로드하기</string>
<string name="smp_server_test_compare_file">파일 비교</string>
<string name="smp_server_test_delete_file">파일 삭제하기</string>
<string name="smp_server_test_download_file">파일 다운로드하기</string>
<string name="share_verb">공유</string>
<string name="send_live_message">라이브 메시지 보내기</string>
<string name="show_QR_code">QR코드 보기</string>
<string name="smp_server_test_create_file">파일 만들기</string>
<string name="sender_you_pronoun"></string>
<string name="sending_files_not_yet_supported">파일 보내기는 아직 지원되지 않아요.</string>
<string name="simplex_link_connection"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g>를 통해</string>
<string name="simplex_link_contact">SimpleX 연락처 주소</string>
<string name="simplex_link_group">SimpleX 그룹 링크</string>
<string name="simplex_link_invitation">SimpleX 일회용 초대 링크</string>
<string name="simplex_link_mode">SimpleX 링크</string>
<string name="simplex_link_mode_browser">브라우저를 통해</string>
<string name="simplex_link_mode_browser_warning">브라우저에서 링크를 열면 익명성 또는 보안에 문제가 생길 수 있어요. 신뢰할 수 없는 SimpleX 링크는 빨간색으로 표시되어요.</string>
<string name="simplex_link_mode_full">링크 전체</string>
<string name="sender_may_have_deleted_the_connection_request">보낸 사람이 연결 요청을 삭제했을 수 있어요.</string>
<string name="service_notifications_disabled">즉각적인 알림이 비활성화되었어요!</string>
<string name="settings_notification_preview_mode_title">미리보기 표시</string>
<string name="settings_notification_preview_title">알림 미리보기</string>
<string name="settings_notifications_mode_title">알림 서비스</string>
<string name="simplex_service_notification_text">메시지 받는 중…</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX 채팅</xliff:g> 서비스</string>
<string name="share_file">파일 공유…</string>
<string name="share_image">이미지 공유…</string>
<string name="share_message">메시지 공유…</string>
<string name="send_live_message_desc">라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줘요.</string>
<string name="send_verb">보내기</string>
<string name="share_invitation_link">초대 링크 공유</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">연락하고자 하는 사람이 앱에서 QR 코드를 스캔할 수 있어요.</string>
<string name="security_code">보안 코드</string>
<string name="smp_servers_scan_qr">서버 QR코드 스캔</string>
<string name="smp_servers_test_some_failed">일부 서버가 테스트에 통과하지 못했습니다 :</string>
<string name="smp_servers_test_failed">서버 테스트 실패!</string>
<string name="section_title_welcome_message">환영 메시지</string>
<string name="share_link">링크 공유</string>
<string name="show_call_on_lock_screen">표시하기</string>
<string name="select_contacts">연락처 선택</string>
<string name="skip_inviting_button">멤버 초대 건너뛰기</string>
<string name="sending_via">다음을 통해 보내기</string>
<string name="stop_chat_confirmation">멈추기</string>
<string name="snd_group_event_changed_member_role">%s의 역할을 %s로 변경했어요.</string>
<string name="section_title_for_console">콘솔용</string>
<string name="should_be_at_least_one_visible_profile">적어도 하나의 숨겨지지 않은 사용자 프로필이 있어야 해요.</string>
<string name="set_group_preferences">그룹 설정 지정하기</string>
<string name="snd_conn_event_switch_queue_phase_completed_for_member">%s의 주소를 바꿨어요</string>
<string name="snd_conn_event_switch_queue_phase_completed">주소를 바꿨어요</string>
<string name="snd_group_event_group_profile_updated">그룹 프로필 업데이트됨</string>
<string name="should_be_at_least_one_profile">적어도 하나의 사용자 프로필이 있어야 해요.</string>
<string name="smp_servers_test_server">서버 테스트하기</string>
<string name="smp_servers_use_server">서버 사용하기</string>
<string name="smp_servers_use_server_for_new_conn">새로운 대화에 사용</string>
<string name="smp_servers_invalid_address">잘못된 서버 주소에요!</string>
<string name="secret">비밀</string>
<string name="status_no_e2e_encryption">종단 간 암호화 안 됨</string>
<string name="settings_section_title_themes">테마</string>
<string name="snd_group_event_changed_role_for_yourself">내 역할이 %s로 변경되었어요.</string>
<string name="snd_group_event_user_left">나감</string>
<string name="settings_section_title_help">도움말</string>
<string name="settings_section_title_settings">설정</string>
<string name="send_link_previews">링크 미리보기 보내기</string>
<string name="settings_section_title_socks">SOCKS 프록시</string>
<string name="settings_section_title_support">SIMPLEX CHAT 도와주기</string>
<string name="settings_section_title_you"></string>
<string name="settings_experimental_features">실험적 기능</string>
<string name="show_dev_options">표시 :</string>
<string name="set_contact_name">연락처 이름 설정</string>
<string name="send_us_an_email">개발자에게 이메일 보내기</string>
<string name="smp_servers_enter_manually">서버를 수동으로 입력</string>
<string name="smp_servers_preset_server">미리 설정된 서버</string>
<string name="smp_servers_save">서버 저장하기</string>
<string name="smp_servers_test_servers">서버 테스트하기</string>
<string name="smp_servers_per_user">현재 채팅 프로필의 새로운 연결을 위한 서버</string>
<string name="smp_save_servers_question">서버를 저장할까요\?</string>
<string name="star_on_github">GitHub에서 별 주기</string>
<string name="show_developer_options">개발자 옵션 보기</string>
<string name="settings_section_title_messages">메시지 및 파일</string>
<string name="settings_section_title_incognito">익명 모드</string>
<string name="settings_section_title_experimenta">실험적</string>
<string name="set_password_to_export">내보낼 비밀번호 설정</string>
<string name="smp_servers">SMP 서버</string>
<string name="smp_servers_preset_address">미리 설정된 서버 주소</string>
<string name="smp_servers_your_server">내 서버</string>
<string name="smp_servers_your_server_address">내 서버 주소</string>
<string name="snd_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>을(를) 강퇴했어요.</string>
<string name="stop_chat_to_export_import_or_delete_chat_database">채팅 데이터베이스를 내보내기, 가져오기 또는 삭제 하려면 채팅 기능을 중지해 주세요. 채팅 기능이 중지된 동안에는 메시지를 주고받을 수 없어요.</string>
<string name="store_passphrase_securely">비밀번호를 모르면 변경하거나 찾을 수 없으므로 비밀번호를 안전하게 보관해 주세요.</string>
<string name="submit_passcode">제출하기</string>
<string name="store_passphrase_securely_without_recover">비밀번호를 모르면 채팅에 액세스할 수 없으니 비밀번호를 안전하게 보관해 주세요.</string>
<string name="stop_chat_question">채팅 기능을 중지할까요\?</string>
<string name="stop_chat_to_enable_database_actions">데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기</string>
<string name="switch_receiving_address">수신 주소 바꾸기</string>
</resources>

View File

@@ -47,312 +47,4 @@
<string name="auth_unavailable">Tapatybės nustatymas neprieinamas</string>
<string name="impossible_to_recover_passphrase"><b>Turėkite omenyje</b>: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</string>
<string name="cancel_verb">Atsisakyti</string>
<string name="callstate_connecting">jungiamasi…</string>
<string name="connect_via_link_verb">Prisijungti</string>
<string name="server_connected">prisijungta</string>
<string name="server_connecting">jungiamasi</string>
<string name="display_name_connection_established">ryšys užmegztas</string>
<string name="display_name_connecting">jungiasi…</string>
<string name="connection_error">Ryšio klaida</string>
<string name="smp_server_test_connect">Prisijungti</string>
<string name="notification_contact_connected">Prisijungė</string>
<string name="connect_button">Prisijungti</string>
<string name="group_member_status_connected">prisijungė</string>
<string name="contact_connection_pending">jungiasi…</string>
<string name="group_connection_pending">jungiasi…</string>
<string name="icon_descr_server_status_connected">Prisijungta</string>
<string name="confirm_verb">Patvirtinti</string>
<string name="configure_ICE_servers">Konfigūruoti ICE serverius</string>
<string name="callstate_connected">prisijungta</string>
<string name="rcv_group_event_member_connected">prisijungė</string>
<string name="info_row_connection">Ryšys</string>
<string name="confirm_database_upgrades">Patvirtinti duomenų bazių naujinimus</string>
<string name="group_member_status_connecting">jungiasi</string>
<string name="network_session_mode_entity">Ryšys</string>
<string name="database_passphrase_will_be_updated">Duomenų bazės šifravimo slaptafrazė bus atnaujinta.</string>
<string name="core_version">Branduolio versija: v%s</string>
<string name="delete_address__question">Ištrinti adresą\?</string>
<string name="save_preferences_question">Įrašyti nuostatas\?</string>
<string name="create_profile">Sukurti profilį</string>
<string name="callstatus_rejected">atmestas skambutis</string>
<string name="callstate_ended">užbaigtas</string>
<string name="onboarding_notifications_mode_title">Privatūs pranešimai</string>
<string name="ignore">Nepaisyti</string>
<string name="icon_descr_flip_camera">Apversti kamerą</string>
<string name="icon_descr_call_rejected">Atmestas skambutis</string>
<string name="privacy_and_security">Privatumas ir saugumas</string>
<string name="settings_section_title_device">ĮRENGINYS</string>
<string name="settings_section_title_help">PAGALBA</string>
<string name="encrypt_database">Šifruoti</string>
<string name="remove_passphrase">Šalinti</string>
<string name="button_delete_group">Ištrinti grupę</string>
<string name="v4_2_group_links">Grupių nuorodos</string>
<string name="server_error">klaida</string>
<string name="simplex_link_mode_description">Aprašas</string>
<string name="error_saving_smp_servers">Klaida įrašant SMP serverius</string>
<string name="error_setting_network_config">Klaida atnaujinant tinklo konfigūraciją</string>
<string name="failed_to_create_user_title">Klaida kuriant profilį!</string>
<string name="failed_to_parse_chat_title">Nepavyko įkelti pokalbio</string>
<string name="failed_to_parse_chats_title">Nepavyko įkelti pokalbių</string>
<string name="failed_to_active_user_title">Klaida perjungiant profilį!</string>
<string name="error_sending_message">Klaida siunčiant žinutę</string>
<string name="error_creating_address">Klaida kuriant adresą</string>
<string name="error_joining_group">Klaida prisijungiant prie grupės</string>
<string name="error_receiving_file">Klaida gaunant failą</string>
<string name="error_changing_address">Klaida keičiant adresą</string>
<string name="error_deleting_contact">Klaida ištrinant adresatą</string>
<string name="error_deleting_group">Klaida ištrinant grupę</string>
<string name="smp_server_test_disconnect">Atsijungti</string>
<string name="error_deleting_user">Klaida ištrinant naudotojo profilį</string>
<string name="notification_display_mode_hidden_desc">Slėpti adresatą ir žinutę</string>
<string name="copy_verb">Kopijuoti</string>
<string name="reply_verb">Atsakyti</string>
<string name="delete_message__question">Ištrinti žinutę\?</string>
<string name="icon_descr_file">Failas</string>
<string name="maximum_supported_file_size">Šiuo metu didžiausias palaikomas failo dydis yra <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
<string name="file_not_found">Failas nerastas</string>
<string name="file_saved">Failas įrašytas</string>
<string name="icon_descr_server_status_disconnected">Atsijungta</string>
<string name="icon_descr_server_status_error">Klaida</string>
<string name="ask_your_contact_to_enable_voice">Paprašykite adresato, kad įjungtų balso žinučių siuntimą.</string>
<string name="reset_verb">Atstatyti</string>
<string name="delete_group_menu_action">Ištrinti</string>
<string name="image_descr_qr_code">QR kodas</string>
<string name="icon_descr_help">pagalba</string>
<string name="icon_descr_email">El. paštas</string>
<string name="create_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
<string name="scan_code">Skenuoti kodą</string>
<string name="database_passphrase_and_export">Duomenų bazės slaptafrazė ir eksportavimas</string>
<string name="smp_servers_delete_server">Ištrinti serverį</string>
<string name="error_saving_ICE_servers">Klaida įrašant ICE serverius</string>
<string name="save_servers_button">Įrašyti</string>
<string name="files_and_media_section">Failai ir medija</string>
<string name="delete_files_and_media_all">Ištrinti visus failus</string>
<string name="delete_files_and_media_question">Ištrinti failus ir mediją\?</string>
<string name="delete_archive">Ištrinti archyvą</string>
<string name="delete_chat_archive_question">Ištrinti pokalbio archyvą\?</string>
<string name="snd_group_event_group_profile_updated">grupės profilis atnaujintas</string>
<string name="info_row_group">Grupė</string>
<string name="users_delete_question">Ištrinti pokalbio profilį\?</string>
<string name="network_options_revert">Sugrąžinti</string>
<string name="network_options_save">Įrašyti</string>
<string name="feature_enabled">įjungta</string>
<string name="delete_after">Ištrinti po</string>
<string name="error_updating_user_privacy">Klaida atnaujinant naudotojo privatumą</string>
<string name="simplex_service_notification_text">Gaunamos žinutės…</string>
<string name="hide_notification">Slėpti</string>
<string name="save_verb">Įrašyti</string>
<string name="delete_verb">Ištrinti</string>
<string name="edit_verb">Taisyti</string>
<string name="hide_verb">Slėpti</string>
<string name="reveal_verb">Atskleisti</string>
<string name="for_me_only">Ištrinti man</string>
<string name="for_everybody">Visiems</string>
<string name="icon_descr_edited">taisyta</string>
<string name="observer_cant_send_message_desc">Susisiekite su grupės administratoriumi.</string>
<string name="button_delete_contact">Ištrinti adresatą</string>
<string name="delete_contact_question">Ištrinti adresatą\?</string>
<string name="create_group">Sukurti slaptą grupę</string>
<string name="from_gallery_button">Iš galerijos</string>
<string name="toast_permission_denied">Leidimas atmestas!</string>
<string name="reject_contact_button">Atmesti</string>
<string name="incorrect_code">Neteisingas saugumo kodas!</string>
<string name="smp_servers_save">Įrašyti serverius</string>
<string name="smp_save_servers_question">Įrašyti serverius\?</string>
<string name="create_address">Sukurti adresą</string>
<string name="delete_address">Ištrinti adresą</string>
<string name="error_saving_user_password">Klaida įrašant naudotojo slaptažodį</string>
<string name="create_profile_button">Sukurti</string>
<string name="how_it_works">Kaip tai veikia</string>
<string name="incoming_audio_call">Gaunamas garso skambutis</string>
<string name="incoming_video_call">Gaunamas vaizdo skambutis</string>
<string name="reject">Atmesti</string>
<string name="no_call_on_lock_screen">Išjungti</string>
<string name="settings_experimental_features">Eksperimentinės ypatybės</string>
<string name="database_passphrase">Duomenų bazės slaptafrazė</string>
<string name="delete_database">Ištrinti duomenų bazę</string>
<string name="export_database">Eksportuoti duomenų bazę</string>
<string name="import_database">Importuoti duomenų bazę</string>
<string name="set_password_to_export_desc">Duomenų bazė yra šifruota naudojant atsitiktinę slaptafrazę. Prieš eksportuodami duomenų bazę, pakeiskite slaptafrazę.</string>
<string name="error_exporting_chat_database">Klaida eksportuojant pokalbio duomenų bazę</string>
<string name="error_stopping_chat">Klaida sustabdant pokalbį</string>
<string name="import_database_question">Importuoti pokalbio duomenų bazę\?</string>
<string name="current_passphrase">Dabartinė slaptafrazė…</string>
<string name="encrypted_with_random_passphrase">Duomenų bazė yra šifruota naudojant atsitiktinę slaptafrazę, kurią galite pakeisti.</string>
<string name="encrypt_database_question">Šifruoti duomenų bazę\?</string>
<string name="error_with_info">Klaida: %s</string>
<string name="button_edit_group_profile">Taisyti grupės profilį</string>
<string name="group_link">Grupės nuoroda</string>
<string name="button_create_group_link">Sukurti nuorodą</string>
<string name="info_row_database_id">Duomenų bazės ID</string>
<string name="error_changing_role">Klaida keičiant vaidmenį</string>
<string name="conn_level_desc_direct">tiesioginis</string>
<string name="conn_level_desc_indirect">netiesioginis (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="create_secret_group_title">Sukurti slaptą grupę</string>
<string name="error_saving_group_profile">Klaida įrašant grupės profilį</string>
<string name="dont_show_again">Daugiau neberodyti</string>
<string name="theme_dark">Tamsus</string>
<string name="reset_color">Atstatyti spalvas</string>
<string name="save_color">Įrašyti spalvą</string>
<string name="group_preferences">Grupės nuostatos</string>
<string name="full_deletion">Ištrinti visiems</string>
<string name="direct_messages">Tiesioginės žinutės</string>
<string name="v4_4_disappearing_messages">Išnykstančios žinutės</string>
<string name="timed_messages">Išnykstančios žinutės</string>
<string name="v4_4_french_interface">Sąsaja prancūzų kalba</string>
<string name="v4_3_improved_privacy_and_security">Patobulintas privatumas ir saugumas</string>
<string name="num_contacts_selected">Pažymėta adresatų: %d</string>
<string name="delete_chat_profile_question">Ištrinti pokalbio profilį\?</string>
<string name="error_importing_database">Klaida importuojant pokalbio duomenų bazę</string>
<string name="delete_messages_after">Ištrinti žinutes po</string>
<string name="database_will_be_encrypted">Duomenų bazė bus šifruota.</string>
<string name="alert_title_no_group">Grupė nerasta!</string>
<string name="error_deleting_link_for_group">Klaida ištrinant grupės nuorodą</string>
<string name="error_updating_link_for_group">Klaida atnaujinant grupės nuorodą</string>
<string name="database_upgrade">Duomenų bazės naujinimas</string>
<string name="incompatible_database_version">Nesuderinama duomenų bazės versija</string>
<string name="group_member_status_group_deleted">grupė ištrinta</string>
<string name="save_and_update_group_profile">Įrašyti ir atnaujinti grupės profilį</string>
<string name="delete_messages">Ištrinti žinutes</string>
<string name="error_changing_message_deletion">Klaida keičiant nustatymą</string>
<string name="error_encrypting_database">Klaida šifruojant duomenų bazę</string>
<string name="encrypted_database">Šifruota duomenų bazė</string>
<string name="restore_database_alert_confirm">Atkurti</string>
<string name="create_group_link">Sukurti grupės nuorodą</string>
<string name="delete_link">Ištrinti nuorodą</string>
<string name="delete_link_question">Ištrinti nuorodą\?</string>
<string name="error_creating_link_for_group">Klaida kuriant grupės nuorodą</string>
<string name="how_simplex_works">Kaip <xliff:g id="appName">SimpleX</xliff:g> veikia</string>
<string name="database_encrypted">Duomenų bazė šifruota!</string>
<string name="group_member_status_creator">kūrėjas</string>
<string name="delete_group_question">Ištrinti grupę\?</string>
<string name="user_hide">Slėpti</string>
<string name="error_saving_file">Klaida įrašant failą</string>
<string name="hide_dev_options">Slėpti:</string>
<string name="database_error">Duomenų bazės klaida</string>
<string name="file_with_path">Failas: %s</string>
<string name="profile_password">Profilio slaptažodis</string>
<string name="remove_member_confirmation">Šalinti</string>
<string name="v4_3_improved_server_configuration">Patobulinta serverio konfigūracija</string>
<string name="delete_chat_profile">Ištrinti pokalbio profilį</string>
<string name="delete_profile">Ištrinti profilį</string>
<string name="share_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
<string name="delete_contact_menu_action">Ištrinti</string>
<string name="save_and_notify_contact">Įrašyti ir pranešti adresatui</string>
<string name="enable_automatic_deletion_question">Įjungti automatinį žinučių ištrynimą\?</string>
<string name="error_deleting_database">Klaida ištrinant pokalbio duomenų bazę</string>
<string name="error_starting_chat">Klaida pradedant pokalbį</string>
<string name="hide_profile">Slėpti profilį</string>
<string name="import_database_confirmation">Importuoti</string>
<string name="save_and_notify_contacts">Įrašyti ir pranešti adresatams</string>
<string name="callstate_starting">pradedama…</string>
<string name="use_chat">Naudoti pokalbį</string>
<string name="icon_descr_speaker_off">Išjungti garsiakalbį</string>
<string name="icon_descr_speaker_on">Įjungti garsiakalbį</string>
<string name="alert_title_skipped_messages">Praleistos žinutės</string>
<string name="settings_section_title_settings">NUSTATYMAI</string>
<string name="theme_system">Sistemos</string>
<string name="unknown_message_format">nežinomas žinutės formatas</string>
<string name="simplex_link_contact">SimpleX adresato adresas</string>
<string name="simplex_link_group">SimpleX grupės nuoroda</string>
<string name="simplex_link_invitation">SimpleX vienkartinis pakvietimas</string>
<string name="simplex_link_mode">SimpleX nuorodos</string>
<string name="settings_notification_preview_mode_title">Rodyti peržiūrą</string>
<string name="auth_unlock">Atrakinti</string>
<string name="share_verb">Bendrinti</string>
<string name="tap_to_start_new_chat">Bakstelėkite, norėdami pradėti naują pokalbį</string>
<string name="share_file">Bendrinti failą…</string>
<string name="icon_descr_settings">Nustatymai</string>
<string name="show_QR_code">Rodyti QR kodą</string>
<string name="share_invitation_link">Bendrinti pakvietimo nuorodą</string>
<string name="smp_servers">SMP serveriai</string>
<string name="use_simplex_chat_servers__question">Naudoti <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serverius\?</string>
<string name="network_disable_socks">Naudoti tiesioginį interneto ryšį\?</string>
<string name="update_network_settings_confirmation">Atnaujinti</string>
<string name="update_network_settings_question">Atnaujinti tinklo nustatymus\?</string>
<string name="ntf_channel_messages">SimpleX Chat žinutės</string>
<string name="ntf_channel_calls">SimpleX Chat skambučiai</string>
<string name="notification_preview_mode_message_desc">Rodyti adresatą ir žinutę</string>
<string name="notification_preview_mode_contact_desc">Rodyti tik adresatą</string>
<string name="auth_stop_chat">Stabdyti pokalbį</string>
<string name="share_message">Bendrinti žinutę…</string>
<string name="use_camera_button">Naudoti kamerą</string>
<string name="thank_you_for_installing_simplex">Dėkojame, kad įdiegėte <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="smp_servers_use_server">Naudoti serverį</string>
<string name="network_socks_toggle">Naudoti SOCKS įgaliotąjį serverį (prievadas 9050)</string>
<string name="network_enable_socks">Naudoti SOCKS įgaliotąjį serverį\?</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="share_link">Bendrinti nuorodą</string>
<string name="tap_to_activate_profile">Bakstelėkite, norėdami aktyvuoti profilį.</string>
<string name="language_system">Sistemos</string>
<string name="v4_6_audio_video_calls_descr">Bluetooth palaikymas ir kiti patobulinimai.</string>
<string name="group_invitation_tap_to_join">Bakstelėkite, norėdami prisijungti</string>
<string name="show_call_on_lock_screen">Rodyti</string>
<string name="stop_chat_confirmation">Stabdyti</string>
<string name="upgrade_and_open_chat">Naujinti ir atverti pokalbį</string>
<string name="switch_verb">Perjungti</string>
<string name="icon_descr_received_msg_status_unread">neskaityta</string>
<string name="show_dev_options">Rodyti:</string>
<string name="stop_chat_question">Stabdyti pokalbį\?</string>
<string name="update_database">Atnaujinti</string>
<string name="unknown_error">Nežinoma klaida</string>
<string name="add_contact_or_create_group">Pradėti naują pokalbį</string>
<string name="callstate_waiting_for_answer">laukiama, kol bus atsiliepta…</string>
<string name="callstate_waiting_for_confirmation">laukiama patvirtinimo…</string>
<string name="icon_descr_video_off">Išjungti vaizdą</string>
<string name="icon_descr_video_on">Įjungti vaizdą</string>
<string name="your_privacy">Jūsų privatumas</string>
<string name="settings_section_title_you">JŪS</string>
<string name="wrong_passphrase_title">Neteisinga slaptafrazė!</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="sender_you_pronoun">jūs</string>
<string name="description_via_group_link">per grupės nuorodą</string>
<string name="description_via_contact_address_link">per adresato adreso nuorodą</string>
<string name="description_via_one_time_link">per vienkartinę nuorodą</string>
<string name="simplex_link_connection">per <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode_browser">Per naršyklę</string>
<string name="waiting_for_file">Laukiama failo</string>
<string name="voice_message_with_duration">Balso žinutė (<xliff:g id="duration">%1$s</xliff:g>)</string>
<string name="voice_message_send_text">Balso žinutė…</string>
<string name="view_security_code">Rodyti saugumo kodą</string>
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> adresas</string>
<string name="your_SMP_servers">Jūsų SMP serveriai</string>
<string name="your_ICE_servers">Jūsų ICE serveriai</string>
<string name="users_add">Pridėti profilį</string>
<string name="users_delete_all_chats_deleted">Visi pokalbiai ir žinutės bus ištrinti to neįmanoma bus atšaukti!</string>
<string name="welcome">Sveiki!</string>
<string name="attach">Pridėti</string>
<string name="observer_cant_send_message_title">Jūs negalite siųsti žinučių!</string>
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> logotipas</string>
<string name="your_settings">Jūsų nustatymai</string>
<string name="smp_servers_your_server">Jūsų serveris</string>
<string name="smp_servers_your_server_address">Jūsų serverio adresas</string>
<string name="smp_servers_add_to_another_device">Pridėti į kitą įrenginį</string>
<string name="using_simplex_chat_servers">Naudojami <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serveriai.</string>
<string name="network_settings">Išplėstiniai tinklo nustatymai</string>
<string name="your_current_profile">Jūsų dabartinis profilis</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų adresatais. <xliff:g id="appName">SimpleX</xliff:g> serveriai negali matyti jūsų profilio.</string>
<string name="icon_descr_video_call">vaizdo skambutis</string>
<string name="your_calls">Jūsų skambučiai</string>
<string name="webrtc_ice_servers">WebRTC ICE serveriai</string>
<string name="your_ice_servers">Jūsų ICE serveriai</string>
<string name="snd_group_event_user_left">jūs išėjote</string>
<string name="group_member_role_admin">administratorius</string>
<string name="incognito_random_profile">Jūsų atsitiktinis profilis</string>
<string name="chat_preferences_you_allow">Jūs leidžiate</string>
<string name="your_preferences">Jūsų nuostatos</string>
<string name="v4_3_voice_messages">Balso žinutės</string>
<string name="voice_messages">Balso žinutės</string>
<string name="chat_preferences_yes">taip</string>
<string name="you_joined_this_group">Jūs prisijungėte prie šios grupės</string>
<string name="database_downgrade_warning">Įspėjimas: galite prarasti tam tikrus duomenis!</string>
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Jūs nustosite gauti žinutes iš šios grupės. Pokalbio istorija bus išsaugota.</string>
<string name="group_info_member_you">jūs: <xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="personal_welcome">Sveiki, <xliff:g>%1$s</xliff:g>!</string>
<string name="your_chats">Jūsų pokalbiai</string>
<string name="wrong_passphrase">Neteisinga duomenų bazės slaptafrazė</string>
<string name="icon_descr_video_snd_complete">Vaizdo įrašas išsiųstas</string>
<string name="voice_message">Balso žinutė</string>
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> komanda</string>
<string name="whats_new">Kas naujo</string>
</resources>

View File

@@ -174,6 +174,7 @@
<string name="contribute">Bijdragen</string>
<string name="configure_ICE_servers">ICE servers configureren</string>
<string name="network_session_mode_entity">Verbinding</string>
<string name="core_build_timestamp">Core gebouwd op: %s</string>
<string name="core_version">Core versie: v%s</string>
<string name="callstate_connected">verbonden</string>
<string name="callstate_connecting">Verbinden…</string>
@@ -385,8 +386,6 @@
<string name="file_not_found">Bestand niet gevonden</string>
<string name="file_saved">Bestand opgeslagen</string>
<string name="from_gallery_button">Galerij</string>
<string name="gallery_image_button">Foto</string>
<string name="gallery_video_button">Video</string>
<string name="error_saving_ICE_servers">Fout bij opslaan van ICE servers</string>
<string name="callstate_ended">geëindigd</string>
<string name="group_members_can_send_disappearing">Groepsleden kunnen verdwijnende berichten sturen.</string>
@@ -485,15 +484,17 @@
<string name="join_group_incognito_button">Doe incognito mee</string>
<string name="group_preview_join_as">lid worden als %s</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Het kan gebeuren wanneer:
\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.
\n2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt.
\n3. De verbinding is verbroken.</string>
\n1. De berichten op de server verlopen als ze 30 dagen niet zijn ontvangen,
\n2. De server die u gebruikt om de berichten van deze contactpersoon te ontvangen, is bijgewerkt en opnieuw opgestart.
\n3. De verbinding is verbroken.
\nMaak verbinding met de ontwikkelaars via Instellingen om de updates over de servers te ontvangen.
\nWe zullen serverredundantie toevoegen om verloren berichten te voorkomen.</string>
<string name="joining_group">Deel nemen aan groep</string>
<string name="leave_group_button">Verlaten</string>
<string name="group_member_role_member">Gebruiker</string>
<string name="image_descr_link_preview">link voorbeeld afbeelding</string>
<string name="member_info_section_title_member">GEBRUIKER</string>
<string name="settings_section_title_messages">BERICHTEN EN BESTANDEN</string>
<string name="settings_section_title_messages">BERICHTEN</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 mobiel: tik op <b>Openen in mobiele app</b> en tik vervolgens op <b>Verbinden</b> in de app.</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Gebruiker wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!</string>
<string name="message_delivery_error_title">Fout bij bezorging van bericht</string>
@@ -846,7 +847,7 @@
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="connected_to_server_to_receive_messages_from_contact">U bent verbonden met de server die wordt gebruikt om berichten van dit contact te ontvangen.</string>
<string name="profile_will_be_sent_to_contact_sending_link">Je profiel wordt verzonden naar het contact van wie je deze link hebt ontvangen.</string>
<string name="you_will_join_group">U wordt lid van de groep waar deze link naar verwijst en maakt verbinding met de groepsleden.</string>
<string name="you_will_join_group">U wordt lid van een groep waarnaar deze link verwijst en maakt verbinding met de groepsleden.</string>
<string name="notifications_mode_off">Wordt uitgevoerd wanneer de app is geopend</string>
<string name="settings_notification_preview_mode_title">Toon voorbeeld</string>
<string name="ntf_channel_calls">SimpleX Chat oproepen</string>
@@ -935,7 +936,9 @@
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde.
\nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string>
<string name="invite_prohibited_description">Je probeert een contact met wie je een incognito profiel hebt gedeeld, uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. <xliff:g id="appName">SimpleX</xliff:g> servers kunnen uw profiel niet zien.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten.
\n
\n<xliff:g id="appName">SimpleX</xliff:g> servers kunnen uw profiel niet zien.</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat.</string>
<string name="images_limit_title">Te veel afbeeldingen!</string>
<string name="icon_descr_speaker_off">Luidspreker uit</string>
@@ -1018,6 +1021,7 @@
<string name="to_reveal_profile_enter_password">Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen.</string>
<string name="button_welcome_message">Welkomst bericht</string>
<string name="you_will_still_receive_calls_and_ntfs">U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn.</string>
<string name="settings_send_files_via_xftp">Verzend video\'s en bestanden via XFTP</string>
<string name="database_downgrade">Database downgraden</string>
<string name="invalid_migration_confirmation">Ongeldige migratie bevestiging</string>
<string name="upgrade_and_open_chat">Upgrade en open chat</string>
@@ -1036,6 +1040,7 @@
<string name="hide_dev_options">Verbergen:</string>
<string name="show_developer_options">Ontwikkelaars opties tonen</string>
<string name="settings_section_title_experimenta">EXPERIMENTEEL</string>
<string name="xftp_requires_v461">v4.6.1+ is vereist om te ontvangen via XFTP.</string>
<string name="cancel_file__question">Bestand overdracht annuleren\?</string>
<string name="file_transfer_will_be_cancelled_warning">Bestand overdracht wordt geannuleerd. Als het bezig is, wordt het gestopt.</string>
<string name="delete_profile">Verwijder profiel</string>
@@ -1052,90 +1057,4 @@
<string name="waiting_for_video">Wachten op video</string>
<string name="video_descr">Video</string>
<string name="video_will_be_received_when_contact_completes_uploading">De video wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</string>
<string name="error_saving_xftp_servers">Fout bij opslaan van XFTP-servers</string>
<string name="error_loading_xftp_servers">Fout bij het laden van XFTP servers</string>
<string name="error_xftp_test_server_auth">Server vereist autorisatie om te uploaden, wachtwoord controleren</string>
<string name="smp_server_test_compare_file">Bestand vergelijken</string>
<string name="smp_server_test_delete_file">Bestand verwijderen</string>
<string name="smp_server_test_download_file">Bestand downloaden</string>
<string name="smp_server_test_upload_file">Upload bestand</string>
<string name="xftp_servers">XFTP servers</string>
<string name="your_XFTP_servers">Uw XFTP servers</string>
<string name="host_verb">Host</string>
<string name="port_verb">Poort</string>
<string name="network_socks_proxy_settings">SOCKS proxy instellingen</string>
<string name="network_socks_toggle_use_socks_proxy">Gebruik SOCKS proxy</string>
<string name="disable_onion_hosts_when_not_supported">Stel <i>Use .onion hosts</i> in op Nee als de SOCKS-proxy deze niet ondersteunt.</string>
<string name="smp_server_test_create_file">Bestand maken</string>
<string name="error_loading_smp_servers">Fout bij het laden van SMP servers</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Zorg ervoor dat XFTP server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string>
<string name="network_proxy_port">poort %d</string>
<string name="lock_after">Vergrendelen na</string>
<string name="la_auth_failed">Verificatie mislukt</string>
<string name="la_change_app_passcode">Toegangscode wijzigen</string>
<string name="la_current_app_passcode">Huidige toegangscode</string>
<string name="la_enter_app_passcode">Voer toegangscode in</string>
<string name="la_no_app_password">Geen app toegangscode</string>
<string name="la_lock_mode_passcode">Toegangscode invoer</string>
<string name="la_lock_mode">SimpleX Vergrendel modus</string>
<string name="la_lock_mode_system">Systeem authenticatie</string>
<string name="la_could_not_be_verified">U kon niet worden geverifieerd; probeer het opnieuw.</string>
<string name="la_authenticate">Authenticeren</string>
<string name="la_minutes">%d minuten</string>
<string name="la_seconds">%d seconden</string>
<string name="la_immediately">Onmiddellijk</string>
<string name="la_please_remember_to_store_password">Onthoud of bewaar het veilig - er is geen manier om een verloren wachtwoord te herstellen!</string>
<string name="lock_not_enabled">SimpleX vergrendeling niet ingeschakeld!</string>
<string name="you_can_turn_on_lock">Je kunt SimpleX Vergrendeling aanzetten via Instellingen.</string>
<string name="enable_lock">Vergrendeling inschakelen</string>
<string name="lock_mode">Vergrendeling modus</string>
<string name="submit_passcode">Indienen</string>
<string name="authentication_cancelled">Verificatie geannuleerd</string>
<string name="change_lock_mode">Wijzig de vergrendelings modus</string>
<string name="confirm_passcode">Bevestig toegangscode</string>
<string name="incorrect_passcode">Onjuiste toegangscode</string>
<string name="new_passcode">Nieuwe toegangscode</string>
<string name="la_mode_passcode">Toegangscode</string>
<string name="passcode_changed">Toegangscode gewijzigd!</string>
<string name="passcode_not_changed">Toegangscode niet gewijzigd!</string>
<string name="passcode_set">Toegangscode ingesteld!</string>
<string name="la_mode_system">Systeem</string>
<string name="decryption_error">Decryptie fout</string>
<string name="alert_text_msg_bad_hash">De hash van het vorige bericht is anders.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> berichten overgeslagen.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte.</string>
<string name="alert_text_fragment_please_report_to_developers">Meld het alsjeblieft aan de ontwikkelaars.</string>
<string name="alert_text_fragment_permanent_error_reconnect">Deze fout is permanent voor deze verbinding, maak opnieuw verbinding.</string>
<string name="alert_title_msg_bad_hash">Onjuiste bericht hash</string>
<string name="alert_title_msg_bad_id">Onjuiste bericht ID</string>
<string name="alert_text_msg_bad_id">De ID van het volgende bericht is onjuist (minder of gelijk aan het vorige).
\nHet kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g>-berichten konden niet worden ontsleuteld.</string>
<string name="no_spaces">Geen spaties!</string>
<string name="stop_rcv_file__message">Het ontvangen van het bestand wordt gestopt.</string>
<string name="revoke_file__confirm">Intrekken</string>
<string name="revoke_file__action">Bestand intrekken</string>
<string name="revoke_file__title">Bestand intrekken\?</string>
<string name="stop_file__confirm">Stop</string>
<string name="stop_file__action">Bestand stoppen</string>
<string name="stop_rcv_file__title">Stopt met het ontvangen van een bestand\?</string>
<string name="stop_snd_file__title">Bestand verzenden stoppen\?</string>
<string name="only_your_contact_can_make_calls">Alleen je contact kan bellen.</string>
<string name="revoke_file__message">Het bestand wordt van de servers verwijderd.</string>
<string name="both_you_and_your_contact_can_make_calls">Zowel u als uw contact persoon kunnen bellen.</string>
<string name="only_you_can_make_calls">Alleen jij kunt bellen.</string>
<string name="stop_snd_file__message">Het verzenden van het bestand wordt gestopt.</string>
<string name="allow_calls_only_if">Sta oproepen alleen toe als uw contact persoon dit toestaat.</string>
<string name="allow_your_contacts_to_call">Sta toe dat uw contacten u bellen.</string>
<string name="audio_video_calls">Audio/video oproepen</string>
<string name="available_in_v51">"
\nBeschikbaar in v5.1"</string>
<string name="prohibit_calls">Audio/video gesprekken verbieden.</string>
<string name="calls_prohibited_with_this_contact">Audio/video gesprekken zijn verboden.</string>
<string name="v5_0_large_files_support_descr">Snel en niet wachten tot de afzender online is!</string>
<string name="v5_0_app_passcode">App toegangscode</string>
<string name="v5_0_app_passcode_descr">Stel het in in plaats van systeemverificatie.</string>
<string name="v5_0_polish_interface">Poolse interface</string>
<string name="v5_0_polish_interface_descr">Dank aan de gebruikers draag bij via Weblate!</string>
<string name="v5_0_large_files_support">Video\'s en bestanden tot 1 GB</string>
</resources>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -258,9 +258,7 @@
<string name="toast_permission_denied">Разрешение не получено!</string>
<string name="use_camera_button">Камера</string>
<string name="from_gallery_button">Галерея</string>
<string name="gallery_image_button">Фото</string>
<string name="gallery_video_button">Видео</string>
<string name="choose_file">Файл</string>
<string name="choose_file">Файлы</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Спасибо, что установили <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
<string name="you_can_connect_to_simplex_chat_founder">Вы можете <font color="#0088ff">соединиться с разработчиками</font>, чтобы задать любые вопросы или получать уведомления о новых версиях.</string>
@@ -429,7 +427,9 @@
<string name="display_name__field">Имя профиля:</string>
<string name="full_name__field">"Полное имя:</string>
<string name="your_current_profile">Ваш активный профиль</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам. <xliff:g id="appName">SimpleX</xliff:g> серверы не могут получить доступ к Вашему профилю.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам.
\n
\n<xliff:g id="appName">SimpleX</xliff:g> серверы не могут получить доступ к Вашему профилю.</string>
<string name="edit_image">Поменять аватар</string>
<string name="delete_image">Удалить аватар</string>
<string name="save_preferences_question">Сохранить предпочтения?</string>
@@ -561,10 +561,12 @@
<string name="integrity_msg_bad_id">ошибка ID сообщения</string>
<string name="integrity_msg_duplicate">повторное сообщение</string>
<string name="alert_title_skipped_messages">Пропущенные сообщения</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Это может произойти, когда:
\n1. Клиент отправителя удалил неотправленные сообщения через 2 дня, или сервер через 30 дней.
\n2. Расшифровка сообщения была невозможна, когда Вы или Ваш контакт использовали старую копию базы данных.
\n3. Соединение компроментировано.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">Это может случится, когда:
\n1. Сервер удалил сообщения, если они не были доставлены в течение 30 дней.
\n2. Сервер, через который Вы получаете сообщения от контакта, был обновлён и перезапущен.
\n3. Соединение компроментировано.
\nПожалуйста, соединитесь с девелоперами через Настройки, чтобы получать уведомления о серверах.
\nМы планируем добавить избыточную доставку сообщений, чтобы не терять сообщения.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Конфиденциальность</string>
<string name="your_privacy">Конфиденциальность</string>
@@ -585,7 +587,7 @@
<string name="settings_section_title_socks">SOCKS ПРОКСИ</string>
<string name="settings_section_title_icon">ИКОНКА</string>
<string name="settings_section_title_themes">ТЕМЫ</string>
<string name="settings_section_title_messages">СООБЩЕНИЯ И ФАЙЛЫ</string>
<string name="settings_section_title_messages">СООБЩЕНИЯ</string>
<string name="settings_section_title_calls">ЗВОНКИ</string>
<string name="settings_section_title_incognito">Режим Инкогнито</string>
<!-- DatabaseView.kt -->
@@ -1023,6 +1025,7 @@
<string name="network_session_mode_entity_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого контакта и члена группы</b>.
\n<b>Обратите внимание</b>: если у Вас много контактов, потребление батареи и трафика может быть значительно выше, и некоторые соединения могут не работать.</string>
<string name="network_session_mode_user_description">Отдельное TCP-соединение (и авторизация SOCKS) будет использоваться <b>для каждого профиля чата, который Вы имеете в приложении</b>.</string>
<string name="core_build_timestamp">Ядро скомпилировано: %s</string>
<string name="core_version">Версия ядра: v%s</string>
<string name="users_delete_question">Удалить профиль чата\?</string>
<string name="users_delete_profile_for">Удалить профиль чата для</string>
@@ -1102,6 +1105,7 @@
<string name="v4_6_chinese_spanish_interface_descr">Благодаря пользователям добавьте переводы через Weblate!</string>
<string name="you_will_still_receive_calls_and_ntfs">Вы все равно получите звонки и уведомления в профилях без звука, когда они активные.</string>
<string name="you_can_hide_or_mute_user_profile">Вы можете скрыть профиль или выключить уведомления - подержите, чтобы увидеть меню.</string>
<string name="settings_send_files_via_xftp">Отправлять видео и файлы через XFTP</string>
<string name="image_will_be_received_when_contact_completes_uploading">Изображение будет принято когда Ваш контакт его загрузит.</string>
<string name="file_will_be_received_when_contact_completes_uploading">Файл будет принят когда Ваш контакт загрузит его.</string>
<string name="database_upgrade">Обновление базы данных</string>
@@ -1123,6 +1127,7 @@
<string name="delete_chat_profile">Удалить профиль чата</string>
<string name="delete_profile">Удалить профиль</string>
<string name="profile_password">Пароль профиля</string>
<string name="xftp_requires_v461">v4.6.1+ необходима для приема файлов через XFTP.</string>
<string name="videos_limit_title">Слишком много видео!</string>
<string name="icon_descr_video_asked_to_receive">Запросил прием видео</string>
<string name="video_descr">Видео</string>
@@ -1136,90 +1141,4 @@
<string name="unhide_profile">Раскрыть профиль</string>
<string name="video_will_be_received_when_contact_is_online">Видео будет получено, когда Ваш контакт будет онлайн, пожалуйста, подождите или проверьте позже!</string>
<string name="unhide_chat_profile">Раскрыть профиль чата</string>
<string name="error_loading_smp_servers">Ошибка при загрузке SMP серверов</string>
<string name="error_loading_xftp_servers">Ошибка при загрузке XFTP серверов</string>
<string name="error_saving_xftp_servers">Ошибка при сохранении XFTP серверов</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Проверьте, что адреса XFTP северов в правильном формате и не дублируются.</string>
<string name="error_xftp_test_server_auth">Сервер требует авторизации для загрузки, проверьте пароль.</string>
<string name="smp_server_test_compare_file">Сравнение файла</string>
<string name="smp_server_test_delete_file">Удалить файл</string>
<string name="smp_server_test_download_file">Загрузка файла</string>
<string name="smp_server_test_upload_file">Загрузка файла</string>
<string name="xftp_servers">XFTP серверы</string>
<string name="your_XFTP_servers">Ваши XFTP серверы</string>
<string name="disable_onion_hosts_when_not_supported">Установите <i>Использовать .onion хосты</i> в Нет если SOCKS прокси их не поддерживает.</string>
<string name="la_auth_failed">Ошибка аутентификации</string>
<string name="la_no_app_password">Нет кода доступа</string>
<string name="la_lock_mode_passcode">Ввод кода доступа</string>
<string name="la_lock_mode">Режим Блокировки SimpleX</string>
<string name="la_lock_mode_system">Системная аутентификация</string>
<string name="la_could_not_be_verified">Ошибка аутентификации; попробуйте еще раз.</string>
<string name="la_immediately">Сразу</string>
<string name="la_authenticate">Аутентифицировать</string>
<string name="la_change_app_passcode">Изменить код доступа</string>
<string name="la_current_app_passcode">Текущий Код</string>
<string name="la_enter_app_passcode">Введите Код</string>
<string name="network_socks_proxy_settings">Настройки SOCKS прокси</string>
<string name="network_socks_toggle_use_socks_proxy">Использовать SOCKS прокси</string>
<string name="host_verb">Хост</string>
<string name="port_verb">Порт</string>
<string name="network_proxy_port">порт %d</string>
<string name="enable_lock">Включить блокировку</string>
<string name="authentication_cancelled">Аутентификация отменена</string>
<string name="la_mode_system">Системная</string>
<string name="la_seconds">%d секунд</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> сообщений не удалось расшифровать.</string>
<string name="decryption_error">Ошибка расшифровки</string>
<string name="lock_not_enabled">Блокировка SimpleX не включена!</string>
<string name="alert_title_msg_bad_hash">Ошибка хэш сообщения</string>
<string name="alert_text_msg_bad_hash">Хэш предыдущего сообщения отличается.</string>
<string name="confirm_passcode">Подтвердить Код</string>
<string name="incorrect_passcode">Неправильный код</string>
<string name="lock_after">Заблокировать через</string>
<string name="lock_mode">Режим блокировки</string>
<string name="new_passcode">Новый Код</string>
<string name="submit_passcode">Продолжить</string>
<string name="la_mode_passcode">Код доступа</string>
<string name="passcode_changed">Код доступа изменён!</string>
<string name="passcode_not_changed">Код доступа не изменён!</string>
<string name="passcode_set">Код доступа установлен!</string>
<string name="change_lock_mode">Изменить режим блокировки</string>
<string name="stop_rcv_file__message">Приём файла будет прекращён.</string>
<string name="revoke_file__confirm">Отозвать</string>
<string name="revoke_file__title">Отозвать файл\?</string>
<string name="stop_snd_file__message">Отправка файла будет остановлена.</string>
<string name="stop_file__confirm">Остановить</string>
<string name="stop_file__action">Остановить файл</string>
<string name="stop_snd_file__title">Остановить отправку файла\?</string>
<string name="alert_title_msg_bad_id">Ошибка ID сообщения</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Это может произойти, когда Вы или Ваш контакт используете старую копию базы данных.</string>
<string name="alert_text_fragment_please_report_to_developers">Пожалуйста, сообщите об этой ошибке девелоперам.</string>
<string name="alert_text_msg_bad_id">Неправильный ID предыдущего сообщения (меньше или равен предыдущему).
\nЭто может произойти из-за ошибки программы, или когда соединение компроментировано.</string>
<string name="alert_text_fragment_permanent_error_reconnect">Эта ошибка постоянная для этого соединения, пожалуйста, соединитесь снова.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> сообщений пропущено.</string>
<string name="allow_calls_only_if">Разрешить звонки, только если их разрешает Ваш контакт.</string>
<string name="allow_your_contacts_to_call">Разрешить Вашим контактам звонить Вам.</string>
<string name="audio_video_calls">Аудио/видео звонки</string>
<string name="calls_prohibited_with_this_contact">Аудио/видео звонки запрещены.</string>
<string name="available_in_v51">"
\nДоступно в v5.1"</string>
<string name="both_you_and_your_contact_can_make_calls">Вы и Ваш контакт можете совершать звонки.</string>
<string name="only_you_can_make_calls">Только Вы можете совершать звонки.</string>
<string name="only_your_contact_can_make_calls">Только Ваш контакт может совершать звонки.</string>
<string name="prohibit_calls">Запретить аудио/видео звонки.</string>
<string name="no_spaces">Без пробелов!</string>
<string name="v5_0_large_files_support_descr">Быстрые и не нужно ждать, когда отправитель онлайн!</string>
<string name="v5_0_polish_interface">Польский интерфейс</string>
<string name="v5_0_app_passcode_descr">Установите код вместо системной аутентификации.</string>
<string name="v5_0_polish_interface_descr">Благодаря пользователям добавьте переводы через Weblate!</string>
<string name="v5_0_large_files_support">Видео и файлы до 1гб</string>
<string name="v5_0_app_passcode">Код доступа в приложение</string>
<string name="revoke_file__message">Файл будет удалён с серверов.</string>
<string name="smp_server_test_create_file">Создание файла</string>
<string name="la_minutes">%d минут(ы)</string>
<string name="la_please_remember_to_store_password">Пожалуйста, запомните или сохраните его - восстановить потерянный код доступа невозможно!</string>
<string name="revoke_file__action">Отозвать файл</string>
<string name="stop_rcv_file__title">Остановить приём файла\?</string>
<string name="you_can_turn_on_lock">Вы можете включить Блокировку SimpleX через Настройки.</string>
</resources>

View File

@@ -37,7 +37,7 @@
<string name="delete_files_and_media_all">删除所有文件</string>
<string name="messages_section_title">消息</string>
<string name="delete_messages_after">在此后删除消息</string>
<string name="settings_section_title_messages">消息和文件</string>
<string name="settings_section_title_messages">消息</string>
<string name="users_add">添加个人资料</string>
<string name="users_delete_all_chats_deleted">所有聊天记录和消息将被删除——这一行为无法撤销!</string>
<string name="clear_chat_warning">所有聊天记录和消息将被删除——这一行为无法撤销!只有您的消息会被删除。</string>
@@ -238,7 +238,7 @@
<string name="callstate_connected">已连接</string>
<string name="connect_button">连接</string>
<string name="connect_via_link_verb">连接</string>
<string name="choose_file">文件</string>
<string name="choose_file">选择文件</string>
<string name="network_session_mode_user">聊天资料</string>
<string name="v4_5_transport_isolation_descr">按聊天资料默认或按连接BETA</string>
<string name="smp_servers_check_address">检查服务器地址并再试一次。</string>
@@ -308,7 +308,7 @@
<string name="files_and_media_section">文件和媒体</string>
<string name="current_passphrase">现有密码……</string>
<string name="encrypt_database">加密</string>
<string name="encrypted_database">加密数据库</string>
<string name="encrypted_database">加密数据库</string>
<string name="enter_correct_passphrase">输入正确密码。</string>
<string name="icon_descr_group_inactive">不活跃群组</string>
<string name="group_member_status_creator">创建者</string>
@@ -363,6 +363,7 @@
<string name="encrypted_audio_call">端到端加密语音通话</string>
<string name="enter_one_ICE_server_per_line">ICE 服务器(每行一个)</string>
<string name="create_address">创建地址</string>
<string name="core_build_timestamp">核心构建于:%s</string>
<string name="core_version">核心版本: v%s</string>
<string name="display_name__field">显示名:</string>
<string name="full_name__field">全名:</string>
@@ -429,14 +430,12 @@
<string name="network_disable_socks_info">如果您确认,消息服务器将能够看到您的 IP 地址和您的提供商——以及您正在连接的服务器。</string>
<string name="image_descr">图片</string>
<string name="icon_descr_image_snd_complete">图片已发送</string>
<string name="auth_device_authentication_is_disabled_turning_off">设备证被禁用。关闭 SimpleX 锁定。</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">没有启用设备证。一旦启用设备证,您可以通过设置打开 SimpleX 锁定。</string>
<string name="auth_device_authentication_is_disabled_turning_off">设备证被禁用。关闭 SimpleX 锁定。</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">没有启用设备证。一旦启用设备证,您可以通过设置打开 SimpleX 锁定。</string>
<string name="auth_disable_simplex_lock">禁用 SimpleX 锁定</string>
<string name="maximum_supported_file_size">目前支持的最大文件尺寸是 <xliff:g id="maxFileSize">%1$s</xliff:g></string>
<string name="file_will_be_received_when_contact_is_online">文件将在您的联系人在线时收到,请稍等或稍后再查看!</string>
<string name="from_gallery_button">从图库</string>
<string name="gallery_image_button">照片</string>
<string name="gallery_video_button">视频</string>
<string name="exit_without_saving">退出而不保存</string>
<string name="icon_descr_flip_camera">翻转相机</string>
<string name="enable_automatic_deletion_question">启用自动删除消息?</string>
@@ -453,10 +452,12 @@
<string name="image_descr_link_preview">链接预览图片</string>
<string name="invalid_QR_code">无效的二维码</string>
<string name="onboarding_notifications_mode_subtitle">以后可以通过设置进行更改。</string>
<string name="alert_text_skipped_messages_it_can_happen_when">它可在以下情况发生:
\n1. 消息在发送客户端 2 天后或在服务器上 30 天后过期。
\n2. 消息解密失败,因为您或您的联系人使用了旧的数据库备份
\n3.连接被破坏。</string>
<string name="alert_text_skipped_messages_it_can_happen_when">它可在以下情况发生:
\n1. 如果消息在30天内没有被收到就会在服务器上过期。
\n2. 您用于接收来自此联系人的消息的服务器已更新并重新启动
\n3. 连接受损。
\n请通过设置与开发者联系以接收有关服务器的更新。
\n我们将添加服务器冗余以防止丢失消息。</string>
<string name="leave_group_question">离开群组?</string>
<string name="rcv_group_event_invited_via_your_group_link">通过您的群组链接邀请</string>
<string name="info_row_local_name">本地名称</string>
@@ -501,7 +502,7 @@
\n在启用此功能之前系统将提示您完成身份验证。</string>
<string name="your_chats">您的聊天</string>
<string name="share_file">分享文件……</string>
<string name="share_image">分享媒体……</string>
<string name="share_image">分享图片……</string>
<string name="share_message">分享消息……</string>
<string name="text_field_set_contact_placeholder">设置联系人姓名……</string>
<string name="callstate_received_answer">已收到回复……</string>
@@ -621,7 +622,9 @@
<string name="icon_descr_profile_image_placeholder">资料图片占位符</string>
<string name="smp_servers_per_user">您当前聊天资料的新连接服务器</string>
<string name="your_current_profile">您当前的资料</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">您的资料存储在您的设备上并且仅与您的联系人共享。<xliff:g id="appName">SimpleX</xliff:g> 服务器无法看见您的资料。</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">您的资料存储在您的设备上并且仅与您的联系人共享。
\n
\n<xliff:g id="appName">SimpleX</xliff:g> 服务器无法看见您的资料。</string>
<string name="your_profile_is_stored_on_your_device">您的资料、联系人和发送的消息存储在您的设备上。</string>
<string name="profile_is_only_shared_with_your_contacts">该资料仅与您的联系人共享。</string>
<string name="chat_preferences_on">开启</string>
@@ -855,7 +858,7 @@
<string name="toast_permission_denied">权限被拒绝!</string>
<string name="chat_help_tap_button">点击按钮</string>
<string name="thank_you_for_installing_simplex">感谢您安装 <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="use_camera_button">相机</string>
<string name="use_camera_button">使用相机</string>
<string name="smp_servers_preset_address">预设服务器地址</string>
<string name="smp_servers_scan_qr">扫描服务器二维码</string>
<string name="smp_servers_test_failed">服务器测试失败!</string>
@@ -1019,6 +1022,7 @@
<string name="you_will_still_receive_calls_and_ntfs">当静音配置文件处于活动状态时,您仍会收到来自静音配置文件的电话和通知。</string>
<string name="you_can_hide_or_mute_user_profile">您可以隐藏或静音用户配置文件——长按以显示菜单。</string>
<string name="group_welcome_title">欢迎信息</string>
<string name="settings_send_files_via_xftp">通过 XFTP 发送视频和文件</string>
<string name="confirm_database_upgrades">确认数据库升级</string>
<string name="settings_section_title_experimenta">实验性</string>
<string name="database_upgrade">数据库升级</string>
@@ -1033,6 +1037,7 @@
<string name="database_migrations">迁移:%s</string>
<string name="image_will_be_received_when_contact_completes_uploading">图片将在您的联系人完成上传后收到。</string>
<string name="show_developer_options">显示开发者选项</string>
<string name="xftp_requires_v461">通过 XFTP 接收需要 v4.6.1 以上版本。</string>
<string name="upgrade_and_open_chat">升级并打开聊天</string>
<string name="database_downgrade_warning">警告:您可能会丢失部分数据!</string>
<string name="invalid_migration_confirmation">迁移确认无效</string>
@@ -1053,92 +1058,4 @@
<string name="icon_descr_video_snd_complete">视频已发送</string>
<string name="icon_descr_video_asked_to_receive">要求接收视频</string>
<string name="video_will_be_received_when_contact_completes_uploading">视频将在您的联系人完成上传后收到。</string>
<string name="error_xftp_test_server_auth">服务器需要授权来上传,检查密码</string>
<string name="smp_server_test_upload_file">上传文件</string>
<string name="xftp_servers">XFTP 服务器</string>
<string name="your_XFTP_servers">您的 XFTP 服务器</string>
<string name="disable_onion_hosts_when_not_supported">如果 SOCKS 代理不支持它们,请将 <i>Use .onion hosts</i> 设置为否。</string>
<string name="network_socks_toggle_use_socks_proxy">使用 SOCKS 代理</string>
<string name="port_verb">端口</string>
<string name="smp_server_test_delete_file">删除文件</string>
<string name="smp_server_test_compare_file">对比文件</string>
<string name="host_verb">主机</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">确保 XFTP 服务器地址格式正确、行分隔且不重复。</string>
<string name="smp_server_test_create_file">创建文件</string>
<string name="smp_server_test_download_file">下载文件</string>
<string name="error_loading_smp_servers">加载 SMP 服务器时出错</string>
<string name="error_loading_xftp_servers">加载 XFTP 服务器时出错</string>
<string name="error_saving_xftp_servers">保存 SMP 服务器时出错</string>
<string name="network_proxy_port">端口 %d</string>
<string name="network_socks_proxy_settings">SOCKS 代理设置</string>
<string name="la_lock_mode">SimpleX 锁定模式</string>
<string name="la_lock_mode_system">系统验证</string>
<string name="la_minutes">%d 分钟</string>
<string name="la_seconds">%d 秒</string>
<string name="la_enter_app_passcode">输入密码</string>
<string name="la_no_app_password">没有应用程序密码</string>
<string name="la_lock_mode_passcode">密码输入</string>
<string name="la_please_remember_to_store_password">请牢记或妥善保管——丢失的密码将无法恢复!</string>
<string name="you_can_turn_on_lock">您可以通过设置开启 SimpleX 锁定。</string>
<string name="la_authenticate">身份验证</string>
<string name="la_auth_failed">身份验证失败</string>
<string name="la_change_app_passcode">更改密码</string>
<string name="submit_passcode">提交</string>
<string name="confirm_passcode">确认密码</string>
<string name="enable_lock">启用锁定</string>
<string name="incorrect_passcode">密码错误</string>
<string name="lock_after">在此后锁定</string>
<string name="lock_mode">锁定模式</string>
<string name="new_passcode">新密码</string>
<string name="change_lock_mode">更改锁定模式</string>
<string name="la_mode_passcode">密码</string>
<string name="passcode_changed">密码已更改!</string>
<string name="passcode_not_changed">密码未更改!</string>
<string name="passcode_set">密码已设置!</string>
<string name="la_mode_system">系统</string>
<string name="lock_not_enabled">未启用 SimpleX 锁定!</string>
<string name="la_could_not_be_verified">您的身份无法验证,请再试一次。</string>
<string name="authentication_cancelled">身份验证已取消</string>
<string name="la_current_app_passcode">当前密码</string>
<string name="la_immediately">立即</string>
<string name="alert_title_msg_bad_hash">错误消息散列</string>
<string name="alert_title_msg_bad_id">错误消息 ID</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> 消息解密失败。</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> 已跳过消息。</string>
<string name="alert_text_fragment_permanent_error_reconnect">此错误对于此连接是永久性的,请重新连接。</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">当您或您的连接使用旧数据库备份时,可能会发生这种情况。</string>
<string name="decryption_error">解密错误</string>
<string name="decryption_error_permanent">永久解密错误</string>
<string name="alert_text_fragment_please_report_to_developers">请向开发者报告。</string>
<string name="alert_text_msg_bad_hash">上一条消息的散列不同。</string>
<string name="alert_text_msg_bad_id">下一条消息的 ID 不正确(小于或等于上一条)。
\n它可能是由于某些错误或连接被破坏才发生。</string>
<string name="alert_text_decryption_error_earlier"><xliff:g id="message count" example="1">%1$d</xliff:g> 消息解密失败并且不会显示。</string>
<string name="no_spaces">没有空格!</string>
<string name="stop_file__action">停止文件</string>
<string name="stop_snd_file__title">停止发送文件?</string>
<string name="stop_snd_file__message">即将停止发送文件。</string>
<string name="stop_rcv_file__title">停止接收文件?</string>
<string name="stop_rcv_file__message">即将停止接收文件。</string>
<string name="stop_file__confirm">Stop</string>
<string name="revoke_file__action">撤销文件</string>
<string name="revoke_file__title">撤销文件?</string>
<string name="revoke_file__message">文件将从服务器中删除。</string>
<string name="revoke_file__confirm">撤销</string>
<string name="audio_video_calls">音频/视频通话</string>
<string name="available_in_v51">"
\n在 v5.1 中可用"</string>
<string name="v5_0_app_passcode">应用程序密码</string>
<string name="v5_0_polish_interface">波兰语界面</string>
<string name="v5_0_polish_interface_descr">感谢用户——通过 Weblate 做出贡献!</string>
<string name="v5_0_app_passcode_descr">设置它以代替系统身份验证。</string>
<string name="v5_0_large_files_support">最大 1gb 的视频和文件</string>
<string name="v5_0_large_files_support_descr">快速且无需等待发件人在线!</string>
<string name="prohibit_calls">禁止音频/视频通话。</string>
<string name="both_you_and_your_contact_can_make_calls">您和您的联系人都可以拨打电话。</string>
<string name="only_you_can_make_calls">只有您可以拨打电话。</string>
<string name="only_your_contact_can_make_calls">只有您的联系人可以拨打电话。</string>
<string name="allow_your_contacts_to_call">允许您的联系人给您打电话。</string>
<string name="allow_calls_only_if">仅当您的联系人允许时才允许呼叫。</string>
<string name="calls_prohibited_with_this_contact">禁止音频/视频通话。</string>
</resources>

View File

@@ -11,7 +11,7 @@
<string name="accept_contact_button">接受</string>
<string name="about_simplex_chat">關於 <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="accept_connection_request__question">接受連線請求?</string>
<string name="callstatus_accepted">已接受</string>
<string name="callstatus_accepted">已接受收聽電</string>
<string name="network_enable_socks_info">要在端口 9050 啟動 SOCKS 代理伺服器嗎?在啟用這個選項之前,必須先啟動代理伺服器。</string>
<string name="group_member_role_admin">管理員</string>
<string name="above_then_preposition_continuation">然後選按:</string>
@@ -23,7 +23,7 @@
<string name="display_name__field">顯示名稱:</string>
<string name="full_name__field">全名:</string>
<string name="onboarding_notifications_mode_service_desc"><b>使用更多電量</b>!通知服務會長期在背景中運行 一但有訊息就會顯示在通知內。</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>對電量也不錯</b>。通知服務會每十分鐘運行一次。你可能會錯過通話或訊息。</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>對電量也不錯</b>。通知服務會每十分鐘運行一次。你可能會錯過電話通話或訊息。</string>
<string name="answer_call">回應通話請求</string>
<string name="clear_contacts_selection_button">清除</string>
<string name="allow_direct_messages">允許在群組內選擇成員後傳送訊息。</string>
@@ -36,7 +36,7 @@
<string name="database_initialization_error_title">無法初始化數據庫</string>
<string name="notifications_mode_service">一直開啟</string>
<string name="auth_disable_simplex_lock">關閉 SimpleX 鎖定</string>
<string name="auth_enable_simplex_lock"> SimpleX 鎖定</string>
<string name="auth_enable_simplex_lock"> SimpleX 鎖定</string>
<string name="copy_verb">複製</string>
<string name="reply_verb">回覆</string>
<string name="share_verb">分享</string>
@@ -52,7 +52,7 @@
<string name="add_new_contact_to_create_one_time_QR_code"><b>新增新的聯絡人</b>:新增新的聯絡人可以使用二維碼建立你的一次性二維碼。</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>掃描二維碼</b>:連接到向你出示二維碼的聯絡人。</string>
<string name="choose_file">選擇檔案</string>
<string name="use_camera_button">相機</string>
<string name="use_camera_button">使用相機</string>
<string name="from_gallery_button">從圖片庫選擇圖片</string>
<string name="accept_contact_incognito_button">接受匿名聊天模式</string>
<string name="clear_chat_warning">所有訊息記錄會刪除 - 這不能還原!這些訊息只會在你裝置中刪除。</string>
@@ -76,13 +76,13 @@
<string name="save_preferences_question">儲存設定?</string>
<string name="display_name">顯示名稱</string>
<string name="full_name_optional__prompt">全名(可選)</string>
<string name="callstatus_error">通話出</string>
<string name="callstatus_error">通話出現錯誤</string>
<string name="callstatus_calling">正在撥打 …</string>
<string name="callstatus_in_progress">通話中</string>
<string name="secret">私密</string>
<string name="callstate_connected">已連接</string>
<string name="onboarding_notifications_mode_off_desc"><b>對電量最好</b>。 只有在應用程式運行中的時侯才可以接收訊息通知,後台服務並不會使用。</string>
<string name="encrypted_video_call">已經完成端對端加密視訊通話</string>
<string name="onboarding_notifications_mode_off_desc"><b>對電量最好</b>。 只有在應用程式運行中的時侯才可以接收訊息,後台服務並不會使用。</string>
<string name="encrypted_video_call">端對端加密視訊通話</string>
<string name="video_call_no_encryption">視訊通話(沒有端對端加密)</string>
<string name="call_already_ended">通話已經結束了!</string>
<string name="icon_descr_audio_off">關閉語音</string>
@@ -100,8 +100,8 @@
<string name="auto_accept_images">自動接收圖片</string>
<string name="full_backup">備份應用程式資料</string>
<string name="settings_section_title_icon">應用程式圖示</string>
<string name="chat_database_imported">已匯入對話數據</string>
<string name="keychain_is_storing_securely">Android 金鑰庫是用於安全地儲存密碼 - 確保通知服務的運作</string>
<string name="chat_database_imported">已匯入對話資料</string>
<string name="keychain_is_storing_securely">Android 金鑰庫是用於安全地儲存密碼 - 確保通知服務的運作</string>
<string name="impossible_to_recover_passphrase"><b>請注意</b>:如果你忘記了密碼你將不能再次復原或更改密碼。</string>
<string name="keychain_allows_to_receive_ntfs">當你重新啟動應用程式或更改密碼後, Android 金鑰庫將會用來安全地儲存密碼 - 將會允許接到通知。</string>
<string name="chat_is_stopped_indication">聊天室已停止運作</string>
@@ -122,7 +122,7 @@
<string name="v4_3_improved_server_configuration_desc">使用二維碼掃描並新增伺服器。</string>
<string name="chat_is_running">聊天室運行中</string>
<string name="chat_database_section">聊天室數據庫</string>
<string name="chat_is_stopped">聊天室已停止運作</string>
<string name="chat_is_stopped">聊天室已停止</string>
<string name="stop_chat_confirmation">停止</string>
<string name="chat_database_deleted">已刪除數據庫的對話內容</string>
<string name="stop_chat_to_enable_database_actions">停止聊天室以啟用數據庫功能。</string>
@@ -147,7 +147,7 @@
<string name="ttl_month">%d 月</string>
<string name="callstatus_ended">通話結束 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="icon_descr_cancel_file_preview">取消檔案預覽</string>
<string name="cannot_receive_file">無法接收檔案</string>
<string name="cannot_receive_file">無法接收文件</string>
<string name="failed_to_create_user_duplicate_title">重複的顯示名稱!</string>
<string name="network_session_mode_entity_description">一個單獨的 TCP 連接(和 SOCKS 憑證)將用於<b>每個聯絡人和群組內的成員</b>
\n<b>請注意</b>:如果你有很多連接,你的電話電量和數據流量的消耗率會大大增加,一些連接有機會會連接失敗。</string>
@@ -159,7 +159,7 @@
<string name="group_preferences">群組設定</string>
<string name="contact_preferences">聯絡人設定</string>
<string name="share_image">分享圖片 …</string>
<string name="both_you_and_your_contacts_can_delete">你和你的聯絡人都可以不可逆地刪除已經傳送的訊息</string>
<string name="both_you_and_your_contacts_can_delete">你和你的聯絡人都可以不可逆地刪除已經傳送的訊息</string>
<string name="server_connected">已連接</string>
<string name="simplex_link_mode_description">簡介</string>
<string name="simplex_link_mode_full">完整連結</string>
@@ -185,8 +185,8 @@
<string name="smp_servers_check_address">檢查輸入的伺服器地址然後再試一次。</string>
<string name="chat_console">終端機對話</string>
<string name="star_on_github">於 Github 給個星星</string>
<string name="incognito_info_protects">匿名聊天模式會保護你的真實個人檔案名稱和頭像 — 當有新聯絡人的時候會自動建立一個隨機性的個人檔案</string>
<string name="incognito_info_allows">這樣是允許每一個對話中也擁有不同的顯示名稱並且沒有任何的個人資料可用於分享或有機會外洩</string>
<string name="incognito_info_protects">匿名聊天模式會保護你的真實個人檔案名稱和頭像 — 當有新聯絡人的時候會自動建立一個隨機性的個人檔案</string>
<string name="incognito_info_allows">這樣就會每一個對話中也擁有不同的顯示名稱並且沒有任何的個人資料可用於分享或有機會外洩</string>
<string name="incognito_info_find">若要查找用於匿名聊天模式連接的個人檔案,請點擊聯絡人或群組名稱。</string>
<string name="allow_disappearing_messages_only_if">只有你的聯絡人允許的情況下,才允許自動銷毀訊息。</string>
<string name="allow_your_contacts_to_send_disappearing_messages">允許你的聯絡人傳送自動銷毀的訊息。</string>
@@ -195,7 +195,7 @@
<string name="allow_to_delete_messages">允許將不可撤銷的訊息刪除。</string>
<string name="allow_to_send_voice">允許傳送語音訊息。</string>
<string name="delete_after">多久後刪除</string>
<string name="all_group_members_will_remain_connected">群組內所有成員會保持連接</string>
<string name="all_group_members_will_remain_connected">群組內所有成員會保持連接</string>
<string name="color_primary">自訂顏色</string>
<string name="moderated_description">即時顯示訊息</string>
<string name="simplex_link_group">SimpleX 群組連結</string>
@@ -205,10 +205,10 @@
<string name="failed_to_create_user_duplicate_desc">你已經有一個個人檔案的顯示名稱和現在選擇建立的個人檔案名稱相同。請選擇其他名稱。</string>
<string name="failed_to_active_user_title">個人檔案切換失敗!</string>
<string name="error_joining_group">加入群組時出錯</string>
<string name="sender_cancelled_file_transfer">傳送者已取消傳檔案。</string>
<string name="sender_cancelled_file_transfer">傳送者已取消傳檔案。</string>
<string name="error_receiving_file">接收檔案時出錯</string>
<string name="error_creating_address">建立地址時出錯</string>
<string name="v4_5_private_filenames_descr">為了保護私人檔案,圖片或語音檔案會使用 UTC。</string>
<string name="v4_5_private_filenames_descr">為了保護私人檔案,圖片或語音文件使用 UTC。</string>
<string name="sending_files_not_yet_supported">目前還不支援傳送檔案</string>
<string name="receiving_files_not_yet_supported">目前還不支援接收檔案</string>
<string name="sender_you_pronoun"></string>
@@ -268,7 +268,7 @@
<string name="auth_device_authentication_is_disabled_turning_off">裝置內的螢幕鎖定已關閉。正在關閉 SimpleX 鎖定。</string>
<string name="save_verb">儲存</string>
<string name="images_limit_title">太多圖片!</string>
<string name="error_saving_file">儲存檔案的時候出</string>
<string name="error_saving_file">儲存檔案的時候出現錯誤</string>
<string name="notification_new_contact_request">有新的聯絡人連線請求</string>
<string name="auth_confirm_credential">確認你的憑據</string>
<string name="hide_verb">隱藏</string>
@@ -281,11 +281,11 @@
<string name="error_smp_test_failed_at_step">測試在步驟 %s 失敗。</string>
<string name="error_smp_test_server_auth">伺服器需要授權才能建立佇列,請檢查密碼</string>
<string name="error_smp_test_certificate">伺服器地址的憑證指紋可能不正確</string>
<string name="icon_descr_instant_notifications">即時通知</string>
<string name="icon_descr_instant_notifications">即時收到通知</string>
<string name="service_notifications">即時通知!</string>
<string name="service_notifications_disabled">已禁用即時通知功能</string>
<string name="service_notifications_disabled">已禁用即時收到通知!</string>
<string name="database_initialization_error_desc">數據庫目前沒有正常運作。點擊查看更多</string>
<string name="ntf_channel_calls">SimpleX Chat 話來電</string>
<string name="ntf_channel_calls">SimpleX Chat 話來電</string>
<string name="settings_notifications_mode_title">通知服務</string>
<string name="settings_notification_preview_mode_title">顯示預覽</string>
<string name="settings_notification_preview_title">通知預覽</string>
@@ -293,7 +293,7 @@
<string name="auth_unlock">已解鎖</string>
<string name="auth_log_in_using_credential">使用你的憑據登入</string>
<string name="auth_open_chat_console">使用終端機開啟對話</string>
<string name="message_delivery_error_title">傳送訊息時出錯</string>
<string name="message_delivery_error_title">傳送訊息有錯誤</string>
<string name="message_delivery_error_desc">大概你的聯絡人已經刪除了和你的對話並且已經沒有和你有連線。</string>
<string name="for_me_only">只為我刪除</string>
<string name="for_everybody">為所有人刪除</string>
@@ -329,7 +329,7 @@
<string name="smp_server_test_secure_queue">安全佇列</string>
<string name="smp_server_test_delete_queue">刪除佇列</string>
<string name="smp_server_test_disconnect">斷開連接</string>
<string name="turn_off_battery_optimization">為了使用它,請 <b>禁用電量優化</b> 為了 <xliff:g id="appName">SimpleX</xliff:g> 在下一個對話中。否則,將會禁用通知功能</string>
<string name="turn_off_battery_optimization">為了使用它,請 <b>禁用電量優化</b> 為了 <xliff:g id="appName">SimpleX</xliff:g> 在下一個對話中。否則,將會禁用通知。</string>
<string name="enter_passphrase_notification_desc">在接收通知之前,請你輸入數據庫的密碼</string>
<string name="periodic_notifications_desc">應用程式會定期推送新訊息 — 它每天會消耗百分之幾的電量。 應用程式將不使用推送通知 — 你裝置中的數據不會傳送至伺服器。</string>
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX Chat</xliff:g> 服務</string>
@@ -352,7 +352,7 @@
<string name="edit_verb">修改</string>
<string name="delete_verb">刪除</string>
<string name="reveal_verb">展露</string>
<string name="delete_message__question">刪除訊息?</string>
<string name="delete_message__question">確定要刪除訊息?</string>
<string name="delete_message_cannot_be_undone_warning">訊息會刪除 - 並且不能還原!</string>
<string name="delete_message_mark_deleted_warning">訊息將被標記為刪除。 接收訊息的人(多個) 能夠展露此訊息。</string>
<string name="icon_descr_received_msg_status_unread">未讀</string>
@@ -394,7 +394,7 @@
<string name="view_security_code">查看安全碼</string>
<string name="verify_security_code">驗證安全碼</string>
<string name="icon_descr_send_message">傳送訊息</string>
<string name="error_deleting_user">刪除個人檔案時出</string>
<string name="error_deleting_user">刪除個人檔案時出現錯誤</string>
<string name="auth_stop_chat">停止對話</string>
<string name="image_decoding_exception_desc">圖片不能解碼,嘗試其他圖片或聯絡開發人員。</string>
<string name="icon_descr_file">檔案</string>
@@ -425,6 +425,7 @@
<string name="delete_address">刪除地址</string>
<string name="colored">顏色</string>
<string name="how_to_use_your_servers">如何使用你的伺服器</string>
<string name="core_build_timestamp">核心建立於:%s</string>
<string name="to_connect_via_link_title">使用連結連接</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 桌面版:於應用程式內掃描一個已存在的二維碼,透過二維碼掃描。</string>
<string name="icon_descr_settings">設定</string>
@@ -438,8 +439,8 @@
<string name="section_title_welcome_message">歡迎訊息</string>
<string name="edit_image">編輯圖片</string>
<string name="italic">斜體</string>
<string name="callstatus_rejected">已拒絕</string>
<string name="callstatus_connecting">連接話中 …</string>
<string name="callstatus_rejected">已拒絕收聽電</string>
<string name="callstatus_connecting">連接話中 …</string>
<string name="callstate_waiting_for_confirmation">等待對方確定 …</string>
<string name="contribute">貢獻</string>
<string name="rate_the_app">評價程式</string>
@@ -481,7 +482,7 @@
<string name="your_chat_profile_will_be_sent_to_your_contact">你的個人檔案會傳送給
\n你的聯絡人</string>
<string name="paste_connection_link_below_to_connect">將你收到的連結貼上至下面的框內以開展你與你的聯絡人對話。</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">如果你不能面對面接觸此聯絡人,你可以於 <b>視訊話中掃描二維碼</b>,或者你可以分享一個邀請連結給此聯絡人。</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">如果你不能面對面接觸此聯絡人,你可以於 <b>視訊話中掃描二維碼</b>,或者你可以分享一個邀請連結給此聯絡人。</string>
<string name="your_profile_will_be_sent">你的個人檔案會傳送給你的聯絡人。</string>
<string name="paste_button">貼上</string>
<string name="this_string_is_not_a_connection_link">這些字串不是連接連結!</string>
@@ -505,14 +506,16 @@
<string name="how_to">如何?</string>
<string name="configure_ICE_servers">配置 ICE 伺服器</string>
<string name="saved_ICE_servers_will_be_removed">已儲存的 WebRTC ICE 伺服器將會移除。</string>
<string name="error_saving_ICE_servers">儲存 ICE 伺服器時出錯</string>
<string name="error_saving_ICE_servers">儲存 ICE 伺服器時有錯誤</string>
<string name="network_use_onion_hosts_no">不要</string>
<string name="network_use_onion_hosts_required">需要</string>
<string name="network_use_onion_hosts_prefer_desc">Onion 主機會當有的時侯啟用</string>
<string name="network_use_onion_hosts_required_desc_in_alert">連接時將需要 Onion 主機</string>
<string name="delete_address__question">刪除地址?</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">你可以使用連結或二維碼分享你的地址 - 任何人也可以和你連線。如果你不主動刪除你的聯絡人,你並不會遺失你的聯絡人</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 <xliff:g id="appName">SimpleX</xliff:g> 伺服器並不會看到你的個人檔案。</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。
\n
\n<xliff:g id="appName">SimpleX</xliff:g> 伺服器並不會看到你的個人檔案。</string>
<string name="save_and_notify_contact">儲存並通知你的聯絡人</string>
<string name="save_and_notify_contacts">儲存並通知你的多個聯絡人</string>
<string name="your_profile_is_stored_on_your_device">你的個人檔案,聯絡人和傳送的訊息只會儲存於你的個人裝置內。</string>
@@ -532,10 +535,10 @@
<string name="network_socks_toggle">使用 SOCKS 代理伺服器 (端口 9050)</string>
<string name="network_enable_socks">使用 SOCKS 代理伺服器</string>
<string name="save_and_notify_group_members">儲存並通知群組內的聯絡人</string>
<string name="exit_without_saving">退出並且不儲存</string>
<string name="exit_without_saving">退出並且不儲存</string>
<string name="you_control_your_chat">你的對話由你控制!</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">一個保護你的隱私和傳送安全通訊的應用程式平台。</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">我們不會在伺服器內儲存你的任何聯絡人和息(一旦傳送)。</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">我們不會在伺服器內儲存你的任何聯絡人和息(一旦傳送)。</string>
<string name="create_profile">建立個人檔案</string>
<string name="callstate_received_confirmation">回應已確認</string>
<string name="you_can_connect_to_simplex_chat_founder">你可以透過 <font color="#0088ff">連接到 <xliff:g id="appNameFull">SimpleX Chat</xliff:g> 開發人員提出任何問題並同意更新</font></string>
@@ -555,7 +558,7 @@
<string name="is_not_verified">%s 並未驗證</string>
<string name="your_simplex_contact_address">你的 <xliff:g id="appName">SimpleX</xliff:g> 聯絡地址</string>
<string name="your_chat_profiles">你的個人檔案</string>
<string name="database_passphrase_and_export">數據庫密碼及匯出</string>
<string name="database_passphrase_and_export">資料庫密碼及匯出</string>
<string name="send_us_an_email">傳送電郵</string>
<string name="chat_lock">SimpleX 鎖定</string>
<string name="smp_servers">SMP 服務</string>
@@ -589,7 +592,7 @@
<string name="files_and_media_section">檔案及媒體檔案</string>
<string name="restart_the_app_to_create_a_new_chat_profile">重新啟動應用程式以建立新的個人檔案。</string>
<string name="delete_files_and_media_question">刪除所有檔案及媒體檔案?</string>
<string name="delete_files_and_media_for_all_users">刪除所有你的個人對話檔案</string>
<string name="delete_files_and_media_for_all_users">刪除所有你的個人檔案及媒體檔案</string>
<string name="total_files_count_and_size">%d 檔案(s) 的總共大小為 %s</string>
<string name="messages_section_title">訊息</string>
<string name="chat_item_ttl_none">永不</string>
@@ -597,7 +600,7 @@
<string name="current_passphrase">目前的密碼 …</string>
<string name="encrypt_database">加密</string>
<string name="error_changing_message_deletion">修改設定時出錯</string>
<string name="notifications_will_be_hidden">通知只會在應用程式關閉前才會傳送</string>
<string name="notifications_will_be_hidden">通知服務只會在應用程式關閉前才會傳送</string>
<string name="remove_passphrase">移除</string>
<string name="remove_passphrase_from_keychain">要從金鑰庫移除密碼?</string>
<string name="confirm_new_passphrase">確定新密碼 …</string>
@@ -619,7 +622,7 @@
<string name="button_edit_group_profile">修改群組內的設定</string>
<string name="create_group_link">建立群組連結</string>
<string name="button_create_group_link">建立連結</string>
<string name="delete_link_question">刪除連結?</string>
<string name="delete_link_question">確定要刪除連結?</string>
<string name="button_remove_member">移除成員</string>
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 \"%s\". 所有在群組內的成員都會收到通知</string>
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 \"%s\". 該成員將收到新的邀請</string>
@@ -638,10 +641,10 @@
<string name="v4_5_message_draft">訊息草稿</string>
<string name="v4_5_message_draft_descr">保留最後一則帶附件的訊息草稿。</string>
<string name="v4_5_transport_isolation">傳輸隔離</string>
<string name="import_database_question">匯入對話數據庫?</string>
<string name="import_database_question">匯入對話資料庫?</string>
<string name="store_passphrase_securely_without_recover">請放置你的密碼於安全的地方,如果你遺失了密碼將不可能再次存取它。</string>
<string name="keychain_error">金鑰庫錯誤</string>
<string name="no_contacts_selected">沒有聯絡人可選擇</string>
<string name="keychain_error">鎖匙鏈錯誤</string>
<string name="no_contacts_selected">沒有聯絡人可選擇</string>
<string name="group_link">群組連結</string>
<string name="delete_link">刪除連結</string>
<string name="group_is_decentralized">群組是完全去中心化的 - 只有群組內的成員能看到。</string>
@@ -649,7 +652,7 @@
<string name="ttl_hours">%d 個小時</string>
<string name="whats_new">新功能</string>
<string name="v4_2_auto_accept_contact_requests_desc">帶有可選即時性的訊息</string>
<string name="error_deleting_database">刪除數據庫時出</string>
<string name="error_deleting_database">刪除數據庫時出現錯誤</string>
<string name="import_database_confirmation">匯入</string>
<string name="chat_item_ttl_seconds">%s 秒(s)</string>
<string name="error_encrypting_database">加密數據庫時出錯</string>
@@ -679,13 +682,13 @@
<string name="ignore">無視</string>
<string name="incoming_audio_call">語音通話來電</string>
<string name="paste_the_link_you_received">貼上你收到的連結</string>
<string name="status_e2e_encrypted">已經完成端對端加密</string>
<string name="status_e2e_encrypted">已經端對端加密</string>
<string name="status_no_e2e_encryption">沒有端對端加密</string>
<string name="icon_descr_speaker_off">關閉喇叭</string>
<string name="settings_section_title_messages">訊息</string>
<string name="privacy_and_security">私隱 &amp; 安全性</string>
<string name="settings_section_title_themes">主題</string>
<string name="v4_4_french_interface">界面</string>
<string name="v4_4_french_interface">國語言界面</string>
<string name="encrypted_audio_call">已經完成端對端加密的語音通話</string>
<string name="reject">拒絕</string>
<string name="integrity_msg_bad_id">錯誤的訊息 ID</string>
@@ -710,7 +713,7 @@
<string name="database_will_be_encrypted">數據庫將會加密。</string>
<string name="database_will_be_encrypted_and_passphrase_stored">數據庫將會加密並且密碼會儲存於金鑰庫。</string>
<string name="database_error">數據庫錯誤</string>
<string name="cannot_access_keychain">不能讀取金鑰庫以儲存數據庫密碼</string>
<string name="cannot_access_keychain">不能讀取金鑰庫以儲存資料庫密碼</string>
<string name="error_with_info">錯誤:%s</string>
<string name="file_with_path">檔案:%s</string>
<string name="database_passphrase_is_required">需要數據庫的密碼以開啟對話。</string>
@@ -724,7 +727,7 @@
<string name="restore_database_alert_title">還原數據庫的備份?</string>
<string name="database_restore_error">還原數據庫時出錯</string>
<string name="save_archive">儲存存檔</string>
<string name="delete_archive">刪除存</string>
<string name="delete_archive">刪除</string>
<string name="join_group_button">加入</string>
<string name="join_group_question">確定要加入群組?</string>
<string name="join_group_incognito_button">加入匿名聊天模式</string>
@@ -763,19 +766,19 @@
<string name="update_network_settings_question">更新網路設定?</string>
<string name="update_network_settings_confirmation">更新</string>
<string name="updating_settings_will_reconnect_client_to_all_servers">更新設定會將客戶端重新連接到所有的伺服器。</string>
<string name="users_delete_question">刪除對話資料</string>
<string name="users_delete_profile_for">刪除對話資訊給</string>
<string name="users_delete_question">刪除個人檔案</string>
<string name="users_delete_profile_for">刪除個人檔案</string>
<string name="users_delete_with_connections">檔案和伺服器連接</string>
<string name="users_delete_data_only">只有本機檔案</string>
<string name="incognito_random_profile">你的隨機個人檔案</string>
<string name="incognito_random_profile_description">隨機的個人檔案將會傳送給你的聯絡人</string>
<string name="incognito_random_profile_description">隨機的個人檔案將會傳送給你的聯絡人</string>
<string name="theme_system">系統</string>
<string name="theme_light">明亮</string>
<string name="reset_color">重設顏色</string>
<string name="feature_off">關閉</string>
<string name="feature_received_prohibited">已接收,已禁止</string>
<string name="accept_feature_set_1_day">設定為一日</string>
<string name="contacts_can_mark_messages_for_deletion">聯絡人可以標記訊息為已刪除;你可以看到那些訊息。</string>
<string name="contacts_can_mark_messages_for_deletion">聯絡人可以標記訊息為已刪除;你可以看到</string>
<string name="prohibit_sending_voice_messages">禁止傳送語音訊息</string>
<string name="only_your_contact_can_send_disappearing">只有你的聯絡人可以傳送自動銷毀的訊息</string>
<string name="disappearing_prohibited_in_this_chat">自動銷毀訊息已被禁止於此聊天室。</string>
@@ -789,17 +792,17 @@
<string name="run_chat_section">運行對話</string>
<string name="database_passphrase">數據庫密碼</string>
<string name="export_database">匯出數據庫</string>
<string name="import_database">匯入數據</string>
<string name="import_database">匯入資料</string>
<string name="new_database_archive">新的數據庫存檔</string>
<string name="old_database_archive">舊的數據庫存檔</string>
<string name="delete_database">刪除數據庫</string>
<string name="error_starting_chat">開始新對話時出錯</string>
<string name="stop_chat_question">停止對話?</string>
<string name="set_password_to_export">設定密碼以匯出</string>
<string name="error_stopping_chat">停止對話時出</string>
<string name="error_stopping_chat">停止對話時出現錯誤</string>
<string name="set_password_to_export_desc">已受加密的數據庫是使用一個隨機性的文字。請在修改前將它匯出。</string>
<string name="error_exporting_chat_database">匯出數據庫時出</string>
<string name="error_importing_database">匯入數據庫時出</string>
<string name="error_exporting_chat_database">匯出數據庫時出現錯誤</string>
<string name="error_importing_database">匯入數據庫時出現錯誤</string>
<string name="database_passphrase_will_be_updated">受加密的數據庫密碼會再次更新。</string>
<string name="delete_chat_archive_question">刪除封存對話?</string>
<string name="encrypt_database_question">加密數據庫?</string>
@@ -824,15 +827,15 @@
<string name="status_contact_has_no_e2e_encryption">對話沒有經過端對端加密</string>
<string name="database_encrypted">數據庫已加密!</string>
<string name="encrypted_database">已加密數據庫</string>
<string name="chat_archive_section">封存對話</string>
<string name="chat_archive_section">對話封存</string>
<string name="snd_group_event_group_profile_updated">群組資料已經更新</string>
<string name="group_member_role_member">成員</string>
<string name="group_info_member_you">你:<xliff:g id="group_info_you">%1$s</xliff:g></string>
<string name="button_delete_group">刪除群組</string>
<string name="v4_4_live_messages">即時訊息</string>
<string name="chat_archive_header">封存對話</string>
<string name="error_removing_member">移除成員時出</string>
<string name="error_changing_role">修改身份時出</string>
<string name="chat_archive_header">對話封存</string>
<string name="error_removing_member">移除成員時出現錯誤</string>
<string name="error_changing_role">修改身份時出現錯誤</string>
<string name="info_row_group">群組</string>
<string name="info_row_connection">連線</string>
<string name="conn_level_desc_direct">直接</string>
@@ -847,7 +850,7 @@
<string name="group_members_can_send_disappearing">群組內的成員可以傳送自動銷毀的訊息。</string>
<string name="disappearing_messages_are_prohibited">自動銷毀訊息於這個群組內是禁用的。</string>
<string name="feature_offered_item">提供 %s</string>
<string name="error_saving_group_profile">儲存群組檔案時出錯</string>
<string name="error_saving_group_profile">儲存群組檔案時有錯誤</string>
<string name="network_options_revert">恢復</string>
<string name="theme">主題</string>
<string name="save_color">儲存顏色</string>
@@ -911,7 +914,7 @@
<string name="to_protect_privacy_simplex_has_ids_for_queues">為了保護隱私,而不像是其他平台般需要提取和存儲用戶的 ID資料<xliff:g id="appName">SimpleX</xliff:g> 本平台具有SimpleX自家隊列的標識符對於你的每個聯絡人也是獨一無二的。</string>
<string name="onboarding_notifications_mode_off">當應用程式是開啟</string>
<string name="you_control_servers_to_receive_your_contacts_to_send">你可以控制通過哪一個伺服器 <b>來接收</b> 你的聯絡人訊息 這些伺服器用來接收他們傳送給你的訊息。</string>
<string name="allow_accepting_calls_from_lock_screen">透過設定啟用於上鎖畫面顯示來電通知</string>
<string name="allow_accepting_calls_from_lock_screen">透過設定啟用於上鎖畫面顯示來電通知</string>
<string name="delete_chat_profile_action_cannot_be_undone_warning">這操作不能還原 - 你現有的個人檔案,聯絡人,訊息和檔案將會不可逆地的失去。</string>
<string name="you_must_use_the_most_recent_version_of_database">你必須在裝置上使用最新版本的對話數據庫,否則你可能會停止接收某些聯絡人的訊息。</string>
<string name="delete_files_and_media_desc">這操作不能還原 - 所有已經接收和傳送的檔案和媒體檔案將會刪除。低解析度圖片將保留。</string>
@@ -945,15 +948,15 @@
<string name="settings_section_title_you"></string>
<string name="settings_experimental_features">實驗性功能</string>
<string name="database_is_not_encrypted">你對話的數據庫並未加受加密 - 設置密碼保護它。</string>
<string name="passphrase_is_different">數據庫密碼與存在金鑰庫中的密碼不同。</string>
<string name="passphrase_is_different">資料庫密碼與存在金鑰庫中的密碼不同。</string>
<string name="unknown_error">不明的錯誤</string>
<string name="restore_database_alert_desc">還原數據庫備份後請輸入舊密碼。這個操作是不能撤銷的!</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">你可以透過應用程式的設定或透過數據庫去重新啟動應用程式來開始新的對話。</string>
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">你可以透過應用程式的設置或重新啟動應用程式來開始新的對話。</string>
<string name="you_are_invited_to_group_join_to_connect_with_group_members">你已經被邀请加入至群組。加入後可與群組內的成員對話。</string>
<string name="you_joined_this_group">你已加入至群組</string>
<string name="icon_descr_contact_checked">已確認聯絡人</string>
<string name="your_chat_database">你的對話數據庫</string>
<string name="delete_chat_profile_question">刪除對話資料</string>
<string name="delete_chat_profile_question">刪除個人檔案</string>
<string name="wrong_passphrase">錯誤的數據庫密碼</string>
<string name="unknown_database_error_with_info">未知的數據庫錯誤:%s</string>
<string name="wrong_passphrase_title">密碼錯誤!</string>
@@ -968,93 +971,60 @@
<string name="snd_group_event_changed_role_for_yourself">你修改了自己的身份為 %s</string>
<string name="snd_conn_event_switch_queue_phase_completed">你修改了地址</string>
<string name="moderated_item_description">由 %s 管理</string>
<string name="moderate_message_will_be_deleted_warning">將為所有成員刪除該息。</string>
<string name="delete_member_message__question">刪除成員息?</string>
<string name="moderate_message_will_be_deleted_warning">將為所有成員刪除該息。</string>
<string name="delete_member_message__question">刪除成員息?</string>
<string name="moderate_verb">主持</string>
<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="moderate_message_will_be_marked_warning">息將對所有成員標記為已審核。</string>
<string name="observer_cant_send_message_title">不能發送消息!</string>
<string name="you_are_observer">是觀察者</string>
<string name="group_member_role_observer">觀察者</string>
<string name="error_updating_link_for_group">更新群組接時出錯</string>
<string name="error_updating_link_for_group">更新群組接時出錯</string>
<string name="observer_cant_send_message_desc">請聯繫群管理員。</string>
<string name="initial_member_role">初始角色</string>
<string name="language_system">系統</string>
<string name="you_can_hide_or_mute_user_profile">可以隱藏或靜音用戶檔案 - 按住它以顯示菜單。</string>
<string name="smp_save_servers_question">儲存伺服器?</string>
<string name="you_can_hide_or_mute_user_profile">可以隱藏或靜音用戶配置文件 - 按住它以顯示菜單。</string>
<string name="smp_save_servers_question">保存服務器?</string>
<string name="confirm_password">確認密碼</string>
<string name="hidden_profile_password">隱藏的個人資料密碼</string>
<string name="hide_profile">隱藏個人資料</string>
<string name="password_to_show">顯示密碼</string>
<string name="save_profile_password">存個人資料密碼</string>
<string name="to_reveal_profile_enter_password">要顯示的隱藏個人資料,請在的聊天個人資料頁面的搜索字段中輸入完整密碼。</string>
<string name="button_welcome_message">歡迎</string>
<string name="save_and_update_group_profile">存和更新組配置檔案</string>
<string name="save_welcome_message_question">存歡迎息?</string>
<string name="save_profile_password">存個人資料密碼</string>
<string name="to_reveal_profile_enter_password">要顯示的隱藏個人資料,請在的聊天個人資料頁面的搜索字段中輸入完整密碼。</string>
<string name="button_welcome_message">歡迎</string>
<string name="save_and_update_group_profile">存和更新組配置文件</string>
<string name="save_welcome_message_question">存歡迎息?</string>
<string name="cant_delete_user_profile">無法刪除用戶個人資料!</string>
<string name="user_hide">隱藏</string>
<string name="make_profile_private">將個人資料設為私密!</string>
<string name="v4_6_audio_video_calls">語音和視頻通話</string>
<string name="v4_6_audio_video_calls">視頻通話</string>
<string name="v4_6_chinese_spanish_interface">中文和西班牙文界面</string>
<string name="v4_6_reduced_battery_usage">進一步減少電池使用</string>
<string name="v4_6_group_moderation">組審核</string>
<string name="v4_6_group_moderation">組審核</string>
<string name="v4_6_reduced_battery_usage_descr">更多改進即將推出!</string>
<string name="v4_6_group_moderation_descr">現在管理員可以:
\n- 刪除成員的息。
\n- 刪除成員的息。
\n- 禁用成員(“觀察員”角色)</string>
<string name="v4_6_hidden_chat_profiles_descr">使用密碼保護的聊天資料!</string>
<string name="relay_server_protects_ip">中繼服器保護的 IP 地址,但它可以觀察通話的持續時間。</string>
<string name="v4_6_hidden_chat_profiles_descr">使用密碼保護的聊天資料!</string>
<string name="relay_server_protects_ip">中繼服器保護的 IP 地址,但它可以觀察通話的持續時間。</string>
<string name="button_add_welcome_message">添加歡迎信息</string>
<string name="error_saving_user_password">保存用戶密碼時出錯</string>
<string name="error_updating_user_privacy">更新用戶隱私時出錯</string>
<string name="relay_server_if_necessary">中繼服器僅在必要時使用。 另一方可以觀察到的 IP 地址。</string>
<string name="enter_password_to_show">輸入密碼去搜尋</string>
<string name="v4_6_group_welcome_message">組歡迎訊</string>
<string name="relay_server_if_necessary">中繼服器僅在必要時使用。 另一方可以觀察到的 IP 地址。</string>
<string name="enter_password_to_show">在上面輸入密碼以顯示</string>
<string name="v4_6_group_welcome_message">组欢迎信</string>
<string name="v4_6_hidden_chat_profiles">隱藏的聊天資料</string>
<string name="dont_show_again">不再顯示</string>
<string name="user_mute">靜音</string>
<string name="muted_when_inactive">Muted when inactive!</string>
<string name="v4_6_group_welcome_message_descr">設置向新成員顯示的息!</string>
<string name="tap_to_activate_profile">點擊以激活配置檔案</string>
<string name="v4_6_group_welcome_message_descr">設置向新成員顯示的息!</string>
<string name="tap_to_activate_profile">點擊以激活配置文件</string>
<string name="v4_6_audio_video_calls_descr">支持藍牙和其他改進。</string>
<string name="should_be_at_least_one_visible_profile">至少有一個可見的用戶配置檔案</string>
<string name="group_welcome_title">歡迎</string>
<string name="should_be_at_least_one_visible_profile">應該至少有一個可見的用戶配置文件</string>
<string name="group_welcome_title">歡迎</string>
<string name="v4_6_chinese_spanish_interface_descr">感謝用戶——通過 Weblate 做出貢獻!</string>
<string name="should_be_at_least_one_profile">應該至少有一個用戶配置檔案</string>
<string name="should_be_at_least_one_profile">應該至少有一個用戶配置文件</string>
<string name="user_unmute">取消靜音</string>
<string name="you_will_still_receive_calls_and_ntfs">當靜音配置檔案處於活動狀態時,仍會收到來自靜音配置檔案的通話和通知。</string>
<string name="you_will_still_receive_calls_and_ntfs">當靜音配置文件處於活動狀態時,仍會收到來自靜音配置文件的電話和通知。</string>
<string name="user_unhide">取消隱藏</string>
<string name="video_will_be_received_when_contact_is_online">影片將會在你的聯絡人在線時接收,請你等等或者稍後再檢查!</string>
<string name="confirm_database_upgrades">確認數據庫更新</string>
<string name="incompatible_database_version">數據庫版本不相容</string>
<string name="database_downgrade">數據庫降級</string>
<string name="database_upgrade">數據庫升級</string>
<string name="invalid_migration_confirmation">無效的遷移確認</string>
<string name="mtr_error_no_down_migration">數據庫現行版本比應用程式新,但是無法降級遷出:%s</string>
<string name="mtr_error_different">在應用程式/數據庫的不同遷移:%s/%s</string>
<string name="database_migrations">遷移:%s</string>
<string name="database_downgrade_warning">警告:你可能會遺失部分數據!</string>
<string name="image_will_be_received_when_contact_completes_uploading">圖片將會在你的聯絡人完成上傳後接收。</string>
<string name="file_will_be_received_when_contact_completes_uploading">檔案將會在你的聯絡人完成上傳後接收。</string>
<string name="settings_section_title_experimenta">實驗性</string>
<string name="upgrade_and_open_chat">升級和開始對話</string>
<string name="cancel_file__question">取消傳輸檔案?</string>
<string name="file_transfer_will_be_cancelled_warning">檔案傳遞將會取消。若是在傳遞檔案中,亦會暫停。</string>
<string name="show_developer_options">顯示開發者選項</string>
<string name="delete_profile">刪除資料</string>
<string name="unhide_chat_profile">取消隱藏聊天資料</string>
<string name="unhide_profile">取消隱藏個人資料</string>
<string name="delete_chat_profile">刪除對話資料</string>
<string name="icon_descr_video_asked_to_receive">詢問以接收影片</string>
<string name="videos_limit_desc">同一時間只能傳送十段影片</string>
<string name="videos_limit_title">過量影片!</string>
<string name="video_descr">影片</string>
<string name="icon_descr_video_snd_complete">已傳送影片</string>
<string name="icon_descr_waiting_for_video">等待影片中</string>
<string name="video_will_be_received_when_contact_completes_uploading">影片將會在你的聯絡人完成上傳後接收</string>
<string name="waiting_for_video">等待影片中</string>
<string name="hide_dev_options">隱藏:</string>
<string name="show_dev_options">顯示:</string>
<string name="developer_options">數據庫IDs和傳輸隔離選項。</string>
<string name="downgrade_and_open_chat">降級和開啟對話</string>
<string name="profile_password">個人資料密碼</string>
<string name="settings_send_files_via_xftp">通過 XFTP 傳送文件</string>
</resources>

View File

@@ -32,7 +32,6 @@
<string name="moderated_description">moderated</string>
<string name="invalid_chat">invalid chat</string>
<string name="invalid_data">invalid data</string>
<string name="decryption_error">Decryption error</string>
<!-- PendingContactConnection - ChatModel.kt -->
<string name="connection_local_display_name">connection <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
@@ -61,11 +60,7 @@
<!-- SimpleXAPI.kt -->
<string name="error_saving_smp_servers">Error saving SMP servers</string>
<string name="error_saving_xftp_servers">Error saving XFTP servers</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">Make sure SMP server addresses are in correct format, line separated and are not duplicated.</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Make sure XFTP server addresses are in correct format, line separated and are not duplicated.</string>
<string name="error_loading_smp_servers">Error loading SMP servers</string>
<string name="error_loading_xftp_servers">Error loading XFTP servers</string>
<string name="error_setting_network_config">Error updating network configuration</string>
<string name="failed_to_parse_chat_title">Failed to load chat</string>
<string name="failed_to_parse_chats_title">Failed to load chats</string>
@@ -101,18 +96,12 @@
<string name="error_changing_address">Error changing address</string>
<string name="error_smp_test_failed_at_step">Test failed at step %s.</string>
<string name="error_smp_test_server_auth">Server requires authorization to create queues, check password</string>
<string name="error_xftp_test_server_auth">Server requires authorization to upload, check password</string>
<string name="error_smp_test_certificate">Possibly, certificate fingerprint in server address is incorrect</string>
<string name="smp_server_test_connect">Connect</string>
<string name="smp_server_test_disconnect">Disconnect</string>
<string name="smp_server_test_create_queue">Create queue</string>
<string name="smp_server_test_secure_queue">Secure queue</string>
<string name="smp_server_test_delete_queue">Delete queue</string>
<string name="smp_server_test_create_file">Create file</string>
<string name="smp_server_test_upload_file">Upload file</string>
<string name="smp_server_test_download_file">Download file</string>
<string name="smp_server_test_compare_file">Compare file</string>
<string name="smp_server_test_delete_file">Delete file</string>
<string name="smp_server_test_disconnect">Disconnect</string>
<string name="error_deleting_user">Error deleting user profile</string>
<string name="error_updating_user_privacy">Error updating user privacy</string>
@@ -166,20 +155,6 @@
<string name="la_notice_title_simplex_lock">SimpleX Lock</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled.</string>
<string name="la_notice_turn_on">Turn on</string>
<string name="la_lock_mode">SimpleX Lock mode</string>
<string name="la_lock_mode_system">System authentication</string>
<string name="la_lock_mode_passcode">Passcode entry</string>
<string name="la_auth_failed">Authentication failed</string>
<string name="la_could_not_be_verified">You could not be verified; please try again.</string>
<string name="la_no_app_password">No app passcode</string>
<string name="la_enter_app_passcode">Enter Passcode</string>
<string name="la_current_app_passcode">Current Passcode</string>
<string name="la_change_app_passcode">Change passcode</string>
<string name="la_authenticate">Authenticate</string>
<string name="la_immediately">Immediately</string>
<string name="la_seconds">%d seconds</string>
<string name="la_minutes">%d minutes</string>
<string name="la_please_remember_to_store_password">Please remember or store it securely - there is no way to recover a lost password!</string>
<!-- LocalAuthentication.kt -->
<string name="auth_simplex_lock_turned_on">SimpleX Lock turned on</string>
@@ -194,8 +169,6 @@
<string name="auth_device_authentication_is_disabled_turning_off">Device authentication is disabled. Turning off SimpleX Lock.</string>
<string name="auth_stop_chat">Stop chat</string>
<string name="auth_open_chat_console">Open chat console</string>
<string name="lock_not_enabled">SimpleX Lock not enabled!</string>
<string name="you_can_turn_on_lock">You can turn on SimpleX Lock via Settings.</string>
<!-- Chat Alerts - ChatItemView.kt -->
<string name="message_delivery_error_title">Message delivery error</string>
@@ -218,18 +191,10 @@
<string name="delete_member_message__question">Delete member message?</string>
<string name="moderate_message_will_be_deleted_warning">The message will be deleted for all members.</string>
<string name="moderate_message_will_be_marked_warning">The message will be marked as moderated for all members.</string>
<string name="cancel_file__question">Cancel file transfer?</string>
<string name="file_transfer_will_be_cancelled_warning">File transfer will be cancelled. If it\'s in progress it will be stoppped.</string>
<string name="for_me_only">Delete for me</string>
<string name="for_everybody">For everyone</string>
<string name="stop_file__action">Stop file</string>
<string name="stop_snd_file__title">Stop sending file?</string>
<string name="stop_snd_file__message">Sending file will be stopped.</string>
<string name="stop_rcv_file__title">Stop receiving file?</string>
<string name="stop_rcv_file__message">Receiving file will be stopped.</string>
<string name="stop_file__confirm">Stop</string>
<string name="revoke_file__action">Revoke file</string>
<string name="revoke_file__title">Revoke file?</string>
<string name="revoke_file__message">File will be deleted from servers.</string>
<string name="revoke_file__confirm">Revoke</string>
<!-- CIMetaView.kt -->
<string name="icon_descr_edited">edited</string>
@@ -253,7 +218,7 @@
<!-- ShareListView.kt -->
<string name="share_message">Share message…</string>
<string name="share_image">Share media</string>
<string name="share_image">Share image</string>
<string name="share_file">Share file…</string>
<!-- ComposeView.kt, helpers -->
@@ -360,11 +325,9 @@
<!-- GetImageView -->
<string name="toast_permission_denied">Permission Denied!</string>
<string name="use_camera_button">Camera</string>
<string name="use_camera_button">Use Camera</string>
<string name="from_gallery_button">From Gallery</string>
<string name="choose_file">File</string>
<string name="gallery_image_button">Image</string>
<string name="gallery_video_button">Video</string>
<string name="choose_file">Choose file</string>
<!-- help - ChatHelpView.kt -->
<string name="thank_you_for_installing_simplex">Thank you for installing <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
@@ -513,14 +476,12 @@
<string name="smp_servers_delete_server">Delete server</string>
<string name="smp_servers_per_user">The servers for new connections of your current chat profile</string>
<string name="smp_save_servers_question">Save servers?</string>
<string name="xftp_servers">XFTP servers</string>
<string name="install_simplex_chat_for_terminal">Install <xliff:g id="appNameFull">SimpleX Chat</xliff:g> for terminal</string>
<string name="star_on_github">Star on GitHub</string>
<string name="contribute">Contribute</string>
<string name="rate_the_app">Rate the app</string>
<string name="use_simplex_chat_servers__question">Use <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers?</string>
<string name="your_SMP_servers">Your SMP servers</string>
<string name="your_XFTP_servers">Your XFTP servers</string>
<string name="using_simplex_chat_servers">Using <xliff:g id="appNameFull">SimpleX Chat</xliff:g> servers.</string>
<string name="how_to">How to</string>
<string name="how_to_use_your_servers">How to use your servers</string>
@@ -535,11 +496,6 @@
<string name="network_settings">Advanced network settings</string>
<string name="network_settings_title">Network settings</string>
<string name="network_socks_toggle">Use SOCKS proxy (port 9050)</string>
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
<string name="network_proxy_port">port %d</string>
<string name="host_verb">Host</string>
<string name="port_verb">Port</string>
<string name="network_enable_socks">Use SOCKS proxy?</string>
<string name="network_enable_socks_info">Access the servers via SOCKS proxy on port 9050? Proxy must be started before enabling this option.</string>
<string name="network_disable_socks">Use direct Internet connection?</string>
@@ -561,12 +517,12 @@
<string name="network_session_mode_user_description">A separate TCP connection (and SOCKS credential) will be used <b>for each chat profile you have in the app</b>.</string>
<string name="network_session_mode_entity_description">A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</string>
<string name="update_network_session_mode_question">Update transport isolation mode?</string>
<string name="disable_onion_hosts_when_not_supported">Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.</string>
<string name="appearance_settings">Appearance</string>
<string name="app_version_title">App version</string>
<string name="app_version_name">App version: v%s</string>
<string name="app_version_code">App build: %s</string>
<string name="core_version">Core version: v%s</string>
<string name="core_build_timestamp">Core built at: %s</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="show_dev_options">Show:</string>
<string name="hide_dev_options">Hide:</string>
@@ -591,7 +547,7 @@
<string name="display_name__field">Display name:</string>
<string name="full_name__field">"Full name:</string>
<string name="your_current_profile">Your current profile</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts. <xliff:g id="appName">SimpleX</xliff:g> servers cannot see your profile.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Your profile is stored on your device and shared only with your contacts.\n\n<xliff:g id="appName">SimpleX</xliff:g> servers cannot see your profile.</string>
<string name="edit_image">Edit image</string>
<string name="delete_image">Delete image</string>
<string name="save_preferences_question">Save preferences?</string>
@@ -600,6 +556,7 @@
<string name="save_and_notify_group_members">Save and notify group members</string>
<string name="exit_without_saving">Exit without saving</string>
<!-- HiddenProfileView.kt -->
<string name="hide_profile">Hide profile</string>
<string name="password_to_show">Password to show</string>
@@ -616,7 +573,6 @@
<string name="create_profile">Create profile</string>
<string name="your_profile_is_stored_on_your_device">Your profile, contacts and delivered messages are stored on your device.</string>
<string name="profile_is_only_shared_with_your_contacts">The profile is only shared with your contacts.</string>
<string name="no_spaces">No spaces!</string>
<string name="display_name_cannot_contain_whitespace">Display name cannot contain whitespace.</string>
<string name="display_name">Display Name</string>
<string name="full_name_optional__prompt">Full Name (optional)</string>
@@ -752,16 +708,7 @@
<string name="integrity_msg_bad_id">bad message ID</string>
<string name="integrity_msg_duplicate">duplicate message</string>
<string name="alert_title_skipped_messages">Skipped messages</string>
<string name="alert_text_skipped_messages_it_can_happen_when">It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised.</string>
<string name="alert_title_msg_bad_hash">Bad message hash</string>
<string name="alert_text_msg_bad_hash">The hash of the previous message is different."</string>
<string name="alert_title_msg_bad_id">Bad message ID</string>
<string name="alert_text_msg_bad_id">The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised.</string>
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messages failed to decrypt.</string>
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messages skipped.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">It can happen when you or your connection used the old database backup.</string>
<string name="alert_text_fragment_permanent_error_reconnect">This error is permanent for this connection, please re-connect.</string>
<string name="alert_text_fragment_please_report_to_developers">Please report it to the developers.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">It can happen when:\n1. The messages expire on the server if they were not received for 30 days,\n2. The server you use to receive the messages from this contact was updated and restarted.\n3. The connection is compromised.\nPlease connect to the developers via Settings to receive the updates about the servers.\nWe will be adding server redundancy to prevent lost messages.</string>
<!-- Privacy settings -->
<string name="privacy_and_security">Privacy &amp; security</string>
@@ -770,20 +717,6 @@
<string name="auto_accept_images">Auto-accept images</string>
<string name="send_link_previews">Send link previews</string>
<string name="full_backup">App data backup</string>
<string name="enable_lock">Enable lock</string>
<string name="lock_mode">Lock mode</string>
<string name="lock_after">Lock after</string>
<string name="submit_passcode">Submit</string>
<string name="confirm_passcode">Confirm Passcode</string>
<string name="incorrect_passcode">Incorrect passcode</string>
<string name="new_passcode">New Passcode</string>
<string name="authentication_cancelled">Authentication cancelled</string>
<string name="la_mode_system">System</string>
<string name="la_mode_passcode">Passcode</string>
<string name="passcode_set">Passcode set!</string>
<string name="passcode_changed">Passcode changed!</string>
<string name="passcode_not_changed">Passcode not changed!</string>
<string name="change_lock_mode">Change lock mode</string>
<!-- Settings sections -->
<string name="settings_section_title_you">YOU</string>
@@ -799,10 +732,12 @@
<string name="settings_section_title_language" translatable="false">LANGUAGE</string>
<string name="settings_section_title_icon">APP ICON</string>
<string name="settings_section_title_themes">THEMES</string>
<string name="settings_section_title_messages">MESSAGES AND FILES</string>
<string name="settings_section_title_messages">MESSAGES</string>
<string name="settings_section_title_calls">CALLS</string>
<string name="settings_section_title_incognito">Incognito mode</string>
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
<string name="settings_send_files_via_xftp">Send videos and files via XFTP</string>
<string name="xftp_requires_v461">v4.6.1+ is required to receive via XFTP.</string>
<!-- DatabaseView.kt -->
<string name="your_chat_database">Your chat database</string>
@@ -1183,8 +1118,6 @@
<string name="direct_messages">Direct messages</string>
<string name="full_deletion">Delete for everyone</string>
<string name="voice_messages">Voice messages</string>
<string name="audio_video_calls">Audio/video calls</string>
<string name="available_in_v51">\nAvailable in v5.1</string>
<string name="feature_enabled">enabled</string>
<string name="feature_enabled_for_you">enabled for you</string>
<string name="feature_enabled_for_contact">enabled for contact</string>
@@ -1201,9 +1134,6 @@
<string name="allow_your_contacts_to_send_voice_messages">Allow your contacts to send voice messages.</string>
<string name="allow_voice_messages_only_if">Allow voice messages only if your contact allows them.</string>
<string name="prohibit_sending_voice_messages">Prohibit sending voice messages.</string>
<string name="allow_your_contacts_to_call">Allow your contacts to call you.</string>
<string name="allow_calls_only_if">Allow calls only if your contact allows them.</string>
<string name="prohibit_calls">Prohibit audio/video calls.</string>
<string name="both_you_and_your_contact_can_send_disappearing">Both you and your contact can send disappearing messages.</string>
<string name="only_you_can_send_disappearing">Only you can send disappearing messages.</string>
<string name="only_your_contact_can_send_disappearing">Only your contact can send disappearing messages.</string>
@@ -1216,10 +1146,6 @@
<string name="only_you_can_send_voice">Only you can send voice messages.</string>
<string name="only_your_contact_can_send_voice">Only your contact can send voice messages.</string>
<string name="voice_prohibited_in_this_chat">Voice messages are prohibited in this chat.</string>
<string name="both_you_and_your_contact_can_make_calls">Both you and your contact can make calls.</string>
<string name="only_you_can_make_calls">Only you can make calls.</string>
<string name="only_your_contact_can_make_calls">Only your contact can make calls.</string>
<string name="calls_prohibited_with_this_contact">Audio/video calls are prohibited.</string>
<string name="allow_to_send_disappearing">Allow to send disappearing messages.</string>
<string name="prohibit_sending_disappearing">Prohibit sending disappearing messages.</string>
<string name="allow_direct_messages">Allow sending direct messages to members.</string>
@@ -1306,10 +1232,4 @@
<string name="v4_6_reduced_battery_usage_descr">More improvements are coming soon!</string>
<string name="v4_6_chinese_spanish_interface">Chinese and Spanish interface</string>
<string name="v4_6_chinese_spanish_interface_descr">Thanks to the users contribute via Weblate!</string>
<string name="v5_0_large_files_support">Videos and files up to 1gb</string>
<string name="v5_0_large_files_support_descr">Fast and no wait until the sender is online!</string>
<string name="v5_0_app_passcode">App passcode</string>
<string name="v5_0_app_passcode_descr">Set it instead of system authentication.</string>
<string name="v5_0_polish_interface">Polish interface</string>
<string name="v5_0_polish_interface_descr">Thanks to the users contribute via Weblate!</string>
</resources>

View File

@@ -23,10 +23,7 @@ struct ContentView: View {
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
@State private var showSettings = false
@State private var showWhatsNew = false
@State private var showChooseLAMode = false
@State private var showSetPasscode = false
var body: some View {
ZStack {
@@ -34,20 +31,6 @@ struct ContentView: View {
if chatModel.showCallView, let call = chatModel.activeCall {
callView(call)
}
if !showSettings, let la = chatModel.laRequest {
LocalAuthView(authRequest: la)
} else if showSetPasscode {
SetAppPasscodeView {
prefPerformLA = true
showSetPasscode = false
privacyLocalAuthModeDefault.set(.passcode)
alertManager.showAlert(laTurnedOnAlert())
} cancel: {
prefPerformLA = false
showSetPasscode = false
alertManager.showAlert(laPasscodeNotSetAlert())
}
}
}
.onAppear {
if prefPerformLA { requestNtfAuthorization() }
@@ -57,13 +40,6 @@ struct ContentView: View {
initAuthenticate()
}
.alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! }
.sheet(isPresented: $showSettings) {
SettingsView(showSettings: $showSettings)
}
.confirmationDialog("SimpleX Lock mode", isPresented: $showChooseLAMode, titleVisibility: .visible) {
Button("System authentication") { initialEnableLA() }
Button("Passcode entry") { showSetPasscode = true }
}
}
@ViewBuilder private func contentView() -> some View {
@@ -106,7 +82,7 @@ struct ContentView: View {
private func mainView() -> some View {
ZStack(alignment: .top) {
ChatListView(showSettings: $showSettings).privacySensitive(protectScreen)
ChatListView().privacySensitive(protectScreen)
.onAppear {
if !prefPerformLA { requestNtfAuthorization() }
// Local Authentication notice is to be shown on next start after onboarding is complete
@@ -156,7 +132,6 @@ struct ContentView: View {
}
private func initAuthenticate() {
logger.debug("initAuthenticate")
if CallController.useCallKit() && chatModel.showCallView && chatModel.activeCall != nil {
userAuthorized = false
} else if doAuthenticate {
@@ -177,18 +152,14 @@ struct ContentView: View {
private func justAuthenticate() {
userAuthorized = false
let laMode = privacyLocalAuthModeDefault.get()
authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason")) { laResult in
logger.debug("authenticate callback: \(String(describing: laResult))")
authenticate(reason: NSLocalizedString("Unlock", comment: "authentication reason")) { laResult in
switch (laResult) {
case .success:
userAuthorized = true
canConnectCall = true
lastSuccessfulUnlock = ProcessInfo.processInfo.systemUptime
case .failed:
if laMode == .passcode {
AlertManager.shared.showAlert(laFailedAlert())
}
break
case .unavailable:
userAuthorized = true
prefPerformLA = false
@@ -214,28 +185,25 @@ struct ContentView: View {
Alert(
title: Text("SimpleX Lock"),
message: Text("To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled."),
primaryButton: .default(Text("Turn on")) { showChooseLAMode = true },
primaryButton: .default(Text("Turn on")) {
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
prefPerformLA = true
alertManager.showAlert(laTurnedOnAlert())
case .failed:
prefPerformLA = false
alertManager.showAlert(laFailedAlert())
case .unavailable:
prefPerformLA = false
alertManager.showAlert(laUnavailableInstructionAlert())
}
}
},
secondaryButton: .cancel()
)
}
private func initialEnableLA () {
privacyLocalAuthModeDefault.set(.system)
authenticate(reason: NSLocalizedString("Enable SimpleX Lock", comment: "authentication reason")) { laResult in
switch laResult {
case .success:
prefPerformLA = true
alertManager.showAlert(laTurnedOnAlert())
case .failed:
prefPerformLA = false
alertManager.showAlert(laFailedAlert())
case .unavailable:
prefPerformLA = false
alertManager.showAlert(laUnavailableInstructionAlert())
}
}
}
func notificationAlert() -> Alert {
Alert(
title: Text("Notifications are disabled!"),

View File

@@ -21,7 +21,6 @@ final class ChatModel: ObservableObject {
@Published var chatDbChanged = false
@Published var chatDbEncrypted: Bool?
@Published var chatDbStatus: DBMigrationResult?
@Published var laRequest: LocalAuthRequest?
// list of chat "previews"
@Published var chats: [Chat] = []
// map of connections network statuses, key is agent connection id

View File

@@ -187,7 +187,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn
}
func apiStartChat() throws -> Bool {
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true, xftp: true))
let r = chatSendCmdSync(.startChat(subscribe: true, expire: true))
switch r {
case .chatStarted: return true
case .chatRunning: return false
@@ -745,7 +745,6 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? {
func cancelFile(user: User, fileId: Int64) async {
if let chatItem = await apiCancelFile(fileId: fileId) {
DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) }
cleanupFile(chatItem)
}
}
@@ -1324,36 +1323,37 @@ func processReceivedMsg(_ res: ChatResponse) async {
if active(user) {
m.updateGroup(groupInfo)
}
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileStart(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileComplete(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileSndCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileError(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .sndFileStart(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupDirectFile(aChatItem)
let cItem = aChatItem.chatItem
let mc = cItem.content.msgContent
if aChatItem.chatInfo.chatType == .direct,
case .file = mc,
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
case let .sndFileRcvCancelled(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupDirectFile(aChatItem)
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
chatItemSimpleUpdate(user, aChatItem)
case let .sndFileCompleteXFTP(user, aChatItem, _):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
case let .sndFileError(user, aChatItem):
chatItemSimpleUpdate(user, aChatItem)
cleanupFile(aChatItem)
let cItem = aChatItem.chatItem
let mc = cItem.content.msgContent
if case .file = mc,
let fileName = cItem.file?.filePath {
removeFile(fileName)
}
case let .callInvitation(invitation):
m.callInvitations[invitation.contact.id] = invitation
activateCall(invitation)

View File

@@ -106,8 +106,7 @@ struct SimpleXApp: App {
private func authenticationExpired() -> Bool {
if let enteredBackground = enteredBackground {
let delay = Double(UserDefaults.standard.integer(forKey: DEFAULT_LA_LOCK_DELAY))
return ProcessInfo.processInfo.systemUptime - enteredBackground >= delay
return ProcessInfo.processInfo.systemUptime - enteredBackground >= 30
} else {
return true
}

Some files were not shown because too many files have changed in this diff Show More