From e76440ee66cbd6be0208adbd37020d913e5b47e2 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:32:47 +0300 Subject: [PATCH 01/13] desktop: local alias update (#3026) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../simplex/common/views/chat/ChatInfoView.kt | 30 +++++++++++-------- .../views/helpers/DefaultBasicTextField.kt | 10 ++----- .../newchat/ContactConnectionInfoView.kt | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 87f8a7e651..170f870130 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -278,7 +279,7 @@ fun ChatInfoLayout( ChatInfoHeader(chat.chatInfo, contact) } - LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged) + LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) SectionSpacer() if (customUserProfile != null) { SectionView(generalGetString(MR.strings.incognito).uppercase()) { @@ -403,13 +404,16 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { @Composable fun LocalAliasEditor( + chatId: String, initialValue: String, center: Boolean = true, leadingIcon: Boolean = false, focus: Boolean = false, updateValue: (String) -> Unit ) { - var value by rememberSaveable { mutableStateOf(initialValue) } + val state = remember(chatId) { + mutableStateOf(TextFieldValue(initialValue)) + } var updatedValueAtLeastOnce = remember { false } val modifier = if (center) Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).widthIn(min = 100.dp) @@ -418,7 +422,7 @@ fun LocalAliasEditor( Row(Modifier.fillMaxWidth(), horizontalArrangement = if (center) Arrangement.Center else Arrangement.Start) { DefaultBasicTextField( modifier, - value, + state, { Text( generalGetString(MR.strings.text_field_set_contact_placeholder), @@ -431,27 +435,27 @@ fun LocalAliasEditor( } else null, color = MaterialTheme.colors.secondary, focus = focus, - textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center), - keyboardActions = KeyboardActions(onDone = { updateValue(value) }) + textStyle = TextStyle.Default.copy(textAlign = if (state.value.text.isEmpty() || !center) TextAlign.Start else TextAlign.Center), + keyboardActions = KeyboardActions(onDone = { updateValue(state.value.text) }) ) { - value = it + state.value = it updatedValueAtLeastOnce = true } } - LaunchedEffect(Unit) { - var prevValue = value - snapshotFlow { value } + LaunchedEffect(chatId) { + var prevValue = state.value + snapshotFlow { state.value } .distinctUntilChanged() .onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing .conflate() // get the latest value - .filter { it == value && it != prevValue } // don't process old ones + .filter { it == state.value && it != prevValue } // don't process old ones .collect { - updateValue(it) + updateValue(it.text) prevValue = it } } - DisposableEffect(Unit) { - onDispose { if (updatedValueAtLeastOnce) updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast + DisposableEffect(chatId) { + onDispose { if (updatedValueAtLeastOnce) updateValue(state.value.text) } // just in case snapshotFlow will be canceled when user presses Back too fast } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index 65eb11321e..71801e7a5f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.launch @Composable fun DefaultBasicTextField( modifier: Modifier, - initialValue: String, + state: MutableState, placeholder: (@Composable () -> Unit)? = null, leadingIcon: (@Composable () -> Unit)? = null, focus: Boolean = false, @@ -41,11 +41,8 @@ fun DefaultBasicTextField( selectTextOnFocus: Boolean = false, keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions: KeyboardActions = KeyboardActions(), - onValueChange: (String) -> Unit, + onValueChange: (TextFieldValue) -> Unit, ) { - val state = remember { - mutableStateOf(TextFieldValue(initialValue)) - } val focusRequester = remember { FocusRequester() } val keyboard = LocalSoftwareKeyboardController.current @@ -83,8 +80,7 @@ fun DefaultBasicTextField( minHeight = TextFieldDefaults.MinHeight ), onValueChange = { - state.value = it - onValueChange(it.text) + onValueChange(it) }, cursorBrush = SolidColor(colors.cursorColor(false).value), visualTransformation = VisualTransformation.None, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index fe62a7d9da..934c050d8a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -126,7 +126,7 @@ private fun ContactConnectionInfoLayout( ) if (contactConnection.groupLinkId == null) { - LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged) + LocalAliasEditor(contactConnection.id, contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged) } SectionView { From 113a57c7c759d4e9674cdea2428c8a028622c397 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:43:51 +0100 Subject: [PATCH 02/13] ios: update chat_read_file (#3037) --- apps/ios/SimpleXChat/CryptoFile.swift | 25 +++++++++++++++---------- apps/ios/SimpleXChat/SimpleX.h | 11 ++++++----- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift index d641464ee0..dcb2be9ae0 100644 --- a/apps/ios/SimpleXChat/CryptoFile.swift +++ b/apps/ios/SimpleXChat/CryptoFile.swift @@ -25,20 +25,25 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { } } -enum ReadFileResult: Decodable { - case result(fileSize: Int) - case error(readError: String) -} - public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data { var cPath = path.cString(using: .utf8)! var cKey = cryptoArgs.fileKey.cString(using: .utf8)! var cNonce = cryptoArgs.fileNonce.cString(using: .utf8)! - let r = chat_read_file(&cPath, &cKey, &cNonce)! - let d = String.init(cString: r).data(using: .utf8)! - switch try jsonDecoder.decode(ReadFileResult.self, from: d) { - case let .error(err): throw RuntimeError(err) - case let .result(size): return Data(bytes: r.advanced(by: d.count + 1), count: size) + let ptr = chat_read_file(&cPath, &cKey, &cNonce)! + let status = UInt8(ptr.pointee) + switch status { + case 0: // ok + let dLen = Data(bytes: ptr.advanced(by: 1), count: 4) + let len = dLen.withUnsafeBytes { $0.load(as: UInt32.self) } + let d = Data(bytes: ptr.advanced(by: 5), count: Int(len)) + free(ptr) + return d + case 1: // error + let err = String.init(cString: ptr) + free(ptr) + throw RuntimeError(err) + default: + throw RuntimeError("unexpected chat_read_file status: \(status)") } } diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 55b44dee31..67c2fa728c 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -26,16 +26,17 @@ extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_encrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len); -// chat_write_file returns NUL-terminated string with JSON of WriteFileResult +// chat_write_file returns null-terminated string with JSON of WriteFileResult extern char *chat_write_file(char *path, char *data, int len); // chat_read_file returns a buffer with: -// 1. NUL-terminated C string with JSON of ReadFileResult, followed by -// 2. file data, the length is defined in ReadFileResult +// result status (1 byte), then if +// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length. +// status == 1 (error): null-terminated error message string. extern char *chat_read_file(char *path, char *key, char *nonce); -// chat_encrypt_file returns NUL-terminated string with JSON of WriteFileResult +// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult extern char *chat_encrypt_file(char *fromPath, char *toPath); -// chat_decrypt_file returns NUL-terminated string with the error message +// chat_decrypt_file returns null-terminated string with the error message extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath); From ad656224077c68b997c7839e2f19ca594991bdba Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Fri, 8 Sep 2023 00:45:00 +0300 Subject: [PATCH 03/13] desktop: catch Toast exception (#3028) --- .../kotlin/chat/simplex/common/model/NtfManager.desktop.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index 486b147f81..94e985328e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -104,7 +104,11 @@ object NtfManager { actions.forEach { builder.action(it.first, it.second) } - prevNtfs.add(chatId to builder.toast()) + try { + prevNtfs.add(chatId to builder.toast()) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } } private fun prepareIconPath(icon: ImageBitmap?): String? = if (icon != null) { From 45682aa7ced97b3e9ac87f4c8d6f998181b26b1f Mon Sep 17 00:00:00 2001 From: sh <37271604+shumvgolove@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:45:22 +0300 Subject: [PATCH 04/13] website: fix the apple-app-site-association (#3038) --- website/src/.well-known/README.md | 4 +++- .../index.json} | 0 2 files changed, 3 insertions(+), 1 deletion(-) rename website/src/.well-known/{apple-app-site-association => apple-app-site-association/index.json} (100%) diff --git a/website/src/.well-known/README.md b/website/src/.well-known/README.md index ec4c7f57e8..6346c85a76 100644 --- a/website/src/.well-known/README.md +++ b/website/src/.well-known/README.md @@ -12,4 +12,6 @@ File `assetlinks.json` includes certificate hashes for: ## iOS -`apple-app-site-association` currently does not work, as it needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension. +`apple-app-site-association` needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension. + +To workaround this (thanks to [StackOverflow - Serve json data from github pages](https://stackoverflow.com/questions/39199042/serve-json-data-from-github-pages)) we're creating directory named `apple-app-site-association` with `index.json` file that contains all the necessary configs. \ No newline at end of file diff --git a/website/src/.well-known/apple-app-site-association b/website/src/.well-known/apple-app-site-association/index.json similarity index 100% rename from website/src/.well-known/apple-app-site-association rename to website/src/.well-known/apple-app-site-association/index.json From 281d9c7f794de59dff035a99deffc2842c49547c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:21:56 +0100 Subject: [PATCH 05/13] ios: add Finnish and Ukranian interface languages (#3040) --- .../cs.xcloc/Localized Contents/cs.xliff | 8 + .../de.xcloc/Localized Contents/de.xliff | 8 + .../en.xcloc/Localized Contents/en.xliff | 10 + .../es.xcloc/Localized Contents/es.xliff | 8 + .../AccentColor.colorset/Contents.json | 15 + .../Shared/Assets.xcassets/Contents.json | 6 + .../AccentColor.colorset/Contents.json | 23 + .../Shared/Assets.xcassets/Contents.json | 6 + .../SimpleX NSE/en.lproj/InfoPlist.strings | 6 + .../en.lproj/Localizable.strings | 30 + .../en.lproj/SimpleX--iOS--InfoPlist.strings | 10 + .../fi.xcloc/contents.json | 12 + .../fr.xcloc/Localized Contents/fr.xliff | 8 + .../it.xcloc/Localized Contents/it.xliff | 8 + .../ja.xcloc/Localized Contents/ja.xliff | 8 + .../nl.xcloc/Localized Contents/nl.xliff | 8 + .../pl.xcloc/Localized Contents/pl.xliff | 8 + .../ru.xcloc/Localized Contents/ru.xliff | 8 + .../th.xcloc/Localized Contents/th.xliff | 8 + .../AccentColor.colorset/Contents.json | 15 + .../Shared/Assets.xcassets/Contents.json | 6 + .../uk.xcloc/Localized Contents/uk.xliff | 14 +- .../AccentColor.colorset/Contents.json | 23 + .../Shared/Assets.xcassets/Contents.json | 6 + .../SimpleX NSE/en.lproj/InfoPlist.strings | 6 + .../en.lproj/Localizable.strings | 30 + .../en.lproj/SimpleX--iOS--InfoPlist.strings | 10 + .../uk.xcloc/contents.json | 12 + .../Localized Contents/zh-Hans.xliff | 8 + .../SimpleX NSE/fi.lproj/InfoPlist.strings | 9 + .../SimpleX NSE/uk.lproj/InfoPlist.strings | 9 + apps/ios/SimpleX.xcodeproj/project.pbxproj | 14 + apps/ios/fi.lproj/Localizable.strings | 3675 +++++++++++++++++ .../fi.lproj/SimpleX--iOS--InfoPlist.strings | 15 + apps/ios/uk.lproj/Localizable.strings | 3675 +++++++++++++++++ .../uk.lproj/SimpleX--iOS--InfoPlist.strings | 15 + scripts/ios/export-localizations.sh | 2 +- scripts/ios/import-localizations.sh | 2 +- 38 files changed, 7735 insertions(+), 9 deletions(-) create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings create mode 100644 apps/ios/SimpleX Localizations/fi.xcloc/contents.json create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings create mode 100644 apps/ios/SimpleX Localizations/uk.xcloc/contents.json create mode 100644 apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings create mode 100644 apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings create mode 100644 apps/ios/fi.lproj/Localizable.strings create mode 100644 apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings create mode 100644 apps/ios/uk.lproj/Localizable.strings create mode 100644 apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index bef40f5ae1..8e90ae4594 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1819,6 +1819,10 @@ Šifrovat databázi? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Zašifrovaná databáze @@ -1949,6 +1953,10 @@ Chyba při vytváření profilu! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Chyba při mazání databáze chatu diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index d9286fb4f5..fe164da9a9 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1819,6 +1819,10 @@ Datenbank verschlüsseln? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Verschlüsselte Datenbank @@ -1949,6 +1953,10 @@ Fehler beim Erstellen des Profils! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Fehler beim Löschen der Chat-Datenbank diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 2ef116055a..5374efbf0f 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1819,6 +1819,11 @@ Encrypt database? No comment provided by engineer. + + Encrypt local files + Encrypt local files + No comment provided by engineer. + Encrypted database Encrypted database @@ -1949,6 +1954,11 @@ Error creating profile! No comment provided by engineer. + + Error decrypting file + Error decrypting file + No comment provided by engineer. + Error deleting chat database Error deleting chat database diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 174261590f..84325c1180 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1819,6 +1819,10 @@ ¿Cifrar base de datos? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Base de datos cifrada @@ -1949,6 +1953,10 @@ ¡Error al crear perfil! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Error al eliminar base de datos diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..e919fc253a --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "fi" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..124ddbcc33 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cf485752ea --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,30 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..3af673b19f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,10 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json new file mode 100644 index 0000000000..c46e0f6a71 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "fi", + "toolInfo" : { + "toolBuildNumber" : "15A5219j", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "15.0" + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 113e003091..95de1b8b27 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1819,6 +1819,10 @@ Chiffrer la base de données ? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Base de données chiffrée @@ -1949,6 +1953,10 @@ Erreur lors de la création du profil ! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Erreur lors de la suppression de la base de données du chat diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index de3b0c0a67..fb6ea10da1 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1819,6 +1819,10 @@ Crittografare il database? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Database crittografato @@ -1949,6 +1953,10 @@ Errore nella creazione del profilo! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Errore nell'eliminazione del database della chat diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 1bba3acde0..c7460be601 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1818,6 +1818,10 @@ データベースを暗号化しますか? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database 暗号化済みデータベース @@ -1948,6 +1952,10 @@ プロフィール作成にエラー発生! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database チャットデータベース削除にエラー発生 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index c32f79b785..7882a062e7 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -1819,6 +1819,10 @@ Database versleutelen? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Versleutelde database @@ -1949,6 +1953,10 @@ Fout bij aanmaken van profiel! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Fout bij het verwijderen van de chat database diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 68bc9b929e..7402249c01 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1819,6 +1819,10 @@ Zaszyfrować bazę danych? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Zaszyfrowana baza danych @@ -1949,6 +1953,10 @@ Błąd tworzenia profilu! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Błąd usuwania bazy danych czatu diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 5641ba085e..eec4fd40de 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1819,6 +1819,10 @@ Зашифровать базу данных? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database База данных зашифрована @@ -1949,6 +1953,10 @@ Ошибка создания профиля! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database Ошибка при удалении данных чата diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 72182b2c7a..11bde620f3 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1807,6 +1807,10 @@ Encrypt ฐานข้อมูล? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database Encrypt ฐานข้อมูลเรียบร้อยแล้ว @@ -1937,6 +1941,10 @@ เกิดข้อผิดพลาดในการสร้างโปรไฟล์! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..4b8ee6308e --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "uk" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 3051a61122..52c69fbfac 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1891,7 +1891,7 @@ Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - Встановіть [SimpleX Chat для терміналу] (https://github.com/simplex-chat/simplex-chat) + Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. @@ -2586,7 +2586,7 @@ We will be adding server redundancy to prevent lost messages. Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - Читайте більше в нашому [GitHub репозиторії] (https://github.com/simplex-chat/simplex-chat#readme). + Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme). No comment provided by engineer. @@ -3892,17 +3892,17 @@ SimpleX servers cannot see your profile. [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - [Внесок] (https://github.com/simplex-chat/simplex-chat#contribute) + [Внесок](https://github.com/simplex-chat/simplex-chat#contribute) No comment provided by engineer. [Send us email](mailto:chat@simplex.chat) - [Напишіть нам електронною поштою] (mailto:chat@simplex.chat) + [Напишіть нам електронною поштою](mailto:chat@simplex.chat) No comment provided by engineer. [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - [Зірка на GitHub] (https://github.com/simplex-chat/simplex-chat) + [Зірка на GitHub](https://github.com/simplex-chat/simplex-chat) No comment provided by engineer. @@ -5369,7 +5369,7 @@ SimpleX servers cannot see your profile. Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Читайте більше в [Посібнику користувача] (https://simplex.chat/docs/guide/readme.html#connect-to-friends). + Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. @@ -5419,7 +5419,7 @@ SimpleX servers cannot see your profile. Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Читайте більше в [Посібнику користувача] (https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). + Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..124ddbcc33 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cf485752ea --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,30 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..3af673b19f --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,10 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json new file mode 100644 index 0000000000..6ad42fd109 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "uk", + "toolInfo" : { + "toolBuildNumber" : "15A5219j", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "15.0" + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index d77ef1e819..8fa66159d4 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1808,6 +1808,10 @@ 加密数据库? No comment provided by engineer. + + Encrypt local files + No comment provided by engineer. + Encrypted database 加密数据库 @@ -1938,6 +1942,10 @@ 创建资料错误! No comment provided by engineer. + + Error decrypting file + No comment provided by engineer. + Error deleting chat database 删除聊天数据库错误 diff --git a/apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings new file mode 100644 index 0000000000..28a503d909 --- /dev/null +++ b/apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. Kaikki oikeudet pidätetään."; + diff --git a/apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings new file mode 100644 index 0000000000..da1f5367b3 --- /dev/null +++ b/apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Авторське право © 2022 SimpleX Chat. Всі права захищені."; + diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index fc301a3ab8..026bc963a0 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -268,6 +268,8 @@ 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; + 5C136D8E2AAB3D14006DE2FC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = "fi.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; + 5C136D8F2AAB3D14006DE2FC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = ""; }; 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = ""; }; @@ -296,6 +298,8 @@ 5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = ""; }; 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = ""; }; + 5C636F662AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = "uk.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; + 5C636F672AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; 5C65DAE429C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 5C65DAE629C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C65DAE729C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -415,6 +419,8 @@ 5CE2BA96284537A800EC33A6 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = ""; }; 5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = ""; }; 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; + 5CE6C7B32AAB1515007F345C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + 5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; @@ -1006,6 +1012,8 @@ pl, ja, th, + fi, + uk, ); mainGroup = 5CA059BD279559F40002BEB4; packageReferences = ( @@ -1287,6 +1295,8 @@ 5C6D183329E93FBA00D430B3 /* pl */, 5CAC411B2A192DE800C331A2 /* ja */, 5CA3ED502A9422D1005D71E2 /* th */, + 5C136D8F2AAB3D14006DE2FC /* fi */, + 5C636F672AAB3D2400751C84 /* uk */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1306,6 +1316,8 @@ 5CAB912529E93F9400F34A95 /* pl */, 5CAC41182A192D8400C331A2 /* ja */, 5CA3ED4D2A942170005D71E2 /* th */, + 5CE6C7B32AAB1515007F345C /* fi */, + 5CE6C7B42AAB1527007F345C /* uk */, ); name = Localizable.strings; sourceTree = ""; @@ -1324,6 +1336,8 @@ 5C6D183229E93FBA00D430B3 /* pl */, 5CAC411A2A192DE800C331A2 /* ja */, 5CA3ED4F2A9422D1005D71E2 /* th */, + 5C136D8E2AAB3D14006DE2FC /* fi */, + 5C636F662AAB3D2400751C84 /* uk */, ); name = "SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..47cce5061d --- /dev/null +++ b/apps/ios/fi.lproj/Localizable.strings @@ -0,0 +1,3675 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" (" = " ("; + +/* No comment provided by engineer. */ +" (can be copied)" = " (voidaan kopioida)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- vakaampi viestien toimitus.\n- hieman paremmat ryhmät.\n- ja paljon muuta!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- ääniviestit enintään 5 minuuttia.\n- mukautettu katoamisaika.\n- historian muokkaaminen."; + +/* No comment provided by engineer. */ +", " = ", "; + +/* No comment provided by engineer. */ +": " = ": "; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 värillinen!"; + +/* No comment provided by engineer. */ +"." = "."; + +/* No comment provided by engineer. */ +"(" = "("; + +/* No comment provided by engineer. */ +")" = ")"; + +/* No comment provided by engineer. */ +"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Osallistu](https://github.com/simplex-chat/simplex-chat#contribute)"; + +/* No comment provided by engineer. */ +"[Send us email](mailto:chat@simplex.chat)" = "[Lähetä meille sähköpostia](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Tähti GitHubissa](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille."; + +/* No comment provided by engineer. */ +"**Create link / QR code** for your contact to use." = "**Luo linkki / QR-koodi* kontaktille."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**e2e-salattu** äänipuhelu"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**e2e-salattu** videopuhelu"; + +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on."; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta)."; + +/* No comment provided by engineer. */ +"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**."; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen."; + +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin."; + +/* No comment provided by engineer. */ +"**Scan QR code**: to connect to your contact in person or via video call." = "**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta."; + +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* copied message info title, # */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Historia"; + +/* copied message info */ +"## In reply to" = "## vastauksena"; + +/* No comment provided by engineer. */ +"#secret#" = "#salaisuus#"; + +/* No comment provided by engineer. */ +"%@" = "% @"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (nykyinen)"; + +/* copied message info */ +"%@ (current):" = "% (nykyinen):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / % @"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ % @"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ ja %@ yhdistetty"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ klo %2$@:"; + +/* notification title */ +"%@ is connected!" = "%@ on yhdistetty!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ ei ole vahvistettu"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ on vahvistettu"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ palvelimet"; + +/* notification title */ +"%@ wants to connect!" = "%@ haluaa muodostaa yhteyden!"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ ja %lld muut jäsenet yhdistetty"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d päivää"; + +/* time interval */ +"%d hours" = "%d tuntia"; + +/* time interval */ +"%d min" = "%d min"; + +/* time interval */ +"%d months" = "%d kuukautta"; + +/* time interval */ +"%d sec" = "%d sek"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d ohitettua viestiä"; + +/* time interval */ +"%d weeks" = "%d viikkoa"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld kontaktia valittu"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld tiedosto(a), joiden kokonaiskoko on %@"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld jäsenet"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld minuuttia"; + +/* No comment provided by engineer. */ +"%lld second(s)" = "%lld sekunti(a)"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld sekuntia"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldd"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldh"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldm"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldmth"; + +/* No comment provided by engineer. */ +"%llds" = "%llds"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldw"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "%u viestien salauksen purku epäonnistui."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u viestit ohitettu."; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p> Hei! </p>\n<p> <a href=\"%@\"> Ollaan yhteydessä SimpleX Chatin kautta</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* No comment provided by engineer. */ +"0s" = "0s"; + +/* time interval */ +"1 day" = "1 päivä"; + +/* time interval */ +"1 hour" = "1 tunti"; + +/* No comment provided by engineer. */ +"1 minute" = "1 minuutti"; + +/* time interval */ +"1 month" = "1 kuukausi"; + +/* time interval */ +"1 week" = "1 viikko"; + +/* No comment provided by engineer. */ +"1-time link" = "Kertakäyttölinkki"; + +/* No comment provided by engineer. */ +"5 minutes" = "5 minuuttia"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 sekuntia"; + +/* No comment provided by engineer. */ +"A few more things" = "Muutama asia lisää"; + +/* notification title */ +"A new contact" = "Uusi kontakti"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Uusi satunnainen profiili jaetaan."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Erillistä TCP-yhteyttä käytetään **jokaiselle sovelluksessa olevalle chat-profiilille**."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä**.\n**Huomaa**: jos kontakteja on useita, akun ja liikenteen kulutus voi olla huomattavasti suurempi ja jotkin yhteydet voivat epäonnistua."; + +/* No comment provided by engineer. */ +"Abort" = "Keskeytä"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Keskeytä osoitteenvaihto"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Keskeytä osoitteenvaihto?"; + +/* No comment provided by engineer. */ +"About SimpleX" = "Tietoja SimpleX:stä"; + +/* No comment provided by engineer. */ +"About SimpleX address" = "Tietoja SimpleX osoitteesta"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "Tietoja SimpleX Chatistä"; + +/* No comment provided by engineer. */ +"above, then choose:" = "edellä, valitse sitten:"; + +/* No comment provided by engineer. */ +"Accent color" = "Korostusväri"; + +/* accept contact request via notification + accept incoming call via notification */ +"Accept" = "Hyväksy"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Hyväksy yhteyspyyntö?"; + +/* notification body */ +"Accept contact request from %@?" = "Hyväksy kontaktipyyntö %@:ltä?"; + +/* accept contact request via notification */ +"Accept incognito" = "Hyväksy tuntematon"; + +/* call status */ +"accepted call" = "hyväksytty puhelu"; + +/* No comment provided by engineer. */ +"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi."; + +/* No comment provided by engineer. */ +"Add preset servers" = "Lisää esiasetettuja palvelimia"; + +/* No comment provided by engineer. */ +"Add profile" = "Lisää profiili"; + +/* No comment provided by engineer. */ +"Add server…" = "Lisää palvelin…"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Lisää palvelimia skannaamalla QR-koodeja."; + +/* No comment provided by engineer. */ +"Add to another device" = "Lisää toiseen laitteeseen"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Lisää tervetuloviesti"; + +/* No comment provided by engineer. */ +"Address" = "Osoite"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta."; + +/* member role */ +"admin" = "ylläpitäjä"; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Ylläpitäjät voivat luoda linkkejä ryhmiin liittymiseen."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Verkon lisäasetukset"; + +/* chat item text */ +"agreeing encryption for %@…" = "salauksesta sovitaan %@:lle…"; + +/* chat item text */ +"agreeing encryption…" = "hyväksyy salausta…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Kaikki sovelluksen tiedot poistetaan."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Kaikki keskustelut ja viestit poistetaan - tätä ei voi kumota!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "Kaikki tiedot poistetaan, kun se syötetään."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Kaikki ryhmän jäsenet pysyvät yhteydessä."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Kaikki kontaktisi pysyvät yhteydessä."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään kontakteillesi."; + +/* No comment provided by engineer. */ +"Allow" = "Salli"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Salli puhelut vain, jos kontaktisi sallii ne."; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you." = "Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle."; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "Salli reaktiot viesteihin vain, jos kontaktisi sallii ne."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Salli viestireaktiot."; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "Salli yksityisviestien lähettäminen jäsenille."; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "Salli katoavien viestien lähettäminen."; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages." = "Salli lähetettyjen viestien peruuttamaton poistaminen."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "Salli tiedostojen ja median lähettäminen."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "Salli ääniviestien lähettäminen."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "Salli ääniviestit vain, jos kontaktisi sallii ne."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Salli ääniviestit?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "Salli kontaktiesi lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "Salli kontaktiesi soittaa sinulle."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages." = "Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti."; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Salli kontaktiesi lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Salli kontaktiesi lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"Already connected?" = "Oletko jo muodostanut yhteyden?"; + +/* pref value */ +"always" = "aina"; + +/* No comment provided by engineer. */ +"Always use relay" = "Käytä aina relettä"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti."; + +/* No comment provided by engineer. */ +"Answer call" = "Vastaa puheluun"; + +/* No comment provided by engineer. */ +"App build: %@" = "Sovellusversio: %@"; + +/* No comment provided by engineer. */ +"App icon" = "Sovelluksen kuvake"; + +/* No comment provided by engineer. */ +"App passcode" = "Sovelluksen pääsykoodi"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla."; + +/* No comment provided by engineer. */ +"App version" = "Sovellusversio"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Sovellusversio: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Ulkonäkö"; + +/* No comment provided by engineer. */ +"Attach" = "Liitä"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Ääni- ja videopuhelut"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Ääni- ja videopuhelut"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "äänipuhelu (ei e2e-salattu)"; + +/* chat feature */ +"Audio/video calls" = "Ääni/videopuhelut"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "Ääni-/videopuhelut ovat kiellettyjä."; + +/* PIN entry */ +"Authentication cancelled" = "Tunnistautuminen peruutettu"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Tunnistautuminen epäonnistui"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "Tunnistautuminen vaaditaan ennen kuin puhelu yhdistetään, mutta puheluita voi jäädä vastaamatta."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Tunnistautuminen ei ole käytettävissä"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Hyväksy automaattisesti"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "Hyväksy yhteydenottopyynnöt automaattisesti"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Hyväksy kuvat automaattisesti"; + +/* No comment provided by engineer. */ +"Back" = "Takaisin"; + +/* integrity error chat item */ +"bad message hash" = "virheellinen viestin tarkiste"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Virheellinen viestin tarkiste"; + +/* integrity error chat item */ +"bad message ID" = "virheellinen viestin tunniste"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Virheellinen viestin tunniste"; + +/* No comment provided by engineer. */ +"Better messages" = "Parempia viestejä"; + +/* No comment provided by engineer. */ +"bold" = "lihavoitu"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "Sekä sinä että kontaktisi voivat käyttää viestireaktioita."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages." = "Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit."; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "Sekä sinä että kontaktisi voitte soittaa puheluita."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "Sekä sinä että kontaktisi voitte lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "Sekä sinä että kontaktisi voitte lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; + +/* No comment provided by engineer. */ +"Call already ended!" = "Puhelu on jo päättynyt!"; + +/* call status */ +"call error" = "soittovirhe"; + +/* call status */ +"call in progress" = "puhelu käynnissä"; + +/* call status */ +"calling…" = "soittaa…"; + +/* No comment provided by engineer. */ +"Calls" = "Puhelut"; + +/* No comment provided by engineer. */ +"Can't delete user profile!" = "Käyttäjäprofiilia ei voi poistaa!"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Kontaktia ei voi kutsua!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Kontakteja ei voi kutsua!"; + +/* No comment provided by engineer. */ +"Cancel" = "Peruuta"; + +/* feature offered item */ +"cancelled %@" = "peruutettu %@"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi"; + +/* No comment provided by engineer. */ +"Cannot receive file" = "Tiedostoa ei voi vastaanottaa"; + +/* No comment provided by engineer. */ +"Change" = "Muuta"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Muutetaanko tietokannan tunnuslause?"; + +/* authentication reason */ +"Change lock mode" = "Vaihda lukitustilaa"; + +/* No comment provided by engineer. */ +"Change member role?" = "Vaihda jäsenroolia?"; + +/* authentication reason */ +"Change passcode" = "Vaihda pääsykoodi"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Vaihda vastaanotto-osoitetta"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Vaihda vastaanotto-osoite?"; + +/* No comment provided by engineer. */ +"Change role" = "Vaihda rooli"; + +/* authentication reason */ +"Change self-destruct mode" = "Vaihda itsetuhotilaa"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "Vaihda itsetuhoutuva pääsykoodi"; + +/* chat item text */ +"changed address for you" = "muuttunut osoite sinulle"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "%1$@:n roolin muuttui %2$@:ksi"; + +/* rcv group event chat item */ +"changed your role to %@" = "roolisi muuttui %@:ksi"; + +/* chat item text */ +"changing address for %@…" = "osoitteen muuttaminen %@:lle…"; + +/* chat item text */ +"changing address…" = "muuttamassa osoitetta…"; + +/* No comment provided by engineer. */ +"Chat archive" = "Chat-arkisto"; + +/* No comment provided by engineer. */ +"Chat console" = "Chat-konsoli"; + +/* No comment provided by engineer. */ +"Chat database" = "Chat-tietokanta"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Chat-tietokanta poistettu"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Chat-tietokanta tuotu"; + +/* No comment provided by engineer. */ +"Chat is running" = "Chat on käynnissä"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Chat on pysäytetty"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Chat-asetukset"; + +/* No comment provided by engineer. */ +"Chats" = "Keskustelut"; + +/* No comment provided by engineer. */ +"Check server address and try again." = "Tarkista palvelimen osoite ja yritä uudelleen."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Kiinalainen ja espanjalainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"Choose file" = "Valitse tiedosto"; + +/* No comment provided by engineer. */ +"Choose from library" = "Valitse kirjastosta"; + +/* No comment provided by engineer. */ +"Clear" = "Tyhjennä"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Tyhjennä keskustelu"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Tyhjennä keskustelu?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Tyhjennä vahvistus"; + +/* No comment provided by engineer. */ +"colored" = "värillinen"; + +/* No comment provided by engineer. */ +"Colors" = "Värit"; + +/* server test step */ +"Compare file" = "Vertaa tiedostoa"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Vertaa turvakoodeja kontaktiesi kanssa."; + +/* No comment provided by engineer. */ +"complete" = "valmis"; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "Määritä ICE-palvelimet"; + +/* No comment provided by engineer. */ +"Confirm" = "Vahvista"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Vahvista tietokannan päivitykset"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Vahvista uusi tunnuslause…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Vahvista pääsykoodi"; + +/* No comment provided by engineer. */ +"Confirm password" = "Vahvista salasana"; + +/* server test step */ +"Connect" = "Yhdistä"; + +/* No comment provided by engineer. */ +"Connect directly" = "Yhdistä suoraan"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Yhdistä Incognito"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "ole yhteydessä SimpleX Chat -kehittäjiin."; + +/* No comment provided by engineer. */ +"Connect via contact link" = "Yhdistä kontaktilinkillä"; + +/* No comment provided by engineer. */ +"Connect via group link?" = "Yhdistetäänkö ryhmälinkin kautta?"; + +/* No comment provided by engineer. */ +"Connect via link" = "Yhdistä linkin kautta"; + +/* No comment provided by engineer. */ +"Connect via link / QR code" = "Yhdistä linkillä / QR-koodilla"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Yhdistä kertalinkillä"; + +/* No comment provided by engineer. */ +"connected" = "yhdistetty"; + +/* No comment provided by engineer. */ +"connecting" = "yhdistää"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "yhdistäminen (hyväksytty)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "yhdistäminen (ilmoitettu)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "yhdistäminen (esitelty)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "yhdistäminen (esittelykutsu)"; + +/* call status */ +"connecting call" = "yhdistää puhelun…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Yhteyden muodostaminen palvelimeen…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Yhteyden muodostaminen palvelimeen... (virhe: %@)"; + +/* chat list item title */ +"connecting…" = "yhdistää…"; + +/* No comment provided by engineer. */ +"Connection" = "Yhteys"; + +/* No comment provided by engineer. */ +"Connection error" = "Yhteysvirhe"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "Yhteysvirhe (AUTH)"; + +/* chat list item title (it should not be shown */ +"connection established" = "yhteys luotu"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Yhteyspyyntö lähetetty!"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Yhteyden aikakatkaisu"; + +/* connection information */ +"connection:%@" = "yhteys:%@"; + +/* No comment provided by engineer. */ +"Contact allows" = "Kontakti sallii"; + +/* No comment provided by engineer. */ +"Contact already exists" = "Kontakti on jo olemassa"; + +/* No comment provided by engineer. */ +"Contact and all messages will be deleted - this cannot be undone!" = "Kontakti ja kaikki viestit poistetaan - tätä ei voi perua!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "kontaktilla on e2e-salaus"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "kontaktilla ei ole e2e-salausta"; + +/* notification */ +"Contact hidden:" = "Kontakti piilotettu:"; + +/* notification */ +"Contact is connected" = "Kontakti on yhdistetty"; + +/* No comment provided by engineer. */ +"Contact is not connected yet!" = "Kontaktia ei ole vielä yhdistetty!"; + +/* No comment provided by engineer. */ +"Contact name" = "Kontaktin nimi"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Kontaktin asetukset"; + +/* No comment provided by engineer. */ +"Contacts" = "Kontaktit"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "Kontaktit voivat merkitä viestit poistettaviksi; voit katsella niitä."; + +/* No comment provided by engineer. */ +"Continue" = "Jatka"; + +/* chat item action */ +"Copy" = "Kopioi"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Ydinversio: v%@"; + +/* No comment provided by engineer. */ +"Create" = "Luo"; + +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä."; + +/* server test step */ +"Create file" = "Luo tiedosto"; + +/* No comment provided by engineer. */ +"Create group link" = "Luo ryhmälinkki"; + +/* No comment provided by engineer. */ +"Create link" = "Luo linkki"; + +/* No comment provided by engineer. */ +"Create one-time invitation link" = "Luo kertakutsulinkki"; + +/* server test step */ +"Create queue" = "Luo jono"; + +/* No comment provided by engineer. */ +"Create secret group" = "Luo salainen ryhmä"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "Luo SimpleX-osoite"; + +/* No comment provided by engineer. */ +"Create your profile" = "Luo profiilisi"; + +/* No comment provided by engineer. */ +"Created on %@" = "Luotu %@"; + +/* No comment provided by engineer. */ +"creator" = "luoja"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Nykyinen pääsykoodi"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Nykyinen tunnuslause…"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Nykyinen tuettu enimmäistiedostokoko on %@."; + +/* dropdown time picker choice */ +"custom" = "mukautettu"; + +/* No comment provided by engineer. */ +"Custom time" = "Mukautettu aika"; + +/* No comment provided by engineer. */ +"Dark" = "Tumma"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Tietokannan alentaminen"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Tietokanta salattu!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Tietokannan salaustunnuslause päivitetään ja tallennetaan avainnippuun.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Tietokannan salauksen tunnuslause päivitetään.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Tietokantavirhe"; + +/* No comment provided by engineer. */ +"Database ID" = "Tietokannan tunnus"; + +/* copied message info */ +"Database ID: %d" = "Tietokannan tunnus: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Tietokantatunnukset ja kuljetuseristysvaihtoehto."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "Tietokanta on salattu satunnaisella tunnuslauseella, voit muuttaa sitä."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "Tietokanta on salattu satunnaisella tunnuslauseella. Vaihda se ennen vientiä."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Tietokannan tunnuslause"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Tietokannan tunnuslause ja vienti"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Tietokannan tunnuslause eroaa avainnippuun tallennetusta."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Keskustelun avaamiseen tarvitaan tietokannan tunnuslause."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Tietokannan päivitys"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "tietokantaversio on uudempi kuin sovellus, mutta ei alaspäin siirtymistä varten: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "Tietokanta salataan ja tunnuslause tallennetaan avainnippuun.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "Tietokanta salataan.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "Tietokanta siirretään, kun sovellus käynnistyy uudelleen"; + +/* time unit */ +"days" = "päivää"; + +/* No comment provided by engineer. */ +"Decentralized" = "Hajautettu"; + +/* message decrypt error item */ +"Decryption error" = "Salauksen purkuvirhe"; + +/* pref value */ +"default (%@)" = "oletusarvo (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "oletusarvo (ei)"; + +/* No comment provided by engineer. */ +"default (yes)" = "oletusarvo (kyllä)"; + +/* chat item action */ +"Delete" = "Poista"; + +/* No comment provided by engineer. */ +"Delete address" = "Poista osoite"; + +/* No comment provided by engineer. */ +"Delete address?" = "Poista osoite?"; + +/* No comment provided by engineer. */ +"Delete after" = "Poista jälkeen"; + +/* No comment provided by engineer. */ +"Delete all files" = "Poista kaikki tiedostot"; + +/* No comment provided by engineer. */ +"Delete archive" = "Poista arkisto"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "Poista keskusteluarkisto?"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Poista keskusteluprofiili"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Poista keskusteluprofiili?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Poista yhteys"; + +/* No comment provided by engineer. */ +"Delete contact" = "Poista kontakti"; + +/* No comment provided by engineer. */ +"Delete Contact" = "Poista kontakti"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Poista kontakti?"; + +/* No comment provided by engineer. */ +"Delete database" = "Poista tietokanta"; + +/* server test step */ +"Delete file" = "Poista tiedosto"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Poista tiedostot ja media?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Poista tiedostot kaikista keskusteluprofiileista"; + +/* chat feature */ +"Delete for everyone" = "Poista kaikilta"; + +/* No comment provided by engineer. */ +"Delete for me" = "Poista minulta"; + +/* No comment provided by engineer. */ +"Delete group" = "Poista ryhmä"; + +/* No comment provided by engineer. */ +"Delete group?" = "Poista ryhmä?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Poista kutsu"; + +/* No comment provided by engineer. */ +"Delete link" = "Poista linkki"; + +/* No comment provided by engineer. */ +"Delete link?" = "Poista linkki?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Poista jäsenviesti?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Poista viesti?"; + +/* No comment provided by engineer. */ +"Delete messages" = "Poista viestit"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Poista viestit tämän jälkeen"; + +/* No comment provided by engineer. */ +"Delete old database" = "Poista vanha tietokanta"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Poista vanha tietokanta?"; + +/* No comment provided by engineer. */ +"Delete pending connection" = "Poista vireillä oleva yhteys"; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Poistetaanko odottava yhteys?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Poista profiili"; + +/* server test step */ +"Delete queue" = "Poista jono"; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Poista käyttäjäprofiili?"; + +/* deleted chat item */ +"deleted" = "poistettu"; + +/* No comment provided by engineer. */ +"Deleted at" = "Poistettu klo"; + +/* copied message info */ +"Deleted at: %@" = "Poistettu klo: %@"; + +/* rcv group event chat item */ +"deleted group" = "poistettu ryhmä"; + +/* No comment provided by engineer. */ +"Delivery" = "Toimitus"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Toimituskuittaukset poissa käytöstä!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Toimituskuittaukset!"; + +/* No comment provided by engineer. */ +"Description" = "Kuvaus"; + +/* No comment provided by engineer. */ +"Develop" = "Kehitä"; + +/* No comment provided by engineer. */ +"Developer tools" = "Kehittäjätyökalut"; + +/* No comment provided by engineer. */ +"Device" = "Laite"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Laitteen todennus on poistettu käytöstä. SimpleX Lock kytketään pois päältä."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Laitteen todennus ei ole käytössä. Voit ottaa SimpleX Lockin käyttöön Asetuksista, kun olet ottanut laitteen todennuksen käyttöön."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "eri siirtyminen sovelluksessa/tietokannassa: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Eri nimet, avatarit ja kuljetuseristys."; + +/* connection level description */ +"direct" = "suora"; + +/* chat feature */ +"Direct messages" = "Yksityisviestit"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this group." = "Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä."; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "Poista käytöstä (pidä ohitukset)"; + +/* No comment provided by engineer. */ +"Disable for all" = "Poista käytöstä kaikilta"; + +/* authentication reason */ +"Disable SimpleX Lock" = "Poista SimpleX Lock käytöstä"; + +/* No comment provided by engineer. */ +"disabled" = "ei käytössä"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Tuhoutuva viesti"; + +/* chat feature */ +"Disappearing messages" = "Tuhoutuvat viestit"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Katoavat viestit ovat kiellettyjä tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this group." = "Katoavat viestit ovat kiellettyjä tässä ryhmässä."; + +/* No comment provided by engineer. */ +"Disappears at" = "Katoaa klo"; + +/* copied message info */ +"Disappears at: %@" = "Katoaa klo: %@"; + +/* server test step */ +"Disconnect" = "Katkaise"; + +/* No comment provided by engineer. */ +"Display name" = "Näyttönimi"; + +/* No comment provided by engineer. */ +"Display name:" = "Näyttönimi:"; + +/* No comment provided by engineer. */ +"Do it later" = "Tee myöhemmin"; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "Älä käytä SimpleX-sovellusta hätäpuheluihin."; + +/* No comment provided by engineer. */ +"Don't create address" = "Älä luo osoitetta"; + +/* No comment provided by engineer. */ +"Don't enable" = "Älä salli"; + +/* No comment provided by engineer. */ +"Don't show again" = "Älä näytä uudelleen"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Alenna ja avaa keskustelu"; + +/* server test step */ +"Download file" = "Lataa tiedosto"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Päällekkäinen näyttönimi!"; + +/* integrity error chat item */ +"duplicate message" = "päällekkäinen viesti"; + +/* No comment provided by engineer. */ +"Duration" = "Kesto"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "e2e-salattu"; + +/* chat item action */ +"Edit" = "Muokkaa"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Muokkaa ryhmäprofiilia"; + +/* No comment provided by engineer. */ +"Enable" = "Salli"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Salli (pidä ohitukset)"; + +/* No comment provided by engineer. */ +"Enable automatic message deletion?" = "Ota automaattinen viestien poisto käyttöön?"; + +/* No comment provided by engineer. */ +"Enable for all" = "Salli kaikille"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Salli välittömät ilmoitukset?"; + +/* No comment provided by engineer. */ +"Enable lock" = "Ota lukitus käyttöön"; + +/* No comment provided by engineer. */ +"Enable notifications" = "Salli ilmoitukset"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "Salli säännölliset ilmoitukset?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Ota itsetuho käyttöön"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Ota itsetuhoava pääsykoodi käyttöön"; + +/* authentication reason */ +"Enable SimpleX Lock" = "Ota SimpleX Lock käyttöön"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "Ota TCP-säilytys käyttöön"; + +/* enabled status */ +"enabled" = "käytössä"; + +/* enabled status */ +"enabled for contact" = "käytössä kontaktille"; + +/* enabled status */ +"enabled for you" = "käytössä sinulle"; + +/* No comment provided by engineer. */ +"Encrypt" = "Salaa"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Salaa tietokanta?"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Salattu tietokanta"; + +/* notification */ +"Encrypted message or another event" = "Salattu viesti tai muu tapahtuma"; + +/* notification */ +"Encrypted message: database error" = "Salattu viesti: tietokantavirhe"; + +/* notification */ +"Encrypted message: database migration error" = "Salattu viesti: tietokannan siirtovirhe"; + +/* notification */ +"Encrypted message: keychain error" = "Salattu viesti: avainnipun virhe"; + +/* notification */ +"Encrypted message: no passphrase" = "Salattu viesti: ei tunnuslausetta"; + +/* notification */ +"Encrypted message: unexpected error" = "Salattu viesti: odottamaton virhe"; + +/* chat item text */ +"encryption agreed" = "salaus sovittu"; + +/* chat item text */ +"encryption agreed for %@" = "salaus sovittu %@:lle"; + +/* chat item text */ +"encryption ok" = "salaus ok"; + +/* chat item text */ +"encryption ok for %@" = "salaus ok %@:lle"; + +/* chat item text */ +"encryption re-negotiation allowed" = "salauksen uudelleenneuvottelu sallittu"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "salauksen uudelleenneuvottelu sallittu %@:lle"; + +/* chat item text */ +"encryption re-negotiation required" = "tarvitaan salauksen uudelleenneuvottelu"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "tarvitaan salauksen uudelleenneuvottelu %@:lle"; + +/* No comment provided by engineer. */ +"ended" = "päättyi"; + +/* call status */ +"ended call %@" = "puhelu päättyi %@:lle"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Anna oikea tunnuslause."; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Syötä pääsykoodi"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Syötä tunnuslause…"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "Kirjoita yllä oleva salasana näyttääksesi!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "Syötä palvelin manuaalisesti"; + +/* placeholder */ +"Enter welcome message…" = "Kirjoita tervetuloviesti…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Kirjoita tervetuloviesti... (valinnainen)"; + +/* No comment provided by engineer. */ +"error" = "virhe"; + +/* No comment provided by engineer. */ +"Error" = "Virhe"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Virhe osoitteenmuutoksen keskeytyksessä"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Virhe kontaktipyynnön hyväksymisessä"; + +/* No comment provided by engineer. */ +"Error accessing database file" = "Virhe tietokantatiedoston käyttämisessä"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Virhe lisättäessä jäseniä"; + +/* No comment provided by engineer. */ +"Error changing address" = "Virhe osoitteenvaihdossa"; + +/* No comment provided by engineer. */ +"Error changing role" = "Virhe roolin vaihdossa"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Virhe asetuksen muuttamisessa"; + +/* No comment provided by engineer. */ +"Error creating address" = "Virhe osoitteen luomisessa"; + +/* No comment provided by engineer. */ +"Error creating group" = "Virhe ryhmän luomisessa"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Virhe ryhmälinkin luomisessa"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Virhe profiilin luomisessa!"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Virhe keskustelujen tietokannan poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Virhe keskutelun poistamisessa!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Virhe yhteyden poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting contact" = "Virhe kontaktin poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Virhe tietokannan poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Virhe vanhan tietokannan poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Virhe tokenin poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Virhe käyttäjäprofiilin poistamisessa"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Virhe toimituskuittauksien sallimisessa!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Virhe ilmoitusten käyttöönotossa"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Virhe tietokannan salauksessa"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Virhe vietäessä keskustelujen tietokantaa"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Virhe keskustelujen tietokannan tuonnissa"; + +/* No comment provided by engineer. */ +"Error joining group" = "Virhe ryhmään liittymisessä"; + +/* No comment provided by engineer. */ +"Error loading %@ servers" = "Virhe %@-palvelimien lataamisessa"; + +/* No comment provided by engineer. */ +"Error receiving file" = "Virhe tiedoston vastaanottamisessa"; + +/* No comment provided by engineer. */ +"Error removing member" = "Virhe poistettaessa jäsentä"; + +/* No comment provided by engineer. */ +"Error saving %@ servers" = "Virhe %@ palvelimien tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Virhe ryhmäprofiilin tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "Virhe ICE-palvelimien tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Virhe pääsykoodin tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Virhe tunnuslauseen tallentamisessa avainnippuun"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Virhe käyttäjän salasanan tallentamisessa"; + +/* No comment provided by engineer. */ +"Error sending email" = "Virhe sähköpostin lähettämisessä"; + +/* No comment provided by engineer. */ +"Error sending message" = "Virhe viestin lähettämisessä"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Virhe toimituskuittauksien asettamisessa!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Virhe käynnistettäessä keskustelua"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Virhe keskustelun lopettamisessa"; + +/* No comment provided by engineer. */ +"Error switching profile!" = "Virhe profiilin vaihdossa!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Virhe yhteyden synkronoinnissa"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Virhe ryhmälinkin päivittämisessä"; + +/* No comment provided by engineer. */ +"Error updating message" = "Virhe viestin päivityksessä"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Virhe asetusten päivittämisessä"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Virhe päivitettäessä käyttäjän tietosuojaa"; + +/* No comment provided by engineer. */ +"Error: " = "Virhe: "; + +/* No comment provided by engineer. */ +"Error: %@" = "Virhe: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Virhe: ei tietokantatiedostoa"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Virhe: URL on virheellinen"; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Jopa kun ei käytössä keskustelussa."; + +/* No comment provided by engineer. */ +"event happened" = "tapahtuma tapahtui"; + +/* No comment provided by engineer. */ +"Exit without saving" = "Poistu tallentamatta"; + +/* No comment provided by engineer. */ +"Export database" = "Vie tietokanta"; + +/* No comment provided by engineer. */ +"Export error:" = "Vientivirhe:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Viety tietokanta-arkisto."; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Tietokanta-arkiston vienti…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Tunnuslauseen poisto epäonnistui"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!"; + +/* No comment provided by engineer. */ +"Favorite" = "Suosikki"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "Tiedosto poistetaan palvelimilta."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "Tiedosto vastaanotetaan, kun kontaktisi on ladannut sen."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "Tiedosto vastaanotetaan, kun kontakti on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"File: %@" = "Tiedosto: %@"; + +/* No comment provided by engineer. */ +"Files & media" = "Tiedostot & media"; + +/* chat feature */ +"Files and media" = "Tiedostot ja media"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "Tiedostot ja media ovat tässä ryhmässä kiellettyjä."; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Tiedostot ja media kielletty!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Suodata lukemattomia- ja suosikkikeskusteluja."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Vihdoinkin meillä! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Löydä keskustelut nopeammin"; + +/* No comment provided by engineer. */ +"Fix" = "Korjaa"; + +/* No comment provided by engineer. */ +"Fix connection" = "Korjaa yhteys"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Korjaa yhteys?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Korjaa salaus varmuuskopioiden palauttamisen jälkeen."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Kontakti ei tue korjausta"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Ryhmän jäsen ei tue korjausta"; + +/* No comment provided by engineer. */ +"For console" = "Konsoliin"; + +/* No comment provided by engineer. */ +"French interface" = "Ranskalainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"Full link" = "Koko linkki"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Koko nimi (valinnainen)"; + +/* No comment provided by engineer. */ +"Full name:" = "Koko nimi:"; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Täysin uudistettu - toimii taustalla!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Entistä pienempi akun käyttö"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIFit ja tarrat"; + +/* No comment provided by engineer. */ +"Group" = "Ryhmä"; + +/* No comment provided by engineer. */ +"group deleted" = "ryhmä poistettu"; + +/* No comment provided by engineer. */ +"Group display name" = "Ryhmän näyttönimi"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "Ryhmän näyttönimi (valinnainen)"; + +/* No comment provided by engineer. */ +"Group image" = "Ryhmäkuva"; + +/* No comment provided by engineer. */ +"Group invitation" = "Ryhmän kutsu"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "Vanhentunut ryhmäkutsu"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "Ryhmäkutsu ei ole enää voimassa, lähettäjä poisti sen."; + +/* No comment provided by engineer. */ +"Group link" = "Ryhmälinkki"; + +/* No comment provided by engineer. */ +"Group links" = "Ryhmälinkit"; + +/* No comment provided by engineer. */ +"Group members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Group members can irreversibly delete sent messages." = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti."; + +/* No comment provided by engineer. */ +"Group members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; + +/* No comment provided by engineer. */ +"Group members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Group members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; + +/* No comment provided by engineer. */ +"Group members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; + +/* notification */ +"Group message:" = "Ryhmäviesti:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Ryhmän moderointi"; + +/* No comment provided by engineer. */ +"Group preferences" = "Ryhmän asetukset"; + +/* No comment provided by engineer. */ +"Group profile" = "Ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Ryhmäprofiili tallennetaan jäsenten laitteille, ei palvelimille."; + +/* snd group event chat item */ +"group profile updated" = "ryhmäprofiili päivitetty"; + +/* No comment provided by engineer. */ +"Group welcome message" = "Ryhmän tervetuloviesti"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Ryhmä poistetaan kaikilta jäseniltä - tätä ei voi kumota!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "Ryhmä poistetaan sinulta - tätä ei voi perua!"; + +/* No comment provided by engineer. */ +"Help" = "Apua"; + +/* No comment provided by engineer. */ +"Hidden" = "Piilotettu"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Piilotetut keskusteluprofiilit"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Piilotettu profiilin salasana"; + +/* chat item action */ +"Hide" = "Piilota"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Piilota sovellusnäyttö viimeisimmissä sovelluksissa."; + +/* No comment provided by engineer. */ +"Hide profile" = "Piilota profiili"; + +/* No comment provided by engineer. */ +"Hide:" = "Piilota:"; + +/* No comment provided by engineer. */ +"History" = "Historia"; + +/* time unit */ +"hours" = "tuntia"; + +/* No comment provided by engineer. */ +"How it works" = "Kuinka se toimii"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "Miten SimpleX toimii"; + +/* No comment provided by engineer. */ +"How to" = "Miten"; + +/* No comment provided by engineer. */ +"How to use it" = "Kuinka sitä käytetään"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Miten käytät palvelimiasi"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "ICE-palvelimet (yksi per rivi)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki."; + +/* No comment provided by engineer. */ +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "Jos syötät itsetuhoutuvan pääsykoodin sovellusta avattaessa:"; + +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Jos haluat käyttää keskustelua nyt, napauta **Tee se myöhemmin** alla (sinulle tarjotaan tietokannan siirtämistä, kun käynnistät sovelluksen uudelleen)."; + +/* No comment provided by engineer. */ +"Ignore" = "Sivuuta"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "Kuva vastaanotetaan, kun kontaktisi on ladannut sen."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"Immediately" = "Heti"; + +/* No comment provided by engineer. */ +"Immune to spam and abuse" = "Immuuni roskapostille ja väärinkäytöksille"; + +/* No comment provided by engineer. */ +"Import" = "Tuo"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Tuo keskustelujen-tietokanta?"; + +/* No comment provided by engineer. */ +"Import database" = "Tuo tietokanta"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Parannettu yksityisyys ja turvallisuus"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Parannettu palvelimen kokoonpano"; + +/* No comment provided by engineer. */ +"In reply to" = "Vastauksena"; + +/* No comment provided by engineer. */ +"Incognito" = "Incognito"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Incognito-tila"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Incognito-tila suojaa yksityisyyttäsi käyttämällä uutta satunnaista profiilia jokaiselle kontaktille."; + +/* chat list item description */ +"incognito via contact address link" = "incognito kontaktilinkin kautta"; + +/* chat list item description */ +"incognito via group link" = "incognito ryhmälinkin kautta"; + +/* chat list item description */ +"incognito via one-time link" = "incognito kertalinkillä"; + +/* notification */ +"Incoming audio call" = "Saapuva äänipuhelu"; + +/* notification */ +"Incoming call" = "Saapuva puhelu"; + +/* notification */ +"Incoming video call" = "Saapuva videopuhelu"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Yhteensopimaton tietokantaversio"; + +/* PIN entry */ +"Incorrect passcode" = "Väärä pääsykoodi"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Väärä turvakoodi!"; + +/* connection level description */ +"indirect (%d)" = "epäsuora (%d)"; + +/* chat item action */ +"Info" = "Tiedot"; + +/* No comment provided by engineer. */ +"Initial role" = "Alkuperäinen rooli"; + +/* No comment provided by engineer. */ +"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; + +/* No comment provided by engineer. */ +"Instantly" = "Heti"; + +/* No comment provided by engineer. */ +"Interface" = "Käyttöliittymä"; + +/* invalid chat data */ +"invalid chat" = "virheellinen keskustelu"; + +/* No comment provided by engineer. */ +"invalid chat data" = "virheelliset keskustelu-tiedot"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Virheellinen yhteyslinkki"; + +/* invalid chat item */ +"invalid data" = "virheelliset tiedot"; + +/* No comment provided by engineer. */ +"Invalid server address!" = "Virheellinen palvelinosoite!"; + +/* item status text */ +"Invalid status" = "Virheellinen tila"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "Vanhentunut kutsu!"; + +/* group name */ +"invitation to group %@" = "kutsu ryhmään %@"; + +/* No comment provided by engineer. */ +"Invite friends" = "Kutsu ystäviä"; + +/* No comment provided by engineer. */ +"Invite members" = "Kutsu jäseniä"; + +/* No comment provided by engineer. */ +"Invite to group" = "Kutsu ryhmään"; + +/* No comment provided by engineer. */ +"invited" = "kutsuttu"; + +/* rcv group event chat item */ +"invited %@" = "kutsuttu %@"; + +/* chat list item title */ +"invited to connect" = "kutsuttu yhteydenpitoon"; + +/* rcv group event chat item */ +"invited via your group link" = "kutsuttu ryhmäsi linkin kautta"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen - se mahdollistaa push-ilmoitusten vastaanottamisen."; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen sen muuttamisen tai sovelluksen uudelleen käynnistämisen jälkeen - se mahdollistaa push-ilmoitusten vastaanottamisen."; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Peruuttamaton viestin poisto"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "Viestien peruuttamaton poisto on kielletty tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this group." = "Viestien peruuttamaton poisto on kielletty tässä ryhmässä."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Se voi tapahtua, kun sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta."; + +/* No comment provided by engineer. */ +"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." = "Se voi tapahtua, kun:\n1. Viestit vanhenivat lähettävässä päätelaitteessa kahden päivän päästä tai palvelimella 30 päivän kuluttua.\n2. Viestin salauksen purku epäonnistui, koska sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta.\n3. Yhteys vaarantui."; + +/* No comment provided by engineer. */ +"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "Italialainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"italic" = "kursivoitu"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Japanilainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"Join" = "Liity"; + +/* No comment provided by engineer. */ +"join as %@" = "Liity %@:nä"; + +/* No comment provided by engineer. */ +"Join group" = "Liity ryhmään"; + +/* No comment provided by engineer. */ +"Join incognito" = "Liity incognito-tilassa"; + +/* No comment provided by engineer. */ +"Joining group" = "Liittyy ryhmään"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Pidä kontaktisi"; + +/* No comment provided by engineer. */ +"Keychain error" = "Avainnipun virhe"; + +/* No comment provided by engineer. */ +"KeyChain error" = "Avainnipun virhe"; + +/* No comment provided by engineer. */ +"Large file!" = "Suuri tiedosto!"; + +/* No comment provided by engineer. */ +"Learn more" = "Lue lisää"; + +/* No comment provided by engineer. */ +"Leave" = "Poistu"; + +/* No comment provided by engineer. */ +"Leave group" = "Poistu ryhmästä"; + +/* No comment provided by engineer. */ +"Leave group?" = "Poistu ryhmästä?"; + +/* rcv group event chat item */ +"left" = "poistunut"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Jutellaan SimpleX Chatissa"; + +/* No comment provided by engineer. */ +"Light" = "Vaalea"; + +/* No comment provided by engineer. */ +"Limitations" = "Rajoitukset"; + +/* No comment provided by engineer. */ +"LIVE" = "LIVE"; + +/* No comment provided by engineer. */ +"Live message!" = "Live-viesti!"; + +/* No comment provided by engineer. */ +"Live messages" = "Live-viestit"; + +/* No comment provided by engineer. */ +"Local name" = "Paikallinen nimi"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Vain paikalliset profiilitiedot"; + +/* No comment provided by engineer. */ +"Lock after" = "Lukitse jälkeen"; + +/* No comment provided by engineer. */ +"Lock mode" = "Lukitustila"; + +/* No comment provided by engineer. */ +"Make a private connection" = "Luo yksityinen yhteys"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Hävitä yksi viesti"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Tee profiilista yksityinen!"; + +/* No comment provided by engineer. */ +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@)."; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä."; + +/* No comment provided by engineer. */ +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?*"; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Merkitse poistetuksi kaikilta"; + +/* No comment provided by engineer. */ +"Mark read" = "Merkitse luetuksi"; + +/* No comment provided by engineer. */ +"Mark verified" = "Merkitse vahvistetuksi"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Markdown viesteissä"; + +/* marked deleted chat item preview text */ +"marked deleted" = "merkitty poistetuksi"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Enintään 30 sekuntia, vastaanotetaan välittömästi."; + +/* member role */ +"member" = "jäsen"; + +/* No comment provided by engineer. */ +"Member" = "Jäsen"; + +/* rcv group event chat item */ +"member connected" = "yhdistetty"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Jäsenen rooli muuttuu muotoon \"%@\". Kaikille ryhmän jäsenille ilmoitetaan asiasta."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Jäsenen rooli muutetaan muotoon \"%@\". Jäsen saa uuden kutsun."; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Jäsen poistetaan ryhmästä - tätä ei voi perua!"; + +/* item status text */ +"Message delivery error" = "Viestin toimitusvirhe"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Viestien toimituskuittaukset!"; + +/* No comment provided by engineer. */ +"Message draft" = "Viestiluonnos"; + +/* chat feature */ +"Message reactions" = "Viestireaktiot"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "Viestireaktiot ovat kiellettyjä tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "Viestireaktiot ovat kiellettyjä tässä ryhmässä."; + +/* notification */ +"message received" = "viesti vastaanotettu"; + +/* No comment provided by engineer. */ +"Message text" = "Viestin teksti"; + +/* No comment provided by engineer. */ +"Messages" = "Viestit"; + +/* No comment provided by engineer. */ +"Messages & files" = "Viestit ja tiedostot"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Siirretään tietokannan arkistoa…"; + +/* No comment provided by engineer. */ +"Migration error:" = "Siirtovirhe:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Siirto epäonnistui. Jatka nykyisen tietokannan käyttöä napauttamalla alla **Poistu**. Ilmoita ongelmasta sovelluskehittäjille keskustelussa tai sähköpostitse [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Siirto on valmis"; + +/* No comment provided by engineer. */ +"Migrations: %@" = "Siirrot: %@"; + +/* time unit */ +"minutes" = "minuuttia"; + +/* call status */ +"missed call" = "vastaamaton puhelu"; + +/* chat item action */ +"Moderate" = "Moderoi"; + +/* moderated chat item */ +"moderated" = "moderoitu"; + +/* No comment provided by engineer. */ +"Moderated at" = "Moderoitu klo"; + +/* copied message info */ +"Moderated at: %@" = "Moderoitu klo: %@"; + +/* No comment provided by engineer. */ +"moderated by %@" = "%@ moderoi"; + +/* time unit */ +"months" = "kuukautta"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Lisää parannuksia on tulossa pian!"; + +/* item status description */ +"Most likely this connection is deleted." = "Todennäköisesti tämä yhteys on poistettu."; + +/* No comment provided by engineer. */ +"Most likely this contact has deleted the connection with you." = "Todennäköisesti tämä kontakti on poistanut yhteyden sinuun."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Useita keskusteluprofiileja"; + +/* No comment provided by engineer. */ +"Mute" = "Mykistä"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Mykistetty ei-aktiivisena!"; + +/* No comment provided by engineer. */ +"Name" = "Nimi"; + +/* No comment provided by engineer. */ +"Network & servers" = "Verkko ja palvelimet"; + +/* No comment provided by engineer. */ +"Network settings" = "Verkkoasetukset"; + +/* No comment provided by engineer. */ +"Network status" = "Verkon tila"; + +/* No comment provided by engineer. */ +"never" = "ei koskaan"; + +/* notification */ +"New contact request" = "Uusi kontaktipyyntö"; + +/* notification */ +"New contact:" = "Uusi kontakti:"; + +/* No comment provided by engineer. */ +"New database archive" = "Uusi tietokanta-arkisto"; + +/* No comment provided by engineer. */ +"New display name" = "Uusi näyttönimi"; + +/* No comment provided by engineer. */ +"New in %@" = "Uutta %@"; + +/* No comment provided by engineer. */ +"New member role" = "Uusi jäsenrooli"; + +/* notification */ +"new message" = "uusi viesti"; + +/* notification */ +"New message" = "Uusi viesti"; + +/* No comment provided by engineer. */ +"New Passcode" = "Uusi pääsykoodi"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Uusi tunnuslause…"; + +/* pref value */ +"no" = "ei"; + +/* No comment provided by engineer. */ +"No" = "Ei"; + +/* Authentication unavailable */ +"No app password" = "Ei sovelluksen salasanaa"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Kontakteja ei ole valittu"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Ei lisättäviä kontakteja"; + +/* No comment provided by engineer. */ +"No delivery information" = "Ei toimitustietoja"; + +/* No comment provided by engineer. */ +"No device token!" = "Ei laitetunnusta!"; + +/* No comment provided by engineer. */ +"no e2e encryption" = "ei e2e-salausta"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Ei suodatettuja keskusteluja"; + +/* No comment provided by engineer. */ +"No group!" = "Ryhmää ei löydy!"; + +/* No comment provided by engineer. */ +"No history" = "Ei historiaa"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Ei lupaa ääniviestin tallentamiseen"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Ei vastaanotettuja tai lähetettyjä tiedostoja"; + +/* copied message info in history */ +"no text" = "ei tekstiä"; + +/* No comment provided by engineer. */ +"Notifications" = "Ilmoitukset"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Ilmoitukset on poistettu käytöstä!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Nyt järjestelmänvalvojat voivat:\n- poistaa jäsenten viestit.\n- poista jäsenet käytöstä (\"tarkkailija\" rooli)"; + +/* member role */ +"observer" = "tarkkailija"; + +/* enabled status + group pref value */ +"off" = "pois"; + +/* No comment provided by engineer. */ +"Off" = "Pois"; + +/* No comment provided by engineer. */ +"Off (Local)" = "Pois (Paikallinen)"; + +/* feature offered item */ +"offered %@" = "tarjottu %@"; + +/* feature offered item */ +"offered %@: %@" = "tarjottu %1$@: %2$@"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Old database" = "Vanha tietokanta"; + +/* No comment provided by engineer. */ +"Old database archive" = "Vanha tietokanta-arkisto"; + +/* group pref value */ +"on" = "päällä"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "Kertakutsulinkki"; + +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Onion-isäntiä ei käytetä."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**."; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Vain ryhmän omistajat voivat sallia tiedostoja ja mediaa."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Vain ryhmän omistajat voivat ottaa ääniviestit käyttöön."; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Vain sinä voit lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi)."; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Vain sinä voit soittaa puheluita."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Vain sinä voit lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Vain sinä voit lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Vain kontaktisi voi lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi)."; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Vain kontaktisi voi soittaa puheluita."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Vain kontaktisi voi lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Vain kontaktisi voi lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"Open chat" = "Avaa keskustelu"; + +/* authentication reason */ +"Open chat console" = "Avaa keskustelukonsoli"; + +/* No comment provided by engineer. */ +"Open Settings" = "Avaa Asetukset"; + +/* authentication reason */ +"Open user profiles" = "Avaa käyttäjäprofiilit"; + +/* No comment provided by engineer. */ +"Open-source protocol and code – anybody can run the servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; + +/* No comment provided by engineer. */ +"Opening database…" = "Avataan tietokantaa…"; + +/* No comment provided by engineer. */ +"Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "Linkin avaaminen selaimessa voi heikentää yhteyden yksityisyyttä ja turvallisuutta. Epäluotetut SimpleX-linkit näkyvät punaisina."; + +/* No comment provided by engineer. */ +"or chat with the developers" = "tai keskustele kehittäjien kanssa"; + +/* member role */ +"owner" = "omistaja"; + +/* No comment provided by engineer. */ +"Passcode" = "Pääsykoodi"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "Pääsykoodi vaihdettu!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Pääsykoodin syöttö"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "Pääsykoodia ei ole muutettu!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "Pääsykoodi asetettu!"; + +/* No comment provided by engineer. */ +"Password to show" = "Salasana näytettäväksi"; + +/* No comment provided by engineer. */ +"Paste" = "Liitä"; + +/* No comment provided by engineer. */ +"Paste image" = "Liitä kuva"; + +/* No comment provided by engineer. */ +"Paste received link" = "Liitä vastaanotettu linkki"; + +/* placeholder */ +"Paste the link you received to connect with your contact." = "Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi."; + +/* No comment provided by engineer. */ +"peer-to-peer" = "vertais"; + +/* No comment provided by engineer. */ +"People can connect to you only via the links you share." = "Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta."; + +/* No comment provided by engineer. */ +"Periodically" = "Ajoittain"; + +/* message decrypt error item */ +"Permanent decryption error" = "Pysyvä salauksen purkuvirhe"; + +/* No comment provided by engineer. */ +"PING count" = "PING-määrä"; + +/* No comment provided by engineer. */ +"PING interval" = "PING-väli"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Pyydä kontaktiasi sallimaan ääniviestien lähettäminen."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Tarkista verkkoyhteytesi %@:lla ja yritä uudelleen."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Tarkista omasi ja kontaktin asetukset."; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Ota yhteyttä ryhmän ylläpitäjään."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Anna oikea nykyinen tunnuslause."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Anna edellinen salasana tietokannan varmuuskopion palauttamisen jälkeen. Tätä toimintoa ei voi kumota."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Muista tai säilytä se turvallisesti - kadonnutta pääsykoodia ei voi palauttaa!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Ilmoita siitä kehittäjille."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Käynnistä sovellus uudelleen ja siirrä tietokanta push-ilmoitusten ottamiseksi käyttöön."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Säilytä tunnuslause turvallisesti, ET pääse keskusteluihin, jos kadotat sen."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen."; + +/* No comment provided by engineer. */ +"Polish interface" = "Puolalainen käyttöliittymä"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; + +/* No comment provided by engineer. */ +"Preset server" = "Esiasetettu palvelin"; + +/* No comment provided by engineer. */ +"Preset server address" = "Esiasetettu palvelimen osoite"; + +/* No comment provided by engineer. */ +"Preview" = "Esikatselu"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Yksityisyys ja turvallisuus"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "Yksityisyys uudelleen määritettynä"; + +/* No comment provided by engineer. */ +"Private filenames" = "Yksityiset tiedostonimet"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "Profiili- ja palvelinyhteydet"; + +/* No comment provided by engineer. */ +"Profile image" = "Profiilikuva"; + +/* No comment provided by engineer. */ +"Profile password" = "Profiilin salasana"; + +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "Profiilipäivitys lähetetään kontakteillesi."; + +/* No comment provided by engineer. */ +"Prohibit audio/video calls." = "Estä ääni- ja videopuhelut."; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "Estä peruuttamaton viestien poistaminen."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "Estä viestireaktiot."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "Estä viestireaktiot."; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Estä suorien viestien lähettäminen jäsenille."; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "Estä katoavien viestien lähettäminen."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Estä tiedostojen ja median lähettäminen."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "Estä ääniviestien lähettäminen."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Suojaa sovellusnäyttö"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Suojaa keskusteluprofiilisi salasanalla!"; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Protokollan aikakatkaisu"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Protokollan aikakatkaisu per KB"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push-ilmoitukset"; + +/* No comment provided by engineer. */ +"Rate the app" = "Arvioi sovellus"; + +/* chat item menu */ +"React…" = "Reagoi…"; + +/* No comment provided by engineer. */ +"Read" = "Lue"; + +/* No comment provided by engineer. */ +"Read more" = "Lue lisää"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; + +/* No comment provided by engineer. */ +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme)."; + +/* No comment provided by engineer. */ +"Read more in our GitHub repository." = "Lue lisää GitHub-tietovarastostamme."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "Kuittaukset pois käytöstä"; + +/* No comment provided by engineer. */ +"received answer…" = "vastaus saatu…"; + +/* No comment provided by engineer. */ +"Received at" = "Vastaanotettu klo"; + +/* copied message info */ +"Received at: %@" = "Vastaanotettu klo: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "vahvistus saatu…"; + +/* notification */ +"Received file event" = "Tiedoston vastaanottotapahtuma"; + +/* message info title */ +"Received message" = "Vastaanotettu viesti"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "Tiedoston vastaanotto pysäytetään."; + +/* No comment provided by engineer. */ +"Receiving via" = "Vastaanotto kautta"; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "Vastaanottajat näkevät päivitykset, kun kirjoitat niitä."; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Yhdistä kaikki yhdistetyt palvelimet uudelleen pakottaaksesi viestin toimituksen. Tämä käyttää ylimääräistä liikennettä."; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Yhdistä palvelimet uudelleen?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Tietue päivitetty klo"; + +/* copied message info */ +"Record updated at: %@" = "Tietue päivitetty klo: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Pienempi akun käyttö"; + +/* reject incoming call via notification */ +"Reject" = "Hylkää"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Hylkää (lähettäjälle EI ilmoiteta)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Hylkää yhteyspyyntö"; + +/* call status */ +"rejected call" = "hylätty puhelu"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Välityspalvelinta käytetään vain tarvittaessa. Toinen osapuoli voi tarkkailla IP-osoitettasi."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa."; + +/* No comment provided by engineer. */ +"Remove" = "Poista"; + +/* No comment provided by engineer. */ +"Remove member" = "Poista jäsen"; + +/* No comment provided by engineer. */ +"Remove member?" = "Poista jäsen?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Poista tunnuslause avainnipusta?"; + +/* No comment provided by engineer. */ +"removed" = "poistettu"; + +/* rcv group event chat item */ +"removed %@" = "%@ poistettu"; + +/* rcv group event chat item */ +"removed you" = "poisti sinut"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Neuvottele uudelleen"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Uudelleenneuvottele salaus"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Uudelleenneuvottele salaus?"; + +/* chat item action */ +"Reply" = "Vastaa"; + +/* No comment provided by engineer. */ +"Required" = "Pakollinen"; + +/* No comment provided by engineer. */ +"Reset" = "Oletustilaan"; + +/* No comment provided by engineer. */ +"Reset colors" = "Oletusvärit"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Palauta oletusasetukset"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Käynnistä sovellus uudelleen käyttääksesi tuotua keskustelujen-tietokantaa"; + +/* No comment provided by engineer. */ +"Restore" = "Palauta"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Palauta tietokannan varmuuskopio"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Palauta tietokannan varmuuskopio?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Virhe tietokannan palauttamisessa"; + +/* chat item action */ +"Reveal" = "Paljasta"; + +/* No comment provided by engineer. */ +"Revert" = "Palauta"; + +/* No comment provided by engineer. */ +"Revoke" = "Peruuta"; + +/* cancel file action */ +"Revoke file" = "Peruuta tiedosto"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Peruuta tiedosto?"; + +/* No comment provided by engineer. */ +"Role" = "Rooli"; + +/* No comment provided by engineer. */ +"Run chat" = "Käynnistä chat"; + +/* chat item action */ +"Save" = "Tallenna"; + +/* No comment provided by engineer. */ +"Save (and notify contacts)" = "Tallenna (ja ilmoita kontakteille)"; + +/* No comment provided by engineer. */ +"Save and notify contact" = "Tallenna ja ilmoita kontaktille"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "Tallenna ja ilmoita ryhmän jäsenille"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Tallenna ja päivitä ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Save archive" = "Tallenna arkisto"; + +/* No comment provided by engineer. */ +"Save auto-accept settings" = "Tallenna automaattisen hyväksynnän asetukset"; + +/* No comment provided by engineer. */ +"Save group profile" = "Tallenna ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Tallenna tunnuslause ja avaa keskustelu"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Tallenna tunnuslause Avainnippuun"; + +/* No comment provided by engineer. */ +"Save preferences?" = "Tallenna asetukset?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Tallenna profiilin salasana"; + +/* No comment provided by engineer. */ +"Save servers" = "Tallenna palvelimet"; + +/* No comment provided by engineer. */ +"Save servers?" = "Tallenna palvelimet?"; + +/* No comment provided by engineer. */ +"Save settings?" = "Tallenna asetukset?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Tallenna tervetuloviesti?"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "Tallennetut WebRTC ICE -palvelimet poistetaan"; + +/* No comment provided by engineer. */ +"Scan code" = "Skannaa koodi"; + +/* No comment provided by engineer. */ +"Scan QR code" = "Skannaa QR-koodi"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Skannaa turvakoodi kontaktisi sovelluksesta."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "Skannaa palvelimen QR-koodi"; + +/* No comment provided by engineer. */ +"Search" = "Haku"; + +/* network option */ +"sec" = "sek"; + +/* time unit */ +"seconds" = "sekuntia"; + +/* No comment provided by engineer. */ +"secret" = "salainen"; + +/* server test step */ +"Secure queue" = "Turvallinen jono"; + +/* No comment provided by engineer. */ +"Security assessment" = "Turvallisuusarviointi"; + +/* No comment provided by engineer. */ +"Security code" = "Turvakoodi"; + +/* chat item text */ +"security code changed" = "turvakoodi on muuttunut"; + +/* No comment provided by engineer. */ +"Select" = "Valitse"; + +/* No comment provided by engineer. */ +"Self-destruct" = "Itsetuho"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Itsetuhoutuva pääsykoodi"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Itsetuhoutuva pääsykoodi vaihdettu!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Itsetuhoutuva pääsykoodi käytössä!"; + +/* No comment provided by engineer. */ +"Send" = "Lähetä"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Lähetä live-viesti - se päivittyy vastaanottajille, kun kirjoitat sitä"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Lähetä toimituskuittaukset vastaanottajalle"; + +/* No comment provided by engineer. */ +"Send direct message" = "Lähetä yksityisviesti"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Lähetä katoava viesti"; + +/* No comment provided by engineer. */ +"Send link previews" = "Lähetä linkkien esikatselu"; + +/* No comment provided by engineer. */ +"Send live message" = "Lähetä live-viesti"; + +/* No comment provided by engineer. */ +"Send notifications" = "Lähetys ilmoitukset"; + +/* No comment provided by engineer. */ +"Send notifications:" = "Lähetys ilmoitukset:"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Lähetä kysymyksiä ja ideoita"; + +/* No comment provided by engineer. */ +"Send receipts" = "Lähetä kuittaukset"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Lähetä ne galleriasta tai mukautetuista näppäimistöistä."; + +/* No comment provided by engineer. */ +"Sender cancelled file transfer." = "Lähettäjä peruutti tiedoston siirron."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "Lähettäjä on saattanut poistaa yhteyspyynnön."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille näkyvissä keskusteluprofiileissa."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "Tiedoston lähettäminen lopetetaan."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "Kuittauksien lähettäminen ei ole käytössä %lld kontakteille"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Kuittien lähettäminen ei ole käytössä %lld ryhmille"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Kuittauksien lähettäminen on käytössä %lld kontakteille"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Kuittauksien lähettäminen on käytössä %lld ryhmille"; + +/* No comment provided by engineer. */ +"Sending via" = "Lähetetään kautta"; + +/* No comment provided by engineer. */ +"Sent at" = "Lähetetty klo"; + +/* copied message info */ +"Sent at: %@" = "Lähetetty klo: %@"; + +/* notification */ +"Sent file event" = "Lähetetty tiedosto tapahtuma"; + +/* message info title */ +"Sent message" = "Lähetetty viesti"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Lähetetyt viestit poistetaan asetetun ajan kuluttua."; + +/* server test error */ +"Server requires authorization to create queues, check password" = "Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana"; + +/* server test error */ +"Server requires authorization to upload, check password" = "Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Palvelintesti epäonnistui!"; + +/* No comment provided by engineer. */ +"Servers" = "Palvelimet"; + +/* No comment provided by engineer. */ +"Set 1 day" = "Aseta 1 päivä"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Aseta kontaktin nimi…"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Aseta ryhmän asetukset"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Aseta se järjestelmän todennuksen sijaan."; + +/* No comment provided by engineer. */ +"Set passcode" = "Aseta pääsykoodi"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Aseta tunnuslause vientiä varten"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Aseta uusille jäsenille näytettävä viesti!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Aseta aikakatkaisut välityspalvelimelle/VPN:lle"; + +/* No comment provided by engineer. */ +"Settings" = "Asetukset"; + +/* chat item action */ +"Share" = "Jaa"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Jaa kertakäyttölinkki"; + +/* No comment provided by engineer. */ +"Share address" = "Jaa osoite"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "Jaa osoite kontakteille?"; + +/* No comment provided by engineer. */ +"Share link" = "Jaa linkki"; + +/* No comment provided by engineer. */ +"Share one-time invitation link" = "Jaa kertakutsulinkki"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Jaa kontaktien kanssa"; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Näytä puhelut puhelinhistoriassa"; + +/* No comment provided by engineer. */ +"Show developer options" = "Näytä kehittäjävaihtoehdot"; + +/* No comment provided by engineer. */ +"Show last messages" = "Näytä viimeiset viestit"; + +/* No comment provided by engineer. */ +"Show preview" = "Näytä esikatselu"; + +/* No comment provided by engineer. */ +"Show:" = "Näytä:"; + +/* No comment provided by engineer. */ +"SimpleX address" = "SimpleX-osoite"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "SimpleX-osoite"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "Trail of Bits on tarkastanut SimpleX Chatin tietoturvan."; + +/* simplex link type */ +"SimpleX contact address" = "SimpleX-yhteystiedot"; + +/* notification */ +"SimpleX encrypted message or connection event" = "SimpleX-salattu viesti tai yhteystapahtuma"; + +/* simplex link type */ +"SimpleX group link" = "SimpleX-ryhmän linkki"; + +/* No comment provided by engineer. */ +"SimpleX links" = "SimpleX-linkit"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX Lock"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "SimpleX Lock -tila"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "SimpleX Lock ei ole käytössä!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX Lock päällä"; + +/* simplex link type */ +"SimpleX one-time invitation" = "SimpleX-kertakutsu"; + +/* No comment provided by engineer. */ +"Skip" = "Ohita"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Ohitetut viestit"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Pienryhmät (max 20)"; + +/* No comment provided by engineer. */ +"SMP servers" = "SMP-palvelimet"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja."; + +/* notification title */ +"Somebody" = "Joku"; + +/* No comment provided by engineer. */ +"Start a new chat" = "Aloita uusi keskustelu"; + +/* No comment provided by engineer. */ +"Start chat" = "Aloita keskustelu"; + +/* No comment provided by engineer. */ +"Start migration" = "Aloita siirto"; + +/* No comment provided by engineer. */ +"starting…" = "alkaa…"; + +/* No comment provided by engineer. */ +"Stop" = "Lopeta"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "Pysäytä keskustelu tietokantatoimien mahdollistamiseksi"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Lopeta keskustelu?"; + +/* cancel file action */ +"Stop file" = "Pysäytä tiedosto"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Lopeta tiedoston vastaanottaminen?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Lopeta tiedoston lähettäminen?"; + +/* No comment provided by engineer. */ +"Stop sharing" = "Lopeta jakaminen"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "Lopeta osoitteen jakaminen?"; + +/* authentication reason */ +"Stop SimpleX" = "Lopeta SimpleX"; + +/* No comment provided by engineer. */ +"strike" = "soita"; + +/* No comment provided by engineer. */ +"Submit" = "Lähetä"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "SimpleX Chat tuki"; + +/* No comment provided by engineer. */ +"System" = "Järjestelmä"; + +/* No comment provided by engineer. */ +"System authentication" = "Järjestelmän todennus"; + +/* No comment provided by engineer. */ +"Take picture" = "Ota kuva"; + +/* No comment provided by engineer. */ +"Tap button " = "Napauta painiketta "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Aktivoi profiili napauttamalla."; + +/* No comment provided by engineer. */ +"Tap to join" = "Liity napauttamalla"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "Napauta liittyäksesi incognito-tilassa"; + +/* No comment provided by engineer. */ +"Tap to start a new chat" = "Aloita uusi keskustelu napauttamalla"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "TCP-yhteyden aikakatkaisu"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* server test failure */ +"Test failed at step %@." = "Testi epäonnistui vaiheessa %@."; + +/* No comment provided by engineer. */ +"Test server" = "Testipalvelin"; + +/* No comment provided by engineer. */ +"Test servers" = "Testipalvelimet"; + +/* No comment provided by engineer. */ +"Tests failed!" = "Testit epäonnistuivat!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "Kiitos SimpleX Chatin asentamisesta!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Kiitos käyttäjille - [osallistu Weblaten avulla](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Kiitokset käyttäjille – osallistu Weblaten kautta!"; + +/* No comment provided by engineer. */ +"The 1st platform without any user identifiers – private by design." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Hyväksymäsi yhteys peruuntuu!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "Kontakti, jolle jaoit tämän linkin, EI voi muodostaa yhteyttä!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Luotu arkisto on käytettävissä sovelluksen Asetukset / Tietokanta / Vanha tietokanta-arkisto kautta."; + +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!"; + +/* No comment provided by engineer. */ +"The group is fully decentralized – it is visible only to the members." = "Ryhmä on täysin hajautettu - se näkyy vain jäsenille."; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "Edellisen viestin tarkiste on erilainen."; + +/* No comment provided by engineer. */ +"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." = "Seuraavan viestin tunnus on väärä (pienempi tai yhtä suuri kuin edellisen).\nTämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Viesti poistetaan kaikilta jäseniltä."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Viesti merkitään moderoiduksi kaikille jäsenille."; + +/* No comment provided by engineer. */ +"The next generation of private messaging" = "Seuraavan sukupolven yksityisviestit"; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; + +/* No comment provided by engineer. */ +"The profile is only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "Lähettäjälle EI ilmoiteta"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**."; + +/* No comment provided by engineer. */ +"Theme" = "Teema"; + +/* No comment provided by engineer. */ +"There should be at least one user profile." = "Käyttäjäprofiileja tulee olla vähintään yksi."; + +/* No comment provided by engineer. */ +"There should be at least one visible user profile." = "Näkyviä käyttäjäprofiileja tulee olla vähintään yksi."; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Nämä asetukset koskevat nykyistä profiiliasi **%@**."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa."; + +/* No comment provided by engineer. */ +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Tätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät."; + +/* No comment provided by engineer. */ +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti."; + +/* notification title */ +"this contact" = "tämä kontakti"; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Tätä ryhmää ei enää ole olemassa."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**."; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Voit esittää kysymyksiä ja saada päivityksiä:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "Kontaktisi voi muodostaa yhteyden skannaamalla QR-koodin tai käyttämällä sovelluksessa olevaa linkkiä."; + +/* No comment provided by engineer. */ +"To make a new connection" = "Uuden yhteyden luominen"; + +/* No comment provided by engineer. */ +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Suojaa tietosi ottamalla SimpleX Lock käyttöön.\nSinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla."; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi."; + +/* No comment provided by engineer. */ +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia."; + +/* No comment provided by engineer. */ +"Transport isolation" = "Kuljetuksen eristäminen"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta."; + +/* No comment provided by engineer. */ +"Turn off" = "Sammuta"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "Kytke ilmoitukset pois päältä?"; + +/* No comment provided by engineer. */ +"Turn on" = "Kytke päälle"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Ääniviestiä ei voi tallentaa"; + +/* item status description */ +"Unexpected error: %@" = "Odottamaton virhe: %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Odottamaton siirtotila"; + +/* No comment provided by engineer. */ +"Unfav." = "Epäsuotuisa."; + +/* No comment provided by engineer. */ +"Unhide" = "Näytä"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Näytä keskusteluprofiili"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Näytä profiili"; + +/* No comment provided by engineer. */ +"Unit" = "Yksikkö"; + +/* connection info */ +"unknown" = "tuntematon"; + +/* callkit banner */ +"Unknown caller" = "Tuntematon soittaja"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Tuntematon tietokantavirhe: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Tuntematon virhe"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi."; + +/* No comment provided by engineer. */ +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä.\nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa."; + +/* No comment provided by engineer. */ +"Unlock" = "Avaa"; + +/* authentication reason */ +"Unlock app" = "Avaa sovellus"; + +/* No comment provided by engineer. */ +"Unmute" = "Poista mykistys"; + +/* No comment provided by engineer. */ +"Unread" = "Lukematon"; + +/* No comment provided by engineer. */ +"Update" = "Päivitä"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "Päivitä .onion-isäntien asetus?"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Päivitä tietokannan tunnuslause"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Päivitä verkkoasetukset?"; + +/* No comment provided by engineer. */ +"Update transport isolation mode?" = "Päivitä kuljetuksen eristystila?"; + +/* rcv group event chat item */ +"updated group profile" = "päivitetty ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Päivitä ja avaa keskustelu"; + +/* server test step */ +"Upload file" = "Lataa tiedosto"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Käytä .onion-isäntiä"; + +/* No comment provided by engineer. */ +"Use chat" = "Käytä chattia"; + +/* No comment provided by engineer. */ +"Use current profile" = "Käytä nykyistä profiilia"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Käytä uusiin yhteyksiin"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "Käytä iOS:n puhelujen käyttöliittymää"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Käytä uutta incognito-profiilia"; + +/* No comment provided by engineer. */ +"Use server" = "Käytä palvelinta"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "Käytä SimpleX Chat palvelimia?"; + +/* No comment provided by engineer. */ +"User profile" = "Käyttäjäprofiili"; + +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = ".onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan."; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Käyttää SimpleX Chat -palvelimia."; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Tarkista yhteyden suojaus"; + +/* No comment provided by engineer. */ +"Verify security code" = "Tarkista turvakoodi"; + +/* No comment provided by engineer. */ +"Via browser" = "Selaimella"; + +/* chat list item description */ +"via contact address link" = "kontaktiosoitelinkillä"; + +/* chat list item description */ +"via group link" = "ryhmälinkillä"; + +/* chat list item description */ +"via one-time link" = "kertalinkillä"; + +/* No comment provided by engineer. */ +"via relay" = "releellä"; + +/* No comment provided by engineer. */ +"Video call" = "Videopuhelu"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "videopuhelu (ei e2e-salattu)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "Video vastaanotetaan, kun kontaktisi on ladannut sen."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "Video vastaanotetaan, kun kontaktisi on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "Videot ja tiedostot 1 Gt asti"; + +/* No comment provided by engineer. */ +"View security code" = "Näytä turvakoodi"; + +/* No comment provided by engineer. */ +"Voice message…" = "Ääniviesti…"; + +/* chat feature */ +"Voice messages" = "Ääniviestit"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "Ääniviestit ovat kiellettyjä tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this group." = "Ääniviestit ovat kiellettyjä tässä ryhmässä."; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Ääniviestit kielletty!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "odottaa vastaamista…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "odottaa vahvistusta…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "Odottaa tiedostoa"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Odottaa kuvaa"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Odottaa videota"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "haluaa olla yhteydessä sinuun!"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Varoitus: saatat menettää joitain tietoja!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "WebRTC ICE -palvelimet"; + +/* time unit */ +"weeks" = "viikkoa"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Tervetuloa %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Tervetuloviesti"; + +/* No comment provided by engineer. */ +"What's new" = "Uusimmat"; + +/* No comment provided by engineer. */ +"When available" = "Kun saatavilla"; + +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen."; + +/* No comment provided by engineer. */ +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "Valinnaisella tervetuloviestillä."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Väärä tietokannan tunnuslause"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Väärä tunnuslause!"; + +/* No comment provided by engineer. */ +"XFTP servers" = "XFTP-palvelimet"; + +/* pref value */ +"yes" = "kyllä"; + +/* No comment provided by engineer. */ +"You" = "Sinä"; + +/* No comment provided by engineer. */ +"You accepted connection" = "Hyväksyit yhteyden"; + +/* No comment provided by engineer. */ +"You allow" = "Sallit"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Sinulla on jo keskusteluprofiili samalla näyttönimellä. Valitse toinen nimi."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Olet jo muodostanut yhteyden %@:n kanssa."; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta."; + +/* No comment provided by engineer. */ +"you are invited to group" = "sinut on kutsuttu ryhmään"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Sinut on kutsuttu ryhmään"; + +/* No comment provided by engineer. */ +"you are observer" = "olet tarkkailija"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta."; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta."; + +/* No comment provided by engineer. */ +"You can create it later" = "Voit luoda sen myöhemmin"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Voit ottaa käyttöön myöhemmin asetusten kautta"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; + +/* notification body */ +"You can now send messages to %@" = "Voit nyt lähettää viestejä %@:lle"; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista."; + +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Voit jakaa linkin tai QR-koodin - kuka tahansa voi liittyä ryhmään. Et menetä ryhmän jäseniä, jos poistat sen myöhemmin."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa."; + +/* No comment provided by engineer. */ +"You can share your address as a link or QR code - anybody can connect to you." = "Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen"; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "Voit ottaa SimpleX Lockin käyttöön Asetusten kautta."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Voit käyttää markdownia viestien muotoiluun:"; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Et voi lähettää viestejä!"; + +/* chat item text */ +"you changed address" = "muutit osoitetta"; + +/* chat item text */ +"you changed address for %@" = "muutit osoitetta %@:ksi"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "vaihdoit roolin itsellesi %@:ksi"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "olet vaihtanut %1$@:n roolin %2$@:ksi"; + +/* No comment provided by engineer. */ +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Sinua ei voitu todentaa; yritä uudelleen."; + +/* No comment provided by engineer. */ +"You have no chats" = "Sinulla ei ole keskusteluja"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Kutsuit kontaktin"; + +/* No comment provided by engineer. */ +"You joined this group" = "Liityit tähän ryhmään"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Liityit tähän ryhmään. Muodostetaan yhteyttä ryhmän jäsenten kutsumiseksi."; + +/* snd group event chat item */ +"you left" = "lähdit"; + +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Sinun tulee käyttää keskustelujen-tietokannan uusinta versiota AINOSTAAN yhdessä laitteessa, muuten saatat lakata vastaanottamasta viestejä joiltakin kontakteilta."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Hylkäsit ryhmäkutsun"; + +/* snd group event chat item */ +"you removed %@" = "poistit %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Lähetit ryhmäkutsun"; + +/* chat list item description */ +"you shared one-time link" = "jaoit kertalinkin"; + +/* chat list item description */ +"you shared one-time link incognito" = "jaoit kertalinkin incognito-tilassa"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Sinut yhdistetään, kun yhteyspyyntösi on hyväksytty, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Sinut yhdistetään, kun kontaktisi laite on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen."; + +/* No comment provided by engineer. */ +"You will join a group this link refers to and connect to its group members." = "Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin."; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "Et enää saa viestejä tästä ryhmästä. Keskusteluhistoria säilytetään."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Et menetä kontaktejasi, jos poistat osoitteesi myöhemmin."; + +/* No comment provided by engineer. */ +"you: " = "sinä: "; + +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Yrität kutsua kontaktia, jonka kanssa olet jakanut inkognito-profiilin, ryhmään, jossa käytät pääprofiiliasi"; + +/* No comment provided by engineer. */ +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi"; + +/* No comment provided by engineer. */ +"Your %@ servers" = "%@-palvelimesi"; + +/* No comment provided by engineer. */ +"Your calls" = "Puhelusi"; + +/* No comment provided by engineer. */ +"Your chat database" = "Keskustelut-tietokantasi"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi."; + +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "Keskusteluprofiilisi lähetetään ryhmän jäsenille"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Keskusteluprofiilisi"; + +/* No comment provided by engineer. */ +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa.\nVoit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä)."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@)."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "Kontaktisi voivat sallia viestien täydellisen poistamisen."; + +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Kontaktisi SimpleX:ssä näkevät sen.\nVoit muuttaa sitä Asetuksista."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "Kontaktisi pysyvät yhdistettyinä."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla."; + +/* No comment provided by engineer. */ +"Your current profile" = "Nykyinen profiilisi"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "ICE-palvelimesi"; + +/* No comment provided by engineer. */ +"Your preferences" = "Asetuksesi"; + +/* No comment provided by engineer. */ +"Your privacy" = "Yksityisyytesi"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Profiilisi **%@** jaetaan."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa.\nSimpleX-palvelimet eivät näe profiiliasi."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi."; + +/* No comment provided by engineer. */ +"Your random profile" = "Satunnainen profiilisi"; + +/* No comment provided by engineer. */ +"Your server" = "Palvelimesi"; + +/* No comment provided by engineer. */ +"Your server address" = "Palvelimesi osoite"; + +/* No comment provided by engineer. */ +"Your settings" = "Asetuksesi"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "SimpleX-osoitteesi"; + +/* No comment provided by engineer. */ +"Your SMP servers" = "SMP-palvelimesi"; + +/* No comment provided by engineer. */ +"Your XFTP servers" = "XFTP-palvelimesi"; + diff --git a/apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..969e43e449 --- /dev/null +++ b/apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,15 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX tarvitsee pääsyn kameraan, jotta se voi skannata QR-koodeja muodostaakseen yhteyden muihin käyttäjiin ja videopuheluita varten."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX käyttää Face ID:tä paikalliseen todennukseen"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX tarvitsee pääsyn valokuvakirjastoon kuvattujen ja vastaanotettujen medioiden tallentamista varten"; + diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..a9213527c6 --- /dev/null +++ b/apps/ios/uk.lproj/Localizable.strings @@ -0,0 +1,3675 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" (" = " ("; + +/* No comment provided by engineer. */ +" (can be copied)" = " (можна скопіювати)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_курсив_"; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- стабільніша доставка повідомлень.\n- трохи кращі групи.\n- і багато іншого!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- голосові повідомлення до 5 хвилин.\n- користувальницький час зникнення.\n- історія редагування."; + +/* No comment provided by engineer. */ +", " = ", "; + +/* No comment provided by engineer. */ +": " = ": "; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 кольоровий!"; + +/* No comment provided by engineer. */ +"." = "."; + +/* No comment provided by engineer. */ +"(" = "("; + +/* No comment provided by engineer. */ +")" = ")"; + +/* No comment provided by engineer. */ +"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Внесок](https://github.com/simplex-chat/simplex-chat#contribute)"; + +/* No comment provided by engineer. */ +"[Send us email](mailto:chat@simplex.chat)" = "[Напишіть нам електронною поштою](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту."; + +/* No comment provided by engineer. */ +"**Create link / QR code** for your contact to use." = "**Створіть посилання / QR-код** для використання вашим контактом."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**e2e encrypted** аудіодзвінок"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**e2e encrypted** відеодзвінок"; + +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте."; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком)."; + +/* No comment provided by engineer. */ +"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставте отримане посилання** або відкрийте його в браузері і натисніть **Відкрити в мобільному додатку**."; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його."; + +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло."; + +/* No comment provided by engineer. */ +"**Scan QR code**: to connect to your contact in person or via video call." = "**Відскануйте QR-код**: щоб з'єднатися з вашим контактом особисто або за допомогою відеодзвінка."; + +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*жирний*"; + +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Історія"; + +/* copied message info */ +"## In reply to" = "## У відповідь на"; + +/* No comment provided by engineer. */ +"#secret#" = "#секрет#"; + +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (поточний)"; + +/* copied message info */ +"%@ (current):" = "%@ (поточний):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / %@"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ %@"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ і %@ підключено"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ за %2$@:"; + +/* notification title */ +"%@ is connected!" = "%@ підключено!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ не перевірено"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ перевірено"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ сервери"; + +/* notification title */ +"%@ wants to connect!" = "%@ хоче підключитися!"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ та %lld інші підключені учасники"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d днів"; + +/* time interval */ +"%d hours" = "%d годин"; + +/* time interval */ +"%d min" = "%d хв"; + +/* time interval */ +"%d months" = "%d місяців"; + +/* time interval */ +"%d sec" = "%d сек"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d пропущено повідомлення(ь)"; + +/* time interval */ +"%d weeks" = "%d тижнів"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld контакт(и) вибрані"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld файл(и) загальним розміром %@"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld учасників"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld хвилин"; + +/* No comment provided by engineer. */ +"%lld second(s)" = "%lld секунд(и)"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld секунд"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldd"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldh"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldm"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldmth"; + +/* No comment provided by engineer. */ +"%llds" = "%llds"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldw"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "%u повідомлень не вдалося розшифрувати."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u повідомлень пропущено."; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Привіт!</p>\n<p><a href=\"%@\"> Зв'яжіться зі мною через SimpleX Chat</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~закреслити~"; + +/* No comment provided by engineer. */ +"0s" = "0с"; + +/* time interval */ +"1 day" = "1 день"; + +/* time interval */ +"1 hour" = "1 година"; + +/* No comment provided by engineer. */ +"1 minute" = "1 хвилина"; + +/* time interval */ +"1 month" = "1 місяць"; + +/* time interval */ +"1 week" = "1 тиждень"; + +/* No comment provided by engineer. */ +"1-time link" = "1-разове посилання"; + +/* No comment provided by engineer. */ +"5 minutes" = "5 хвилин"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 секунд"; + +/* No comment provided by engineer. */ +"A few more things" = "Ще кілька речей"; + +/* notification title */ +"A new contact" = "Новий контакт"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Буде створено новий випадковий профіль."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Для кожного профілю чату, який ви маєте в додатку, буде використовуватися окреме TCP-з'єднання."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Для кожного контакту та учасника групи буде використовуватися окреме TCP-з'єднання.\n**Зверніть увагу: якщо у вас багато з'єднань, споживання заряду акумулятора і трафіку може бути значно вищим, а деякі з'єднання можуть обірватися."; + +/* No comment provided by engineer. */ +"Abort" = "Скасувати"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Скасувати зміну адреси"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Скасувати зміну адреси?"; + +/* No comment provided by engineer. */ +"About SimpleX" = "Про SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX address" = "Про адресу SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "Про чат SimpleX"; + +/* No comment provided by engineer. */ +"above, then choose:" = "вище, а потім обирайте:"; + +/* No comment provided by engineer. */ +"Accent color" = "Акцентний колір"; + +/* accept contact request via notification + accept incoming call via notification */ +"Accept" = "Прийняти"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Прийняти запит на підключення?"; + +/* notification body */ +"Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; + +/* accept contact request via notification */ +"Accept incognito" = "Прийняти інкогніто"; + +/* call status */ +"accepted call" = "прийнято виклик"; + +/* No comment provided by engineer. */ +"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам."; + +/* No comment provided by engineer. */ +"Add preset servers" = "Додавання попередньо встановлених серверів"; + +/* No comment provided by engineer. */ +"Add profile" = "Додати профіль"; + +/* No comment provided by engineer. */ +"Add server…" = "Додати сервер…"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Додайте сервери, відсканувавши QR-код."; + +/* No comment provided by engineer. */ +"Add to another device" = "Додати до іншого пристрою"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Додати вітальне повідомлення"; + +/* No comment provided by engineer. */ +"Address" = "Адреса"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "Зміна адреси буде скасована. Буде використано стару адресу отримання."; + +/* member role */ +"admin" = "адмін"; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Адміни можуть створювати посилання для приєднання до груп."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Розширені налаштування мережі"; + +/* chat item text */ +"agreeing encryption for %@…" = "узгодження шифрування для %@…"; + +/* chat item text */ +"agreeing encryption…" = "узгодження шифрування…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Всі дані програми видаляються."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Всі чати та повідомлення будуть видалені - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "Всі дані стираються при введенні."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Всі учасники групи залишаться на зв'язку."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Всі ваші контакти залишаться на зв'язку."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Всі ваші контакти залишаться на зв'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам."; + +/* No comment provided by engineer. */ +"Allow" = "Дозволити"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх."; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you." = "Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити."; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Дозволити реакцію на повідомлення."; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "Дозволяє надсилати прямі повідомлення користувачам."; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "Дозволити надсилання зникаючих повідомлень."; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages." = "Дозволяє безповоротно видаляти надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "Дозволяє надсилати файли та медіа."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "Дозволити надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "Дозволяйте голосові повідомлення, тільки якщо ваш контакт дозволяє їх."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Дозволити голосові повідомлення?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "Дозвольте вашим контактам додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "Дозвольте вашим контактам телефонувати вам."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages." = "Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Дозвольте своїм контактам надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Дозвольте своїм контактам надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Already connected?" = "Вже підключено?"; + +/* pref value */ +"always" = "завжди"; + +/* No comment provided by engineer. */ +"Always use relay" = "Завжди використовуйте реле"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі."; + +/* No comment provided by engineer. */ +"Answer call" = "Відповісти на дзвінок"; + +/* No comment provided by engineer. */ +"App build: %@" = "Збірка програми: %@"; + +/* No comment provided by engineer. */ +"App icon" = "Іконка програми"; + +/* No comment provided by engineer. */ +"App passcode" = "Пароль додатку"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Пароль програми замінено на пароль самознищення."; + +/* No comment provided by engineer. */ +"App version" = "Версія програми"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Версія програми: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Зовнішній вигляд"; + +/* No comment provided by engineer. */ +"Attach" = "Прикріпити"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Аудіо та відео дзвінки"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Аудіо та відеодзвінки"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "аудіовиклик (без шифрування e2e)"; + +/* chat feature */ +"Audio/video calls" = "Аудіо/відео дзвінки"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "Аудіо/відео дзвінки заборонені."; + +/* PIN entry */ +"Authentication cancelled" = "Аутентифікацію скасовано"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Не вдалося пройти автентифікацію"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "Перед з'єднанням дзвінка потрібно пройти автентифікацію, але ви можете пропустити дзвінки."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Автентифікація недоступна"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Автоприйняття"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "Автоматичне прийняття запитів на контакт"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Автоматичне прийняття зображень"; + +/* No comment provided by engineer. */ +"Back" = "Назад"; + +/* integrity error chat item */ +"bad message hash" = "невірний хеш повідомлення"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Поганий хеш повідомлення"; + +/* integrity error chat item */ +"bad message ID" = "невірний ідентифікатор повідомлення"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Неправильний ідентифікатор повідомлення"; + +/* No comment provided by engineer. */ +"Better messages" = "Кращі повідомлення"; + +/* No comment provided by engineer. */ +"bold" = "жирний"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "Реакції на повідомлення можете додавати як ви, так і ваш контакт."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages." = "І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "Дзвонити можете як ви, так і ваш контакт."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "Ви і ваш контакт можете надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "Надсилати голосові повідомлення можете як ви, так і ваш контакт."; + +/* No comment provided by engineer. */ +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; + +/* No comment provided by engineer. */ +"Call already ended!" = "Дзвінок вже закінчився!"; + +/* call status */ +"call error" = "помилка дзвінка"; + +/* call status */ +"call in progress" = "виклик у процесі"; + +/* call status */ +"calling…" = "дзвоніть…"; + +/* No comment provided by engineer. */ +"Calls" = "Дзвінки"; + +/* No comment provided by engineer. */ +"Can't delete user profile!" = "Не можу видалити профіль користувача!"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Не вдається запросити контакт!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Неможливо запросити контакти!"; + +/* No comment provided by engineer. */ +"Cancel" = "Скасувати"; + +/* feature offered item */ +"cancelled %@" = "скасовано %@"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних"; + +/* No comment provided by engineer. */ +"Cannot receive file" = "Не вдається отримати файл"; + +/* No comment provided by engineer. */ +"Change" = "Зміна"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Змінити пароль до бази даних?"; + +/* authentication reason */ +"Change lock mode" = "Зміна режиму блокування"; + +/* No comment provided by engineer. */ +"Change member role?" = "Змінити роль учасника?"; + +/* authentication reason */ +"Change passcode" = "Змінити пароль"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Змінити адресу отримання"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Змінити адресу отримання?"; + +/* No comment provided by engineer. */ +"Change role" = "Змінити роль"; + +/* authentication reason */ +"Change self-destruct mode" = "Змінити режим самознищення"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "Змінити пароль самознищення"; + +/* chat item text */ +"changed address for you" = "змінили для вас адресу"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "змінено роль %1$@ на %2$@"; + +/* rcv group event chat item */ +"changed your role to %@" = "змінили свою роль на %@"; + +/* chat item text */ +"changing address for %@…" = "зміна адреси для %@…"; + +/* chat item text */ +"changing address…" = "змінює адресу…"; + +/* No comment provided by engineer. */ +"Chat archive" = "Архів чату"; + +/* No comment provided by engineer. */ +"Chat console" = "Консоль чату"; + +/* No comment provided by engineer. */ +"Chat database" = "База даних чату"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Видалено базу даних чату"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Імпорт бази даних чату"; + +/* No comment provided by engineer. */ +"Chat is running" = "Чат запущено"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Чат зупинено"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Налаштування чату"; + +/* No comment provided by engineer. */ +"Chats" = "Чати"; + +/* No comment provided by engineer. */ +"Check server address and try again." = "Перевірте адресу сервера та спробуйте ще раз."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Інтерфейс китайською та іспанською мовами"; + +/* No comment provided by engineer. */ +"Choose file" = "Виберіть файл"; + +/* No comment provided by engineer. */ +"Choose from library" = "Виберіть з бібліотеки"; + +/* No comment provided by engineer. */ +"Clear" = "Чисто"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Ясна розмова"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Відверта розмова?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Очистити перевірку"; + +/* No comment provided by engineer. */ +"colored" = "кольоровий"; + +/* No comment provided by engineer. */ +"Colors" = "Кольори"; + +/* server test step */ +"Compare file" = "Порівняти файл"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Порівняйте коди безпеки зі своїми контактами."; + +/* No comment provided by engineer. */ +"complete" = "завершено"; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "Налаштування серверів ICE"; + +/* No comment provided by engineer. */ +"Confirm" = "Підтвердити"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Підтвердити оновлення бази даних"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Підтвердіть нову парольну фразу…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Підтвердити пароль"; + +/* No comment provided by engineer. */ +"Confirm password" = "Підтвердити пароль"; + +/* server test step */ +"Connect" = "Підключіться"; + +/* No comment provided by engineer. */ +"Connect directly" = "Підключіться безпосередньо"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Підключайтеся інкогніто"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "зв'язатися з розробниками SimpleX Chat."; + +/* No comment provided by engineer. */ +"Connect via contact link" = "Підключіться за контактним посиланням"; + +/* No comment provided by engineer. */ +"Connect via group link?" = "Підключитися за груповим посиланням?"; + +/* No comment provided by engineer. */ +"Connect via link" = "Підключіться за посиланням"; + +/* No comment provided by engineer. */ +"Connect via link / QR code" = "Підключитися за посиланням / QR-кодом"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Під'єднатися за одноразовим посиланням"; + +/* No comment provided by engineer. */ +"connected" = "з'єднаний"; + +/* No comment provided by engineer. */ +"connecting" = "з'єднання"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "з'єднання (прийнято)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "з'єднання (оголошено)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "з'єднання (введено)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "з'єднання (вступне запрошення)"; + +/* call status */ +"connecting call" = "підключення дзвінка…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Підключення до сервера…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Підключення до сервера... (помилка: %@)"; + +/* chat list item title */ +"connecting…" = "з'єднання…"; + +/* No comment provided by engineer. */ +"Connection" = "Підключення"; + +/* No comment provided by engineer. */ +"Connection error" = "Помилка підключення"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "Помилка підключення (AUTH)"; + +/* chat list item title (it should not be shown */ +"connection established" = "з'єднання встановлене"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Запит на підключення відправлено!"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Тайм-аут з'єднання"; + +/* connection information */ +"connection:%@" = "з'єднання:%@"; + +/* No comment provided by engineer. */ +"Contact allows" = "Контакт дозволяє"; + +/* No comment provided by engineer. */ +"Contact already exists" = "Контакт вже існує"; + +/* No comment provided by engineer. */ +"Contact and all messages will be deleted - this cannot be undone!" = "Контакт і всі повідомлення будуть видалені - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "контакт має шифрування e2e"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "контакт не має шифрування e2e"; + +/* notification */ +"Contact hidden:" = "Контакт приховано:"; + +/* notification */ +"Contact is connected" = "Контакт підключений"; + +/* No comment provided by engineer. */ +"Contact is not connected yet!" = "Контакт ще не підключено!"; + +/* No comment provided by engineer. */ +"Contact name" = "Ім'я контактної особи"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Налаштування контактів"; + +/* No comment provided by engineer. */ +"Contacts" = "Контакти"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути."; + +/* No comment provided by engineer. */ +"Continue" = "Продовжуйте"; + +/* chat item action */ +"Copy" = "Копіювати"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Основна версія: v%@"; + +/* No comment provided by engineer. */ +"Create" = "Створити"; + +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "Створіть адресу, щоб люди могли з вами зв'язатися."; + +/* server test step */ +"Create file" = "Створити файл"; + +/* No comment provided by engineer. */ +"Create group link" = "Створити групове посилання"; + +/* No comment provided by engineer. */ +"Create link" = "Створити посилання"; + +/* No comment provided by engineer. */ +"Create one-time invitation link" = "Створіть одноразове посилання-запрошення"; + +/* server test step */ +"Create queue" = "Створити чергу"; + +/* No comment provided by engineer. */ +"Create secret group" = "Створити секретну групу"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "Створіть адресу SimpleX"; + +/* No comment provided by engineer. */ +"Create your profile" = "Створіть свій профіль"; + +/* No comment provided by engineer. */ +"Created on %@" = "Створено %@"; + +/* No comment provided by engineer. */ +"creator" = "творець"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Поточний пароль"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Поточна парольна фраза…"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; + +/* dropdown time picker choice */ +"custom" = "звичайний"; + +/* No comment provided by engineer. */ +"Custom time" = "Індивідуальний час"; + +/* No comment provided by engineer. */ +"Dark" = "Темний"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Пониження версії бази даних"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "База даних зашифрована!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Парольна фраза шифрування бази даних буде оновлена та збережена у в’язці ключів.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Ключову фразу шифрування бази даних буде оновлено.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Помилка в базі даних"; + +/* No comment provided by engineer. */ +"Database ID" = "Ідентифікатор бази даних"; + +/* copied message info */ +"Database ID: %d" = "Ідентифікатор бази даних: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Ідентифікатори бази даних та опція ізоляції транспорту."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "База даних зашифрована за допомогою випадкової парольної фрази, яку ви можете змінити."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "База даних зашифрована за допомогою випадкової парольної фрази. Будь ласка, змініть його перед експортом."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Ключова фраза бази даних"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Ключова фраза бази даних та експорт"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Парольна фраза бази даних відрізняється від збереженої у в’язці ключів."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Для відкриття чату потрібно ввести пароль до бази даних."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Оновлення бази даних"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "версія бази даних новіша, ніж додаток, але без міграції вниз для: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "База даних буде зашифрована, а парольна фраза збережена у в’язці ключів.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "База даних буде зашифрована.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "База даних буде перенесена під час перезапуску програми"; + +/* time unit */ +"days" = "днів"; + +/* No comment provided by engineer. */ +"Decentralized" = "Децентралізований"; + +/* message decrypt error item */ +"Decryption error" = "Помилка розшифровки"; + +/* pref value */ +"default (%@)" = "за замовчуванням (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "за замовчуванням (ні)"; + +/* No comment provided by engineer. */ +"default (yes)" = "за замовчуванням (так)"; + +/* chat item action */ +"Delete" = "Видалити"; + +/* No comment provided by engineer. */ +"Delete address" = "Видалити адресу"; + +/* No comment provided by engineer. */ +"Delete address?" = "Видалити адресу?"; + +/* No comment provided by engineer. */ +"Delete after" = "Видалити після"; + +/* No comment provided by engineer. */ +"Delete all files" = "Видалити всі файли"; + +/* No comment provided by engineer. */ +"Delete archive" = "Видалити архів"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "Видалити архів чату?"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Видалити профіль чату"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Видалити профіль чату?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Видалити підключення"; + +/* No comment provided by engineer. */ +"Delete contact" = "Видалити контакт"; + +/* No comment provided by engineer. */ +"Delete Contact" = "Видалити контакт"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Видалити контакт?"; + +/* No comment provided by engineer. */ +"Delete database" = "Видалити базу даних"; + +/* server test step */ +"Delete file" = "Видалити файл"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Видаляти файли та медіа?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Видалення файлів для всіх профілів чату"; + +/* chat feature */ +"Delete for everyone" = "Видалити для всіх"; + +/* No comment provided by engineer. */ +"Delete for me" = "Видалити для мене"; + +/* No comment provided by engineer. */ +"Delete group" = "Видалити групу"; + +/* No comment provided by engineer. */ +"Delete group?" = "Видалити групу?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Видалити запрошення"; + +/* No comment provided by engineer. */ +"Delete link" = "Видалити посилання"; + +/* No comment provided by engineer. */ +"Delete link?" = "Видалити посилання?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Видалити повідомлення учасника?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Видалити повідомлення?"; + +/* No comment provided by engineer. */ +"Delete messages" = "Видалити повідомлення"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Видаляйте повідомлення після"; + +/* No comment provided by engineer. */ +"Delete old database" = "Видалення старої бази даних"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Видалити стару базу даних?"; + +/* No comment provided by engineer. */ +"Delete pending connection" = "Видалити очікуване з'єднання"; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Видалити очікуване з'єднання?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Видалити профіль"; + +/* server test step */ +"Delete queue" = "Видалити чергу"; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Видалити профіль користувача?"; + +/* deleted chat item */ +"deleted" = "видалено"; + +/* No comment provided by engineer. */ +"Deleted at" = "Видалено за"; + +/* copied message info */ +"Deleted at: %@" = "Видалено за: %@"; + +/* rcv group event chat item */ +"deleted group" = "видалено групу"; + +/* No comment provided by engineer. */ +"Delivery" = "Доставка"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Квитанції про доставку відключені!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Квитанції про доставку!"; + +/* No comment provided by engineer. */ +"Description" = "Опис"; + +/* No comment provided by engineer. */ +"Develop" = "Розробник"; + +/* No comment provided by engineer. */ +"Developer tools" = "Інструменти для розробників"; + +/* No comment provided by engineer. */ +"Device" = "Пристрій"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Автентифікацію пристрою вимкнено. Вимкнення SimpleX Lock."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Автентифікація пристрою не ввімкнена. Ви можете увімкнути SimpleX Lock у Налаштуваннях, коли увімкнете автентифікацію пристрою."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "різна міграція в додатку/базі даних: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Різні імена, аватарки та транспортна ізоляція."; + +/* connection level description */ +"direct" = "прямо"; + +/* chat feature */ +"Direct messages" = "Прямі повідомлення"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this group." = "У цій групі заборонені прямі повідомлення між учасниками."; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "Вимкнути (зберегти перевизначення)"; + +/* No comment provided by engineer. */ +"Disable for all" = "Вимкнути для всіх"; + +/* authentication reason */ +"Disable SimpleX Lock" = "Вимкнути SimpleX Lock"; + +/* No comment provided by engineer. */ +"disabled" = "вимкнено"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Зникаюче повідомлення"; + +/* chat feature */ +"Disappearing messages" = "Зникаючі повідомлення"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Зникаючі повідомлення в цьому чаті заборонені."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this group." = "У цій групі заборонено зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Disappears at" = "Зникає за"; + +/* copied message info */ +"Disappears at: %@" = "Зникає за: %@"; + +/* server test step */ +"Disconnect" = "Від'єднати"; + +/* No comment provided by engineer. */ +"Display name" = "Відображуване ім'я"; + +/* No comment provided by engineer. */ +"Display name:" = "Відображуване ім'я:"; + +/* No comment provided by engineer. */ +"Do it later" = "Зробіть це пізніше"; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "НЕ використовуйте SimpleX для екстрених викликів."; + +/* No comment provided by engineer. */ +"Don't create address" = "Не створювати адресу"; + +/* No comment provided by engineer. */ +"Don't enable" = "Не вмикати"; + +/* No comment provided by engineer. */ +"Don't show again" = "Більше не показувати"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Пониження та відкритий чат"; + +/* server test step */ +"Download file" = "Завантажити файл"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Дублююче ім'я користувача!"; + +/* integrity error chat item */ +"duplicate message" = "дублююче повідомлення"; + +/* No comment provided by engineer. */ +"Duration" = "Тривалість"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "e2e зашифрований"; + +/* chat item action */ +"Edit" = "Редагувати"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Редагування профілю групи"; + +/* No comment provided by engineer. */ +"Enable" = "Увімкнути"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Увімкнути (зберегти перевизначення)"; + +/* No comment provided by engineer. */ +"Enable automatic message deletion?" = "Увімкнути автоматичне видалення повідомлень?"; + +/* No comment provided by engineer. */ +"Enable for all" = "Увімкнути для всіх"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Увімкнути миттєві сповіщення?"; + +/* No comment provided by engineer. */ +"Enable lock" = "Увімкнути блокування"; + +/* No comment provided by engineer. */ +"Enable notifications" = "Увімкнути сповіщення"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "Увімкнути періодичні сповіщення?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Увімкнути самознищення"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Увімкнути пароль самознищення"; + +/* authentication reason */ +"Enable SimpleX Lock" = "Увімкнути SimpleX Lock"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "Увімкнути TCP keep-alive"; + +/* enabled status */ +"enabled" = "увімкнено"; + +/* enabled status */ +"enabled for contact" = "увімкнено для контакту"; + +/* enabled status */ +"enabled for you" = "увімкнено для вас"; + +/* No comment provided by engineer. */ +"Encrypt" = "Зашифрувати"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Зашифрувати базу даних?"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Зашифрована база даних"; + +/* notification */ +"Encrypted message or another event" = "Зашифроване повідомлення або інша подія"; + +/* notification */ +"Encrypted message: database error" = "Зашифроване повідомлення: помилка бази даних"; + +/* notification */ +"Encrypted message: database migration error" = "Зашифроване повідомлення: помилка міграції бази даних"; + +/* notification */ +"Encrypted message: keychain error" = "Зашифроване повідомлення: помилка ланцюжка ключів"; + +/* notification */ +"Encrypted message: no passphrase" = "Зашифроване повідомлення: без ключової фрази"; + +/* notification */ +"Encrypted message: unexpected error" = "Зашифроване повідомлення: несподівана помилка"; + +/* chat item text */ +"encryption agreed" = "узгоджено шифрування"; + +/* chat item text */ +"encryption agreed for %@" = "узгоджене шифрування для %@"; + +/* chat item text */ +"encryption ok" = "шифрування ok"; + +/* chat item text */ +"encryption ok for %@" = "шифрування ok для %@"; + +/* chat item text */ +"encryption re-negotiation allowed" = "переузгодження шифрування дозволено"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "переузгодження шифрування дозволено для %@"; + +/* chat item text */ +"encryption re-negotiation required" = "потрібне повторне узгодження шифрування"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "для %@ потрібне повторне узгодження шифрування"; + +/* No comment provided by engineer. */ +"ended" = "закінчився"; + +/* call status */ +"ended call %@" = "закінчився виклик %@"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Введіть правильну парольну фразу."; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Введіть пароль"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Введіть пароль…"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "Введіть пароль вище, щоб показати!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "Увійдіть на сервер вручну"; + +/* placeholder */ +"Enter welcome message…" = "Введіть вітальне повідомлення…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Введіть вітальне повідомлення... (необов'язково)"; + +/* No comment provided by engineer. */ +"error" = "помилка"; + +/* No comment provided by engineer. */ +"Error" = "Помилка"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Помилка скасування зміни адреси"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; + +/* No comment provided by engineer. */ +"Error accessing database file" = "Помилка доступу до файлу бази даних"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Помилка додавання користувача(ів)"; + +/* No comment provided by engineer. */ +"Error changing address" = "Помилка зміни адреси"; + +/* No comment provided by engineer. */ +"Error changing role" = "Помилка зміни ролі"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Помилка зміни налаштування"; + +/* No comment provided by engineer. */ +"Error creating address" = "Помилка створення адреси"; + +/* No comment provided by engineer. */ +"Error creating group" = "Помилка створення групи"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Помилка створення посилання на групу"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Помилка створення профілю!"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Помилка видалення бази даних чату"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Помилка видалення чату!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Помилка видалення з'єднання"; + +/* No comment provided by engineer. */ +"Error deleting contact" = "Помилка видалення контакту"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Помилка видалення бази даних"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Помилка видалення старої бази даних"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Помилка видалення токена"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Помилка видалення профілю користувача"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Помилка активації підтвердження доставлення!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Помилка увімкнення сповіщень"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Помилка шифрування бази даних"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Помилка експорту бази даних чату"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Помилка імпорту бази даних чату"; + +/* No comment provided by engineer. */ +"Error joining group" = "Помилка приєднання до групи"; + +/* No comment provided by engineer. */ +"Error loading %@ servers" = "Помилка завантаження %@ серверів"; + +/* No comment provided by engineer. */ +"Error receiving file" = "Помилка отримання файлу"; + +/* No comment provided by engineer. */ +"Error removing member" = "Помилка видалення учасника"; + +/* No comment provided by engineer. */ +"Error saving %@ servers" = "Помилка збереження %@ серверів"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Помилка збереження профілю групи"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "Помилка збереження серверів ICE"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Помилка збереження пароля"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Помилка збереження пароля на keychain"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Помилка збереження пароля користувача"; + +/* No comment provided by engineer. */ +"Error sending email" = "Помилка надсилання електронного листа"; + +/* No comment provided by engineer. */ +"Error sending message" = "Помилка надсилання повідомлення"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Помилка встановлення підтвердження доставлення!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Помилка запуску чату"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Помилка зупинки чату"; + +/* No comment provided by engineer. */ +"Error switching profile!" = "Помилка перемикання профілю!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Помилка синхронізації з'єднання"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Помилка оновлення посилання на групу"; + +/* No comment provided by engineer. */ +"Error updating message" = "Повідомлення про помилку оновлення"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Помилка оновлення налаштувань"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Помилка оновлення конфіденційності користувача"; + +/* No comment provided by engineer. */ +"Error: " = "Помилка: "; + +/* No comment provided by engineer. */ +"Error: %@" = "Помилка: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Помилка: немає файлу бази даних"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Помилка: URL-адреса невірна"; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Навіть коли вимкнений у розмові."; + +/* No comment provided by engineer. */ +"event happened" = "відбулася подія"; + +/* No comment provided by engineer. */ +"Exit without saving" = "Вихід без збереження"; + +/* No comment provided by engineer. */ +"Export database" = "Експорт бази даних"; + +/* No comment provided by engineer. */ +"Export error:" = "Помилка експорту:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Експортований архів бази даних."; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Експорт архіву бази даних…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Не вдалося видалити парольну фразу"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Швидко і без очікування, поки відправник буде онлайн!"; + +/* No comment provided by engineer. */ +"Favorite" = "Улюблений"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "Файл буде видалено з серверів."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "Файл буде отримано, коли ваш контакт завершить завантаження."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "Файл буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"File: %@" = "Файл: %@"; + +/* No comment provided by engineer. */ +"Files & media" = "Файли та медіа"; + +/* chat feature */ +"Files and media" = "Файли і медіа"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "Файли та медіа в цій групі заборонені."; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Файли та медіа заборонені!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Фільтруйте непрочитані та улюблені чати."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Нарешті, вони у нас є! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Швидше знаходьте чати"; + +/* No comment provided by engineer. */ +"Fix" = "Виправити"; + +/* No comment provided by engineer. */ +"Fix connection" = "Виправити з'єднання"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Полагодити зв'язок?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Виправити шифрування після відновлення резервних копій."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Виправлення не підтримується контактом"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Виправлення не підтримується учасником групи"; + +/* No comment provided by engineer. */ +"For console" = "Для консолі"; + +/* No comment provided by engineer. */ +"French interface" = "Французький інтерфейс"; + +/* No comment provided by engineer. */ +"Full link" = "Повне посилання"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Повне ім'я (необов'язково)"; + +/* No comment provided by engineer. */ +"Full name:" = "Повне ім'я:"; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Повністю перероблено - робота у фоновому режимі!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Подальше зменшення використання акумулятора"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIF-файли та наклейки"; + +/* No comment provided by engineer. */ +"Group" = "Група"; + +/* No comment provided by engineer. */ +"group deleted" = "групу видалено"; + +/* No comment provided by engineer. */ +"Group display name" = "Назва групи для відображення"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "Повна назва групи (необов'язково)"; + +/* No comment provided by engineer. */ +"Group image" = "Зображення групи"; + +/* No comment provided by engineer. */ +"Group invitation" = "Групове запрошення"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "Термін дії групового запрошення закінчився"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "Групове запрошення більше не дійсне, воно було видалено відправником."; + +/* No comment provided by engineer. */ +"Group link" = "Посилання на групу"; + +/* No comment provided by engineer. */ +"Group links" = "Групові посилання"; + +/* No comment provided by engineer. */ +"Group members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Group members can irreversibly delete sent messages." = "Учасники групи можуть безповоротно видаляти надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Group members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; + +/* No comment provided by engineer. */ +"Group members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Group members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; + +/* No comment provided by engineer. */ +"Group members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; + +/* notification */ +"Group message:" = "Групове повідомлення:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Модерація груп"; + +/* No comment provided by engineer. */ +"Group preferences" = "Параметри груп"; + +/* No comment provided by engineer. */ +"Group profile" = "Профіль групи"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Профіль групи зберігається на пристроях учасників, а не на серверах."; + +/* snd group event chat item */ +"group profile updated" = "оновлено профіль групи"; + +/* No comment provided by engineer. */ +"Group welcome message" = "Привітальне повідомлення групи"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Група буде видалена для всіх учасників - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "Група буде видалена для вас - це не може бути скасовано!"; + +/* No comment provided by engineer. */ +"Help" = "Довідка"; + +/* No comment provided by engineer. */ +"Hidden" = "Приховано"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Приховані профілі чату"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Прихований пароль до профілю"; + +/* chat item action */ +"Hide" = "Приховати"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Приховати екран програми в останніх програмах."; + +/* No comment provided by engineer. */ +"Hide profile" = "Приховати профіль"; + +/* No comment provided by engineer. */ +"Hide:" = "Приховати:"; + +/* No comment provided by engineer. */ +"History" = "Історія"; + +/* time unit */ +"hours" = "години"; + +/* No comment provided by engineer. */ +"How it works" = "Як це працює"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "Як працює SimpleX"; + +/* No comment provided by engineer. */ +"How to" = "Як зробити"; + +/* No comment provided by engineer. */ +"How to use it" = "Як ним користуватися"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Як користуватися вашими серверами"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "Сервери ICE (по одному на лінію)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням."; + +/* No comment provided by engineer. */ +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Якщо ви не можете зустрітися особисто, ви можете **сканувати QR-код у відеодзвінку**, або ваш контакт може поділитися посиланням на запрошення."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "Якщо ви введете пароль самознищення під час відкриття програми:"; + +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Якщо вам потрібно скористатися чатом зараз, натисніть **Зробити це пізніше** нижче (вам буде запропоновано перенести базу даних при перезапуску програми)."; + +/* No comment provided by engineer. */ +"Ignore" = "Ігнорувати"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "Зображення буде отримано, коли ваш контакт завершить завантаження."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "Зображення буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"Immediately" = "Негайно"; + +/* No comment provided by engineer. */ +"Immune to spam and abuse" = "Імунітет до спаму та зловживань"; + +/* No comment provided by engineer. */ +"Import" = "Імпорт"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Імпортувати базу даних чату?"; + +/* No comment provided by engineer. */ +"Import database" = "Імпорт бази даних"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Покращена конфіденційність та безпека"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Покращена конфігурація сервера"; + +/* No comment provided by engineer. */ +"In reply to" = "У відповідь на"; + +/* No comment provided by engineer. */ +"Incognito" = "Інкогніто"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Режим інкогніто"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Режим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту."; + +/* chat list item description */ +"incognito via contact address link" = "інкогніто за посиланням на контактну адресу"; + +/* chat list item description */ +"incognito via group link" = "інкогніто через групове посилання"; + +/* chat list item description */ +"incognito via one-time link" = "інкогніто за одноразовим посиланням"; + +/* notification */ +"Incoming audio call" = "Вхідний аудіовиклик"; + +/* notification */ +"Incoming call" = "Вхідний дзвінок"; + +/* notification */ +"Incoming video call" = "Вхідний відеодзвінок"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несумісна версія бази даних"; + +/* PIN entry */ +"Incorrect passcode" = "Неправильний пароль"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Неправильний код безпеки!"; + +/* connection level description */ +"indirect (%d)" = "непрямий (%d)"; + +/* chat item action */ +"Info" = "Інформація"; + +/* No comment provided by engineer. */ +"Initial role" = "Початкова роль"; + +/* No comment provided by engineer. */ +"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Миттєві пуш-сповіщення будуть приховані!\n"; + +/* No comment provided by engineer. */ +"Instantly" = "Миттєво"; + +/* No comment provided by engineer. */ +"Interface" = "Інтерфейс"; + +/* invalid chat data */ +"invalid chat" = "недійсний чат"; + +/* No comment provided by engineer. */ +"invalid chat data" = "невірні дані чату"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Неправильне посилання для підключення"; + +/* invalid chat item */ +"invalid data" = "невірні дані"; + +/* No comment provided by engineer. */ +"Invalid server address!" = "Неправильна адреса сервера!"; + +/* item status text */ +"Invalid status" = "Недійсний статус"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "Термін дії запрошення закінчився!"; + +/* group name */ +"invitation to group %@" = "запрошення до групи %@"; + +/* No comment provided by engineer. */ +"Invite friends" = "Запросити друзів"; + +/* No comment provided by engineer. */ +"Invite members" = "Запросити учасників"; + +/* No comment provided by engineer. */ +"Invite to group" = "Запросити до групи"; + +/* No comment provided by engineer. */ +"invited" = "запрошені"; + +/* rcv group event chat item */ +"invited %@" = "запрошений %@"; + +/* chat list item title */ +"invited to connect" = "запрошуємо приєднатися"; + +/* rcv group event chat item */ +"invited via your group link" = "запрошені за посиланням у вашій групі"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain використовується для безпечного зберігання пароля - це дає змогу отримувати миттєві повідомлення."; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення."; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Безповоротне видалення повідомлення"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "У цьому чаті заборонено безповоротне видалення повідомлень."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this group." = "У цій групі заборонено безповоротне видалення повідомлень."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Це може статися, якщо ви або ваше з'єднання використовували стару резервну копію бази даних."; + +/* No comment provided by engineer. */ +"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." = "Це може статися, коли:\n1. Термін дії повідомлень закінчився в клієнті-відправнику через 2 дні або на сервері через 30 днів.\n2. Не вдалося розшифрувати повідомлення, тому що ви або ваш контакт використовували стару резервну копію бази даних.\n3. З'єднання було скомпрометовано."; + +/* No comment provided by engineer. */ +"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "Італійський інтерфейс"; + +/* No comment provided by engineer. */ +"italic" = "курсив"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Японський інтерфейс"; + +/* No comment provided by engineer. */ +"Join" = "Приєднуйтесь"; + +/* No comment provided by engineer. */ +"join as %@" = "приєднатися як %@"; + +/* No comment provided by engineer. */ +"Join group" = "Приєднуйтесь до групи"; + +/* No comment provided by engineer. */ +"Join incognito" = "Приєднуйтесь інкогніто"; + +/* No comment provided by engineer. */ +"Joining group" = "Приєднання до групи"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Зберігайте свої зв'язки"; + +/* No comment provided by engineer. */ +"Keychain error" = "помилка KeyChain"; + +/* No comment provided by engineer. */ +"KeyChain error" = "помилка KeyChain"; + +/* No comment provided by engineer. */ +"Large file!" = "Великий файл!"; + +/* No comment provided by engineer. */ +"Learn more" = "Дізнайтеся більше"; + +/* No comment provided by engineer. */ +"Leave" = "Залишити"; + +/* No comment provided by engineer. */ +"Leave group" = "Покинути групу"; + +/* No comment provided by engineer. */ +"Leave group?" = "Покинути групу?"; + +/* rcv group event chat item */ +"left" = "ліворуч"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Поговоримо в чаті SimpleX"; + +/* No comment provided by engineer. */ +"Light" = "Світлий"; + +/* No comment provided by engineer. */ +"Limitations" = "Обмеження"; + +/* No comment provided by engineer. */ +"LIVE" = "НАЖИВО"; + +/* No comment provided by engineer. */ +"Live message!" = "Живе повідомлення!"; + +/* No comment provided by engineer. */ +"Live messages" = "Живі повідомлення"; + +/* No comment provided by engineer. */ +"Local name" = "Місцева назва"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Тільки локальні дані профілю"; + +/* No comment provided by engineer. */ +"Lock after" = "Блокування після"; + +/* No comment provided by engineer. */ +"Lock mode" = "Режим блокування"; + +/* No comment provided by engineer. */ +"Make a private connection" = "Створіть приватне з'єднання"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Зробити так, щоб одне повідомлення зникло"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Зробіть профіль приватним!"; + +/* No comment provided by engineer. */ +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@)."; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються."; + +/* No comment provided by engineer. */ +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*"; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Позначити видалено для всіх"; + +/* No comment provided by engineer. */ +"Mark read" = "Позначити прочитано"; + +/* No comment provided by engineer. */ +"Mark verified" = "Позначити перевірено"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Виправлення в повідомленнях"; + +/* marked deleted chat item preview text */ +"marked deleted" = "з позначкою видалено"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Максимум 30 секунд, отримується миттєво."; + +/* member role */ +"member" = "учасник"; + +/* No comment provided by engineer. */ +"Member" = "Учасник"; + +/* rcv group event chat item */ +"member connected" = "з'єднаний"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Роль учасника буде змінено на \"%@\". Всі учасники групи будуть повідомлені про це."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль учасника буде змінено на \"%@\". Учасник отримає нове запрошення."; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Учасник буде видалений з групи - це неможливо скасувати!"; + +/* item status text */ +"Message delivery error" = "Помилка доставки повідомлення"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Підтвердження доставки повідомлення!"; + +/* No comment provided by engineer. */ +"Message draft" = "Чернетка повідомлення"; + +/* chat feature */ +"Message reactions" = "Реакції на повідомлення"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "Реакції на повідомлення в цьому чаті заборонені."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "Реакції на повідомлення в цій групі заборонені."; + +/* notification */ +"message received" = "повідомлення отримано"; + +/* No comment provided by engineer. */ +"Message text" = "Текст повідомлення"; + +/* No comment provided by engineer. */ +"Messages" = "Повідомлення"; + +/* No comment provided by engineer. */ +"Messages & files" = "Повідомлення та файли"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Перенесення архіву бази даних…"; + +/* No comment provided by engineer. */ +"Migration error:" = "Помилка міграції:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Міграція не вдалася. Натисніть **Пропустити** нижче, щоб продовжити використовувати поточну базу даних. Будь ласка, повідомте про проблему розробникам програми через чат або електронну пошту [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Міграцію завершено"; + +/* No comment provided by engineer. */ +"Migrations: %@" = "Міграції: %@"; + +/* time unit */ +"minutes" = "хвилини"; + +/* call status */ +"missed call" = "пропущений дзвінок"; + +/* chat item action */ +"Moderate" = "Модерується"; + +/* moderated chat item */ +"moderated" = "модерується"; + +/* No comment provided by engineer. */ +"Moderated at" = "Модерується на"; + +/* copied message info */ +"Moderated at: %@" = "Модерується за: %@"; + +/* No comment provided by engineer. */ +"moderated by %@" = "модерується %@"; + +/* time unit */ +"months" = "місяців"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Незабаром буде ще більше покращень!"; + +/* item status description */ +"Most likely this connection is deleted." = "Швидше за все, це з'єднання видалено."; + +/* No comment provided by engineer. */ +"Most likely this contact has deleted the connection with you." = "Швидше за все, цей контакт видалив зв'язок з вами."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Кілька профілів чату"; + +/* No comment provided by engineer. */ +"Mute" = "Вимкнути звук"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Вимкнено, коли неактивний!"; + +/* No comment provided by engineer. */ +"Name" = "Ім'я"; + +/* No comment provided by engineer. */ +"Network & servers" = "Мережа та сервери"; + +/* No comment provided by engineer. */ +"Network settings" = "Налаштування мережі"; + +/* No comment provided by engineer. */ +"Network status" = "Стан мережі"; + +/* No comment provided by engineer. */ +"never" = "ніколи"; + +/* notification */ +"New contact request" = "Новий запит на контакт"; + +/* notification */ +"New contact:" = "Новий контакт:"; + +/* No comment provided by engineer. */ +"New database archive" = "Новий архів бази даних"; + +/* No comment provided by engineer. */ +"New display name" = "Нове ім'я відображення"; + +/* No comment provided by engineer. */ +"New in %@" = "Нове в %@"; + +/* No comment provided by engineer. */ +"New member role" = "Нова роль учасника"; + +/* notification */ +"new message" = "нове повідомлення"; + +/* notification */ +"New message" = "Нове повідомлення"; + +/* No comment provided by engineer. */ +"New Passcode" = "Новий пароль"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Новий пароль…"; + +/* pref value */ +"no" = "ні"; + +/* No comment provided by engineer. */ +"No" = "Ні"; + +/* Authentication unavailable */ +"No app password" = "Немає пароля програми"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Не вибрано жодного контакту"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Немає контактів для додавання"; + +/* No comment provided by engineer. */ +"No delivery information" = "Немає інформації про доставку"; + +/* No comment provided by engineer. */ +"No device token!" = "Токен пристрою відсутній!"; + +/* No comment provided by engineer. */ +"no e2e encryption" = "без шифрування e2e"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Немає фільтрованих чатів"; + +/* No comment provided by engineer. */ +"No group!" = "Групу не знайдено!"; + +/* No comment provided by engineer. */ +"No history" = "Немає історії"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Немає отриманих або відправлених файлів"; + +/* copied message info in history */ +"no text" = "без тексту"; + +/* No comment provided by engineer. */ +"Notifications" = "Сповіщення"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Сповіщення вимкнено!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Тепер адміністратори можуть\n- видаляти повідомлення користувачів.\n- відключати користувачів (роль \"спостерігач\")"; + +/* member role */ +"observer" = "спостерігач"; + +/* enabled status + group pref value */ +"off" = "вимкнено"; + +/* No comment provided by engineer. */ +"Off" = "Вимкнено"; + +/* No comment provided by engineer. */ +"Off (Local)" = "Вимкнено (локально)"; + +/* feature offered item */ +"offered %@" = "запропоновано %@"; + +/* feature offered item */ +"offered %@: %@" = "запропонував %1$@: %2$@"; + +/* No comment provided by engineer. */ +"Ok" = "Гаразд"; + +/* No comment provided by engineer. */ +"Old database" = "Стара база даних"; + +/* No comment provided by engineer. */ +"Old database archive" = "Старий архів бази даних"; + +/* group pref value */ +"on" = "увімкнено"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "Посилання на одноразове запрошення"; + +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Onion хости не будуть використовуватися."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Тільки власники груп можуть змінювати налаштування групи."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Тільки власники груп можуть вмикати файли та медіа."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Тільки власники груп можуть вмикати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Тільки ви можете додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення)."; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Дзвонити можете тільки ви."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Тільки ви можете надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Тільки ви можете надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Тільки ваш контакт може додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення)."; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Тільки ваш контакт може здійснювати дзвінки."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Тільки ваш контакт може надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Тільки ваш контакт може надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Open chat" = "Відкритий чат"; + +/* authentication reason */ +"Open chat console" = "Відкрийте консоль чату"; + +/* No comment provided by engineer. */ +"Open Settings" = "Відкрийте Налаштування"; + +/* authentication reason */ +"Open user profiles" = "Відкрити профілі користувачів"; + +/* No comment provided by engineer. */ +"Open-source protocol and code – anybody can run the servers." = "Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери."; + +/* No comment provided by engineer. */ +"Opening database…" = "Відкриття бази даних…"; + +/* No comment provided by engineer. */ +"Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "Відкриття посилання в браузері може знизити конфіденційність і безпеку з'єднання. Ненадійні посилання SimpleX будуть червоного кольору."; + +/* No comment provided by engineer. */ +"or chat with the developers" = "або поспілкуйтеся з розробниками"; + +/* member role */ +"owner" = "власник"; + +/* No comment provided by engineer. */ +"Passcode" = "Пароль"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "Пароль змінено!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Введення пароля"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "Пароль не змінено!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "Пароль встановлено!"; + +/* No comment provided by engineer. */ +"Password to show" = "Показати пароль"; + +/* No comment provided by engineer. */ +"Paste" = "Вставити"; + +/* No comment provided by engineer. */ +"Paste image" = "Вставити зображення"; + +/* No comment provided by engineer. */ +"Paste received link" = "Вставте отримане посилання"; + +/* placeholder */ +"Paste the link you received to connect with your contact." = "Вставте отримане посилання для зв'язку з вашим контактом."; + +/* No comment provided by engineer. */ +"peer-to-peer" = "одноранговий"; + +/* No comment provided by engineer. */ +"People can connect to you only via the links you share." = "Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся."; + +/* No comment provided by engineer. */ +"Periodically" = "Періодично"; + +/* message decrypt error item */ +"Permanent decryption error" = "Постійна помилка розшифрування"; + +/* No comment provided by engineer. */ +"PING count" = "Кількість PING"; + +/* No comment provided by engineer. */ +"PING interval" = "Інтервал PING"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Будь ласка, перевірте підключення до мережі за допомогою %@ і спробуйте ще раз."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Будь ласка, перевірте свої та контактні налаштування."; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Зверніться до адміністратора групи."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Будь ласка, введіть правильний поточний пароль."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Будь ласка, введіть попередній пароль після відновлення резервної копії бази даних. Ця дія не може бути скасована."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Будь ласка, запам'ятайте або надійно зберігайте його - втрачений пароль неможливо відновити!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Будь ласка, повідомте про це розробникам."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Будь ласка, перезапустіть додаток і перенесіть базу даних, щоб увімкнути push-сповіщення."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Будь ласка, зберігайте пароль надійно, ви НЕ зможете отримати доступ до чату, якщо втратите його."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите."; + +/* No comment provided by engineer. */ +"Polish interface" = "Польський інтерфейс"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; + +/* No comment provided by engineer. */ +"Preset server" = "Попередньо встановлений сервер"; + +/* No comment provided by engineer. */ +"Preset server address" = "Попередньо встановлена адреса сервера"; + +/* No comment provided by engineer. */ +"Preview" = "Попередній перегляд"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Конфіденційність і безпека"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "Конфіденційність переглянута"; + +/* No comment provided by engineer. */ +"Private filenames" = "Приватні імена файлів"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "З'єднання профілю та сервера"; + +/* No comment provided by engineer. */ +"Profile image" = "Зображення профілю"; + +/* No comment provided by engineer. */ +"Profile password" = "Пароль до профілю"; + +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "Оновлення профілю буде надіслано вашим контактам."; + +/* No comment provided by engineer. */ +"Prohibit audio/video calls." = "Заборонити аудіо/відеодзвінки."; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "Заборонити незворотне видалення повідомлень."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "Заборонити реакцію на повідомлення."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "Заборонити реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Заборонити надсилати прямі повідомлення учасникам."; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "Заборонити надсилання зникаючих повідомлень."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Заборонити надсилання файлів і медіа."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "Заборонити надсилання голосових повідомлень."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Захистіть екран програми"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Захистіть свої профілі чату паролем!"; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Тайм-аут протоколу"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Тайм-аут протоколу на КБ"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push-повідомлення"; + +/* No comment provided by engineer. */ +"Rate the app" = "Оцініть додаток"; + +/* chat item menu */ +"React…" = "Реагуй…"; + +/* No comment provided by engineer. */ +"Read" = "Читати"; + +/* No comment provided by engineer. */ +"Read more" = "Читати далі"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; + +/* No comment provided by engineer. */ +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme)."; + +/* No comment provided by engineer. */ +"Read more in our GitHub repository." = "Читайте більше в нашому репозиторії на GitHub."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "Підтвердження виключені"; + +/* No comment provided by engineer. */ +"received answer…" = "отримали відповідь…"; + +/* No comment provided by engineer. */ +"Received at" = "Отримано за"; + +/* copied message info */ +"Received at: %@" = "Отримано за: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "отримали підтвердження…"; + +/* notification */ +"Received file event" = "Подія отримання файлу"; + +/* message info title */ +"Received message" = "Отримано повідомлення"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "Отримання файлу буде зупинено."; + +/* No comment provided by engineer. */ +"Receiving via" = "Отримання через"; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "Одержувачі бачать оновлення, коли ви їх вводите."; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік."; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Перепідключити сервери?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Запис оновлено за"; + +/* copied message info */ +"Record updated at: %@" = "Запис оновлено за: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Зменшення використання акумулятора"; + +/* reject incoming call via notification */ +"Reject" = "Відхилити"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Відхилити (відправника НЕ повідомлено)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Відхилити запит на контакт"; + +/* call status */ +"rejected call" = "відхилений виклик"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка."; + +/* No comment provided by engineer. */ +"Remove" = "Видалити"; + +/* No comment provided by engineer. */ +"Remove member" = "Видалити учасника"; + +/* No comment provided by engineer. */ +"Remove member?" = "Видалити учасника?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Видалити парольну фразу з брелока?"; + +/* No comment provided by engineer. */ +"removed" = "видалено"; + +/* rcv group event chat item */ +"removed %@" = "видалено %@"; + +/* rcv group event chat item */ +"removed you" = "прибрали вас"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Переузгодьте"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Переузгодьте шифрування"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Переузгодьте шифрування?"; + +/* chat item action */ +"Reply" = "Відповісти"; + +/* No comment provided by engineer. */ +"Required" = "Потрібно"; + +/* No comment provided by engineer. */ +"Reset" = "Перезавантаження"; + +/* No comment provided by engineer. */ +"Reset colors" = "Скинути кольори"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Відновити налаштування за замовчуванням"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Перезапустіть програму, щоб створити новий профіль чату"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Перезапустіть програму, щоб використовувати імпортовану базу даних чату"; + +/* No comment provided by engineer. */ +"Restore" = "Відновити"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Відновлення резервної копії бази даних"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Відновити резервну копію бази даних?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Відновлення помилки бази даних"; + +/* chat item action */ +"Reveal" = "Показувати"; + +/* No comment provided by engineer. */ +"Revert" = "Повернутися"; + +/* No comment provided by engineer. */ +"Revoke" = "Відкликати"; + +/* cancel file action */ +"Revoke file" = "Відкликати файл"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Відкликати файл?"; + +/* No comment provided by engineer. */ +"Role" = "Роль"; + +/* No comment provided by engineer. */ +"Run chat" = "Запустити чат"; + +/* chat item action */ +"Save" = "Зберегти"; + +/* No comment provided by engineer. */ +"Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; + +/* No comment provided by engineer. */ +"Save and notify contact" = "Зберегти та повідомити контакт"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "Зберегти та повідомити учасників групи"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Збереження та оновлення профілю групи"; + +/* No comment provided by engineer. */ +"Save archive" = "Зберегти архів"; + +/* No comment provided by engineer. */ +"Save auto-accept settings" = "Зберегти налаштування автоприйому"; + +/* No comment provided by engineer. */ +"Save group profile" = "Зберегти профіль групи"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Збережіть пароль і відкрийте чат"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Збережіть парольну фразу в Keychain"; + +/* No comment provided by engineer. */ +"Save preferences?" = "Зберегти налаштування?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Зберегти пароль профілю"; + +/* No comment provided by engineer. */ +"Save servers" = "Зберегти сервери"; + +/* No comment provided by engineer. */ +"Save servers?" = "Зберегти сервери?"; + +/* No comment provided by engineer. */ +"Save settings?" = "Зберегти налаштування?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Зберегти вітальне повідомлення?"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "Збережені сервери WebRTC ICE буде видалено"; + +/* No comment provided by engineer. */ +"Scan code" = "Сканувати код"; + +/* No comment provided by engineer. */ +"Scan QR code" = "Відскануйте QR-код"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Відскануйте код безпеки з додатку вашого контакту."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "Відскануйте QR-код сервера"; + +/* No comment provided by engineer. */ +"Search" = "Пошук"; + +/* network option */ +"sec" = "сек"; + +/* time unit */ +"seconds" = "секунди"; + +/* No comment provided by engineer. */ +"secret" = "таємниця"; + +/* server test step */ +"Secure queue" = "Безпечна черга"; + +/* No comment provided by engineer. */ +"Security assessment" = "Оцінка безпеки"; + +/* No comment provided by engineer. */ +"Security code" = "Код безпеки"; + +/* chat item text */ +"security code changed" = "змінено код безпеки"; + +/* No comment provided by engineer. */ +"Select" = "Виберіть"; + +/* No comment provided by engineer. */ +"Self-destruct" = "Самознищення"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Пароль самознищення"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Пароль самознищення змінено!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Пароль самознищення ввімкнено!"; + +/* No comment provided by engineer. */ +"Send" = "Надіслати"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Надсилання звітів про доставку"; + +/* No comment provided by engineer. */ +"Send direct message" = "Надішліть пряме повідомлення"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Надіслати зникаюче повідомлення"; + +/* No comment provided by engineer. */ +"Send link previews" = "Надіслати попередній перегляд за посиланням"; + +/* No comment provided by engineer. */ +"Send live message" = "Надіслати живе повідомлення"; + +/* No comment provided by engineer. */ +"Send notifications" = "Надсилати сповіщення"; + +/* No comment provided by engineer. */ +"Send notifications:" = "Надсилати сповіщення:"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Надсилайте запитання та ідеї"; + +/* No comment provided by engineer. */ +"Send receipts" = "Надіслати підтвердження"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Надсилайте їх із галереї чи власних клавіатур."; + +/* No comment provided by engineer. */ +"Sender cancelled file transfer." = "Відправник скасував передачу файлу."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "Можливо, відправник видалив запит на підключення."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Надсилання підтверджень доставки буде ввімкнено для всіх контактів у всіх видимих профілях чату."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "Надсилання підтверджень доставки буде ввімкнено для всіх контактів."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "Надсилання файлу буде зупинено."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "Надсилання підтвердження вимкнено для контактів %lld"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Відправлення підтверджень вимкнено для груп %lld"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Для контактів %lld увімкнено надсилання підтвердження"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Для груп %lld увімкнено надсилання підтвердження"; + +/* No comment provided by engineer. */ +"Sending via" = "Надсилання через"; + +/* No comment provided by engineer. */ +"Sent at" = "Надіслано за"; + +/* copied message info */ +"Sent at: %@" = "Надіслано за: %@"; + +/* notification */ +"Sent file event" = "Подія надісланого файлу"; + +/* message info title */ +"Sent message" = "Надіслано повідомлення"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Надіслані повідомлення будуть видалені через встановлений час."; + +/* server test error */ +"Server requires authorization to create queues, check password" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; + +/* server test error */ +"Server requires authorization to upload, check password" = "Сервер вимагає авторизації для завантаження, перевірте пароль"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Тест сервера завершився невдало!"; + +/* No comment provided by engineer. */ +"Servers" = "Сервери"; + +/* No comment provided by engineer. */ +"Set 1 day" = "Встановити 1 день"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Встановити ім'я контакту…"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Встановіть налаштування групи"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Встановіть його замість аутентифікації системи."; + +/* No comment provided by engineer. */ +"Set passcode" = "Встановити пароль"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Встановити ключову фразу для експорту"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Налаштуйте повідомлення, яке показуватиметься новим користувачам!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Встановлення таймаутів для проксі/VPN"; + +/* No comment provided by engineer. */ +"Settings" = "Налаштування"; + +/* chat item action */ +"Share" = "Поділіться"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Поділитися 1-разовим посиланням"; + +/* No comment provided by engineer. */ +"Share address" = "Поділитися адресою"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "Поділіться адресою з контактами?"; + +/* No comment provided by engineer. */ +"Share link" = "Поділіться посиланням"; + +/* No comment provided by engineer. */ +"Share one-time invitation link" = "Поділіться посиланням на одноразове запрошення"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Поділіться з контактами"; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Показувати дзвінки в історії дзвінків"; + +/* No comment provided by engineer. */ +"Show developer options" = "Показати опції розробника"; + +/* No comment provided by engineer. */ +"Show last messages" = "Показати останні повідомлення"; + +/* No comment provided by engineer. */ +"Show preview" = "Показати попередній перегляд"; + +/* No comment provided by engineer. */ +"Show:" = "Показати:"; + +/* No comment provided by engineer. */ +"SimpleX address" = "Адреса SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "Адреса SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "Безпека SimpleX Chat була перевірена компанією Trail of Bits."; + +/* simplex link type */ +"SimpleX contact address" = "Контактна адреса SimpleX"; + +/* notification */ +"SimpleX encrypted message or connection event" = "Зашифроване повідомлення SimpleX або подія підключення"; + +/* simplex link type */ +"SimpleX group link" = "Посилання на групу SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX links" = "Посилання SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX Lock"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "Режим SimpleX Lock"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "SimpleX Lock не ввімкнено!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX Lock увімкнено"; + +/* simplex link type */ +"SimpleX one-time invitation" = "Одноразове запрошення SimpleX"; + +/* No comment provided by engineer. */ +"Skip" = "Пропустити"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Пропущені повідомлення"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Невеликі групи (максимум 20 осіб)"; + +/* No comment provided by engineer. */ +"SMP servers" = "Сервери SMP"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше."; + +/* notification title */ +"Somebody" = "Хтось"; + +/* No comment provided by engineer. */ +"Start a new chat" = "Почніть новий чат"; + +/* No comment provided by engineer. */ +"Start chat" = "Почати чат"; + +/* No comment provided by engineer. */ +"Start migration" = "Почати міграцію"; + +/* No comment provided by engineer. */ +"starting…" = "починаючи…"; + +/* No comment provided by engineer. */ +"Stop" = "Зупинити"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "Зупиніть чат, щоб увімкнути дії з базою даних"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Зупинити чат?"; + +/* cancel file action */ +"Stop file" = "Зупинити файл"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Припинити отримання файлу?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Припинити надсилання файлу?"; + +/* No comment provided by engineer. */ +"Stop sharing" = "Припиніть ділитися"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "Припинити ділитися адресою?"; + +/* authentication reason */ +"Stop SimpleX" = "Зупинити SimpleX"; + +/* No comment provided by engineer. */ +"strike" = "закреслено"; + +/* No comment provided by engineer. */ +"Submit" = "Надіслати"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "Підтримка чату SimpleX"; + +/* No comment provided by engineer. */ +"System" = "Система"; + +/* No comment provided by engineer. */ +"System authentication" = "Автентифікація системи"; + +/* No comment provided by engineer. */ +"Take picture" = "Сфотографуйте"; + +/* No comment provided by engineer. */ +"Tap button " = "Натисніть кнопку "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Натисніть, щоб активувати профіль."; + +/* No comment provided by engineer. */ +"Tap to join" = "Натисніть, щоб приєднатися"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "Натисніть, щоб приєднатися інкогніто"; + +/* No comment provided by engineer. */ +"Tap to start a new chat" = "Натисніть, щоб почати новий чат"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "Тайм-аут TCP-з'єднання"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* server test failure */ +"Test failed at step %@." = "Тест завершився невдало на кроці %@."; + +/* No comment provided by engineer. */ +"Test server" = "Тестовий сервер"; + +/* No comment provided by engineer. */ +"Test servers" = "Тестові сервери"; + +/* No comment provided by engineer. */ +"Tests failed!" = "Тести не пройшли!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "Дякуємо, що встановили SimpleX Chat!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Дякуємо користувачам - [внесок через Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; + +/* No comment provided by engineer. */ +"The 1st platform without any user identifiers – private by design." = "Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном."; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Спроба змінити пароль до бази даних не була завершена."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Прийняте вами з'єднання буде скасовано!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "Контакт, з яким ви поділилися цим посиланням, НЕ зможе підключитися!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Створений архів доступний через Налаштування програми / База даних / Старий архів бази даних."; + +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!"; + +/* No comment provided by engineer. */ +"The group is fully decentralized – it is visible only to the members." = "Група повністю децентралізована - її бачать лише учасники."; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "Хеш попереднього повідомлення відрізняється."; + +/* No comment provided by engineer. */ +"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." = "Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому).\nЦе може статися через помилку або коли з'єднання скомпрометовано."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Повідомлення буде видалено для всіх учасників."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Повідомлення буде позначено як модероване для всіх учасників."; + +/* No comment provided by engineer. */ +"The next generation of private messaging" = "Наступне покоління приватних повідомлень"; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити."; + +/* No comment provided by engineer. */ +"The profile is only shared with your contacts." = "Профіль доступний лише вашим контактам."; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Другу галочку ми пропустили! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "Відправник НЕ буде повідомлений"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "Сервери для нових підключень вашого поточного профілю чату **%@**."; + +/* No comment provided by engineer. */ +"Theme" = "Тема"; + +/* No comment provided by engineer. */ +"There should be at least one user profile." = "Повинен бути принаймні один профіль користувача."; + +/* No comment provided by engineer. */ +"There should be at least one visible user profile." = "Повинен бути принаймні один видимий профіль користувача."; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Ці налаштування стосуються вашого поточного профілю **%@**."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Їх можна перевизначити в налаштуваннях контактів і груп."; + +/* No comment provided by engineer. */ +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться."; + +/* No comment provided by engineer. */ +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені."; + +/* notification title */ +"this contact" = "цей контакт"; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "У цій групі більше %lld учасників, підтвердження доставки не надсилаються."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Цієї групи більше не існує."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**."; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Задати будь-які питання та отримувати новини:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку."; + +/* No comment provided by engineer. */ +"To make a new connection" = "Щоб створити нове з'єднання"; + +/* No comment provided by engineer. */ +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "Для захисту часового поясу у файлах зображень/голосу використовується UTC."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Щоб захистити вашу інформацію, увімкніть SimpleX Lock.\nПеред увімкненням цієї функції вам буде запропоновано пройти автентифікацію."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**."; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату."; + +/* No comment provided by engineer. */ +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях."; + +/* No comment provided by engineer. */ +"Transport isolation" = "Транспортна ізоляція"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту."; + +/* No comment provided by engineer. */ +"Turn off" = "Вимкнути"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "Вимкнути сповіщення?"; + +/* No comment provided by engineer. */ +"Turn on" = "Ввімкнути"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Не вдається записати голосове повідомлення"; + +/* item status description */ +"Unexpected error: %@" = "Неочікувана помилка: %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Неочікуваний стан міграції"; + +/* No comment provided by engineer. */ +"Unfav." = "Нелюб."; + +/* No comment provided by engineer. */ +"Unhide" = "Показати"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Показати профіль чату"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Показати профіль"; + +/* No comment provided by engineer. */ +"Unit" = "Одиниця"; + +/* connection info */ +"unknown" = "невідомий"; + +/* callkit banner */ +"Unknown caller" = "Невідомий абонент"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Невідома помилка бази даних: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Невідома помилка"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим \"Не турбувати\", щоб уникнути переривань."; + +/* No comment provided by engineer. */ +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це.\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею."; + +/* No comment provided by engineer. */ +"Unlock" = "Розблокувати"; + +/* authentication reason */ +"Unlock app" = "Розблокувати додаток"; + +/* No comment provided by engineer. */ +"Unmute" = "Увімкнути звук"; + +/* No comment provided by engineer. */ +"Unread" = "Непрочитане"; + +/* No comment provided by engineer. */ +"Update" = "Оновлення"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "Оновити налаштування хостів .onion?"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Оновити парольну фразу бази даних"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Оновити налаштування мережі?"; + +/* No comment provided by engineer. */ +"Update transport isolation mode?" = "Оновити режим транспортної ізоляції?"; + +/* rcv group event chat item */ +"updated group profile" = "оновлений профіль групи"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів."; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Оновлення та відкритий чат"; + +/* server test step */ +"Upload file" = "Завантажити файл"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Використовуйте хости .onion"; + +/* No comment provided by engineer. */ +"Use chat" = "Використовуйте чат"; + +/* No comment provided by engineer. */ +"Use current profile" = "Використовувати поточний профіль"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Використовуйте для нових з'єднань"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Використовуйте новий профіль інкогніто"; + +/* No comment provided by engineer. */ +"Use server" = "Використовувати сервер"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "Використовувати сервери SimpleX Chat?"; + +/* No comment provided by engineer. */ +"User profile" = "Профіль користувача"; + +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = "Для використання хостів .onion потрібен сумісний VPN-провайдер."; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Використання серверів SimpleX Chat."; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Перевірте безпеку з'єднання"; + +/* No comment provided by engineer. */ +"Verify security code" = "Підтвердіть код безпеки"; + +/* No comment provided by engineer. */ +"Via browser" = "Через браузер"; + +/* chat list item description */ +"via contact address link" = "за посиланням на контактну адресу"; + +/* chat list item description */ +"via group link" = "за посиланням на групу"; + +/* chat list item description */ +"via one-time link" = "за одноразовим посиланням"; + +/* No comment provided by engineer. */ +"via relay" = "за допомогою ретранслятора"; + +/* No comment provided by engineer. */ +"Video call" = "Відеодзвінок"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "відеодзвінок (без шифрування e2e)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "Відео буде отримано, коли ваш контакт завершить завантаження."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "Відео буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "Відео та файли до 1 Гб"; + +/* No comment provided by engineer. */ +"View security code" = "Переглянути код безпеки"; + +/* No comment provided by engineer. */ +"Voice message…" = "Голосове повідомлення…"; + +/* chat feature */ +"Voice messages" = "Голосові повідомлення"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "Голосові повідомлення в цьому чаті заборонені."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this group." = "Голосові повідомлення в цій групі заборонені."; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Голосові повідомлення заборонені!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "в очікуванні відповіді…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "чекаємо на підтвердження…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "Очікування файлу"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Очікування зображення"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Чекаємо на відео"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "хоче зв'язатися з вами!"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Попередження: ви можете втратити деякі дані!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "Сервери WebRTC ICE"; + +/* time unit */ +"weeks" = "тижнів"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Ласкаво просимо %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Вітальне повідомлення"; + +/* No comment provided by engineer. */ +"What's new" = "Що нового"; + +/* No comment provided by engineer. */ +"When available" = "За наявності"; + +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його."; + +/* No comment provided by engineer. */ +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "З необов'язковим вітальним повідомленням."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильний пароль до бази даних"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Неправильний пароль!"; + +/* No comment provided by engineer. */ +"XFTP servers" = "Сервери XFTP"; + +/* pref value */ +"yes" = "так"; + +/* No comment provided by engineer. */ +"You" = "Ти"; + +/* No comment provided by engineer. */ +"You accepted connection" = "Ви прийняли підключення"; + +/* No comment provided by engineer. */ +"You allow" = "Ви дозволяєте"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Ви вже маєте профіль у чаті з таким самим іменем. Будь ласка, виберіть інше ім'я."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Ви вже підключені до %@."; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту."; + +/* No comment provided by engineer. */ +"you are invited to group" = "вас запрошують до групи"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Запрошуємо вас до групи"; + +/* No comment provided by engineer. */ +"you are observer" = "ви спостерігач"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми."; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Ви також можете підключитися за посиланням. Якщо воно відкриється в браузері, натисніть кнопку **Відкрити в мобільному додатку**."; + +/* No comment provided by engineer. */ +"You can create it later" = "Ви можете створити його пізніше"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Ви можете увімкнути пізніше в Налаштуваннях"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Ви можете приховати або вимкнути звук профілю користувача - проведіть по ньому вправо."; + +/* notification body */ +"You can now send messages to %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; + +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо згодом видалите її."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**."; + +/* No comment provided by engineer. */ +"You can share your address as a link or QR code - anybody can connect to you." = "Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Запустити чат можна через Налаштування програми / База даних або перезапустивши програму"; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "Увімкнути SimpleX Lock можна в Налаштуваннях."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Ви можете використовувати розмітку для форматування повідомлень:"; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Ви не можете надсилати повідомлення!"; + +/* chat item text */ +"you changed address" = "ви змінили адресу"; + +/* chat item text */ +"you changed address for %@" = "ви змінили адресу на %@"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "ви змінили роль для себе на %@"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "ви змінили роль %1$@ на %2$@"; + +/* No comment provided by engineer. */ +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Вас не вдалося верифікувати, спробуйте ще раз."; + +/* No comment provided by engineer. */ +"You have no chats" = "У вас немає чатів"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Ви запросили контакт"; + +/* No comment provided by engineer. */ +"You joined this group" = "Ви приєдналися до цієї групи"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи."; + +/* snd group event chat item */ +"you left" = "ти пішов"; + +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Ви відхилили запрошення до групи"; + +/* snd group event chat item */ +"you removed %@" = "ви видалили %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Ви надіслали запрошення до групи"; + +/* chat list item description */ +"you shared one-time link" = "ви поділилися одноразовим посиланням"; + +/* chat list item description */ +"you shared one-time link incognito" = "ви поділилися одноразовим посиланням інкогніто"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Ви будете з'єднані, коли пристрій вашого контакту буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі."; + +/* No comment provided by engineer. */ +"You will join a group this link refers to and connect to its group members." = "Ви приєднаєтеся до групи, на яку посилається це посилання, і з'єднаєтеся з її учасниками."; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Ви не втратите свої контакти, якщо згодом видалите свою адресу."; + +/* No comment provided by engineer. */ +"you: " = "ти: "; + +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Ви намагаєтеся запросити контакт, з яким ви поділилися профілем інкогніто, до групи, в якій ви використовуєте свій основний профіль"; + +/* No comment provided by engineer. */ +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено"; + +/* No comment provided by engineer. */ +"Your %@ servers" = "Ваші сервери %@"; + +/* No comment provided by engineer. */ +"Your calls" = "Твої дзвінки"; + +/* No comment provided by engineer. */ +"Your chat database" = "Ваша база даних чату"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її."; + +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "Ваш профіль у чаті буде надіслано учасникам групи"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Ваші профілі чату"; + +/* No comment provided by engineer. */ +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Для завершення з'єднання ваш контакт має бути онлайн.\nВи можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням)."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "Ваші контакти можуть дозволити повне видалення повідомлень."; + +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Ваші контакти в SimpleX побачать це.\nВи можете змінити його в Налаштуваннях."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "Ваші контакти залишаться на зв'язку."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою."; + +/* No comment provided by engineer. */ +"Your current profile" = "Ваш поточний профіль"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "Ваші сервери ICE"; + +/* No comment provided by engineer. */ +"Your preferences" = "Ваші уподобання"; + +/* No comment provided by engineer. */ +"Your privacy" = "Ваша конфіденційність"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам.\nСервери SimpleX не бачать ваш профіль."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; + +/* No comment provided by engineer. */ +"Your random profile" = "Ваш випадковий профіль"; + +/* No comment provided by engineer. */ +"Your server" = "Ваш сервер"; + +/* No comment provided by engineer. */ +"Your server address" = "Адреса вашого сервера"; + +/* No comment provided by engineer. */ +"Your settings" = "Ваші налаштування"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "Ваша адреса SimpleX"; + +/* No comment provided by engineer. */ +"Your SMP servers" = "Ваші SMP-сервери"; + +/* No comment provided by engineer. */ +"Your XFTP servers" = "Ваші XFTP-сервери"; + diff --git a/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..2e3c6b8930 --- /dev/null +++ b/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,15 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX потребує доступу до камери, щоб сканувати QR-коди для з'єднання з іншими користувачами та для відеодзвінків."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX використовує Face ID для локальної автентифікації"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX потребує доступу до фототеки для збереження захоплених та отриманих медіафайлів"; + diff --git a/scripts/ios/export-localizations.sh b/scripts/ios/export-localizations.sh index ee97415bc0..df880e2694 100755 --- a/scripts/ios/export-localizations.sh +++ b/scripts/ios/export-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en cs de es fr it ja nl pl ru zh-Hans ) +langs=( en cs de es fi fr it ja nl pl ru uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" diff --git a/scripts/ios/import-localizations.sh b/scripts/ios/import-localizations.sh index 40f9d944b9..542c3a7f61 100755 --- a/scripts/ios/import-localizations.sh +++ b/scripts/ios/import-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en cs de es fr it ja nl pl ru th zh-Hans ) +langs=( en cs de es fi fr it ja nl pl ru th uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" From 272b02b6863b7fd18b0ea5cae4d34e5691422d51 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 10 Sep 2023 10:45:30 +0100 Subject: [PATCH 06/13] docs: readme follow updates section, add rel=me for mastodon link --- README.md | 21 ++++++++++++++++++--- website/src/_includes/footer.html | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 561a322c62..403bb1efa6 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ ## Welcome to SimpleX Chat! 1. 📲 [Install the app](#install-the-app). -2. ↔️ [Connect to the team](#connect-to-the-team-via-the-app) and [join user groups](#join-user-groups). +2. ↔️ [Connect to the team](#connect-to-the-team), [join user groups](#join-user-groups) and [follow our updates](#follow-our-updates). 3. 🤝 [Make a private connection](#make-a-private-connection) with a friend. 4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat). 5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations). @@ -40,14 +40,22 @@ - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**! - 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows. -## Connect to the team via the app +## Connect to the team + +You can connect to the team via the app using "chat with the developers button" available when you have no conversations in the profile, "Send questions and ideas" in the app settings or via our [SimpleX address](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). Please connect to: - to ask any questions - to suggest any improvements - to share anything relevant +We are replying the questions manually, so it is not instant – it can take up to 24 hours. + +If you are interested in helping us to integrate open-source language models, and in [joining our team](./docs/JOIN_TEAM.md), please get in touch. + ## Join user groups +You can join the groups created by other users via the new [directory service](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). We are not responsible for the content shared in these groups. + **Please note**: The groups below are created for the users to be able to ask questions, make suggestions and ask questions about SimpleX Chat only. You also can: @@ -79,7 +87,14 @@ There are groups in other languages, that we have the apps interface translated You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code. -You can also join the group created by other users by searching for them via the [directory service](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion). We are not responsible for the content shared in these groups. +## Follow our updates + +We publish our updates and releases via: + +- [Reddit](https://www.reddit.com/r/SimpleXChat/), [Twitter](https://twitter.com/SimpleXChat), [Lemmy](https://lemmy.ml/c/simplex), [Mastodon](https://mastodon.social/@simplex) and [Nostr](https://snort.social/p/npub1exv22uulqnmlluszc4yk92jhs2e5ajcs6mu3t00a6avzjcalj9csm7d828). +- SimpleX Chat [team profile](#connect-to-the-team). +- [blog](https://simplex.chat/blog/) and [RSS feed](https://simplex.chat/feed.rss). +- [mailing list](https://simplex.chat/#join-simplex), very rarely. ## Make a private connection diff --git a/website/src/_includes/footer.html b/website/src/_includes/footer.html index d6b2372c3e..1c23806760 100644 --- a/website/src/_includes/footer.html +++ b/website/src/_includes/footer.html @@ -63,7 +63,7 @@ <path fill-rule="evenodd" clip-rule="evenodd" d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512ZM348.077 268.251C347.047 271.254 346.018 274.257 344.263 277.017C337.633 287.302 327.867 292.139 315.221 292.951C302.173 293.789 289.233 295.487 276.333 297.437C274.064 297.779 271.793 298.108 269.521 298.438C258.687 300.005 247.858 301.572 237.36 304.645C189.946 318.517 148.927 341.344 115.048 374.257C109.197 379.941 103.451 385.688 98.1836 391.819C95.7632 394.637 93.0278 397.201 89.1831 398.521L89.1304 398.538C87.9731 398.938 86.7466 399.359 85.6548 398.491C84.7729 397.79 84.9731 396.874 85.1665 395.992C85.1948 395.862 85.2231 395.733 85.248 395.605C85.6963 393.277 86.7407 391.14 88.1821 389.194C90.146 386.544 92.084 383.878 94.022 381.213C99.7344 373.356 105.446 365.5 111.82 358.036C121.707 346.457 133.167 336.2 144.787 326.072C168.56 305.352 195.75 289.087 225.833 276.646C228.757 275.438 231.732 274.327 234.707 273.217C236.528 272.538 238.349 271.858 240.158 271.155C240.361 271.076 240.574 271.007 240.789 270.938C241.635 270.663 242.518 270.377 243.024 269.423C242.177 268.706 241.15 268.783 240.177 268.857C239.968 268.873 239.762 268.889 239.561 268.896C217.297 269.744 195.694 273.47 174.884 280.725C167.117 283.432 159.668 286.729 152.295 290.172C149.855 291.311 147.46 292.248 145.208 290.121C142.917 287.958 143.432 285.598 144.912 283.182C154.453 267.61 165.746 253.295 179.889 240.66C197.997 224.483 219.395 213.954 244.032 208.49C276.996 201.18 306.323 208.416 332.639 226.84L333.331 227.326C337.818 230.48 342.154 233.529 348.424 232.938C356.094 232.213 362.589 229.362 367.853 224.391C376.977 215.774 378.435 205.5 372.124 194.314C365.358 182.325 356.674 171.414 347.996 160.512L347.97 160.479C344.757 156.442 341.94 152.257 340.075 147.559C337.776 141.771 339.282 136.798 343.771 132.374C346.562 129.623 349.879 127.444 353.499 125.654C352.84 124.302 351.792 124.372 350.828 124.437C350.692 124.445 350.558 124.454 350.427 124.459C349.885 124.479 349.345 124.565 348.806 124.651C348.214 124.745 347.624 124.839 347.031 124.847C346.607 124.852 346.152 124.89 345.689 124.929C343.945 125.074 342.084 125.23 341.357 123.684C340.391 121.631 341.399 119.227 343.191 117.554C347.403 113.623 352.741 112.595 358.621 113.133C364.851 113.703 370.044 116.374 374.959 119.561C377.904 121.469 380.753 123.504 383.548 125.587C386.107 127.494 385.938 128.352 382.863 129.358C378.374 130.828 373.854 132.237 369.158 133.088C368.043 133.29 366.97 133.568 365.925 133.915C365.13 134.178 364.353 134.481 363.586 134.82C356.43 137.985 354.645 142.229 357.784 148.742C360.525 154.429 364.434 159.469 368.344 164.509C369.887 166.498 371.431 168.487 372.902 170.517C373.603 171.483 374.306 172.449 375.008 173.415C379.468 179.541 383.926 185.666 387.914 192.062C400.479 212.217 392.457 231.471 374.209 243.009C372.334 244.194 370.384 245.277 368.434 246.36C365.527 247.975 362.621 249.589 359.962 251.543C354.495 255.563 350.765 260.588 348.648 266.601C348.455 267.149 348.266 267.7 348.077 268.251ZM364.513 333.922C365.149 335.249 364.924 336.668 364.888 338.06C364.714 340.459 365.214 342.489 363.518 344.059C362.183 344.074 361.458 343.352 360.753 342.65C360.563 342.461 360.375 342.273 360.177 342.103C356.286 338.75 352.875 334.988 348.808 331.769C341.752 326.184 333.477 324.483 324.302 324.521C315.342 324.559 307.177 327.164 299.004 329.771C297.717 330.183 296.429 330.594 295.138 330.995L289.747 332.666C278.586 336.121 267.42 339.577 256.422 343.413C246.833 346.757 238.413 339.996 239.31 331.924C240.31 322.924 243.14 314.443 250.956 307.944C254.783 304.762 259.324 303.146 264.323 302.431C267.762 301.938 271.221 301.553 274.679 301.168C276.062 301.014 277.446 300.859 278.828 300.698C279.451 300.625 280.078 300.607 280.639 301.144C280.923 302.208 280.246 302.979 279.585 303.731C279.429 303.91 279.273 304.088 279.131 304.268C274.998 309.516 270.639 314.606 265.876 319.418C265.727 319.568 265.549 319.722 265.365 319.879C264.61 320.528 263.763 321.258 264.454 322.215C265.352 323.459 266.887 322.692 268.166 322.415C273.744 321.205 279.287 319.864 284.829 318.523C294.818 316.107 304.808 313.69 315.003 312.043C328.089 309.928 340.422 311.512 351.549 318.553C357.728 322.463 361.604 327.85 364.513 333.922Z" /> </svg> </a> - <a href="https://mastodon.social/@simplex" target="_blank"> + <a rel="me" href="https://mastodon.social/@simplex" target="_blank"> <svg class="fill-primary-light dark:fill-primary-dark" width="32" height="32" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M256 512C397.385 512 512 397.385 512 256C512 114.615 397.385 0 256 0C114.615 0 0 114.615 0 256C0 397.385 114.615 512 256 512ZM339.329 99.0275C317.654 93.9388 295.564 92.2207 273.231 91.3254C272.186 91.2944 271.157 91.2596 270.14 91.2251C268.091 91.1557 266.089 91.0879 264.086 91.0567C246.35 90.7812 228.654 91.5123 210.994 93.1846C197.065 94.5035 183.244 96.4768 169.675 99.9727C162.199 101.899 154.73 103.994 147.977 107.812C134.904 115.202 125.174 125.999 117.725 138.921C108.323 155.228 103.916 173.02 103.377 191.682C102.882 208.853 102.937 226.049 103.186 243.227C103.396 257.66 104.034 272.095 104.854 286.507C105.678 300.995 107.728 315.348 110.963 329.508C114.411 344.606 119.377 359.13 127.439 372.451C138.635 390.948 154.331 403.859 174.933 410.499C187.604 414.582 200.649 417.051 213.826 418.828C237.461 422.015 261.021 421.897 284.461 417.12C294.296 415.115 303.892 412.325 313.09 408.232C314.413 407.643 314.878 406.946 314.79 405.46C314.549 401.383 314.383 397.303 314.217 393.222C314.143 391.401 314.069 389.581 313.989 387.761C313.915 386.091 313.827 384.422 313.737 382.714C313.695 381.925 313.653 381.128 313.612 380.318C313.426 380.354 313.261 380.382 313.11 380.408C312.825 380.457 312.591 380.497 312.364 380.56C294.125 385.58 275.582 388.162 256.641 387.555C245.877 387.21 235.125 386.735 224.588 384.226C217.159 382.457 210.199 379.654 204.32 374.599C197.016 368.318 193.251 360.094 191.688 350.786C191.199 347.876 190.939 344.927 190.672 341.898C190.548 340.499 190.423 339.084 190.274 337.647C190.638 337.712 190.939 337.764 191.198 337.808C191.658 337.888 191.989 337.945 192.317 338.012C193.433 338.242 194.548 338.478 195.663 338.714C197.9 339.188 200.137 339.662 202.384 340.08C217.874 342.967 233.476 344.947 249.2 346.01C261.24 346.825 273.283 346.962 285.306 346.061C292.653 345.511 299.987 344.776 307.32 344.042C310.694 343.704 314.068 343.366 317.444 343.046C329.993 341.856 342.163 339.055 353.868 334.339C368.08 328.613 381.069 320.958 391.661 309.671C397.704 303.232 402.22 295.971 403.793 287.033C406.787 270.025 408.278 252.868 409.117 235.648C409.506 227.659 409.68 219.659 409.854 211.661C409.877 210.641 409.899 209.622 409.922 208.602C410.225 194.937 409.755 181.331 406.589 167.943C402.18 149.303 393.93 132.763 380.157 119.248C373.506 112.723 366.119 107.246 357.182 104.291C351.293 102.344 345.36 100.443 339.329 99.0275ZM206.301 147.05C215.154 146.705 223.699 148.074 231.686 152.148C239.099 156.066 244.656 161.596 248.834 168.559C250.24 170.904 251.634 173.256 253.028 175.609C253.877 177.042 254.725 178.475 255.577 179.906C255.751 180.198 255.939 180.481 256.169 180.825C256.289 181.005 256.42 181.202 256.567 181.426C256.835 180.985 257.092 180.564 257.34 180.156C257.857 179.31 258.338 178.52 258.815 177.728C259.514 176.567 260.2 175.397 260.885 174.228C262.344 171.739 263.803 169.25 265.39 166.845C274.175 153.529 286.908 147.555 302.51 147.037C311.492 146.739 320.132 148.161 328.153 152.407C340.022 158.69 347.454 168.621 351.49 181.281C353.776 188.451 354.772 195.835 354.782 203.337C354.809 224.399 354.804 245.462 354.8 266.525C354.798 274.145 354.797 281.765 354.796 289.385C354.796 291.539 354.794 291.541 352.618 291.541C345.311 291.541 338.005 291.541 330.699 291.541L322.729 291.541L320.556 291.541V289.22C320.556 282.775 320.555 276.331 320.555 269.886C320.555 249.152 320.555 228.417 320.557 207.683C320.557 203.313 320.182 198.991 318.855 194.802C316.363 186.935 311.094 182.212 302.904 181.058C297.135 180.245 291.436 180.588 286.145 183.355C279.524 186.817 276.269 192.781 274.923 199.777C274.066 204.228 273.672 208.827 273.621 213.366C273.507 223.706 273.527 234.048 273.547 244.39C273.555 248.39 273.563 252.389 273.563 256.389V258.442H239.519C239.519 257.757 239.52 257.127 239.519 256.498C239.515 252.169 239.515 247.841 239.515 243.512C239.515 232.533 239.514 221.553 239.453 210.574C239.422 205.073 238.685 199.648 236.607 194.488C233.57 186.946 228.11 182.277 220.046 181.187C216.643 180.726 213.072 180.737 209.67 181.204C202.089 182.246 197.072 186.701 194.521 193.923C193.001 198.228 192.528 202.699 192.528 207.236C192.526 227.665 192.526 248.094 192.526 268.522C192.526 275.434 192.527 282.345 192.527 289.256V291.425H158.285V289.671C158.285 281.418 158.28 273.166 158.274 264.914C158.259 244.215 158.245 223.516 158.321 202.817C158.362 191.725 160.39 180.983 165.938 171.217C174.826 155.574 188.484 147.745 206.301 147.05Z" /> </svg> From 364d889056ffd93d36bfaac7afeabb8c140e35e0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 10 Sep 2023 10:53:38 +0100 Subject: [PATCH 07/13] docs: add mastodon rel=me to readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 403bb1efa6..d4103fd104 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GitHub downloads](https://img.shields.io/github/downloads/simplex-chat/simplex-chat/total)](https://github.com/simplex-chat/simplex-chat/releases) [![GitHub release](https://img.shields.io/github/v/release/simplex-chat/simplex-chat)](https://github.com/simplex-chat/simplex-chat/releases) [![Join on Reddit](https://img.shields.io/reddit/subreddit-subscribers/SimpleXChat?style=social)](https://www.reddit.com/r/SimpleXChat) -[![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)](https://mastodon.social/@simplex) +<a rel="me" href="https://mastodon.social/@simplex">![Follow on Mastodon](https://img.shields.io/mastodon/follow/108619463746856738?domain=https%3A%2F%2Fmastodon.social&style=social)</a> | 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) | From a87aaa50c79c13ca871b4df55dcba66fd302f450 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 10 Sep 2023 11:21:40 +0100 Subject: [PATCH 08/13] website: add nostr.json for NIP-05 --- website/src/.well-known/nostr.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 website/src/.well-known/nostr.json diff --git a/website/src/.well-known/nostr.json b/website/src/.well-known/nostr.json new file mode 100644 index 0000000000..8c409fb86c --- /dev/null +++ b/website/src/.well-known/nostr.json @@ -0,0 +1,18 @@ +{ + "names": { + "_": "c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171" + }, + "relays": { + "c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171": [ + "wss://nostr.orangepill.dev", + "wss://eden.nostr.land", + "wss://relay.damus.io", + "wss://relay.snort.social", + "wss://relay.current.fyi", + "wss://nos.lol", + "wss://relay.nostr.bg", + "wss://nostr-verified.wellorder.net", + "wss://nostr.milou.lol" + ] + } +} From 54e1e10382b1eb9736b151f25e110187070fa69a Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Sun, 10 Sep 2023 19:05:12 +0300 Subject: [PATCH 09/13] multiplatform: local file encryption (#3043) * multiplatform: file encryption * setting * fixed sharing * check * fixes, change lock icon * update JNI bindings (crashes on desktop) * fix JNI * fix errors and warnings * fix saving --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../common/platform/RecAndPlay.android.kt | 44 +++++++--- .../simplex/common/platform/Share.android.kt | 36 ++++++-- .../views/chat/item/CIImageView.android.kt | 4 +- .../views/chat/item/ChatItemView.android.kt | 2 +- .../chat/item/ImageFullScreenView.android.kt | 4 +- .../common/views/helpers/Utils.android.kt | 47 ++++++---- .../src/commonMain/cpp/android/simplex-api.c | 80 ++++++++++++++++- .../src/commonMain/cpp/desktop/simplex-api.c | 79 ++++++++++++++++- .../chat/simplex/common/model/ChatModel.kt | 12 ++- .../chat/simplex/common/model/CryptoFile.kt | 59 +++++++++++++ .../chat/simplex/common/model/SimpleXAPI.kt | 6 +- .../chat/simplex/common/platform/AppCommon.kt | 3 +- .../chat/simplex/common/platform/Core.kt | 5 ++ .../chat/simplex/common/platform/Files.kt | 11 +++ .../simplex/common/platform/RecAndPlay.kt | 4 +- .../chat/simplex/common/platform/Share.kt | 3 +- .../simplex/common/views/chat/ChatView.kt | 10 +-- .../simplex/common/views/chat/ComposeView.kt | 21 +++-- .../common/views/chat/ComposeVoiceView.kt | 3 +- .../common/views/chat/item/CIFileView.kt | 17 +++- .../common/views/chat/item/CIImageView.kt | 29 +++--- .../common/views/chat/item/CIMetaView.kt | 13 ++- .../views/chat/item/CIRcvDecryptionError.kt | 4 +- .../common/views/chat/item/CIVoiceView.kt | 16 ++-- .../common/views/chat/item/ChatItemView.kt | 4 +- .../common/views/chat/item/FramedItemView.kt | 2 +- .../views/chat/item/ImageFullScreenView.kt | 6 +- .../common/views/chat/item/TextItemView.kt | 2 +- .../common/views/database/DatabaseView.kt | 2 +- .../simplex/common/views/helpers/Utils.kt | 88 +++++++++++++------ .../simplex/common/views/newchat/QRCode.kt | 3 +- .../views/usersettings/PrivacySettings.kt | 1 + .../views/usersettings/UserProfilesView.kt | 7 +- .../commonMain/resources/MR/base/strings.xml | 1 + .../resources/MR/images/ic_lock_open.svg | 1 - .../MR/images/ic_lock_open_right.svg | 1 + .../common/platform/AppCommon.desktop.kt | 2 + .../common/platform/RecAndPlay.desktop.kt | 4 +- .../simplex/common/platform/Share.desktop.kt | 12 ++- .../views/chat/item/CIImageView.desktop.kt | 2 +- .../views/chat/item/ChatItemView.desktop.kt | 2 +- .../chat/item/ImageFullScreenView.desktop.kt | 9 +- .../common/views/helpers/Utils.desktop.kt | 12 ++- 43 files changed, 522 insertions(+), 151 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt delete mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index c24ade47d9..ebc1b416b5 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -1,6 +1,5 @@ package chat.simplex.common.platform -import android.app.Application import android.content.Context import android.media.* import android.media.AudioManager.AudioPlaybackCallback @@ -8,10 +7,10 @@ import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED import android.os.Build import androidx.compose.runtime.* -import chat.simplex.res.MR -import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.* import chat.simplex.common.platform.AudioPlayer.duration import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR import kotlinx.coroutines.* import java.io.* @@ -134,20 +133,25 @@ actual object AudioPlayer: AudioPlayerInterface { } // Returns real duration of the track - private fun start(filePath: String, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { - if (!File(filePath).exists()) { - Log.e(TAG, "No such file: $filePath") + private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + val absoluteFilePath = getAppFilePath(fileSource.filePath) + if (!File(absoluteFilePath).exists()) { + Log.e(TAG, "No such file: ${fileSource.filePath}") return null } VideoPlayer.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != filePath) { + if (current == null || current.first != fileSource.filePath) { stopListener() player.reset() runCatching { - player.setDataSource(filePath) + if (fileSource.cryptoArgs != null) { + player.setDataSource(CryptoMediaSource(readCryptoFile(absoluteFilePath, fileSource.cryptoArgs))) + } else { + player.setDataSource(absoluteFilePath) + } }.onFailure { Log.e(TAG, it.stackTraceToString()) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) @@ -162,7 +166,7 @@ actual object AudioPlayer: AudioPlayerInterface { } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = filePath to onProgressUpdate + currentlyPlaying.value = fileSource.filePath to onProgressUpdate progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { @@ -229,7 +233,7 @@ actual object AudioPlayer: AudioPlayerInterface { } override fun play( - filePath: String?, + fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, @@ -238,7 +242,7 @@ actual object AudioPlayer: AudioPlayerInterface { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(filePath ?: return, progress.value) { pro, state -> + val realDuration = start(fileSource, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -283,3 +287,21 @@ actual object AudioPlayer: AudioPlayerInterface { } actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer + +class CryptoMediaSource(val data: ByteArray) : MediaDataSource() { + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + if (position >= data.size) return -1 + + val endPosition: Int = (position + size).toInt() + var sizeLeft: Int = size + if (endPosition > data.size) { + sizeLeft -= endPosition - data.size + } + + System.arraycopy(data, position.toInt(), buffer, offset, sizeLeft) + return sizeLeft + } + + override fun getSize(): Long = data.size.toLong() + override fun close() {} +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt index 811974b2d5..a370bbf405 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt @@ -8,13 +8,15 @@ import android.provider.MediaStore import android.webkit.MimeTypeMap import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler -import chat.simplex.common.helpers.toUri -import chat.simplex.common.model.CIFile -import chat.simplex.common.views.helpers.generalGetString -import chat.simplex.common.views.helpers.getAppFileUri +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import chat.simplex.common.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.* import java.io.BufferedOutputStream import java.io.File import chat.simplex.res.MR +import java.io.ByteArrayOutputStream actual fun ClipboardManager.shareText(text: String) { val sendIntent: Intent = Intent().apply { @@ -28,9 +30,17 @@ actual fun ClipboardManager.shareText(text: String) { androidAppContext.startActivity(shareIntent) } -actual fun shareFile(text: String, filePath: String) { - val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) - val ext = filePath.substringAfterLast(".") +actual fun shareFile(text: String, fileSource: CryptoFile) { + val uri = if (fileSource.cryptoArgs != null) { + val tmpFile = File(tmpDir, fileSource.filePath) + tmpFile.deleteOnExit() + ChatModel.filesToDelete.add(tmpFile) + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath) + FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(tmpFile.absolutePath)).toURI() + } else { + getAppFileUri(fileSource.filePath) + } + val ext = fileSource.filePath.substringAfterLast(".") val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND @@ -84,8 +94,16 @@ fun saveImage(ciFile: CIFile?) { uri?.let { androidAppContext.contentResolver.openOutputStream(uri)?.let { stream -> val outputStream = BufferedOutputStream(stream) - File(filePath).inputStream().use { it.copyTo(outputStream) } - outputStream.close() + if (ciFile.fileSource?.cryptoArgs != null) { + createTmpFileAndDelete { tmpFile -> + decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath) + tmpFile.inputStream().use { it.copyTo(outputStream) } + } + outputStream.close() + } else { + File(filePath).inputStream().use { it.copyTo(outputStream) } + outputStream.close() + } showToast(generalGetString(MR.strings.image_saved)) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index dc8e9dd541..28c00ec018 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -19,7 +19,7 @@ import java.net.URI @Composable actual fun SimpleAndAnimatedImageView( - uri: URI, + data: ByteArray, imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, @@ -27,7 +27,7 @@ actual fun SimpleAndAnimatedImageView( ) { val context = LocalContext.current val imagePainter = rememberAsyncImagePainter( - ImageRequest.Builder(context).data(data = uri.toUri()).size(coil.size.Size.ORIGINAL).build(), + ImageRequest.Builder(context).data(data = data).size(coil.size.Size.ORIGINAL).build(), placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil imageLoader = imageLoader ) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt index 15421299a8..8bb70c4a09 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt @@ -26,7 +26,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) { @Composable actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) { val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) - ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = { + ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { when (cItem.content.msgContent) { is MsgContent.MCImage -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt index d23ee58db2..ade538a044 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt @@ -26,7 +26,7 @@ import dev.icerock.moko.resources.compose.stringResource import java.net.URI @Composable -actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) { +actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) { // I'm making a new instance of imageLoader here because if I use one instance in multiple places // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want val imageLoader = ImageLoader.Builder(LocalContext.current) @@ -40,7 +40,7 @@ actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageB .build() Image( rememberAsyncImagePainter( - ImageRequest.Builder(LocalContext.current).data(data = uri.toUri()).size(Size.ORIGINAL).build(), + ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(), placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil imageLoader = imageLoader ), diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 67c41c3d79..e3c857716d 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.helpers -import android.app.Application import android.content.res.Resources import android.graphics.* import android.graphics.Typeface @@ -12,11 +11,8 @@ import android.text.Spanned import android.text.SpannedString import android.text.style.* import android.util.Base64 -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.BaselineShift @@ -159,17 +155,18 @@ actual fun getAppFileUri(fileName: String): URI = FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(getAppFilePath(fileName))).toURI() // https://developer.android.com/training/data-storage/shared/documents-files#bitmap -actual fun getLoadedImage(file: CIFile?): ImageBitmap? { +actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? { val filePath = getLoadedFilePath(file) - return if (filePath != null) { + return if (filePath != null && file != null) { try { - val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) - val parcelFileDescriptor = androidAppContext.contentResolver.openFileDescriptor(uri.toUri(), "r") - val fileDescriptor = parcelFileDescriptor?.fileDescriptor - val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000) - parcelFileDescriptor?.close() - image.asImageBitmap() + val data = if (file.fileSource?.cryptoArgs != null) { + readCryptoFile(getAppFilePath(file.fileSource.filePath), file.fileSource.cryptoArgs) + } else { + File(getAppFilePath(file.fileName)).readBytes() + } + decodeSampledBitmapFromByteArray(data, 1000, 1000).asImageBitmap() to data } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) null } } else { @@ -178,17 +175,17 @@ actual fun getLoadedImage(file: CIFile?): ImageBitmap? { } // https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap -private fun decodeSampledBitmapFromFileDescriptor(fileDescriptor: FileDescriptor?, reqWidth: Int, reqHeight: Int): Bitmap { +private fun decodeSampledBitmapFromByteArray(data: ByteArray, reqWidth: Int, reqHeight: Int): Bitmap { // First decode with inJustDecodeBounds=true to check dimensions return BitmapFactory.Options().run { inJustDecodeBounds = true - BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this) + BitmapFactory.decodeByteArray(data, 0, data.size) // Calculate inSampleSize inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) // Decode bitmap with inSampleSize set inJustDecodeBounds = false - BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this) + BitmapFactory.decodeByteArray(data, 0, data.size) } } @@ -254,6 +251,26 @@ actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitma }?.asImageBitmap() } +actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? { + return if (Build.VERSION.SDK_INT >= 31) { + val source = ImageDecoder.createSource(data) + try { + ImageDecoder.decodeBitmap(source) + } catch (e: android.graphics.ImageDecoder.DecodeException) { + Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}") + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.image_decoding_exception_title), + text = generalGetString(MR.strings.image_decoding_exception_desc) + ) + } + null + } + } else { + BitmapFactory.decodeByteArray(data, 0, data.size) + }?.asImageBitmap() +} + actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? { return if (Build.VERSION.SDK_INT >= 28) { val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index 7b6c032c8a..eb4714710c 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -1,5 +1,6 @@ #include <jni.h> -//#include <string.h> +#include <string.h> +#include <stdint.h> //#include <stdlib.h> //#include <android/log.h> @@ -45,6 +46,10 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); +extern char *chat_write_file(const char *path, char *ptr, int length); +extern char *chat_read_file(const char *path, const char *key, const char *nonce); +extern char *chat_encrypt_file(const char *from_path, const char *to_path); +extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path); JNIEXPORT jobjectArray JNICALL Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { @@ -115,3 +120,76 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, __unused (*env)->ReleaseStringUTFChars(env, salt, _salt); return res; } + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { + const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); + jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer); + jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer); + jstring res = (*env)->NewStringUTF(env, chat_write_file(_path, buff, capacity)); + (*env)->ReleaseStringUTFChars(env, path, _path); + return res; +} + +JNIEXPORT jobjectArray JNICALL +Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) { + const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); + const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE); + const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE); + + jbyte *res = chat_read_file(_path, _key, _nonce); + (*env)->ReleaseStringUTFChars(env, path, _path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + + jint status = (jint)res[0]; + jbyteArray arr; + if (status == 0) { + union { + uint32_t w; + uint8_t b[4]; + } len; + len.b[0] = (uint8_t)res[1]; + len.b[1] = (uint8_t)res[2]; + len.b[2] = (uint8_t)res[3]; + len.b[3] = (uint8_t)res[4]; + arr = (*env)->NewByteArray(env, len.w); + (*env)->SetByteArrayRegion(env, arr, 0, len.w, res + 5); + } else { + int len = strlen(res + 1); // + 1 offset here is to not include status byte + arr = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, arr, 0, len, res + 1); + } + + jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); + jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"), + (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Integer"), "<init>", "(I)V"), + status); + (*env)->SetObjectArrayElement(env, ret, 0, statusObj); + (*env)->SetObjectArrayElement(env, ret, 1, arr); + return ret; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) { + const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE); + const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_encrypt_file(_from_path, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring key, jstring nonce, jstring to_path) { + const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE); + const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE); + const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE); + const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 8e869ca2d9..ddc5c92f93 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -1,6 +1,7 @@ #include <jni.h> #include <string.h> #include <stdlib.h> +#include <stdint.h> // from the RTS void hs_init(int * argc, char **argv[]); @@ -20,7 +21,10 @@ extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); extern char *chat_password_hash(const char *pwd, const char *salt); - +extern char *chat_write_file(const char *path, char *ptr, int length); +extern char *chat_read_file(const char *path, const char *key, const char *nonce); +extern char *chat_encrypt_file(const char *from_path, const char *to_path); +extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path); // As a reference: https://stackoverflow.com/a/60002045 jstring decode_to_utf8_string(JNIEnv *env, char *string) { @@ -128,3 +132,76 @@ Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass cl (*env)->ReleaseStringUTFChars(env, salt, _salt); return res; } + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { + const char *_path = encode_to_utf8_chars(env, path); + jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer); + jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer); + jstring res = decode_to_utf8_string(env, chat_write_file(_path, buff, capacity)); + (*env)->ReleaseStringUTFChars(env, path, _path); + return res; +} + +JNIEXPORT jobjectArray JNICALL +Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) { + const char *_path = encode_to_utf8_chars(env, path); + const char *_key = encode_to_utf8_chars(env, key); + const char *_nonce = encode_to_utf8_chars(env, nonce); + + jbyte *res = chat_read_file(_path, _key, _nonce); + (*env)->ReleaseStringUTFChars(env, path, _path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + + jint status = (jint)res[0]; + jbyteArray arr; + if (status == 0) { + union { + uint32_t w; + uint8_t b[4]; + } len; + len.b[0] = (uint8_t)res[1]; + len.b[1] = (uint8_t)res[2]; + len.b[2] = (uint8_t)res[3]; + len.b[3] = (uint8_t)res[4]; + arr = (*env)->NewByteArray(env, len.w); + (*env)->SetByteArrayRegion(env, arr, 0, len.w, res + 5); + } else { + int len = strlen(res + 1); // + 1 offset here is to not include status byte + arr = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, arr, 0, len, res + 1); + } + + jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); + jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"), + (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Integer"), "<init>", "(I)V"), + status); + (*env)->SetObjectArrayElement(env, ret, 0, statusObj); + (*env)->SetObjectArrayElement(env, ret, 1, arr); + return ret; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) { + const char *_from_path = encode_to_utf8_chars(env, from_path); + const char *_to_path = encode_to_utf8_chars(env, to_path); + jstring res = decode_to_utf8_string(env, chat_encrypt_file(_from_path, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring key, jstring nonce, jstring to_path) { + const char *_from_path = encode_to_utf8_chars(env, from_path); + const char *_key = encode_to_utf8_chars(env, key); + const char *_nonce = encode_to_utf8_chars(env, nonce); + const char *_to_path = encode_to_utf8_chars(env, to_path); + jstring res = decode_to_utf8_string(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index a0120eb96e..fc0867aad6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -13,6 +13,7 @@ import chat.simplex.common.views.chat.ComposeState import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.platform.AudioPlayer +import chat.simplex.common.platform.chatController import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @@ -1394,6 +1395,13 @@ data class ChatItem ( private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID + val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null + + val encryptLocalFile: Boolean + get() = file?.fileProtocol == FileProtocol.XFTP && + content.msgContent !is MsgContent.MCVideo && + chatController.appPrefs.privacyEncryptLocalFiles.get() + val memberDisplayName: String? get() = if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName else null @@ -2077,7 +2085,7 @@ class CIFile( } @Serializable -class CryptoFile( +data class CryptoFile( val filePath: String, val cryptoArgs: CryptoFileArgs? ) { @@ -2087,7 +2095,7 @@ class CryptoFile( } @Serializable -class CryptoFileArgs(val fileKey: String, val fileNonce: String) +data class CryptoFileArgs(val fileKey: String, val fileNonce: String) class CancelAction( val uiActionId: StringResource, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt new file mode 100644 index 0000000000..037d27af33 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt @@ -0,0 +1,59 @@ +package chat.simplex.common.model + +import chat.simplex.common.platform.* +import kotlinx.serialization.* +import java.nio.ByteBuffer + +@Serializable +sealed class WriteFileResult { + @Serializable @SerialName("result") data class Result(val cryptoArgs: CryptoFileArgs): WriteFileResult() + @Serializable @SerialName("error") data class Error(val writeError: String): WriteFileResult() +} + +/* + fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { + val str = chatWriteFile(path, data) + return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) { + is WriteFileResult.Result -> d.cryptoArgs + is WriteFileResult.Error -> throw Exception(d.writeError) + } +} +* */ + +fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { + val buffer = ByteBuffer.allocateDirect(data.size) + buffer.put(data) + buffer.rewind() + val str = chatWriteFile(path, buffer) + return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) { + is WriteFileResult.Result -> d.cryptoArgs + is WriteFileResult.Error -> throw Exception(d.writeError) + } +} + +fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray { + val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce) + val status = (res[0] as Integer).toInt() + val arr = res[1] as ByteArray + if (status == 0) { + return arr + } else { + throw Exception(String(arr)) + } +} + +fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs { + val str = chatEncryptFile(fromPath, toPath) + val d = json.decodeFromString(WriteFileResult.serializer(), str) + return when (d) { + is WriteFileResult.Result -> d.cryptoArgs + is WriteFileResult.Error -> throw Exception(d.writeError) + } +} + +fun decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) { + val err = chatDecryptFile(fromPath, cryptoArgs.fileKey, cryptoArgs.fileNonce, toPath) + if (err != "") { + throw Exception(err) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 612c167bfe..0a178ca2f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource -import chat.simplex.common.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* @@ -94,6 +93,7 @@ class AppPreferences { val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) + val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -249,6 +249,7 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" + private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" @@ -1413,8 +1414,7 @@ object ChatController { ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { - // TODO encrypt images and voice - withApi { receiveFile(r.user, file.fileId, encrypted = false, auto = true) } + withApi { receiveFile(r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) } } if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id)) { ntfManager.notifyMessageReceived(r.user, cInfo, cItem) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt index fff77ee23b..d36a6aec16 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -1,8 +1,9 @@ package chat.simplex.common.platform import chat.simplex.common.BuildConfigCommon -import chat.simplex.common.model.ChatController +import chat.simplex.common.model.* import chat.simplex.common.ui.theme.DefaultTheme +import java.io.File import java.util.* enum class AppPlatform { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 341f4e9548..801a0270e2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -4,6 +4,7 @@ import chat.simplex.common.model.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.serialization.decodeFromString +import java.nio.ByteBuffer // ghc's rts external fun initHS() @@ -19,6 +20,10 @@ external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String external fun chatPasswordHash(pwd: String, salt: String): String +external fun chatWriteFile(path: String, buffer: ByteBuffer): String +external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> +external fun chatEncryptFile(fromPath: String, toPath: String): String +external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String val chatModel: ChatModel get() = chatController.chatModel diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index 53b0f8bd96..71a9f204f8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -2,6 +2,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable import chat.simplex.common.model.CIFile +import chat.simplex.common.model.CryptoFile import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import java.io.* @@ -71,6 +72,16 @@ fun getLoadedFilePath(file: CIFile?): String? { } } +fun getLoadedFileSource(file: CIFile?): CryptoFile? { + val f = file?.fileSource?.filePath + return if (f != null && file.loaded) { + val filePath = getAppFilePath(f) + if (File(filePath).exists()) file.fileSource else null + } else { + null + } +} + /** * [rememberedValue] is used in `remember(rememberedValue)`. So when the value changes, file saver will update a callback function * */ diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt index bbc5cbe667..2d6bb2a371 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -1,7 +1,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.MutableState -import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.* import kotlinx.coroutines.CoroutineScope interface RecorderInterface { @@ -18,7 +18,7 @@ expect class RecorderNative(): RecorderInterface interface AudioPlayerInterface { fun play( - filePath: String?, + fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt index 03ad4b5441..72bb3caaac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt @@ -2,8 +2,9 @@ package chat.simplex.common.platform import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler +import chat.simplex.common.model.CryptoFile expect fun UriHandler.sendEmail(subject: String, body: CharSequence) expect fun ClipboardManager.shareText(text: String) -expect fun shareFile(text: String, filePath: String) +expect fun shareFile(text: String, fileSource: CryptoFile) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index f6e328afdb..c8381cdcb7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1117,7 +1117,7 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha } sealed class ProviderMedia { - data class Image(val uri: URI, val image: ImageBitmap): ProviderMedia() + data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia() data class Video(val uri: URI, val preview: String): ProviderMedia() } @@ -1155,11 +1155,11 @@ private fun providerForGallery( val item = item(internalIndex, initialChatId)?.second ?: return null return when (item.content.msgContent) { is MsgContent.MCImage -> { - val imageBitmap: ImageBitmap? = getLoadedImage(item.file) + val res = getLoadedImage(item.file) val filePath = getLoadedFilePath(item.file) - if (imageBitmap != null && filePath != null) { - val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) - ProviderMedia.Image(uri, imageBitmap) + if (res != null && filePath != null) { + val (imageBitmap: ImageBitmap, data: ByteArray) = res + ProviderMedia.Image(data, imageBitmap) } else null } is MsgContent.MCVideo -> { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 01090705d7..4d6bc297f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -411,8 +411,8 @@ fun ComposeView( is ComposePreview.MediaPreview -> { preview.content.forEachIndexed { index, it -> val file = when (it) { - is UploadContent.SimpleImage -> saveImage(it.uri) - is UploadContent.AnimatedImage -> saveAnimImage(it.uri) + is UploadContent.SimpleImage -> saveImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) + is UploadContent.AnimatedImage -> saveAnimImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) is UploadContent.Video -> saveFileFromUri(it.uri, encrypted = false) } if (file != null) { @@ -429,16 +429,21 @@ fun ComposeView( val tmpFile = File(preview.voice) AudioPlayer.stop(tmpFile.absolutePath) val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, ""))) - withContext(Dispatchers.IO) { - Files.move(tmpFile.toPath(), actualFile.toPath()) - } - // TODO encrypt voice files - files.add(CryptoFile.plain(actualFile.name)) + files.add(withContext(Dispatchers.IO) { + if (chatController.appPrefs.privacyEncryptLocalFiles.get()) { + val args = encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath) + tmpFile.delete() + CryptoFile(actualFile.name, args) + } else { + Files.move(tmpFile.toPath(), actualFile.toPath()) + CryptoFile.plain(actualFile.name) + } + }) deleteUnusedFiles() msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000)) } is ComposePreview.FilePreview -> { - val file = saveFileFromUri(preview.uri, encrypted = false) + val file = saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) if (file != null) { files.add((file)) msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else "")) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt index 99d7de96be..a4c90d30dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt @@ -17,6 +17,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.CryptoFile import chat.simplex.common.model.durationText import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -52,7 +53,7 @@ fun ComposeVoiceView( IconButton( onClick = { if (!audioPlaying.value) { - AudioPlayer.play(filePath, audioPlaying, progress, duration, false) + AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, false) } else { AudioPlayer.pause(audioPlaying, progress) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 4642600fcb..8de805ba54 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -71,7 +71,8 @@ fun CIFileView( when (file.fileStatus) { is CIFileStatus.RcvInvitation -> { if (fileSizeValid()) { - receiveFile(file.fileId, false) + val encrypted = file.fileProtocol == FileProtocol.XFTP && chatController.appPrefs.privacyEncryptLocalFiles.get() + receiveFile(file.fileId, encrypted) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), @@ -184,9 +185,9 @@ fun CIFileView( ) { fileIndicator() val metaReserve = if (edited) - " " + " " else - " " + " " if (file != null) { Column { Text( @@ -211,7 +212,15 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = rememberFileChooserLauncher(false, ciFile) { to: URI? -> val filePath = getLoadedFilePath(ciFile) if (filePath != null && to != null) { - copyFileToFile(File(filePath), to) {} + if (ciFile?.fileSource?.cryptoArgs != null) { + createTmpFileAndDelete { tmpFile -> + decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath) + copyFileToFile(tmpFile, to) {} + tmpFile.delete() + } + } else { + copyFileToFile(File(filePath), to) {} + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 75d6a9c304..23d1f1d0cc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -29,6 +29,8 @@ import java.net.URI fun CIImageView( image: String, file: CIFile?, + encryptLocalFile: Boolean, + metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState<Boolean>, receiveFile: (Long, Boolean) -> Unit @@ -48,7 +50,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = Color.White + tint = metaColor ) } @@ -132,28 +134,31 @@ fun CIImageView( return false } - fun imageAndFilePath(file: CIFile?): Pair<ImageBitmap?, String?> { - val imageBitmap: ImageBitmap? = getLoadedImage(file) - val filePath = getLoadedFilePath(file) - return imageBitmap to filePath + fun imageAndFilePath(file: CIFile?): Triple<ImageBitmap, ByteArray, String>? { + val res = getLoadedImage(file) + if (res != null) { + val (imageBitmap: ImageBitmap, data: ByteArray) = res + val filePath = getLoadedFilePath(file)!! + return Triple(imageBitmap, data, filePath) + } + return null } Box( Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), contentAlignment = Alignment.TopEnd ) { - val (imageBitmap, filePath) = remember(file) { imageAndFilePath(file) } - if (imageBitmap != null && filePath != null) { - val uri = remember(filePath) { getAppFileUri(filePath.substringAfterLast(File.separator)) } - SimpleAndAnimatedImageView(uri, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) }) + val res = remember(file) { imageAndFilePath(file) } + if (res != null) { + val (imageBitmap, data, _) = res + SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) }) } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { when (file.fileStatus) { CIFileStatus.RcvInvitation -> if (fileSizeValid()) { - // TODO encrypt image - receiveFile(file.fileId, false) + receiveFile(file.fileId, encryptLocalFile) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), @@ -187,7 +192,7 @@ fun CIImageView( @Composable expect fun SimpleAndAnimatedImageView( - uri: URI, + data: ByteArray, imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index ab121c6272..72f7137b55 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -44,14 +44,14 @@ fun CIMetaView( modifier = Modifier.padding(start = 3.dp) ) } else { - CIMetaText(chatItem.meta, timedMessagesTTL, metaColor, paleMetaColor) + CIMetaText(chatItem.meta, timedMessagesTTL, encrypted = chatItem.encryptedFile, metaColor, paleMetaColor) } } } @Composable // changing this function requires updating reserveSpaceForMeta -private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color, paleColor: Color) { +private fun CIMetaText(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, color: Color, paleColor: Color) { if (meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) Spacer(Modifier.width(3.dp)) @@ -77,11 +77,15 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color, paleColor: Col StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent) Spacer(Modifier.width(4.dp)) } + if (encrypted != null) { + StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) + Spacer(Modifier.width(4.dp)) + } Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } // the conditions in this function should match CIMetaText -fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String { +fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?): String { val iconSpace = " " var res = "" if (meta.itemEdited) res += iconSpace @@ -95,6 +99,9 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String { if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) { res += iconSpace } + if (encrypted != null) { + res += iconSpace + } return res + meta.timestampText } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 2918d885b1..8de309fc8f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -166,7 +166,7 @@ fun DecryptionErrorItemFixButton( Text( buildAnnotatedString { append(generalGetString(MR.strings.fix_connection)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } withStyle(reserveTimestampStyle) { append(" ") } // for icon }, color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary @@ -196,7 +196,7 @@ fun DecryptionErrorItem( Text( buildAnnotatedString { withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 6ec39bb4f0..941bc315b6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -20,8 +20,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* -import chat.simplex.common.platform.getLoadedFilePath -import chat.simplex.common.platform.AudioPlayer +import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged @@ -45,14 +44,16 @@ fun CIVoiceView( ) { if (file != null) { val f = file.fileSource?.filePath - val filePath = remember(f, file.fileStatus) { getLoadedFilePath(file) } + val fileSource = remember(f, file.fileStatus) { getLoadedFileSource(file) } var brokenAudio by rememberSaveable(f) { mutableStateOf(false) } val audioPlaying = rememberSaveable(f) { mutableStateOf(false) } val progress = rememberSaveable(f) { mutableStateOf(0) } val duration = rememberSaveable(f) { mutableStateOf(providedDurationSec * 1000) } val play = { - AudioPlayer.play(filePath, audioPlaying, progress, duration, true) - brokenAudio = !audioPlaying.value + if (fileSource != null) { + AudioPlayer.play(fileSource, audioPlaying, progress, duration, true) + brokenAudio = !audioPlaying.value + } } val pause = { AudioPlayer.pause(audioPlaying, progress) @@ -67,7 +68,7 @@ fun CIVoiceView( } } VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { - AudioPlayer.seekTo(it, progress, filePath) + AudioPlayer.seekTo(it, progress, fileSource?.filePath) } } else { VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile) @@ -269,8 +270,7 @@ private fun VoiceMsgIndicator( } } else { if (file?.fileStatus is CIFileStatus.RcvInvitation) { - // TODO encrypt voice - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, false) }, {}, longClick = longClick) + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, chatController.appPrefs.privacyEncryptLocalFiles.get()) }, {}, longClick = longClick) } else if (file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index cc2d97e3f5..60ef7e8cfe 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -191,9 +191,9 @@ fun ChatItemView( } val clipboard = LocalClipboardManager.current ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { - val filePath = getLoadedFilePath(cItem.file) + val fileSource = getLoadedFileSource(cItem.file) when { - filePath != null -> shareFile(cItem.text, filePath) + fileSource != null -> shareFile(cItem.text, fileSource) else -> clipboard.shareText(cItem.content.text) } showMenu.value = false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 92cf62a855..122e54c3b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -226,7 +226,7 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, ci.encryptLocalFile, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 270c671fc1..9664cabc41 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -123,8 +123,8 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> // LALAL // https://github.com/JetBrains/compose-multiplatform/pull/2015/files#diff-841b3825c504584012e1d1c834d731bae794cce6acad425d81847c8bbbf239e0R24 if (media is ProviderMedia.Image) { - val (uri: URI, imageBitmap: ImageBitmap) = media - FullScreenImageView(modifier, uri, imageBitmap) + val (data: ByteArray, imageBitmap: ImageBitmap) = media + FullScreenImageView(modifier, data, imageBitmap) } else if (media is ProviderMedia.Video) { val preview = remember(media.uri.path) { base64ToBitmap(media.preview) } VideoView(modifier, media.uri, preview, index == settledCurrentPage) @@ -138,7 +138,7 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> } @Composable -expect fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) +expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) @Composable private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 64855e3195..eabab138ba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -76,7 +76,7 @@ fun MarkdownText ( val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { "\n" } else if (meta != null) { - reserveSpaceForMeta(meta, chatTTL) + reserveSpaceForMeta(meta, chatTTL, null) // LALAL } else { " " } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 9ecd7dae31..fa0f8f54d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -178,7 +178,7 @@ fun DatabaseLayout( SectionView(stringResource(MR.strings.chat_database_section)) { val unencrypted = chatDbEncrypted == false SettingsActionItem( - if (unencrypted) painterResource(MR.images.ic_lock_open) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) + if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_lock), stringResource(MR.strings.database_passphrase), click = showSettingsModal() { DatabaseEncryptionView(it) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index b9eeee12bc..6aaf7a9fdf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -67,7 +67,7 @@ const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB expect fun getAppFileUri(fileName: String): URI // https://developer.android.com/training/data-storage/shared/documents-files#bitmap -expect fun getLoadedImage(file: CIFile?): ImageBitmap? +expect fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? expect fun getFileName(uri: URI): String? @@ -77,6 +77,8 @@ expect fun getFileSize(uri: URI): Long? expect fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean = true): ImageBitmap? +expect fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? + expect fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean = true): Any? fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverrides? { @@ -95,31 +97,34 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri return null } -fun saveImage(uri: URI): CryptoFile? { +fun saveImage(uri: URI, encrypted: Boolean): CryptoFile? { val bitmap = getBitmapFromUri(uri) ?: return null - return saveImage(bitmap) + return saveImage(bitmap, encrypted) } -fun saveImage(image: ImageBitmap): CryptoFile? { - // TODO encrypt image +fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? { return try { val ext = if (image.hasAlpha()) "png" else "jpg" val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE) - val fileToSave = generateNewFileName("IMG", ext) - val file = File(getAppFilePath(fileToSave)) - val output = FileOutputStream(file) - dataResized.writeTo(output) - output.flush() - output.close() - CryptoFile.plain(fileToSave) + val destFileName = generateNewFileName("IMG", ext) + val destFile = File(getAppFilePath(destFileName)) + if (encrypted) { + val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray()) + CryptoFile(destFileName, args) + } else { + val output = FileOutputStream(destFile) + dataResized.writeTo(output) + output.flush() + output.close() + CryptoFile.plain(destFileName) + } } catch (e: Exception) { Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}") null } } -fun saveAnimImage(uri: URI): CryptoFile? { - // TODO encrypt image +fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { return try { val filename = getFileName(uri)?.lowercase() var ext = when { @@ -129,15 +134,15 @@ fun saveAnimImage(uri: URI): CryptoFile? { } // Just in case the image has a strange extension if (ext.length < 3 || ext.length > 4) ext = "gif" - val fileToSave = generateNewFileName("IMG", ext) - val file = File(getAppFilePath(fileToSave)) - val output = FileOutputStream(file) - uri.inputStream().use { input -> - output.use { output -> - input?.copyTo(output) - } + val destFileName = generateNewFileName("IMG", ext) + val destFile = File(getAppFilePath(destFileName)) + if (encrypted) { + val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readAllBytes() ?: return null) + CryptoFile(destFileName, args) + } else { + Files.copy(uri.inputStream(), destFile.toPath()) + CryptoFile.plain(destFileName) } - CryptoFile.plain(fileToSave) } catch (e: Exception) { Log.e(TAG, "Util.kt saveAnimImage error: ${e.message}") null @@ -150,22 +155,40 @@ fun saveFileFromUri(uri: URI, encrypted: Boolean): CryptoFile? { return try { val inputStream = uri.inputStream() val fileToSave = getFileName(uri) - // TODO encrypt file if "encrypted" is true - if (inputStream != null && fileToSave != null) { + return if (inputStream != null && fileToSave != null) { val destFileName = uniqueCombine(fileToSave) val destFile = File(getAppFilePath(destFileName)) - Files.copy(inputStream, destFile.toPath()) - CryptoFile.plain(destFileName) + if (encrypted) { + createTmpFileAndDelete { tmpFile -> + Files.copy(inputStream, tmpFile.toPath()) + val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath) + CryptoFile(destFileName, args) + } + } else { + Files.copy(inputStream, destFile.toPath()) + CryptoFile.plain(destFileName) + } } else { Log.e(TAG, "Util.kt saveFileFromUri null inputStream") null } } catch (e: Exception) { - Log.e(TAG, "Util.kt saveFileFromUri error: ${e.message}") + Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}") null } } +fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T { + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + ChatModel.filesToDelete.add(tmpFile) + try { + return onCreated(tmpFile) + } finally { + tmpFile.delete() + } +} + fun generateNewFileName(prefix: String, ext: String): String { val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) sdf.timeZone = TimeZone.getTimeZone("GMT") @@ -266,6 +289,17 @@ fun blendARGB( return Color(r, g, b, a) } +fun InputStream.toByteArray(): ByteArray = + ByteArrayOutputStream().use { output -> + val b = ByteArray(4096) + var n = read(b) + while (n != -1) { + output.write(b, 0, n); + n = read(b) + } + return output.toByteArray() + } + expect fun ByteArray.toBase64StringForPassphrase(): String // Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index a848d3777b..6632925964 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource import boofcv.alg.drawing.FiducialImageEngine import boofcv.alg.fiducial.qrcode.* +import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.helpers.* @@ -45,7 +46,7 @@ fun QRCode( .let { if (withLogo) it.addLogo() else it } val file = saveTempImageUncompressed(image, false) if (file != null) { - shareFile("", file.absolutePath) + shareFile("", CryptoFile.plain(file.absolutePath)) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 81d56a3816..ef0940b2a0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -64,6 +64,7 @@ fun PrivacySettingsView( SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles) SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SettingsPreferenceItem( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index ac3a68fc49..7929413c93 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -164,10 +164,9 @@ private fun UserProfilesLayout( ) { if (profileHidden.value) { SectionView { - SettingsActionItem(painterResource(MR.images.ic_lock_open), stringResource(MR.strings.enter_password_to_show), click = { + SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = { profileHidden.value = false - } - ) + }) } SectionSpacer() } @@ -223,7 +222,7 @@ private fun UserView( Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { DefaultDropdownMenu(showMenu) { if (user.hidden) { - ItemAction(stringResource(MR.strings.user_unhide), painterResource(MR.images.ic_lock_open), onClick = { + ItemAction(stringResource(MR.strings.user_unhide), painterResource(MR.images.ic_lock_open_right), onClick = { showMenu.value = false unhideUser(user) }) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 2b83ff869a..ae55ad7e58 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -855,6 +855,7 @@ <string name="privacy_and_security">Privacy & security</string> <string name="your_privacy">Your privacy</string> <string name="protect_app_screen">Protect app screen</string> + <string name="encrypt_local_files">Encrypt local files</string> <string name="auto_accept_images">Auto-accept images</string> <string name="send_link_previews">Send link previews</string> <string name="privacy_show_last_messages">Show last messages</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg deleted file mode 100644 index bf6b7b47b4..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h387v-95.385q0-53.782-37.373-91.198Q534.254 201 479.863 201q-46.363 0-81.363 28T354 300.5q-3 13-11.75 21.25T321.983 330q-12.311 0-20.397-8.5-8.086-8.5-6.086-20 10-68 61.902-113t122.629-45q77.383 0 131.926 54.551Q666.5 252.603 666.5 330v95H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm0-57.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222 482.5v431-431Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg new file mode 100644 index 0000000000..3188cf798e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M222-142.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222-142.5v-431 431Zm0 57.5q-23.719 0-40.609-16.891Q164.5-118.781 164.5-142.5v-431q0-23.719 16.891-40.609Q198.281-631 222-631h329.5v-95.018q0-77.832 54.349-132.157Q660.198-912.5 738-912.5q70 0 121.25 44T922-759q2 11.5-6.638 22.25T895.75-726q-12.66 0-20.705-6-8.045-6-9.545-18.5-9-44.5-44.55-74.5T738-855q-54.333 0-91.667 37.333Q609-780.333 609-726.231V-631h129q23.719 0 40.609 16.891Q795.5-597.219 795.5-573.5v431q0 23.719-16.891 40.609Q761.719-85 738-85H222Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 471389d0cd..612217925b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -25,6 +25,8 @@ fun initApp() { initChatController() runMigrations() } + // LALAL + //testCrypto() } private fun applyAppLocale() { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index 2ba6f3b3f6..6e85ea91c6 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -1,7 +1,7 @@ package chat.simplex.common.platform import androidx.compose.runtime.MutableState -import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.* import chat.simplex.common.views.usersettings.showInDevelopingAlert import kotlinx.coroutines.CoroutineScope @@ -18,7 +18,7 @@ actual class RecorderNative: RecorderInterface { } actual object AudioPlayer: AudioPlayerInterface { - override fun play(filePath: String?, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) { + override fun play(fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, resetOnEnd: Boolean) { showInDevelopingAlert() } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt index 84e24a1d55..1d5ab45bbb 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt @@ -3,6 +3,8 @@ package chat.simplex.common.platform import androidx.compose.ui.platform.ClipboardManager import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.AnnotatedString +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.getAppFileUri import chat.simplex.common.views.helpers.withApi import java.io.File import java.net.URI @@ -20,12 +22,16 @@ actual fun ClipboardManager.shareText(text: String) { showToast(MR.strings.copied.localized()) } -actual fun shareFile(text: String, filePath: String) { +actual fun shareFile(text: String, fileSource: CryptoFile) { withApi { FileChooserLauncher(false) { to: URI? -> if (to != null) { - copyFileToFile(File(filePath), to) {} + if (fileSource.cryptoArgs != null) { + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, to.path) + } else { + copyFileToFile(File(fileSource.filePath), to) {} + } } - }.launch(filePath) + }.launch(fileSource.filePath) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt index 214946b1c9..711e09267d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt @@ -11,7 +11,7 @@ import java.net.URI @Composable actual fun SimpleAndAnimatedImageView( - uri: URI, + data: ByteArray, imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt index 9b265a5f5f..c1d9eeec52 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -31,7 +31,7 @@ actual fun ReactionIcon(text: String, fontSize: TextUnit) { @Composable actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) { - ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = { + ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { when (cItem.content.msgContent) { is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } else -> {} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt index e4e483092d..a73c2784ed 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt @@ -4,19 +4,16 @@ import androidx.compose.foundation.Image import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.layout.ContentScale import chat.simplex.common.platform.VideoPlayer -import chat.simplex.common.views.helpers.getBitmapFromUri +import chat.simplex.common.views.helpers.getBitmapFromByteArray import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import java.net.URI @Composable -actual fun FullScreenImageView(modifier: Modifier, uri: URI, imageBitmap: ImageBitmap) { +actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) { Image( - getBitmapFromUri(uri, false) ?: MR.images.decentralized.image.toComposeImageBitmap(), + getBitmapFromByteArray(data, false) ?: MR.images.decentralized.image.toComposeImageBitmap(), contentDescription = stringResource(MR.strings.image_descr), contentScale = ContentScale.Fit, modifier = modifier, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt index f4a9ac9b78..4fa768a5d3 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -6,8 +6,10 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Density import chat.simplex.common.model.CIFile +import chat.simplex.common.model.readCryptoFile import chat.simplex.common.platform.* import chat.simplex.common.simplexWindowState +import java.io.ByteArrayInputStream import java.io.File import java.net.URI import javax.imageio.ImageIO @@ -88,11 +90,12 @@ actual fun escapedHtmlToAnnotatedString(text: String, density: Density): Annotat actual fun getAppFileUri(fileName: String): URI = URI("file:" + appFilesDir.absolutePath + File.separator + fileName) -actual fun getLoadedImage(file: CIFile?): ImageBitmap? { +actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? { val filePath = getLoadedFilePath(file) return if (filePath != null) { - val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) - getBitmapFromUri(uri, false) + val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes() + val bitmap = getBitmapFromByteArray(data, false) + if (bitmap != null) bitmap to data else null } else { null } @@ -107,6 +110,9 @@ actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length() actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = ImageIO.read(uri.inputStream()).toComposeImageBitmap() +actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? = + ImageIO.read(ByteArrayInputStream(data)).toComposeImageBitmap() + // LALAL implement to support animated drawable actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? = null From 55954a004bcfd2d196d505176a0e7c15e3e4cacc Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 10 Sep 2023 18:53:34 +0100 Subject: [PATCH 10/13] android, desktop: notices about SOCKS proxy limitations (#3044) --- .../common/views/usersettings/NetworkAndServers.kt | 8 +++++++- .../common/src/commonMain/resources/MR/base/strings.xml | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 447b65eff6..f2fee926a7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -180,7 +180,13 @@ fun NetworkAndServersView( SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) } if (networkUseSocksProxy.value) { - SectionCustomFooter { Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) } + SectionCustomFooter { + Column { + Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) + } + } Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) } else { Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ae55ad7e58..43c237f972 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -615,7 +615,7 @@ <string name="network_use_onion_hosts_required">Required</string> <string name="network_use_onion_hosts_prefer_desc">Onion hosts will be used when available.</string> <string name="network_use_onion_hosts_no_desc">Onion hosts will not be used.</string> - <string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.</string> + <string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string> <string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string> <string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string> @@ -626,6 +626,7 @@ <string name="network_session_mode_entity_description"><![CDATA[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"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and link previews use direct network connection.]]></string> <string name="appearance_settings">Appearance</string> <string name="customize_theme_title">Customize theme</string> <string name="theme_colors_section_title">THEME COLORS</string> From 7b582b2cf90706eb00a51ce27e297923cc470611 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:04:50 +0100 Subject: [PATCH 11/13] android, desktop: update SOCKS notice --- .../common/src/commonMain/resources/MR/base/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 43c237f972..8e035420d5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -626,7 +626,7 @@ <string name="network_session_mode_entity_description"><![CDATA[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"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string> - <string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and link previews use direct network connection.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string> <string name="appearance_settings">Appearance</string> <string name="customize_theme_title">Customize theme</string> <string name="theme_colors_section_title">THEME COLORS</string> From 2dff6c88594d221997246b024b5ef1eda708d30c Mon Sep 17 00:00:00 2001 From: Alexander Bondarenko <486682+dpwiz@users.noreply.github.com> Date: Sun, 10 Sep 2023 22:40:15 +0300 Subject: [PATCH 12/13] core: do not subscribe to new connections from iOS NSE (subscribe=off flag), subscribe in app when it activates (#3016) * Trace auto-subs flag * Replace Bool with SubscriptionMode * Add subscriptionMode to chat controller * Start using subscriptionMode in event handlers * Add need_subs to chat connections * Add onlyNeeded to subscribeUserConnections * Post-rebase fixes * Pass onlyNeeded to Store functions * Drop needs_sub for connections registered with agent * update simplexmq, fix activate * fix rebase, reduce diff * fix rebase, tests * fix rebase, executeMany, always subscribe on activate * test * update queries * Update src/Simplex/Chat.hs Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * unset connections to subscribe on start * update simplexmq --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 194 ++++++++++++------ src/Simplex/Chat/Controller.hs | 11 +- .../M20230903_connections_to_subscribe.hs | 20 ++ src/Simplex/Chat/Migrations/chat_schema.sql | 2 + src/Simplex/Chat/Store/Connections.hs | 24 ++- src/Simplex/Chat/Store/Direct.hs | 27 +-- src/Simplex/Chat/Store/Files.hs | 33 +-- src/Simplex/Chat/Store/Groups.hs | 51 ++--- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Store/Profiles.hs | 8 +- src/Simplex/Chat/Store/Shared.hs | 11 +- stack.yaml | 2 +- tests/ChatTests/Direct.hs | 31 +++ 16 files changed, 286 insertions(+), 137 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs diff --git a/cabal.project b/cabal.project index ec72b72fcc..983468726a 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 351f42650c57f310fc1ea858ff9b7178823f1fd4 + tag: 0cabe0690beee90f460ad7bada72294222e7e109 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index dbbc7475c3..493985085a 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."351f42650c57f310fc1ea858ff9b7178823f1fd4" = "12r13yc0qk9dkii58808862wraqrk66rzmkrgyp6lg1xrazrd0d2"; + "https://github.com/simplex-chat/simplexmq.git"."0cabe0690beee90f460ad7bada72294222e7e109" = "1yfcrifb2l59wgl14q56ywlil2g2zs57ic62s617whh3w2mnh0kz"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 335e0ee108..7750069b50 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -110,6 +110,7 @@ library Simplex.Chat.Migrations.M20230814_indexes Simplex.Chat.Migrations.M20230827_file_encryption Simplex.Chat.Migrations.M20230829_connections_chat_vrange + Simplex.Chat.Migrations.M20230903_connections_to_subscribe Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 359e7f5b5d..49c5fc94ed 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -89,7 +89,7 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), UserProtocol, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport.Client (defaultSocksProxy) @@ -194,6 +194,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize notifyQ <- newTBQueueIO tbqSize + subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty @@ -207,7 +208,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen showLiveItems <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg tempDirectory <- newTVarIO tempDir - pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile} + pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, subscriptionMode, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile} where configServers :: DefaultAgentServers configServers = @@ -246,6 +247,8 @@ cfgServers = \case startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ()) startChatController subConns enableExpireCIs startXFTPWorkers = do asks smpAgent >>= resumeAgentClient + unless subConns $ + chatWriteVar subscriptionMode SMOnlyCreate users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers) restoreCalls s <- asks agentAsync @@ -255,7 +258,7 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do a1 <- async $ race_ notificationSubscriber agentSubscriber a2 <- if subConns - then Just <$> async (subscribeUsers users) + then Just <$> async (subscribeUsers False users) else pure Nothing atomically . writeTVar s $ Just (a1, a2) when startXFTPWorkers $ do @@ -283,14 +286,14 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do startExpireCIThread user setExpireCIFlag user True -subscribeUsers :: forall m. ChatMonad' m => [User] -> m () -subscribeUsers users = do +subscribeUsers :: forall m. ChatMonad' m => Bool -> [User] -> m () +subscribeUsers onlyNeeded users = do let (us, us') = partition activeUser users subscribe us subscribe us' where subscribe :: [User] -> m () - subscribe = mapM_ $ runExceptT . subscribeUserConnections Agent.subscribeConnections + subscribe = mapM_ $ runExceptT . subscribeUserConnections onlyNeeded Agent.subscribeConnections startFilesToReceive :: forall m. ChatMonad' m => [User] -> m () startFilesToReceive users = do @@ -464,14 +467,16 @@ processChatCommand = \case APIActivateChat -> withUser $ \_ -> do restoreCalls withAgent foregroundAgent - withStoreCtx' (Just "APIActivateChat, getUsers") getUsers >>= void . forkIO . startFilesToReceive + users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers + void . forkIO $ subscribeUsers True users + void . forkIO $ startFilesToReceive users setAllExpireCIFlags True ok_ APISuspendChat t -> do setAllExpireCIFlags False withAgent (`suspendAgent` t) ok_ - ResubscribeAllConnections -> withStoreCtx' (Just "ResubscribeAllConnections, getUsers") getUsers >>= subscribeUsers >> ok_ + ResubscribeAllConnections -> withStoreCtx' (Just "ResubscribeAllConnections, getUsers") getUsers >>= subscribeUsers False >> ok_ -- has to be called before StartChat SetTempFolder tf -> do createDirectoryIfMissing True tf @@ -567,15 +572,16 @@ processChatCommand = \case smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do + subMode <- chatReadVar subscriptionMode (agentConnId_, fileConnReq) <- if isJust fileInline then pure (Nothing, Nothing) - else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing) + else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode) let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} chSize <- asks $ fileChunkSize . config withStore' $ \db -> do - ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize + ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize subMode fileStatus <- case fileInline of Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1 _ -> pure CIFSSndStored @@ -1273,8 +1279,9 @@ processChatCommand = \case APIAddContact userId incognito -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile + subMode <- chatReadVar subscriptionMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode + conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode toView $ CRNewContactConnection user conn pure $ CRInvitation user cReq conn AddContact incognito -> withUser $ \User {userId} -> @@ -1295,12 +1302,13 @@ processChatCommand = \case Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' Nothing -> throwChatError CEConnectionIncognitoChangeProhibited APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do + subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing dm <- directMessage $ XInfo profileToSend - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode + conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode toView $ CRNewContactConnection user conn pure $ CRSentConfirmation user APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq @@ -1317,8 +1325,9 @@ processChatCommand = \case ListContacts -> withUser $ \User {userId} -> processChatCommand $ APIListContacts userId APICreateMyAddress userId -> withUserId userId $ \user -> withChatLock "createMyAddress" . procCmd $ do - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing - withStore $ \db -> createUserContactLink db user connId cReq + subMode <- chatReadVar subscriptionMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing subMode + withStore $ \db -> createUserContactLink db user connId cReq subMode pure $ CRUserContactLinkCreated user cReq CreateMyAddress -> withUser $ \User {userId} -> processChatCommand $ APICreateMyAddress userId @@ -1423,8 +1432,9 @@ processChatCommand = \case case contactMember contact members of Nothing -> do gVar <- asks idsDrg - (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing - member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq + subMode <- chatReadVar subscriptionMode + (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode + member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole} @@ -1443,10 +1453,11 @@ processChatCommand = \case let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation Contact {activeConn = Connection {peerChatVRange}} = ct withChatLock "joinGroup" . procCmd $ do + subMode <- chatReadVar subscriptionMode dm <- directMessage $ XGrpAcpt (memberId (membership :: GroupMember)) - agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm + agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm subMode withStore' $ \db -> do - createMemberConnection db userId fromMember agentConnId peerChatVRange + createMemberConnection db userId fromMember agentConnId peerChatVRange subMode updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted updateCIGroupInvitationStatus user @@ -1557,9 +1568,10 @@ processChatCommand = \case assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact $ Just crClientData - withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) subMode + withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo cReq mRole APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withChatLock "groupLinkMemberRole " $ do gInfo <- withStore $ \db -> getGroupInfo db user groupId @@ -1845,13 +1857,14 @@ processChatCommand = \case (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> drgRandomBytes 16 xContactId <- maybe randomXContactId pure xContactId_ + subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing dm <- directMessage (XContact profileToSend $ Just xContactId) - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId + conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode toView $ CRNewContactConnection user conn pure $ CRSentInvitation user incognitoProfile contactMember :: Contact -> [GroupMember] -> Maybe GroupMember @@ -2240,9 +2253,11 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI case (xftpRcvFile, fileConnReq) of -- direct file protocol (Nothing, Just connReq) -> do - connIds <- joinAgentConnectionAsync user True connReq =<< directMessage (XFileAcpt fName) + subMode <- chatReadVar subscriptionMode + dm <- directMessage $ XFileAcpt fName + connIds <- joinAgentConnectionAsync user True connReq dm subMode filePath <- getRcvFilePath fileId filePath_ fName True - withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath + withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath subMode -- XFTP (Just XFTPRcvFile {cryptoArgs}, _) -> do filePath <- getRcvFilePath fileId filePath_ fName False @@ -2283,8 +2298,9 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName | otherwise -> do -- accepting via a new connection - connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation - withStoreCtx (Just "acceptFile, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath + subMode <- chatReadVar subscriptionMode + connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation subMode + withStoreCtx (Just "acceptFile, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath subMode receiveInline :: m Bool receiveInline = do ChatConfig {fileChunkSize, inlineFiles = InlineFilesConfig {receiveChunks, offerChunks}} <- asks config @@ -2356,17 +2372,19 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do + subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile dm <- directMessage $ XInfo profileToSend - acId <- withAgent $ \a -> acceptContact a True invId dm - withStore' $ \db -> createAcceptedContact db user acId cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile + acId <- withAgent $ \a -> acceptContact a True invId dm subMode + withStore' $ \db -> createAcceptedContact db user acId cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do + subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile - (cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend + (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode withStore' $ \db -> do - ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile + ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode setCommandConnId db user cmdId connId pure ct @@ -2413,18 +2431,28 @@ agentSubscriber = do type AgentBatchSubscribe m = AgentClient -> [ConnId] -> ExceptT AgentErrorType m (Map ConnId (Either AgentErrorType ())) -subscribeUserConnections :: forall m. ChatMonad m => AgentBatchSubscribe m -> User -> m () -subscribeUserConnections agentBatchSubscribe user@User {userId} = do +subscribeUserConnections :: forall m. ChatMonad m => Bool -> AgentBatchSubscribe m -> User -> m () +subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do -- get user connections ce <- asks $ subscriptionEvents . config - (ctConns, cts) <- getContactConns - (ucConns, ucs) <- getUserContactLinkConns - (gs, mConns, ms) <- getGroupMemberConns - (sftConns, sfts) <- getSndFileTransferConns - (rftConns, rfts) <- getRcvFileTransferConns - (pcConns, pcs) <- getPendingContactConns + (conns, cts, ucs, gs, ms, sfts, rfts, pcs) <- + if onlyNeeded + then do + (conns, entities) <- withStore' getConnectionsToSubscribe + let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities + pure (conns, cts, ucs, [], ms, sfts, rfts, pcs) + else do + withStore' unsetConnectionToSubscribe + (ctConns, cts) <- getContactConns + (ucConns, ucs) <- getUserContactLinkConns + (gs, mConns, ms) <- getGroupMemberConns + (sftConns, sfts) <- getSndFileTransferConns + (rftConns, rfts) <- getRcvFileTransferConns + (pcConns, pcs) <- getPendingContactConns + let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] + pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) -- subscribe using batched commands - rs <- withAgent (`agentBatchSubscribe` concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]) + rs <- withAgent $ \a -> agentBatchSubscribe a conns -- send connection events to view contactSubsToView rs cts ce contactLinkSubsToView rs ucs @@ -2433,6 +2461,29 @@ subscribeUserConnections agentBatchSubscribe user@User {userId} = do rcvFileSubsToView rs rfts pendingConnSubsToView rs pcs where + addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case + RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs) + RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs') + RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs) + SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs) + RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs) + UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs) + addConn :: Connection -> a -> Map ConnId a -> Map ConnId a + addConn = M.insert . aConnId + toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} = + PendingContactConnection + { pccConnId = connId, + pccAgentConnId = agentConnId, + pccConnStatus = connStatus, + viaContactUri = False, + viaUserContactLink, + groupLinkId, + customUserProfileId, + connReqInv = Nothing, + localAlias, + createdAt, + updatedAt = createdAt + } getContactConns :: m ([ConnId], Map ConnId Contact) getContactConns = do cts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContacts") getUserContacts @@ -2971,9 +3022,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) forM_ groupId_ $ \groupId -> do + subMode <- chatReadVar subscriptionMode gVar <- asks idsDrg - groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation - withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds peerChatVRange + groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode + withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds peerChatVRange subMode _ -> pure () Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> when (maybe False ((== ConnReady) . connStatus) activeConn) $ do @@ -3920,8 +3972,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do then unless cancelled $ case fileConnReq_ of -- receiving via a separate connection Just fileConnReq -> do - connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk - withStore' $ \db -> createSndDirectFTConnection db user fileId connIds + subMode <- chatReadVar subscriptionMode + dm <- directMessage XOk + connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode + withStore' $ \db -> createSndDirectFTConnection db user fileId connIds subMode -- receiving inline _ -> do event <- withStore $ \db -> do @@ -4015,10 +4069,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do if fName == fileName then unless cancelled $ case (fileConnReq_, activeConn) of (Just fileConnReq, _) -> do + subMode <- chatReadVar subscriptionMode -- receiving via a separate connection -- [async agent commands] no continuation needed, but command should be asynchronous for stability - connIds <- joinAgentConnectionAsync user True fileConnReq =<< directMessage XOk - withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m + dm <- directMessage XOk + connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode + withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m subMode (_, Just conn) -> do -- receiving inline event <- withStore $ \db -> do @@ -4049,9 +4105,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId if sameGroupLinkId groupLinkId groupLinkId' then do - connIds <- joinAgentConnectionAsync user True connRequest =<< directMessage (XGrpAcpt memberId) + subMode <- chatReadVar subscriptionMode + dm <- directMessage $ XGrpAcpt memberId + connIds <- joinAgentConnectionAsync user True connRequest dm subMode withStore' $ \db -> do - createMemberConnectionAsync db user hostId connIds peerChatVRange + createMemberConnectionAsync db user hostId connIds peerChatVRange subMode updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) @@ -4285,18 +4343,19 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do then messageWarning "x.grp.mem.intro ignored: member already exists" else do when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) + subMode <- chatReadVar subscriptionMode -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second - groupConnIds <- createConn + groupConnIds <- createConn subMode directConnIds <- case memberChatVRange of - Nothing -> Just <$> createConn + Nothing -> Just <$> createConn subMode Just mcvr - | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> Just <$> createConn -- pure Nothing - | otherwise -> Just <$> createConn + | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> Just <$> createConn subMode -- pure Nothing + | otherwise -> Just <$> createConn subMode let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing - void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId + void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId subMode _ -> messageError "x.grp.mem.intro can be only sent by host member" where - createConn = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation + createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation subMode sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m () sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do @@ -4330,14 +4389,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do Nothing -> withStore $ \db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced Just m' -> pure m' withStore' $ \db -> saveMemberInvitation db toMember introInv + subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito dm <- directMessage $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) -- [async agent commands] no continuation needed, but commands should be asynchronous for stability - groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm - directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm + groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm subMode + directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm subMode let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange - withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId + withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId subMode xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m () xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta @@ -4838,16 +4898,16 @@ cancelCIFile user file_ = fileAgentConnIds <- cancelFile' user (mkCIFileInfo file) True deleteAgentConnectionsAsync user fileAgentConnIds -createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> m (CommandId, ConnId) -createAgentConnectionAsync user cmdFunction enableNtfs cMode = do +createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> m (CommandId, ConnId) +createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction - connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode + connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode subMode pure (cmdId, connId) -joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> m (CommandId, ConnId) -joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do +joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m (CommandId, ConnId) +joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo + connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo subMode pure (cmdId, connId) allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m () @@ -4857,11 +4917,11 @@ allowAgentConnectionAsync user conn@Connection {connId} confId msg = do withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm withStore' $ \db -> updateConnectionStatus db conn ConnAccepted -agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> m (CommandId, ConnId) -agentAcceptContactAsync user enableNtfs invId msg = do +agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> m (CommandId, ConnId) +agentAcceptContactAsync user enableNtfs invId msg subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact dm <- directMessage msg - connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm + connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm subMode pure (cmdId, connId) deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m () diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 6380da6478..af9aa964cf 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -62,7 +62,7 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) @@ -176,6 +176,7 @@ data ChatController = ChatController outputQ :: TBQueue (Maybe CorrId, ChatResponse), notifyQ :: TBQueue Notification, sendNotification :: Notification -> IO (), + subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, sndFiles :: TVar (Map Int64 Handle), rcvFiles :: TVar (Map Int64 Handle), @@ -960,6 +961,14 @@ type ChatMonad' m = (MonadUnliftIO m, MonadReader ChatController m) type ChatMonad m = (ChatMonad' m, MonadError ChatError m) +chatReadVar :: ChatMonad' m => (ChatController -> TVar a) -> m a +chatReadVar f = asks f >>= readTVarIO +{-# INLINE chatReadVar #-} + +chatWriteVar :: ChatMonad' m => (ChatController -> TVar a) -> a -> m () +chatWriteVar f value = asks f >>= atomically . (`writeTVar` value) +{-# INLINE chatWriteVar #-} + tryChatError :: ChatMonad m => m a -> m (Either ChatError a) tryChatError = tryAllErrors mkChatError {-# INLINE tryChatError #-} diff --git a/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs b/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs new file mode 100644 index 0000000000..48ad8dbf86 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230903_connections_to_subscribe where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230903_connections_to_subscribe :: Query +m20230903_connections_to_subscribe = + [sql| +ALTER TABLE connections ADD COLUMN to_subscribe INTEGER DEFAULT 0 NOT NULL; +CREATE INDEX idx_connections_to_subscribe ON connections(to_subscribe); +|] + +down_m20230903_connections_to_subscribe :: Query +down_m20230903_connections_to_subscribe = + [sql| +DROP INDEX idx_connections_to_subscribe; +ALTER TABLE connections DROP COLUMN to_subscribe; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index f0731b6ef4..c71cc9aa90 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -287,6 +287,7 @@ CREATE TABLE connections( auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL), peer_chat_min_version INTEGER NOT NULL DEFAULT 1, peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + to_subscribe INTEGER DEFAULT 0 NOT NULL, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE @@ -711,3 +712,4 @@ CREATE INDEX idx_chat_items_user_id_item_status ON chat_items( user_id, item_status ); +CREATE INDEX idx_connections_to_subscribe ON connections(to_subscribe); diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 4bd092b7bd..025755c924 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -6,25 +7,30 @@ module Simplex.Chat.Store.Connections ( getConnectionEntity, + getConnectionsToSubscribe, + unsetConnectionToSubscribe, ) where import Control.Applicative ((<|>)) import Control.Monad.Except import Data.Int (Int64) -import Data.Maybe (fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text) import Data.Time.Clock (UTCTime (..)) -import Database.SQLite.Simple ((:.) (..)) +import Database.SQLite.Simple (Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow') import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Util (eitherToMaybe) getConnectionEntity :: DB.Connection -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity getConnectionEntity db user@User {userId, userContactId} agentConnId = do @@ -142,3 +148,17 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do userContact_ :: [(ConnReqContact, Maybe GroupId)] -> Either StoreError UserContact userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId} userContact_ _ = Left SEUserContactLinkNotFound + +getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity]) +getConnectionsToSubscribe db = do + aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1" + entities <- forM aConnIds $ \acId -> do + getUserByAConnId db acId >>= \case + Just user -> eitherToMaybe <$> runExceptT (getConnectionEntity db user acId) + Nothing -> pure Nothing + unsetConnectionToSubscribe db + let connIds = map (\(AgentConnId connId) -> connId) aConnIds + pure (connIds, catMaybes entities) + +unsetConnectionToSubscribe :: DB.Connection -> IO () +unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1" diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 7df2858e9f..609da128a7 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -75,6 +75,7 @@ import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Version getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection @@ -109,8 +110,8 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let pccConnStatus = ConnJoined @@ -119,10 +120,10 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, - via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?) + via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt)) + ((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt, subMode == SMOnlyCreate)) pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} @@ -162,17 +163,17 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" (userId, cReqHash) -createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> IO PendingContactConnection -createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile = do +createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> IO PendingContactConnection +createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile DB.execute db [sql| INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, custom_user_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) + (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, custom_user_profile_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?,?) |] - (userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt) + (userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt, subMode == SMOnlyCreate) pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} @@ -587,8 +588,8 @@ deleteContactRequest db User {userId} contactRequestId = do (userId, userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact -createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do +createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> IO Contact +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case @@ -600,7 +601,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id) VALUES (?,?,?,?,?,?,?,?,?)" (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId) contactId <- insertedRowId db - activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt + activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt} @@ -616,7 +617,7 @@ getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO C getContact_ db user@User {userId} contactId deleted = ExceptT . fmap join . firstRow (toContactOrError user) (SEContactNotFound contactId) $ DB.query - db + db [sql| SELECT -- Contact diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index fa085908e0..685d67e4de 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -100,6 +100,7 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Protocol (SubscriptionMode (..)) getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -156,8 +157,8 @@ getPendingSndChunks db fileId connId = |] (fileId, connId) -createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> IO FileTransferMeta -createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize = do +createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> SubscriptionMode -> IO FileTransferMeta +createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize subMode = do currentTs <- getCurrentTime DB.execute db @@ -165,7 +166,7 @@ createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitatio ((userId, contactId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs)) fileId <- insertedRowId db forM_ acId_ $ \acId -> do - Connection {connId} <- createSndFileConnection_ db userId fileId acId + Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode let fileStatus = FSNew DB.execute db @@ -173,10 +174,10 @@ createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitatio (fileId, fileStatus, fileInline, connId, currentTs, currentTs) pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} -createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> IO () -createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) = do +createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () +createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) subMode = do currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId + Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode setCommandConnId db user cmdId connId DB.execute db @@ -193,10 +194,10 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation fileId <- insertedRowId db pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} -createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> IO () -createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} = do +createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO () +createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId + Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode setCommandConnId db user cmdId connId DB.execute db @@ -422,10 +423,10 @@ getChatRefByFileId db User {userId} fileId = |] (userId, fileId) -createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection -createSndFileConnection_ db userId fileId agentConnId = do +createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection +createSndFileConnection_ db userId fileId agentConnId subMode = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs + createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -644,14 +645,14 @@ getRcvFileTransfer db User {userId} fileId = do _ -> pure Nothing cancelled = fromMaybe False cancelled_ -acceptRcvFileTransfer :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> ExceptT StoreError IO AChatItem -acceptRcvFileTransfer db user@User {userId} fileId (cmdId, acId) connStatus filePath = ExceptT $ do +acceptRcvFileTransfer :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem +acceptRcvFileTransfer db user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do currentTs <- getCurrentTime acceptRcvFT_ db user fileId filePath Nothing currentTs DB.execute db - "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (acId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) + "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" + (acId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs, subMode == SMOnlyCreate) connId <- insertedRowId db setCommandConnId db user cmdId connId runExceptT $ getChatItemByFileId db user fileId diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 59f1b6090d..81fc37cce0 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -105,6 +105,7 @@ import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Protocol (SubscriptionMode) import Simplex.Messaging.Util (eitherToMaybe) import Simplex.Messaging.Version import UnliftIO.STM @@ -135,8 +136,8 @@ toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just member Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences)) toMaybeGroupMember _ _ = Nothing -createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> ExceptT StoreError IO () -createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole = +createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () +createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole subMode = checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do currentTs <- getCurrentTime DB.execute @@ -144,7 +145,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = @@ -536,7 +537,7 @@ groupMemberQuery = LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) FROM connections cc - where cc.user_id = ? AND cc.group_member_id = m.group_member_id + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id ) |] @@ -614,12 +615,12 @@ getGroupInvitation db user groupId = firstRow fromOnly (SEGroupNotFound groupId) $ DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId) -createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember -createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile, activeConn = Connection {peerChatVRange}} memberRole agentConnId connRequest = +createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile, activeConn = Connection {peerChatVRange}} memberRole agentConnId connRequest subMode = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt - void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt + void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode pure member where createMember_ memberId createdAt = do @@ -654,13 +655,13 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con :. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt) ) -createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> ExceptT StoreError IO () -createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange = +createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> SubscriptionMode -> ExceptT StoreError IO () +createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange subMode = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt groupMemberId <- liftIO $ insertedRowId db - Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt + Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode setCommandConnId db user cmdId connId where insertMember_ memberId createdAt = @@ -713,15 +714,15 @@ getMemberInvitation db User {userId} groupMemberId = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) -createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> IO () -createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange = do +createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> SubscriptionMode -> IO () +createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange subMode = do currentTs <- getCurrentTime - void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs + void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode -createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> IO () -createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange = do +createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> SubscriptionMode -> IO () +createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange subMode = do currentTs <- getCurrentTime - Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs + Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode setCommandConnId db user cmdId connId updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () @@ -920,14 +921,14 @@ getIntroduction_ db reMember toMember = ExceptT $ do in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember -createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do +createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do let mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- liftIO getCurrentTime newMember <- case directConnIds of Just (directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs + Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode liftIO $ setCommandConnId db user directCmdId directConnId (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Just contactId, memProfileId} @@ -936,18 +937,18 @@ createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupM pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Nothing, memProfileId} liftIO $ do member <- createNewMember_ db user gInfo newMember currentTs - conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs + conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs subMode liftIO $ setCommandConnId db user groupCmdId groupConnId pure (member :: GroupMember) {activeConn = Just conn} -createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> IO () -createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId = do +createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO () +createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- getCurrentTime - Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs + Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode setCommandConnId db user groupCmdId groupConnId forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs + Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode setCommandConnId db user directCmdId directConnId contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs @@ -977,7 +978,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = |] [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] -createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> IO Connection +createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index b763f9a540..cbcc4ddd28 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -78,6 +78,7 @@ import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses import Simplex.Chat.Migrations.M20230814_indexes import Simplex.Chat.Migrations.M20230827_file_encryption import Simplex.Chat.Migrations.M20230829_connections_chat_vrange +import Simplex.Chat.Migrations.M20230903_connections_to_subscribe import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -155,7 +156,8 @@ schemaMigrations = ("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses), ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes), ("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption), - ("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange) + ("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange), + ("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 0c2f1f636a..7f3c9841c0 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -80,7 +80,7 @@ import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..)) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (safeDecodeUtf8) @@ -293,8 +293,8 @@ getUserContactProfiles db User {userId} = toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> (Profile) toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences} -createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> ExceptT StoreError IO () -createUserContactLink db User {userId} agentConnId cReq = +createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO () +createUserContactLink db User {userId} agentConnId cReq subMode = checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime DB.execute @@ -302,7 +302,7 @@ createUserContactLink db User {userId} agentConnId cReq = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection] getUserAddressConnections db User {userId} = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 7ec307ab49..1e9f2888af 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -36,6 +36,7 @@ import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (allFinally) import Simplex.Messaging.Version import UnliftIO.STM @@ -158,8 +159,8 @@ toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, v Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) toMaybeConnection _ = Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection -createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> IO Connection +createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode = do viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId -> maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId) let viaGroupLink = isJust viaLinkGroupId @@ -169,12 +170,12 @@ createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange INSERT INTO connections ( user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at, - peer_chat_min_version, peer_chat_max_version - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + peer_chat_min_version, peer_chat_max_version, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) - :. (minV, maxV) + :. (minV, maxV, subMode == SMOnlyCreate) ) connId <- insertedRowId db pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} diff --git a/stack.yaml b/stack.yaml index 9c6b35432f..18d5afe8b9 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 351f42650c57f310fc1ea858ff9b7178823f1fd4 + commit: 0cabe0690beee90f460ad7bada72294222e7e109 - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 4bb87b1e96..3db4052226 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -57,6 +57,8 @@ chatDirectTests = do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles it "encrypt/decrypt database" testDatabaseEncryption + describe "coordination between app and NSE" $ do + it "should not subscribe in NSE and subscribe in the app" testSubscribeAppNSE describe "mute/unmute messages" $ do it "mute/unmute contact" testMuteContact it "mute/unmute group" testMuteGroup @@ -970,6 +972,35 @@ testDatabaseEncryption tmp = do withTestChat tmp "alice" $ \alice -> do testChatWorking alice bob +testSubscribeAppNSE :: HasCallStack => FilePath -> IO () +testSubscribeAppNSE tmp = + withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat tmp "alice" aliceProfile $ \alice -> do + withTestChatOpts tmp testOpts {maintenance = True} "alice" $ \nseAlice -> do + alice ##> "/_app suspend 1" + alice <## "ok" + alice <## "chat suspended" + nseAlice ##> "/_start subscribe=off expire=off xftp=off" + nseAlice <## "chat started" + nseAlice ##> "/ad" + cLink <- getContactLink nseAlice True + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + (nseAlice </) + alice ##> "/_app activate" + alice <## "ok" + alice <## "Your address is active! To show: /sa" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + threadDelay 100000 + alice <##> bob + testMuteContact :: HasCallStack => FilePath -> IO () testMuteContact = testChat2 aliceProfile bobProfile $ From ff657a444c3abd6350281ac2ce04a31282502e80 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 10 Sep 2023 21:12:52 +0100 Subject: [PATCH 13/13] core: 5.3.0.7 --- package.yaml | 2 +- simplex-chat.cabal | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.yaml b/package.yaml index 68e7acda7a..811a6cd8a6 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.3.0.6 +version: 5.3.0.7 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7750069b50..ebd3d1d646 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.3.0.6 +version: 5.3.0.7 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat