mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
169 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8721e8000 | |||
| 03882367da | |||
| 4d700d113d | |||
| 17bdd2a1d2 | |||
| 80a68012a2 | |||
| 3742906f75 | |||
| ae90edcdb5 | |||
| 9e76aadb0f | |||
| 043544d7ec | |||
| 2bf7d1dddc | |||
| e1741118ce | |||
| 58fb3f7f2d | |||
| 48e92a7e9b | |||
| 5bf16da09d | |||
| 23ca3dd665 | |||
| 2caff25fa2 | |||
| 37f835be8c | |||
| a6c1f2f776 | |||
| acf0dfc38c | |||
| 67f13831ce | |||
| d50e65af20 | |||
| b8d74b74a6 | |||
| 802a28d759 | |||
| c85d43b349 | |||
| 64ad491197 | |||
| ddd8e719ef | |||
| e51f7a51cc | |||
| e66f5d488b | |||
| b386cc83a7 | |||
| 09481e09b6 | |||
| 6913bf1a46 | |||
| e6c87ff00b | |||
| 5f41cf3c52 | |||
| 9edbe2e589 | |||
| b6876712f0 | |||
| 5b4c183466 | |||
| 393238f47c | |||
| aea526f69d | |||
| 13cf1cc004 | |||
| 1766a8bb2c | |||
| 9a5732ab70 | |||
| 2923ca1356 | |||
| 96f0083384 | |||
| febfc396e3 | |||
| 29735a807b | |||
| eb36f64676 | |||
| 4e01970d69 | |||
| e5713087e3 | |||
| b40fc7ff18 | |||
| 06ad2b7972 | |||
| 14eeb4451c | |||
| ead67adeb8 | |||
| c2d70a5107 | |||
| 90dffc975a | |||
| e5ba7caddc | |||
| ec6cee1389 | |||
| 1d16a19373 | |||
| 04c90b4f07 | |||
| 747cbb8e09 | |||
| 834487e548 | |||
| e1b6b54209 | |||
| 6366d60e4c | |||
| fabea36682 | |||
| 991332a809 | |||
| 85537a99e8 | |||
| 717c90c58b | |||
| ccb52e0acd | |||
| d84b30c071 | |||
| 5ae0afe1fe | |||
| d250e503b0 | |||
| afb0ae3d03 | |||
| 1a3f0bed47 | |||
| 1e280fb7e1 | |||
| 6feac55380 | |||
| 93d8eac037 | |||
| a11f99be3d | |||
| da17639309 | |||
| 10301aa742 | |||
| 2148d50393 | |||
| 12fb2a4ec5 | |||
| 8085e5b85c | |||
| 4ba310ec16 | |||
| 865c56f400 | |||
| c510e73256 | |||
| 73638129bc | |||
| 1a7a79d504 | |||
| d3268e4a72 | |||
| 15a93014a5 | |||
| e7735329bc | |||
| 3e222c68eb | |||
| a596bd9011 | |||
| 21a49710a8 | |||
| ce6fdb2558 | |||
| 0baee848a6 | |||
| 6f304bc9e6 | |||
| 1ca0dfffa0 | |||
| 1420084f5e | |||
| 3e03474437 | |||
| 95366e4d1b | |||
| df1775a1e6 | |||
| 30ccea18ab | |||
| 4cd90d74ad | |||
| 7f1214688a | |||
| aa89d0d156 | |||
| 787cd94362 | |||
| ec61a7fc51 | |||
| 9b627534f5 | |||
| 400a3707b2 | |||
| 38a5676b37 | |||
| f00cfa9108 | |||
| afa24722b2 | |||
| ea5cec53bc | |||
| 61dc649c70 | |||
| b20824e16c | |||
| 39330fdce3 | |||
| 6b725a8ef7 | |||
| cbcdeb2b43 | |||
| 4351610eca | |||
| 935d826a21 | |||
| a8c8137ade | |||
| 7b33e1fba8 | |||
| ade7bba97b | |||
| 08dd321311 | |||
| 67961180c9 | |||
| 1093892ede | |||
| ef05fa4905 | |||
| 6a99a4f1ae | |||
| 4895f396a2 | |||
| c3dffc5909 | |||
| af73e5993d | |||
| dfec1cbb02 | |||
| 8d8f7b2524 | |||
| db7b81587f | |||
| 31bb744ba7 | |||
| f4b349162f | |||
| 7f8adf8f03 | |||
| 1f4bb8a224 | |||
| 0c3dc8a6e9 | |||
| 1f15cf54af | |||
| c96ba30018 | |||
| ffea61917d | |||
| f5c11b8faf | |||
| 48b4b23204 | |||
| 9df78c8ac8 | |||
| 450bfe2e17 | |||
| c79eb36a7a | |||
| a58b3a42db | |||
| e344958224 | |||
| 1ba210fe77 | |||
| 7898395359 | |||
| aeb732c2f6 | |||
| 8d6fe2be99 | |||
| babbca48f8 | |||
| 2a9c138a23 | |||
| 47c6daf0cc | |||
| 60d6a47bdb | |||
| cfc323862f | |||
| b0c9ba05f3 | |||
| 00d5f3b769 | |||
| 858f0f2650 | |||
| a8fa9b5e58 | |||
| f379fd0f8c | |||
| 34a3387830 | |||
| 12200a74ff | |||
| fda41817e9 | |||
| 9b7fbfd513 | |||
| e21b4d4236 | |||
| bfc178faf3 | |||
| d7f9e17bcb |
@@ -52,9 +52,9 @@ jobs:
|
||||
- os: ubuntu-20.04
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-ubuntu-20_04-x86-64
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-22.04
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-ubuntu-18_04-x86-64
|
||||
asset_name: simplex-chat-ubuntu-22_04-x86-64
|
||||
- os: macos-latest
|
||||
cache_path: ~/.cabal/store
|
||||
asset_name: simplex-chat-macos-x86-64
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
run: brew install pkg-config
|
||||
|
||||
- name: Unix prepare cabal.project.local for Ubuntu
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-18.04'
|
||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ignore-project: False" >> cabal.project.local
|
||||
@@ -112,8 +112,8 @@ jobs:
|
||||
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
|
||||
|
||||
- name: Unix test
|
||||
if: matrix.os != 'windows-latest' && matrix.os != 'ubuntu-20.04'
|
||||
timeout-minutes: 20
|
||||
if: matrix.os != 'windows-latest'
|
||||
timeout-minutes: 30
|
||||
shell: bash
|
||||
run: cabal test --test-show-details=direct
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[](https://www.reddit.com/r/SimpleXChat)
|
||||
[](https://mastodon.social/@simplex)
|
||||
|
||||
| 19/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
|
||||
| 30/03/2023 | EN, [FR](/docs/lang/fr/README.md), [CZ](/docs/lang/cs/README.md) |
|
||||
|
||||
<img src="images/simplex-chat-logo.svg" alt="SimpleX logo" width="100%">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
## Join user groups
|
||||
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D)
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX-Group-3](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FeDEgekVhh0zIBYvUupGWZ96kiBEMbXwK%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAS7Z3zQyOhZ9o1qzI9OTZRySpkFTagdLMa6Rc7opuNh4%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22cG3W1qxZGzyrvHTugx16bg%3D%3D%22%7D)
|
||||
|
||||
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
|
||||
|
||||
@@ -66,6 +66,10 @@ The channel through which you share the link does not have to be secure - it is
|
||||
|
||||
After you connect, you can [verify connection security code](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification).
|
||||
|
||||
## User guide (NEW)
|
||||
|
||||
Read about the app features and settings in the new [User guide](./docs/guide/README.md).
|
||||
|
||||
## Help translating SimpleX Chat
|
||||
|
||||
Thanks to our users and [Weblate](https://hosted.weblate.org/engage/simplex-chat/), SimpleX Chat apps, website and documents are translated to many other languages.
|
||||
@@ -75,16 +79,18 @@ Join our translators to help SimpleX grow!
|
||||
|locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs|
|
||||
|:----:|:-------:|:---------:|:---------:|:---------:|:---------:|
|
||||
|🇬🇧 en|English | |✓|✓|✓|✓|
|
||||
|ar|العربية |[jermanuts](https://github.com/jermanuts)||[](https://hosted.weblate.org/projects/simplex-chat/website/ar/)||
|
||||
|🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[](https://hosted.weblate.org/projects/simplex-chat/android/cs/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)|
|
||||
|🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[](https://hosted.weblate.org/projects/simplex-chat/android/de/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[](https://hosted.weblate.org/projects/simplex-chat/website/de/)||
|
||||
|🇪🇸 es|Español ||[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|||
|
||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi_sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|
||||
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|
||||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|
||||
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|
||||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|
||||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|
||||
|🇷🇺 ru|Русский ||[](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|
||||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)|||
|
||||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br> |<br><br>[](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
|
||||
|
||||
Languages in progress: Arabic, Hindi, Japanese, Spanish and [many others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
|
||||
Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us!
|
||||
|
||||
## Contribute
|
||||
|
||||
@@ -175,13 +181,17 @@ You can use SimpleX with your own servers and still communicate with people usin
|
||||
|
||||
Recent updates:
|
||||
|
||||
[Feb 04, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
[Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.md).
|
||||
|
||||
[Jan 03, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
|
||||
[Dec 06, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||
[Feb 4, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md).
|
||||
|
||||
[Nov 08, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
[Jan 3, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md).
|
||||
|
||||
[Dec 6, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md).
|
||||
|
||||
[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md).
|
||||
|
||||
[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md).
|
||||
|
||||
@@ -281,17 +291,21 @@ If you are considering developing with SimpleX platform please get in touch for
|
||||
- ✅ Multiple user profiles in the same chat database.
|
||||
- ✅ Optionally avoid re-using the same TCP session for multiple connections.
|
||||
- ✅ Preserve message drafts.
|
||||
- 🏗 File server to optimize for efficient and private sending of large files.
|
||||
- 🏗 Improved audio & video calls.
|
||||
- ✅ File server to optimize for efficient and private sending of large files.
|
||||
- ✅ Improved audio & video calls.
|
||||
- ✅ Support older Android OS and 32-bit CPUs.
|
||||
- ✅ Hidden chat profiles.
|
||||
- 🏗 Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
|
||||
- 🏗 Video messages.
|
||||
- 🏗 SMP queue redundancy and rotation (manual is supported).
|
||||
- 🏗 Reduced battery and traffic usage in large groups.
|
||||
- 🏗 Support older Android OS and 32-bit CPUs.
|
||||
- Include optional message into connection request sent via contact address.
|
||||
- Ephemeral/disappearing/OTR conversations with the existing contacts.
|
||||
- Access password/pin (with optional alternative access password).
|
||||
- Local app files encryption.
|
||||
- Video messages.
|
||||
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
|
||||
- Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
|
||||
- Privately share your location.
|
||||
- Feeds/broadcasts.
|
||||
- Web widgets for custom interactivity in the chats.
|
||||
- Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.).
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "chat.simplex.app"
|
||||
minSdk 26
|
||||
targetSdk 32
|
||||
versionCode 107
|
||||
versionName "4.6"
|
||||
versionCode 117
|
||||
versionName "5.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -77,7 +77,18 @@ android {
|
||||
def isBundle = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("bundle") }) != null
|
||||
// if (isRelease) {
|
||||
// Comma separated list of languages that will be included in the apk
|
||||
android.defaultConfig.resConfigs("en", "cs", "de", "es", "fr", "it", "nl", "ru", "zh-rCN")
|
||||
android.defaultConfig.resConfigs(
|
||||
"en",
|
||||
"cs",
|
||||
"de",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"nl",
|
||||
"pl",
|
||||
"ru",
|
||||
"zh-rCN"
|
||||
)
|
||||
// }
|
||||
if (isBundle) {
|
||||
defaultConfig.ndk.abiFilters 'arm64-v8a', 'armeabi-v7a'
|
||||
@@ -143,6 +154,9 @@ dependencies {
|
||||
implementation "io.coil-kt:coil-compose:2.1.0"
|
||||
implementation "io.coil-kt:coil-gif:2.1.0"
|
||||
|
||||
// Video support
|
||||
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
|
||||
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
@@ -150,19 +164,12 @@ dependencies {
|
||||
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
|
||||
}
|
||||
|
||||
def buildType = "unknown"
|
||||
// Don't do anything if no compression is needed
|
||||
if (compression_level != "0") {
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name == 'packageDebug') {
|
||||
task.doLast {
|
||||
buildType = "debug"
|
||||
}
|
||||
task.finalizedBy compressApk
|
||||
} else if (task.name == 'packageRelease') {
|
||||
task.doLast {
|
||||
buildType = "release"
|
||||
}
|
||||
task.finalizedBy compressApk
|
||||
}
|
||||
}
|
||||
@@ -170,6 +177,13 @@ if (compression_level != "0") {
|
||||
|
||||
tasks.register("compressApk") {
|
||||
doLast {
|
||||
def isRelease = gradle.getStartParameter().taskNames.find({ it.toLowerCase().contains("release") }) != null
|
||||
def buildType
|
||||
if (isRelease) {
|
||||
buildType = "release"
|
||||
} else {
|
||||
buildType = "debug"
|
||||
}
|
||||
def javaHome = System.properties['java.home'] ?: org.gradle.internal.jvm.Jvm.current().getJavaHome()
|
||||
def sdkDir = android.getSdkDirectory().getAbsolutePath()
|
||||
def keyAlias = ""
|
||||
|
||||
@@ -35,7 +35,7 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass
|
||||
// from simplex-chat
|
||||
typedef long* chat_ctrl;
|
||||
|
||||
extern char *chat_migrate_init(const char *path, const char *key, chat_ctrl *ctrl);
|
||||
extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl);
|
||||
extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd);
|
||||
extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated
|
||||
extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait);
|
||||
@@ -44,13 +44,15 @@ extern char *chat_parse_server(const char *str);
|
||||
extern char *chat_password_hash(const char *pwd, const char *salt);
|
||||
|
||||
JNIEXPORT jobjectArray JNICALL
|
||||
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey) {
|
||||
Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) {
|
||||
const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE);
|
||||
const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE);
|
||||
const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE);
|
||||
jlong _ctrl = (jlong) 0;
|
||||
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, &_ctrl));
|
||||
jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl));
|
||||
(*env)->ReleaseStringUTFChars(env, dbPath, _dbPath);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _dbKey);
|
||||
(*env)->ReleaseStringUTFChars(env, dbKey, _confirm);
|
||||
|
||||
// Creating array of Object's (boxed values can be passed, eg. Long instead of long)
|
||||
jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL);
|
||||
|
||||
@@ -12,8 +12,7 @@ import androidx.activity.viewModels
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Lock
|
||||
import androidx.compose.runtime.*
|
||||
@@ -25,8 +24,8 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.*
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.app.MainActivity.Companion.enteredBackground
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.model.NtfManager.Companion.getUserIdFromIntent
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
@@ -37,8 +36,11 @@ import chat.simplex.app.views.chat.ChatView
|
||||
import chat.simplex.app.views.chatlist.*
|
||||
import chat.simplex.app.views.database.DatabaseErrorView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.localauth.SetAppPasscodeView
|
||||
import chat.simplex.app.views.newchat.*
|
||||
import chat.simplex.app.views.onboarding.*
|
||||
import chat.simplex.app.views.usersettings.LAMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
@@ -93,7 +95,7 @@ class MainActivity: FragmentActivity() {
|
||||
laFailed,
|
||||
::runAuthenticate,
|
||||
::setPerformLA,
|
||||
showLANotice = { m.controller.showLANotice(this) }
|
||||
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -111,7 +113,8 @@ class MainActivity: FragmentActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val enteredBackgroundVal = enteredBackground.value
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= 30_000) {
|
||||
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
|
||||
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
|
||||
runAuthenticate()
|
||||
}
|
||||
}
|
||||
@@ -128,6 +131,7 @@ class MainActivity: FragmentActivity() {
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
VideoPlayer.stopAll()
|
||||
enteredBackground.value = elapsedRealtime()
|
||||
}
|
||||
|
||||
@@ -164,16 +168,27 @@ class MainActivity: FragmentActivity() {
|
||||
delay(50)
|
||||
withContext(Dispatchers.Main) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_unlock),
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_unlock)
|
||||
else
|
||||
generalGetString(R.string.la_enter_app_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_log_in_using_credential)
|
||||
else
|
||||
generalGetString(R.string.auth_unlock),
|
||||
this@MainActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success ->
|
||||
userAuthorized.value = true
|
||||
is LAResult.Error, LAResult.Failed ->
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
laFailed.value = true
|
||||
LAResult.Unavailable -> {
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) {
|
||||
laFailedAlert()
|
||||
}
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
userAuthorized.value = true
|
||||
m.performLA.value = false
|
||||
m.controller.appPrefs.performLA.set(false)
|
||||
@@ -187,21 +202,116 @@ class MainActivity: FragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPerformLA(on: Boolean) {
|
||||
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (on) {
|
||||
enableLA()
|
||||
} else {
|
||||
disableLA()
|
||||
private fun showLANotice(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
if (!laNoticeShown.get()) {
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.la_notice_title_simplex_lock),
|
||||
text = generalGetString(R.string.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled),
|
||||
confirmText = generalGetString(R.string.la_notice_turn_on),
|
||||
onConfirm = {
|
||||
withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager
|
||||
showChooseLAMode(laNoticeShown, activity)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableLA() {
|
||||
private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>, activity: FragmentActivity) {
|
||||
Log.d(TAG, "showLANotice")
|
||||
laNoticeShown.set(true)
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.la_lock_mode),
|
||||
text = null,
|
||||
confirmText = generalGetString(R.string.la_lock_mode_passcode),
|
||||
dismissText = generalGetString(R.string.la_lock_mode_system),
|
||||
onConfirm = {
|
||||
AlertManager.shared.hideAlert()
|
||||
setPasscode()
|
||||
},
|
||||
onDismiss = {
|
||||
AlertManager.shared.hideAlert()
|
||||
initialEnableLA(activity)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun initialEnableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
val appPrefs = m.controller.appPrefs
|
||||
m.controller.appPrefs.laMode.set(LAMode.SYSTEM)
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_enable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
this@MainActivity,
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
m.showAdvertiseLAUnavailableAlert.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun setPasscode() {
|
||||
val chatModel = vm.chatModel
|
||||
val appPrefs = chatModel.controller.appPrefs
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
chatModel.performLA.value = true
|
||||
appPrefs.performLA.set(true)
|
||||
appPrefs.laMode.set(LAMode.PASSCODE)
|
||||
laTurnedOnAlert()
|
||||
},
|
||||
cancel = {
|
||||
chatModel.performLA.value = false
|
||||
appPrefs.performLA.set(false)
|
||||
laPasscodeNotSetAlert()
|
||||
},
|
||||
close)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPerformLA(on: Boolean, activity: FragmentActivity) {
|
||||
vm.chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (on) {
|
||||
enableLA(activity)
|
||||
} else {
|
||||
disableLA(activity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_enable_simplex_lock)
|
||||
else
|
||||
generalGetString(R.string.new_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_confirm_credential)
|
||||
else
|
||||
"",
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
@@ -210,11 +320,13 @@ class MainActivity: FragmentActivity() {
|
||||
prefPerformLA.set(true)
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laFailedAlert()
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableInstructionAlert()
|
||||
@@ -224,24 +336,33 @@ class MainActivity: FragmentActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun disableLA() {
|
||||
private fun disableLA(activity: FragmentActivity) {
|
||||
val m = vm.chatModel
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_disable_simplex_lock),
|
||||
generalGetString(R.string.auth_confirm_credential),
|
||||
this@MainActivity,
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_disable_simplex_lock)
|
||||
else
|
||||
generalGetString(R.string.la_enter_app_passcode),
|
||||
if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM)
|
||||
generalGetString(R.string.auth_confirm_credential)
|
||||
else
|
||||
generalGetString(R.string.auth_disable_simplex_lock),
|
||||
activity,
|
||||
completed = { laResult ->
|
||||
val prefPerformLA = m.controller.appPrefs.performLA
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
ksAppPassword.remove()
|
||||
}
|
||||
is LAResult.Error, LAResult.Failed -> {
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
is LAResult.Error -> {
|
||||
m.performLA.value = true
|
||||
prefPerformLA.set(true)
|
||||
laFailedAlert()
|
||||
}
|
||||
LAResult.Unavailable -> {
|
||||
is LAResult.Unavailable -> {
|
||||
m.performLA.value = false
|
||||
prefPerformLA.set(false)
|
||||
laUnavailableTurningOffAlert()
|
||||
@@ -263,7 +384,7 @@ fun MainPage(
|
||||
userAuthorized: MutableState<Boolean?>,
|
||||
laFailed: MutableState<Boolean>,
|
||||
runAuthenticate: () -> Unit,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit,
|
||||
showLANotice: () -> Unit
|
||||
) {
|
||||
var showChatDatabaseError by rememberSaveable {
|
||||
@@ -391,6 +512,14 @@ fun MainPage(
|
||||
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
|
||||
AlertManager.shared.showInView()
|
||||
}
|
||||
|
||||
DisposableEffectOnRotate {
|
||||
// When using lock delay = 0 and screen rotates, the app will be locked which is not useful.
|
||||
// Let's prolong the unlocked period to 3 sec for screen rotation to take place
|
||||
if (chatModel.controller.appPrefs.laLockDelay.get() == 0) {
|
||||
enteredBackground.value = elapsedRealtime() + 3000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
@@ -401,7 +530,8 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId")
|
||||
if (chatId != null) {
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
awaitChatStartedIfNeeded(chatModel)
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||
chatModel.controller.changeActiveUser(userId, null)
|
||||
}
|
||||
val cInfo = chatModel.getChat(chatId)?.chatInfo
|
||||
@@ -413,7 +543,8 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
NtfManager.ShowChatsAction -> {
|
||||
Log.d(TAG, "processNotificationIntent: ShowChatsAction")
|
||||
withBGApi {
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId) {
|
||||
awaitChatStartedIfNeeded(chatModel)
|
||||
if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) {
|
||||
chatModel.controller.changeActiveUser(userId, null)
|
||||
}
|
||||
chatModel.chatId.value = null
|
||||
@@ -451,14 +582,23 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
when {
|
||||
"text/plain" == intent.type -> intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Text(it)
|
||||
intent.type == "text/plain" -> {
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (text != null) {
|
||||
chatModel.sharedContent.value = SharedContent.Text(text)
|
||||
}
|
||||
}
|
||||
intent.type?.startsWith("image/") == true -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(it))
|
||||
} // All other mime types
|
||||
else -> (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
|
||||
isMediaIntent(intent) -> {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
|
||||
if (uri != null) {
|
||||
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri))
|
||||
} // All other mime types
|
||||
}
|
||||
else -> {
|
||||
val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
|
||||
if (uri != null) {
|
||||
chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -466,16 +606,23 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) {
|
||||
// Close active chat and show a list of chats
|
||||
chatModel.chatId.value = null
|
||||
chatModel.clearOverlays.value = true
|
||||
Log.e(TAG, "ACTION_SEND_MULTIPLE ${intent.type}")
|
||||
when {
|
||||
intent.type?.startsWith("image/") == true -> (intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>)?.let {
|
||||
chatModel.sharedContent.value = SharedContent.Images(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", it)
|
||||
} // All other mime types
|
||||
isMediaIntent(intent) -> {
|
||||
val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri>
|
||||
if (uris != null) {
|
||||
chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris)
|
||||
} // All other mime types
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isMediaIntent(intent: Intent): Boolean =
|
||||
intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true
|
||||
|
||||
fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
@@ -504,6 +651,20 @@ fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) {
|
||||
// Still decrypting database
|
||||
if (chatModel.chatRunning.value == null) {
|
||||
val step = 50L
|
||||
for (i in 0..(timeout / step)) {
|
||||
if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) {
|
||||
break
|
||||
}
|
||||
delay(step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//fun testJson() {
|
||||
// val str: String = """
|
||||
// """.trimIndent()
|
||||
|
||||
@@ -26,7 +26,7 @@ external fun pipeStdOutToSocket(socketName: String) : Int
|
||||
|
||||
// SimpleX API
|
||||
typealias ChatCtrl = Long
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String): Array<Any>
|
||||
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
|
||||
external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String
|
||||
external fun chatRecvMsg(ctrl: ChatCtrl): String
|
||||
external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String
|
||||
@@ -41,10 +41,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
|
||||
val defaultLocale: Locale = Locale.getDefault()
|
||||
|
||||
fun initChatController(useKey: String? = null, startChat: Boolean = true) {
|
||||
fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) {
|
||||
val dbKey = useKey ?: DatabaseUtils.useDatabaseKey()
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey)
|
||||
val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated[0] as String)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
|
||||
|
||||
@@ -326,4 +326,4 @@ class SimplexService: Service() {
|
||||
|
||||
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,8 @@ class ChatModel(val controller: ChatController) {
|
||||
|
||||
val terminalItems = mutableStateListOf<TerminalItem>()
|
||||
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
|
||||
val userSMPServers = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||
// Allows to temporary save servers that are being edited on multiple screens
|
||||
val userSMPServersUnsaved = mutableStateOf<(List<ServerCfg>)?>(null)
|
||||
val presetSMPServers = mutableStateOf<(List<String>)?>(null)
|
||||
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
|
||||
|
||||
// set when app opened from external intent
|
||||
@@ -192,10 +190,13 @@ class ChatModel(val controller: ChatController) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.none { it.id == cItem.id }) {
|
||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,19 +223,19 @@ class ChatModel(val controller: ChatController) {
|
||||
res = true
|
||||
}
|
||||
// update current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
chatItems[itemIndex] = cItem
|
||||
return false
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
return if (chatId.value == cInfo.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
chatItems[itemIndex] = cItem
|
||||
false
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
true
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return res
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,6 +729,7 @@ data class Contact(
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
|
||||
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
|
||||
ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser
|
||||
}
|
||||
override val timedMessagesTTL: Int? get() = with(mergedPreferences.timedMessages) { if (enabled.forUser) userPreference.pref.ttl else null }
|
||||
override val displayName get() = localAlias.ifEmpty { profile.displayName }
|
||||
@@ -746,12 +748,14 @@ data class Contact(
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
|
||||
ChatFeature.Calls -> mergedPreferences.calls.contactPreference.allow != FeatureAllowed.NO
|
||||
}
|
||||
|
||||
fun userAllowsFeature(feature: ChatFeature): Boolean = when (feature) {
|
||||
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.FullDelete -> mergedPreferences.fullDelete.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
|
||||
ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -881,6 +885,7 @@ data class GroupInfo (
|
||||
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
|
||||
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
|
||||
ChatFeature.Voice -> fullGroupPreferences.voice.on
|
||||
ChatFeature.Calls -> false
|
||||
}
|
||||
override val timedMessagesTTL: Int? get() = with(fullGroupPreferences.timedMessages) { if (on) ttl else null }
|
||||
override val displayName get() = groupProfile.displayName
|
||||
@@ -1307,6 +1312,7 @@ data class ChatItem (
|
||||
is CIContent.SndCall -> showNtfDir
|
||||
is CIContent.RcvCall -> false // notification is shown on CallInvitation instead
|
||||
is CIContent.RcvIntegrityError -> showNtfDir
|
||||
is CIContent.RcvDecryptionError -> showNtfDir
|
||||
is CIContent.RcvGroupInvitation -> showNtfDir
|
||||
is CIContent.SndGroupInvitation -> showNtfDir
|
||||
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
|
||||
@@ -1419,7 +1425,7 @@ data class ChatItem (
|
||||
file = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private const val TEMP_DELETED_CHAT_ITEM_ID = -1L
|
||||
const val TEMP_LIVE_CHAT_ITEM_ID = -2L
|
||||
|
||||
@@ -1464,10 +1470,10 @@ data class ChatItem (
|
||||
file = null
|
||||
)
|
||||
|
||||
fun invalidJSON(json: String): ChatItem =
|
||||
fun invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String): ChatItem =
|
||||
ChatItem(
|
||||
chatDir = CIDirection.DirectSnd(),
|
||||
meta = CIMeta.invalidJSON(),
|
||||
chatDir = chatDir ?: CIDirection.DirectSnd(),
|
||||
meta = meta ?: CIMeta.invalidJSON(),
|
||||
content = CIContent.InvalidJSON(json),
|
||||
quotedItem = null,
|
||||
file = null
|
||||
@@ -1610,6 +1616,7 @@ sealed class CIContent: ItemContent {
|
||||
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@Serializable @SerialName("rcvGroupEvent") class RcvGroupEventContent(val rcvGroupEvent: RcvGroupEvent): CIContent() { override val msgContent: MsgContent? get() = null }
|
||||
@@ -1636,6 +1643,7 @@ sealed class CIContent: ItemContent {
|
||||
is SndCall -> status.text(duration)
|
||||
is RcvCall -> status.text(duration)
|
||||
is RcvIntegrityError -> msgError.text
|
||||
is RcvDecryptionError -> msgDecryptError.text
|
||||
is RcvGroupInvitation -> groupInvitation.text
|
||||
is SndGroupInvitation -> groupInvitation.text
|
||||
is RcvGroupEventContent -> rcvGroupEvent.text
|
||||
@@ -1674,6 +1682,17 @@ sealed class CIContent: ItemContent {
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class MsgDecryptError {
|
||||
@SerialName("ratchetHeader") RatchetHeader,
|
||||
@SerialName("tooManySkipped") TooManySkipped;
|
||||
|
||||
val text: String get() = when (this) {
|
||||
RatchetHeader -> generalGetString(R.string.decryption_error)
|
||||
TooManySkipped -> generalGetString(R.string.decryption_error)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class CIQuote (
|
||||
val chatDir: CIDirection? = null,
|
||||
@@ -1711,18 +1730,40 @@ class CIFile(
|
||||
val fileName: String,
|
||||
val fileSize: Long,
|
||||
val filePath: String? = null,
|
||||
val fileStatus: CIFileStatus
|
||||
val fileStatus: CIFileStatus,
|
||||
val fileProtocol: FileProtocol
|
||||
) {
|
||||
val loaded: Boolean = when (fileStatus) {
|
||||
CIFileStatus.SndStored -> true
|
||||
CIFileStatus.SndTransfer -> true
|
||||
CIFileStatus.SndComplete -> true
|
||||
CIFileStatus.SndCancelled -> true
|
||||
CIFileStatus.RcvInvitation -> false
|
||||
CIFileStatus.RcvAccepted -> false
|
||||
CIFileStatus.RcvTransfer -> false
|
||||
CIFileStatus.RcvCancelled -> false
|
||||
CIFileStatus.RcvComplete -> true
|
||||
is CIFileStatus.SndStored -> true
|
||||
is CIFileStatus.SndTransfer -> true
|
||||
is CIFileStatus.SndComplete -> true
|
||||
is CIFileStatus.SndCancelled -> true
|
||||
is CIFileStatus.SndError -> true
|
||||
is CIFileStatus.RcvInvitation -> false
|
||||
is CIFileStatus.RcvAccepted -> false
|
||||
is CIFileStatus.RcvTransfer -> false
|
||||
is CIFileStatus.RcvCancelled -> false
|
||||
is CIFileStatus.RcvComplete -> true
|
||||
is CIFileStatus.RcvError -> false
|
||||
}
|
||||
|
||||
val cancelAction: CancelAction? = when (fileStatus) {
|
||||
is CIFileStatus.SndStored -> sndCancelAction
|
||||
is CIFileStatus.SndTransfer -> sndCancelAction
|
||||
is CIFileStatus.SndComplete ->
|
||||
if (fileProtocol == FileProtocol.XFTP) {
|
||||
revokeCancelAction
|
||||
} else {
|
||||
null
|
||||
}
|
||||
is CIFileStatus.SndCancelled -> null
|
||||
is CIFileStatus.SndError -> null
|
||||
is CIFileStatus.RcvInvitation -> null
|
||||
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
||||
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
||||
is CIFileStatus.RcvCancelled -> null
|
||||
is CIFileStatus.RcvComplete -> null
|
||||
is CIFileStatus.RcvError -> null
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -1733,21 +1774,67 @@ class CIFile(
|
||||
filePath: String? = "test.txt",
|
||||
fileStatus: CIFileStatus = CIFileStatus.RcvComplete
|
||||
): CIFile =
|
||||
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus)
|
||||
CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class CIFileStatus {
|
||||
@SerialName("snd_stored") SndStored,
|
||||
@SerialName("snd_transfer") SndTransfer,
|
||||
@SerialName("snd_complete") SndComplete,
|
||||
@SerialName("snd_cancelled") SndCancelled,
|
||||
@SerialName("rcv_invitation") RcvInvitation,
|
||||
@SerialName("rcv_accepted") RcvAccepted,
|
||||
@SerialName("rcv_transfer") RcvTransfer,
|
||||
@SerialName("rcv_complete") RcvComplete,
|
||||
@SerialName("rcv_cancelled") RcvCancelled;
|
||||
class CancelAction(
|
||||
val uiActionId: Int,
|
||||
val alert: AlertInfo
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class AlertInfo(
|
||||
val titleId: Int,
|
||||
val messageId: Int,
|
||||
val confirmId: Int
|
||||
)
|
||||
|
||||
private val sndCancelAction: CancelAction = CancelAction(
|
||||
uiActionId = R.string.stop_file__action,
|
||||
alert = AlertInfo(
|
||||
titleId = R.string.stop_snd_file__title,
|
||||
messageId = R.string.stop_snd_file__message,
|
||||
confirmId = R.string.stop_file__confirm
|
||||
)
|
||||
)
|
||||
private val revokeCancelAction: CancelAction = CancelAction(
|
||||
uiActionId = R.string.revoke_file__action,
|
||||
alert = AlertInfo(
|
||||
titleId = R.string.revoke_file__title,
|
||||
messageId = R.string.revoke_file__message,
|
||||
confirmId = R.string.revoke_file__confirm
|
||||
)
|
||||
)
|
||||
private val rcvCancelAction: CancelAction = CancelAction(
|
||||
uiActionId = R.string.stop_file__action,
|
||||
alert = AlertInfo(
|
||||
titleId = R.string.stop_rcv_file__title,
|
||||
messageId = R.string.stop_rcv_file__message,
|
||||
confirmId = R.string.stop_file__confirm
|
||||
)
|
||||
)
|
||||
|
||||
@Serializable
|
||||
enum class FileProtocol {
|
||||
@SerialName("smp") SMP,
|
||||
@SerialName("xftp") XFTP;
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class CIFileStatus {
|
||||
@Serializable @SerialName("sndStored") object SndStored: CIFileStatus()
|
||||
@Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus()
|
||||
@Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus()
|
||||
@Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("sndError") object SndError: CIFileStatus()
|
||||
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
|
||||
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
|
||||
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
|
||||
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
|
||||
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
||||
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
||||
}
|
||||
|
||||
@Suppress("SERIALIZER_TYPE_INCOMPATIBLE")
|
||||
@@ -1758,6 +1845,7 @@ sealed class MsgContent {
|
||||
@Serializable(with = MsgContentSerializer::class) class MCText(override val text: String): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCLink(override val text: String, val preview: LinkPreview): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCImage(override val text: String, val image: String): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCVideo(override val text: String, val image: String, val duration: Int): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
|
||||
@Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
|
||||
@@ -1811,6 +1899,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
element<String>("text")
|
||||
element<String>("image")
|
||||
})
|
||||
element("MCVideo", buildClassSerialDescriptor("MCVideo") {
|
||||
element<String>("text")
|
||||
element<String>("image")
|
||||
element<Int>("duration")
|
||||
})
|
||||
element("MCFile", buildClassSerialDescriptor("MCFile") {
|
||||
element<String>("text")
|
||||
})
|
||||
@@ -1834,6 +1927,11 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
|
||||
MsgContent.MCImage(text, image)
|
||||
}
|
||||
"video" -> {
|
||||
val image = json["image"]?.jsonPrimitive?.content ?: "unknown message format"
|
||||
val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0
|
||||
MsgContent.MCVideo(text, image, duration)
|
||||
}
|
||||
"voice" -> {
|
||||
val duration = json["duration"]?.jsonPrimitive?.intOrNull ?: 0
|
||||
MsgContent.MCVoice(text, duration)
|
||||
@@ -1869,6 +1967,13 @@ object MsgContentSerializer : KSerializer<MsgContent> {
|
||||
put("text", value.text)
|
||||
put("image", value.image)
|
||||
}
|
||||
is MsgContent.MCVideo ->
|
||||
buildJsonObject {
|
||||
put("type", "video")
|
||||
put("text", value.text)
|
||||
put("image", value.image)
|
||||
put("duration", value.duration)
|
||||
}
|
||||
is MsgContent.MCVoice ->
|
||||
buildJsonObject {
|
||||
put("type", "voice")
|
||||
@@ -2004,7 +2109,11 @@ enum class CICallStatus {
|
||||
}
|
||||
}
|
||||
|
||||
fun durationText(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60)
|
||||
fun durationText(sec: Int): String {
|
||||
val s = sec % 60
|
||||
val m = sec / 60
|
||||
return if (m < 60) "%02d:%02d".format(m, s) else "%02d:%02d:%02d".format(m / 60, m % 60, s)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class MsgErrorType() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,3 +27,4 @@ val WarningOrange = Color(255, 127, 0, 255)
|
||||
val WarningYellow = Color(255, 192, 0, 255)
|
||||
val FileLight = Color(183, 190, 199, 255)
|
||||
val FileDark = Color(101, 101, 106, 255)
|
||||
val MenuTextColorDark = Color.White.copy(alpha = 0.8f)
|
||||
|
||||
@@ -8,4 +8,4 @@ val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ val DEFAULT_PADDING = 16.dp
|
||||
val DEFAULT_SPACE_AFTER_ICON = 4.dp
|
||||
val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2
|
||||
val DEFAULT_BOTTOM_PADDING = 48.dp
|
||||
val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp
|
||||
|
||||
val DarkColorPalette = darkColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
@@ -27,7 +28,7 @@ val DarkColorPalette = darkColors(
|
||||
// surface = Color.Black,
|
||||
// background = Color(0xFF121212),
|
||||
// surface = Color(0xFF121212),
|
||||
// error = Color(0xFFCF6679),
|
||||
error = Color.Red,
|
||||
onBackground = Color(0xFFFFFBFA),
|
||||
onSurface = Color(0xFFFFFBFA),
|
||||
// onError: Color = Color.Black,
|
||||
@@ -36,6 +37,7 @@ val LightColorPalette = lightColors(
|
||||
primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files
|
||||
primaryVariant = SimplexGreen,
|
||||
secondary = LightGray,
|
||||
error = Color.Red,
|
||||
// background = Color.White,
|
||||
// surface = Color.White
|
||||
// onPrimary = Color.White,
|
||||
@@ -74,4 +76,4 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme.colors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.*
|
||||
@@ -26,61 +29,72 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.model.Profile
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.AppBarTitle
|
||||
import chat.simplex.app.views.helpers.withApi
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.OnboardingStage
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
||||
fun isValidDisplayName(name: String) : Boolean {
|
||||
return (name.firstOrNull { it.isWhitespace() }) == null
|
||||
return (name.firstOrNull { it.isWhitespace() }) == null && !name.startsWith("@") && !name.startsWith("#")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
val displayName = remember { mutableStateOf("") }
|
||||
val fullName = remember { mutableStateOf("") }
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val fullName = rememberSaveable { mutableStateOf("") }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.create_profile), false)
|
||||
ReadableText(R.string.your_profile_is_stored_on_your_device)
|
||||
ReadableText(R.string.profile_is_only_shared_with_your_contacts)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
stringResource(R.string.display_name),
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(bottom = 3.dp)
|
||||
)
|
||||
ProfileNameField(displayName, focusRequester)
|
||||
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
|
||||
Text(
|
||||
errorText,
|
||||
fontSize = 15.sp,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
stringResource(R.string.full_name_optional__prompt),
|
||||
style = MaterialTheme.typography.h6,
|
||||
modifier = Modifier.padding(bottom = 5.dp)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
/*CloseSheetBar(close = {
|
||||
if (chatModel.users.isEmpty()) {
|
||||
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})*/
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
|
||||
AppBarTitleCentered(stringResource(R.string.create_profile))
|
||||
ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues())
|
||||
ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING * 1.5f))
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(R.string.display_name),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Text(
|
||||
stringResource(R.string.no_spaces),
|
||||
fontSize = 16.sp,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Text(
|
||||
stringResource(R.string.full_name_optional__prompt),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(fullName, "", ::isValidDisplayName)
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Row {
|
||||
if (chatModel.users.isEmpty()) {
|
||||
SimpleButton(
|
||||
SimpleButtonDecorated(
|
||||
text = stringResource(R.string.about_simplex),
|
||||
icon = Icons.Outlined.ArrowBackIosNew
|
||||
icon = Icons.Outlined.ArrowBackIosNew,
|
||||
textDecoration = TextDecoration.None,
|
||||
fontWeight = FontWeight.Medium
|
||||
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
|
||||
}
|
||||
|
||||
Spacer(Modifier.fillMaxWidth().weight(1f))
|
||||
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
val createModifier: Modifier
|
||||
val createColor: Color
|
||||
@@ -93,7 +107,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
|
||||
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor)
|
||||
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
|
||||
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = createColor)
|
||||
}
|
||||
}
|
||||
@@ -128,24 +142,50 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameField(name: MutableState<String>, focusRequester: FocusRequester? = null) {
|
||||
fun ProfileNameField(name: MutableState<String>, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) {
|
||||
var valid by rememberSaveable { mutableStateOf(true) }
|
||||
var focused by rememberSaveable { mutableStateOf(false) }
|
||||
val strokeColor by remember {
|
||||
derivedStateOf {
|
||||
if (valid) {
|
||||
if (focused) {
|
||||
HighOrLowlight.copy(alpha = 0.6f)
|
||||
} else {
|
||||
HighOrLowlight.copy(alpha = 0.3f)
|
||||
}
|
||||
} else Color.Red
|
||||
}
|
||||
}
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.secondary)
|
||||
.height(40.dp)
|
||||
.clip(RoundedCornerShape(5.dp))
|
||||
.padding(8.dp)
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
.navigationBarsWithImePadding()
|
||||
BasicTextField(
|
||||
value = name.value,
|
||||
onValueChange = { name.value = it },
|
||||
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(HighOrLowlight)
|
||||
)
|
||||
}
|
||||
.onFocusChanged { focused = it.isFocused }
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(50)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
BasicTextField(
|
||||
value = name.value,
|
||||
onValueChange = { name.value = it },
|
||||
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
|
||||
textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(HighOrLowlight)
|
||||
)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { name.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
valid = isValid(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,4 +106,4 @@ class CallManager(val chatModel: ChatModel) {
|
||||
chatModel.controller.ntfManager.cancelCallNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -36,7 +38,6 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.model.NtfManager.Companion.OpenChatAction
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.ProfileImage
|
||||
import chat.simplex.app.views.onboarding.SimpleXLogo
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
class IncomingCallActivity: ComponentActivity() {
|
||||
@@ -186,6 +187,17 @@ fun IncomingCallLockScreenAlertLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SimpleXLogo() {
|
||||
Image(
|
||||
painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo),
|
||||
contentDescription = stringResource(R.string.image_descr_simplex_logo),
|
||||
modifier = Modifier
|
||||
.padding(vertical = DEFAULT_PADDING)
|
||||
.fillMaxWidth(0.80f)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockScreenCallButton(text: String, icon: ImageVector, color: Color, action: () -> Unit) {
|
||||
Surface(
|
||||
|
||||
@@ -48,4 +48,4 @@ class SoundPlayer {
|
||||
companion object {
|
||||
val shared = SoundPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,10 +230,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
|
||||
}
|
||||
},
|
||||
receiveFile = { fileId ->
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
withApi { chatModel.controller.receiveFile(user, fileId) }
|
||||
}
|
||||
withApi { chatModel.controller.receiveFile(user, fileId) }
|
||||
},
|
||||
cancelFile = { fileId ->
|
||||
withApi { chatModel.controller.cancelFile(user, fileId) }
|
||||
},
|
||||
joinGroup = { groupId ->
|
||||
withApi { chatModel.controller.apiJoinGroup(groupId) }
|
||||
@@ -313,6 +313,7 @@ fun ChatLayout(
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
@@ -357,7 +358,7 @@ fun ChatLayout(
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -377,7 +378,7 @@ fun ChatInfoToolbar(
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var showMenu by rememberSaveable { mutableStateOf(false) }
|
||||
val showMenu = rememberSaveable { mutableStateOf(false) }
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val onBackClicked = {
|
||||
if (!showSearch) {
|
||||
@@ -392,15 +393,15 @@ fun ChatInfoToolbar(
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(android.R.string.search_go).capitalize(Locale.current), Icons.Outlined.Search, onClick = {
|
||||
showMenu = false
|
||||
showMenu.value = false
|
||||
showSearch = true
|
||||
})
|
||||
}
|
||||
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu = false
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}) {
|
||||
Icon(Icons.Outlined.Phone, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
|
||||
@@ -408,14 +409,14 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(R.string.icon_descr_video_call).capitalize(Locale.current), Icons.Outlined.Videocam, onClick = {
|
||||
showMenu = false
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu = false
|
||||
showMenu.value = false
|
||||
addMembers(chat.chatInfo.groupInfo)
|
||||
}) {
|
||||
Icon(Icons.Outlined.PersonAdd, stringResource(R.string.icon_descr_add_members), tint = MaterialTheme.colors.primary)
|
||||
@@ -428,7 +429,7 @@ fun ChatInfoToolbar(
|
||||
if (ntfsEnabled.value) stringResource(R.string.mute_chat) else stringResource(R.string.unmute_chat),
|
||||
if (ntfsEnabled.value) Icons.Outlined.NotificationsOff else Icons.Outlined.Notifications,
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showMenu.value = false
|
||||
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
|
||||
scope.launch {
|
||||
delay(200)
|
||||
@@ -439,7 +440,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
barButtons.add {
|
||||
IconButton({ showMenu = true }) {
|
||||
IconButton({ showMenu.value = true }) {
|
||||
Icon(Icons.Default.MoreVert, stringResource(R.string.icon_descr_more_button), tint = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
@@ -456,11 +457,7 @@ fun ChatInfoToolbar(
|
||||
Divider(Modifier.padding(top = AppBarHeight))
|
||||
|
||||
Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
Modifier.widthIn(min = 220.dp)
|
||||
) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
menuItems.forEach { it() }
|
||||
}
|
||||
}
|
||||
@@ -530,6 +527,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
loadPrevMessages: (ChatInfo) -> Unit,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
@@ -576,6 +574,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
stopListening = true
|
||||
}
|
||||
}
|
||||
DisposableEffectOnGone(
|
||||
whenGone = {
|
||||
VideoPlayer.releaseAll()
|
||||
}
|
||||
)
|
||||
LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem ->
|
||||
CompositionLocalProvider(
|
||||
@@ -595,10 +598,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) {
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
if (cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) {
|
||||
if (composeState.value.editing) {
|
||||
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
|
||||
} else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,11 +643,11 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
} else {
|
||||
Spacer(Modifier.size(42.dp))
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
}
|
||||
} else {
|
||||
Box(Modifier.padding(start = 104.dp, end = 12.dp).then(swipeableModifier)) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
@@ -653,7 +658,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,36 +784,27 @@ fun BoxWithConstraintsScope.FloatingButtons(
|
||||
}
|
||||
val showButtonWithCounter = topUnreadCount > 0
|
||||
val height = with(LocalDensity.current) { maxHeight.toPx() }
|
||||
var showDropDown by remember { mutableStateOf(false) }
|
||||
val showDropDown = remember { mutableStateOf(false) }
|
||||
|
||||
TopEndFloatingButton(
|
||||
Modifier.padding(end = 16.dp, top = 24.dp).align(Alignment.TopEnd),
|
||||
topUnreadCount,
|
||||
showButtonWithCounter,
|
||||
onClick = { scope.launch { listState.animateScrollBy(height) } },
|
||||
onLongClick = { showDropDown = true }
|
||||
onLongClick = { showDropDown.value = true }
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showDropDown,
|
||||
onDismissRequest = { showDropDown = false },
|
||||
Modifier.width(220.dp),
|
||||
offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - 16.dp, 24.dp + fabSize)) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.mark_read),
|
||||
Icons.Outlined.Check,
|
||||
onClick = {
|
||||
markRead(
|
||||
CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1),
|
||||
bottomUnreadCount
|
||||
)
|
||||
showDropDown = false
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
generalGetString(R.string.mark_read),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
showDropDown.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,21 +929,26 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ProviderMedia {
|
||||
data class Image(val uri: Uri, val image: Bitmap): ProviderMedia()
|
||||
data class Video(val uri: Uri, val preview: String): ProviderMedia()
|
||||
}
|
||||
|
||||
private fun providerForGallery(
|
||||
listStateIndex: Int,
|
||||
chatItems: List<ChatItem>,
|
||||
cItemId: Long,
|
||||
scrollTo: (Int) -> Unit
|
||||
): ImageGalleryProvider {
|
||||
fun canShowImage(item: ChatItem): Boolean =
|
||||
item.content.msgContent is MsgContent.MCImage && item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null
|
||||
fun canShowMedia(item: ChatItem): Boolean =
|
||||
(item.content.msgContent is MsgContent.MCImage || item.content.msgContent is MsgContent.MCVideo) && (item.file?.loaded == true && getLoadedFilePath(SimplexApp.context, item.file) != null)
|
||||
|
||||
fun item(skipInternalIndex: Int, initialChatId: Long): Pair<Int, ChatItem>? {
|
||||
var processedInternalIndex = -skipInternalIndex.sign
|
||||
val indexOfFirst = chatItems.indexOfFirst { it.id == initialChatId }
|
||||
for (chatItemsIndex in if (skipInternalIndex >= 0) indexOfFirst downTo 0 else indexOfFirst..chatItems.lastIndex) {
|
||||
val item = chatItems[chatItemsIndex]
|
||||
if (canShowImage(item)) {
|
||||
if (canShowMedia(item)) {
|
||||
processedInternalIndex += skipInternalIndex.sign
|
||||
}
|
||||
if (processedInternalIndex == skipInternalIndex) {
|
||||
@@ -961,16 +962,28 @@ private fun providerForGallery(
|
||||
var initialChatId = cItemId
|
||||
return object: ImageGalleryProvider {
|
||||
override val initialIndex: Int = initialIndex
|
||||
override val totalImagesSize = mutableStateOf(Int.MAX_VALUE)
|
||||
override fun getImage(index: Int): Pair<Bitmap, Uri>? {
|
||||
override val totalMediaSize = mutableStateOf(Int.MAX_VALUE)
|
||||
override fun getMedia(index: Int): ProviderMedia? {
|
||||
val internalIndex = initialIndex - index
|
||||
val file = item(internalIndex, initialChatId)?.second?.file
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, file)
|
||||
return if (imageBitmap != null && filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
imageBitmap to uri
|
||||
} else null
|
||||
val item = item(internalIndex, initialChatId)?.second ?: return null
|
||||
return when (item.content.msgContent) {
|
||||
is MsgContent.MCImage -> {
|
||||
val imageBitmap: Bitmap? = getLoadedImage(SimplexApp.context, item.file)
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
|
||||
if (imageBitmap != null && filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
ProviderMedia.Image(uri, imageBitmap)
|
||||
} else null
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
val filePath = getLoadedFilePath(SimplexApp.context, item.file)
|
||||
if (filePath != null) {
|
||||
val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath))
|
||||
ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image)
|
||||
} else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun currentPageChanged(index: Int) {
|
||||
@@ -982,7 +995,7 @@ private fun providerForGallery(
|
||||
|
||||
override fun scrollToStart() {
|
||||
initialIndex = 0
|
||||
initialChatId = chatItems.first { canShowImage(it) }.id
|
||||
initialChatId = chatItems.first { canShowMedia(it) }.id
|
||||
}
|
||||
|
||||
override fun onDismiss(index: Int) {
|
||||
@@ -1060,6 +1073,7 @@ fun PreviewChatLayout() {
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
@@ -1119,6 +1133,7 @@ fun PreviewGroupChatLayout() {
|
||||
loadPrevMessages = { _ -> },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
|
||||
@@ -4,22 +4,29 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Videocam
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.durationText
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING_HALF
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.chat.item.SentColorLight
|
||||
import chat.simplex.app.views.helpers.UploadContent
|
||||
import chat.simplex.app.views.helpers.base64ToBitmap
|
||||
|
||||
@Composable
|
||||
fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabled: Boolean) {
|
||||
fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) {
|
||||
Row(
|
||||
Modifier
|
||||
.padding(top = 8.dp)
|
||||
@@ -31,13 +38,32 @@ fun ComposeImageView(images: List<String>, cancelImages: () -> Unit, cancelEnabl
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF),
|
||||
) {
|
||||
items(images.size) { index ->
|
||||
val imageBitmap = base64ToBitmap(images[index]).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
"preview image",
|
||||
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
|
||||
)
|
||||
itemsIndexed(media.images) { index, item ->
|
||||
val content = media.content[index]
|
||||
if (content is UploadContent.Video) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
val imageBitmap = base64ToBitmap(item).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
"preview video",
|
||||
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.Videocam,
|
||||
"preview video",
|
||||
Modifier
|
||||
.size(20.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val imageBitmap = base64ToBitmap(item).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
"preview image",
|
||||
modifier = Modifier.widthIn(max = 80.dp).height(60.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cancelEnabled) {
|
||||
|
||||
@@ -12,11 +12,10 @@ import android.graphics.drawable.AnimatedImageDrawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
@@ -32,10 +31,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toFile
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -52,7 +49,7 @@ import java.nio.file.Files
|
||||
sealed class ComposePreview {
|
||||
@Serializable object NoPreview: ComposePreview()
|
||||
@Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview()
|
||||
@Serializable class ImagePreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
|
||||
@Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview()
|
||||
@Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview()
|
||||
@Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview()
|
||||
}
|
||||
@@ -98,7 +95,7 @@ data class ComposeState(
|
||||
val sendEnabled: () -> Boolean
|
||||
get() = {
|
||||
val hasContent = when (preview) {
|
||||
is ComposePreview.ImagePreview -> true
|
||||
is ComposePreview.MediaPreview -> true
|
||||
is ComposePreview.VoicePreview -> true
|
||||
is ComposePreview.FilePreview -> true
|
||||
else -> message.isNotEmpty() || liveMessage != null
|
||||
@@ -111,7 +108,7 @@ data class ComposeState(
|
||||
val linkPreviewAllowed: Boolean
|
||||
get() =
|
||||
when (preview) {
|
||||
is ComposePreview.ImagePreview -> false
|
||||
is ComposePreview.MediaPreview -> false
|
||||
is ComposePreview.VoicePreview -> false
|
||||
is ComposePreview.FilePreview -> false
|
||||
else -> useLinkPreviews
|
||||
@@ -161,7 +158,8 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview {
|
||||
is MsgContent.MCText -> ComposePreview.NoPreview
|
||||
is MsgContent.MCLink -> ComposePreview.CLinkPreview(linkPreview = mc.preview)
|
||||
// TODO: include correct type
|
||||
is MsgContent.MCImage -> ComposePreview.ImagePreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
|
||||
is MsgContent.MCImage -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
|
||||
is MsgContent.MCVideo -> ComposePreview.MediaPreview(images = listOf(mc.image), listOf(UploadContent.SimpleImage(getAppFileUri(fileName))))
|
||||
is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true)
|
||||
is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName))
|
||||
is MsgContent.MCUnknown, null -> ComposePreview.NoPreview
|
||||
@@ -182,6 +180,7 @@ fun ComposeView(
|
||||
val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val cancelledLinks = rememberSaveable { mutableSetOf<String>() }
|
||||
val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get()
|
||||
val maxFileSize = getMaxFileSize(FileProtocol.XFTP)
|
||||
val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground)
|
||||
val textStyle = remember { mutableStateOf(smallFont) }
|
||||
val cameraLauncher = rememberCameraLauncher { uri: Uri? ->
|
||||
@@ -189,7 +188,7 @@ fun ComposeView(
|
||||
val bitmap: Bitmap? = getBitmapFromUri(uri)
|
||||
if (bitmap != null) {
|
||||
val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000)
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.ImagePreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
|
||||
composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri))))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,43 +199,56 @@ fun ComposeView(
|
||||
Toast.makeText(context, generalGetString(R.string.toast_permission_denied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val processPickedImage = { uris: List<Uri>, text: String? ->
|
||||
val processPickedMedia = { uris: List<Uri>, text: String? ->
|
||||
val content = ArrayList<UploadContent>()
|
||||
val imagesPreview = ArrayList<String>()
|
||||
uris.forEach { uri ->
|
||||
val drawable = getDrawableFromUri(uri)
|
||||
var bitmap: Bitmap? = if (drawable != null) getBitmapFromUri(uri) else null
|
||||
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
|
||||
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
|
||||
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
|
||||
if (isAnimNewApi || isAnimOldApi) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
|
||||
content.add(UploadContent.AnimatedImage(uri))
|
||||
} else {
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE))
|
||||
)
|
||||
var bitmap: Bitmap? = null
|
||||
val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(SimplexApp.context, uri)?.split(".")?.last())?.contains("image/") == true
|
||||
when {
|
||||
isImage -> {
|
||||
// Image
|
||||
val drawable = getDrawableFromUri(uri)
|
||||
bitmap = if (drawable != null) getBitmapFromUri(uri) else null
|
||||
val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable
|
||||
val isAnimOldApi = Build.VERSION.SDK_INT < 28 &&
|
||||
(getFileName(SimplexApp.context, uri)?.endsWith(".gif") == true || getFileName(SimplexApp.context, uri)?.endsWith(".webp") == true)
|
||||
if (isAnimNewApi || isAnimOldApi) {
|
||||
// It's a gif or webp
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
content.add(UploadContent.AnimatedImage(uri))
|
||||
} else {
|
||||
bitmap = null
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
content.add(UploadContent.SimpleImage(uri))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// Video
|
||||
val res = getBitmapFromVideo(uri)
|
||||
bitmap = res.preview
|
||||
val durationMs = res.duration
|
||||
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
|
||||
}
|
||||
} else {
|
||||
content.add(UploadContent.SimpleImage(uri))
|
||||
}
|
||||
if (bitmap != null) {
|
||||
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
|
||||
}
|
||||
}
|
||||
|
||||
if (imagesPreview.isNotEmpty()) {
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.ImagePreview(imagesPreview, content))
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content))
|
||||
}
|
||||
}
|
||||
val processPickedFile = { uri: Uri?, text: String? ->
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(context, uri)
|
||||
if (fileSize != null && fileSize <= MAX_FILE_SIZE) {
|
||||
if (fileSize != null && fileSize <= maxFileSize) {
|
||||
val fileName = getFileName(SimplexApp.context, uri)
|
||||
if (fileName != null) {
|
||||
composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri))
|
||||
@@ -244,19 +256,22 @@ fun ComposeView(
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(MAX_FILE_SIZE))
|
||||
String.format(generalGetString(R.string.maximum_supported_file_size), formatBytes(maxFileSize))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
val galleryLauncher = rememberLauncherForActivityResult(contract = PickMultipleFromGallery()) { processPickedImage(it, null) }
|
||||
val galleryLauncherFallback = rememberGetMultipleContentsLauncher { processPickedImage(it, null) }
|
||||
val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it, null) }
|
||||
val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
|
||||
val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) }
|
||||
val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) }
|
||||
|
||||
val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) }
|
||||
val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) }
|
||||
|
||||
LaunchedEffect(attachmentOption.value) {
|
||||
when (attachmentOption.value) {
|
||||
AttachmentOption.TakePhoto -> {
|
||||
AttachmentOption.CameraPhoto -> {
|
||||
when (PackageManager.PERMISSION_GRANTED) {
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> {
|
||||
cameraLauncher.launchWithFallback()
|
||||
@@ -267,15 +282,23 @@ fun ComposeView(
|
||||
}
|
||||
attachmentOption.value = null
|
||||
}
|
||||
AttachmentOption.PickImage -> {
|
||||
AttachmentOption.GalleryImage -> {
|
||||
try {
|
||||
galleryLauncher.launch(0)
|
||||
galleryImageLauncher.launch(0)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
galleryLauncherFallback.launch("image/*")
|
||||
galleryImageLauncherFallback.launch("image/*")
|
||||
}
|
||||
attachmentOption.value = null
|
||||
}
|
||||
AttachmentOption.PickFile -> {
|
||||
AttachmentOption.GalleryVideo -> {
|
||||
try {
|
||||
galleryVideoLauncher.launch(0)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
galleryVideoLauncherFallback.launch("video/*")
|
||||
}
|
||||
attachmentOption.value = null
|
||||
}
|
||||
AttachmentOption.File -> {
|
||||
filesLauncher.launch("*/*")
|
||||
attachmentOption.value = null
|
||||
}
|
||||
@@ -392,6 +415,7 @@ fun ComposeView(
|
||||
is MsgContent.MCText -> checkLinkPreview()
|
||||
is MsgContent.MCLink -> checkLinkPreview()
|
||||
is MsgContent.MCImage -> MsgContent.MCImage(msgText, image = msgContent.image)
|
||||
is MsgContent.MCVideo -> MsgContent.MCVideo(msgText, image = msgContent.image, duration = msgContent.duration)
|
||||
is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration)
|
||||
is MsgContent.MCFile -> MsgContent.MCFile(msgText)
|
||||
is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json)
|
||||
@@ -431,15 +455,20 @@ fun ComposeView(
|
||||
when (val preview = cs.preview) {
|
||||
ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText))
|
||||
is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview())
|
||||
is ComposePreview.ImagePreview -> {
|
||||
is ComposePreview.MediaPreview -> {
|
||||
preview.content.forEachIndexed { index, it ->
|
||||
val file = when (it) {
|
||||
is UploadContent.SimpleImage -> saveImage(context, it.uri)
|
||||
is UploadContent.AnimatedImage -> saveAnimImage(context, it.uri)
|
||||
is UploadContent.Video -> saveFileFromUri(context, it.uri)
|
||||
}
|
||||
if (file != null) {
|
||||
files.add(file)
|
||||
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
|
||||
if (it is UploadContent.Video) {
|
||||
msgs.add(MsgContent.MCVideo(if (preview.content.lastIndex == index) msgText else "", preview.images[index], it.duration))
|
||||
} else {
|
||||
msgs.add(MsgContent.MCImage(if (preview.content.lastIndex == index) msgText else "", preview.images[index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,7 +502,11 @@ fun ComposeView(
|
||||
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
|
||||
)
|
||||
}
|
||||
if (sent == null && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.VoicePreview)) {
|
||||
if (sent == null &&
|
||||
(cs.preview is ComposePreview.MediaPreview ||
|
||||
cs.preview is ComposePreview.FilePreview ||
|
||||
cs.preview is ComposePreview.VoicePreview)
|
||||
) {
|
||||
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
|
||||
}
|
||||
}
|
||||
@@ -595,8 +628,8 @@ fun ComposeView(
|
||||
when (val preview = composeState.value.preview) {
|
||||
ComposePreview.NoPreview -> {}
|
||||
is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
|
||||
is ComposePreview.ImagePreview -> ComposeImageView(
|
||||
preview.images,
|
||||
is ComposePreview.MediaPreview -> ComposeImageView(
|
||||
preview,
|
||||
::cancelImages,
|
||||
cancelEnabled = !composeState.value.editing
|
||||
)
|
||||
@@ -634,7 +667,7 @@ fun ComposeView(
|
||||
|
||||
when (val shared = chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> onMessageChange(shared.text)
|
||||
is SharedContent.Images -> processPickedImage(shared.uris, shared.text)
|
||||
is SharedContent.Media -> processPickedMedia(shared.uris, shared.text)
|
||||
is SharedContent.File -> processPickedFile(shared.uri, shared.text)
|
||||
null -> {}
|
||||
}
|
||||
@@ -767,7 +800,7 @@ class PickFromGallery: ActivityResultContract<Int, Uri?>() {
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data
|
||||
}
|
||||
|
||||
class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
override fun createIntent(context: Context, input: Int) =
|
||||
Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply {
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
@@ -792,3 +825,30 @@ class PickMultipleFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
else
|
||||
emptyList()
|
||||
}
|
||||
|
||||
|
||||
class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() {
|
||||
override fun createIntent(context: Context, input: Int) =
|
||||
Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI).apply {
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
type = "video/*"
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> =
|
||||
if (intent?.data != null)
|
||||
listOf(intent.data!!)
|
||||
else if (intent?.clipData != null)
|
||||
with(intent.clipData!!) {
|
||||
val uris = ArrayList<Uri>()
|
||||
for (i in 0 until kotlin.math.min(itemCount, 10)) {
|
||||
val uri = getItemAt(i).uri
|
||||
if (uri != null) uris.add(uri)
|
||||
}
|
||||
if (itemCount > 10) {
|
||||
AlertManager.shared.showAlertMsg(R.string.videos_limit_title, R.string.videos_limit_desc)
|
||||
}
|
||||
uris
|
||||
}
|
||||
else
|
||||
emptyList()
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ private fun ContactPreferencesLayout(
|
||||
AppBarTitle(stringResource(R.string.contact_preferences))
|
||||
val timedMessages: MutableState<Boolean> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.timedMessagesAllowed) }
|
||||
val onTTLUpdated = { ttl: Int? ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl ?: 86400))
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesTTL = ttl))
|
||||
}
|
||||
TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl ->
|
||||
applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL))
|
||||
@@ -104,6 +104,11 @@ private fun ContactPreferencesLayout(
|
||||
applyPrefs(featuresAllowed.copy(voice = it))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowCalls: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) }
|
||||
FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) {
|
||||
applyPrefs(featuresAllowed.copy(calls = it))
|
||||
}
|
||||
SectionSpacer()
|
||||
ResetSaveButtons(
|
||||
reset = reset,
|
||||
save = savePrefs,
|
||||
@@ -138,6 +143,7 @@ private fun FeatureSection(
|
||||
ContactFeatureAllowed.values(userDefault).map { it to it.text },
|
||||
allowFeature,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
@@ -147,7 +153,7 @@ private fun FeatureSection(
|
||||
pref.contactPreference.allow.text
|
||||
)
|
||||
}
|
||||
SectionTextFooter(feature.enabledDescription(enabled))
|
||||
SectionTextFooter(feature.enabledDescription(enabled) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -75,7 +75,7 @@ fun SendMsgView(
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 8.dp)) {
|
||||
val cs = composeState.value
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.ImagePreview || cs.preview is ComposePreview.FilePreview)
|
||||
val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
|
||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
||||
@@ -155,20 +155,18 @@ fun SendMsgView(
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
sendLiveMessage != null && updateLiveMessage != null
|
||||
) {
|
||||
var showDropdown by rememberSaveable { mutableStateOf(false) }
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown = true }
|
||||
val showDropdown = rememberSaveable { mutableStateOf(false) }
|
||||
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showDropdown,
|
||||
onDismissRequest = { showDropdown = false },
|
||||
Modifier.width(220.dp),
|
||||
DefaultDropdownMenu(
|
||||
showDropdown,
|
||||
) {
|
||||
ItemAction(
|
||||
generalGetString(R.string.send_live_message),
|
||||
Icons.Filled.Bolt,
|
||||
onClick = {
|
||||
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
|
||||
showDropdown = false
|
||||
showDropdown.value = false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -227,7 +225,7 @@ private fun NativeKeyboard(
|
||||
} catch (e: Exception) {
|
||||
return@OnCommitContentListener false
|
||||
}
|
||||
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Images("", listOf(inputContentInfo.contentUri))
|
||||
SimplexApp.context.chatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri))
|
||||
true
|
||||
}
|
||||
return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit)
|
||||
|
||||
@@ -134,4 +134,4 @@ private fun splitToParts(s: String, length: Int): String {
|
||||
return (0..(s.length - 1) / length)
|
||||
.map { s.drop(it * length).take(length) }
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -28,6 +28,7 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.ChatInfoToolbarTitle
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.newchat.InfoAboutIncognito
|
||||
import chat.simplex.app.views.usersettings.SettingsActionItem
|
||||
|
||||
@Composable
|
||||
@@ -37,6 +38,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch
|
||||
var allowModifyMembers by remember { mutableStateOf(true) }
|
||||
BackHandler(onBack = close)
|
||||
AddGroupMembersLayout(
|
||||
chatModel.incognito.value,
|
||||
groupInfo = groupInfo,
|
||||
creatingGroup = creatingGroup,
|
||||
contactsToAdd = getContactsToAdd(chatModel),
|
||||
@@ -85,6 +87,7 @@ fun getContactsToAdd(chatModel: ChatModel): List<Contact> {
|
||||
|
||||
@Composable
|
||||
fun AddGroupMembersLayout(
|
||||
chatModelIncognito: Boolean,
|
||||
groupInfo: GroupInfo,
|
||||
creatingGroup: Boolean,
|
||||
contactsToAdd: List<Contact>,
|
||||
@@ -105,6 +108,14 @@ fun AddGroupMembersLayout(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.button_add_members))
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
false,
|
||||
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
|
||||
generalGetString(R.string.group_main_profile_sent),
|
||||
true
|
||||
)
|
||||
Spacer(Modifier.size(DEFAULT_PADDING))
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
@@ -318,6 +329,7 @@ fun showProhibitedToInviteIncognitoAlertDialog() {
|
||||
fun PreviewAddGroupMembersLayout() {
|
||||
SimpleXTheme {
|
||||
AddGroupMembersLayout(
|
||||
chatModelIncognito = false,
|
||||
groupInfo = GroupInfo.sampleData,
|
||||
creatingGroup = false,
|
||||
contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData),
|
||||
|
||||
+9
-24
@@ -5,7 +5,6 @@ import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -13,7 +12,6 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -23,7 +21,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.TAG
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.*
|
||||
@@ -54,20 +51,15 @@ fun GroupMemberInfoView(
|
||||
developerTools,
|
||||
connectionCode,
|
||||
getContactChat = { chatModel.getContactChat(it) },
|
||||
knownDirectChat = {
|
||||
withApi {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItems.addAll(it.chatItems)
|
||||
chatModel.chatId.value = it.chatInfo.id
|
||||
closeAll()
|
||||
}
|
||||
},
|
||||
newDirectChat = {
|
||||
openDirectChat = {
|
||||
withApi {
|
||||
val c = chatModel.controller.apiGetChat(ChatType.Direct, it)
|
||||
if (c != null) {
|
||||
chatModel.addChat(c)
|
||||
if (chatModel.getContactChat(it) == null) {
|
||||
chatModel.addChat(c)
|
||||
}
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItems.addAll(c.chatItems)
|
||||
chatModel.chatId.value = c.id
|
||||
closeAll()
|
||||
}
|
||||
@@ -150,8 +142,7 @@ fun GroupMemberInfoLayout(
|
||||
developerTools: Boolean,
|
||||
connectionCode: String?,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
knownDirectChat: (Chat) -> Unit,
|
||||
newDirectChat: (Long) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
switchMemberAddress: () -> Unit,
|
||||
@@ -176,13 +167,8 @@ fun GroupMemberInfoLayout(
|
||||
if (contactId != null) {
|
||||
SectionView {
|
||||
val chat = getContactChat(contactId)
|
||||
if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
|
||||
OpenChatButton(onClick = { knownDirectChat(chat) })
|
||||
if (connectionCode != null) {
|
||||
SectionDivider()
|
||||
}
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
OpenChatButton(onClick = { newDirectChat(contactId) })
|
||||
if ((chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) || groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
if (connectionCode != null) {
|
||||
SectionDivider()
|
||||
}
|
||||
@@ -364,8 +350,7 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
developerTools = false,
|
||||
connectionCode = "123",
|
||||
getContactChat = { Chat.sampleData },
|
||||
knownDirectChat = {},
|
||||
newDirectChat = {},
|
||||
openDirectChat = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {},
|
||||
switchMemberAddress = {},
|
||||
|
||||
+80
-55
@@ -1,7 +1,6 @@
|
||||
package chat.simplex.app.views.chat.group
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -14,6 +13,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -23,6 +23,7 @@ import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.ProfileNameField
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.isValidDisplayName
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
import chat.simplex.app.views.usersettings.*
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
@@ -53,14 +54,32 @@ fun GroupProfileLayout(
|
||||
saveProfile: (GroupProfile) -> Unit,
|
||||
) {
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val displayName = remember { mutableStateOf(groupProfile.displayName) }
|
||||
val fullName = remember { mutableStateOf(groupProfile.fullName) }
|
||||
val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) }
|
||||
val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) }
|
||||
val scope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
val dataUnchanged =
|
||||
displayName.value == groupProfile.displayName &&
|
||||
fullName.value == groupProfile.fullName &&
|
||||
chosenImage.value == null
|
||||
val closeWithAlert = {
|
||||
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
|
||||
close()
|
||||
} else {
|
||||
showUnsavedChangesAlert({
|
||||
saveProfile(
|
||||
groupProfile.copy(
|
||||
displayName = displayName.value,
|
||||
fullName = fullName.value,
|
||||
image = profileImage.value
|
||||
)
|
||||
)
|
||||
}, close)
|
||||
}
|
||||
}
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
@@ -76,23 +95,16 @@ fun GroupProfileLayout(
|
||||
sheetState = bottomSheetModalState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
ModalView(close = closeWithAlert) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.group_profile_is_stored_on_members_devices),
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
Modifier.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
ReadableText(R.string.group_profile_is_stored_on_members_devices, TextAlign.Center)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -101,7 +113,7 @@ fun GroupProfileLayout(
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(192.dp, profileImage.value)
|
||||
ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f))
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
@@ -109,51 +121,54 @@ fun GroupProfileLayout(
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.group_display_name_field),
|
||||
Modifier.padding(bottom = 3.dp)
|
||||
)
|
||||
ProfileNameField(displayName, focusRequester)
|
||||
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
|
||||
Text(
|
||||
errorText,
|
||||
fontSize = 15.sp,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Text(
|
||||
stringResource(R.string.group_full_name_field),
|
||||
Modifier.padding(bottom = 5.dp)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Row {
|
||||
TextButton(stringResource(R.string.cancel_verb)) {
|
||||
close.invoke()
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
if (enabled) {
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(R.string.group_display_name_field),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
modifier = Modifier.clickable {
|
||||
saveProfile(groupProfile.copy(
|
||||
displayName = displayName.value,
|
||||
fullName = fullName.value,
|
||||
image = profileImage.value
|
||||
))
|
||||
},
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
color = HighOrLowlight
|
||||
stringResource(R.string.no_spaces),
|
||||
fontSize = 16.sp,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Text(
|
||||
stringResource(R.string.group_full_name_field),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
if (enabled) {
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
modifier = Modifier.clickable {
|
||||
saveProfile(
|
||||
groupProfile.copy(
|
||||
displayName = displayName.value,
|
||||
fullName = fullName.value,
|
||||
image = profileImage.value
|
||||
)
|
||||
)
|
||||
},
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.save_group_profile),
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(DEFAULT_BOTTOM_BUTTON_PADDING))
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
delay(300)
|
||||
focusRequester.requestFocus()
|
||||
@@ -164,6 +179,16 @@ fun GroupProfileLayout(
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
confirmText = generalGetString(R.string.save_and_notify_group_members),
|
||||
dismissText = generalGetString(R.string.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
|
||||
@@ -154,4 +154,4 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
|
||||
// Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//}
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat.item
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -16,6 +17,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -64,7 +66,7 @@ fun CIFileView(
|
||||
|
||||
fun fileSizeValid(): Boolean {
|
||||
if (file != null) {
|
||||
return file.fileSize <= MAX_FILE_SIZE
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -72,22 +74,30 @@ fun CIFileView(
|
||||
fun fileAction() {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation -> {
|
||||
is CIFileStatus.RcvInvitation -> {
|
||||
if (fileSizeValid()) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
}
|
||||
CIFileStatus.RcvAccepted ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_file),
|
||||
String.format(generalGetString(R.string.file_will_be_received_when_contact_is_online), MAX_FILE_SIZE)
|
||||
)
|
||||
CIFileStatus.RcvComplete -> {
|
||||
is CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_file),
|
||||
generalGetString(R.string.file_will_be_received_when_contact_completes_uploading)
|
||||
)
|
||||
FileProtocol.SMP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_file),
|
||||
generalGetString(R.string.file_will_be_received_when_contact_is_online)
|
||||
)
|
||||
}
|
||||
is CIFileStatus.RcvComplete -> {
|
||||
val filePath = getLoadedFilePath(context, file)
|
||||
if (filePath != null) {
|
||||
saveFileLauncher.launch(file.fileName)
|
||||
@@ -105,10 +115,24 @@ fun CIFileView(
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(32.dp),
|
||||
color = if (isInDarkTheme()) FileDark else FileLight,
|
||||
strokeWidth = 4.dp
|
||||
strokeWidth = 3.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun progressCircle(progress: Long, total: Long) {
|
||||
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
|
||||
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
|
||||
val strokeColor = if (isInDarkTheme()) FileDark else FileLight
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
color = Color.Transparent,
|
||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
|
||||
) {
|
||||
Box(Modifier.size(32.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun fileIndicator() {
|
||||
Box(
|
||||
@@ -120,19 +144,34 @@ fun CIFileView(
|
||||
) {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.SndStored -> fileIcon()
|
||||
CIFileStatus.SndTransfer -> progressIndicator()
|
||||
CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
|
||||
CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
CIFileStatus.RcvInvitation ->
|
||||
is CIFileStatus.SndStored ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressIndicator()
|
||||
FileProtocol.SMP -> fileIcon()
|
||||
}
|
||||
is CIFileStatus.SndTransfer ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
|
||||
FileProtocol.SMP -> progressIndicator()
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(innerIcon = Icons.Filled.Check)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.SndError -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.RcvInvitation ->
|
||||
if (fileSizeValid())
|
||||
fileIcon(innerIcon = Icons.Outlined.ArrowDownward, color = MaterialTheme.colors.primary)
|
||||
else
|
||||
fileIcon(innerIcon = Icons.Outlined.PriorityHigh, color = WarningOrange)
|
||||
CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
|
||||
CIFileStatus.RcvTransfer -> progressIndicator()
|
||||
CIFileStatus.RcvComplete -> fileIcon()
|
||||
CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = Icons.Outlined.MoreHoriz)
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
|
||||
} else {
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvComplete -> fileIcon()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = Icons.Outlined.Close)
|
||||
}
|
||||
} else {
|
||||
fileIcon()
|
||||
@@ -191,7 +230,7 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
|
||||
ChatItem.getFileMsgContentSample(),
|
||||
ChatItem.getFileMsgContentSample(fileName = "some_long_file_name_here", fileStatus = CIFileStatus.RcvInvitation),
|
||||
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvAccepted),
|
||||
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer),
|
||||
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10)),
|
||||
ChatItem.getFileMsgContentSample(fileStatus = CIFileStatus.RcvCancelled),
|
||||
ChatItem.getFileMsgContentSample(fileSize = 1_000_000_000, fileStatus = CIFileStatus.RcvInvitation),
|
||||
ChatItem.getFileMsgContentSample(text = "Hello there", fileStatus = CIFileStatus.RcvInvitation),
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -9,8 +10,7 @@ import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.outlined.ArrowDownward
|
||||
import androidx.compose.material.icons.outlined.MoreHoriz
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.*
|
||||
@@ -27,8 +28,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.CIFile
|
||||
import chat.simplex.app.model.CIFileStatus
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
@@ -45,6 +45,25 @@ fun CIImageView(
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
@Composable
|
||||
fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(16.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun fileIcon(icon: ImageVector, @StringRes stringId: Int) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(stringId),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun loadingIndicator() {
|
||||
if (file != null) {
|
||||
@@ -55,39 +74,20 @@ fun CIImageView(
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.SndTransfer ->
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(16.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
CIFileStatus.SndComplete ->
|
||||
Icon(
|
||||
Icons.Filled.Check,
|
||||
stringResource(R.string.icon_descr_image_snd_complete),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
Icon(
|
||||
Icons.Outlined.MoreHoriz,
|
||||
stringResource(R.string.icon_descr_waiting_for_image),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
CIFileStatus.RcvTransfer ->
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(16.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
CIFileStatus.RcvInvitation ->
|
||||
Icon(
|
||||
Icons.Outlined.ArrowDownward,
|
||||
stringResource(R.string.icon_descr_asked_to_receive),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
is CIFileStatus.SndStored ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressIndicator()
|
||||
FileProtocol.SMP -> {}
|
||||
}
|
||||
is CIFileStatus.SndTransfer -> progressIndicator()
|
||||
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_image_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_image)
|
||||
is CIFileStatus.RcvTransfer -> progressIndicator()
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ fun CIImageView(
|
||||
|
||||
fun fileSizeValid(): Boolean {
|
||||
if (file != null) {
|
||||
return file.fileSize <= MAX_FILE_SIZE
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -179,15 +179,23 @@ fun CIImageView(
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(MAX_FILE_SIZE))
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvAccepted ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_image),
|
||||
generalGetString(R.string.image_will_be_received_when_contact_is_online)
|
||||
)
|
||||
CIFileStatus.RcvTransfer -> {} // ?
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_image),
|
||||
generalGetString(R.string.image_will_be_received_when_contact_completes_uploading)
|
||||
)
|
||||
FileProtocol.SMP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_image),
|
||||
generalGetString(R.string.image_will_be_received_when_contact_is_online)
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
|
||||
CIFileStatus.RcvComplete -> {} // ?
|
||||
CIFileStatus.RcvCancelled -> {} // TODO
|
||||
else -> {}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun CIRcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean) {
|
||||
CIMsgError(ci, timedMessagesTTL, showMember) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.decryption_error),
|
||||
text = when (msgDecryptError) {
|
||||
MsgDecryptError.RatchetHeader -> String.format(generalGetString(R.string.alert_text_decryption_error_header), msgCount.toLong()) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
|
||||
MsgDecryptError.TooManySkipped -> String.format(generalGetString(R.string.alert_text_decryption_error_too_many_skipped), msgCount.toLong()) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_permanent_error_reconnect)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package chat.simplex.app.views.chat.item
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.*
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun CIVideoView(
|
||||
image: String,
|
||||
duration: Int,
|
||||
file: CIFile?,
|
||||
imageProvider: () -> ImageGalleryProvider,
|
||||
showMenu: MutableState<Boolean>,
|
||||
receiveFile: (Long) -> Unit
|
||||
) {
|
||||
Box(
|
||||
Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID),
|
||||
contentAlignment = Alignment.TopEnd
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val filePath = remember(file) { getLoadedFilePath(SimplexApp.context, file) }
|
||||
val preview = remember(image) { base64ToBitmap(image) }
|
||||
if (file != null && filePath != null) {
|
||||
val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) }
|
||||
val view = LocalView.current
|
||||
VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = {
|
||||
hideKeyboard(view)
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
ImageFullScreenView(imageProvider, close)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Box {
|
||||
ImageView(preview, showMenu, onClick = {
|
||||
if (file != null) {
|
||||
when (file.fileStatus) {
|
||||
CIFileStatus.RcvInvitation ->
|
||||
receiveFileIfValidSize(file, receiveFile)
|
||||
CIFileStatus.RcvAccepted ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_video),
|
||||
generalGetString(R.string.video_will_be_received_when_contact_completes_uploading)
|
||||
)
|
||||
|
||||
FileProtocol.SMP ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.waiting_for_video),
|
||||
generalGetString(R.string.video_will_be_received_when_contact_is_online)
|
||||
)
|
||||
}
|
||||
CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ?
|
||||
CIFileStatus.RcvComplete -> {} // ?
|
||||
CIFileStatus.RcvCancelled -> {} // TODO
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (file != null) {
|
||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||
}
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
||||
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
loadingIndicator(file)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true, context) }
|
||||
val videoPlaying = remember(uri.path) { player.videoPlaying }
|
||||
val progress = remember(uri.path) { player.progress }
|
||||
val duration = remember(uri.path) { player.duration }
|
||||
val preview by remember { player.preview }
|
||||
// val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled }
|
||||
val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo }
|
||||
val play = {
|
||||
player.enableSound(true)
|
||||
player.play(true)
|
||||
}
|
||||
val stop = {
|
||||
player.enableSound(false)
|
||||
player.stop()
|
||||
}
|
||||
val showPreview = remember { derivedStateOf { !videoPlaying.value || progress.value == 0L } }
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
Box {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
StyledPlayerView(ctx).apply {
|
||||
useController = false
|
||||
resizeMode = RESIZE_MODE_FIXED_WIDTH
|
||||
this.player = player.player
|
||||
}
|
||||
},
|
||||
Modifier
|
||||
.width(width)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = { if (player.player.playWhenReady) stop() else onClick() }
|
||||
)
|
||||
)
|
||||
if (showPreview.value) {
|
||||
ImageView(preview, showMenu, onClick)
|
||||
PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play)
|
||||
}
|
||||
DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) {
|
||||
Surface(
|
||||
Modifier.align(Alignment.Center),
|
||||
color = Color.Black.copy(alpha = 0.25f),
|
||||
shape = RoundedCornerShape(percent = 50)
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.defaultMinSize(minWidth = 40.dp, minHeight = 40.dp)
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = null,
|
||||
Modifier.size(25.dp),
|
||||
tint = if (error) WarningOrange else Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, duration: MutableState<Long>, progress: MutableState<Long>/*, soundEnabled: MutableState<Boolean>*/) {
|
||||
if (duration.value > 0L || progress.value > 0) {
|
||||
Row {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(DEFAULT_PADDING_HALF)
|
||||
.background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50))
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
val time = if (progress.value > 0) progress.value else duration.value
|
||||
val timeStr = durationText((time / 1000).toInt())
|
||||
val width = if (timeStr.length <= 5) 44 else 50
|
||||
Text(
|
||||
timeStr,
|
||||
Modifier.widthIn(min = with(LocalDensity.current) { width.sp.toDp() }).padding(horizontal = 4.dp),
|
||||
fontSize = 13.sp,
|
||||
color = Color.White
|
||||
)
|
||||
/*if (!soundEnabled.value) {
|
||||
Icon(Icons.Outlined.VolumeOff, null,
|
||||
Modifier.padding(start = 5.dp).size(10.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}*/
|
||||
}
|
||||
if (!playing.value) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(top = DEFAULT_PADDING_HALF)
|
||||
.background(Color.Black.copy(alpha = 0.35f), RoundedCornerShape(percent = 50))
|
||||
.padding(vertical = 2.dp, horizontal = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
formatBytes(file.fileSize),
|
||||
Modifier.padding(horizontal = 4.dp),
|
||||
fontSize = 13.sp,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImageView(preview: Bitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
val windowWidth = LocalWindowWidth()
|
||||
val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp }
|
||||
Image(
|
||||
preview.asImageBitmap(),
|
||||
contentDescription = stringResource(R.string.video_descr),
|
||||
modifier = Modifier
|
||||
.width(width)
|
||||
.combinedClickable(
|
||||
onLongClick = { showMenu.value = true },
|
||||
onClick = onClick
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocalWindowWidth(): Dp {
|
||||
val view = LocalView.current
|
||||
val density = LocalDensity.current.density
|
||||
return remember {
|
||||
val rect = Rect()
|
||||
view.getWindowVisibleDisplayFrame(rect)
|
||||
(rect.width() / density).dp
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun progressIndicator() {
|
||||
CircularProgressIndicator(
|
||||
Modifier.size(16.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun fileIcon(icon: ImageVector, @StringRes stringId: Int) {
|
||||
Icon(
|
||||
icon,
|
||||
stringResource(stringId),
|
||||
Modifier.fillMaxSize(),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun progressCircle(progress: Long, total: Long) {
|
||||
val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat()
|
||||
val strokeWidth = with(LocalDensity.current) { 2.dp.toPx() }
|
||||
val strokeColor = Color.White
|
||||
Surface(
|
||||
Modifier.drawRingModifier(angle, strokeColor, strokeWidth),
|
||||
color = Color.Transparent,
|
||||
shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50))
|
||||
) {
|
||||
Box(Modifier.size(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun loadingIndicator(file: CIFile?) {
|
||||
if (file != null) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(8.dp)
|
||||
.size(20.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
when (file.fileStatus) {
|
||||
is CIFileStatus.SndStored ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressIndicator()
|
||||
FileProtocol.SMP -> {}
|
||||
}
|
||||
is CIFileStatus.SndTransfer ->
|
||||
when (file.fileProtocol) {
|
||||
FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal)
|
||||
FileProtocol.SMP -> progressIndicator()
|
||||
}
|
||||
is CIFileStatus.SndComplete -> fileIcon(Icons.Filled.Check, R.string.icon_descr_video_snd_complete)
|
||||
is CIFileStatus.SndCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
is CIFileStatus.SndError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
is CIFileStatus.RcvInvitation -> fileIcon(Icons.Outlined.ArrowDownward, R.string.icon_descr_video_asked_to_receive)
|
||||
is CIFileStatus.RcvAccepted -> fileIcon(Icons.Outlined.MoreHoriz, R.string.icon_descr_waiting_for_video)
|
||||
is CIFileStatus.RcvTransfer ->
|
||||
if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) {
|
||||
progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal)
|
||||
} else {
|
||||
progressIndicator()
|
||||
}
|
||||
is CIFileStatus.RcvCancelled -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
is CIFileStatus.RcvError -> fileIcon(Icons.Outlined.Close, R.string.icon_descr_file)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fileSizeValid(file: CIFile?): Boolean {
|
||||
if (file != null) {
|
||||
return file.fileSize <= getMaxFileSize(file.fileProtocol)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) {
|
||||
if (fileSizeValid(file)) {
|
||||
receiveFile(file.fileId)
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.large_file),
|
||||
String.format(generalGetString(R.string.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol)))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun videoViewFullWidth(windowWidth: Dp): Dp {
|
||||
val approximatePadding = 100.dp
|
||||
return minOf(1000.dp, windowWidth - approximatePadding)
|
||||
}
|
||||
@@ -210,9 +210,9 @@ private fun VoiceMsgIndicator(
|
||||
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
|
||||
}
|
||||
} else {
|
||||
if (file?.fileStatus == CIFileStatus.RcvInvitation
|
||||
|| file?.fileStatus == CIFileStatus.RcvTransfer
|
||||
|| file?.fileStatus == CIFileStatus.RcvAccepted
|
||||
if (file?.fileStatus is CIFileStatus.RcvInvitation
|
||||
|| file?.fileStatus is CIFileStatus.RcvTransfer
|
||||
|| file?.fileStatus is CIFileStatus.RcvAccepted
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
@@ -228,7 +228,7 @@ private fun VoiceMsgIndicator(
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
|
||||
fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = drawWithCache {
|
||||
val brush = Brush.linearGradient(
|
||||
0f to Color.Transparent,
|
||||
0f to color,
|
||||
|
||||
@@ -22,8 +22,7 @@ import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.ComposeContextItem
|
||||
import chat.simplex.app.views.chat.ComposeState
|
||||
import chat.simplex.app.views.helpers.*
|
||||
@@ -43,6 +42,7 @@ fun ChatItemView(
|
||||
linkMode: SimplexLinkMode,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
@@ -104,11 +104,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable
|
||||
fun MsgContentItemDropdownMenu() {
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cItem.meta.itemDeleted == null && !live) {
|
||||
ItemAction(stringResource(R.string.reply_verb), Icons.Outlined.Reply, onClick = {
|
||||
if (composeState.value.editing) {
|
||||
@@ -131,7 +127,7 @@ fun ChatItemView(
|
||||
copyText(context, cItem.content.text)
|
||||
showMenu.value = false
|
||||
})
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) {
|
||||
val filePath = getLoadedFilePath(context, cItem.file)
|
||||
if (filePath != null) {
|
||||
val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
@@ -144,8 +140,7 @@ fun ChatItemView(
|
||||
writePermissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
is MsgContent.MCVoice -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
|
||||
else -> {}
|
||||
}
|
||||
showMenu.value = false
|
||||
@@ -168,6 +163,9 @@ fun ChatItemView(
|
||||
}
|
||||
)
|
||||
}
|
||||
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive)) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
@@ -180,11 +178,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemDropdownMenu() {
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (!cItem.isDeletedContent) {
|
||||
ItemAction(
|
||||
stringResource(R.string.reveal_verb),
|
||||
@@ -224,11 +218,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable fun DeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
|
||||
}
|
||||
}
|
||||
@@ -244,7 +234,8 @@ fun ChatItemView(
|
||||
is CIContent.RcvDeleted -> DeletedItem()
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
|
||||
is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> CIEventView(cItem)
|
||||
@@ -270,6 +261,24 @@ fun ChatItemView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CancelFileItemAction(
|
||||
fileId: Long,
|
||||
showMenu: MutableState<Boolean>,
|
||||
cancelFile: (Long) -> Unit,
|
||||
cancelAction: CancelAction
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(cancelAction.uiActionId),
|
||||
Icons.Outlined.Close,
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
cancelFileAlertDialog(fileId, cancelFile = cancelFile, cancelAction = cancelAction)
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteItemAction(
|
||||
cItem: ChatItem,
|
||||
@@ -307,22 +316,37 @@ fun ModerateItemAction(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = MaterialTheme.colors.onBackground) {
|
||||
DropdownMenuItem(onClick) {
|
||||
Row {
|
||||
fun ItemAction(text: String, icon: ImageVector, onClick: () -> Unit, color: Color = Color.Unspecified) {
|
||||
val finalColor = if (color == Color.Unspecified) {
|
||||
if (isInDarkTheme()) MenuTextColorDark else Color.Black
|
||||
} else color
|
||||
DropdownMenuItem(onClick, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = color
|
||||
color = finalColor
|
||||
)
|
||||
Icon(icon, text, tint = color)
|
||||
Icon(icon, text, tint = finalColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileAlertDialog(fileId: Long, cancelFile: (Long) -> Unit, cancelAction: CancelAction) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(cancelAction.alert.titleId),
|
||||
text = generalGetString(cancelAction.alert.messageId),
|
||||
confirmText = generalGetString(cancelAction.alert.confirmId),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
cancelFile(fileId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialogButtons(
|
||||
title = generalGetString(R.string.delete_message__question),
|
||||
@@ -332,7 +356,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmInternal)
|
||||
@@ -383,6 +407,7 @@ fun PreviewChatItemView() {
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
@@ -403,6 +428,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
|
||||
deleteMessage = { _, _ -> },
|
||||
receiveFile = {},
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
|
||||
@@ -125,6 +125,18 @@ fun FramedItemView(
|
||||
modifier = Modifier.size(68.dp).clipToBounds()
|
||||
)
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap()
|
||||
Image(
|
||||
imageBitmap,
|
||||
contentDescription = stringResource(R.string.video_descr),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(68.dp).clipToBounds()
|
||||
)
|
||||
}
|
||||
is MsgContent.MCFile, is MsgContent.MCVoice -> {
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
ciQuotedMsgView(qi)
|
||||
@@ -151,7 +163,8 @@ fun FramedItemView(
|
||||
}
|
||||
}
|
||||
|
||||
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
|
||||
val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) &&
|
||||
!ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null
|
||||
|
||||
Box(Modifier
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
@@ -198,6 +211,14 @@ fun FramedItemView(
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
|
||||
if (mc.text != "") {
|
||||
|
||||
+119
-54
@@ -3,21 +3,28 @@ package chat.simplex.app.views.chat.item
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.input.pointer.*
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.view.isVisible
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.chat.ProviderMedia
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import coil.ImageLoader
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
@@ -26,13 +33,16 @@ import coil.decode.ImageDecoderDecoder
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.google.accompanist.pager.*
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
interface ImageGalleryProvider {
|
||||
val initialIndex: Int
|
||||
val totalImagesSize: MutableState<Int>
|
||||
fun getImage(index: Int): Pair<Bitmap, Uri>?
|
||||
val totalMediaSize: MutableState<Int>
|
||||
fun getMedia(index: Int): ProviderMedia?
|
||||
fun currentPageChanged(index: Int)
|
||||
fun scrollToStart()
|
||||
fun onDismiss(index: Int)
|
||||
@@ -48,13 +58,17 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
// Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank,
|
||||
// which makes this blank page visible for a moment. Prevent it by doing the check ourselves
|
||||
LaunchedEffect(Unit) {
|
||||
if (provider.getImage(provider.initialIndex - 1) == null) {
|
||||
if (provider.getMedia(provider.initialIndex - 1) == null) {
|
||||
provider.scrollToStart()
|
||||
pagerState.scrollToPage(0)
|
||||
}
|
||||
}
|
||||
val scope = rememberCoroutineScope()
|
||||
HorizontalPager(count = remember { provider.totalImagesSize }.value, state = pagerState) { index ->
|
||||
val playersToRelease = rememberSaveable { mutableSetOf<Uri>() }
|
||||
DisposableEffectOnGone(
|
||||
whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } }
|
||||
)
|
||||
HorizontalPager(count = remember { provider.totalMediaSize }.value, state = pagerState) { index ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
@@ -74,13 +88,13 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
if (settledCurrentPage != provider.initialIndex)
|
||||
provider.currentPageChanged(index)
|
||||
}
|
||||
val image = provider.getImage(index)
|
||||
if (image == null) {
|
||||
val media = provider.getMedia(index)
|
||||
if (media == null) {
|
||||
// No such image. Let's shrink total pages size or scroll to start of the list of pages to remove blank page automatically
|
||||
SideEffect {
|
||||
scope.launch {
|
||||
when (settledCurrentPage) {
|
||||
index - 1 -> provider.totalImagesSize.value = settledCurrentPage + 1
|
||||
index - 1 -> provider.totalMediaSize.value = settledCurrentPage + 1
|
||||
index + 1 -> {
|
||||
provider.scrollToStart()
|
||||
pagerState.scrollToPage(0)
|
||||
@@ -89,7 +103,6 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val (imageBitmap: Bitmap, uri: Uri) = image
|
||||
var scale by remember { mutableStateOf(1f) }
|
||||
var translationX by remember { mutableStateOf(0f) }
|
||||
var translationY by remember { mutableStateOf(0f) }
|
||||
@@ -100,54 +113,106 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () ->
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
// 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)
|
||||
.components {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
val modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
viewWidth = it.size.width
|
||||
}
|
||||
.build()
|
||||
Image(
|
||||
rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier
|
||||
.onGloballyPositioned {
|
||||
viewWidth = it.size.width
|
||||
}
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = translationX,
|
||||
translationY = translationY,
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures(
|
||||
{ allowTranslate },
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
|
||||
if (scale > 1 && allowTranslate) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else if (allowTranslate) {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = translationX,
|
||||
translationY = translationY,
|
||||
)
|
||||
.pointerInput(Unit) {
|
||||
detectTransformGestures(
|
||||
{ allowTranslate },
|
||||
onGesture = { _, pan, gestureZoom, _ ->
|
||||
scale = (scale * gestureZoom).coerceIn(1f, 20f)
|
||||
allowTranslate = viewWidth * (scale - 1f) - ((translationX + pan.x * scale).absoluteValue * 2) > 0
|
||||
if (scale > 1 && allowTranslate) {
|
||||
translationX += pan.x * scale
|
||||
translationY += pan.y * scale
|
||||
} else if (allowTranslate) {
|
||||
translationX = 0f
|
||||
translationY = 0f
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
.fillMaxSize()
|
||||
if (media is ProviderMedia.Image) {
|
||||
val (uri: Uri, imageBitmap: Bitmap) = media
|
||||
// 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)
|
||||
.components {
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.fillMaxSize(),
|
||||
)
|
||||
.build()
|
||||
Image(
|
||||
rememberAsyncImagePainter(
|
||||
ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(),
|
||||
placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil
|
||||
imageLoader = imageLoader
|
||||
),
|
||||
contentDescription = stringResource(R.string.image_descr),
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = modifier,
|
||||
)
|
||||
} else if (media is ProviderMedia.Video) {
|
||||
val preview = remember(media.uri.path) { base64ToBitmap(media.preview) }
|
||||
VideoView(modifier, media.uri, preview, index == settledCurrentPage)
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { playersToRelease.add(media.uri) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) {
|
||||
val context = LocalContext.current
|
||||
val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true, context) }
|
||||
val isCurrentPage = rememberUpdatedState(currentPage)
|
||||
val play = {
|
||||
player.play(true)
|
||||
}
|
||||
val stop = {
|
||||
player.stop()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
player.enableSound(true)
|
||||
snapshotFlow { isCurrentPage.value }
|
||||
.distinctUntilChanged()
|
||||
.collect { if (it) play() else stop() }
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
StyledPlayerView(ctx).apply {
|
||||
resizeMode = if (ctx.resources.configuration.screenWidthDp > ctx.resources.configuration.screenHeightDp) {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT
|
||||
} else {
|
||||
AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH
|
||||
}
|
||||
setShowPreviousButton(false)
|
||||
setShowNextButton(false)
|
||||
setShowSubtitleButton(false)
|
||||
setShowVrButton(false)
|
||||
controllerAutoShow = false
|
||||
findViewById<View>(com.google.android.exoplayer2.R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb())
|
||||
findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false
|
||||
this.player = player.player
|
||||
}
|
||||
},
|
||||
modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-7
@@ -17,19 +17,41 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatItem
|
||||
import chat.simplex.app.model.MsgErrorType
|
||||
import chat.simplex.app.ui.theme.SimpleXTheme
|
||||
import chat.simplex.app.views.helpers.AlertManager
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
|
||||
CIMsgError(ci, timedMessagesTTL, showMember) {
|
||||
when (msgError) {
|
||||
is MsgErrorType.MsgSkipped ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.alert_title_skipped_messages),
|
||||
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
|
||||
)
|
||||
is MsgErrorType.MsgBadHash ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.alert_title_msg_bad_hash),
|
||||
text = generalGetString(R.string.alert_text_msg_bad_hash) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_encryption_out_of_sync_old_database) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
|
||||
)
|
||||
is MsgErrorType.MsgBadId, is MsgErrorType.MsgDuplicate ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.alert_title_msg_bad_id),
|
||||
text = generalGetString(R.string.alert_text_msg_bad_id) + "\n" +
|
||||
generalGetString(R.string.alert_text_fragment_please_report_to_developers)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) {
|
||||
Surface(
|
||||
Modifier.clickable(onClick = {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.alert_title_skipped_messages),
|
||||
text = generalGetString(R.string.alert_text_skipped_messages_it_can_happen_when)
|
||||
)
|
||||
}),
|
||||
Modifier.clickable(onClick = onClick),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
color = ReceivedColorLight,
|
||||
) {
|
||||
@@ -59,6 +81,7 @@ fun IntegrityErrorItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boo
|
||||
fun IntegrityErrorItemViewPreview() {
|
||||
SimpleXTheme {
|
||||
IntegrityErrorItemView(
|
||||
MsgErrorType.MsgBadHash(),
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
)
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth().weight(1f)) {
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
if (ci.meta.itemDeleted is CIDeleted.Moderated) {
|
||||
MarkedDeletedText(String.format(generalGetString(R.string.moderated_item_description), ci.meta.itemDeleted.byGroupMember.chatViewName))
|
||||
} else {
|
||||
|
||||
+9
-29
@@ -194,26 +194,14 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<
|
||||
|
||||
@Composable
|
||||
fun MarkUnreadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>) {
|
||||
DropdownMenuItem({
|
||||
markChatUnread(chat, chatModel)
|
||||
showMenu.value = false
|
||||
}) {
|
||||
Row {
|
||||
Text(
|
||||
stringResource(R.string.mark_unread),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
.padding(end = 15.dp),
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Icon(
|
||||
Icons.Outlined.MarkChatUnread,
|
||||
stringResource(R.string.mark_unread),
|
||||
tint = MaterialTheme.colors.onBackground
|
||||
)
|
||||
ItemAction(
|
||||
stringResource(R.string.mark_unread),
|
||||
Icons.Outlined.MarkChatUnread,
|
||||
onClick = {
|
||||
markChatUnread(chat, chatModel)
|
||||
showMenu.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -447,7 +435,7 @@ fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
AlertManager.shared.hideAlert()
|
||||
@@ -589,15 +577,7 @@ fun ChatListNavLinkLayout(
|
||||
chatLinkPreview()
|
||||
}
|
||||
if (dropdownMenuItems != null) {
|
||||
Box(Modifier.padding(horizontal = 16.dp)) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
dropdownMenuItems()
|
||||
}
|
||||
}
|
||||
DefaultDropdownMenu(showMenu, dropdownMenuItems = dropdownMenuItems)
|
||||
}
|
||||
}
|
||||
Divider(Modifier.padding(horizontal = 8.dp))
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.capitalize
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -36,7 +37,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) {
|
||||
fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit, stopped: Boolean) {
|
||||
val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val showNewChatSheet = {
|
||||
@@ -208,7 +209,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
} else if (chatModel.users.isEmpty()) {
|
||||
NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } }
|
||||
} else {
|
||||
val users by remember { derivedStateOf { chatModel.users.toList() } }
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
@@ -247,7 +248,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) {
|
||||
IconButton(onClick = onButtonClicked) {
|
||||
Box {
|
||||
ProfileImage(
|
||||
|
||||
@@ -86,7 +86,7 @@ fun ChatPreviewView(
|
||||
fun attachment(): Pair<ImageVector, String?>? =
|
||||
when (draft.preview) {
|
||||
is ComposePreview.FilePreview -> Icons.Filled.InsertDriveFile to draft.preview.fileName
|
||||
is ComposePreview.ImagePreview -> Icons.Outlined.Image to null
|
||||
is ComposePreview.MediaPreview -> Icons.Outlined.Image to null
|
||||
is ComposePreview.VoicePreview -> Icons.Filled.PlayArrow to durationText(draft.preview.durationMs / 1000)
|
||||
else -> null
|
||||
}
|
||||
|
||||
@@ -24,12 +24,15 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.Indigo
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
var searchInList by rememberSaveable { mutableStateOf("") }
|
||||
val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) }
|
||||
val switchingUsers = rememberSaveable { mutableStateOf(false) }
|
||||
Scaffold(
|
||||
topBar = { Column { ShareListToolbar(chatModel, stopped) { searchInList = it.trim() } } },
|
||||
topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } },
|
||||
) {
|
||||
Box(Modifier.padding(it)) {
|
||||
Column(
|
||||
@@ -45,23 +48,41 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
|
||||
}
|
||||
}
|
||||
}
|
||||
UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = {
|
||||
chatModel.sharedContent.value = null
|
||||
})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmptyList() {
|
||||
Box {
|
||||
Text(stringResource(R.string.you_have_no_chats), Modifier.align(Alignment.Center), color = HighOrLowlight)
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(R.string.you_have_no_chats), color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, stopped: Boolean, onSearchValueChanged: (String) -> Unit) {
|
||||
var showSearch by rememberSaveable { mutableStateOf(false) }
|
||||
val hideSearchOnBack = { onSearchValueChanged(""); showSearch = false }
|
||||
if (showSearch) {
|
||||
BackHandler(onBack = hideSearchOnBack)
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } }
|
||||
val navButton: @Composable RowScope.() -> Unit = {
|
||||
when {
|
||||
showSearch -> NavigationButtonBack(hideSearchOnBack)
|
||||
users.size > 1 -> {
|
||||
val allRead = users
|
||||
.filter { u -> !u.user.activeUser && !u.user.hidden }
|
||||
.all { u -> u.unreadCount == 0 }
|
||||
UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) {
|
||||
userPickerState.value = AnimatedViewState.VISIBLE
|
||||
}
|
||||
}
|
||||
else -> NavigationButtonBack { chatModel.sharedContent.value = null }
|
||||
}
|
||||
}
|
||||
if (chatModel.chats.size >= 8) {
|
||||
barButtons.add {
|
||||
IconButton({ showSearch = true }) {
|
||||
@@ -87,13 +108,13 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal
|
||||
}
|
||||
|
||||
DefaultTopAppBar(
|
||||
navigationButton = { if (showSearch) NavigationButtonBack(hideSearchOnBack) else NavigationButtonBack { chatModel.sharedContent.value = null } },
|
||||
navigationButton = navButton,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
when (chatModel.sharedContent.value) {
|
||||
is SharedContent.Text -> stringResource(R.string.share_message)
|
||||
is SharedContent.Images -> stringResource(R.string.share_image)
|
||||
is SharedContent.Media -> stringResource(R.string.share_image)
|
||||
is SharedContent.File -> stringResource(R.string.share_file)
|
||||
else -> stringResource(R.string.share_message)
|
||||
},
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package chat.simplex.app.views.chatlist
|
||||
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.draw.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
@@ -34,7 +35,15 @@ import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedViewState>, switchingUsers: MutableState<Boolean>, openSettings: () -> Unit) {
|
||||
fun UserPicker(
|
||||
chatModel: ChatModel,
|
||||
userPickerState: MutableStateFlow<AnimatedViewState>,
|
||||
switchingUsers: MutableState<Boolean>,
|
||||
showSettings: Boolean = true,
|
||||
showCancel: Boolean = false,
|
||||
cancelClicked: () -> Unit = {},
|
||||
settingsClicked: () -> Unit = {},
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var newChat by remember { mutableStateOf(userPickerState.value) }
|
||||
val users by remember {
|
||||
@@ -97,16 +106,17 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.widthIn(min = 220.dp)
|
||||
.widthIn(min = 260.dp)
|
||||
.width(IntrinsicSize.Min)
|
||||
.height(IntrinsicSize.Min)
|
||||
.shadow(8.dp, MaterialTheme.shapes.medium, clip = false)
|
||||
.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)
|
||||
.shadow(8.dp, RoundedCornerShape(corner = CornerSize(25.dp)), clip = true)
|
||||
.background(if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.clip(RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
) {
|
||||
Column(Modifier.weight(1f).verticalScroll(rememberScrollState())) {
|
||||
users.forEach { u ->
|
||||
UserProfilePickerItem(u.user, u.unreadCount, openSettings = {
|
||||
openSettings()
|
||||
UserProfilePickerItem(u.user, u.unreadCount, PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING * 2), openSettings = {
|
||||
settingsClicked()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}) {
|
||||
userPickerState.value = AnimatedViewState.HIDING
|
||||
@@ -126,16 +136,24 @@ fun UserPicker(chatModel: ChatModel, userPickerState: MutableStateFlow<AnimatedV
|
||||
if (u.user.activeUser) Divider(Modifier.requiredHeight(0.5.dp))
|
||||
}
|
||||
}
|
||||
SettingsPickerItem {
|
||||
openSettings()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
if (showSettings) {
|
||||
SettingsPickerItem {
|
||||
settingsClicked()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
if (showCancel) {
|
||||
CancelPickerItem {
|
||||
cancelClicked()
|
||||
userPickerState.value = AnimatedViewState.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues = PaddingValues(start = 8.dp, end = DEFAULT_PADDING), onLongClick: () -> Unit = {}, openSettings: () -> Unit = {}, onClick: () -> Unit) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -146,7 +164,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, onLongClick: () -> Unit
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = if (!u.activeUser) LocalIndication.current else null
|
||||
)
|
||||
.padding(PaddingValues(start = 8.dp, end = DEFAULT_PADDING)),
|
||||
.padding(padding),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -195,6 +213,7 @@ fun UserProfileRow(u: User) {
|
||||
u.displayName,
|
||||
modifier = Modifier
|
||||
.padding(start = 8.dp, end = 8.dp),
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
fontWeight = if (u.activeUser) FontWeight.Medium else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
@@ -202,12 +221,26 @@ fun UserProfileRow(u: User) {
|
||||
|
||||
@Composable
|
||||
private fun SettingsPickerItem(onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, minHeight = 68.dp) {
|
||||
SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING * 2.2f, end = DEFAULT_PADDING * 2), minHeight = 68.dp) {
|
||||
val text = generalGetString(R.string.settings_section_title_settings).lowercase().capitalize(Locale.current)
|
||||
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING * 1.5f))
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CancelPickerItem(onClick: () -> Unit) {
|
||||
SectionItemViewSpaceBetween(onClick, padding = PaddingValues(start = DEFAULT_PADDING * 2.2f, end = DEFAULT_PADDING * 2), minHeight = 68.dp) {
|
||||
val text = generalGetString(R.string.cancel_verb)
|
||||
Icon(Icons.Outlined.Close, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
Spacer(Modifier.width(DEFAULT_PADDING * 1.5f))
|
||||
Text(
|
||||
text,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
)
|
||||
Icon(Icons.Outlined.Settings, text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -42,9 +42,9 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
|
||||
val storedKey = remember { val key = DatabaseUtils.getDatabaseKey(); mutableStateOf(key != null && key != "") }
|
||||
val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") }
|
||||
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.getDatabaseKey() ?: "" else "") }
|
||||
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
|
||||
val newKey = rememberSaveable { mutableStateOf("") }
|
||||
val confirmNewKey = rememberSaveable { mutableStateOf("") }
|
||||
|
||||
@@ -89,7 +89,7 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
prefs.initialRandomDBPassphrase.set(false)
|
||||
initialRandomDBPassphrase.value = false
|
||||
if (useKeychain.value) {
|
||||
DatabaseUtils.setDatabaseKey(newKey.value)
|
||||
DatabaseUtils.ksDatabasePassword.set(newKey.value)
|
||||
}
|
||||
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
|
||||
operationEnded(m, progressIndicator) {
|
||||
@@ -150,7 +150,7 @@ fun DatabaseEncryptionLayout(
|
||||
text = generalGetString(R.string.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
|
||||
confirmText = generalGetString(R.string.remove_passphrase),
|
||||
onConfirm = {
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
setUseKeychain(false, useKeychain, prefs)
|
||||
storedKey.value = false
|
||||
},
|
||||
@@ -366,7 +366,7 @@ fun PassphraseField(
|
||||
showStrength: Boolean = false,
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
dependsOn: MutableState<String>? = null,
|
||||
dependsOn: State<Any?>? = null,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(key.value)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
@@ -479,7 +479,7 @@ private fun passphraseEntropy(s: String): Double {
|
||||
return s.length * log2(poolSize.toDouble())
|
||||
}
|
||||
|
||||
private enum class PassphraseStrength(val color: Color) {
|
||||
enum class PassphraseStrength(val color: Color) {
|
||||
VERY_WEAK(Color.Red), WEAK(WarningOrange), REASONABLE(WarningYellow), STRONG(SimplexGreen);
|
||||
|
||||
companion object {
|
||||
@@ -522,4 +522,4 @@ fun PreviewDatabaseEncryptionLayout() {
|
||||
onConfirmEncrypt = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+120
-66
@@ -4,6 +4,7 @@ import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -13,6 +14,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
@@ -20,6 +22,7 @@ import chat.simplex.app.R
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.AppVersionText
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
@@ -35,28 +38,43 @@ fun DatabaseErrorView(
|
||||
) {
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val dbKey = remember { mutableStateOf("") }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) }
|
||||
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
val context = LocalContext.current
|
||||
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
|
||||
val saveAndRunChatOnClick: () -> Unit = {
|
||||
DatabaseUtils.setDatabaseKey(dbKey.value)
|
||||
|
||||
fun callRunChat(confirmMigrations: MigrationConfirmation? = null) {
|
||||
val useKey = if (useKeychain) null else dbKey.value
|
||||
runChat(useKey, confirmMigrations, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
|
||||
fun saveAndRunChatOnClick() {
|
||||
DatabaseUtils.ksDatabasePassword.set(dbKey.value)
|
||||
storedDBKey = dbKey.value
|
||||
appPreferences.storeDBPassphrase.set(true)
|
||||
useKeychain = true
|
||||
appPreferences.initialRandomDBPassphrase.set(false)
|
||||
runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
callRunChat()
|
||||
}
|
||||
val title = when (chatDbStatus.value) {
|
||||
is DBMigrationResult.OK -> ""
|
||||
is DBMigrationResult.ErrorNotADatabase -> if (useKeychain && !storedDBKey.isNullOrEmpty())
|
||||
generalGetString(R.string.wrong_passphrase)
|
||||
else
|
||||
generalGetString(R.string.encrypted_database)
|
||||
is DBMigrationResult.Error -> generalGetString(R.string.database_error)
|
||||
is DBMigrationResult.ErrorKeychain -> generalGetString(R.string.keychain_error)
|
||||
is DBMigrationResult.Unknown -> generalGetString(R.string.database_error)
|
||||
null -> "" // should never be here
|
||||
|
||||
@Composable
|
||||
fun DatabaseErrorDetails(@StringRes title: Int, content: @Composable ColumnScope.() -> Unit) {
|
||||
Text(
|
||||
generalGetString(title),
|
||||
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FileNameText(dbFile: String) {
|
||||
Text(String.format(generalGetString(R.string.file_with_path), dbFile.split("/").lastOrNull() ?: dbFile))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MigrationsText(ms: List<String>) {
|
||||
Text(String.format(generalGetString(R.string.database_migrations), ms.joinToString(", ")))
|
||||
}
|
||||
|
||||
Column(
|
||||
@@ -64,63 +82,91 @@ fun DatabaseErrorView(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
Modifier.padding(start = 16.dp, top = 16.dp, bottom = 24.dp),
|
||||
style = MaterialTheme.typography.h1
|
||||
)
|
||||
SectionView(null, padding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
|
||||
val buttonEnabled = validKey(dbKey.value) && !progressIndicator.value
|
||||
when (val status = chatDbStatus.value) {
|
||||
is DBMigrationResult.ErrorNotADatabase ->
|
||||
if (useKeychain && !storedDBKey.isNullOrEmpty()) {
|
||||
DatabaseErrorDetails(R.string.wrong_passphrase) {
|
||||
Text(generalGetString(R.string.passphrase_is_different))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
saveAndRunChatOnClick()
|
||||
}
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
|
||||
SectionSpacer()
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
} else {
|
||||
FileNameText(status.dbFile)
|
||||
}
|
||||
} else {
|
||||
DatabaseErrorDetails(R.string.encrypted_database) {
|
||||
Text(generalGetString(R.string.database_passphrase_is_required))
|
||||
DatabaseKeyField(dbKey, buttonEnabled) {
|
||||
if (useKeychain) saveAndRunChatOnClick() else runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences)
|
||||
}
|
||||
if (useKeychain) {
|
||||
SaveAndOpenButton(buttonEnabled, saveAndRunChatOnClick)
|
||||
DatabaseKeyField(dbKey, buttonEnabled, ::saveAndRunChatOnClick)
|
||||
SaveAndOpenButton(buttonEnabled, ::saveAndRunChatOnClick)
|
||||
} else {
|
||||
OpenChatButton(buttonEnabled) { runChat(dbKey.value, chatDbStatus, progressIndicator, appPreferences) }
|
||||
DatabaseKeyField(dbKey, buttonEnabled) { callRunChat() }
|
||||
OpenChatButton(buttonEnabled) { callRunChat() }
|
||||
}
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
Text(String.format(generalGetString(R.string.file_with_path), status.dbFile))
|
||||
Text(String.format(generalGetString(R.string.error_with_info), status.migrationError))
|
||||
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
|
||||
is MigrationError.Upgrade ->
|
||||
DatabaseErrorDetails(R.string.database_upgrade) {
|
||||
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
|
||||
Text(generalGetString(R.string.upgrade_and_open_chat))
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
FileNameText(status.dbFile)
|
||||
MigrationsText(err.upMigrations.map { it.upName })
|
||||
AppVersionText()
|
||||
}
|
||||
is MigrationError.Downgrade ->
|
||||
DatabaseErrorDetails(R.string.database_downgrade) {
|
||||
TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) {
|
||||
Text(generalGetString(R.string.downgrade_and_open_chat))
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Text(generalGetString(R.string.database_downgrade_warning), fontWeight = FontWeight.Bold)
|
||||
FileNameText(status.dbFile)
|
||||
MigrationsText(err.downMigrations)
|
||||
AppVersionText()
|
||||
}
|
||||
is MigrationError.Error ->
|
||||
DatabaseErrorDetails(R.string.incompatible_database_version) {
|
||||
FileNameText(status.dbFile)
|
||||
Text(String.format(generalGetString(R.string.error_with_info), mtrErrorDescription(err.mtrError)))
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.ErrorSQL ->
|
||||
DatabaseErrorDetails(R.string.database_error) {
|
||||
FileNameText(status.dbFile)
|
||||
Text(String.format(generalGetString(R.string.error_with_info), status.migrationSQLError))
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
is DBMigrationResult.ErrorKeychain ->
|
||||
DatabaseErrorDetails(R.string.keychain_error) {
|
||||
Text(generalGetString(R.string.cannot_access_keychain))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
is DBMigrationResult.InvalidConfirmation ->
|
||||
DatabaseErrorDetails(R.string.invalid_migration_confirmation) {
|
||||
// this can only happen if incorrect parameter is passed
|
||||
}
|
||||
is DBMigrationResult.Unknown ->
|
||||
DatabaseErrorDetails(R.string.database_error) {
|
||||
Text(String.format(generalGetString(R.string.unknown_database_error_with_info), status.json))
|
||||
}
|
||||
is DBMigrationResult.OK -> {
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
if (restoreDbFromBackup.value) {
|
||||
SectionSpacer()
|
||||
Text(generalGetString(R.string.database_backup_can_be_restored))
|
||||
Spacer(Modifier.size(16.dp))
|
||||
RestoreDbButton {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.restore_database_alert_title),
|
||||
text = generalGetString(R.string.restore_database_alert_desc),
|
||||
confirmText = generalGetString(R.string.restore_database_alert_confirm),
|
||||
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
is DBMigrationResult.OK -> {}
|
||||
null -> {}
|
||||
}
|
||||
if (restoreDbFromBackup.value) {
|
||||
SectionSpacer()
|
||||
Text(generalGetString(R.string.database_backup_can_be_restored))
|
||||
Spacer(Modifier.size(16.dp))
|
||||
RestoreDbButton {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.restore_database_alert_title),
|
||||
text = generalGetString(R.string.restore_database_alert_desc),
|
||||
confirmText = generalGetString(R.string.restore_database_alert_confirm),
|
||||
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +187,8 @@ fun DatabaseErrorView(
|
||||
}
|
||||
|
||||
private fun runChat(
|
||||
dbKey: String,
|
||||
dbKey: String? = null,
|
||||
confirmMigrations: MigrationConfirmation? = null,
|
||||
chatDbStatus: State<DBMigrationResult?>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
prefs: AppPreferences
|
||||
@@ -150,7 +197,7 @@ private fun runChat(
|
||||
if (progressIndicator.value) return@launch
|
||||
progressIndicator.value = true
|
||||
try {
|
||||
SimplexApp.context.initChatController(dbKey)
|
||||
SimplexApp.context.initChatController(dbKey, confirmMigrations)
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
|
||||
}
|
||||
@@ -163,18 +210,17 @@ private fun runChat(
|
||||
NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
}
|
||||
is DBMigrationResult.ErrorNotADatabase -> {
|
||||
is DBMigrationResult.ErrorNotADatabase ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.wrong_passphrase_title), generalGetString(R.string.enter_correct_passphrase))
|
||||
}
|
||||
is DBMigrationResult.Error -> {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationError)
|
||||
}
|
||||
is DBMigrationResult.ErrorKeychain -> {
|
||||
is DBMigrationResult.ErrorSQL ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_error), status.migrationSQLError)
|
||||
is DBMigrationResult.ErrorKeychain ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.keychain_error))
|
||||
}
|
||||
is DBMigrationResult.Unknown -> {
|
||||
is DBMigrationResult.Unknown ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), status.json)
|
||||
}
|
||||
is DBMigrationResult.InvalidConfirmation ->
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.invalid_migration_confirmation))
|
||||
is DBMigrationResult.ErrorMigration -> {}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +250,14 @@ private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPref
|
||||
}
|
||||
}
|
||||
|
||||
private fun mtrErrorDescription(err: MTRError): String =
|
||||
when (err) {
|
||||
is MTRError.NoDown ->
|
||||
String.format(generalGetString(R.string.mtr_error_no_down_migration), err.dbMigrations.joinToString(", "))
|
||||
is MTRError.Different ->
|
||||
String.format(generalGetString(R.string.mtr_error_different), err.appMigration, err.dbMigration)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
PassphraseField(
|
||||
|
||||
@@ -52,7 +52,7 @@ fun DatabaseView(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val progressIndicator = remember { mutableStateOf(false) }
|
||||
val runChat = remember { mutableStateOf(m.chatRunning.value ?: true) }
|
||||
val runChat = remember { m.chatRunning }
|
||||
val prefs = m.controller.appPrefs
|
||||
val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
|
||||
val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) }
|
||||
@@ -76,7 +76,7 @@ fun DatabaseView(
|
||||
) {
|
||||
DatabaseLayout(
|
||||
progressIndicator.value,
|
||||
runChat.value,
|
||||
runChat.value != false,
|
||||
m.chatDbChanged.value,
|
||||
useKeychain.value,
|
||||
m.chatDbEncrypted.value,
|
||||
@@ -160,7 +160,7 @@ fun DatabaseLayout(
|
||||
AppBarTitle(stringResource(R.string.your_chat_database))
|
||||
|
||||
SectionView(stringResource(R.string.messages_section_title).uppercase()) {
|
||||
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!progressIndicator && !chatDbChanged), onChatItemTTLSelected) }
|
||||
SectionItemView { TtlOptions(chatItemTTL, enabled = rememberUpdatedState(!stopped && !progressIndicator), onChatItemTTLSelected) }
|
||||
}
|
||||
SectionTextFooter(
|
||||
remember(currentUser?.displayName) {
|
||||
@@ -388,7 +388,7 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String {
|
||||
return stringResource(if (chatArchiveTime < chatLastStart) R.string.old_database_archive else R.string.new_database_archive)
|
||||
}
|
||||
|
||||
private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) {
|
||||
withApi {
|
||||
try {
|
||||
if (chatDbChanged.value) {
|
||||
@@ -417,7 +417,7 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean>, chatLastStar
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.stop_chat_question),
|
||||
text = generalGetString(R.string.stop_chat_to_export_import_or_delete_chat_database),
|
||||
@@ -434,7 +434,7 @@ private fun exportProhibitedAlert() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
|
||||
if (m.controller.appPrefs.performLA.get()) {
|
||||
authenticate(
|
||||
generalGetString(R.string.auth_stop_chat),
|
||||
@@ -442,12 +442,13 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success, LAResult.Unavailable -> {
|
||||
LAResult.Success, is LAResult.Unavailable -> {
|
||||
stopChat(m, runChat, context)
|
||||
}
|
||||
is LAResult.Error -> {
|
||||
runChat.value = true
|
||||
}
|
||||
LAResult.Failed -> {
|
||||
is LAResult.Failed -> {
|
||||
runChat.value = true
|
||||
}
|
||||
}
|
||||
@@ -458,7 +459,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean>, context:
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean>, context: Context) {
|
||||
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
|
||||
withApi {
|
||||
try {
|
||||
m.controller.apiStopChat()
|
||||
@@ -592,7 +593,7 @@ private fun importArchive(
|
||||
try {
|
||||
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
|
||||
m.controller.apiImportArchive(config)
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
|
||||
@@ -647,7 +648,7 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
|
||||
try {
|
||||
m.controller.apiDeleteStorage()
|
||||
m.chatDbDeleted.value = true
|
||||
DatabaseUtils.removeDatabaseKey()
|
||||
DatabaseUtils.ksDatabasePassword.remove()
|
||||
m.controller.appPrefs.storeDBPassphrase.set(true)
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_deleted), generalGetString(R.string.restart_the_app_to_create_a_new_chat_profile))
|
||||
|
||||
@@ -3,13 +3,15 @@ package chat.simplex.app.views.helpers
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import chat.simplex.app.R
|
||||
@@ -33,13 +35,14 @@ class AlertManager {
|
||||
text: String? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
|
||||
onDismissRequest = this::hideAlert,
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
buttons = buttons
|
||||
title = alertTitle(title),
|
||||
text = alertText(text),
|
||||
buttons = buttons,
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -51,22 +54,21 @@ class AlertManager {
|
||||
) {
|
||||
showAlert {
|
||||
Dialog(onDismissRequest = this::hideAlert) {
|
||||
Column(Modifier.background(MaterialTheme.colors.background, MaterialTheme.shapes.medium)) {
|
||||
Text(title,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING, bottom = if (text == null) DEFAULT_PADDING else DEFAULT_PADDING_HALF),
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
Column(
|
||||
Modifier
|
||||
.background(if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background, RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
.padding(bottom = DEFAULT_PADDING)
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
if (text != null) {
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
Text(
|
||||
text,
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING),
|
||||
fontSize = 14.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
|
||||
if (text != null) {
|
||||
Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = HighOrLowlight)
|
||||
}
|
||||
buttons()
|
||||
}
|
||||
}
|
||||
@@ -84,24 +86,28 @@ class AlertManager {
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
destructive: Boolean = false
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
title = alertTitle(title),
|
||||
text = alertText(text),
|
||||
buttons = {
|
||||
Row (
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(dismissText) }
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(dismissText) }
|
||||
}
|
||||
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -116,16 +122,15 @@ class AlertManager {
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
destructive: Boolean = false
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
title = alertTitle(title),
|
||||
text = alertText(text),
|
||||
buttons = {
|
||||
Column(
|
||||
Modifier.fillMaxWidth().padding(horizontal = 8.dp).padding(top = 16.dp, bottom = 2.dp),
|
||||
horizontalAlignment = Alignment.End
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
onDismiss?.invoke()
|
||||
@@ -134,9 +139,11 @@ class AlertManager {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
|
||||
}) { Text(confirmText, color = if (destructive) Color.Red else Color.Unspecified, textAlign = TextAlign.End) }
|
||||
}
|
||||
},
|
||||
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -145,18 +152,24 @@ class AlertManager {
|
||||
title: String, text: String? = null,
|
||||
confirmText: String = generalGetString(R.string.ok), onConfirm: (() -> Unit)? = null
|
||||
) {
|
||||
val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) }
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = this::hideAlert,
|
||||
title = { Text(title) },
|
||||
text = alertText,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText) }
|
||||
}
|
||||
title = alertTitle(title),
|
||||
text = alertText(text),
|
||||
buttons = {
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
TextButton(onClick = {
|
||||
onConfirm?.invoke()
|
||||
hideAlert()
|
||||
}) { Text(confirmText, color = Color.Unspecified) }
|
||||
}
|
||||
},
|
||||
backgroundColor = if (isInDarkTheme()) Color(0xff222222) else MaterialTheme.colors.background,
|
||||
shape = RoundedCornerShape(corner = CornerSize(25.dp))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -177,3 +190,30 @@ class AlertManager {
|
||||
val shared = AlertManager()
|
||||
}
|
||||
}
|
||||
|
||||
private fun alertTitle(title: String): (@Composable () -> Unit)? {
|
||||
return {
|
||||
Text(
|
||||
title,
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun alertText(text: String?): (@Composable () -> Unit)? {
|
||||
return if (text == null) {
|
||||
null
|
||||
} else {
|
||||
({
|
||||
Text(
|
||||
text,
|
||||
Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 16.sp,
|
||||
color = HighOrLowlight
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
+18
-9
@@ -1,21 +1,26 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.newchat.ActionButton
|
||||
|
||||
sealed class AttachmentOption {
|
||||
object TakePhoto: AttachmentOption()
|
||||
object PickImage: AttachmentOption()
|
||||
object PickFile: AttachmentOption()
|
||||
object CameraPhoto: AttachmentOption()
|
||||
object GalleryImage: AttachmentOption()
|
||||
object GalleryVideo: AttachmentOption()
|
||||
object File: AttachmentOption()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -37,16 +42,20 @@ fun ChooseAttachmentView(
|
||||
.padding(horizontal = 8.dp, vertical = 30.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
ActionButton(null, stringResource(R.string.use_camera_button), icon = Icons.Outlined.PhotoCamera) {
|
||||
attachmentOption.value = AttachmentOption.TakePhoto
|
||||
ActionButton(Modifier.fillMaxWidth(0.25f), null, stringResource(R.string.use_camera_button), icon = painterResource(R.drawable.ic_camera_enhance)) {
|
||||
attachmentOption.value = AttachmentOption.CameraPhoto
|
||||
hide()
|
||||
}
|
||||
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
|
||||
attachmentOption.value = AttachmentOption.PickImage
|
||||
ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(R.string.gallery_image_button), icon = painterResource(R.drawable.ic_add_photo)) {
|
||||
attachmentOption.value = AttachmentOption.GalleryImage
|
||||
hide()
|
||||
}
|
||||
ActionButton(null, stringResource(R.string.choose_file), icon = Icons.Outlined.InsertDriveFile) {
|
||||
attachmentOption.value = AttachmentOption.PickFile
|
||||
ActionButton(Modifier.fillMaxWidth(0.50f), null, stringResource(R.string.gallery_video_button), icon = painterResource(R.drawable.ic_smart_display)) {
|
||||
attachmentOption.value = AttachmentOption.GalleryVideo
|
||||
hide()
|
||||
}
|
||||
ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(R.string.choose_file), icon = painterResource(R.drawable.ic_note_add)) {
|
||||
attachmentOption.value = AttachmentOption.File
|
||||
hide()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -28,7 +30,7 @@ fun CloseSheetBar(close: () -> Unit, endButtons: @Composable RowScope.() -> Unit
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
NavigationButtonBack(close)
|
||||
NavigationButtonBack(onButtonClicked = close)
|
||||
Row {
|
||||
endButtons()
|
||||
}
|
||||
@@ -54,6 +56,19 @@ fun AppBarTitle(title: String, withPadding: Boolean = true) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColumnScope.AppBarTitleCentered(title: String, withPadding: Boolean = true) {
|
||||
Text(
|
||||
title,
|
||||
Modifier
|
||||
.padding(bottom = if (withPadding) DEFAULT_PADDING * 1.5f else 0.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.h1,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
|
||||
@@ -3,6 +3,7 @@ package chat.simplex.app.views.helpers
|
||||
import android.util.Log
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.model.AppPreferences
|
||||
import chat.simplex.app.model.SharedPreference
|
||||
import chat.simplex.app.views.usersettings.Cryptor
|
||||
import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
@@ -16,30 +17,36 @@ object DatabaseUtils {
|
||||
}
|
||||
|
||||
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
|
||||
private const val APP_PASSWORD_ALIAS: String = "appPassword"
|
||||
|
||||
val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase)
|
||||
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
|
||||
|
||||
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
|
||||
fun get(): String? {
|
||||
return cryptor.decryptData(
|
||||
passphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
initVector.get()?.toByteArrayFromBase64() ?: return null,
|
||||
alias,
|
||||
)
|
||||
}
|
||||
|
||||
fun set(key: String) {
|
||||
val data = cryptor.encryptText(key, alias)
|
||||
passphrase.set(data.first.toBase64String())
|
||||
initVector.set(data.second.toBase64String())
|
||||
}
|
||||
|
||||
fun remove() {
|
||||
cryptor.deleteKey(alias)
|
||||
passphrase.set(null)
|
||||
initVector.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasDatabase(rootDir: String): Boolean =
|
||||
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
|
||||
|
||||
fun getDatabaseKey(): String? {
|
||||
return cryptor.decryptData(
|
||||
appPreferences.encryptedDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
appPreferences.initializationVectorDBPassphrase.get()?.toByteArrayFromBase64() ?: return null,
|
||||
DATABASE_PASSWORD_ALIAS,
|
||||
)
|
||||
}
|
||||
|
||||
fun setDatabaseKey(key: String) {
|
||||
val data = cryptor.encryptText(key, DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(data.first.toBase64String())
|
||||
appPreferences.initializationVectorDBPassphrase.set(data.second.toBase64String())
|
||||
}
|
||||
|
||||
fun removeDatabaseKey() {
|
||||
cryptor.deleteKey(DATABASE_PASSWORD_ALIAS)
|
||||
appPreferences.encryptedDBPassphrase.set(null)
|
||||
appPreferences.initializationVectorDBPassphrase.set(null)
|
||||
}
|
||||
|
||||
fun useDatabaseKey(): String {
|
||||
Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}")
|
||||
var dbKey = ""
|
||||
@@ -47,10 +54,10 @@ object DatabaseUtils {
|
||||
if (useKeychain) {
|
||||
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
|
||||
dbKey = randomDatabasePassword()
|
||||
setDatabaseKey(dbKey)
|
||||
ksDatabasePassword.set(dbKey)
|
||||
appPreferences.initialRandomDBPassphrase.set(true)
|
||||
} else {
|
||||
dbKey = getDatabaseKey() ?: ""
|
||||
dbKey = ksDatabasePassword.get() ?: ""
|
||||
}
|
||||
}
|
||||
return dbKey
|
||||
@@ -66,8 +73,39 @@ object DatabaseUtils {
|
||||
@Serializable
|
||||
sealed class DBMigrationResult {
|
||||
@Serializable @SerialName("ok") object OK: DBMigrationResult()
|
||||
@Serializable @SerialName("invalidConfirmation") object InvalidConfirmation: DBMigrationResult()
|
||||
@Serializable @SerialName("errorNotADatabase") class ErrorNotADatabase(val dbFile: String): DBMigrationResult()
|
||||
@Serializable @SerialName("error") class Error(val dbFile: String, val migrationError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorMigration") class ErrorMigration(val dbFile: String, val migrationError: MigrationError): DBMigrationResult()
|
||||
@Serializable @SerialName("errorSQL") class ErrorSQL(val dbFile: String, val migrationSQLError: String): DBMigrationResult()
|
||||
@Serializable @SerialName("errorKeychain") object ErrorKeychain: DBMigrationResult()
|
||||
@Serializable @SerialName("unknown") class Unknown(val json: String): DBMigrationResult()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class MigrationConfirmation(val value: String) {
|
||||
YesUp("yesUp"),
|
||||
YesUpDown ("yesUpDown"),
|
||||
Error("error")
|
||||
}
|
||||
|
||||
fun defaultMigrationConfirmation(appPrefs: AppPreferences): MigrationConfirmation =
|
||||
if (appPrefs.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
|
||||
|
||||
@Serializable
|
||||
sealed class MigrationError {
|
||||
@Serializable @SerialName("upgrade") class Upgrade(val upMigrations: List<UpMigration>): MigrationError()
|
||||
@Serializable @SerialName("downgrade") class Downgrade(val downMigrations: List<String>): MigrationError()
|
||||
@Serializable @SerialName("migrationError") class Error(val mtrError: MTRError): MigrationError()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UpMigration(
|
||||
val upName: String,
|
||||
// val withDown: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class MTRError {
|
||||
@Serializable @SerialName("noDown") class NoDown(val dbMigrations: List<String>): MTRError()
|
||||
@Serializable @SerialName("different") class Different(val appMigration: String, val dbMigration: String): MTRError()
|
||||
}
|
||||
|
||||
+118
-2
@@ -3,10 +3,15 @@ package chat.simplex.app.views.helpers
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.foundation.text.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.TextFieldDefaults.indicatorLine
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.Error
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -14,13 +19,18 @@ import androidx.compose.ui.focus.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.views.database.PassphraseStrength
|
||||
import chat.simplex.app.views.database.validKey
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
@@ -110,3 +120,109 @@ fun DefaultBasicTextField(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun DefaultConfigurableTextField(
|
||||
state: MutableState<TextFieldValue>,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier,
|
||||
showPasswordStrength: Boolean = false,
|
||||
isValid: (String) -> Boolean,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
keyboardType: KeyboardType = KeyboardType.Text,
|
||||
dependsOn: State<Any?>? = null,
|
||||
) {
|
||||
var valid by remember { mutableStateOf(validKey(state.value.text)) }
|
||||
var showKey by remember { mutableStateOf(false) }
|
||||
val icon = if (valid) {
|
||||
if (showKey) Icons.Filled.VisibilityOff else Icons.Filled.Visibility
|
||||
} else Icons.Outlined.Error
|
||||
val iconColor = if (valid) {
|
||||
if (showPasswordStrength && state.value.text.isNotEmpty()) PassphraseStrength.check(state.value.text).color else HighOrLowlight
|
||||
} else Color.Red
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val keyboardOptions = KeyboardOptions(
|
||||
imeAction = if (keyboardActions.onNext != null) ImeAction.Next else ImeAction.Done,
|
||||
autoCorrect = keyboardType != KeyboardType.Password,
|
||||
keyboardType = keyboardType
|
||||
)
|
||||
val enabled = true
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
focusedIndicatorColor = Color.Unspecified,
|
||||
unfocusedIndicatorColor = Color.Unspecified,
|
||||
)
|
||||
val color = MaterialTheme.colors.onBackground
|
||||
val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize)
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.backgroundColor(enabled).value, shape)
|
||||
.indicatorLine(enabled, false, interactionSource, colors)
|
||||
.defaultMinSize(
|
||||
minWidth = TextFieldDefaults.MinWidth,
|
||||
minHeight = TextFieldDefaults.MinHeight
|
||||
),
|
||||
onValueChange = {
|
||||
state.value = it
|
||||
},
|
||||
cursorBrush = SolidColor(colors.cursorColor(false).value),
|
||||
visualTransformation = if (showKey || keyboardType != KeyboardType.Password)
|
||||
VisualTransformation.None
|
||||
else
|
||||
VisualTransformation { TransformedText(AnnotatedString(it.text.map { "*" }.joinToString(separator = "")), OffsetMapping.Identity) },
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
keyboard?.hide()
|
||||
keyboardActions.onDone?.invoke(this)
|
||||
}),
|
||||
singleLine = true,
|
||||
textStyle = TextStyle.Default.copy(
|
||||
color = color,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.TextFieldDecorationBox(
|
||||
value = state.value.text,
|
||||
innerTextField = innerTextField,
|
||||
placeholder = { Text(placeholder, color = HighOrLowlight) },
|
||||
singleLine = true,
|
||||
enabled = enabled,
|
||||
isError = !valid,
|
||||
trailingIcon = {
|
||||
if (keyboardType == KeyboardType.Password || !valid) {
|
||||
IconButton({ showKey = !showKey }) {
|
||||
Icon(icon, null, tint = iconColor)
|
||||
}
|
||||
}
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
contentPadding = TextFieldDefaults.textFieldWithLabelPadding(start = 0.dp, end = 0.dp),
|
||||
visualTransformation = VisualTransformation.None,
|
||||
colors = colors
|
||||
)
|
||||
}
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { state.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
valid = isValid(it.text)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
snapshotFlow { dependsOn?.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
valid = isValid(state.value.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun DefaultDropdownMenu(
|
||||
showMenu: MutableState<Boolean>,
|
||||
offset: DpOffset = DpOffset(0.dp, 0.dp),
|
||||
dropdownMenuItems: (@Composable () -> Unit)?
|
||||
) {
|
||||
MaterialTheme(
|
||||
colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background),
|
||||
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
) {
|
||||
DropdownMenu(
|
||||
expanded = showMenu.value,
|
||||
onDismissRequest = { showMenu.value = false },
|
||||
Modifier
|
||||
.widthIn(min = 250.dp)
|
||||
.padding(vertical = 4.dp),
|
||||
offset = offset,
|
||||
) {
|
||||
dropdownMenuItems?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu(
|
||||
expanded: MutableState<Boolean>,
|
||||
modifier: Modifier = Modifier,
|
||||
dropdownMenuItems: (@Composable () -> Unit)?
|
||||
) {
|
||||
MaterialTheme(
|
||||
colors = MaterialTheme.colors.copy(surface = if (isInDarkTheme()) Color(0xFF080808) else MaterialTheme.colors.background),
|
||||
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp)))
|
||||
) {
|
||||
ExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = 200.dp).then(modifier),
|
||||
expanded = expanded.value,
|
||||
onDismissRequest = {
|
||||
expanded.value = false
|
||||
}
|
||||
) {
|
||||
dropdownMenuItems?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,10 +45,10 @@ fun DefaultTopAppBar(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationButtonBack(onButtonClicked: () -> Unit) {
|
||||
IconButton(onButtonClicked) {
|
||||
fun NavigationButtonBack(onButtonClicked: (() -> Unit)?) {
|
||||
IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) {
|
||||
Icon(
|
||||
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = MaterialTheme.colors.primary
|
||||
Icons.Outlined.ArrowBackIos, stringResource(R.string.back), tint = if (onButtonClicked != null) MaterialTheme.colors.primary else HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
sealed class SharedContent {
|
||||
data class Text(val text: String): SharedContent()
|
||||
data class Images(val text: String, val uris: List<Uri>): SharedContent()
|
||||
data class Media(val text: String, val uris: List<Uri>): SharedContent()
|
||||
data class File(val text: String, val uri: Uri): SharedContent()
|
||||
}
|
||||
|
||||
@@ -48,4 +48,5 @@ object UriSerializer : KSerializer<Uri> {
|
||||
sealed class UploadContent {
|
||||
@Serializable data class SimpleImage(val uri: Uri): UploadContent()
|
||||
@Serializable data class AnimatedImage(val uri: Uri): UploadContent()
|
||||
@Serializable data class Video(val uri: Uri, val duration: Int): UploadContent()
|
||||
}
|
||||
|
||||
+10
-12
@@ -15,8 +15,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun <T> ExposedDropDownSettingRow(
|
||||
@@ -33,7 +32,7 @@ fun <T> ExposedDropDownSettingRow(
|
||||
Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
@@ -46,9 +45,9 @@ fun <T> ExposedDropDownSettingRow(
|
||||
Text(title, Modifier.weight(1f), color = if (enabled.value) Color.Unspecified else HighOrLowlight)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded && enabled.value
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
@@ -66,29 +65,28 @@ fun <T> ExposedDropDownSettingRow(
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
Icon(
|
||||
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
generalGetString(R.string.icon_descr_more_button),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
DefaultExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = 200.dp),
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
onSelected(selectionOption.first)
|
||||
expanded = false
|
||||
}
|
||||
expanded.value = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||
) {
|
||||
Text(
|
||||
selectionOption.second + (if (label != null) " $label" else ""),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = if (isInDarkTheme()) MenuTextColorDark else Color.Black,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Collections
|
||||
import androidx.compose.material.icons.outlined.PhotoCamera
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
@@ -174,7 +173,18 @@ fun rememberGetContentLauncher(cb: (Uri?) -> Unit): ManagedActivityResultLaunche
|
||||
|
||||
@Composable
|
||||
fun rememberGetMultipleContentsLauncher(cb: (List<Uri>) -> Unit): ManagedActivityResultLauncher<String, List<Uri>> =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetMultipleContents(), cb)
|
||||
rememberLauncherForActivityResult(contract = GetMultipleContentsAndMimeTypes(), cb)
|
||||
|
||||
class GetMultipleContentsAndMimeTypes: ActivityResultContracts.GetMultipleContents() {
|
||||
override fun createIntent(context: Context, input: String): Intent {
|
||||
val mimeTypes = input.split(";")
|
||||
return super.createIntent(context, mimeTypes[0]).apply {
|
||||
if (mimeTypes.isNotEmpty()) {
|
||||
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() {
|
||||
try {
|
||||
@@ -248,7 +258,7 @@ fun GetImageBottomSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Collections) {
|
||||
ActionButton(null, stringResource(R.string.from_gallery_button), icon = Icons.Outlined.Image) {
|
||||
try {
|
||||
galleryLauncher.launch(0)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
|
||||
+56
-16
@@ -1,36 +1,66 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.widget.Toast
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricManager.Authenticators.*
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.localauth.LocalAuthView
|
||||
import chat.simplex.app.views.usersettings.LAMode
|
||||
|
||||
sealed class LAResult {
|
||||
object Success: LAResult()
|
||||
class Error(val errString: CharSequence): LAResult()
|
||||
object Failed: LAResult()
|
||||
object Unavailable: LAResult()
|
||||
class Failed(val errString: CharSequence? = null): LAResult()
|
||||
class Unavailable(val errString: CharSequence? = null): LAResult()
|
||||
}
|
||||
|
||||
data class LocalAuthRequest (
|
||||
val title: String?,
|
||||
val reason: String,
|
||||
val password: String,
|
||||
val completed: (LAResult) -> Unit
|
||||
) {
|
||||
companion object {
|
||||
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { }
|
||||
}
|
||||
}
|
||||
|
||||
fun authenticate(
|
||||
promptTitle: String,
|
||||
promptSubtitle: String,
|
||||
activity: FragmentActivity,
|
||||
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
|
||||
completed: (LAResult) -> Unit
|
||||
) {
|
||||
when {
|
||||
SDK_INT in 28..29 ->
|
||||
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
SDK_INT > 29 ->
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
|
||||
else ->
|
||||
completed(LAResult.Unavailable)
|
||||
when (usingLAMode) {
|
||||
LAMode.SYSTEM -> when {
|
||||
SDK_INT in 28..29 ->
|
||||
// KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL)
|
||||
SDK_INT > 29 ->
|
||||
authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password)))
|
||||
ModalManager.shared.showCustomModal(animated = false) { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) {
|
||||
close()
|
||||
completed(it)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +96,7 @@ private fun authenticateWithBiometricManager(
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
super.onAuthenticationFailed()
|
||||
completed(LAResult.Failed)
|
||||
completed(LAResult.Failed())
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -78,9 +108,7 @@ private fun authenticateWithBiometricManager(
|
||||
.build()
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
else -> {
|
||||
completed(LAResult.Unavailable)
|
||||
}
|
||||
else -> completed(LAResult.Unavailable())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +117,18 @@ fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.auth_you_will_be_required_to_authenticate_when_you_start_or_resume)
|
||||
)
|
||||
|
||||
fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.lock_not_enabled),
|
||||
generalGetString(R.string.you_can_turn_on_lock)
|
||||
)
|
||||
|
||||
fun laFailedAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.la_auth_failed),
|
||||
text = generalGetString(R.string.la_could_not_be_verified)
|
||||
)
|
||||
}
|
||||
|
||||
fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg(
|
||||
generalGetString(R.string.auth_unavailable),
|
||||
generalGetString(R.string.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled)
|
||||
|
||||
@@ -39,6 +39,7 @@ class RecorderNative(private val recordedBytesLimit: Long): Recorder {
|
||||
}
|
||||
|
||||
override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String {
|
||||
VideoPlayer.stopAll()
|
||||
AudioPlayer.stop()
|
||||
val rec: MediaRecorder
|
||||
recorder = initRecorder().also { rec = it }
|
||||
@@ -152,6 +153,7 @@ object AudioPlayer {
|
||||
return null
|
||||
}
|
||||
|
||||
VideoPlayer.stopAll()
|
||||
RecorderNative.stopRecording?.invoke()
|
||||
val current = currentlyPlaying.value
|
||||
if (current == null || current.first != filePath) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -28,6 +30,21 @@ fun SimpleButton(text: String, icon: ImageVector,
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonDecorated(text: String, icon: ImageVector,
|
||||
color: Color = MaterialTheme.colors.primary,
|
||||
textDecoration: TextDecoration = TextDecoration.Underline,
|
||||
fontWeight: FontWeight = FontWeight.Normal,
|
||||
click: () -> Unit) {
|
||||
SimpleButtonFrame(click) {
|
||||
Icon(
|
||||
icon, text, tint = color,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
Text(text, style = MaterialTheme.typography.caption, fontWeight = fontWeight, color = color, textDecoration = textDecoration)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButton(
|
||||
text: String, icon: ImageVector,
|
||||
@@ -61,9 +78,9 @@ fun SimpleButtonIconEnded(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SimpleButtonFrame(click: () -> Unit, disabled: Boolean = false, content: @Composable () -> Unit) {
|
||||
fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled: Boolean = false, content: @Composable () -> Unit) {
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
val modifier = if (disabled) Modifier else Modifier.clickable { click() }
|
||||
val modifier = if (disabled) modifier else modifier.clickable { click() }
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier.padding(8.dp)
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.content.res.Resources
|
||||
import android.graphics.*
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.provider.OpenableColumns
|
||||
@@ -237,12 +238,18 @@ const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
|
||||
const val MAX_VOICE_SIZE_FOR_SENDING: Long = 94680 // 6 chunks * 15780 bytes per chunk
|
||||
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 43_000
|
||||
|
||||
const val MAX_FILE_SIZE: Long = 8000000
|
||||
const val MAX_FILE_SIZE_SMP: Long = 8000000
|
||||
|
||||
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824
|
||||
|
||||
fun getFilesDirectory(context: Context): String {
|
||||
return context.filesDir.toString()
|
||||
}
|
||||
|
||||
fun getTempFilesDirectory(context: Context): String {
|
||||
return "${getFilesDirectory(context)}/temp_files"
|
||||
}
|
||||
|
||||
fun getAppFilesDirectory(context: Context): String {
|
||||
return "${getFilesDirectory(context)}/app_files"
|
||||
}
|
||||
@@ -489,9 +496,9 @@ fun formatBytes(bytes: Long): String {
|
||||
return "0 bytes"
|
||||
}
|
||||
val bytesDouble = bytes.toDouble()
|
||||
val k = 1000.toDouble()
|
||||
val k = 1024.toDouble()
|
||||
val units = arrayOf("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
||||
val i = kotlin.math.floor(log2(bytesDouble) / log2(k))
|
||||
val i = floor(log2(bytesDouble) / log2(k))
|
||||
val size = bytesDouble / k.pow(i)
|
||||
val unit = units[i.toInt()]
|
||||
|
||||
@@ -536,6 +543,26 @@ fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in
|
||||
return fileCount to bytes
|
||||
}
|
||||
|
||||
fun getMaxFileSize(fileProtocol: FileProtocol): Long {
|
||||
return when (fileProtocol) {
|
||||
FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP
|
||||
FileProtocol.SMP -> MAX_FILE_SIZE_SMP
|
||||
}
|
||||
}
|
||||
|
||||
fun getBitmapFromVideo(uri: Uri, timestamp: Long? = null, random: Boolean = true): VideoPlayer.PreviewAndDuration {
|
||||
val mmr = MediaMetadataRetriever()
|
||||
mmr.setDataSource(SimplexApp.context, uri)
|
||||
val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong()
|
||||
val image = when {
|
||||
timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST)
|
||||
random -> mmr.frameAtTime
|
||||
else -> mmr.getFrameAtIndex(0)
|
||||
}
|
||||
mmr.release()
|
||||
return VideoPlayer.PreviewAndDuration(image, durationMs, timestamp ?: 0)
|
||||
}
|
||||
|
||||
fun Color.darker(factor: Float = 0.1f): Color =
|
||||
Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha)
|
||||
|
||||
@@ -593,3 +620,40 @@ fun UriHandler.openUriCatching(uri: String) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
fun IntSize.Companion.Saver(): Saver<IntSize, *> = Saver(
|
||||
save = { it.width to it.height },
|
||||
restore = { IntSize(it.first, it.second) }
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
always()
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
val orientation = activity.resources.configuration.orientation
|
||||
onDispose {
|
||||
whenDispose()
|
||||
if (orientation == activity.resources.configuration.orientation) {
|
||||
whenGone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
always()
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
val orientation = activity.resources.configuration.orientation
|
||||
onDispose {
|
||||
whenDispose()
|
||||
if (orientation != activity.resources.configuration.orientation) {
|
||||
whenRotate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package chat.simplex.app.views.helpers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.media.session.PlaybackState
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import chat.simplex.app.*
|
||||
import chat.simplex.app.R
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.C.*
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultDataSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
|
||||
class VideoPlayer private constructor(
|
||||
private val uri: Uri,
|
||||
private val gallery: Boolean,
|
||||
private val defaultPreview: Bitmap,
|
||||
defaultDuration: Long,
|
||||
soundEnabled: Boolean,
|
||||
context: Context
|
||||
) {
|
||||
companion object {
|
||||
private val players: MutableMap<Pair<Uri, Boolean>, VideoPlayer> = mutableMapOf()
|
||||
private val previewsAndDurations: MutableMap<Uri, PreviewAndDuration> = mutableMapOf()
|
||||
|
||||
fun getOrCreate(
|
||||
uri: Uri,
|
||||
gallery: Boolean,
|
||||
defaultPreview: Bitmap,
|
||||
defaultDuration: Long,
|
||||
soundEnabled: Boolean,
|
||||
context: Context
|
||||
): VideoPlayer =
|
||||
players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled, context) }
|
||||
|
||||
fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean =
|
||||
player(fileName, gallery)?.enableSound(enable) == true
|
||||
|
||||
private fun player(fileName: String?, gallery: Boolean): VideoPlayer? {
|
||||
fileName ?: return null
|
||||
return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery }
|
||||
}
|
||||
|
||||
fun release(uri: Uri, gallery: Boolean, remove: Boolean) =
|
||||
player(uri.path, gallery)?.release(remove)
|
||||
|
||||
fun stopAll() {
|
||||
players.values.forEach { it.stop() }
|
||||
}
|
||||
|
||||
fun releaseAll() {
|
||||
players.values.forEach { it.release(false) }
|
||||
players.clear()
|
||||
previewsAndDurations.clear()
|
||||
}
|
||||
}
|
||||
|
||||
data class PreviewAndDuration(val preview: Bitmap?, val duration: Long?, val timestamp: Long)
|
||||
|
||||
private val currentVolume: Float
|
||||
val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled)
|
||||
val brokenVideo: MutableState<Boolean> = mutableStateOf(false)
|
||||
val videoPlaying: MutableState<Boolean> = mutableStateOf(false)
|
||||
val progress: MutableState<Long> = mutableStateOf(0L)
|
||||
val duration: MutableState<Long> = mutableStateOf(defaultDuration)
|
||||
val preview: MutableState<Bitmap> = mutableStateOf(defaultPreview)
|
||||
|
||||
init {
|
||||
setPreviewAndDuration()
|
||||
}
|
||||
|
||||
val player = ExoPlayer.Builder(context,
|
||||
DefaultRenderersFactory(context))
|
||||
/*.setLoadControl(DefaultLoadControl.Builder()
|
||||
.setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed
|
||||
.createDefaultLoadControl())*/
|
||||
.setSeekBackIncrementMs(10_000)
|
||||
.setSeekForwardIncrementMs(10_000)
|
||||
.build()
|
||||
.apply {
|
||||
// Repeat the same track endlessly
|
||||
repeatMode = 1
|
||||
currentVolume = volume
|
||||
if (!soundEnabled) {
|
||||
volume = 0f
|
||||
}
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setContentType(CONTENT_TYPE_MUSIC)
|
||||
.setUsage(USAGE_MEDIA)
|
||||
.build(),
|
||||
true // disallow to play multiple instances simultaneously
|
||||
)
|
||||
}
|
||||
|
||||
private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null)
|
||||
private var progressJob: Job? = null
|
||||
|
||||
enum class TrackState {
|
||||
PLAYING, PAUSED, STOPPED
|
||||
}
|
||||
|
||||
private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean {
|
||||
val filepath = getAppFilePath(SimplexApp.context, uri)
|
||||
if (filepath == null || !File(filepath).exists()) {
|
||||
Log.e(TAG, "No such file: $uri")
|
||||
brokenVideo.value = true
|
||||
return false
|
||||
}
|
||||
|
||||
if (soundEnabled.value) {
|
||||
RecorderNative.stopRecording?.invoke()
|
||||
}
|
||||
AudioPlayer.stop()
|
||||
stopAll()
|
||||
if (listener.value == null) {
|
||||
runCatching {
|
||||
val dataSourceFactory = DefaultDataSource.Factory(SimplexApp.context, DefaultHttpDataSource.Factory())
|
||||
val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri))
|
||||
player.setMediaSource(source, seek ?: 0L)
|
||||
}.onFailure {
|
||||
Log.e(TAG, it.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), it.message)
|
||||
brokenVideo.value = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (player.playbackState == PlaybackState.STATE_NONE || player.playbackState == PlaybackState.STATE_STOPPED) {
|
||||
runCatching { player.prepare() }.onFailure {
|
||||
// Can happen when video file is broken
|
||||
Log.e(TAG, it.stackTraceToString())
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.unknown_error), it.message)
|
||||
brokenVideo.value = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (seek != null) player.seekTo(seek)
|
||||
player.play()
|
||||
listener.value = onProgressUpdate
|
||||
// Player can only be accessed in one specific thread
|
||||
progressJob = CoroutineScope(Dispatchers.Main).launch {
|
||||
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||
while (isActive && player.playbackState != Player.STATE_IDLE && player.playWhenReady) {
|
||||
// Even when current position is equal to duration, the player has isPlaying == true for some time,
|
||||
// so help to make the playback stopped in UI immediately
|
||||
if (player.currentPosition == player.duration) {
|
||||
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||
break
|
||||
}
|
||||
delay(50)
|
||||
onProgressUpdate(player.currentPosition, TrackState.PLAYING)
|
||||
}
|
||||
/*
|
||||
* Since coroutine is still NOT canceled, means player ended (no stop/no pause). But in some cases
|
||||
* the player can show position != duration even if they actually equal.
|
||||
* Let's say to a listener that the position == duration in case of coroutine finished without cancel
|
||||
* */
|
||||
if (isActive) {
|
||||
onProgressUpdate(player.duration, TrackState.PAUSED)
|
||||
}
|
||||
onProgressUpdate(null, TrackState.PAUSED)
|
||||
}
|
||||
player.addListener(object: Player.Listener{
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
super.onIsPlayingChanged(isPlaying)
|
||||
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
|
||||
// videoPlaying.value = isPlaying
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
player.stop()
|
||||
stopListener()
|
||||
}
|
||||
|
||||
private fun stopListener() {
|
||||
val afterCoroutineCancel: CompletionHandler = {
|
||||
// Notify prev video listener about stop
|
||||
listener.value?.invoke(null, TrackState.STOPPED)
|
||||
}
|
||||
/** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be:
|
||||
* [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.STOPPED] (in this order)
|
||||
* */
|
||||
if (progressJob != null) {
|
||||
progressJob?.invokeOnCompletion(afterCoroutineCancel)
|
||||
} else {
|
||||
afterCoroutineCancel(null)
|
||||
}
|
||||
progressJob?.cancel()
|
||||
progressJob = null
|
||||
}
|
||||
|
||||
fun play(resetOnEnd: Boolean) {
|
||||
if (progress.value == duration.value) {
|
||||
progress.value = 0
|
||||
}
|
||||
videoPlaying.value = start(progress.value) { pro, _ ->
|
||||
if (pro != null) {
|
||||
progress.value = pro
|
||||
}
|
||||
if (pro == null || pro == duration.value) {
|
||||
videoPlaying.value = false
|
||||
if (pro == duration.value) {
|
||||
progress.value = if (resetOnEnd) 0 else duration.value
|
||||
}/* else if (state == TrackState.STOPPED) {
|
||||
progress.value = 0 //
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enableSound(enable: Boolean): Boolean {
|
||||
if (soundEnabled.value == enable) return false
|
||||
soundEnabled.value = enable
|
||||
player.volume = if (enable) currentVolume else 0f
|
||||
return true
|
||||
}
|
||||
|
||||
fun release(remove: Boolean) {
|
||||
player.release()
|
||||
if (remove) {
|
||||
players.remove(uri to gallery)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setPreviewAndDuration() {
|
||||
// It freezes main thread, doing it in IO thread
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val previewAndDuration = previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) }
|
||||
withContext(Dispatchers.Main) {
|
||||
preview.value = previewAndDuration.preview ?: defaultPreview
|
||||
duration.value = (previewAndDuration.duration ?: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
|
||||
val passcode = rememberSaveable { mutableStateOf("") }
|
||||
PasscodeView(passcode, authRequest.title ?: stringResource(R.string.la_enter_app_passcode), authRequest.reason, stringResource(R.string.submit_passcode),
|
||||
submit = {
|
||||
val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(R.string.incorrect_passcode))
|
||||
authRequest.completed(r)
|
||||
},
|
||||
cancel = {
|
||||
authRequest.completed(LAResult.Error(generalGetString(R.string.authentication_cancelled)))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.ui.theme.DEFAULT_PADDING
|
||||
import chat.simplex.app.ui.theme.SimpleButton
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun PasscodeView(
|
||||
passcode: MutableState<String>,
|
||||
title: String,
|
||||
reason: String? = null,
|
||||
submitLabel: String,
|
||||
submitEnabled: ((String) -> Boolean)? = null,
|
||||
submit: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
) {
|
||||
@Composable
|
||||
fun VerticalLayout() {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(title, style = MaterialTheme.typography.h1)
|
||||
if (reason != null) {
|
||||
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
}
|
||||
PasscodeEntry(passcode, true)
|
||||
Row {
|
||||
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
|
||||
Spacer(Modifier.size(20.dp))
|
||||
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HorizontalLayout() {
|
||||
Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) {
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(title, style = MaterialTheme.typography.h1)
|
||||
if (reason != null) {
|
||||
Text(reason, Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
}
|
||||
PasscodeEntry(passcode, false)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
// Just to fill space to correctly calculate the height
|
||||
Column {
|
||||
Text("", style = MaterialTheme.typography.h1)
|
||||
if (reason != null) {
|
||||
Text("", Modifier.padding(top = 5.dp), style = MaterialTheme.typography.subtitle1)
|
||||
}
|
||||
PasscodeView(remember { mutableStateOf("") })
|
||||
}
|
||||
BoxWithConstraints {
|
||||
val s = minOf(maxWidth, maxHeight) / 3.5f
|
||||
Column(
|
||||
Modifier.padding(start = 30.dp).height(s * 3),
|
||||
verticalArrangement = Arrangement.SpaceEvenly
|
||||
) {
|
||||
SimpleButton(generalGetString(R.string.cancel_verb), icon = Icons.Default.Close, click = cancel)
|
||||
SimpleButton(submitLabel, icon = Icons.Default.Done, disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
VerticalLayout()
|
||||
} else {
|
||||
HorizontalLayout()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.outlined.Backspace
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
|
||||
@Composable
|
||||
fun PasscodeEntry(
|
||||
password: MutableState<String>,
|
||||
vertical: Boolean,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
PasscodeView(password)
|
||||
BoxWithConstraints {
|
||||
if (vertical) {
|
||||
VerticalPasswordGrid(password)
|
||||
} else {
|
||||
HorizontalPasswordGrid(password)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PasscodeView(password: MutableState<String>) {
|
||||
var showPasscode by rememberSaveable { mutableStateOf(false) }
|
||||
Text(
|
||||
if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) },
|
||||
Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode },
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState<String>) {
|
||||
val s = minOf(maxWidth, maxHeight) / 4 - 1.dp
|
||||
Column(Modifier.width(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 1, 2, 3, password)
|
||||
Divider()
|
||||
DigitsRow(s, 4, 5, 6, password)
|
||||
Divider()
|
||||
DigitsRow(s, 7, 8, 9, password)
|
||||
Divider()
|
||||
Row(Modifier.requiredHeight(s)) {
|
||||
PasswordEdit(s, Icons.Default.Close) {
|
||||
password.value = ""
|
||||
}
|
||||
VerticalDivider()
|
||||
PasswordDigit(s, 0, password)
|
||||
VerticalDivider()
|
||||
PasswordEdit(s, Icons.Outlined.Backspace) {
|
||||
password.value = password.value.dropLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BoxWithConstraintsScope.HorizontalPasswordGrid(password: MutableState<String>) {
|
||||
val s = minOf(maxWidth, maxHeight) / 3.5f - 1.dp
|
||||
Column(Modifier.width(IntrinsicSize.Min)) {
|
||||
Row(Modifier.height(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 1, 2, 3, password);
|
||||
VerticalDivider()
|
||||
PasswordEdit(s, Icons.Default.Close) {
|
||||
password.value = ""
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Row(Modifier.height(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 4, 5, 6, password)
|
||||
VerticalDivider()
|
||||
PasswordDigit(s, 0, password)
|
||||
}
|
||||
Divider()
|
||||
Row(Modifier.height(IntrinsicSize.Min)) {
|
||||
DigitsRow(s, 7, 8, 9, password)
|
||||
VerticalDivider()
|
||||
PasswordEdit(s, Icons.Outlined.Backspace) {
|
||||
password.value = password.value.dropLast(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun splitPassword(showPassword: Boolean, password: String): String {
|
||||
val n = if (password.length < 8) 8 else 4
|
||||
return password.mapIndexed { index, c -> (if (showPassword) c.toString() else "●") + (if ((index + 1) % n == 0) " " else "") }.joinToString("")
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DigitsRow(size: Dp, d1: Int, d2: Int, d3: Int, password: MutableState<String>) {
|
||||
Row(Modifier.height(size)) {
|
||||
PasswordDigit(size, d1, password)
|
||||
VerticalDivider()
|
||||
PasswordDigit(size, d2, password)
|
||||
VerticalDivider()
|
||||
PasswordDigit(size, d3, password)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordDigit(size: Dp, d: Int, password: MutableState<String>) {
|
||||
val s = d.toString()
|
||||
return PasswordButton(size, action = {
|
||||
if (password.value.length < 16) {
|
||||
password.value += s
|
||||
}
|
||||
}) {
|
||||
Text(
|
||||
s,
|
||||
style = TextStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 30.sp,
|
||||
letterSpacing = (-0.5).sp
|
||||
),
|
||||
color = HighOrLowlight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordEdit(size: Dp, image: ImageVector, action: () -> Unit) {
|
||||
PasswordButton(size, action) {
|
||||
Icon(image, null, tint = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordButton(size: Dp, action: () -> Unit, content: @Composable BoxScope.() -> Unit) {
|
||||
return Box(
|
||||
Modifier.size(size)
|
||||
.background(MaterialTheme.colors.background, RoundedCornerShape(50))
|
||||
.clickable { action() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VerticalDivider(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = MaterialTheme.colors.onSurface.copy(alpha = DividerAlpha),
|
||||
thickness: Dp = 1.dp,
|
||||
startIndent: Dp = 0.dp
|
||||
) {
|
||||
val indentMod = if (startIndent.value != 0f) {
|
||||
Modifier.padding(top = startIndent)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
val targetThickness = if (thickness == Dp.Hairline) {
|
||||
(1f / LocalDensity.current.density).dp
|
||||
} else {
|
||||
thickness
|
||||
}
|
||||
Box(
|
||||
modifier.then(indentMod)
|
||||
.fillMaxHeight()
|
||||
.width(targetThickness)
|
||||
.background(color = color)
|
||||
)
|
||||
}
|
||||
|
||||
private const val DividerAlpha = 0.12f
|
||||
@@ -0,0 +1,48 @@
|
||||
package chat.simplex.app.views.localauth
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.helpers.generalGetString
|
||||
|
||||
@Composable
|
||||
fun SetAppPasscodeView(
|
||||
submit: () -> Unit,
|
||||
cancel: () -> Unit,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val passcode = rememberSaveable { mutableStateOf("") }
|
||||
var enteredPassword by rememberSaveable { mutableStateOf("") }
|
||||
var confirming by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
fun SetPasswordView(title: String, submitLabel: String, submitEnabled: (((String) -> Boolean))? = null, submit: () -> Unit) {
|
||||
PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) {
|
||||
close()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
if (confirming) {
|
||||
SetPasswordView(
|
||||
generalGetString(R.string.confirm_passcode),
|
||||
generalGetString(R.string.confirm_verb),
|
||||
submitEnabled = { pwd -> pwd == enteredPassword }
|
||||
) {
|
||||
if (passcode.value == enteredPassword) {
|
||||
ksAppPassword.set(passcode.value)
|
||||
enteredPassword = ""
|
||||
passcode.value = ""
|
||||
close()
|
||||
submit()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) {
|
||||
enteredPassword = passcode.value
|
||||
passcode.value = ""
|
||||
confirming = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,13 +88,14 @@ fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String) {
|
||||
fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String, centered: Boolean = false) {
|
||||
if (chatModelIncognito) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
if (supportedIncognito) Icons.Filled.TheaterComedy else Icons.Outlined.Info,
|
||||
@@ -102,14 +103,15 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean
|
||||
tint = if (supportedIncognito) Indigo else WarningOrange,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
)
|
||||
Text(onText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
Text(onText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
} else {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.Info,
|
||||
@@ -117,7 +119,7 @@ fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean
|
||||
tint = HighOrLowlight,
|
||||
modifier = Modifier.padding(end = 10.dp).size(20.dp)
|
||||
)
|
||||
Text(offText, textAlign = TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
Text(offText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package chat.simplex.app.views.newchat
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -15,6 +14,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -26,6 +27,7 @@ import chat.simplex.app.views.chat.group.AddGroupMembersView
|
||||
import chat.simplex.app.views.chatlist.setGroupMembers
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.isValidDisplayName
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
import chat.simplex.app.views.usersettings.DeleteImageButton
|
||||
import chat.simplex.app.views.usersettings.EditImageButton
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
@@ -36,7 +38,6 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
AddGroupLayout(
|
||||
chatModel.incognito.value,
|
||||
createGroup = { groupProfile ->
|
||||
withApi {
|
||||
val groupInfo = chatModel.controller.apiNewGroup(groupProfile)
|
||||
@@ -57,11 +58,11 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
|
||||
fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) {
|
||||
val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden)
|
||||
val scope = rememberCoroutineScope()
|
||||
val displayName = remember { mutableStateOf("") }
|
||||
val fullName = remember { mutableStateOf("") }
|
||||
val displayName = rememberSaveable { mutableStateOf("") }
|
||||
val fullName = rememberSaveable { mutableStateOf("") }
|
||||
val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) }
|
||||
val profileImage = rememberSaveable { mutableStateOf<String?>(null) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
@@ -88,14 +89,8 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.create_secret_group_title), false)
|
||||
Text(stringResource(R.string.group_is_decentralized))
|
||||
InfoAboutIncognito(
|
||||
chatModelIncognito,
|
||||
false,
|
||||
generalGetString(R.string.group_unsupported_incognito_main_profile_sent),
|
||||
generalGetString(R.string.group_main_profile_sent)
|
||||
)
|
||||
AppBarTitleCentered(stringResource(R.string.create_secret_group_title))
|
||||
ReadableText(R.string.group_is_decentralized, TextAlign.Center)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -104,7 +99,7 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(size = 192.dp, image = profileImage.value)
|
||||
ProfileImage(108.dp, image = profileImage.value)
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
@@ -112,24 +107,28 @@ fun AddGroupLayout(chatModelIncognito: Boolean, createGroup: (GroupProfile) -> U
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.group_display_name_field),
|
||||
Modifier.padding(bottom = 3.dp)
|
||||
)
|
||||
ProfileNameField(displayName, focusRequester)
|
||||
val errorText = if (!isValidDisplayName(displayName.value)) stringResource(R.string.display_name_cannot_contain_whitespace) else ""
|
||||
Text(
|
||||
errorText,
|
||||
fontSize = 15.sp,
|
||||
color = MaterialTheme.colors.error
|
||||
)
|
||||
Spacer(Modifier.height(3.dp))
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(R.string.group_display_name_field),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
Text(
|
||||
stringResource(R.string.no_spaces),
|
||||
fontSize = 16.sp,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Text(
|
||||
stringResource(R.string.group_full_name_field),
|
||||
Modifier.padding(bottom = 5.dp)
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
|
||||
ProfileNameField(fullName, "")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
if (enabled) {
|
||||
@@ -163,7 +162,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
|
||||
) {
|
||||
Surface(shape = RoundedCornerShape(20.dp)) {
|
||||
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color)
|
||||
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color, fontWeight = FontWeight.Bold)
|
||||
Icon(Icons.Outlined.ArrowForwardIos, stringResource(R.string.create_profile_button), tint = color)
|
||||
}
|
||||
}
|
||||
@@ -175,7 +174,6 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
|
||||
fun PreviewAddGroupLayout() {
|
||||
SimpleXTheme {
|
||||
AddGroupLayout(
|
||||
chatModelIncognito = false,
|
||||
createGroup = {},
|
||||
close = {}
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.*
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -211,6 +212,52 @@ fun ActionButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(
|
||||
modifier: Modifier,
|
||||
text: String?,
|
||||
comment: String?,
|
||||
icon: Painter,
|
||||
tint: Color = MaterialTheme.colors.primary,
|
||||
disabled: Boolean = false,
|
||||
click: () -> Unit = {}
|
||||
) {
|
||||
Surface(modifier, shape = RoundedCornerShape(18.dp)) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = click)
|
||||
.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val tint = if (disabled) HighOrLowlight else tint
|
||||
Icon(
|
||||
icon, text,
|
||||
tint = tint,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.padding(bottom = 8.dp)
|
||||
)
|
||||
if (text != null) {
|
||||
Text(
|
||||
text,
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = tint,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
}
|
||||
if (comment != null) {
|
||||
Text(
|
||||
comment,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.body2
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewNewChatSheet() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -55,8 +56,13 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadableText(@StringRes stringResId: Int) {
|
||||
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(bottom = 12.dp), lineHeight = 22.sp)
|
||||
fun ReadableText(@StringRes stringResId: Int, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
|
||||
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReadableText(text: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
|
||||
Text(text, modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@@ -70,4 +76,4 @@ fun PreviewHowItWorks() {
|
||||
SimpleXTheme {
|
||||
HowItWorks(user = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-18
@@ -12,6 +12,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
@@ -23,22 +25,25 @@ import chat.simplex.app.views.usersettings.changeNotificationsMode
|
||||
|
||||
@Composable
|
||||
fun SetNotificationsMode(m: ChatModel) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(20.dp)
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.onboarding_notifications_mode_title), false)
|
||||
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
|
||||
Text(stringResource(R.string.onboarding_notifications_mode_subtitle))
|
||||
Spacer(Modifier.padding(DEFAULT_PADDING_HALF))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 14.dp)
|
||||
) {
|
||||
//CloseSheetBar(null)
|
||||
AppBarTitleCentered(stringResource(R.string.onboarding_notifications_mode_title))
|
||||
val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) }
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
|
||||
Text(stringResource(R.string.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING * 2f))
|
||||
NotificationButton(currentMode, NotificationsMode.OFF, R.string.onboarding_notifications_mode_off, R.string.onboarding_notifications_mode_off_desc)
|
||||
NotificationButton(currentMode, NotificationsMode.PERIODIC, R.string.onboarding_notifications_mode_periodic, R.string.onboarding_notifications_mode_periodic_desc)
|
||||
NotificationButton(currentMode, NotificationsMode.SERVICE, R.string.onboarding_notifications_mode_service, R.string.onboarding_notifications_mode_service_desc)
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage) {
|
||||
}
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = 16.dp), contentAlignment = Alignment.Center) {
|
||||
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
|
||||
changeNotificationsMode(currentMode.value, m)
|
||||
}
|
||||
}
|
||||
@@ -51,18 +56,24 @@ private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mod
|
||||
TextButton(
|
||||
onClick = { currentMode.value = mode },
|
||||
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight.copy(alpha = 0.5f)),
|
||||
shape = RoundedCornerShape(15.dp),
|
||||
shape = RoundedCornerShape(35.dp),
|
||||
) {
|
||||
Column(Modifier.padding(bottom = 6.dp).padding(horizontal = 8.dp)) {
|
||||
Column(Modifier.padding(horizontal = 14.dp).padding(top = 4.dp, bottom = 8.dp)) {
|
||||
Text(
|
||||
stringResource(title),
|
||||
style = MaterialTheme.typography.h2,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (currentMode.value == mode) MaterialTheme.colors.primary else HighOrLowlight,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(annotatedStringResource(description),
|
||||
Modifier.align(Alignment.CenterHorizontally),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
lineHeight = 24.sp,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Text(annotatedStringResource(description), color = MaterialTheme.colors.onBackground, lineHeight = 24.sp)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ArrowForwardIos
|
||||
@@ -44,13 +45,13 @@ fun SimpleXInfoLayout(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
.padding(start = DEFAULT_PADDING * 1.5f, end = DEFAULT_PADDING * 1.5f, top = DEFAULT_PADDING * 4,/* bottom = DEFAULT_PADDING * 4*/),
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth().padding(top = 8.dp), contentAlignment = Alignment.Center) {
|
||||
Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) {
|
||||
SimpleXLogo()
|
||||
}
|
||||
|
||||
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 36.dp).padding(horizontal = 48.dp), textAlign = TextAlign.Center)
|
||||
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 60.dp).padding(horizontal = 48.dp), textAlign = TextAlign.Center)
|
||||
|
||||
InfoRow(painterResource(R.drawable.privacy), R.string.privacy_redefined, R.string.first_platform_without_user_ids, width = 80.dp)
|
||||
InfoRow(painterResource(R.drawable.shield), R.string.immune_to_spam_and_abuse, R.string.people_can_connect_only_via_links_you_share)
|
||||
@@ -68,11 +69,12 @@ fun SimpleXInfoLayout(
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp), contentAlignment = Alignment.Center
|
||||
.padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING), contentAlignment = Alignment.Center
|
||||
) {
|
||||
SimpleButton(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
|
||||
SimpleButtonDecorated(text = stringResource(R.string.how_it_works), icon = Icons.Outlined.Info,
|
||||
click = showModal { HowItWorks(user, onboardingStage) })
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +85,7 @@ fun SimpleXLogo() {
|
||||
contentDescription = stringResource(R.string.image_descr_simplex_logo),
|
||||
modifier = Modifier
|
||||
.padding(vertical = DEFAULT_PADDING)
|
||||
.fillMaxWidth(0.80f)
|
||||
.fillMaxWidth(0.60f)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,9 +105,9 @@ private fun InfoRow(icon: Painter, @StringRes titleId: Int, @StringRes textId: I
|
||||
@Composable
|
||||
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
|
||||
if (user == null) {
|
||||
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, onclick)
|
||||
OnboardingActionButton(R.string.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick)
|
||||
} else {
|
||||
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, onclick)
|
||||
OnboardingActionButton(R.string.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,16 +116,30 @@ fun OnboardingActionButton(
|
||||
@StringRes labelId: Int,
|
||||
onboarding: OnboardingStage?,
|
||||
onboardingStage: MutableState<OnboardingStage?>,
|
||||
border: Boolean,
|
||||
onclick: (() -> Unit)?
|
||||
) {
|
||||
val modifier = if (border) {
|
||||
Modifier
|
||||
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
|
||||
.padding(
|
||||
horizontal = DEFAULT_PADDING * 3,
|
||||
vertical = 4.dp
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
SimpleButtonFrame(click = {
|
||||
onclick?.invoke()
|
||||
onboardingStage.value = onboarding
|
||||
}) {
|
||||
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
|
||||
}, modifier) {
|
||||
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
|
||||
Icon(
|
||||
Icons.Outlined.ArrowForwardIos, "next stage", tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, top = 5.dp)
|
||||
.size(15.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package chat.simplex.app.views.onboarding
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -40,11 +40,13 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start
|
||||
horizontalAlignment = Alignment.Start,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
) {
|
||||
Icon(icon, stringResource(titleId), tint = HighOrLowlight)
|
||||
Text(
|
||||
@@ -107,8 +109,9 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
|
||||
ModalView(close = close) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = DEFAULT_PADDING)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
@@ -307,6 +310,27 @@ private val versionDescriptions: List<VersionDescription> = listOf(
|
||||
)
|
||||
)
|
||||
),
|
||||
VersionDescription(
|
||||
version = "v5.0",
|
||||
features = listOf(
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.UploadFile,
|
||||
titleId = R.string.v5_0_large_files_support,
|
||||
descrId = R.string.v5_0_large_files_support_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Lock,
|
||||
titleId = R.string.v5_0_app_passcode,
|
||||
descrId = R.string.v5_0_app_passcode_descr
|
||||
),
|
||||
FeatureDescription(
|
||||
icon = Icons.Outlined.Translate,
|
||||
titleId = R.string.v5_0_polish_interface,
|
||||
descrId = R.string.v5_0_polish_interface_descr,
|
||||
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
private val lastVersion = versionDescriptions.last().version
|
||||
|
||||
+29
-32
@@ -12,6 +12,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -102,15 +103,6 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
saveCfg(newCfg)
|
||||
}
|
||||
|
||||
fun updateSettingsDialog(action: () -> Unit) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.update_network_settings_question),
|
||||
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onConfirm = action
|
||||
)
|
||||
}
|
||||
|
||||
AdvancedNetworkSettingsLayout(
|
||||
networkTCPConnectTimeout,
|
||||
networkTCPTimeout,
|
||||
@@ -121,10 +113,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) {
|
||||
networkTCPKeepIntvl,
|
||||
networkTCPKeepCnt,
|
||||
resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults,
|
||||
reset = { updateSettingsDialog(::reset) },
|
||||
reset = { showUpdateNetworkSettingsDialog(::reset) },
|
||||
footerDisabled = buildCfg() == currentCfg.value,
|
||||
revert = { updateView(currentCfg.value) },
|
||||
save = { updateSettingsDialog { saveCfg(buildCfg()) } }
|
||||
save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -262,14 +254,14 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val expanded = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Text(title)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
@@ -285,24 +277,22 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int>
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Icon(
|
||||
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
generalGetString(R.string.invite_to_group_button),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
DefaultExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
selection.value = selectionOption
|
||||
expanded = false
|
||||
}
|
||||
expanded.value = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||
) {
|
||||
Text(
|
||||
"$selectionOption $label",
|
||||
@@ -323,14 +313,14 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
Text(title)
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
expanded = expanded.value,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
) {
|
||||
val df = DecimalFormat("#.#")
|
||||
@@ -348,24 +338,22 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List
|
||||
)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Icon(
|
||||
if (!expanded) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
if (!expanded.value) Icons.Outlined.ExpandMore else Icons.Outlined.ExpandLess,
|
||||
generalGetString(R.string.invite_to_group_button),
|
||||
modifier = Modifier.padding(start = 8.dp),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
}
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
}
|
||||
DefaultExposedDropdownMenu(
|
||||
expanded = expanded
|
||||
) {
|
||||
values.forEach { selectionOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
selection.value = selectionOption
|
||||
expanded = false
|
||||
}
|
||||
expanded.value = false
|
||||
},
|
||||
contentPadding = PaddingValues(horizontal = DEFAULT_PADDING * 1.5f)
|
||||
) {
|
||||
Text(
|
||||
"${df.format(selectionOption / 1_000_000.toDouble())} $label",
|
||||
@@ -415,6 +403,15 @@ fun FooterButton(icon: ImageVector, title: String, action: () -> Unit, disabled:
|
||||
}
|
||||
}
|
||||
|
||||
fun showUpdateNetworkSettingsDialog(action: () -> Unit) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.update_network_settings_question),
|
||||
text = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
confirmText = generalGetString(R.string.update_network_settings_confirmation),
|
||||
onConfirm = action
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
|
||||
@@ -236,6 +236,7 @@ private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
|
||||
"fr" to "Français",
|
||||
"it" to "Italiano",
|
||||
"nl" to "Nederlands",
|
||||
"pl" to "Polski",
|
||||
"ru" to "Русский",
|
||||
"zh-CN" to "简体中文"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
import chat.simplex.app.views.TerminalView
|
||||
import chat.simplex.app.views.helpers.*
|
||||
|
||||
@Composable
|
||||
fun DeveloperView(
|
||||
m: ChatModel,
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
withAuth: (block: () -> Unit) -> Unit
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
AppBarTitle(stringResource(R.string.settings_developer_tools))
|
||||
val developerTools = m.controller.appPrefs.developerTools
|
||||
val devTools = remember { mutableStateOf(developerTools.get()) }
|
||||
SectionView() {
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.DriveFolderUpload, stringResource(R.string.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades)
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.Code, stringResource(R.string.show_developer_options), developerTools, devTools)
|
||||
}
|
||||
SectionTextFooter(
|
||||
generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) + " " +
|
||||
generalGetString(R.string.developer_options)
|
||||
)
|
||||
SectionSpacer()
|
||||
}
|
||||
}
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Videocam
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.ChatModel
|
||||
|
||||
@Composable
|
||||
fun ExperimentalFeaturesView(chatModel: ChatModel, enableCalls: MutableState<Boolean>) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.settings_experimental_features),
|
||||
style = MaterialTheme.typography.h1,
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 24.dp)
|
||||
)
|
||||
SectionView("") {
|
||||
SettingsPreferenceItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), chatModel.controller.appPrefs.experimentalCalls, enableCalls)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -69,11 +69,12 @@ private fun HiddenProfileLayout(
|
||||
|
||||
val hidePassword = rememberSaveable { mutableStateOf("") }
|
||||
val confirmHidePassword = rememberSaveable { mutableStateOf("") }
|
||||
val passwordValid by remember { derivedStateOf { hidePassword.value == hidePassword.value.trim() } }
|
||||
val confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } }
|
||||
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || confirmHidePassword.value == "" || !confirmValid } }
|
||||
val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } }
|
||||
SectionView(stringResource(R.string.hidden_profile_password).uppercase()) {
|
||||
SectionItemView {
|
||||
PassphraseField(hidePassword, generalGetString(R.string.password_to_show), isValid = { true }, showStrength = true)
|
||||
PassphraseField(hidePassword, generalGetString(R.string.password_to_show), isValid = { passwordValid }, showStrength = true)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
@@ -86,4 +87,4 @@ private fun HiddenProfileLayout(
|
||||
}
|
||||
SectionTextFooter(stringResource(R.string.to_reveal_profile_enter_password))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+142
-8
@@ -1,18 +1,26 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionCustomFooter
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import SectionViewSelectable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
@@ -43,6 +51,7 @@ fun NetworkAndServersView(
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = onionHosts,
|
||||
sessionMode = sessionMode,
|
||||
proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } },
|
||||
showModal = showModal,
|
||||
showSettingsModal = showSettingsModal,
|
||||
showCustomModal = showCustomModal,
|
||||
@@ -86,7 +95,7 @@ fun NetworkAndServersView(
|
||||
OnionHosts.PREFER -> generalGetString(R.string.network_use_onion_hosts_prefer_desc_in_alert)
|
||||
OnionHosts.REQUIRED -> generalGetString(R.string.network_use_onion_hosts_required_desc_in_alert)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_onion_hosts_settings_question),
|
||||
startsWith,
|
||||
onDismiss = {
|
||||
@@ -113,7 +122,7 @@ fun NetworkAndServersView(
|
||||
TransportSessionMode.User -> generalGetString(R.string.network_session_mode_user_description)
|
||||
TransportSessionMode.Entity -> generalGetString(R.string.network_session_mode_entity_description)
|
||||
}
|
||||
updateNetworkSettingsDialog(
|
||||
showUpdateNetworkSettingsDialog(
|
||||
title = generalGetString(R.string.update_network_session_mode_question),
|
||||
startsWith,
|
||||
onDismiss = { sessionMode.value = prevValue }
|
||||
@@ -138,6 +147,7 @@ fun NetworkAndServersView(
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
proxyPort: State<Int>,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit),
|
||||
@@ -152,10 +162,14 @@ fun NetworkAndServersView(
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.network_and_servers))
|
||||
SectionView(generalGetString(R.string.settings_section_title_messages)) {
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> SMPServersView(m, close) })
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.smp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.SMP, close) })
|
||||
SectionDivider()
|
||||
|
||||
SettingsActionItem(Icons.Outlined.Dns, stringResource(R.string.xftp_servers), showCustomModal { m, close -> ProtocolServersView(m, ServerProtocol.XFTP, close) })
|
||||
SectionDivider()
|
||||
|
||||
SectionItemView {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showSettingsModal)
|
||||
}
|
||||
SectionDivider()
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showSettingsModal, useOnion)
|
||||
@@ -166,7 +180,10 @@ fun NetworkAndServersView(
|
||||
}
|
||||
SettingsActionItem(Icons.Outlined.Cable, stringResource(R.string.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) })
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
if (networkUseSocksProxy.value) {
|
||||
SectionCustomFooter { Text(annotatedStringResource(R.string.disable_onion_hosts_when_not_supported)) }
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
SectionView(generalGetString(R.string.settings_section_title_calls)) {
|
||||
SettingsActionItem(Icons.Outlined.ElectricalServices, stringResource(R.string.webrtc_ice_servers), showModal { RTCServersView(it) })
|
||||
}
|
||||
@@ -176,7 +193,9 @@ fun NetworkAndServersView(
|
||||
@Composable
|
||||
fun UseSocksProxySwitch(
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
toggleSocksProxy: (Boolean) -> Unit
|
||||
proxyPort: State<Int>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)
|
||||
) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -193,7 +212,19 @@ fun UseSocksProxySwitch(
|
||||
stringResource(R.string.network_socks_toggle),
|
||||
tint = HighOrLowlight
|
||||
)
|
||||
Text(stringResource(R.string.network_socks_toggle))
|
||||
if (networkUseSocksProxy.value) {
|
||||
Row {
|
||||
Text(generalGetString(R.string.network_socks_toggle_use_socks_proxy) + " (")
|
||||
Text(
|
||||
generalGetString(R.string.network_proxy_port).format(proxyPort.value),
|
||||
Modifier.clickable { showSettingsModal { SockProxySettings(it) }() },
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
Text(")")
|
||||
}
|
||||
} else {
|
||||
Text(stringResource(R.string.network_socks_toggle))
|
||||
}
|
||||
}
|
||||
Switch(
|
||||
checked = networkUseSocksProxy.value,
|
||||
@@ -206,6 +237,83 @@ fun UseSocksProxySwitch(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SockProxySettings(m: ChatModel) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_BOTTOM_PADDING),
|
||||
) {
|
||||
val defaultHostPort = remember { "localhost:9050" }
|
||||
AppBarTitle(generalGetString(R.string.network_socks_proxy_settings))
|
||||
val hostPort by remember { m.controller.appPrefs.networkProxyHostPort.state }
|
||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPort?.split(":")?.lastOrNull() ?: "9050"))
|
||||
}
|
||||
val save = {
|
||||
withBGApi {
|
||||
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
|
||||
}
|
||||
}
|
||||
SectionView {
|
||||
SectionItemView {
|
||||
ResetToDefaultsButton({
|
||||
showUpdateNetworkSettingsDialog {
|
||||
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
|
||||
val newHost = defaultHostPort.split(":").first()
|
||||
val newPort = defaultHostPort.split(":").last()
|
||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||
save()
|
||||
}
|
||||
}, disabled = hostPort == defaultHostPort)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(R.string.host_verb),
|
||||
modifier = Modifier,
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(R.string.port_verb),
|
||||
modifier = Modifier,
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionCustomFooter {
|
||||
NetworkSectionFooter(
|
||||
revert = {
|
||||
val prevHost = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.firstOrNull() ?: "localhost"
|
||||
val prevPort = m.controller.appPrefs.networkProxyHostPort.get()?.split(":")?.lastOrNull() ?: "9050"
|
||||
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
|
||||
},
|
||||
save = { showUpdateNetworkSettingsDialog { save() } },
|
||||
revertDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text),
|
||||
saveDisabled = hostPort == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UseOnionHosts(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
@@ -274,7 +382,32 @@ private fun SessionModePicker(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNetworkSettingsDialog(
|
||||
@Composable
|
||||
private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FooterButton(Icons.Outlined.Replay, stringResource(R.string.network_options_revert), revert, revertDisabled)
|
||||
FooterButton(Icons.Outlined.Check, stringResource(R.string.network_options_save), save, saveDisabled)
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/106223
|
||||
private fun validHost(s: String): Boolean {
|
||||
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
||||
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
|
||||
return s.matches(validIp) || s.matches(validHostname)
|
||||
}
|
||||
|
||||
// https://ihateregex.io/expr/port/
|
||||
private fun validPort(s: String): Boolean {
|
||||
val validPort = Regex("^(6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4})$")
|
||||
return s.isNotBlank() && s.matches(validPort)
|
||||
}
|
||||
|
||||
private fun showUpdateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
message: String = generalGetString(R.string.updating_settings_will_reconnect_client_to_all_servers),
|
||||
@@ -298,6 +431,7 @@ fun PreviewNetworkAndServersLayout() {
|
||||
NetworkAndServersLayout(
|
||||
developerTools = true,
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
proxyPort = remember { mutableStateOf(9050) },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showCustomModal = { {} },
|
||||
|
||||
@@ -80,6 +80,11 @@ private fun PreferencesLayout(
|
||||
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
val allowCalls = remember(preferences) { mutableStateOf(preferences.calls.allow) }
|
||||
FeatureSection(ChatFeature.Calls, allowCalls) {
|
||||
applyPrefs(preferences.copy(calls = SimpleChatPreference(allow = it)))
|
||||
}
|
||||
SectionSpacer()
|
||||
ResetSaveButtons(
|
||||
reset = reset,
|
||||
save = savePrefs,
|
||||
@@ -97,11 +102,12 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
|
||||
FeatureAllowed.values().map { it to it.text },
|
||||
allowFeature,
|
||||
icon = feature.icon,
|
||||
onSelected = onSelected
|
||||
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
|
||||
onSelected = onSelected,
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(feature.allowDescription(allowFeature.value))
|
||||
SectionTextFooter(feature.allowDescription(allowFeature.value) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+260
-6
@@ -7,22 +7,41 @@ import SectionTextFooter
|
||||
import SectionView
|
||||
import android.view.WindowManager
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.HighOrLowlight
|
||||
import chat.simplex.app.ui.theme.SimplexGreen
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
|
||||
import chat.simplex.app.views.localauth.SetAppPasscodeView
|
||||
|
||||
enum class LAMode {
|
||||
SYSTEM,
|
||||
PASSCODE;
|
||||
|
||||
val text: String
|
||||
get() = when (this) {
|
||||
SYSTEM -> generalGetString(R.string.la_mode_system)
|
||||
PASSCODE -> generalGetString(R.string.la_mode_passcode)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrivacySettingsView(
|
||||
chatModel: ChatModel,
|
||||
setPerformLA: (Boolean) -> Unit
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -31,7 +50,7 @@ fun PrivacySettingsView(
|
||||
val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode
|
||||
AppBarTitle(stringResource(R.string.your_privacy))
|
||||
SectionView(stringResource(R.string.settings_section_title_device)) {
|
||||
ChatLockItem(chatModel.performLA, setPerformLA)
|
||||
ChatLockItem(chatModel, showSettingsModal, setPerformLA)
|
||||
SectionDivider()
|
||||
val context = LocalContext.current
|
||||
SettingsPreferenceItem(Icons.Outlined.VisibilityOff, stringResource(R.string.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on ->
|
||||
@@ -52,10 +71,12 @@ fun PrivacySettingsView(
|
||||
SectionDivider()
|
||||
SettingsPreferenceItem(Icons.Outlined.TravelExplore, stringResource(R.string.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||
SectionDivider()
|
||||
SectionItemView { SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
simplexLinkMode.set(it)
|
||||
chatModel.simplexLinkMode.value = it
|
||||
}) }
|
||||
SectionItemView {
|
||||
SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = {
|
||||
simplexLinkMode.set(it)
|
||||
chatModel.simplexLinkMode.value = it
|
||||
})
|
||||
}
|
||||
}
|
||||
if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) {
|
||||
SectionTextFooter(stringResource(R.string.simplex_link_mode_browser_warning))
|
||||
@@ -83,3 +104,236 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
private val laDelays = listOf(10, 30, 60, 180, 0)
|
||||
|
||||
@Composable
|
||||
fun SimplexLockView(
|
||||
chatModel: ChatModel,
|
||||
currentLAMode: SharedPreference<LAMode>,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit
|
||||
) {
|
||||
val performLA = remember { chatModel.performLA }
|
||||
val laMode = remember { chatModel.controller.appPrefs.laMode.state }
|
||||
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
|
||||
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
|
||||
val activity = LocalContext.current as FragmentActivity
|
||||
|
||||
fun resetLAEnabled(onOff: Boolean) {
|
||||
chatModel.controller.appPrefs.performLA.set(onOff)
|
||||
chatModel.performLA.value = onOff
|
||||
}
|
||||
|
||||
fun disableUnavailableLA() {
|
||||
resetLAEnabled(false)
|
||||
currentLAMode.set(LAMode.SYSTEM)
|
||||
laUnavailableInstructionAlert()
|
||||
}
|
||||
|
||||
fun toggleLAMode(toLAMode: LAMode) {
|
||||
authenticate(
|
||||
if (toLAMode == LAMode.SYSTEM) {
|
||||
generalGetString(R.string.la_enter_app_passcode)
|
||||
} else {
|
||||
generalGetString(R.string.chat_lock)
|
||||
},
|
||||
generalGetString(R.string.change_lock_mode), activity
|
||||
) { laResult ->
|
||||
when (laResult) {
|
||||
is LAResult.Error -> {
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
LAResult.Success -> {
|
||||
when (toLAMode) {
|
||||
LAMode.SYSTEM -> {
|
||||
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
currentLAMode.set(toLAMode)
|
||||
ksAppPassword.remove()
|
||||
laTurnedOnAlert()
|
||||
}
|
||||
is LAResult.Unavailable, is LAResult.Error -> {
|
||||
laFailedAlert()
|
||||
}
|
||||
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
laLockDelay.set(30)
|
||||
currentLAMode.set(toLAMode)
|
||||
passcodeAlert(generalGetString(R.string.passcode_set))
|
||||
},
|
||||
cancel = {},
|
||||
close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
is LAResult.Unavailable -> disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changeLAPassword() {
|
||||
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity) { laResult ->
|
||||
when (laResult) {
|
||||
LAResult.Success -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
passcodeAlert(generalGetString(R.string.passcode_changed))
|
||||
}, cancel = {
|
||||
passcodeAlert(generalGetString(R.string.passcode_not_changed))
|
||||
}, close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is LAResult.Error -> laFailedAlert()
|
||||
is LAResult.Failed -> {}
|
||||
is LAResult.Unavailable -> disableUnavailableLA()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.chat_lock))
|
||||
SectionView {
|
||||
EnableLock(performLA) { performLAToggle ->
|
||||
performLA.value = performLAToggle
|
||||
chatModel.controller.appPrefs.laNoticeShown.set(true)
|
||||
if (performLAToggle) {
|
||||
when (currentLAMode.state.value) {
|
||||
LAMode.SYSTEM -> {
|
||||
setPerformLA(true, activity)
|
||||
}
|
||||
LAMode.PASSCODE -> {
|
||||
ModalManager.shared.showCustomModal { close ->
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
SetAppPasscodeView(
|
||||
submit = {
|
||||
laLockDelay.set(30)
|
||||
chatModel.controller.appPrefs.performLA.set(true)
|
||||
passcodeAlert(generalGetString(R.string.passcode_set))
|
||||
},
|
||||
cancel = {
|
||||
resetLAEnabled(false)
|
||||
}, close
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setPerformLA(false, activity)
|
||||
}
|
||||
}
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
LockModeSelector(laMode) { newLAMode ->
|
||||
if (laMode.value == newLAMode) return@LockModeSelector
|
||||
if (chatModel.controller.appPrefs.performLA.get()) {
|
||||
toggleLAMode(newLAMode)
|
||||
} else {
|
||||
currentLAMode.set(newLAMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (performLA.value) {
|
||||
SectionDivider()
|
||||
SectionItemView {
|
||||
LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) }
|
||||
}
|
||||
if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) {
|
||||
SectionDivider()
|
||||
SectionItemView({ changeLAPassword() }) {
|
||||
Text(generalGetString(R.string.la_change_app_passcode))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
|
||||
SectionItemView {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
stringResource(R.string.enable_lock), Modifier
|
||||
.padding(end = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
)
|
||||
Switch(
|
||||
checked = performLA.value,
|
||||
onCheckedChange = onCheckedChange,
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockModeSelector(state: State<LAMode>, onSelected: (LAMode) -> Unit) {
|
||||
val values by remember { mutableStateOf(LAMode.values().map { it to it.text }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.lock_mode),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LockDelaySelector(state: State<Int>, onSelected: (Int) -> Unit) {
|
||||
val delays = remember { if (laDelays.contains(state.value)) laDelays else listOf(state.value) + laDelays }
|
||||
val values by remember { mutableStateOf(delays.map { it to laDelayText(it) }) }
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(R.string.lock_after),
|
||||
values,
|
||||
state,
|
||||
icon = null,
|
||||
enabled = remember { mutableStateOf(true) },
|
||||
onSelected = onSelected
|
||||
)
|
||||
}
|
||||
|
||||
private fun laDelayText(t: Int): String {
|
||||
val m = t / 60
|
||||
val s = t % 60
|
||||
return if (t == 0) {
|
||||
generalGetString(R.string.la_immediately)
|
||||
} else if (m == 0 || s != 0) {
|
||||
// there are no options where both minutes and seconds are needed
|
||||
generalGetString(R.string.la_seconds).format(s)
|
||||
} else {
|
||||
generalGetString(R.string.la_minutes).format(m)
|
||||
}
|
||||
}
|
||||
|
||||
private fun passcodeAlert(title: String) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = title,
|
||||
text = generalGetString(R.string.la_please_remember_to_store_password)
|
||||
)
|
||||
}
|
||||
|
||||
+16
-7
@@ -32,12 +32,13 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
|
||||
fun ProtocolServerView(m: ChatModel, server: ServerCfg, serverProtocol: ServerProtocol, onUpdate: (ServerCfg) -> Unit, onDelete: () -> Unit) {
|
||||
var testing by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
SMPServerLayout(
|
||||
ProtocolServerLayout(
|
||||
testing,
|
||||
server,
|
||||
serverProtocol,
|
||||
testServer = {
|
||||
testing = true
|
||||
scope.launch {
|
||||
@@ -68,9 +69,10 @@ fun SMPServerView(m: ChatModel, server: ServerCfg, onUpdate: (ServerCfg) -> Unit
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServerLayout(
|
||||
private fun ProtocolServerLayout(
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
serverProtocol: ServerProtocol,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
@@ -86,7 +88,7 @@ private fun SMPServerLayout(
|
||||
if (server.preset) {
|
||||
PresetServer(testing, server, testServer, onUpdate, onDelete)
|
||||
} else {
|
||||
CustomServer(testing, server, testServer, onUpdate, onDelete)
|
||||
CustomServer(testing, server, serverProtocol, testServer, onUpdate, onDelete)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,12 +121,19 @@ private fun PresetServer(
|
||||
private fun CustomServer(
|
||||
testing: Boolean,
|
||||
server: ServerCfg,
|
||||
serverProtocol: ServerProtocol,
|
||||
testServer: () -> Unit,
|
||||
onUpdate: (ServerCfg) -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
) {
|
||||
val serverAddress = remember { mutableStateOf(server.server) }
|
||||
val valid = remember { derivedStateOf { parseServerAddress(serverAddress.value)?.valid == true } }
|
||||
val valid = remember {
|
||||
derivedStateOf {
|
||||
with(parseServerAddress(serverAddress.value)) {
|
||||
this?.valid == true && this.serverProtocol == serverProtocol
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionView(
|
||||
stringResource(R.string.smp_servers_your_server_address).uppercase(),
|
||||
icon = Icons.Outlined.ErrorOutline,
|
||||
@@ -187,9 +196,9 @@ fun ShowTestStatus(server: ServerCfg, modifier: Modifier = Modifier) =
|
||||
else -> Icon(Icons.Outlined.Check, null, modifier, tint = Color.Transparent)
|
||||
}
|
||||
|
||||
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, SMPTestFailure?> =
|
||||
suspend fun testServerConnection(server: ServerCfg, m: ChatModel): Pair<ServerCfg, ProtocolTestFailure?> =
|
||||
try {
|
||||
val r = m.controller.testSMPServer(server.server)
|
||||
val r = m.controller.testProtoServer(server.server)
|
||||
server.copy(tested = r == null) to r
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "testServerConnection ${e.stackTraceToString()}")
|
||||
+47
-30
@@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
@@ -27,17 +28,19 @@ import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () -> Unit) {
|
||||
var presetServers by remember { mutableStateOf(emptyList<String>()) }
|
||||
var servers by remember {
|
||||
mutableStateOf(m.userSMPServersUnsaved.value ?: m.userSMPServers.value ?: emptyList())
|
||||
mutableStateOf(m.userSMPServersUnsaved.value ?: emptyList())
|
||||
}
|
||||
val currServers = remember { mutableStateOf(servers) }
|
||||
val testing = rememberSaveable { mutableStateOf(false) }
|
||||
val serversUnchanged = remember { derivedStateOf { servers == m.userSMPServers.value || testing.value } }
|
||||
val serversUnchanged = remember { derivedStateOf { servers == currServers.value || testing.value } }
|
||||
val allServersDisabled = remember { derivedStateOf { servers.all { !it.enabled } } }
|
||||
val saveDisabled = remember {
|
||||
derivedStateOf {
|
||||
servers.isEmpty() ||
|
||||
servers == m.userSMPServers.value ||
|
||||
servers == currServers.value ||
|
||||
testing.value ||
|
||||
!servers.all { srv ->
|
||||
val address = parseServerAddress(srv.server)
|
||||
@@ -47,13 +50,25 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val res = m.controller.getUserProtoServers(serverProtocol)
|
||||
if (res != null) {
|
||||
currServers.value = res.protoServers
|
||||
presetServers = res.presetServers
|
||||
if (servers.isEmpty()) {
|
||||
servers = currServers.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showServer(server: ServerCfg) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
var old by remember { mutableStateOf(server) }
|
||||
val index = servers.indexOf(old)
|
||||
SMPServerView(
|
||||
ProtocolServerView(
|
||||
m,
|
||||
old,
|
||||
serverProtocol,
|
||||
onUpdate = { updated ->
|
||||
val newServers = ArrayList(servers)
|
||||
newServers.removeAt(index)
|
||||
@@ -75,11 +90,12 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
ModalView(
|
||||
close = {
|
||||
if (saveDisabled.value) close()
|
||||
else showUnsavedChangesAlert({ saveSMPServers(servers, m, close) }, close)
|
||||
else showUnsavedChangesAlert({ saveServers(serverProtocol, currServers, servers, m, close) }, close)
|
||||
},
|
||||
background = if (isInDarkTheme()) MaterialTheme.colors.background else SettingsBackgroundLight
|
||||
) {
|
||||
SMPServersLayout(
|
||||
ProtocolServersLayout(
|
||||
serverProtocol,
|
||||
testing = testing.value,
|
||||
servers = servers,
|
||||
serversUnchanged = serversUnchanged.value,
|
||||
@@ -97,12 +113,12 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
// No saving until something will be changed on the next screen to prevent blank servers on the list
|
||||
showServer(servers.last())
|
||||
}) {
|
||||
Text(stringResource(R.string.smp_servers_enter_manually))
|
||||
Text(stringResource(R.string.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
ScanSMPServer {
|
||||
ScanProtocolServer {
|
||||
close()
|
||||
servers = servers + it
|
||||
m.userSMPServersUnsaved.value = servers
|
||||
@@ -110,15 +126,15 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.smp_servers_scan_qr))
|
||||
Text(stringResource(R.string.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
val hasAllPresets = hasAllPresets(servers, m)
|
||||
val hasAllPresets = hasAllPresets(presetServers, servers, m)
|
||||
if (!hasAllPresets) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
servers = (servers + addAllPresets(servers, m)).sortedByDescending { it.preset }
|
||||
servers = (servers + addAllPresets(presetServers, servers, m)).sortedByDescending { it.preset }
|
||||
}) {
|
||||
Text(stringResource(R.string.smp_servers_preset_add), color = MaterialTheme.colors.onBackground)
|
||||
Text(stringResource(R.string.smp_servers_preset_add), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,11 +150,11 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
},
|
||||
resetServers = {
|
||||
servers = m.userSMPServers.value ?: emptyList()
|
||||
servers = currServers.value ?: emptyList()
|
||||
m.userSMPServersUnsaved.value = null
|
||||
},
|
||||
saveSMPServers = {
|
||||
saveSMPServers(servers, m)
|
||||
saveServers(serverProtocol, currServers, servers, m)
|
||||
},
|
||||
showServer = ::showServer,
|
||||
)
|
||||
@@ -161,7 +177,8 @@ fun SMPServersView(m: ChatModel, close: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SMPServersLayout(
|
||||
private fun ProtocolServersLayout(
|
||||
serverProtocol: ServerProtocol,
|
||||
testing: Boolean,
|
||||
servers: List<ServerCfg>,
|
||||
serversUnchanged: Boolean,
|
||||
@@ -180,12 +197,12 @@ private fun SMPServersLayout(
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_PADDING),
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_SMP_servers))
|
||||
AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.your_SMP_servers else R.string.your_XFTP_servers))
|
||||
|
||||
SectionView(stringResource(R.string.smp_servers).uppercase()) {
|
||||
SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) R.string.smp_servers else R.string.xftp_servers).uppercase()) {
|
||||
for (srv in servers) {
|
||||
SectionItemView({ showServer(srv) }, disabled = testing) {
|
||||
SmpServerView(srv, servers, testing)
|
||||
ProtocolServerView(serverProtocol, srv, servers, testing)
|
||||
}
|
||||
SectionDivider()
|
||||
}
|
||||
@@ -232,10 +249,10 @@ private fun SMPServersLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SmpServerView(srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
|
||||
private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, servers: List<ServerCfg>, disabled: Boolean) {
|
||||
val address = parseServerAddress(srv.server)
|
||||
when {
|
||||
address == null || !address.valid || !uniqueAddress(srv, address, servers) -> InvalidServer()
|
||||
address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer()
|
||||
!srv.enabled -> Icon(Icons.Outlined.DoNotDisturb, null, tint = HighOrLowlight)
|
||||
else -> ShowTestStatus(srv)
|
||||
}
|
||||
@@ -271,12 +288,12 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List<Se
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasAllPresets(servers: List<ServerCfg>, m: ChatModel): Boolean =
|
||||
m.presetSMPServers.value?.all { hasPreset(it, servers) } ?: true
|
||||
private fun hasAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): Boolean =
|
||||
presetServers.all { hasPreset(it, servers) } ?: true
|
||||
|
||||
private fun addAllPresets(servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
private fun addAllPresets(presetServers: List<String>, servers: List<ServerCfg>, m: ChatModel): List<ServerCfg> {
|
||||
val toAdd = ArrayList<ServerCfg>()
|
||||
for (srv in m.presetSMPServers.value ?: emptyList()) {
|
||||
for (srv in presetServers) {
|
||||
if (!hasPreset(srv, servers)) {
|
||||
toAdd.add(ServerCfg(srv, preset = true, tested = null, enabled = true))
|
||||
}
|
||||
@@ -313,8 +330,8 @@ private fun resetTestStatus(servers: List<ServerCfg>): List<ServerCfg> {
|
||||
return copy
|
||||
}
|
||||
|
||||
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, SMPTestFailure> {
|
||||
val fs: MutableMap<String, SMPTestFailure> = mutableMapOf()
|
||||
private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpdated: (List<ServerCfg>) -> Unit): Map<String, ProtocolTestFailure> {
|
||||
val fs: MutableMap<String, ProtocolTestFailure> = mutableMapOf()
|
||||
val updatedServers = ArrayList<ServerCfg>(servers)
|
||||
for ((index, server) in servers.withIndex()) {
|
||||
if (server.enabled) {
|
||||
@@ -331,10 +348,10 @@ private suspend fun runServersTest(servers: List<ServerCfg>, m: ChatModel, onUpd
|
||||
return fs
|
||||
}
|
||||
|
||||
private fun saveSMPServers(servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
|
||||
private fun saveServers(protocol: ServerProtocol, currServers: MutableState<List<ServerCfg>>, servers: List<ServerCfg>, m: ChatModel, afterSave: () -> Unit = {}) {
|
||||
withApi {
|
||||
if (m.controller.setUserSMPServers(servers)) {
|
||||
m.userSMPServers.value = servers
|
||||
if (m.controller.setUserProtoServers(protocol, servers)) {
|
||||
currServers.value = servers
|
||||
m.userSMPServersUnsaved.value = null
|
||||
}
|
||||
afterSave()
|
||||
+3
-4
@@ -2,7 +2,6 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -17,16 +16,16 @@ import chat.simplex.app.views.newchat.QRCodeScanner
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@Composable
|
||||
fun ScanSMPServer(onNext: (ServerCfg) -> Unit) {
|
||||
fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) {
|
||||
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
ScanSMPServerLayout(onNext)
|
||||
ScanProtocolServerLayout(onNext)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanSMPServerLayout(onNext: (ServerCfg) -> Unit) {
|
||||
private fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
+25
-35
@@ -42,7 +42,7 @@ import chat.simplex.app.views.onboarding.SimpleXInfo
|
||||
import chat.simplex.app.views.onboarding.WhatsNewView
|
||||
|
||||
@Composable
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean, FragmentActivity) -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
val stopped = chatModel.chatRunning.value == false
|
||||
|
||||
@@ -57,7 +57,6 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) {
|
||||
chatModel.chatDbEncrypted.value == true,
|
||||
chatModel.incognito,
|
||||
chatModel.controller.appPrefs.incognito,
|
||||
developerTools = chatModel.controller.appPrefs.developerTools,
|
||||
user.displayName,
|
||||
setPerformLA = setPerformLA,
|
||||
showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } },
|
||||
@@ -126,9 +125,8 @@ fun SettingsLayout(
|
||||
encrypted: Boolean,
|
||||
incognito: MutableState<Boolean>,
|
||||
incognitoPref: SharedPreference<Boolean>,
|
||||
developerTools: SharedPreference<Boolean>,
|
||||
userDisplayName: String,
|
||||
setPerformLA: (Boolean) -> Unit,
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit,
|
||||
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit,
|
||||
@@ -176,7 +174,7 @@ fun SettingsLayout(
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Videocam, stringResource(R.string.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, setPerformLA) }, disabled = stopped)
|
||||
SettingsActionItem(Icons.Outlined.Lock, stringResource(R.string.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
SettingsActionItem(Icons.Outlined.LightMode, stringResource(R.string.appearance_settings), showSettingsModal { AppearanceView(it) }, disabled = stopped)
|
||||
SectionDivider()
|
||||
@@ -207,17 +205,8 @@ fun SettingsLayout(
|
||||
SectionSpacer()
|
||||
|
||||
SectionView(stringResource(R.string.settings_section_title_develop)) {
|
||||
val devTools = remember { mutableStateOf(developerTools.get()) }
|
||||
SettingsPreferenceItem(Icons.Outlined.Construction, stringResource(R.string.settings_developer_tools), developerTools, devTools)
|
||||
SettingsActionItem(Icons.Outlined.Code, stringResource(R.string.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) })
|
||||
SectionDivider()
|
||||
if (devTools.value) {
|
||||
ChatConsoleItem { withAuth(showCustomModal { it, close -> TerminalView(it, close) }) }
|
||||
SectionDivider()
|
||||
InstallTerminalAppItem(uriHandler)
|
||||
SectionDivider()
|
||||
}
|
||||
// SettingsActionItem(Icons.Outlined.Science, stringResource(R.string.settings_experimental_features), showSettingsModal { ExperimentalFeaturesView(it, enableCalls) })
|
||||
// SectionDivider()
|
||||
AppVersionItem(showVersion)
|
||||
}
|
||||
}
|
||||
@@ -303,13 +292,20 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable fun ChatLockItem(performLA: MutableState<Boolean>, setPerformLA: (Boolean) -> Unit) {
|
||||
SectionItemView() {
|
||||
@Composable
|
||||
fun ChatLockItem(
|
||||
chatModel: ChatModel,
|
||||
showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
|
||||
setPerformLA: (Boolean, FragmentActivity) -> Unit
|
||||
) {
|
||||
val performLA = remember { chatModel.performLA }
|
||||
val currentLAMode = remember { chatModel.controller.appPrefs.laMode }
|
||||
SectionItemView(showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
Icons.Outlined.Lock,
|
||||
if (performLA.value) Icons.Filled.Lock else Icons.Outlined.Lock,
|
||||
contentDescription = stringResource(R.string.chat_lock),
|
||||
tint = HighOrLowlight,
|
||||
tint = if (performLA.value) SimplexGreen else HighOrLowlight,
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
@@ -318,14 +314,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
.fillMaxWidth()
|
||||
.weight(1F)
|
||||
)
|
||||
Switch(
|
||||
checked = performLA.value,
|
||||
onCheckedChange = { setPerformLA(it) },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colors.primary,
|
||||
uncheckedThumbColor = HighOrLowlight
|
||||
)
|
||||
)
|
||||
Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(androidx.compose.ui.R.string.off), color = HighOrLowlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,7 +359,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun ChatConsoleItem(showTerminal: () -> Unit) {
|
||||
@Composable fun ChatConsoleItem(showTerminal: () -> Unit) {
|
||||
SectionItemView(showTerminal) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_outline_terminal),
|
||||
@@ -382,7 +371,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun InstallTerminalAppItem(uriHandler: UriHandler) {
|
||||
@Composable fun InstallTerminalAppItem(uriHandler: UriHandler) {
|
||||
SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_github),
|
||||
@@ -395,9 +384,11 @@ fun MaintainIncognitoState(chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
@Composable private fun AppVersionItem(showVersion: () -> Unit) {
|
||||
SectionItemView(showVersion) {
|
||||
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
SectionItemView(showVersion) { AppVersionText() }
|
||||
}
|
||||
|
||||
@Composable fun AppVersionText() {
|
||||
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
}
|
||||
|
||||
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondary, stopped: Boolean = false) {
|
||||
@@ -524,7 +515,7 @@ private fun runAuth(context: Context, onFinish: (success: Boolean) -> Unit) {
|
||||
generalGetString(R.string.auth_log_in_using_credential),
|
||||
context as FragmentActivity,
|
||||
completed = { laResult ->
|
||||
onFinish(laResult == LAResult.Success || laResult == LAResult.Unavailable)
|
||||
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -544,9 +535,8 @@ fun PreviewSettingsLayout() {
|
||||
encrypted = false,
|
||||
incognito = remember { mutableStateOf(false) },
|
||||
incognitoPref = SharedPreference({ false }, {}),
|
||||
developerTools = SharedPreference({ false }, {}),
|
||||
userDisplayName = "Alice",
|
||||
setPerformLA = {},
|
||||
setPerformLA = { _, _ -> },
|
||||
showModal = { {} },
|
||||
showSettingsModal = { {} },
|
||||
showSettingsModalWithSearch = { },
|
||||
|
||||
+86
-133
@@ -1,14 +1,10 @@
|
||||
package chat.simplex.app.views.usersettings
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.*
|
||||
@@ -16,18 +12,20 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.ProfileNameField
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.isValidDisplayName
|
||||
import chat.simplex.app.views.onboarding.ReadableText
|
||||
import com.google.accompanist.insets.ProvideWindowInsets
|
||||
import com.google.accompanist.insets.navigationBarsWithImePadding
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -36,10 +34,8 @@ import kotlinx.coroutines.launch
|
||||
fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
val user = chatModel.currentUser.value
|
||||
if (user != null) {
|
||||
val editProfile = rememberSaveable { mutableStateOf(false) }
|
||||
var profile by remember { mutableStateOf(user.profile.toProfile()) }
|
||||
UserProfileLayout(
|
||||
editProfile = editProfile,
|
||||
profile = profile,
|
||||
close,
|
||||
saveProfile = { displayName, fullName, image ->
|
||||
@@ -49,7 +45,7 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
chatModel.updateCurrentUser(newProfile)
|
||||
profile = newProfile
|
||||
}
|
||||
editProfile.value = false
|
||||
close()
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -58,7 +54,6 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) {
|
||||
|
||||
@Composable
|
||||
fun UserProfileLayout(
|
||||
editProfile: MutableState<Boolean>,
|
||||
profile: Profile,
|
||||
close: () -> Unit,
|
||||
saveProfile: (String, String, String?) -> Unit,
|
||||
@@ -72,6 +67,7 @@ fun UserProfileLayout(
|
||||
val scrollState = rememberScrollState()
|
||||
val keyboardState by getKeyboardState()
|
||||
var savedKeyboardState by remember { mutableStateOf(keyboardState) }
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
|
||||
ModalBottomSheetLayout(
|
||||
scrimColor = Color.Black.copy(alpha = 0.12F),
|
||||
@@ -87,97 +83,89 @@ fun UserProfileLayout(
|
||||
sheetState = bottomSheetModalState,
|
||||
sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)
|
||||
) {
|
||||
ModalView(close = close) {
|
||||
val dataUnchanged =
|
||||
displayName.value == profile.displayName &&
|
||||
fullName.value == profile.fullName &&
|
||||
chosenImage.value == null
|
||||
|
||||
val closeWithAlert = {
|
||||
if (dataUnchanged || !(displayName.value.isNotEmpty() && isValidDisplayName(displayName.value))) {
|
||||
close()
|
||||
} else {
|
||||
showUnsavedChangesAlert({ saveProfile(displayName.value, fullName.value, profileImage.value) }, close)
|
||||
}
|
||||
}
|
||||
ModalView(close = closeWithAlert) {
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = DEFAULT_PADDING),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
AppBarTitle(stringResource(R.string.your_current_profile), false)
|
||||
Text(
|
||||
stringResource(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it),
|
||||
Modifier.padding(bottom = 24.dp),
|
||||
color = MaterialTheme.colors.onBackground,
|
||||
lineHeight = 22.sp
|
||||
)
|
||||
if (editProfile.value) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
AppBarTitleCentered(stringResource(R.string.your_current_profile))
|
||||
ReadableText(generalGetString(R.string.your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it), TextAlign.Center)
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(192.dp, profileImage.value)
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
DeleteImageButton { profileImage.value = null }
|
||||
}
|
||||
Box(contentAlignment = Alignment.TopEnd) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
ProfileImage(108.dp, profileImage.value, color = HighOrLowlight.copy(alpha = 0.1f))
|
||||
EditImageButton { scope.launch { bottomSheetModalState.show() } }
|
||||
}
|
||||
if (profileImage.value != null) {
|
||||
DeleteImageButton { profileImage.value = null }
|
||||
}
|
||||
}
|
||||
Box {
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Icon(Icons.Outlined.Info, tint = Color.Red, contentDescription = stringResource(R.string.display_name_cannot_contain_whitespace))
|
||||
}
|
||||
ProfileNameTextField(displayName)
|
||||
}
|
||||
ProfileNameTextField(fullName)
|
||||
Row {
|
||||
TextButton(stringResource(R.string.cancel_verb)) {
|
||||
displayName.value = profile.displayName
|
||||
fullName.value = profile.fullName
|
||||
profileImage.value = profile.image
|
||||
editProfile.value = false
|
||||
}
|
||||
Spacer(Modifier.padding(horizontal = 8.dp))
|
||||
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
val saveModifier: Modifier
|
||||
val saveColor: Color
|
||||
if (enabled) {
|
||||
saveModifier = Modifier
|
||||
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
|
||||
saveColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
saveModifier = Modifier
|
||||
saveColor = HighOrLowlight
|
||||
}
|
||||
}
|
||||
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
stringResource(R.string.display_name__field),
|
||||
fontSize = 16.sp
|
||||
)
|
||||
if (!isValidDisplayName(displayName.value)) {
|
||||
Spacer(Modifier.size(DEFAULT_PADDING_HALF))
|
||||
Text(
|
||||
stringResource(R.string.save_and_notify_contacts),
|
||||
modifier = saveModifier,
|
||||
color = saveColor
|
||||
stringResource(R.string.no_spaces),
|
||||
fontSize = 16.sp,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp), contentAlignment = Alignment.Center
|
||||
) {
|
||||
ProfileImage(192.dp, profile.image)
|
||||
if (profile.image == null) {
|
||||
EditImageButton {
|
||||
editProfile.value = true
|
||||
scope.launch { bottomSheetModalState.show() }
|
||||
}
|
||||
}
|
||||
}
|
||||
ProfileNameRow(stringResource(R.string.display_name__field), profile.displayName)
|
||||
ProfileNameRow(stringResource(R.string.full_name__field), profile.fullName)
|
||||
TextButton(stringResource(R.string.edit_verb)) { editProfile.value = true }
|
||||
ProfileNameField(displayName, "", ::isValidDisplayName, focusRequester)
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
Text(
|
||||
stringResource(R.string.full_name__field),
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
|
||||
)
|
||||
ProfileNameField(fullName)
|
||||
|
||||
Spacer(Modifier.height(DEFAULT_PADDING))
|
||||
val enabled = !dataUnchanged && displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
|
||||
val saveModifier: Modifier
|
||||
val saveColor: Color
|
||||
if (enabled) {
|
||||
saveModifier = Modifier
|
||||
.clickable { saveProfile(displayName.value, fullName.value, profileImage.value) }
|
||||
saveColor = MaterialTheme.colors.primary
|
||||
} else {
|
||||
saveModifier = Modifier
|
||||
saveColor = HighOrLowlight
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.save_and_notify_contacts),
|
||||
modifier = saveModifier,
|
||||
color = saveColor
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(DEFAULT_BOTTOM_BUTTON_PADDING))
|
||||
if (savedKeyboardState != keyboardState) {
|
||||
LaunchedEffect(keyboardState) {
|
||||
scope.launch {
|
||||
@@ -192,60 +180,17 @@ fun UserProfileLayout(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameTextField(name: MutableState<String>) {
|
||||
BasicTextField(
|
||||
value = name.value,
|
||||
onValueChange = { name.value = it },
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp)
|
||||
.padding(start = 28.dp)
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
autoCorrect = false
|
||||
),
|
||||
singleLine = true
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameRow(label: String, text: String) {
|
||||
Row(Modifier.padding(bottom = 24.dp)) {
|
||||
Text(
|
||||
label,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
Text(
|
||||
text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextButton(text: String, click: () -> Unit) {
|
||||
Text(
|
||||
text,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.clickable(onClick = click),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditImageButton(click: () -> Unit) {
|
||||
IconButton(
|
||||
onClick = click,
|
||||
modifier = Modifier.background(Color(1f, 1f, 1f, 0.2f), shape = CircleShape)
|
||||
modifier = Modifier.size(30.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.PhotoCamera,
|
||||
contentDescription = stringResource(R.string.edit_image),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.size(36.dp)
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -261,6 +206,16 @@ fun DeleteImageButton(click: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(R.string.save_preferences_question),
|
||||
confirmText = generalGetString(R.string.save_and_notify_contacts),
|
||||
dismissText = generalGetString(R.string.exit_without_saving),
|
||||
onConfirm = save,
|
||||
onDismiss = revert,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
@@ -273,7 +228,6 @@ fun PreviewUserProfileLayoutEditOff() {
|
||||
UserProfileLayout(
|
||||
profile = Profile.sampleData,
|
||||
close = {},
|
||||
editProfile = remember { mutableStateOf(false) },
|
||||
saveProfile = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
@@ -291,7 +245,6 @@ fun PreviewUserProfileLayoutEditOn() {
|
||||
UserProfileLayout(
|
||||
profile = Profile.sampleData,
|
||||
close = {},
|
||||
editProfile = remember { mutableStateOf(true) },
|
||||
saveProfile = { _, _, _ -> }
|
||||
)
|
||||
}
|
||||
|
||||
+145
-60
@@ -2,9 +2,11 @@ package chat.simplex.app.views.usersettings
|
||||
|
||||
import SectionDivider
|
||||
import SectionItemView
|
||||
import SectionItemViewSpaceBetween
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
@@ -17,6 +19,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.R
|
||||
import chat.simplex.app.chatPasswordHash
|
||||
@@ -24,10 +27,11 @@ import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.chat.item.ItemAction
|
||||
import chat.simplex.app.views.chatlist.UserProfilePickerItem
|
||||
import chat.simplex.app.views.chatlist.UserProfileRow
|
||||
import chat.simplex.app.views.database.PassphraseField
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.onboarding.CreateProfile
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>) {
|
||||
@@ -41,6 +45,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
searchTextOrPassword = searchTextOrPassword,
|
||||
showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice,
|
||||
visibleUsersCount = visibleUsersCount(m),
|
||||
prefPerformLA = m.controller.appPrefs.performLA.get(),
|
||||
addUser = {
|
||||
ModalManager.shared.showModalCloseable { close ->
|
||||
CreateProfile(m, close)
|
||||
@@ -48,7 +53,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
},
|
||||
activateUser = { user ->
|
||||
withBGApi {
|
||||
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value))
|
||||
m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim()))
|
||||
}
|
||||
},
|
||||
removeUser = { user ->
|
||||
@@ -67,16 +72,16 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
Column {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
removeUser(m, user, users, true, searchTextOrPassword.value)
|
||||
removeUser(m, user, users, true, searchTextOrPassword.value.trim())
|
||||
}) {
|
||||
Text(stringResource(R.string.users_delete_with_connections), color = Color.Red)
|
||||
Text(stringResource(R.string.users_delete_with_connections), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
removeUser(m, user, users, false, searchTextOrPassword.value)
|
||||
removeUser(m, user, users, false, searchTextOrPassword.value.trim())
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.users_delete_data_only), color = Color.Red)
|
||||
Text(stringResource(R.string.users_delete_data_only), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,15 +98,28 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden:
|
||||
}
|
||||
},
|
||||
unhideUser = { user ->
|
||||
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, userViewPassword(user, searchTextOrPassword.value)) }
|
||||
if (passwordEntryRequired(user, searchTextOrPassword.value)) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ProfileActionView(UserProfileAction.UNHIDE, user) { pwd ->
|
||||
withBGApi {
|
||||
setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) }
|
||||
close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, searchTextOrPassword.value.trim()) } }
|
||||
}
|
||||
},
|
||||
muteUser = { user ->
|
||||
setUserPrivacy(m, onSuccess = { if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert) }) {
|
||||
m.controller.apiMuteUser(user.userId, userViewPassword(user, searchTextOrPassword.value))
|
||||
withBGApi {
|
||||
setUserPrivacy(m, onSuccess = {
|
||||
if (m.controller.appPrefs.showMuteProfileAlert.get()) showMuteProfileAlert(m.controller.appPrefs.showMuteProfileAlert)
|
||||
}) { m.controller.apiMuteUser(user.userId) }
|
||||
}
|
||||
},
|
||||
unmuteUser = { user ->
|
||||
setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId, userViewPassword(user, searchTextOrPassword.value)) }
|
||||
withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } }
|
||||
},
|
||||
showHiddenProfile = { user ->
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
@@ -125,6 +143,7 @@ private fun UserProfilesView(
|
||||
searchTextOrPassword: MutableState<String>,
|
||||
profileHidden: MutableState<Boolean>,
|
||||
visibleUsersCount: Int,
|
||||
prefPerformLA: Boolean,
|
||||
showHiddenProfilesNotice: SharedPreference<Boolean>,
|
||||
addUser: () -> Unit,
|
||||
activateUser: (User) -> Unit,
|
||||
@@ -153,10 +172,10 @@ private fun UserProfilesView(
|
||||
|
||||
SectionView {
|
||||
for (user in filteredUsers) {
|
||||
UserView(user, users, visibleUsersCount, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
|
||||
UserView(user, users, visibleUsersCount, prefPerformLA, activateUser, removeUser, unhideUser, muteUser, unmuteUser, showHiddenProfile)
|
||||
SectionDivider()
|
||||
}
|
||||
if (searchTextOrPassword.value.isEmpty()) {
|
||||
if (searchTextOrPassword.value.trim().isEmpty()) {
|
||||
SectionItemView(addUser, minHeight = 68.dp) {
|
||||
Icon(Icons.Outlined.Add, stringResource(R.string.users_add), tint = MaterialTheme.colors.primary)
|
||||
Spacer(Modifier.padding(horizontal = 4.dp))
|
||||
@@ -186,6 +205,7 @@ private fun UserView(
|
||||
user: User,
|
||||
users: List<User>,
|
||||
visibleUsersCount: Int,
|
||||
prefPerformLA: Boolean,
|
||||
activateUser: (User) -> Unit,
|
||||
removeUser: (User) -> Unit,
|
||||
unhideUser: (User) -> Unit,
|
||||
@@ -193,102 +213,167 @@ private fun UserView(
|
||||
unmuteUser: (User) -> Unit,
|
||||
showHiddenProfile: (User) -> Unit,
|
||||
) {
|
||||
var showDropdownMenu by remember { mutableStateOf(false) }
|
||||
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showDropdownMenu = true }) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
UserProfilePickerItem(user, onLongClick = { if (users.size > 1) showMenu.value = true }) {
|
||||
activateUser(user)
|
||||
}
|
||||
Box(Modifier.padding(horizontal = 16.dp)) {
|
||||
DropdownMenu(
|
||||
expanded = showDropdownMenu,
|
||||
onDismissRequest = { showDropdownMenu = false },
|
||||
Modifier.width(220.dp)
|
||||
) {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (user.hidden) {
|
||||
ItemAction(stringResource(R.string.user_unhide), Icons.Outlined.LockOpen, onClick = {
|
||||
showDropdownMenu = false
|
||||
showMenu.value = false
|
||||
unhideUser(user)
|
||||
})
|
||||
} else {
|
||||
if (visibleUsersCount > 1) {
|
||||
if (visibleUsersCount > 1 && prefPerformLA) {
|
||||
ItemAction(stringResource(R.string.user_hide), Icons.Outlined.Lock, onClick = {
|
||||
showDropdownMenu = false
|
||||
showMenu.value = false
|
||||
showHiddenProfile(user)
|
||||
})
|
||||
}
|
||||
if (user.showNtfs) {
|
||||
ItemAction(stringResource(R.string.user_mute), Icons.Outlined.Notifications, onClick = {
|
||||
showDropdownMenu = false
|
||||
ItemAction(stringResource(R.string.user_mute), Icons.Outlined.NotificationsOff, onClick = {
|
||||
showMenu.value = false
|
||||
muteUser(user)
|
||||
})
|
||||
} else {
|
||||
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.NotificationsOff, onClick = {
|
||||
showDropdownMenu = false
|
||||
ItemAction(stringResource(R.string.user_unmute), Icons.Outlined.Notifications, onClick = {
|
||||
showMenu.value = false
|
||||
unmuteUser(user)
|
||||
})
|
||||
}
|
||||
}
|
||||
ItemAction(stringResource(R.string.delete_verb), Icons.Outlined.Delete, color = Color.Red, onClick = {
|
||||
removeUser(user)
|
||||
showDropdownMenu = false
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class UserProfileAction {
|
||||
DELETE,
|
||||
UNHIDE
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileActionView(action: UserProfileAction, user: User, doAction: (String) -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = DEFAULT_BOTTOM_PADDING),
|
||||
) {
|
||||
val actionPassword = rememberSaveable { mutableStateOf("") }
|
||||
val passwordValid by remember { derivedStateOf { actionPassword.value == actionPassword.value.trim() } }
|
||||
val actionEnabled by remember { derivedStateOf { actionPassword.value != "" && passwordValid && correctPassword(user, actionPassword.value) } }
|
||||
|
||||
@Composable fun ActionHeader(@StringRes title: Int) {
|
||||
AppBarTitle(stringResource(title))
|
||||
SectionView(padding = PaddingValues(start = 8.dp, end = DEFAULT_PADDING)) {
|
||||
UserProfileRow(user)
|
||||
}
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
@Composable fun PasswordAndAction(@StringRes label: Int, color: Color = MaterialTheme.colors.primary) {
|
||||
SectionView() {
|
||||
SectionItemView {
|
||||
PassphraseField(actionPassword, generalGetString(R.string.profile_password), isValid = { passwordValid }, showStrength = true)
|
||||
}
|
||||
SectionItemViewSpaceBetween({ doAction(actionPassword.value) }, disabled = !actionEnabled, minHeight = TextFieldDefaults.MinHeight) {
|
||||
Text(generalGetString(label), color = if (actionEnabled) color else HighOrLowlight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (action) {
|
||||
UserProfileAction.DELETE -> {
|
||||
ActionHeader(R.string.delete_profile)
|
||||
PasswordAndAction(R.string.delete_chat_profile, color = Color.Red)
|
||||
if (actionEnabled) {
|
||||
SectionTextFooter(stringResource(R.string.users_delete_all_chats_deleted))
|
||||
}
|
||||
}
|
||||
UserProfileAction.UNHIDE -> {
|
||||
ActionHeader(R.string.unhide_profile)
|
||||
PasswordAndAction(R.string.unhide_chat_profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List<User> {
|
||||
val s = searchTextOrPassword.trim()
|
||||
val lower = s.lowercase()
|
||||
return m.users.filter { u ->
|
||||
if ((u.user.activeUser || u.user.viewPwdHash == null) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
|
||||
if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) {
|
||||
true
|
||||
} else if (u.user.viewPwdHash != null) {
|
||||
s != "" && chatPasswordHash(s, u.user.viewPwdHash.salt) == u.user.viewPwdHash.hash
|
||||
} else {
|
||||
false
|
||||
correctPassword(u.user, s)
|
||||
}
|
||||
}.map { it.user }
|
||||
}
|
||||
|
||||
private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size
|
||||
|
||||
private fun correctPassword(user: User, pwd: String): Boolean {
|
||||
val ph = user.viewPwdHash
|
||||
return ph != null && pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
|
||||
}
|
||||
|
||||
private fun userViewPassword(user: User, searchTextOrPassword: String): String? =
|
||||
if (user.activeUser || !user.hidden) null else searchTextOrPassword
|
||||
if (user.hidden) searchTextOrPassword.trim() else null
|
||||
|
||||
private fun passwordEntryRequired(user: User, searchTextOrPassword: String): Boolean =
|
||||
user.hidden && user.activeUser && !correctPassword(user, searchTextOrPassword.trim())
|
||||
|
||||
private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, searchTextOrPassword: String) {
|
||||
if (users.size < 2) return
|
||||
|
||||
withBGApi {
|
||||
suspend fun deleteUser(user: User) {
|
||||
m.controller.apiDeleteUser(user.userId, delSMPQueues, userViewPassword(user, searchTextOrPassword))
|
||||
m.removeUser(user)
|
||||
}
|
||||
try {
|
||||
if (user.activeUser) {
|
||||
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
|
||||
if (newActive != null) {
|
||||
m.controller.changeActiveUser_(newActive.userId, null)
|
||||
deleteUser(user.copy(activeUser = false))
|
||||
if (passwordEntryRequired(user, searchTextOrPassword)) {
|
||||
ModalManager.shared.showModalCloseable(true) { close ->
|
||||
ProfileActionView(UserProfileAction.DELETE, user) { pwd ->
|
||||
withBGApi {
|
||||
doRemoveUser(m, user, users, delSMPQueues, pwd)
|
||||
close()
|
||||
}
|
||||
} else {
|
||||
deleteUser(user)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
|
||||
}
|
||||
} else {
|
||||
withBGApi { doRemoveUser(m, user, users, delSMPQueues, userViewPassword(user, searchTextOrPassword.trim())) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun setUserPrivacy(m: ChatModel, onSuccess: (() -> Unit)? = null, api: suspend () -> User) {
|
||||
withBGApi {
|
||||
try {
|
||||
m.updateUser(api())
|
||||
onSuccess?.invoke()
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.error_updating_user_privacy),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
private suspend fun doRemoveUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, viewPwd: String?) {
|
||||
if (users.size < 2) return
|
||||
|
||||
suspend fun deleteUser(user: User) {
|
||||
m.controller.apiDeleteUser(user.userId, delSMPQueues, viewPwd)
|
||||
m.removeUser(user)
|
||||
}
|
||||
try {
|
||||
if (user.activeUser) {
|
||||
val newActive = users.firstOrNull { u -> !u.activeUser && !u.hidden }
|
||||
if (newActive != null) {
|
||||
m.controller.changeActiveUser_(newActive.userId, null)
|
||||
deleteUser(user.copy(activeUser = false))
|
||||
}
|
||||
} else {
|
||||
deleteUser(user)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_deleting_user), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setUserPrivacy(m: ChatModel, onSuccess: (() -> Unit)? = null, api: suspend () -> User) {
|
||||
try {
|
||||
m.updateUser(api())
|
||||
onSuccess?.invoke()
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(R.string.error_updating_user_privacy),
|
||||
text = e.stackTraceToString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,4 +387,4 @@ private fun showMuteProfileAlert(showMuteProfileAlert: SharedPreference<Boolean>
|
||||
showMuteProfileAlert.set(false)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ fun VersionInfoView(info: CoreVersionInfo) {
|
||||
Text(String.format(stringResource(R.string.app_version_name), BuildConfig.VERSION_NAME))
|
||||
Text(String.format(stringResource(R.string.app_version_code), BuildConfig.VERSION_CODE))
|
||||
Text(String.format(stringResource(R.string.core_version), info.version))
|
||||
Text(String.format(stringResource(R.string.core_build_timestamp), info.buildTimestamp))
|
||||
val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit
|
||||
Text(String.format(stringResource(R.string.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M181.5,835q-22.97,0 -40.23,-17.27Q124,800.47 124,777.5L124,182q0,-22.97 17.27,-40.23Q158.53,124.5 181.5,124.5L561,124.5q12.25,0 20.63,8.46T590,153.18q0,12.32 -8.38,20.58T561,182L181.5,182v595.5L777,777.5L777,399q0,-12.25 8.43,-20.63 8.43,-8.38 20.5,-8.38 12.07,0 20.33,8.38T834.5,399v378.5q0,22.97 -17.27,40.23Q799.97,835 777,835L181.5,835ZM727.83,341.5q-12.32,0 -20.58,-8.38T699,312.5L699,261h-51.5q-12.25,0 -20.63,-8.43 -8.38,-8.43 -8.38,-20.5 0,-12.07 8.38,-20.33t20.63,-8.25L699,203.5v-52q0,-11.68 8.43,-20.09 8.43,-8.41 20.5,-8.41 12.07,0 20.33,8.41 8.25,8.41 8.25,20.09v52h52q11.68,0 20.09,8.46Q837,220.43 837,232.17q0,12.32 -8.41,20.58Q820.17,261 808.5,261h-52v51.5q0,12.25 -8.46,20.63t-20.21,8.38ZM273,676.5h413.17q9.82,0 13.82,-7.75T698,653L585.58,503.6q-4.7,-6.1 -11.46,-6.1 -6.76,0 -11.61,6L448,653.5l-81.46,-106.39q-4.69,-5.61 -11.5,-5.61 -6.81,0 -11.72,5.58L261.57,653.02q-5.07,7.98 -0.95,15.73T273,676.5ZM181.5,399v378.5L181.5,182v217Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M480.03,697Q551,697 602.75,645.94t51.75,-122.5Q654.5,452 602.72,400q-51.78,-52 -122.75,-52Q408,348 356.75,400.06t-51.25,123.5Q305.5,595 356.78,646q51.28,51 123.25,51ZM435.5,482.5l31,-71.42Q470,402.5 480,403t13.5,9.08l30.16,70.42 65.92,27.58q9.42,4.16 9.42,13.79t-9.42,13.05L523.5,564l-30,69.92Q490,643 480,643.5t-13.5,-8.58L435.5,564l-65.58,-27.08Q360,533.5 360,523.87t9.92,-13.79L435.5,482.5ZM142.5,835.5q-22.97,0 -40.23,-17.27Q85,800.97 85,778L85,268.5q0,-21.97 17.27,-39.73Q119.53,211 142.5,211h147l54.91,-66.5q7.59,-10 19.11,-15 11.52,-5 24.98,-5h183q13.47,0 24.98,5 11.52,5 19.52,15l54.5,66.5h147q21.97,0 39.73,17.77Q875,246.53 875,268.5L875,778q0,22.97 -17.77,40.23Q839.47,835.5 817.5,835.5h-675ZM817.5,778L817.5,268.5h-675L142.5,778h675ZM480,522.5Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M255.69,838.85q-22.26,0 -38.46,-16.2 -16.2,-16.2 -16.2,-38.44L201.04,175.79q0,-22.24 16.2,-38.44 16.2,-16.2 38.61,-16.2h333.61l169.5,169v493.89q0,22.41 -16.2,38.61 -16.2,16.2 -38.46,16.2L255.69,838.85ZM574.42,304.23L574.42,151.35L255.85,151.35q-9.23,0 -16.92,7.69 -7.69,7.69 -7.69,16.92v608.08q0,9.23 7.69,16.92 7.69,7.69 16.92,7.69h448.31q9.23,0 16.92,-7.69 7.69,-7.69 7.69,-16.92L728.77,304.23L574.42,304.23ZM231.23,151.35v152.88,-152.88 657.31,-657.31Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M451.5,595v99q0,12.25 8.43,20.63 8.43,8.38 20.5,8.38 12.07,0 20.33,-8.38T509,694v-99h100.5q11.68,0 20.09,-8.43 8.41,-8.43 8.41,-20.5 0,-12.07 -8.41,-20.33 -8.41,-8.25 -20.09,-8.25L509,537.5L509,437q0,-11.68 -8.46,-20.09 -8.46,-8.41 -20.21,-8.41 -12.32,0 -20.58,8.41 -8.25,8.41 -8.25,20.09v100.5h-100q-12.25,0 -20.63,8.46t-8.38,20.21q0,12.32 8.38,20.58T351.5,595h100ZM222,875q-22.97,0 -40.23,-17.27Q164.5,840.47 164.5,817.5v-675q0,-22.97 17.27,-40.23Q199.03,85 222,85h335q11.91,0 22.71,4.75 10.79,4.75 18.91,12.34l179.26,179.31Q786,289.5 790.75,300.29q4.75,10.8 4.75,22.71v494.5q0,22.97 -17.27,40.23Q760.97,875 738,875L222,875ZM552.5,296L552.5,142.5L222,142.5v675h516L738,325L581.5,325q-12.25,0 -20.63,-8.38T552.5,296ZM222,142.5L222,325 222,142.5v675,-675Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m436.5,615.5 l175.5,-114q13,-8.5 13,-23.51T612,454L436.5,340q-14.5,-9.5 -29,-1.27Q393,346.96 393,364v227.5q0,17.76 14.5,25.88 14.5,8.12 29,-1.88ZM142.5,795.5q-22.97,0 -40.23,-17.27Q85,760.97 85,738L85,222q0,-22.97 17.27,-40.23Q119.53,164.5 142.5,164.5h675q23.72,0 40.61,17.27Q875,199.03 875,222v516q0,22.97 -16.89,40.23Q841.22,795.5 817.5,795.5h-675ZM142.5,738L142.5,222v516ZM142.5,738h675L817.5,222h-675v516Z"/>
|
||||
</vector>
|
||||
@@ -26,4 +26,25 @@
|
||||
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
|
||||
<string name="switch_receiving_address_desc">هذه الميزة تجريبية! ستعمل فقط إذا كان لدى العميل الآخر الإصدار 4.2 مثبتًا. يجب أن ترى الرسالة في المحادثة بمجرد اكتمال تغيير العنوان - يرجى التحقق من أنه لا يزال بإمكانك تلقي الرسائل من جهة الاتصال هذه (أو عضو المجموعة).</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">هذا الارتباط ليس ارتباط اتصال صالح!</string>
|
||||
<string name="allow_verb">يسمح</string>
|
||||
<string name="smp_servers_preset_add">أضف خوادم محددة مسبقًا</string>
|
||||
<string name="smp_servers_add_to_another_device">أضف إلى جهاز آخر</string>
|
||||
<string name="users_delete_all_chats_deleted">سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا!</string>
|
||||
<string name="network_enable_socks_info">الوصول إلى الخوادم عبر بروكسي SOCKS على المنفذ 9050؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار.</string>
|
||||
<string name="accept_requests">قبول طلبات</string>
|
||||
<string name="smp_servers_add">إضافة خادم …</string>
|
||||
<string name="network_settings">إعدادات الشبكة المتقدمة</string>
|
||||
<string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string>
|
||||
<string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة الاتصال الخاصة بك بذلك.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك.</string>
|
||||
<string name="group_member_role_admin">مسؤل</string>
|
||||
<string name="users_add">إضافة ملف التعريف</string>
|
||||
<string name="allow_direct_messages">السماح بإرسال رسائل مباشرة إلى الأعضاء.</string>
|
||||
<string name="accept_contact_incognito_button">قبول التخفي</string>
|
||||
<string name="button_add_welcome_message">أضف رسالة ترحيب</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">أضف الخوادم عن طريق مسح رموز QR.</string>
|
||||
<string name="v4_2_group_links_desc">يمكن للمسؤولين إنشاء روابط للانضمام إلى المجموعات.</string>
|
||||
<string name="accept_connection_request__question">قبول طلب الاتصال؟</string>
|
||||
<string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string>
|
||||
<string name="callstatus_accepted">مكالمة مقبولة</string>
|
||||
</resources>
|
||||
@@ -11,7 +11,7 @@
|
||||
<string name="smp_servers_add">Přidat server…</string>
|
||||
<string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu 9050\? Před povolením této možnosti musí být spuštěna proxy.</string>
|
||||
<string name="accept_feature">Přijmout</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Umožněte svým kontaktům odesílat mizící zprávy.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Povolte svým kontaktům odesílat mizící zprávy.</string>
|
||||
<string name="about_simplex_chat">O <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="smp_servers_add_to_another_device">Přidat do jiného zařízení</string>
|
||||
<string name="accept_requests">Přijímat žádosti</string>
|
||||
@@ -93,7 +93,7 @@
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">Váš profil bude odeslán kontaktu, od kterého jste obdrželi tento odkaz.</string>
|
||||
<string name="server_connected">připojeno</string>
|
||||
<string name="server_error">chyba</string>
|
||||
<string name="server_connecting">připojení</string>
|
||||
<string name="server_connecting">připojování</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Pokus o připojení k serveru používanému pro příjem zpráv od tohoto kontaktu.</string>
|
||||
<string name="deleted_description">smazáno</string>
|
||||
<string name="invalid_chat">neplatný chat</string>
|
||||
@@ -101,7 +101,7 @@
|
||||
<string name="connection_local_display_name">spojení <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="display_name_connection_established">spojení navázáno</string>
|
||||
<string name="display_name_invited_to_connect">pozvánka k připojení</string>
|
||||
<string name="display_name_connecting">připojení…</string>
|
||||
<string name="display_name_connecting">připojování…</string>
|
||||
<string name="description_you_shared_one_time_link">sdíleli jste jednorázový odkaz</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">sdíleli jste jednorázový odkaz inkognito</string>
|
||||
<string name="description_via_group_link">prostřednictvím skupinového odkazu</string>
|
||||
@@ -186,9 +186,11 @@
|
||||
<string name="to_share_with_your_contact">(sdílet s kontaktem)</string>
|
||||
<string name="only_stored_on_members_devices">(uloženo pouze členy skupiny)</string>
|
||||
<string name="toast_permission_denied">Oprávnění zamítnuto!</string>
|
||||
<string name="use_camera_button">Použít fotoaparát</string>
|
||||
<string name="use_camera_button">Fotoaparát</string>
|
||||
<string name="from_gallery_button">Z Galerie</string>
|
||||
<string name="choose_file">Vybrat soubor</string>
|
||||
<string name="choose_file">Soubor</string>
|
||||
<string name="gallery_image_button">Obrázek</string>
|
||||
<string name="gallery_video_button">Video</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pro zahájení nové konverzace</string>
|
||||
<string name="chat_help_tap_button">Klepněte na tlačítko</string>
|
||||
<string name="above_then_preposition_continuation">potom:</string>
|
||||
@@ -277,7 +279,7 @@
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_icon">IKONA APLIKACE</string>
|
||||
<string name="settings_section_title_themes">TÉMATA</string>
|
||||
<string name="settings_section_title_messages">ZPRÁVY</string>
|
||||
<string name="settings_section_title_messages">ZPRÁVY A SOUBORY</string>
|
||||
<string name="settings_section_title_calls">VOLÁNÍ</string>
|
||||
<string name="export_database">Export databáze</string>
|
||||
<string name="import_database">Import databáze</string>
|
||||
@@ -655,14 +657,11 @@
|
||||
<string name="app_version_title">Verze aplikace</string>
|
||||
<string name="app_version_name">Verze aplikace: v%s</string>
|
||||
<string name="core_version">Verze jádra: v%s</string>
|
||||
<string name="core_build_timestamp">Jádro sestaveno: %s</string>
|
||||
<string name="delete_address__question">Smazat adresu\?</string>
|
||||
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
|
||||
<string name="contact_requests">Žádosti o kontakt</string>
|
||||
<string name="display_name__field">Zobrazované jméno:</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty.
|
||||
\n
|
||||
\n<xliff:g id="appName">SimpleX</xliff:g> servery váš profil vidět nemohou.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery váš profil vidět nemohou.</string>
|
||||
<string name="save_preferences_question">Uložit předvolby\?</string>
|
||||
<string name="save_and_notify_contact">Uložit a upozornit kontakt</string>
|
||||
<string name="save_and_notify_contacts">Uložit a upozornit kontakty</string>
|
||||
@@ -685,7 +684,7 @@
|
||||
<string name="callstate_waiting_for_confirmation">čekání na potvrzení…</string>
|
||||
<string name="callstate_received_answer">obdržel odpověď…</string>
|
||||
<string name="callstate_received_confirmation">obdržel potvrzení…</string>
|
||||
<string name="callstate_connecting">připojení…</string>
|
||||
<string name="callstate_connecting">připojování…</string>
|
||||
<string name="privacy_redefined">Nové vymezení soukromí</string>
|
||||
<string name="first_platform_without_user_ids">1. platforma bez jakýchkoliv uživatelských identifikátorů – soukromá již od návrhu.</string>
|
||||
<string name="immune_to_spam_and_abuse">Odolná vůči spamu a zneužití</string>
|
||||
@@ -726,11 +725,9 @@
|
||||
<string name="answer_call">Přijmout hovor</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> přeskočená zpráva (zprávy)</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Může se to stát, když:
|
||||
\n1. Zprávy na serveru vyprší, pokud nebyly přijaty po dobu 30 dnů,
|
||||
\n2. Server, který používáte pro příjem zpráv od tohoto kontaktu, byl aktualizován a restartován.
|
||||
\n3. Spojení je narušeno.
|
||||
\nPřipojte se k vývojářům prostřednictvím Nastavení, abyste mohli dostávat aktualizace o serverech.
|
||||
\nBudeme přidávat redundantní servery, abychom zabránili ztrátě zpráv.</string>
|
||||
\n1. Zprávy vypršely v odesílajícím klientovi po 2 dnech nebo na serveru po 30 dnech.
|
||||
\n2. Dešifrování zprávy se nezdařilo, protože vy nebo váš kontakt jste použili starou zálohu databáze.
|
||||
\n3. Spojení je kompromitováno.</string>
|
||||
<string name="settings_section_title_you">VY</string>
|
||||
<string name="settings_section_title_support">PODPOŘIT SIMPLEX CHAT</string>
|
||||
<string name="settings_section_title_develop">VÝVOJ</string>
|
||||
@@ -854,7 +851,7 @@
|
||||
<string name="skip_inviting_button">Přeskočit pozvání členů</string>
|
||||
<string name="select_contacts">Vybrat kontakty</string>
|
||||
<string name="icon_descr_contact_checked">Zkontrolované kontakty</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> kontakt(y) vybrán(y)</string>
|
||||
<string name="num_contacts_selected">%d kontakt(y) vybrán(y)</string>
|
||||
<string name="button_add_members">Pozvat členy</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBERS</string>
|
||||
<string name="group_info_member_you">vy: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
@@ -908,7 +905,7 @@
|
||||
<string name="theme">Téma</string>
|
||||
<string name="chat_preferences_contact_allows">Kontakt povolil</string>
|
||||
<string name="chat_preferences_on">zapnuto</string>
|
||||
<string name="chat_preferences_off">vypnuto</string>
|
||||
<string name="chat_preferences_off">vypnout</string>
|
||||
<string name="chat_preferences">Chat předvolby</string>
|
||||
<string name="contact_preferences">Předvolby kontaktu</string>
|
||||
<string name="group_preferences">Předvolby skupiny</string>
|
||||
@@ -963,7 +960,6 @@
|
||||
<string name="your_contact_address">Vaše adresa</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Váš chat profil bude odeslán
|
||||
\nvašemu kontaktu</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vaše chat profily jsou uloženy lokálně, pouze ve vašem zařízení.</string>
|
||||
<string name="your_chats">Vaše konverzace</string>
|
||||
<string name="paste_connection_link_below_to_connect">Do níže uvedeného pole vložte odkaz, který jste obdrželi pro spojení s kontaktem.</string>
|
||||
<string name="share_invitation_link">Sdílet pozvánku</string>
|
||||
@@ -988,7 +984,7 @@
|
||||
<string name="v4_6_chinese_spanish_interface">Čínské a Španělské rozhranní</string>
|
||||
<string name="v4_6_audio_video_calls">Hlasové a video hovory</string>
|
||||
<string name="confirm_password">Potvrdit heslo</string>
|
||||
<string name="enter_password_to_show">Pro zobrazení zadejte heslo výše!</string>
|
||||
<string name="enter_password_to_show">Zadejte heslo do hledání</string>
|
||||
<string name="v4_6_reduced_battery_usage">Další snížení spotřeby baterie</string>
|
||||
<string name="error_saving_user_password">Chyba ukládání hesla uživatele</string>
|
||||
<string name="error_updating_user_privacy">Chyba aktualizace soukromí uživatele</string>
|
||||
@@ -1018,4 +1014,129 @@
|
||||
<string name="button_welcome_message">Uvítací zpráva</string>
|
||||
<string name="group_welcome_title">Uvítací zpráva</string>
|
||||
<string name="user_unmute">Zrušit ztlumení</string>
|
||||
<string name="to_reveal_profile_enter_password">Chcete-li odhalit svůj skrytý profil, zadejte celé heslo do vyhledávacího pole na stránce Chat profily.</string>
|
||||
<string name="should_be_at_least_one_visible_profile">Měl by tam být alespoň jeden viditelný uživatelský profil.</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Stále budete přijímat volání a upozornění od umlčených profilů pokud budou aktivní.</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Můžete skrýt nebo ztlumit uživatelský profil - Podržte pro menu.</string>
|
||||
<string name="user_unhide">Odkrýt</string>
|
||||
<string name="database_upgrade">Aktualizace databáze</string>
|
||||
<string name="database_downgrade_warning">Upozornění: můžete ztratit nějaká data!</string>
|
||||
<string name="confirm_database_upgrades">Potvrdit aktualizaci databáze</string>
|
||||
<string name="database_downgrade">Původní databáze</string>
|
||||
<string name="mtr_error_no_down_migration">verze databáze je novější než aplikace, ale žádný přechod dolů pro: %s</string>
|
||||
<string name="downgrade_and_open_chat">Snížit a otevřít chat</string>
|
||||
<string name="database_migrations">Migrací: %s</string>
|
||||
<string name="mtr_error_different">různé migrace v aplikaci/databázi: %s / %s</string>
|
||||
<string name="incompatible_database_version">Nekompatibilní verze databáze</string>
|
||||
<string name="invalid_migration_confirmation">Neplatné potvrzení migrace</string>
|
||||
<string name="upgrade_and_open_chat">Zvýšit a otevřít chat</string>
|
||||
<string name="hide_dev_options">Skrýt:</string>
|
||||
<string name="show_developer_options">Zobrazit možnosti vývojáře</string>
|
||||
<string name="settings_section_title_experimenta">POKUSNÝ</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">Obrázek bude přijat, až kontakt dokončí jeho nahrání.</string>
|
||||
<string name="show_dev_options">Zobrazit:</string>
|
||||
<string name="developer_options">ID databáze a možnost Izolace přenosu.</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">Soubor bude přijat, jakmile váš kontakt dokončí nahrávání.</string>
|
||||
<string name="file_transfer_will_be_cancelled_warning">Přenos souboru bude zrušen. Pokud probíhá, bude zastaven.</string>
|
||||
<string name="delete_chat_profile">Smazat chat profil</string>
|
||||
<string name="delete_profile">Smazat profil</string>
|
||||
<string name="profile_password">Heslo profilu</string>
|
||||
<string name="unhide_chat_profile">Odkrýt chat profil</string>
|
||||
<string name="unhide_profile">Odkrýt profil</string>
|
||||
<string name="cancel_file__question">Zrušit přenos souboru\?</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Žádost o přijetí videa</string>
|
||||
<string name="videos_limit_desc">Současně lze odeslat pouze 10 videí</string>
|
||||
<string name="videos_limit_title">Příliš mnoho videí!</string>
|
||||
<string name="video_descr">Video</string>
|
||||
<string name="icon_descr_waiting_for_video">Čekám na video</string>
|
||||
<string name="icon_descr_video_snd_complete">Video odesláno</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">Video bude přijato, až kontakt dokončí jeho nahrávání.</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">Video obdržíte, až bude váš kontakt online, vyčkejte prosím nebo zkontrolujte později!</string>
|
||||
<string name="waiting_for_video">Čekám na video</string>
|
||||
<string name="error_loading_smp_servers">Chyba načítání serverů SMP</string>
|
||||
<string name="error_loading_xftp_servers">Chyba načítání serverů XFTP</string>
|
||||
<string name="error_saving_xftp_servers">Chyba ukládání XFTP serverů</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Ujistěte se, že adresy XFTP serverů jsou ve správném formátu s oddělenými řádky a nejsou duplicitní.</string>
|
||||
<string name="error_xftp_test_server_auth">Server vyžaduje autorizaci pro nahrávání, zkontrolujte heslo</string>
|
||||
<string name="smp_server_test_compare_file">Porovnat soubor</string>
|
||||
<string name="smp_server_test_create_file">Vytvořit soubor</string>
|
||||
<string name="smp_server_test_delete_file">Smazat soubor</string>
|
||||
<string name="smp_server_test_download_file">Stáhnout soubor</string>
|
||||
<string name="smp_server_test_upload_file">Nahrát soubor</string>
|
||||
<string name="xftp_servers">XFTP servery</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Použít SOCKS proxy</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="network_proxy_port">port %d</string>
|
||||
<string name="network_socks_proxy_settings">Nastavení SOCKS proxy</string>
|
||||
<string name="la_lock_mode_passcode">Zadat heslo</string>
|
||||
<string name="la_lock_mode">SimpleX zámek</string>
|
||||
<string name="la_lock_mode_system">Ověření systému</string>
|
||||
<string name="la_authenticate">Ověřit</string>
|
||||
<string name="la_auth_failed">Ověření selhalo</string>
|
||||
<string name="la_change_app_passcode">Změnit heslo</string>
|
||||
<string name="la_current_app_passcode">Aktuální heslo</string>
|
||||
<string name="la_enter_app_passcode">Zadat heslo</string>
|
||||
<string name="la_no_app_password">Bez hesla aplikace</string>
|
||||
<string name="la_could_not_be_verified">Nemohli jste být ověřeni; Zkuste to prosím znovu.</string>
|
||||
<string name="la_minutes">%d minut</string>
|
||||
<string name="la_seconds">%d vteřin</string>
|
||||
<string name="la_immediately">Ihned</string>
|
||||
<string name="la_please_remember_to_store_password">Zapamatujte si jej nebo bezpečně uložte - neexistuje způsob, jak obnovit ztracené heslo!</string>
|
||||
<string name="lock_not_enabled">Zámek SimpleX není povolen!</string>
|
||||
<string name="you_can_turn_on_lock">Zámek SimpleX můžete zapnout v Nastavení.</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Nastavte <i>Použít .onion hostitele</i> na Ne, pokud je SOCKS proxy nepodporuje.</string>
|
||||
<string name="your_XFTP_servers">Vaše XFTP servery</string>
|
||||
<string name="enable_lock">Povolit zámek</string>
|
||||
<string name="lock_after">Zamknout po</string>
|
||||
<string name="lock_mode">Režim zámku</string>
|
||||
<string name="authentication_cancelled">Ověření zrušeno</string>
|
||||
<string name="confirm_passcode">Potvrdit heslo</string>
|
||||
<string name="incorrect_passcode">Nesprávné heslo</string>
|
||||
<string name="new_passcode">Nové heslo</string>
|
||||
<string name="submit_passcode">Odeslat</string>
|
||||
<string name="la_mode_system">Systém</string>
|
||||
<string name="change_lock_mode">Změnit zamykání</string>
|
||||
<string name="la_mode_passcode">Heslo</string>
|
||||
<string name="passcode_changed">Heslo změněno!</string>
|
||||
<string name="passcode_not_changed">Heslo nezměněno!</string>
|
||||
<string name="passcode_set">Heslo nastaveno!</string>
|
||||
<string name="decryption_error">Chyba dešifrování</string>
|
||||
<string name="alert_title_msg_bad_hash">Špatný hash zprávy</string>
|
||||
<string name="alert_title_msg_bad_id">Špatné ID zprávy</string>
|
||||
<string name="alert_text_msg_bad_hash">Hash předchozí zprávy se liší.</string>
|
||||
<string name="alert_text_msg_bad_id">ID další zprávy je nesprávné (menší nebo rovno předchozí).
|
||||
\nMůže se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitováno.</string>
|
||||
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> zprávy se nepodařilo dešifrovat.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> zprývy přeskočeny.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Nahlaste to prosím vývojářům.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Tato chyba je pro toto připojení trvalá, připojte se znovu.</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Může se to stát, když vy nebo vaše připojení použijete starou zálohu databáze.</string>
|
||||
<string name="no_spaces">Žádné mezery!</string>
|
||||
<string name="revoke_file__message">Soubor bude smazán ze serverů.</string>
|
||||
<string name="stop_rcv_file__message">Příjem souboru bude zastaven.</string>
|
||||
<string name="allow_calls_only_if">Povolte hovory, pouze pokud je váš kontakt povolí.</string>
|
||||
<string name="allow_your_contacts_to_call">Povolte svým kontaktům vám volat.</string>
|
||||
<string name="audio_video_calls">Audio/video hovory</string>
|
||||
<string name="available_in_v51">"
|
||||
\nDostupné ve verzi 5.1"</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Volat můžete vy i váš kontakt.</string>
|
||||
<string name="only_you_can_make_calls">Volat můžete pouze vy.</string>
|
||||
<string name="prohibit_calls">Zákaz audio/video hovorů.</string>
|
||||
<string name="calls_prohibited_with_this_contact">Audio/video hovory jsou zakázány.</string>
|
||||
<string name="only_your_contact_can_make_calls">Volat může pouze váš kontakt.</string>
|
||||
<string name="v5_0_app_passcode">Heslo aplikace</string>
|
||||
<string name="v5_0_large_files_support_descr">Rychle a bez čekání, než bude odesílatel online!</string>
|
||||
<string name="v5_0_polish_interface">Polské rozhraní</string>
|
||||
<string name="v5_0_app_passcode_descr">Nastavte jej namísto ověřování systému.</string>
|
||||
<string name="v5_0_large_files_support">Videa a soubory až do velikosti 1 gb</string>
|
||||
<string name="v5_0_polish_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
|
||||
<string name="revoke_file__title">Odvolat soubor\?</string>
|
||||
<string name="revoke_file__confirm">Odvolat</string>
|
||||
<string name="revoke_file__action">Odvolat soubor</string>
|
||||
<string name="stop_snd_file__message">Odesílání souboru bude zastaveno.</string>
|
||||
<string name="stop_file__confirm">Zastavit</string>
|
||||
<string name="stop_file__action">Zastavit soubor</string>
|
||||
<string name="stop_snd_file__title">Zastavit odesílání souboru\?</string>
|
||||
<string name="stop_rcv_file__title">Zastavit příjem souboru\?</string>
|
||||
</resources>
|
||||
@@ -180,7 +180,7 @@
|
||||
<string name="you_have_no_chats">Sie haben keine Chats</string>
|
||||
<!-- ShareListView.kt -->
|
||||
<string name="share_message">Nachricht teilen…</string>
|
||||
<string name="share_image">Bild teilen…</string>
|
||||
<string name="share_image">Medien teilen…</string>
|
||||
<string name="share_file">Datei teilen…</string>
|
||||
<!-- ComposeView.kt, helpers -->
|
||||
<string name="attach">Anhängen</string>
|
||||
@@ -254,9 +254,11 @@
|
||||
<string name="only_stored_on_members_devices">(Wird nur von Gruppenmitgliedern gespeichert)</string>
|
||||
<!-- GetImageView -->
|
||||
<string name="toast_permission_denied">Berechtigung verweigert!</string>
|
||||
<string name="use_camera_button">Kamera\nverwenden</string>
|
||||
<string name="use_camera_button">Kamera</string>
|
||||
<string name="from_gallery_button">Aus dem\nFotoalbum</string>
|
||||
<string name="choose_file">Datei\nauswählen</string>
|
||||
<string name="choose_file">Datei</string>
|
||||
<string name="gallery_image_button">Bild</string>
|
||||
<string name="gallery_video_button">Video</string>
|
||||
<!-- help - ChatHelpView.kt -->
|
||||
<string name="thank_you_for_installing_simplex">Danke, dass Sie <xliff:g id="appNameFull">SimpleX Chat</xliff:g> installiert haben!</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Sie können sich <font color="#0088ff">mit <xliff:g id="appNameFull">SimpleX Chat</xliff:g> Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.</string>
|
||||
@@ -428,7 +430,7 @@
|
||||
<string name="display_name__field">Angezeigter Name:</string>
|
||||
<string name="full_name__field">"Vollständiger Name:</string>
|
||||
<string name="your_current_profile">Mein aktuelles Chat-Profil</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\n\n<xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt. <xliff:g id="appName">SimpleX</xliff:g>-Server können Ihr Profil nicht sehen.</string>
|
||||
<string name="edit_image">Bild bearbeiten</string>
|
||||
<string name="delete_image">Bild löschen</string>
|
||||
<string name="save_preferences_question">Präferenzen speichern?</string>
|
||||
@@ -555,7 +557,10 @@
|
||||
<string name="integrity_msg_bad_id">Falsche Nachrichten-ID</string>
|
||||
<string name="integrity_msg_duplicate">Doppelte Nachricht</string>
|
||||
<string name="alert_title_skipped_messages">Übersprungene Nachrichten</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:\n1. Die Nachrichten verfallen auf dem Server, wenn sie 30 Tage lang nicht empfangen wurden.\n2. Der Server, den Sie zum Empfangen der Nachrichten von diesem Kontakt verwenden, wurde aktualisiert und neu gestartet.\n3. Die Verbindung ist kompromittiert.\nBitte nehmen Sie über die Einstellungen Verbindung mit den Entwicklern auf, um Updates zu den Servern zu erhalten.\nWir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu verhindern.</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Dies kann unter folgenden Umständen passieren:
|
||||
\n1. Die Nachrichten verfallen auf dem sendenden Client-System nach 2 Tagen oder auf dem Server nach 30 Tagen.
|
||||
\n2. Die Nachrichten-Entschlüsselung ist fehlgeschlagen, da von Ihnen oder Ihrem Kontakt ein altes Datenbank-Backup genutzt wurde.
|
||||
\n3. Die Verbindung wurde kompromittiert.</string>
|
||||
<!-- Privacy settings -->
|
||||
<string name="privacy_and_security">Datenschutz & Sicherheit</string>
|
||||
<string name="your_privacy">Meine Privatsphäre</string>
|
||||
@@ -576,7 +581,7 @@
|
||||
<string name="settings_section_title_socks">SOCKS-PROXY</string>
|
||||
<string name="settings_section_title_icon">APP ICON</string>
|
||||
<string name="settings_section_title_themes">DESIGN</string>
|
||||
<string name="settings_section_title_messages">MESSAGES</string>
|
||||
<string name="settings_section_title_messages">NACHRICHTEN und DATEIEN</string>
|
||||
<string name="settings_section_title_calls">CALLS</string>
|
||||
<string name="settings_section_title_incognito">Inkognito Modus</string>
|
||||
<!-- DatabaseView.kt -->
|
||||
@@ -765,7 +770,7 @@
|
||||
<string name="select_contacts">Kontakte auswählen</string>
|
||||
<string name="icon_descr_contact_checked">Kontakt geprüft</string>
|
||||
<string name="clear_contacts_selection_button">Löschen</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> Kontakt(e) ausgewählt</string>
|
||||
<string name="num_contacts_selected">%d Kontakt(e) ausgewählt</string>
|
||||
<string name="no_contacts_selected">Keine Kontakte ausgewählt</string>
|
||||
<string name="invite_prohibited">Kontakt kann nicht eingeladen werden!</string>
|
||||
<string name="invite_prohibited_description">Sie versuchen, einen Kontakt, mit dem Sie ein Inkognito-Profil geteilt haben, in die Gruppe einzuladen, in der Sie Ihr Hauptprofil verwenden.</string>
|
||||
@@ -1000,7 +1005,6 @@
|
||||
<string name="app_version_code">App Build: %s</string>
|
||||
<string name="app_version_title">App Version</string>
|
||||
<string name="app_version_name">App Version: v%s</string>
|
||||
<string name="core_build_timestamp">Core übersetzt am: %s</string>
|
||||
<string name="core_version">Core Version: v%s</string>
|
||||
<string name="users_add">Profil hinzufügen</string>
|
||||
<string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string>
|
||||
@@ -1023,7 +1027,6 @@
|
||||
<string name="users_delete_data_only">Nur lokale Profildaten</string>
|
||||
<string name="users_delete_with_connections">Profil und Serververbindungen</string>
|
||||
<string name="messages_section_description">Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil</string>
|
||||
<string name="your_chat_profiles_stored_locally">Ihre Chat-Profile werden nur lokal auf Ihrem Endgerät gespeichert</string>
|
||||
<string name="failed_to_create_user_duplicate_title">Doppelter Anzeigename!</string>
|
||||
<string name="failed_to_create_user_title">Fehler beim Erstellen des Profils!</string>
|
||||
<string name="failed_to_active_user_title">Fehler beim Umschalten des Profils!</string>
|
||||
@@ -1055,15 +1058,15 @@
|
||||
<string name="observer_cant_send_message_desc">Bitte kontaktieren Sie den Gruppen-Administrator.</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Diese Nachricht wird für alle Gruppenmitglieder gelöscht.</string>
|
||||
<string name="language_system">System</string>
|
||||
<string name="confirm_password">Bestätigen Sie das Passwort</string>
|
||||
<string name="confirm_password">Passwort bestätigen</string>
|
||||
<string name="cant_delete_user_profile">Das Benutzerprofil kann nicht gelöscht werden!</string>
|
||||
<string name="dont_show_again">Nicht nochmals anzeigen</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Chinesische und spanische Bedienoberfläche</string>
|
||||
<string name="v4_6_audio_video_calls">Audio- und Videoanrufe</string>
|
||||
<string name="button_add_welcome_message">Fügen Sie eine Begrüßungsmeldung hinzu</string>
|
||||
<string name="button_add_welcome_message">Begrüßungsmeldung hinzufügen</string>
|
||||
<string name="error_updating_user_privacy">Fehler beim Aktualisieren der Benutzer-Privatsphäre</string>
|
||||
<string name="smp_save_servers_question">Alle Server speichern\?</string>
|
||||
<string name="hide_profile">Verberge das Profil</string>
|
||||
<string name="hide_profile">Profil verbergen</string>
|
||||
<string name="password_to_show">Passwort anzeigen</string>
|
||||
<string name="save_profile_password">Profil-Passwort speichern</string>
|
||||
<string name="error_saving_user_password">Fehler beim Speichern des Benutzer-Passworts</string>
|
||||
@@ -1071,10 +1074,10 @@
|
||||
<string name="button_welcome_message">Begrüßungsmeldung</string>
|
||||
<string name="save_welcome_message_question">Begrüßungsmeldung speichern\?</string>
|
||||
<string name="user_unhide">Verbergen aufheben</string>
|
||||
<string name="enter_password_to_show">Geben Sie oben das Passwort für die Anzeige an!</string>
|
||||
<string name="make_profile_private">Erzeugen Sie ein privates Profil!</string>
|
||||
<string name="enter_password_to_show">Für die Anzeige das Passwort im Suchfeld eingeben</string>
|
||||
<string name="make_profile_private">Privates Profil erzeugen!</string>
|
||||
<string name="user_mute">Stummschalten</string>
|
||||
<string name="tap_to_activate_profile">Tippen Sie, um das Profil zu aktivieren.</string>
|
||||
<string name="tap_to_activate_profile">Tippen Sie auf das Profil um es zu aktivieren.</string>
|
||||
<string name="should_be_at_least_one_profile">Es muss mindestens ein Benutzer-Profil vorhanden sein.</string>
|
||||
<string name="should_be_at_least_one_visible_profile">Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.</string>
|
||||
<string name="user_unmute">Stummschaltung aufheben</string>
|
||||
@@ -1087,14 +1090,135 @@
|
||||
<string name="v4_6_group_welcome_message">Gruppen-Begrüßungsmeldung</string>
|
||||
<string name="v4_6_reduced_battery_usage">Weiter reduzierter Batterieverbrauch</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Legen Sie die Nachricht fest, die neuen Mitgliedern angezeigt werden soll!</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Definieren Sie eine Begrüßungsmeldung, die neuen Mitgliedern angezeigt wird!</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
|
||||
<string name="v4_6_group_moderation">Gruppenmoderation</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Verborgene Chat-Profile</string>
|
||||
<string name="user_hide">Verberge</string>
|
||||
<string name="save_and_update_group_profile">Sichern und aktualisieren des Gruppen-Profils</string>
|
||||
<string name="save_and_update_group_profile">Gruppen-Profil sichern und aktualisieren</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string>
|
||||
<string name="group_welcome_title">Begrüßungsmeldung</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten - für das Menü gedrückt halten.</string>
|
||||
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Meine Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string>
|
||||
<string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string>
|
||||
<string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string>
|
||||
<string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string>
|
||||
<string name="show_dev_options">Anzeigen:</string>
|
||||
<string name="show_developer_options">Entwickleroptionen anzeigen</string>
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTELL</string>
|
||||
<string name="database_upgrade">Datenbank-Aktualisierung</string>
|
||||
<string name="mtr_error_different">Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s</string>
|
||||
<string name="downgrade_and_open_chat">Datenbank herabstufen und den Chat öffnen</string>
|
||||
<string name="incompatible_database_version">Inkompatible Datenbank-Version</string>
|
||||
<string name="database_downgrade_warning">Warnung: Sie könnten einige Daten verlieren!</string>
|
||||
<string name="database_downgrade">Datenbank auf alte Version herabstufen</string>
|
||||
<string name="developer_options">Datenbank-IDs und Transport-Isolationsoption.</string>
|
||||
<string name="mtr_error_no_down_migration">Die Datenbank-Version ist neuer als die App, keine Abwärts-Migration für: %s</string>
|
||||
<string name="hide_dev_options">Verberge:</string>
|
||||
<string name="database_migrations">Migrationen: %s</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">Das Bild wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">Die Datei wird empfangen, sobald das Hochladen durch ihren Kontakt abgeschlossen ist.</string>
|
||||
<string name="cancel_file__question">Dateitransfer abbrechen\?</string>
|
||||
<string name="file_transfer_will_be_cancelled_warning">Der Dateitransfer wird abgebrochen. Falls er gerade abläuft, wird er angehalten.</string>
|
||||
<string name="delete_chat_profile">Chat-Profil löschen</string>
|
||||
<string name="delete_profile">Profil löschen</string>
|
||||
<string name="unhide_profile">Verbergen des Profils aufheben</string>
|
||||
<string name="profile_password">Passwort für Profil</string>
|
||||
<string name="unhide_chat_profile">Verbergen des Chat-Profils aufheben</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Aufforderung zum Empfang des Videos</string>
|
||||
<string name="videos_limit_desc">Es können nur 10 Videos zur gleichen Zeit versendet werden</string>
|
||||
<string name="videos_limit_title">Zu viele Videos auf einmal!</string>
|
||||
<string name="video_descr">Video</string>
|
||||
<string name="icon_descr_video_snd_complete">Video gesendet</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">Das Video wird empfangen, sobald Ihr Kontakt das Hochladen beendet hat.</string>
|
||||
<string name="icon_descr_waiting_for_video">Auf das Video warten</string>
|
||||
<string name="waiting_for_video">Auf das Video warten</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">Das Video wird empfangen, wenn Ihr Kontakt online ist. Bitte warten oder überprüfen Sie es später!</string>
|
||||
<string name="your_XFTP_servers">Ihre XFTP-Server</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="error_saving_xftp_servers">Fehler beim Speichern der XFTP-Server</string>
|
||||
<string name="error_loading_smp_servers">Fehler beim Laden der SMP-Server</string>
|
||||
<string name="error_xftp_test_server_auth">Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung</string>
|
||||
<string name="smp_server_test_download_file">Datei herunterladen</string>
|
||||
<string name="smp_server_test_compare_file">Datei vergleichen</string>
|
||||
<string name="smp_server_test_delete_file">Datei löschen</string>
|
||||
<string name="lock_mode">Sperr-Modus</string>
|
||||
<string name="authentication_cancelled">Authentifizierung abgebrochen</string>
|
||||
<string name="confirm_passcode">Passwort bestätigen</string>
|
||||
<string name="enable_lock">Sperre aktivieren</string>
|
||||
<string name="incorrect_passcode">Passwort falsch</string>
|
||||
<string name="lock_after">Sperre nach</string>
|
||||
<string name="new_passcode">Neues Passwort</string>
|
||||
<string name="la_mode_passcode">Passwort</string>
|
||||
<string name="passcode_changed">Passwort geändert!</string>
|
||||
<string name="passcode_set">Passwort wurde geändert!</string>
|
||||
<string name="change_lock_mode">Sperr-Modus ändern</string>
|
||||
<string name="passcode_not_changed">Passwort wurde nicht geändert!</string>
|
||||
<string name="decryption_error">Entschlüsselungsfehler</string>
|
||||
<string name="decryption_error_permanent">Dauerhafter Entschlüsselungsfehler</string>
|
||||
<string name="error_loading_xftp_servers">Fehler beim Laden der XFTP-Server</string>
|
||||
<string name="la_lock_mode_passcode">Passworteingabe</string>
|
||||
<string name="la_enter_app_passcode">Passwort eingeben</string>
|
||||
<string name="la_no_app_password">Kein App-Passwort</string>
|
||||
<string name="la_authenticate">Authentifizieren</string>
|
||||
<string name="la_minutes">%d Minuten</string>
|
||||
<string name="la_seconds">%d Sekunden</string>
|
||||
<string name="la_immediately">Sofort</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="network_proxy_port">Port %d</string>
|
||||
<string name="smp_server_test_create_file">Datei erstellen</string>
|
||||
<string name="la_current_app_passcode">Aktuelles Passwort</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Dies kann passieren, falls Sie oder Ihre Verbindung ein altes Datenbank-Backup genutzt haben.</string>
|
||||
<string name="la_please_remember_to_store_password">Es gibt keine Möglichkeit ein vergessenes Passwort wiederherzustellen - bitte erinnern Sie sich gut daran oder speichern Sie es sicher ab!</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Bitte melden Sie es den Entwicklern.</string>
|
||||
<string name="la_change_app_passcode">Passwort ändern</string>
|
||||
<string name="alert_title_msg_bad_hash">Ungültiger Nachrichten-Hash</string>
|
||||
<string name="la_auth_failed">Authentifizierung fehlgeschlagen</string>
|
||||
<string name="alert_title_msg_bad_id">Falsche Nachrichten-ID</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string>
|
||||
<string name="la_lock_mode">SimpleX Sperr-Modus</string>
|
||||
<string name="lock_not_enabled">SimpleX Sperre ist nicht aktiviert!</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.</string>
|
||||
<string name="submit_passcode">Übermitteln</string>
|
||||
<string name="la_mode_system">System</string>
|
||||
<string name="la_could_not_be_verified">Sie können nicht überprüft werden - bitte versuchen Sie es nochmal.</string>
|
||||
<string name="xftp_servers">XFTP-Server</string>
|
||||
<string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen).
|
||||
\nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string>
|
||||
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> Nachrichten konnten nicht entschlüsselt werden.</string>
|
||||
<string name="alert_text_msg_bad_hash">Der Hash der vorherigen Nachricht ist unterschiedlich.</string>
|
||||
<string name="you_can_turn_on_lock">Sie können die SimpleX Sperre über die Einstellungen aktivieren.</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS-Proxy Einstellungen</string>
|
||||
<string name="la_lock_mode_system">System-Authentifizierung</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Es handelt sich um einen permanenten Fehler für diese Verbindung - bitte verbinden Sie sich neu.</string>
|
||||
<string name="smp_server_test_upload_file">Datei hochladen</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> Nachrichten übersprungen.</string>
|
||||
<string name="allow_calls_only_if">Anrufe sind nur erlaubt, wenn Ihr Kontakt das ebenfalls erlaubt.</string>
|
||||
<string name="allow_your_contacts_to_call">Erlaubt Ihren Kontakten Sie anzurufen.</string>
|
||||
<string name="audio_video_calls">Audio/Video Anrufe</string>
|
||||
<string name="available_in_v51">"
|
||||
\nVerfügbar in v5.1"</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Sowohl Sie, als auch Ihr Kontakt können Anrufe tätigen.</string>
|
||||
<string name="only_you_can_make_calls">Nur Sie können Anrufe tätigen.</string>
|
||||
<string name="prohibit_calls">Audio/Video Anrufe nicht erlauben.</string>
|
||||
<string name="stop_rcv_file__title">Den Empfang der Datei beenden\?</string>
|
||||
<string name="stop_snd_file__title">Das Senden der Datei beenden\?</string>
|
||||
<string name="stop_rcv_file__message">Der Empfang der Datei wird beendet.</string>
|
||||
<string name="stop_snd_file__message">Das Senden der Datei wird beendet.</string>
|
||||
<string name="stop_file__action">Datei beenden</string>
|
||||
<string name="revoke_file__message">Die Datei wird von den Servern gelöscht.</string>
|
||||
<string name="no_spaces">Keine Leerzeichen!</string>
|
||||
<string name="revoke_file__confirm">Widerrufen</string>
|
||||
<string name="revoke_file__action">Datei widerrufen</string>
|
||||
<string name="revoke_file__title">Datei widerrufen\?</string>
|
||||
<string name="stop_file__confirm">Beenden</string>
|
||||
<string name="v5_0_large_files_support">Videos und Dateien bis zu 1GB</string>
|
||||
<string name="v5_0_large_files_support_descr">Schnell und ohne warten auf den Absender, bis er online ist!</string>
|
||||
<string name="v5_0_polish_interface">Polnische Bedienoberfläche</string>
|
||||
<string name="v5_0_app_passcode_descr">Anstelle der System-Authentifizierung festlegen.</string>
|
||||
<string name="v5_0_polish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
|
||||
<string name="only_your_contact_can_make_calls">Nur Ihr Kontakt kann Anrufe tätigen.</string>
|
||||
<string name="v5_0_app_passcode">App Passwort</string>
|
||||
<string name="calls_prohibited_with_this_contact">Audio/Video Anrufe sind nicht erlaubt.</string>
|
||||
</resources>
|
||||
@@ -10,20 +10,20 @@
|
||||
<string name="chat_item_ttl_day">un dia</string>
|
||||
<string name="chat_item_ttl_month">un mes</string>
|
||||
<string name="chat_item_ttl_week">una semana</string>
|
||||
<string name="allow_disappearing_messages_only_if">Permitir mensajes temporales sólo si tu contacto los permite.</string>
|
||||
<string name="allow_disappearing_messages_only_if">Se permiten mensajes temporales sólo si tu contacto también los permite.</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">Añadir servidores escaneando códigos QR.</string>
|
||||
<string name="smp_servers_preset_add">Añadir servidores predefinidos</string>
|
||||
<string name="all_group_members_will_remain_connected">Todos los miembros del grupo permanecerán conectados.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Permitir la eliminación irreversible de mensajes sólo si tu contacto también lo permite.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la frase de contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Permitir a tus contactos enviar mensajes temporales</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Permitir a tus contactos enviar mensajes de voz.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">Se permite la eliminación irreversible de mensajes sólo si tu contacto también lo permite para tí.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">Android Keystore se usará para almacenar de forma segura la contraseña después de reiniciar la aplicación o cambiar la frase de contraseña - permitirá recibir notificaciones.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">Permites a tus contactos enviar mensajes temporales</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">Permites a tus contactos enviar mensajes de voz.</string>
|
||||
<string name="chat_preferences_always">siempre</string>
|
||||
<string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando, no se iniciará ningún servicio en segundo plano.</string>
|
||||
<string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.</string>
|
||||
<string name="settings_section_title_icon">ICONO DE LA APLICACIÓN</string>
|
||||
<string name="incognito_random_profile_from_contact_description">Se enviará un perfil aleatorio al contacto del que recibió este enlace</string>
|
||||
<string name="turning_off_service_and_periodic">La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.</string>
|
||||
<string name="notifications_mode_service_desc">El servicio en segundo plano está siempre en funcionamiento – las notificaciones se muestran en cuanto los mensajes estén disponibles.</string>
|
||||
<string name="notifications_mode_service_desc">El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>Se puede desactivar en Configuración</b> – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.</string>
|
||||
<string name="notifications_mode_service">Siempre activo</string>
|
||||
<string name="allow_verb">Permitir</string>
|
||||
@@ -47,7 +47,7 @@
|
||||
<string name="accept">Aceptar</string>
|
||||
<string name="audio_call_no_encryption">llamada de audio (sin cifrado e2e)</string>
|
||||
<string name="icon_descr_audio_call">llamada de audio</string>
|
||||
<string name="settings_audio_video_calls">Llamadas de audio y vídeo</string>
|
||||
<string name="settings_audio_video_calls">Llamadas y Videollamadas</string>
|
||||
<string name="icon_descr_audio_off">Audio desactivado</string>
|
||||
<string name="icon_descr_audio_on">Audio activado</string>
|
||||
<string name="integrity_msg_bad_id">ID de mensaje erróneo</string>
|
||||
@@ -55,12 +55,12 @@
|
||||
<string name="users_delete_all_chats_deleted">Se eliminarán todos los chats y mensajes. ¡No puede deshacerse!</string>
|
||||
<string name="accept_feature">Aceptar</string>
|
||||
<string name="allow_to_send_disappearing">Permitir enviar mensajes temporales.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore se utiliza para almacenar de forma segura la frase de contraseña - permite que el servicio de notificación funcione.</string>
|
||||
<string name="keychain_is_storing_securely">Android Keystore se utiliza para almacenar de forma segura la contraseña - permite que el servicio de notificación funcione.</string>
|
||||
<string name="users_add">Añadir perfil</string>
|
||||
<string name="incognito_random_profile_description">Se enviará un perfil aleatorio a tu contacto</string>
|
||||
<string name="color_primary">Acento</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Permitir a tus contactos eliminar irreversiblemente los mensajes enviados.</string>
|
||||
<string name="allow_voice_messages_only_if">Permitir mensajes de voz sólo si tu contacto los permite.</string>
|
||||
<string name="color_primary">Color</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">Permites a tus contactos eliminar irreversiblemente los mensajes enviados.</string>
|
||||
<string name="allow_voice_messages_only_if">Se permiten mensajes de voz sólo si tu contacto también los permite.</string>
|
||||
<string name="allow_direct_messages">Permitir el envío de mensajes directos a los miembros.</string>
|
||||
<string name="allow_to_delete_messages">Permitir la eliminación irreversible de los mensajes enviados.</string>
|
||||
<string name="allow_to_send_voice">Permitir enviar mensajes de voz.</string>
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="integrity_msg_bad_hash">hash de mensaje erróneo</string>
|
||||
<string name="answer_call">Responder llamada</string>
|
||||
<string name="group_member_role_admin">administrador</string>
|
||||
<string name="allow_voice_messages_question">¿Permitir mensajes de voz\?</string>
|
||||
<string name="allow_voice_messages_question">¿Permites los mensajes de voz\?</string>
|
||||
<string name="back">Volver</string>
|
||||
<string name="about_simplex_chat">Sobre <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
<string name="smp_servers_add_to_another_device">Añadir a otro dispositivo</string>
|
||||
@@ -85,31 +85,31 @@
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">Tanto tú como tu contacto podéis enviar mensajes temporales.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Escanear código QR</b>: para conectar con tu contacto que te muestre código QR.</string>
|
||||
<string name="create_profile_button">Crear</string>
|
||||
<string name="create_one_time_link">Crear enlace de invitación de un uso.</string>
|
||||
<string name="create_one_time_link">Crear enlace de invitación de un solo uso.</string>
|
||||
<string name="create_group">Crear grupo secreto</string>
|
||||
<string name="database_passphrase_will_be_updated">La contraseña de cifrado de la base de datos será actualizada.</string>
|
||||
<string name="info_row_database_id">ID de la base de datos</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo están prohibidos.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Los mensajes directos entre miembros del grupo no están permitidos.</string>
|
||||
<string name="passphrase_is_different">La contraseña es distinta a la almacenada en Keystore</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">La base de datos será cifrada y la contraseña se guardará en Keystore.</string>
|
||||
<string name="delete_contact_question">¿Eliminar contacto\?</string>
|
||||
<string name="delete_message__question">Eliminar mensaje\?</string>
|
||||
<string name="delete_message__question">¿Eliminar mensaje\?</string>
|
||||
<string name="delete_chat_profile_question">¿Eliminar el perfil de chat\?</string>
|
||||
<string name="rcv_group_event_group_deleted">grupo eliminado</string>
|
||||
<string name="delete_group_question">¿Eliminar grupo\?</string>
|
||||
<string name="delete_messages_after">Eliminar mensaje después</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar SimpleX Lock en Configuración, después de activar la autenticación de dispositivo.</string>
|
||||
<string name="delete_messages_after">Eliminar después de</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Autenticación de dispositivo desactivada. Puedes habilitar Bloqueo SimpleX en Configuración, después de activar la autenticación de dispositivo.</string>
|
||||
<string name="no_call_on_lock_screen">Desactivar</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales están prohibidos en este chat.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Los mensajes temporales están prohibidos en este grupo.</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">Los mensajes temporales no están permitidos en este chat.</string>
|
||||
<string name="disappearing_messages_are_prohibited">Los mensajes temporales no están permitidos en este grupo.</string>
|
||||
<string name="display_name_cannot_contain_whitespace">El nombre mostrado no puede contener espacios en blanco.</string>
|
||||
<string name="encrypted_video_call">Videollamada con cifrado e2e</string>
|
||||
<string name="display_name_connection_established">conexión establecida</string>
|
||||
<string name="simplex_link_mode_description">Descripción</string>
|
||||
<string name="smp_server_test_connect">Conectar</string>
|
||||
<string name="notification_contact_connected">Conectado</string>
|
||||
<string name="auth_disable_simplex_lock">Desactivar SimpleX Lock</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Autenticación de dispositivo desactivada. SimpleX Lock deshabilitado.</string>
|
||||
<string name="auth_disable_simplex_lock">Desactivar Bloqueo SimpleX</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">Autenticación de dispositivo desactivada. Bloqueo SimpleX deshabilitado.</string>
|
||||
<string name="maximum_supported_file_size">El tamaño máximo de archivo admitido es <xliff:g id="maxFileSize">%1$s</xliff:g></string>
|
||||
<string name="clear_verification">Eliminar verificación</string>
|
||||
<string name="create_profile">Crear perfil</string>
|
||||
@@ -118,7 +118,7 @@
|
||||
<string name="integrity_msg_duplicate">mensaje duplicado</string>
|
||||
<string name="settings_section_title_develop">DESARROLLO</string>
|
||||
<string name="settings_developer_tools">Herramientas desarrollo</string>
|
||||
<string name="delete_files_and_media_for_all_users">Eliminar archivos para todos los perfiles de chat</string>
|
||||
<string name="delete_files_and_media_for_all_users">Eliminar archivos para todos los perfiles Chat</string>
|
||||
<string name="delete_messages">Eliminar mensaje</string>
|
||||
<string name="database_encrypted">¡Base de datos cifrada!</string>
|
||||
<string name="encrypted_with_random_passphrase">La base de datos está cifrada con una contraseña aleatoria, puedes cambiarla.</string>
|
||||
@@ -158,7 +158,7 @@
|
||||
<string name="enable_automatic_deletion_question">¿Activar eliminación automática de mensajes\?</string>
|
||||
<string name="contact_preferences">Preferencias de contacto</string>
|
||||
<string name="ttl_s">%ds</string>
|
||||
<string name="delete_after">Eliminar después</string>
|
||||
<string name="delete_after">Eliminar después de</string>
|
||||
<string name="ttl_sec">%d seg</string>
|
||||
<string name="contact_already_exists">El contácto ya existe</string>
|
||||
<string name="connection_error_auth">Error de conexión (Autenticación)</string>
|
||||
@@ -166,7 +166,7 @@
|
||||
<string name="icon_descr_server_status_disconnected">Desconectado</string>
|
||||
<string name="icon_descr_server_status_connected">Conectado</string>
|
||||
<string name="copied">Copiado en portapapeles</string>
|
||||
<string name="share_one_time_link">Crear enlace de invitación de un uso.</string>
|
||||
<string name="share_one_time_link">Crear enlace de invitación de un solo uso.</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 PC: escanéa el código QR desde la app mediante <b>Escanéo de código QR </b></string>
|
||||
<string name="delete_contact_menu_action">Eliminar</string>
|
||||
<string name="delete_group_menu_action">Eliminar</string>
|
||||
@@ -231,7 +231,7 @@
|
||||
<string name="chat_preferences_default">predefinido (%s)</string>
|
||||
<string name="full_deletion">Eliminar para todos</string>
|
||||
<string name="feature_enabled">activado</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">El contacto puede marcar los mensajes para eliminar; tu podrás verlos.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">El contacto solo puede marcar los mensajes para eliminar. Tu podrás verlos.</string>
|
||||
<string name="ttl_w">%ds</string>
|
||||
<string name="deleted_description">eliminado</string>
|
||||
<string name="connect_via_contact_link">¿Conectar mediante enlace de contacto\?</string>
|
||||
@@ -250,20 +250,20 @@
|
||||
<string name="icon_descr_email">Email</string>
|
||||
<string name="connect_button">Conectar</string>
|
||||
<string name="connect_via_link">Conectar mediante enlace</string>
|
||||
<string name="database_passphrase_and_export">Contraseña y exportar la base de datos</string>
|
||||
<string name="database_passphrase_and_export">Base de Datos y
|
||||
\nContraseña</string>
|
||||
<string name="contribute">Contribuye</string>
|
||||
<string name="core_build_timestamp">Core compilado: %s</string>
|
||||
<string name="core_version">Core versión: v%s</string>
|
||||
<string name="contact_requests">Solicitud del contacto</string>
|
||||
<string name="delete_image">Eliminar imagen</string>
|
||||
<string name="edit_image">Editar imagen</string>
|
||||
<string name="settings_section_title_chats">CHATS</string>
|
||||
<string name="change_verb">Cambiar</string>
|
||||
<string name="notifications_mode_periodic_desc">Comprueba mensajes nuevos de hasta un minuto cada 10 minutos</string>
|
||||
<string name="notifications_mode_periodic_desc">Se realizan comprobaciones de mensajes nuevos periódicas de hasta un minuto de duración cada 10 minutos</string>
|
||||
<string name="clear_contacts_selection_button">Limpiar</string>
|
||||
<string name="change_member_role_question">¿Cambiar rol de grupo\?</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Compara los códigos de seguridad con tus contactos</string>
|
||||
<string name="choose_file">Elije archivo</string>
|
||||
<string name="choose_file">Archivo</string>
|
||||
<string name="clear_verb">Limpiar</string>
|
||||
<string name="clear_chat_button">Limpiar chat</string>
|
||||
<string name="configure_ICE_servers">Configurar servidores ICE</string>
|
||||
@@ -280,14 +280,14 @@
|
||||
<string name="database_initialization_error_title">No se puede iniciar la base de datos</string>
|
||||
<string name="clear_chat_question">Limpiar chat\?</string>
|
||||
<string name="network_session_mode_user">Perfil de Chat</string>
|
||||
<string name="chat_is_stopped_indication">El chat está detenido</string>
|
||||
<string name="chat_is_stopped_indication">Chat está detenido</string>
|
||||
<string name="rcv_group_event_changed_member_role">rol de %s cambiado a %s</string>
|
||||
<string name="change_role">Cambiar rol</string>
|
||||
<string name="v4_5_transport_isolation_descr">Mediante perfil de Chat (por defecto) o por conexión (BETA)</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">cambiando dirección…</string>
|
||||
<string name="chat_preferences">Preferencias de chat</string>
|
||||
<string name="chat_preferences">Preferencias de Chat</string>
|
||||
<string name="feature_cancelled_item">cancelado %s</string>
|
||||
<string name="chat_is_stopped">El chat está detenido</string>
|
||||
<string name="chat_is_stopped">Chat está detenido</string>
|
||||
<string name="settings_section_title_calls">LLAMADAS</string>
|
||||
<string name="chat_is_running">El chat está en ejecución</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">cambiando dirección…</string>
|
||||
@@ -328,7 +328,7 @@
|
||||
<string name="group_invitation_expired">Invitación de grupo caducada</string>
|
||||
<string name="alert_message_group_invitation_expired">La invitación al grupo ya no es válida, ha sido eliminada por el remitente.</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">El grupo se eliminará para tí. ¡No puede deshacerse!</string>
|
||||
<string name="how_to_use_markdown">Cómo usar el marcador</string>
|
||||
<string name="how_to_use_markdown">Cómo usar sintaxis markdown</string>
|
||||
<string name="description_via_one_time_link_incognito">Incógnito mediante enlace de un uso</string>
|
||||
<string name="simplex_link_contact">Dirección de contacto SimpleX</string>
|
||||
<string name="error_saving_smp_servers">Error guardando servidores SMP</string>
|
||||
@@ -336,7 +336,7 @@
|
||||
<string name="error_setting_network_config">Error al actualizar la configuración de red</string>
|
||||
<string name="error_creating_address">Error creando dirección</string>
|
||||
<string name="error_deleting_user">Error eliminando perfil de usuario</string>
|
||||
<string name="auth_enable_simplex_lock">Activar SimpleX Lock</string>
|
||||
<string name="auth_enable_simplex_lock">Activar Bloqueo SimpleX</string>
|
||||
<string name="one_time_link">Enlace de invitación de un uso</string>
|
||||
<string name="smp_servers">Servidores SMP</string>
|
||||
<string name="settings_experimental_features">Características experimentales</string>
|
||||
@@ -346,7 +346,7 @@
|
||||
<string name="failed_to_active_user_title">¡Error cambiando perfil!</string>
|
||||
<string name="smp_servers_enter_manually">Introduce el servidor manualmente</string>
|
||||
<string name="how_to_use_your_servers">Cómo usar tus servidores</string>
|
||||
<string name="error_stopping_chat">Error deteniendo el chat</string>
|
||||
<string name="error_stopping_chat">Error deteniendo Chat</string>
|
||||
<string name="enter_correct_passphrase">Introduce la contraseña correcta.</string>
|
||||
<string name="enter_passphrase">Introduce la contraseña…</string>
|
||||
<string name="icon_descr_group_inactive">Grupo inactivo</string>
|
||||
@@ -359,6 +359,8 @@
|
||||
<string name="error_saving_file">Error al guardar archivo</string>
|
||||
<string name="icon_descr_server_status_error">Error</string>
|
||||
<string name="from_gallery_button">De la Galería</string>
|
||||
<string name="gallery_image_button">Imagen</string>
|
||||
<string name="gallery_video_button">Vídeo</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Si has recibido el enlace de invitación a <xliff:g id="appName">SimpleX Chat</xliff:g>, puedes abrirlo en tu navegador:</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Si eliges rechazar, el remitente NO será notificado.</string>
|
||||
<string name="invalid_contact_link">¡Enlace no válido!</string>
|
||||
@@ -383,8 +385,8 @@
|
||||
<string name="error_deleting_group">Error al eliminar grupo</string>
|
||||
<string name="error_deleting_pending_contact_connection">Error al eliminar conexión de contacto pendiente</string>
|
||||
<string name="hide_notification">Ocultar</string>
|
||||
<string name="notification_preview_mode_hidden">Oculto</string>
|
||||
<string name="notification_display_mode_hidden_desc">Ocultar contacto y mensaje</string>
|
||||
<string name="notification_preview_mode_hidden">Oculta</string>
|
||||
<string name="notification_display_mode_hidden_desc">Se ocultan tanto el contacto como el mensaje</string>
|
||||
<string name="hide_verb">Ocultar</string>
|
||||
<string name="for_everybody">Para todos</string>
|
||||
<string name="icon_descr_file">Archivo</string>
|
||||
@@ -406,7 +408,7 @@
|
||||
<string name="v4_4_french_interface">Interfaz en francés</string>
|
||||
<string name="image_descr">Imagen</string>
|
||||
<string name="file_not_found">Archivo no encontrado</string>
|
||||
<string name="how_to_use_simplex_chat">Cómo usar</string>
|
||||
<string name="how_to_use_simplex_chat">Guía de uso</string>
|
||||
<string name="full_name_optional__prompt">Nombre completo (opcional)</string>
|
||||
<string name="callstate_ended">finalizado</string>
|
||||
<string name="settings_section_title_help">AYUDA</string>
|
||||
@@ -419,7 +421,7 @@
|
||||
<string name="error_changing_role">Error cambiando rol</string>
|
||||
<string name="conn_stats_section_title_servers">SERVIDORES</string>
|
||||
<string name="group_display_name_field">Nombre mostrado del grupo:</string>
|
||||
<string name="group_preferences">Preferencias del grupo</string>
|
||||
<string name="group_preferences">Preferencias de grupo</string>
|
||||
<string name="group_members_can_send_dms">Los miembros del grupo pueden enviar mensajes directos.</string>
|
||||
<string name="group_members_can_delete">Los miembros del grupo pueden eliminar mensajes de forma irreversible.</string>
|
||||
<string name="v4_3_improved_privacy_and_security_desc">Ocultar pantalla de aplicaciones en aplicaciones recientes.</string>
|
||||
@@ -444,16 +446,16 @@
|
||||
<string name="share_link">Compartir enlace</string>
|
||||
<string name="how_it_works">Cómo funciona</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">El mensaje se eliminará. ¡No puede deshacerse!</string>
|
||||
<string name="incognito_info_protects">El modo incógnito protege la identidad del perfil principal, por cada contacto nuevo se genera un perfil nuevo aleatorio.</string>
|
||||
<string name="incognito_info_protects">La función del modo incógnito es proteger la identidad del perfil principal: por cada contacto nuevo se genera un perfil aleatorio.</string>
|
||||
<string name="turn_off_battery_optimization">Para poder usarse <b>deshabilita la optimización de batería</b> para <xliff:g id="appName">SimpleX</xliff:g> en el siguiente cuadro de diálogo. De lo contrario las notificaciones estarán desactivadas.</string>
|
||||
<string name="install_simplex_chat_for_terminal">Instalar <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para terminal</string>
|
||||
<string name="group_invitation_item_description">invitación al grupo <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="rcv_group_event_member_added">invitado <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas en un único perfil de chat.</string>
|
||||
<string name="incognito_info_allows">Permite tener varias conexiones anónimas sin datos compartidos entre estas dentro del mismo perfil.</string>
|
||||
<string name="invite_to_group_button">Invitar al grupo</string>
|
||||
<string name="to_verify_compare">Para comprobar el cifrado de extremo a extremo con su contacto compare (o escanee) el código en sus dispositivos.</string>
|
||||
<string name="database_is_not_encrypted">La base de datos no está cifrada. Establece una contraseña para protegerla.</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no duplicadas.</string>
|
||||
<string name="database_is_not_encrypted">La base de datos no está cifrada. Escribe una contraseña para protegerla.</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
|
||||
<string name="icon_descr_instant_notifications">Notificación instantánea</string>
|
||||
<string name="network_settings_title">Configuración de red</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">No se usarán hosts .onion</string>
|
||||
@@ -465,15 +467,13 @@
|
||||
<string name="conn_level_desc_indirect">indirecto (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="theme_light">Claro</string>
|
||||
<string name="chat_preferences_on">Activado</string>
|
||||
<string name="message_deletion_prohibited">La eliminación irreversible de mensajes está prohibida en este chat.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes está prohibida en este grupo.</string>
|
||||
<string name="message_deletion_prohibited">La eliminación irreversible de mensajes no está permitida en este chat.</string>
|
||||
<string name="message_deletion_prohibited_in_chat">La eliminación irreversible de mensajes no está permitida en este grupo.</string>
|
||||
<string name="v4_3_improved_server_configuration">Configuración del servidor mejorada</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Esto puede suceder cuando:
|
||||
\n1. Los mensajes caducan en el servidor si no se han recibido durante 30 días.
|
||||
\n2. El servidor que utiliza para recibir los mensajes de este contacto fue actualizado y reiniciado.
|
||||
\n3. La conexión está comprometida.
|
||||
\nPor favor, contacta con los desarrolladores a través del menú Configuración para recibir actualizaciones sobre los servidores.
|
||||
\nAñadiremos redundancia de servidores para evitar la pérdida de mensajes.</string>
|
||||
\n1. Los mensajes caducan en el cliente saliente tras 2 días o en el servidor tras 30 días.
|
||||
\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.
|
||||
\n3. La conexión ha sido comprometida.</string>
|
||||
<string name="notification_preview_mode_message">Texto del mensaje</string>
|
||||
<string name="member_info_section_title_member">MIEMBRO</string>
|
||||
<string name="chat_item_ttl_none">nunca</string>
|
||||
@@ -492,7 +492,7 @@
|
||||
<string name="mark_unread">Marcar como no leído</string>
|
||||
<string name="invalid_QR_code">Código QR inválido</string>
|
||||
<string name="incorrect_code">¡Código de seguridad incorrecto!</string>
|
||||
<string name="markdown_in_messages">Marcadores en mensajes</string>
|
||||
<string name="markdown_in_messages">Sintaxis markdown en mensajes</string>
|
||||
<string name="network_use_onion_hosts_no">No</string>
|
||||
<string name="callstatus_missed">llamada perdida</string>
|
||||
<string name="import_database_confirmation">Importar</string>
|
||||
@@ -506,7 +506,7 @@
|
||||
<string name="invalid_data">datos no válidos</string>
|
||||
<string name="invalid_message_format">formato de mensaje no válido</string>
|
||||
<string name="live">EN VIVO</string>
|
||||
<string name="moderated_description">moderado</string>
|
||||
<string name="moderated_description">eliminado por el moderador</string>
|
||||
<string name="display_name_invited_to_connect">invitado a conectarse</string>
|
||||
<string name="service_notifications_disabled">¡Las notificaciones instantáneas están desactivadas!</string>
|
||||
<string name="notification_preview_new_message">mensaje nuevo</string>
|
||||
@@ -528,7 +528,7 @@
|
||||
<string name="snd_group_event_user_left">has salido</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">has cambiado la dirección</string>
|
||||
<string name="feature_off">apagado</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Eliminación del mensaje irreversible</string>
|
||||
<string name="v4_3_irreversible_message_deletion">Eliminación irreversible del mensaje</string>
|
||||
<string name="v4_3_voice_messages_desc">Máximo 40 segundos, recibido al instante.</string>
|
||||
<string name="v4_5_italian_interface">Interfaz en italiano</string>
|
||||
<string name="v4_5_message_draft">Borrador de mensaje</string>
|
||||
@@ -538,15 +538,15 @@
|
||||
<string name="no_details">sin detalles</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="only_stored_on_members_devices">(sólo almacenado por miembros del grupo)</string>
|
||||
<string name="markdown_help">Ayuda marcadores</string>
|
||||
<string name="network_and_servers">Redes y servidores</string>
|
||||
<string name="markdown_help">Ayuda sintaxis markdown</string>
|
||||
<string name="network_and_servers">Redes y Servidores</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">Se usarán hosts .onion cuando estén disponibles.</string>
|
||||
<string name="italic">cursiva</string>
|
||||
<string name="incoming_audio_call">Llamada entrante</string>
|
||||
<string name="video_call_no_encryption">videollamada (sin cifrado e2e)</string>
|
||||
<string name="status_no_e2e_encryption">sin cifrado e2e</string>
|
||||
<string name="import_database">Importar base de datos</string>
|
||||
<string name="settings_section_title_messages">MENSAJES</string>
|
||||
<string name="settings_section_title_messages">MENSAJES Y ARCHIVOS</string>
|
||||
<string name="import_database_question">¿Importar base de datos\?</string>
|
||||
<string name="no_received_app_files">Sin archivos recibidos o enviados</string>
|
||||
<string name="messages_section_title">Mensajes</string>
|
||||
@@ -574,7 +574,7 @@
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">has cambiado la dirección por %s</string>
|
||||
<string name="rcv_group_event_member_left">ha salido</string>
|
||||
<string name="button_leave_group">Salir del grupo</string>
|
||||
<string name="only_group_owners_can_change_prefs">Sólo los propietarios del grupo pueden cambiar las preferencias de grupo.</string>
|
||||
<string name="only_group_owners_can_change_prefs">Sólo los propietarios del grupo pueden modificar las preferencias de grupo.</string>
|
||||
<string name="users_delete_data_only">Sólo datos del perfil local</string>
|
||||
<string name="chat_preferences_no">no</string>
|
||||
<string name="thousand_abbreviation">k</string>
|
||||
@@ -602,8 +602,8 @@
|
||||
<string name="reset_color">Restablecer colores</string>
|
||||
<string name="only_you_can_send_disappearing">Sólo tú puedes enviar mensajes temporales</string>
|
||||
<string name="only_your_contact_can_send_disappearing">Sólo tu contacto puede enviar mensajes temporales.</string>
|
||||
<string name="prohibit_sending_voice">Prohibir el envío de mensajes de voz.</string>
|
||||
<string name="notifications_mode_off">Se ejecuta cuando la aplicación está abierta</string>
|
||||
<string name="prohibit_sending_voice">Prohibes el envío de mensajes de voz.</string>
|
||||
<string name="notifications_mode_off">Se ejecuta sólo cuando la aplicación está abierta</string>
|
||||
<string name="auth_open_chat_console">Abrir la consola de chat</string>
|
||||
<string name="save_verb">Guardar</string>
|
||||
<string name="restore_database_alert_confirm">Restaurar</string>
|
||||
@@ -626,7 +626,7 @@
|
||||
<string name="restore_database_alert_title">¿Restaurar copia de seguridad de la base de datos\?</string>
|
||||
<string name="onboarding_notifications_mode_title">Notificaciones privadas</string>
|
||||
<string name="image_descr_profile_image">imagen del perfil</string>
|
||||
<string name="prohibit_sending_voice_messages">Prohibir el envío de mensajes de voz.</string>
|
||||
<string name="prohibit_sending_voice_messages">Prohibes el envío de mensajes de voz.</string>
|
||||
<string name="protect_app_screen">Proteger la pantalla de la aplicación</string>
|
||||
<string name="read_more_in_github_with_link">Más información en nuestro <font color="#0088ff">repositorio GitHub</font> .</string>
|
||||
<string name="icon_descr_record_voice_message">Grabar mensaje de voz</string>
|
||||
@@ -635,7 +635,7 @@
|
||||
<string name="send_live_message_desc">Envía un mensaje en vivo: se actualizará para el(los) destinatario(s) a medida que se escribe</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">error de envío</string>
|
||||
<string name="sending_via">Enviando mediante</string>
|
||||
<string name="contact_developers">Actualiza la aplicación y ponte en contacto con los desarrolladores.</string>
|
||||
<string name="contact_developers">Por favor, actualiza la aplicación y ponte en contacto con los desarrolladores.</string>
|
||||
<string name="sender_cancelled_file_transfer">El remitente ha cancelado la transferencia de archivos.</string>
|
||||
<string name="smp_server_test_secure_queue">Cola segura</string>
|
||||
<string name="enter_passphrase_notification_title">Se necesita contraseña</string>
|
||||
@@ -651,7 +651,7 @@
|
||||
<string name="network_options_revert">Revertir</string>
|
||||
<string name="network_option_ping_interval">Intervalo PING</string>
|
||||
<string name="network_option_ping_count">Contador PING</string>
|
||||
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcar para eliminar).</string>
|
||||
<string name="only_your_contact_can_delete">Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar).</string>
|
||||
<string name="v4_5_message_draft_descr">Conserva el último borrador del mensaje con los datos adjuntos.</string>
|
||||
<string name="v4_5_private_filenames">Nombres de archivos privados</string>
|
||||
<string name="v4_5_reduced_battery_usage">Reducción del uso de la batería</string>
|
||||
@@ -663,7 +663,7 @@
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(escanear o pegar desde el portapapeles)</string>
|
||||
<string name="icon_descr_profile_image_placeholder">Espacio reservado para la imagen del perfil</string>
|
||||
<string name="image_descr_qr_code">Código QR</string>
|
||||
<string name="chat_with_the_founder">Envía consultas e ideas</string>
|
||||
<string name="chat_with_the_founder">Consultas y sugerencias</string>
|
||||
<string name="smp_servers_preset_address">Dirección del servidor predefinida</string>
|
||||
<string name="send_us_an_email">Contacta por email</string>
|
||||
<string name="rate_the_app">Valora la aplicación</string>
|
||||
@@ -676,7 +676,7 @@
|
||||
<string name="reject">Rechazar</string>
|
||||
<string name="open_verb">Abrir</string>
|
||||
<string name="icon_descr_call_pending_sent">Llamada pendiente</string>
|
||||
<string name="privacy_and_security">Privacidad y seguridad</string>
|
||||
<string name="privacy_and_security">Privacidad y Seguridad</string>
|
||||
<string name="store_passphrase_securely_without_recover">Guarda la contraseña de forma segura, NO podrás acceder al chat si la pierdes.</string>
|
||||
<string name="save_archive">Guardar archivo</string>
|
||||
<string name="restore_database_alert_desc">Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer.</string>
|
||||
@@ -685,16 +685,16 @@
|
||||
<string name="network_option_protocol_timeout">Tiempo de espera del protocolo</string>
|
||||
<string name="network_option_seconds_label">seg</string>
|
||||
<string name="users_delete_with_connections">Perfil y conexiones de servidor</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Prohibir el envío de mensajes temporales</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Prohibes el envío de mensajes temporales</string>
|
||||
<string name="only_you_can_send_voice">Sólo tú puedes enviar mensajes de voz.</string>
|
||||
<string name="only_your_contact_can_send_voice">Sólo tu contacto puede enviar mensajes de voz.</string>
|
||||
<string name="run_chat_section">EJECUTAR CHAT</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">Reinicia la aplicación para poder usar la base de datos importada.</string>
|
||||
<string name="enter_correct_current_passphrase">Introduce la contraseña actual correcta.</string>
|
||||
<string name="feature_received_prohibited">recepción prohibida</string>
|
||||
<string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcar para eliminar).</string>
|
||||
<string name="only_you_can_delete_messages">Sólo tú puedes eliminar mensajes de forma irreversible (tu contacto puede marcarlos para eliminar).</string>
|
||||
<string name="prohibit_direct_messages">Prohibir el envío de mensajes directos a los miembros.</string>
|
||||
<string name="prohibit_sending_disappearing">Prohibir el envío de mensajes temporales</string>
|
||||
<string name="prohibit_sending_disappearing">Prohibes el envío de mensajes temporales</string>
|
||||
<string name="v4_2_security_assessment">Evaluación de la seguridad</string>
|
||||
<string name="receiving_files_not_yet_supported">la recepción de archivos aún no está disponible</string>
|
||||
<string name="sending_files_not_yet_supported">el envío de archivos aún no está disponible</string>
|
||||
@@ -705,7 +705,7 @@
|
||||
<string name="open_chat">Abrir chat</string>
|
||||
<string name="restore_database">Restaurar copia de seguridad de la base de datos</string>
|
||||
<string name="save_passphrase_and_open_chat">Guardar contraseña y abrir el chat</string>
|
||||
<string name="restore_passphrase_not_found_desc">La contraseña no se ha encontrado en Keystore, introdúzcala manualmente. Esto puede haber ocurrido si has restaurado los datos de la aplicación con una herramienta de copia de seguridad. Si no es así, ponte en contacto con los desarrolladores.</string>
|
||||
<string name="restore_passphrase_not_found_desc">La contraseña no se ha encontrado en Keystore, introdúzcala manualmente. Esto puede haber ocurrido si has restaurado los datos de la aplicación con una herramienta de copia de seguridad. Si no es así, por favor ponte en contacto con los desarrolladores.</string>
|
||||
<string name="remove_member_confirmation">Eliminar</string>
|
||||
<string name="button_remove_member">Eliminar miembro</string>
|
||||
<string name="button_send_direct_message">Enviar mensaje directo</string>
|
||||
@@ -736,11 +736,11 @@
|
||||
<string name="stop_chat_confirmation">Detener</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</string>
|
||||
<string name="skip_inviting_button">Omitir invitación a miembros</string>
|
||||
<string name="settings_notification_preview_mode_title">Mostrar vista previa</string>
|
||||
<string name="settings_notification_preview_mode_title">Vista previa</string>
|
||||
<string name="la_notice_turn_on">Activar</string>
|
||||
<string name="share_verb">Compartir</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
|
||||
<string name="set_contact_name">Introduce el nombre del contacto</string>
|
||||
<string name="set_contact_name">Escribe un nombre para el contacto</string>
|
||||
<string name="network_socks_toggle">Usar proxy SOCKS (puerto 9050)</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="member_role_will_be_changed_with_notification">El rol cambiará a \"%s\". Se notificará a todos los miembros del grupo.</string>
|
||||
@@ -748,9 +748,9 @@
|
||||
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
|
||||
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
|
||||
<string name="icon_descr_received_msg_status_unread">no leído</string>
|
||||
<string name="text_field_set_contact_placeholder">Introduce el nombre del contacto…</string>
|
||||
<string name="text_field_set_contact_placeholder">Escribe un nombre para el contacto…</string>
|
||||
<string name="switch_receiving_address_question">¿Cambiar dirección de recepción\?</string>
|
||||
<string name="use_camera_button">Usar cámara</string>
|
||||
<string name="use_camera_button">Cámara</string>
|
||||
<string name="contact_you_shared_link_with_wont_be_able_to_connect">¡El contacto con el que has compartido este enlace NO podrá conectarse!</string>
|
||||
<string name="show_QR_code">Mostrar código QR</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">¡El enlace no es un enlace de conexión válido!</string>
|
||||
@@ -758,23 +758,23 @@
|
||||
<string name="share_invitation_link">Compartir enlace de invitación</string>
|
||||
<string name="update_network_session_mode_question">¿Actualizar el modo de aislamiento de transporte\?</string>
|
||||
<string name="icon_descr_speaker_on">Altavoz activado</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Detener Chat para habilitar acciones sobre la base de datos.</string>
|
||||
<string name="stop_chat_to_enable_database_actions">Detén Chat para habilitar las acciones sobre la base de datos.</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">¡La conexión que has aceptado se cancelará!</string>
|
||||
<string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para obtener más información</string>
|
||||
<string name="moderate_message_will_be_marked_warning">El mensaje será marcado como moderado para todos los miembros.</string>
|
||||
<string name="next_generation_of_private_messaging">La próxima generación de mensajería privada</string>
|
||||
<string name="delete_files_and_media_desc">Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string>
|
||||
<string name="enable_automatic_deletion_message">Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</string>
|
||||
<string name="messages_section_description">Esta configuración se aplica a los mensajes en su perfil actual de Chat</string>
|
||||
<string name="messages_section_description">Esta configuración se aplica a los mensajes en tu perfil actual</string>
|
||||
<string name="this_string_is_not_a_connection_link">¡Esta cadena no es un enlace de conexión!</string>
|
||||
<string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo plano<xliff:g id="appName">SimpleX</xliff:g></b>, utiliza un pequeño porcentaje de la batería al día.</string>
|
||||
<string name="icon_descr_settings">Configuración</string>
|
||||
<string name="icon_descr_speaker_off">Altavoz apagado</string>
|
||||
<string name="add_contact_or_create_group">Inciar chat nuevo</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Detener Chat para exportar, importar o eliminar la base de datos del chat. No podrá recibir ni enviar mensajes mientras el chat esté detenido.</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Detén Chat para poder exportar, importar o eliminar la base de datos. No puedes recibir ni enviar mensajes mientras Chat esté detenido.</string>
|
||||
<string name="thank_you_for_installing_simplex">Gracias por instalar <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger la privacidad, en lugar de los identificadores de usuario que utilizan el resto de plataformas, <xliff:g id="appName">SimpleX</xliff:g> dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos.</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa SimpleX Lock.
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa Bloqueo SimpleX.
|
||||
\nSe te pedirá que completes la autenticación antes de activar esta función.</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Al actualizar la configuración, el cliente se reconectará a todos los servidores.</string>
|
||||
<string name="use_simplex_chat_servers__question">¿Usar servidores <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\?</string>
|
||||
@@ -784,22 +784,22 @@
|
||||
<string name="error_smp_test_server_auth">El servidor requiere autorización para crear colas, comprueba la contraseña</string>
|
||||
<string name="enter_passphrase_notification_desc">Para recibir notificaciones, introduce la contraseña de la base de datos</string>
|
||||
<string name="ntf_channel_calls">Llamadas de chat SimpleX</string>
|
||||
<string name="notifications_mode_periodic">Se inicia periódicamente</string>
|
||||
<string name="notification_preview_mode_message_desc">Mostrar contacto y mensaje</string>
|
||||
<string name="notification_preview_mode_contact_desc">Mostrar sólo contacto</string>
|
||||
<string name="notifications_mode_periodic">Se ejecuta periódicamente</string>
|
||||
<string name="notification_preview_mode_message_desc">Se muestran el nombre del contacto y el mensaje</string>
|
||||
<string name="notification_preview_mode_contact_desc">Se muestra solo el nombre del contacto</string>
|
||||
<string name="auth_simplex_lock_turned_on">Bloqueo SimpleX activado</string>
|
||||
<string name="auth_stop_chat">Detener chat</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">El mensaje se eliminará para todos los miembros.</string>
|
||||
<string name="share_file">Compartir archivo…</string>
|
||||
<string name="images_limit_title">¡Demasiadas imágenes!</string>
|
||||
<string name="image_decoding_exception_desc">La imagen no se puede decodificar. Pruebe otra imagen o pónte en contacto con los desarrolladores.</string>
|
||||
<string name="image_decoding_exception_desc">La imagen no se puede decodificar. Pruebe con otra imagen o contacta con los desarrolladores.</string>
|
||||
<string name="network_enable_socks">¿Usa proxy SOCKS\?</string>
|
||||
<string name="network_use_onion_hosts">Usar hosts .onion</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad.</string>
|
||||
<string name="first_platform_without_user_ids">La primera plataforma sin identificadores de usuario: diseñada para la privacidad.</string>
|
||||
<string name="alert_message_no_group">Este grupo ya no existe.</string>
|
||||
<string name="incognito_info_find">Para encontrar el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string>
|
||||
<string name="incognito_info_find">Para conocer el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string>
|
||||
<string name="accept_feature_set_1_day">Establecer 1 día</string>
|
||||
<string name="v4_4_french_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
|
||||
<string name="v4_5_italian_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
|
||||
@@ -811,7 +811,7 @@
|
||||
<string name="is_not_verified">%s no está verificado</string>
|
||||
<string name="smp_servers_test_server">Probar servidor</string>
|
||||
<string name="smp_servers_test_servers">Probar servidores</string>
|
||||
<string name="star_on_github">Comienza en GitHub</string>
|
||||
<string name="star_on_github">Estrella en GitHub</string>
|
||||
<string name="smp_servers_per_user">Los servidores para nuevas conexiones de tu perfil de Chat actual</string>
|
||||
<string name="network_disable_socks">¿Usar conexión directa a Internet\?</string>
|
||||
<string name="update_onion_hosts_settings_question">¿Actualizar la configuración de los hosts .onion\?</string>
|
||||
@@ -825,9 +825,9 @@
|
||||
<string name="rcv_group_event_updated_group_profile">perfil de grupo actualizado</string>
|
||||
<string name="network_option_tcp_connection_timeout">Tiempo de espera de la conexión TCP agotado</string>
|
||||
<string name="theme">Tema</string>
|
||||
<string name="set_group_preferences">Establecer preferencias de grupo</string>
|
||||
<string name="set_group_preferences">Establece preferencias de grupo</string>
|
||||
<string name="settings_section_title_support">SOPORTE SIMPLEX CHAT</string>
|
||||
<string name="set_password_to_export">Seleccióna contraseña para exportar</string>
|
||||
<string name="set_password_to_export">Escribe la contraseña para exportar</string>
|
||||
<string name="update_database">Actualizar</string>
|
||||
<string name="update_database_passphrase">Actualizar contraseña base de datos</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string>
|
||||
@@ -841,7 +841,7 @@
|
||||
<string name="error_smp_test_failed_at_step">Prueba fallida en el paso %s.</string>
|
||||
<string name="tap_to_start_new_chat">Pulsa para iniciar chat nuevo</string>
|
||||
<string name="share_message">Compartir mensaje…</string>
|
||||
<string name="share_image">Compartir imagen…</string>
|
||||
<string name="share_image">Compartir medios…</string>
|
||||
<string name="show_call_on_lock_screen">Mostrar</string>
|
||||
<string name="unknown_database_error_with_info">Error desconocido en la base de datos: %s</string>
|
||||
<string name="database_backup_can_be_restored">El intento de cambiar la contraseña de la base de datos no se ha completado.</string>
|
||||
@@ -858,23 +858,23 @@
|
||||
<string name="description_via_one_time_link">mediante enlace de un uso</string>
|
||||
<string name="your_chats">Tus chats</string>
|
||||
<string name="voice_message_send_text">Mensaje de voz…</string>
|
||||
<string name="your_contact_address">Tu dirección de contacto</string>
|
||||
<string name="your_contact_address">Mi dirección de contacto</string>
|
||||
<string name="icon_descr_video_off">Desactivar vídeo</string>
|
||||
<string name="icon_descr_video_on">Activar vídeo</string>
|
||||
<string name="wrong_passphrase">Contraseña de base de datos incorrecta</string>
|
||||
<string name="wrong_passphrase_title">¡Contraseña incorrecta!</string>
|
||||
<string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Te has unido a este grupo. Conectando con miembro del grupo invitado.</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Estás utilizando un perfil incógnito para este grupo. Para evitar compartir tu perfil principal, invitar contactos no está permitido</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">Estás usando un perfil incógnito para este grupo, por tanto para evitar compartir tu perfil principal no se permite invitar contactos</string>
|
||||
<string name="you_are_invited_to_group">Has sido invitado al grupo</string>
|
||||
<string name="v4_3_voice_messages">Mensajes de voz</string>
|
||||
<string name="v4_3_irreversible_message_deletion_desc">Tus contactos pueden permitir la eliminación completa de mensajes.</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Tú controlas a través de qué servidor(es) <b>recibes</b> los mensajes. Tus contactos controlan a través de qué servidor(es) <b>envías</b> tus mensajes.</string>
|
||||
<string name="voice_messages">Mensajes de voz</string>
|
||||
<string name="voice_messages_are_prohibited">Los mensajes de voz están prohibidos en este grupo.</string>
|
||||
<string name="voice_messages_are_prohibited">Los mensajes de voz no están permitidos en este grupo.</string>
|
||||
<string name="v4_4_verify_connection_security">Comprobar la seguridad de la conexión</string>
|
||||
<string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a <xliff:g id="contactName" example="Alice">%1$s! </xliff:g>.</string>
|
||||
<string name="welcome">¡Bienvenido!</string>
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil de chat será enviado
|
||||
<string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil Chat será enviado
|
||||
\na tu contacto</string>
|
||||
<string name="your_ICE_servers">Tus servidores ICE</string>
|
||||
<string name="you_rejected_group_invitation">Has rechazado la invitación del grupo.</string>
|
||||
@@ -887,11 +887,11 @@
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> quiere conectarse contigo mediante</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">Tienes un perfil de chat con el mismo nombre mostrado. Debes elegir otro nombre.</string>
|
||||
<string name="you_can_also_connect_by_clicking_the_link">También puedes conectarte haciendo clic en el enlace. Si se abre en el navegador, haz clic en <b>Abrir en aplicación móvil</b>.</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Puedes <font color="#0088ff">conectar con los desarrolladores de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para hacer cualquier pregunta y recibir actualizaciones</font>.</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Puedes <font color="#0088ff">ponerte en contacto con los desarrolladores de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> para consultas y para recibir actualizaciones</font>.</string>
|
||||
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Puedes compartir tu dirección como enlace o como código QR: cualquiera podrá conectarse contigo. Si lo eliminas más tarde tus contactos no se perderán.</string>
|
||||
<string name="observer_cant_send_message_title">¡No puedes enviar mensajes!</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar marcadores para dar formato a los mensajes:</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Debes usar la versión más reciente de tu base de datos SÓLO en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos.</string>
|
||||
<string name="you_can_use_markdown_to_format_messages__prompt">Puedes usar la sintaxis markdown para dar formato a los mensajes:</string>
|
||||
<string name="you_must_use_the_most_recent_version_of_database">Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos.</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Tu contacto debe estar en línea para que se complete la conexión.
|
||||
\nPuedes cancelar esta conexión y eliminar el contacto (e intentarlo más tarde con un enlace nuevo).</string>
|
||||
<string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">La base de datos actual será ELIMINADA y SUSTITUIDA por la importada.
|
||||
@@ -900,7 +900,7 @@
|
||||
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando se acepte tu solicitud de conexión, por favor espere o compruébalo más tarde.</string>
|
||||
<string name="you_will_be_connected_when_your_contacts_device_is_online">Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde.</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</string>
|
||||
<string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que has compartido un perfil incógnito, al grupo en el que utilizas tu perfil principal</string>
|
||||
<string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal</string>
|
||||
<string name="simplex_link_mode_browser">mediante navegador</string>
|
||||
<string name="simplex_link_connection">mediante <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="simplex_service_notification_title">Servicio <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
|
||||
@@ -915,7 +915,7 @@
|
||||
<string name="snd_group_event_member_deleted">has eliminado <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="group_info_member_you">Tú: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puedes compartir un enlace o un código QR: cualquiera podrá unirse al grupo. Si lo eliminas más tarde los miembros del grupo no se perderán.</string>
|
||||
<string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil se usará para los grupos a los que te inviten.</string>
|
||||
<string name="incognito_info_share">Cuando compartes un perfil incógnito con alguien, este perfil también se usará para los grupos a los que te inviten.</string>
|
||||
<string name="your_preferences">Tus preferencias</string>
|
||||
<string name="v4_2_auto_accept_contact_requests_desc">Con mensaje de bienvenida opcional.</string>
|
||||
<string name="you_are_observer">Tu rol es observador</string>
|
||||
@@ -924,7 +924,7 @@
|
||||
<string name="you_invited_your_contact">Has invitado a tu contacto</string>
|
||||
<string name="you_will_be_connected_when_group_host_device_is_online">Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde.</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Tu contacto puede escanear el código QR desde la aplicación.</string>
|
||||
<string name="your_settings">Tu configuración</string>
|
||||
<string name="your_settings">Mi configuración</string>
|
||||
<string name="your_SMP_servers">Tus servidores SMP</string>
|
||||
<string name="you_control_your_chat">¡Tú controlas tu chat!</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</string>
|
||||
@@ -934,15 +934,14 @@
|
||||
<string name="icon_descr_video_call">Videollamada</string>
|
||||
<string name="your_calls">Tus llamadas</string>
|
||||
<string name="your_ice_servers">Tus servidores ICE</string>
|
||||
<string name="your_privacy">Tu privacidad</string>
|
||||
<string name="settings_section_title_you">TU</string>
|
||||
<string name="your_chat_database">Base de datos de Chat</string>
|
||||
<string name="your_privacy">Privacidad</string>
|
||||
<string name="settings_section_title_you">MIS DATOS</string>
|
||||
<string name="your_chat_database">Base de datos Chat</string>
|
||||
<string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.</string>
|
||||
<string name="you_sent_group_invitation">Has enviado una invitación de grupo</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contacto(s) seleccionado(s)</string>
|
||||
<string name="num_contacts_selected">%d contacto(s) seleccionado(s)</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members"> %1$s </xliff:g> MIEMBROS</string>
|
||||
<string name="your_chat_profiles_stored_locally">Tus perfiles de chat se almacenan localmente, sólo en tu dispositivo</string>
|
||||
<string name="voice_prohibited_in_this_chat">Los mensajes de voz están prohibidos en este chat.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Los mensajes de voz no están permitidos en este chat.</string>
|
||||
<string name="whats_new">Novedades</string>
|
||||
<string name="you_have_to_enter_passphrase_every_time">La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación.</string>
|
||||
<string name="you_joined_this_group">Te has unido a este grupo</string>
|
||||
@@ -962,22 +961,20 @@
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> mensaje(s) omitido(s)</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Dejarás de recibir mensajes de este grupo. El historial del chat se conservará.</string>
|
||||
<string name="view_security_code">Ver código de seguridad</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz debes permitir que tu contacto pueda enviarlos.</string>
|
||||
<string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos.</string>
|
||||
<string name="voice_messages_prohibited">¡Mensajes de voz prohibidos!</string>
|
||||
<string name="group_main_profile_sent">Tu perfil de chat se enviará a los miembros del grupo</string>
|
||||
<string name="group_main_profile_sent">Tu perfil Chat será enviado a los miembros del grupo</string>
|
||||
<string name="icon_descr_address">Dirección <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="image_descr_simplex_logo">Logo <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="icon_descr_simplex_team">Equipo <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="your_profile_will_be_sent">Tu perfil de chat se enviará a tu contacto</string>
|
||||
<string name="your_chat_profiles">Tus perfiles de chat</string>
|
||||
<string name="your_simplex_contact_address">Tu dirección de contacto <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="your_profile_will_be_sent">Tu perfil Chat se enviará a tu contacto</string>
|
||||
<string name="your_chat_profiles">Mis perfiles</string>
|
||||
<string name="your_simplex_contact_address">Mi dirección de contacto <xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="smp_servers_your_server">Tu servidor</string>
|
||||
<string name="smp_servers_your_server_address">Dirección de tu servidor</string>
|
||||
<string name="section_title_welcome_message">MENSAJE DE BIENVENIDA</string>
|
||||
<string name="your_current_profile">Tu perfil actual</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.
|
||||
\n
|
||||
\nLos servidores <xliff:g id="appName">SimpleX</xliff:g> no pueden ver tu perfil.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. Los servidores <xliff:g id="appName">SimpleX</xliff:g> no pueden ver tu perfil.</string>
|
||||
<string name="language_system">Sistema</string>
|
||||
<string name="button_add_welcome_message">Agregar mensaje de bienvenida</string>
|
||||
<string name="v4_6_audio_video_calls">Llamadas y videollamadas</string>
|
||||
@@ -993,7 +990,7 @@
|
||||
<string name="user_hide">Ocultar</string>
|
||||
<string name="user_mute">Silenciar</string>
|
||||
<string name="save_and_update_group_profile">Guardar y actualizar perfil del grupo</string>
|
||||
<string name="tap_to_activate_profile">Pulsa para activar el perfil.</string>
|
||||
<string name="tap_to_activate_profile">Pulsa sobre un perfil para activarlo.</string>
|
||||
<string name="should_be_at_least_one_visible_profile">Debe haber al menos un perfil de usuario visible.</string>
|
||||
<string name="user_unhide">Mostrar</string>
|
||||
<string name="button_welcome_message">Mensaje de bienvenida</string>
|
||||
@@ -1004,7 +1001,7 @@
|
||||
<string name="muted_when_inactive">¡Silenciado cuando está inactivo!</string>
|
||||
<string name="v4_6_group_moderation">Moderación de grupos</string>
|
||||
<string name="v4_6_hidden_chat_profiles">Perfiles Chat ocultos</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">¡Proteje los perfiles de Chat con contraseña!</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">¡Protege tus perfiles con contraseña!</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Soporte bluetooth y otras mejoras.</string>
|
||||
<string name="v4_6_group_welcome_message_descr">¡Establece el mensaje mostrado a los miembros nuevos!</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Interfaz en chino y español</string>
|
||||
@@ -1023,4 +1020,124 @@
|
||||
\n- borrar mensajes de los miembros.
|
||||
\n- desactivar el rol a miembros (a rol \"observador\")</string>
|
||||
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Tus perfiles Chat.</string>
|
||||
<string name="database_upgrade">Actualización de la base de datos</string>
|
||||
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
|
||||
<string name="invalid_migration_confirmation">Confirmación de migración no válida</string>
|
||||
<string name="upgrade_and_open_chat">Actualizar y abrir Chat</string>
|
||||
<string name="database_migrations">Migraciones: %s</string>
|
||||
<string name="mtr_error_different">migración diferente en la aplicación/base de datos: %s / %s</string>
|
||||
<string name="downgrade_and_open_chat">Volver a versión anterior y abrir Chat</string>
|
||||
<string name="database_downgrade_warning">Atención: ¡puedes perder algunos datos!</string>
|
||||
<string name="incompatible_database_version">Versión de base de datos incompatible</string>
|
||||
<string name="confirm_database_upgrades">Confirmar actualizaciones de la bases de datos</string>
|
||||
<string name="mtr_error_no_down_migration">la versión de la base de datos es más reciente que la aplicación, pero no hay migración hacia versión anterior para: %s</string>
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
|
||||
<string name="developer_options">ID de base de datos y opción de aislamiento de transporte.</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">El archivo se recibirá cuando tu contacto termine de subirlo.</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">La imagen se recibirá cuando tu contacto termine de subirla.</string>
|
||||
<string name="show_developer_options">Mostrar opciones de desarrollador</string>
|
||||
<string name="hide_dev_options">Ocultar:</string>
|
||||
<string name="show_dev_options">Mostrar:</string>
|
||||
<string name="cancel_file__question">¿Cancelar el envío de archivos\?</string>
|
||||
<string name="file_transfer_will_be_cancelled_warning">El envío de archivos será cancelado. Si está en progreso se detendrá.</string>
|
||||
<string name="delete_chat_profile">Eliminar perfil de chat</string>
|
||||
<string name="profile_password">Contraseña del perfil</string>
|
||||
<string name="unhide_chat_profile">Mostrar perfil de chat</string>
|
||||
<string name="unhide_profile">Mostrar perfil</string>
|
||||
<string name="delete_profile">Eliminar perfil</string>
|
||||
<string name="video_descr">Vídeo</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">El vídeo se recibirá cuando tu contacto esté en línea, por favor espera o compruébalo más tarde.</string>
|
||||
<string name="waiting_for_video">Esperando el vídeo</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Ha pedido recibir el video</string>
|
||||
<string name="videos_limit_title">¡Demasiados vídeos!</string>
|
||||
<string name="icon_descr_video_snd_complete">Vídeo enviado</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">El vídeo se recibirá cuando tu contacto termine de subirlo.</string>
|
||||
<string name="videos_limit_desc">Solo se pueden enviar 10 vídeos de forma simultánea</string>
|
||||
<string name="icon_descr_waiting_for_video">Esperando el vídeo</string>
|
||||
<string name="error_saving_xftp_servers">Error guardando servidores SMP</string>
|
||||
<string name="error_loading_xftp_servers">Error cargando servidores XFTP</string>
|
||||
<string name="error_loading_smp_servers">Error cargando servidores SMP</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Asegúrate de que las direcciones del servidor XFTP tienen el formato correcto, están separadas por líneas y no están duplicadas.</string>
|
||||
<string name="error_xftp_test_server_auth">El servidor requiere autorización para subir, comprueba la contraseña</string>
|
||||
<string name="smp_server_test_compare_file">Comparar archivo</string>
|
||||
<string name="smp_server_test_create_file">Crear archivo</string>
|
||||
<string name="smp_server_test_delete_file">Eliminar archivo</string>
|
||||
<string name="smp_server_test_upload_file">Subir archivo</string>
|
||||
<string name="xftp_servers">Servidores XFTP</string>
|
||||
<string name="your_XFTP_servers">Tus servidores XFTP</string>
|
||||
<string name="port_verb">Puerto</string>
|
||||
<string name="network_proxy_port">puerto %d</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Establece <i>Usar hosts .onion</i> en No si el proxy SOCKS no los admite.</string>
|
||||
<string name="smp_server_test_download_file">Descargar archivo</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Usar proxy SOCKS</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="network_socks_proxy_settings">Configuración proxy SOCKS</string>
|
||||
<string name="la_authenticate">Autenticar</string>
|
||||
<string name="la_current_app_passcode">Código de acceso actual</string>
|
||||
<string name="authentication_cancelled">Autenticación cancelada</string>
|
||||
<string name="change_lock_mode">Cambiar el modo de bloqueo</string>
|
||||
<string name="la_seconds">%d segundos</string>
|
||||
<string name="la_auth_failed">Error de autenticación</string>
|
||||
<string name="la_change_app_passcode">Cambiar el código de acceso</string>
|
||||
<string name="passcode_not_changed">¡Código de acceso no cambiado!</string>
|
||||
<string name="la_lock_mode_system">Autenticación del sistema</string>
|
||||
<string name="la_no_app_password">Sin código de acceso de la aplicación</string>
|
||||
<string name="la_lock_mode_passcode">Campo de código de acceso</string>
|
||||
<string name="la_lock_mode">Modo Bloqueo SimpleX</string>
|
||||
<string name="la_could_not_be_verified">No has podido ser verificado. Inténtelo de nuevo.</string>
|
||||
<string name="la_minutes">%d minutos</string>
|
||||
<string name="la_enter_app_passcode">Introducir el código de acceso</string>
|
||||
<string name="la_immediately">Inmediatamente</string>
|
||||
<string name="la_please_remember_to_store_password">Por favor, recuerda y guarda la contraseña en un lugar seguro. ¡No hay ninguna manera de recuperar una contraseña perdida!</string>
|
||||
<string name="lock_not_enabled">¡Bloqueo SimpleX no activado!</string>
|
||||
<string name="you_can_turn_on_lock">Puedes activar el Bloqueo SimpleX a través de Configuración.</string>
|
||||
<string name="confirm_passcode">Confirmar el código de acceso</string>
|
||||
<string name="enable_lock">Activar bloqueo</string>
|
||||
<string name="lock_after">Bloquear después de</string>
|
||||
<string name="lock_mode">Modo de bloqueo</string>
|
||||
<string name="submit_passcode">Enviar</string>
|
||||
<string name="incorrect_passcode">Código de acceso incorrecto</string>
|
||||
<string name="new_passcode">Nuevo código de acceso</string>
|
||||
<string name="la_mode_passcode">Código de acceso</string>
|
||||
<string name="passcode_changed">¡Código de acceso cambiado!</string>
|
||||
<string name="passcode_set">¡Código de acceso guardado!</string>
|
||||
<string name="la_mode_system">Sistema</string>
|
||||
<string name="decryption_error">Error de descifrado</string>
|
||||
<string name="alert_text_msg_bad_id">El ID del siguiente mensaje es incorrecto (menor o igual que el anterior).
|
||||
\nPuede ocurrir por algún bug o cuando la conexión está comprometida.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Por favor, informa a los desarrolladores.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> mensajes omitidos.</string>
|
||||
<string name="alert_title_msg_bad_hash">Hash de mensaje incorrecto</string>
|
||||
<string name="alert_title_msg_bad_id">ID de mensaje incorrecto</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Puede ocurrir cuando tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.</string>
|
||||
<string name="alert_text_msg_bad_hash">El hash del mensaje anterior es diferente.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">El error es permanente para esta conexión, por favor vuelve a conectarte.</string>
|
||||
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> mensajes no pudieron ser descifrados.</string>
|
||||
<string name="no_spaces">¡Sin espacios!</string>
|
||||
<string name="stop_file__action">Detener archivo</string>
|
||||
<string name="revoke_file__message">El archivo será eliminado de los servidores.</string>
|
||||
<string name="stop_rcv_file__message">Se detendrá la recepción del archivo.</string>
|
||||
<string name="revoke_file__confirm">Revocar</string>
|
||||
<string name="revoke_file__action">Revocar archivo</string>
|
||||
<string name="revoke_file__title">¿Revocar archivo\?</string>
|
||||
<string name="stop_snd_file__message">Se detendrá el envío del archivo.</string>
|
||||
<string name="stop_file__confirm">Detener</string>
|
||||
<string name="stop_rcv_file__title">¿Dejar de recibir el archivo\?</string>
|
||||
<string name="stop_snd_file__title">¿Dejar de enviar el archivo\?</string>
|
||||
<string name="allow_calls_only_if">Se permiten llamadas sólo si tu contacto también las permite.</string>
|
||||
<string name="allow_your_contacts_to_call">Permites que tus contactos puedan llamarte.</string>
|
||||
<string name="audio_video_calls">Llamadas/Videollamadas</string>
|
||||
<string name="calls_prohibited_with_this_contact">Las llamadas/videollamadas no están permitidas.</string>
|
||||
<string name="available_in_v51">"
|
||||
\nDisponible en v5.1"</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Tanto tú como tu contacto podéis realizar llamadas.</string>
|
||||
<string name="only_you_can_make_calls">Solo tú puedes realizar llamadas.</string>
|
||||
<string name="only_your_contact_can_make_calls">Sólo tu contacto puede realizar llamadas.</string>
|
||||
<string name="prohibit_calls">Prohibir las llamadas/videollamadas.</string>
|
||||
<string name="v5_0_app_passcode">Código de acceso a la aplicación</string>
|
||||
<string name="v5_0_polish_interface">Interfaz polaco</string>
|
||||
<string name="v5_0_app_passcode_descr">Úsalo en lugar de la autenticación del sistema.</string>
|
||||
<string name="v5_0_polish_interface_descr">Agradecimientos a los usuarios. ¡Contribuye a través de Weblate!</string>
|
||||
<string name="v5_0_large_files_support">Vídeos y archivos de hasta 1Gb</string>
|
||||
<string name="v5_0_large_files_support_descr">¡Rápido y sin tener que esperar a que el remitente esté en línea!</string>
|
||||
</resources>
|
||||
@@ -156,7 +156,7 @@
|
||||
<string name="contact_connection_pending">connexion…</string>
|
||||
<string name="group_connection_pending">connexion…</string>
|
||||
<string name="share_message">Partager le message…</string>
|
||||
<string name="share_image">Partager l’image…</string>
|
||||
<string name="share_image">Partager le média…</string>
|
||||
<string name="images_limit_desc">Envoi de 10 images en même temps maximum</string>
|
||||
<string name="personal_welcome">Bienvenue <xliff:g>%1$s</xliff:g> !</string>
|
||||
<string name="notifications_mode_periodic_desc">Vérifie les nouveaux messages toutes les 10 minutes pendant 1 minute au maximum.</string>
|
||||
@@ -201,7 +201,9 @@
|
||||
<string name="to_share_with_your_contact">(à partager avec votre contact)</string>
|
||||
<string name="create_group">Créer un groupe secret</string>
|
||||
<string name="from_gallery_button">Depuis la Phototèque</string>
|
||||
<string name="choose_file">Choisir le fichier</string>
|
||||
<string name="choose_file">Fichier</string>
|
||||
<string name="gallery_image_button">Image</string>
|
||||
<string name="gallery_video_button">Vidéo</string>
|
||||
<string name="to_start_a_new_chat_help_header">Pour démarrer une nouvelle discussion</string>
|
||||
<string name="chat_help_tap_button">Appuyez sur le bouton</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scanner un code QR</b> : pour vous connecter à votre contact qui vous montre un code QR.</string>
|
||||
@@ -294,7 +296,7 @@
|
||||
<string name="waiting_for_file">En attente du fichier</string>
|
||||
<string name="voice_message">Message vocal</string>
|
||||
<string name="toast_permission_denied">Autorisation refusée !</string>
|
||||
<string name="use_camera_button">Utiliser l\'Appareil photo</string>
|
||||
<string name="use_camera_button">Appareil\nphoto</string>
|
||||
<string name="thank_you_for_installing_simplex">Merci d\'avoir installé <xliff:g id="appNameFull">SimpleX Chat</xliff:g> !</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Vous pouvez <font color="#0088ff">vous connecter aux développeurs de <xliff:g id="appNameFull">SimpleX Chat</xliff:g> pour leur poser des questions et recevoir des réponses :</font>.</string>
|
||||
<string name="above_then_preposition_continuation">ci-dessus, puis :</string>
|
||||
@@ -437,9 +439,7 @@
|
||||
<string name="section_title_welcome_message">MESSAGE DE BIENVENUE</string>
|
||||
<string name="display_name__field">Nom affiché :</string>
|
||||
<string name="full_name__field">Nom complet :</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts.
|
||||
\n
|
||||
\nLes serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts. Les serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
|
||||
<string name="delete_image">Supprimer l\'image</string>
|
||||
<string name="save_preferences_question">Sauvegarder les préférences \?</string>
|
||||
<string name="you_control_your_chat">Vous maîtrisez vos discussions !</string>
|
||||
@@ -475,12 +475,10 @@
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> message⸱s manqué⸱s</string>
|
||||
<string name="integrity_msg_bad_id">ID de message incorrecte</string>
|
||||
<string name="settings_section_title_settings">PARAMÈTRES</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">C\'est possible quand :
|
||||
\n1. Les messages expirent du serveur (après 30 jours si ils ne sont pas reçu).
|
||||
\n2. Le serveur que vous utilisez pour recevoir les messages de ce contact a été mise à jour ou redémarré.
|
||||
\n3. La connection est compromise.
|
||||
\nVeuillez vous connecter aux développeurs via les Paramètres pour recevoir les mises à jour concernant les serveurs.
|
||||
\nNous allons ajouter une redondance des serveurs pour éviter la perte de messages.</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Cela peut arriver quand :
|
||||
\n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours.
|
||||
\n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.
|
||||
\n3. La connexion a été compromise.</string>
|
||||
<string name="icon_descr_call_rejected">Appel rejeté</string>
|
||||
<string name="rcv_group_event_member_deleted">a retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="snd_group_event_member_deleted">vous avez retiré <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
@@ -549,7 +547,7 @@
|
||||
<string name="settings_experimental_features">Fonctionnalités expérimentales</string>
|
||||
<string name="settings_section_title_socks">SOCKS PROXY</string>
|
||||
<string name="settings_section_title_themes">THEMES</string>
|
||||
<string name="settings_section_title_messages">MESSAGES</string>
|
||||
<string name="settings_section_title_messages">MESSAGES ET FICHIERS</string>
|
||||
<string name="settings_section_title_calls">APPELS</string>
|
||||
<string name="import_database">Importer la base de données</string>
|
||||
<string name="new_database_archive">Nouvelle archive de base de données</string>
|
||||
@@ -744,7 +742,7 @@
|
||||
<string name="member_role_will_be_changed_with_notification">Le rôle sera changé pour «%s». Les membres du groupe seront notifiés.</string>
|
||||
<string name="icon_descr_contact_checked">Contact vérifié⸱e</string>
|
||||
<string name="clear_contacts_selection_button">Effacer</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contact·s sélectionné·e·s</string>
|
||||
<string name="num_contacts_selected">%d contact·s sélectionné·e·s</string>
|
||||
<string name="skip_inviting_button">Passer l’invitation de membres</string>
|
||||
<string name="select_contacts">Sélectionnez des contacts</string>
|
||||
<string name="no_contacts_selected">Aucun contact sélectionné</string>
|
||||
@@ -854,7 +852,7 @@
|
||||
<string name="group_is_decentralized">Le groupe est entièrement décentralisé – il n\'est visible que par ses membres.</string>
|
||||
<string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string>
|
||||
<string name="network_options_revert">Revenir en arrière</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Interdit l’envoi de messages éphémères.</string>
|
||||
<string name="prohibit_sending_disappearing_messages">Interdire l’envoi de messages éphémères.</string>
|
||||
<string name="incognito_info_protects">Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé.</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string>
|
||||
<string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</string>
|
||||
@@ -887,7 +885,7 @@
|
||||
<string name="allow_to_send_voice">Autoriser l\'envoi de messages vocaux.</string>
|
||||
<string name="prohibit_sending_voice">Interdire l\'envoi de messages vocaux.</string>
|
||||
<string name="allow_to_send_disappearing">Autorise l’envoi de messages éphémères.</string>
|
||||
<string name="prohibit_sending_disappearing">Interdit l’envoi de messages éphémères.</string>
|
||||
<string name="prohibit_sending_disappearing">Interdire l’envoi de messages éphémères.</string>
|
||||
<string name="prohibit_message_deletion">Interdire la suppression irréversible des messages.</string>
|
||||
<string name="group_members_can_send_dms">Les membres du groupe peuvent envoyer des messages directs.</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">Les messages directs entre membres sont interdits dans ce groupe.</string>
|
||||
@@ -925,7 +923,6 @@
|
||||
<string name="core_simplexmq_version">simplexmq : v%s (%2s)</string>
|
||||
<string name="app_version_code">Build de l\'app : %s</string>
|
||||
<string name="app_version_name">Version de l\'app : v%s</string>
|
||||
<string name="core_build_timestamp">Cœur compilé le : %s</string>
|
||||
<string name="core_version">Version du cœur : v%s</string>
|
||||
<string name="network_option_ping_count">Nombre de PING</string>
|
||||
<string name="users_delete_all_chats_deleted">Toutes les discussions et tous les messages seront supprimés - il est impossible de revenir en arrière !</string>
|
||||
@@ -942,7 +939,6 @@
|
||||
<string name="users_delete_with_connections">Profil et connexions au serveur</string>
|
||||
<string name="network_session_mode_transport_isolation">Isolement du transport</string>
|
||||
<string name="update_network_session_mode_question">Mettre à jour le mode d\'isolation du transport \?</string>
|
||||
<string name="your_chat_profiles_stored_locally">Vos profils de chat sont stockés localement, uniquement sur votre appareil</string>
|
||||
<string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>.
|
||||
\n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string>
|
||||
<string name="network_session_mode_user">Profil de chat</string>
|
||||
@@ -991,7 +987,7 @@
|
||||
<string name="confirm_password">Confirmer le mot de passe</string>
|
||||
<string name="v4_6_reduced_battery_usage">Réduction accrue de l\'utilisation de la batterie</string>
|
||||
<string name="v4_6_chinese_spanish_interface">Interface en chinois et en espagnol</string>
|
||||
<string name="enter_password_to_show">Entrez le mot de passe ci-dessus pour continuer !</string>
|
||||
<string name="enter_password_to_show">Entrez le mot de passe dans le champ de recherche</string>
|
||||
<string name="v4_6_audio_video_calls">Appels audio et vidéo</string>
|
||||
<string name="v4_6_group_welcome_message">Message d\'accueil du groupe</string>
|
||||
<string name="error_saving_user_password">Erreur d\'enregistrement du mot de passe de l\'utilisateur</string>
|
||||
@@ -1006,13 +1002,13 @@
|
||||
<string name="hide_profile">Masquer le profil</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">D\'autres améliorations sont à venir !</string>
|
||||
<string name="password_to_show">Mot de passe à afficher</string>
|
||||
<string name="make_profile_private">Rendre le profil privé !</string>
|
||||
<string name="make_profile_private">Rendre un profil privé !</string>
|
||||
<string name="user_mute">Mute</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Protégez vos profils de chat par un mot de passe !</string>
|
||||
<string name="tap_to_activate_profile">Appuyez pour activer le profil.</string>
|
||||
<string name="save_and_update_group_profile">Sauvegarder et mettre à jour le profil du groupe</string>
|
||||
<string name="save_profile_password">Enregistrer le mot de passe du profil</string>
|
||||
<string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez un mot de passe complet dans le champ de recherche de la page Profils de chat.</string>
|
||||
<string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page Profils de chat.</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Prise en charge du Bluetooth et autres améliorations.</string>
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="should_be_at_least_one_profile">Il doit y avoir au moins un profil d\'utilisateur.</string>
|
||||
@@ -1023,4 +1019,124 @@
|
||||
<string name="group_welcome_title">Message d\'accueil</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Vous pouvez masquer ou mettre en sourdine un profil d\'utilisateur - maintenez-le enfoncé pour accéder au menu.</string>
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Vous continuerez à recevoir des appels et des notifications des profils mis en sourdine lorsqu\'ils sont actifs.</string>
|
||||
<string name="database_downgrade">Rétrogradation de la base de données</string>
|
||||
<string name="database_upgrade">Mise à niveau de la base de données</string>
|
||||
<string name="incompatible_database_version">Version de la base de données incompatible</string>
|
||||
<string name="downgrade_and_open_chat">Rétrograder et ouvrir le chat</string>
|
||||
<string name="invalid_migration_confirmation">Confirmation de migration invalide</string>
|
||||
<string name="upgrade_and_open_chat">Mettre à niveau et ouvrir le chat</string>
|
||||
<string name="database_migrations">Migrations : %s</string>
|
||||
<string name="database_downgrade_warning">Attention : vous risquez de perdre des données !</string>
|
||||
<string name="confirm_database_upgrades">Confirmer la mise à niveau de la base de données</string>
|
||||
<string name="mtr_error_no_down_migration">la base de données a une version plus récente que celle de l\'application, mais il n\'y a pas de rétrogradation pour : %s</string>
|
||||
<string name="mtr_error_different">migration différente dans l\'app/la base de données : %s / %s</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">L\'image sera reçue lorsque votre contact aura terminé de la mettre en ligne.</string>
|
||||
<string name="show_dev_options">Afficher :</string>
|
||||
<string name="show_developer_options">Afficher les options pour les développeurs</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">Le fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.</string>
|
||||
<string name="developer_options">IDs de base de données et option d\'isolation du transport.</string>
|
||||
<string name="settings_section_title_experimenta">EXPÉRIMENTALE</string>
|
||||
<string name="hide_dev_options">Cacher :</string>
|
||||
<string name="unhide_chat_profile">Dévoiler le profil de chat</string>
|
||||
<string name="unhide_profile">Dévoiler le profil</string>
|
||||
<string name="delete_chat_profile">Supprimer le profil de chat</string>
|
||||
<string name="delete_profile">Supprimer le profil</string>
|
||||
<string name="cancel_file__question">Annuler le transfert de fichiers \?</string>
|
||||
<string name="file_transfer_will_be_cancelled_warning">Le transfert de fichiers sera annulé. S\'il est en cours, il sera interrompu.</string>
|
||||
<string name="profile_password">Mot de passe de profil</string>
|
||||
<string name="videos_limit_title">Trop de vidéos !</string>
|
||||
<string name="video_descr">Vidéo</string>
|
||||
<string name="icon_descr_video_snd_complete">Vidéo envoyée</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer.</string>
|
||||
<string name="icon_descr_waiting_for_video">En attente de la vidéo</string>
|
||||
<string name="waiting_for_video">En attente de la vidéo</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Requête de réception de la vidéo</string>
|
||||
<string name="videos_limit_desc">Seulement 10 vidéos peuvent être envoyées en même temps</string>
|
||||
<string name="smp_server_test_create_file">Créer un fichier</string>
|
||||
<string name="smp_server_test_delete_file">Supprimer le fichier</string>
|
||||
<string name="error_saving_xftp_servers">Erreur lors de la sauvegarde des serveurs XFTP</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées.</string>
|
||||
<string name="error_xftp_test_server_auth">Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe</string>
|
||||
<string name="smp_server_test_upload_file">Transférer le fichier</string>
|
||||
<string name="xftp_servers">Serveurs XFTP</string>
|
||||
<string name="your_XFTP_servers">Vos serveurs XFTP</string>
|
||||
<string name="smp_server_test_compare_file">Comparer le fichier</string>
|
||||
<string name="smp_server_test_download_file">Télécharger le fichier</string>
|
||||
<string name="error_loading_smp_servers">Erreur lors du chargement des serveurs SMP</string>
|
||||
<string name="error_loading_xftp_servers">Erreur lors du chargement des serveurs XFTP</string>
|
||||
<string name="host_verb">Héberger</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="network_proxy_port">port %d</string>
|
||||
<string name="network_socks_proxy_settings">Paramètres de proxy SOCKS</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Utiliser un proxy SOCKS</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Définissez <i>Utiliser les hôtes .onion</i> sur Non si le proxy SOCKS ne les prend pas en charge.</string>
|
||||
<string name="la_lock_mode">Mode de SimpleX Lock</string>
|
||||
<string name="lock_not_enabled">SimpleX Lock n\'est pas activé !</string>
|
||||
<string name="la_lock_mode_system">Authentification du système</string>
|
||||
<string name="la_authenticate">Authentification</string>
|
||||
<string name="la_auth_failed">Echec de l\'authentification</string>
|
||||
<string name="la_change_app_passcode">Modifier le code d\'accès</string>
|
||||
<string name="la_current_app_passcode">Code d\'accès actuel</string>
|
||||
<string name="la_minutes">%d minutes</string>
|
||||
<string name="la_seconds">%d secondes</string>
|
||||
<string name="la_enter_app_passcode">Entrer le code d\'accès</string>
|
||||
<string name="la_immediately">Immédiatement</string>
|
||||
<string name="la_no_app_password">Pas de code d\'accès à l\'app</string>
|
||||
<string name="la_lock_mode_passcode">Saisie du code</string>
|
||||
<string name="la_please_remember_to_store_password">Veuillez le mémoriser ou le conserver en toute sécurité - il n\'y a aucun moyen de récupérer un mot de passe perdu !</string>
|
||||
<string name="enable_lock">Activer le verrouillage</string>
|
||||
<string name="change_lock_mode">Modifier le mode de verrouillage</string>
|
||||
<string name="incorrect_passcode">Code d\'accès erroné</string>
|
||||
<string name="new_passcode">Nouveau code d\'accès</string>
|
||||
<string name="la_mode_passcode">Code d\'accès</string>
|
||||
<string name="passcode_changed">Code d\'accès modifié !</string>
|
||||
<string name="passcode_not_changed">Le code d\'accès n\'a pas été modifié !</string>
|
||||
<string name="passcode_set">Code d\'accès défini !</string>
|
||||
<string name="la_mode_system">Système</string>
|
||||
<string name="authentication_cancelled">Authentification annulée</string>
|
||||
<string name="alert_title_msg_bad_id">Mauvais ID de message</string>
|
||||
<string name="alert_text_msg_bad_hash">Le hash du message précédent est différent.</string>
|
||||
<string name="alert_text_msg_bad_id">L\'ID du message suivant est incorrect (inférieur ou égal au précédent).
|
||||
\nCela peut se produire en raison d\'un bug ou lorsque la connexion est compromise.</string>
|
||||
<string name="decryption_error">Erreur de déchiffrement</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Cela peut se produire lorsque vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.</string>
|
||||
<string name="alert_title_msg_bad_hash">Mauvais hash de message</string>
|
||||
<string name="no_spaces">Pas d\'espace !</string>
|
||||
<string name="allow_calls_only_if">Autoriser les appels que si votre contact les autorise.</string>
|
||||
<string name="allow_your_contacts_to_call">Autorise vos contacts à vous appeler.</string>
|
||||
<string name="audio_video_calls">Appels audio/vidéo</string>
|
||||
<string name="available_in_v51">"
|
||||
\nDisponible dans la v5.1"</string>
|
||||
<string name="prohibit_calls">Interdire les appels audio/vidéo.</string>
|
||||
<string name="revoke_file__message">Le fichier sera supprimé des serveurs.</string>
|
||||
<string name="revoke_file__confirm">Révoquer</string>
|
||||
<string name="revoke_file__action">Révoquer le fichier</string>
|
||||
<string name="revoke_file__title">Révoquer le fichier \?</string>
|
||||
<string name="stop_snd_file__message">L\'envoi du fichier sera interrompu.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Veuillez le signaler aux développeurs.</string>
|
||||
<string name="submit_passcode">Soumettre</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Vous et votre contact pouvez tous deux passer des appels.</string>
|
||||
<string name="only_you_can_make_calls">Vous seul pouvez passer des appels.</string>
|
||||
<string name="calls_prohibited_with_this_contact">Interdire les appels audio/vidéo.</string>
|
||||
<string name="confirm_passcode">Confirmer le code d\'accès</string>
|
||||
<string name="lock_after">Verrouillage après</string>
|
||||
<string name="lock_mode">Mode de verrouillage</string>
|
||||
<string name="only_your_contact_can_make_calls">Seul votre contact peut passer des appels.</string>
|
||||
<string name="stop_rcv_file__message">La réception du fichier sera interrompue.</string>
|
||||
<string name="stop_file__confirm">Arrêter</string>
|
||||
<string name="stop_snd_file__title">Arrêter l\'envoi du fichier \?</string>
|
||||
<string name="stop_file__action">Arrêter le fichier</string>
|
||||
<string name="stop_rcv_file__title">Arrêter de recevoir le fichier \?</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">Cette erreur est persistante pour cette connexion, veuillez vous reconnecter.</string>
|
||||
<string name="la_could_not_be_verified">Vous n\'avez pas pu être vérifié·e ; veuillez réessayer.</string>
|
||||
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messages n\'ont pas pu être déchiffrés.</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messages sautés.</string>
|
||||
<string name="you_can_turn_on_lock">Vous pouvez activer SimpleX Lock dans les Paramètres.</string>
|
||||
<string name="v5_0_polish_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string>
|
||||
<string name="v5_0_large_files_support">Vidéos et fichiers jusqu\'à 1Go</string>
|
||||
<string name="v5_0_app_passcode">Code d\'accès à l\'app</string>
|
||||
<string name="v5_0_large_files_support_descr">Rapide et ne nécessitant pas d\'attendre que l\'expéditeur soit en ligne !</string>
|
||||
<string name="v5_0_polish_interface">Interface en polonais</string>
|
||||
<string name="v5_0_app_passcode_descr">Il permet de remplacer l\'authentification du système.</string>
|
||||
</resources>
|
||||
@@ -180,4 +180,120 @@
|
||||
<string name="restore_database_alert_confirm">वापस लौटाना</string>
|
||||
<string name="a_plus_b">ए + बी</string>
|
||||
<string name="incognito_random_profile">आपका यादृच्छिक प्रोफ़ाइल</string>
|
||||
<string name="chat_preferences">चैट वरीयताएँ</string>
|
||||
<string name="ttl_days">%d दिनों</string>
|
||||
<string name="allow_voice_messages_question">ध्वनि संदेशों की अनुमति दें\?</string>
|
||||
<string name="clear_chat_menu_action">साफ़</string>
|
||||
<string name="icon_descr_audio_off">ऑडियो बंद</string>
|
||||
<string name="icon_descr_audio_on">ऑडियो चालू</string>
|
||||
<string name="v4_4_disappearing_messages">गायब होने वाले संदेश</string>
|
||||
<string name="v4_5_reduced_battery_usage_descr">अधिक सुधार जल्द ही आ रहे हैं!</string>
|
||||
<string name="copy_verb">नकलना</string>
|
||||
<string name="change_role">भूमिका बदलें</string>
|
||||
<string name="ttl_hour">%d घंटा</string>
|
||||
<string name="auth_unavailable">प्रमाणीकरण अनुपलब्ध</string>
|
||||
<string name="icon_descr_edited">संपादित</string>
|
||||
<string name="delete_group_question">समूह मिटाएं\?</string>
|
||||
<string name="contact_already_exists">संपर्क पहले से मौजूद है</string>
|
||||
<string name="notification_preview_mode_contact">संपर्क नाम</string>
|
||||
<string name="notification_preview_somebody">संपर्क छुपाया गया:</string>
|
||||
<string name="delete_verb">मिटाना</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">जब आप पृष्ठभूमि में 30 सेकंड के बाद ऐप को प्रारंभ या फिर से शुरू करते हैं तो आपको प्रमाणित करने की आवश्यकता होगी।</string>
|
||||
<string name="icon_descr_asked_to_receive">प्रतिमा प्राप्त करने को कहा</string>
|
||||
<string name="back">वापस</string>
|
||||
<string name="icon_descr_close_button">बंद करें बटन</string>
|
||||
<string name="create_address">पता बनाना</string>
|
||||
<string name="invite_prohibited">संपर्क आमंत्रित नहीं कर सकते!</string>
|
||||
<string name="icon_descr_contact_checked">संपर्क जांचा गया</string>
|
||||
<string name="contact_preferences">संपर्क वरीयताएँ</string>
|
||||
<string name="rcv_group_event_changed_your_role">आपकी भूमिका को %s में बदल दिया</string>
|
||||
<string name="clear_contacts_selection_button">साफ़</string>
|
||||
<string name="timed_messages">गायब होने वाले संदेश</string>
|
||||
<string name="display_name__field">प्रदर्शित होने वाला नाम:</string>
|
||||
<string name="theme_dark">अँधेरा</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">अपने संपर्कों को ध्वनि संदेश भेजने की अनुमति दें।</string>
|
||||
<string name="ttl_day">%d दिन</string>
|
||||
<string name="ttl_hours">%d घंटे</string>
|
||||
<string name="v4_6_chinese_spanish_interface">चीनी और स्पेनिश इंटरफ़ेस</string>
|
||||
<string name="simplex_link_mode_description">विवरण</string>
|
||||
<string name="notifications_mode_off_desc">ऐप केवल तभी सूचनाएं प्राप्त कर सकता है जब वह चल रहा हो, कोई पृष्ठभूमि सेवा प्रारंभ नहीं की जाएगी</string>
|
||||
<string name="edit_verb">संपादन करना</string>
|
||||
<string name="decentralized">विकेन्द्रीकृत</string>
|
||||
<string name="icon_descr_call_ended">कॉल समाप्त</string>
|
||||
<string name="icon_descr_call_progress">कॉल चल रहा है</string>
|
||||
<string name="auto_accept_images">छवियों को स्वत: स्वीकार करें</string>
|
||||
<string name="chat_archive_header">चैट संग्रह</string>
|
||||
<string name="chat_is_stopped_indication">चैट रोक दी गई है</string>
|
||||
<string name="chat_archive_section">चैट संग्रह</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">आप इस समूह से संदेश प्राप्त करना बंद कर देंगे। चैट इतिहास संरक्षित किया जाएगा।</string>
|
||||
<string name="rcv_group_event_changed_member_role">%s की भूमिका को %s में बदला</string>
|
||||
<string name="group_member_status_complete">पूर्ण</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">इस चैट में गायब होने वाले संदेश प्रतिबंधित हैं।</string>
|
||||
<string name="disappearing_messages_are_prohibited">इस चैट में गायब होने वाले संदेश प्रतिबंधित हैं।</string>
|
||||
<string name="chat_with_developers">डेवलपर्स के साथ चैट करें</string>
|
||||
<string name="alert_title_contact_connection_pending">संपर्क अभी तक जुड़ा नहीं है!</string>
|
||||
<string name="chat_console">चैट कंसोल</string>
|
||||
<string name="all_your_contacts_will_remain_connected">आपके सभी संपर्क जुड़े रहेंगे।</string>
|
||||
<string name="network_session_mode_user">चैट प्रोफ़ाइल</string>
|
||||
<string name="contact_requests">संपर्क अनुरोध</string>
|
||||
<string name="create_profile_button">बनाएं</string>
|
||||
<string name="callstatus_error">कॉल त्रुटि</string>
|
||||
<string name="callstatus_in_progress">कॉल चल रहा है</string>
|
||||
<string name="always_use_relay">हमेशा रिले का प्रयोग करें</string>
|
||||
<string name="icon_descr_audio_call">ऑडियो कॉल</string>
|
||||
<string name="settings_audio_video_calls">ऑडियो और वीडियो कॉल</string>
|
||||
<string name="call_already_ended">कॉल पहले ही खत्म हो चुकी है!</string>
|
||||
<string name="no_call_on_lock_screen">अक्षम करना</string>
|
||||
<string name="status_contact_has_e2e_encryption">संपर्क में ई2ई एन्क्रिप्शन है</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">संपर्क में कोई ई2ई एन्क्रिप्शन नहीं है</string>
|
||||
<string name="chat_is_running">चैट चल रही है</string>
|
||||
<string name="chat_is_stopped">चैट रोक दी गई है</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string>
|
||||
<string name="ttl_min">%d मिनट</string>
|
||||
<string name="settings_section_title_device">उपकरण</string>
|
||||
<string name="group_member_status_creator">रचनाकार</string>
|
||||
<string name="change_member_role_question">समूह भूमिका बदलें\?</string>
|
||||
<string name="ttl_week">%d सप्ताह</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">संपर्क अनुरोधों को स्वत: स्वीकार करें</string>
|
||||
<string name="integrity_msg_bad_hash">खराब संदेश हैश</string>
|
||||
<string name="allow_voice_messages_only_if">ध्वनि संदेशों को केवल तभी अनुमति दें यदि आपका संपर्क उन्हें अनुमति देता है।</string>
|
||||
<string name="incognito_random_profile_from_contact_description">जिस संपर्क से आपने यह लिंक प्राप्त किया है, उसे एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string>
|
||||
<string name="callstatus_calling">कॉल कर रहा है…</string>
|
||||
<string name="feature_cancelled_item">रद्द %s</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">आपके लिए पता बदल दिया</string>
|
||||
<string name="notifications_mode_periodic_desc">प्रत्येक 10 मिनट में 1 मिनट तक नए संदेशों की जाँच करता है</string>
|
||||
<string name="ttl_sec">%d सेकंड</string>
|
||||
<string name="ttl_month">%d महीना</string>
|
||||
<string name="ttl_months">%d महीने</string>
|
||||
<string name="settings_developer_tools">डेवलपर उपकरण</string>
|
||||
<string name="incognito_random_profile_description">आपके संपर्क को एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string>
|
||||
<string name="button_add_welcome_message">स्वागत संदेश जोड़ें</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">अपने संपर्कों को भेजे गए संदेशों को अपरिवर्तनीय रूप से हटाने की अनुमति दें।</string>
|
||||
<string name="allow_to_send_voice">ध्वनि संदेश भेजने की अनुमति दें।</string>
|
||||
<string name="alert_title_cant_invite_contacts">संपर्कों को आमंत्रित नहीं कर सकते!</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s के लिए पता बदल रहा है…</string>
|
||||
<string name="dont_show_again">दोबारा मत दिखाओ</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">आप और आपका संपर्क दोनों ध्वनि संदेश भेज सकते हैं।</string>
|
||||
<string name="settings_section_title_develop">विकास करना</string>
|
||||
<string name="user_mute">आवाज़ बंद करना</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">अपने संपर्कों को गायब होने वाले संदेश भेजने की अनुमति दें।</string>
|
||||
<string name="cancel_file__question">फ़ाइल स्थानांतरण रद्द करें\?</string>
|
||||
<string name="v4_6_audio_video_calls">ऑडियो और वीडियो कॉल</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">अधिक सुधार जल्द ही आ रहे हैं!</string>
|
||||
<string name="icon_descr_video_asked_to_receive">वीडियो प्राप्त करने के लिए कहा</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">संपर्क और सभी संदेशों को हटा दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
|
||||
<string name="ok">ठीक है</string>
|
||||
<string name="save_servers_button">बचाना</string>
|
||||
<string name="language_system">प्रणाली</string>
|
||||
<string name="theme_system">प्रणाली</string>
|
||||
<string name="network_options_save">बचाना</string>
|
||||
<string name="chat_preferences_on">चालू</string>
|
||||
<string name="la_lock_mode_system">प्रणाली प्रमाणीकरण</string>
|
||||
<string name="la_mode_system">प्रणाली</string>
|
||||
<string name="your_SMP_servers">आपका एसएमपी सर्वर</string>
|
||||
<string name="messages_section_title">संदेशों</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">सदस्य को समूह से निकाल दिया जाएगा - इसे पूर्ववत नहीं किया जा सकता!</string>
|
||||
<string name="member_info_section_title_member">सदस्य</string>
|
||||
<string name="group_member_role_member">सदस्य</string>
|
||||
</resources>
|
||||
@@ -152,7 +152,7 @@
|
||||
<string name="tap_to_start_new_chat">Tocca per iniziare una conversazione</string>
|
||||
<string name="chat_with_developers">Scrivi agli sviluppatori</string>
|
||||
<string name="you_have_no_chats">Non hai chat</string>
|
||||
<string name="share_image">Condividi immagine…</string>
|
||||
<string name="share_image">Condividi multimediale…</string>
|
||||
<string name="share_file">Condividi file…</string>
|
||||
<string name="icon_descr_context">Icona contestuale</string>
|
||||
<string name="icon_descr_cancel_file_preview">Annulla anteprima file</string>
|
||||
@@ -298,7 +298,7 @@
|
||||
<string name="display_name">Nome da mostrare</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>Aggiungi un contatto</b>: per creare il tuo codice QR una tantum per il tuo contatto.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Scansiona codice QR</b>: per connetterti al contatto che ti mostra il codice QR.</string>
|
||||
<string name="choose_file">Scegli file</string>
|
||||
<string name="choose_file">File</string>
|
||||
<string name="clear_chat_button">Svuota chat</string>
|
||||
<string name="clear_chat_question">Svuotare la chat\?</string>
|
||||
<string name="clear_verb">Svuota</string>
|
||||
@@ -308,6 +308,8 @@
|
||||
<string name="create_group">Crea gruppo segreto</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 desktop: scansiona dall\'app il codice QR mostrato, tramite <b>Scansiona codice QR</b>.</string>
|
||||
<string name="from_gallery_button">Dalla Galleria</string>
|
||||
<string name="gallery_image_button">Immagine</string>
|
||||
<string name="gallery_video_button">Video</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Se scegli di rifiutare, il mittente NON verrà avvisato.</string>
|
||||
<string name="clear_chat_menu_action">Svuota</string>
|
||||
<string name="icon_descr_close_button">Pulsante di chiusura</string>
|
||||
@@ -545,7 +547,7 @@
|
||||
<string name="to_connect_via_link_title">Per connettersi via link</string>
|
||||
<string name="to_share_with_your_contact">(da condividere con il tuo contatto)</string>
|
||||
<string name="to_start_a_new_chat_help_header">Per iniziare una nuova chat</string>
|
||||
<string name="use_camera_button">Usa la fotocamera</string>
|
||||
<string name="use_camera_button">Fotocamera</string>
|
||||
<string name="you_can_connect_to_simplex_chat_founder">Puoi <font color="#0088ff">connetterti con gli sviluppatori di <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per porre domande e ricevere aggiornamenti</font>.</string>
|
||||
<string name="invalid_contact_link">Link non valido!</string>
|
||||
<string name="invalid_QR_code">Codice QR non valido</string>
|
||||
@@ -665,9 +667,7 @@
|
||||
<string name="you_control_your_chat">Sei tu a controllare la tua chat!</string>
|
||||
<string name="your_current_profile">Il tuo profilo attuale</string>
|
||||
<string name="your_profile_is_stored_on_your_device">Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.
|
||||
\n
|
||||
\nI server di <xliff:g id="appName">SimpleX</xliff:g> non possono vedere il tuo profilo.</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di <xliff:g id="appName">SimpleX</xliff:g> non possono vedere il tuo profilo.</string>
|
||||
<string name="ignore">Ignora</string>
|
||||
<string name="immune_to_spam_and_abuse">Immune a spam e abusi</string>
|
||||
<string name="incoming_audio_call">Chiamata in arrivo</string>
|
||||
@@ -696,11 +696,9 @@
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g> vuole connettersi con te via</string>
|
||||
<string name="you_control_servers_to_receive_your_contacts_to_send">Puoi controllare attraverso quale/i server <b>ricevere</b> i messaggi, i tuoi contatti – i server che usi per inviare loro i messaggi.</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">Può accadere quando:
|
||||
\n1. I messaggi scadono sul server se non sono stati ricevuti per 30 giorni,
|
||||
\n2. Il server usato per ricevere i messaggi da questo contatto è stato aggiornato e riavviato.
|
||||
\n3. La connessione è compromessa.
|
||||
\nConnettiti agli sviluppatori tramite Impostazioni per ricevere aggiornamenti riguardo i server.
|
||||
\nAggiungeremo la ridondanza del server per prevenire la perdita di messaggi.</string>
|
||||
\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.
|
||||
\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio.
|
||||
\n3. La connessione è stata compromessa.</string>
|
||||
<string name="icon_descr_call_rejected">Chiamata rifiutata</string>
|
||||
<string name="icon_descr_call_missed">Chiamata persa</string>
|
||||
<string name="status_no_e2e_encryption">nessuna crittografia e2e</string>
|
||||
@@ -728,7 +726,7 @@
|
||||
<string name="import_database_question">Importare il database della chat\?</string>
|
||||
<string name="import_database">Importa database</string>
|
||||
<string name="settings_section_title_incognito">Modalità incognito</string>
|
||||
<string name="settings_section_title_messages">MESSAGGI</string>
|
||||
<string name="settings_section_title_messages">MESSAGGI E FILE</string>
|
||||
<string name="new_database_archive">Nuovo archivio database</string>
|
||||
<string name="old_database_archive">Vecchio archivio del database</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">Riavvia l\'app per creare un profilo di chat nuovo.</string>
|
||||
@@ -819,7 +817,7 @@
|
||||
<string name="button_send_direct_message">Invia messaggio diretto</string>
|
||||
<string name="skip_inviting_button">Salta l\'invito di membri</string>
|
||||
<string name="switch_verb">Cambia</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g> contatto/i selezionato/i</string>
|
||||
<string name="num_contacts_selected">%d contatto/i selezionato/i</string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> MEMBRI</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">Puoi condividere un link o un codice QR: chiunque potrà unirsi al gruppo. Non perderai i membri del gruppo se in seguito lo elimini.</string>
|
||||
<string name="invite_prohibited_description">Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale</string>
|
||||
@@ -927,7 +925,6 @@
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="app_version_code">Build dell\'app: %s</string>
|
||||
<string name="app_version_name">Versione app: v%s</string>
|
||||
<string name="core_build_timestamp">Core compilato il: %s</string>
|
||||
<string name="smp_servers_per_user">I server per le nuove connessioni del profilo di chat attuale</string>
|
||||
<string name="users_add">Aggiungi profilo</string>
|
||||
<string name="users_delete_question">Eliminare il profilo di chat\?</string>
|
||||
@@ -948,7 +945,6 @@
|
||||
<string name="delete_files_and_media_for_all_users">Elimina i file per tutti i profili di chat</string>
|
||||
<string name="failed_to_active_user_title">Errore nel cambio di profilo!</string>
|
||||
<string name="failed_to_create_user_title">Errore nella creazione del profilo!</string>
|
||||
<string name="your_chat_profiles_stored_locally">I tuoi profili di chat sono memorizzati localmente, solo sul tuo dispositivo</string>
|
||||
<string name="error_deleting_user">Errore nell\'eliminazione del profilo utente</string>
|
||||
<string name="users_delete_with_connections">Profilo e connessioni al server</string>
|
||||
<string name="your_chat_profiles">I tuoi profili di chat</string>
|
||||
@@ -986,7 +982,7 @@
|
||||
<string name="save_welcome_message_question">Salvare il messaggio di benvenuto\?</string>
|
||||
<string name="group_welcome_title">Messaggio di benvenuto</string>
|
||||
<string name="save_and_update_group_profile">Salva e aggiorna il profilo del gruppo</string>
|
||||
<string name="enter_password_to_show">Inserisci la password sopra per mostrare!</string>
|
||||
<string name="enter_password_to_show">Inserisci password nella ricerca</string>
|
||||
<string name="user_mute">Silenzia</string>
|
||||
<string name="tap_to_activate_profile">Tocca per attivare il profilo.</string>
|
||||
<string name="user_unhide">Svela</string>
|
||||
@@ -1023,4 +1019,124 @@
|
||||
<string name="user_hide">Nascondi</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Imposta il messaggio mostrato ai nuovi membri!</string>
|
||||
<string name="user_unmute">Riattiva audio</string>
|
||||
<string name="database_downgrade">Downgrade del database</string>
|
||||
<string name="database_upgrade">Aggiornamento del database</string>
|
||||
<string name="incompatible_database_version">Versione del database incompatibile</string>
|
||||
<string name="upgrade_and_open_chat">Aggiorna e apri chat</string>
|
||||
<string name="developer_options">ID del database e opzione isolamento del trasporto.</string>
|
||||
<string name="hide_dev_options">Nascondi:</string>
|
||||
<string name="show_dev_options">Mostra:</string>
|
||||
<string name="show_developer_options">Mostra opzioni sviluppatore</string>
|
||||
<string name="downgrade_and_open_chat">Esegui downgrade e apri chat</string>
|
||||
<string name="database_migrations">Migrazioni: %s</string>
|
||||
<string name="database_downgrade_warning">Attenzione: potresti perdere alcuni dati!</string>
|
||||
<string name="confirm_database_upgrades">Conferma aggiornamenti database</string>
|
||||
<string name="mtr_error_different">migrazione diversa nell\'app/nel database: %s / %s</string>
|
||||
<string name="invalid_migration_confirmation">Conferma di migrazione non valida</string>
|
||||
<string name="settings_section_title_experimenta">SPERIMENTALE</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">L\'immagine verrà ricevuta quando il tuo contatto completerà l\'invio.</string>
|
||||
<string name="mtr_error_no_down_migration">la versione del database è più recente di quella dell\'app, ma nessuna migrazione downgrade per: %s</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">Il file verrà ricevuto quando il tuo contatto completerà l\'invio.</string>
|
||||
<string name="cancel_file__question">Annullare il trasferimento di file\?</string>
|
||||
<string name="file_transfer_will_be_cancelled_warning">Il trasferimento di file verrà annullato. Se è in corso, verrà interrotto.</string>
|
||||
<string name="unhide_chat_profile">Svela il profilo chat</string>
|
||||
<string name="unhide_profile">Svela profilo</string>
|
||||
<string name="delete_chat_profile">Elimina il profilo di chat</string>
|
||||
<string name="delete_profile">Elimina profilo</string>
|
||||
<string name="profile_password">Password del profilo</string>
|
||||
<string name="videos_limit_desc">È possibile inviare solo 10 video contemporaneamente</string>
|
||||
<string name="videos_limit_title">Troppi video!</string>
|
||||
<string name="icon_descr_video_asked_to_receive">Richiesta di ricevere il video</string>
|
||||
<string name="video_descr">Video</string>
|
||||
<string name="icon_descr_video_snd_complete">Video inviato</string>
|
||||
<string name="video_will_be_received_when_contact_completes_uploading">Il video verrà ricevuto quando il tuo contatto completerà l\'invio.</string>
|
||||
<string name="video_will_be_received_when_contact_is_online">Il video verrà ricevuto quando il tuo contatto sarà in linea, attendi o controlla più tardi!</string>
|
||||
<string name="icon_descr_waiting_for_video">In attesa del video</string>
|
||||
<string name="waiting_for_video">In attesa del video</string>
|
||||
<string name="error_loading_xftp_servers">Errore nel caricamento dei server XFTP</string>
|
||||
<string name="error_saving_xftp_servers">Errore nel salvataggio dei server XFTP</string>
|
||||
<string name="error_xftp_test_server_auth">Il server richiede l\'autorizzazione per l\'invio, controlla la password</string>
|
||||
<string name="smp_server_test_compare_file">Confronta file</string>
|
||||
<string name="smp_server_test_create_file">Crea file</string>
|
||||
<string name="smp_server_test_download_file">Scarica file</string>
|
||||
<string name="smp_server_test_upload_file">Invia file</string>
|
||||
<string name="xftp_servers">Server XFTP</string>
|
||||
<string name="your_XFTP_servers">I tuoi server XFTP</string>
|
||||
<string name="network_socks_proxy_settings">Impostazioni proxy SOCKS</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Usa proxy SOCKS</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="port_verb">Porta</string>
|
||||
<string name="network_proxy_port">porta %d</string>
|
||||
<string name="disable_onion_hosts_when_not_supported">Imposta <i>Usa gli host .onion</i> su No se il proxy SOCKS non li supporta.</string>
|
||||
<string name="smp_server_test_delete_file">Elimina file</string>
|
||||
<string name="error_loading_smp_servers">Errore nel caricamento dei server SMP</string>
|
||||
<string name="ensure_xftp_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi del server XFTP siano nel formato corretto, uno per riga e non doppi.</string>
|
||||
<string name="la_auth_failed">Autenticazione fallita</string>
|
||||
<string name="la_lock_mode_passcode">Inserimento del codice di accesso</string>
|
||||
<string name="la_lock_mode">Modalità di SimpleX Lock</string>
|
||||
<string name="la_lock_mode_system">Autenticazione di sistema</string>
|
||||
<string name="la_authenticate">Autentica</string>
|
||||
<string name="la_change_app_passcode">Cambia codice di accesso</string>
|
||||
<string name="la_current_app_passcode">Codice di accesso attuale</string>
|
||||
<string name="la_minutes">%d minuti</string>
|
||||
<string name="la_seconds">%d secondi</string>
|
||||
<string name="la_enter_app_passcode">Inserisci il codice di accesso</string>
|
||||
<string name="la_immediately">Immediatamente</string>
|
||||
<string name="la_no_app_password">Nessun codice di accesso dell\'app</string>
|
||||
<string name="la_please_remember_to_store_password">Ricordala o conservala in modo sicuro: non c\'è modo di recuperare una password persa!</string>
|
||||
<string name="lock_not_enabled">SimpleX Lock non attivato!</string>
|
||||
<string name="you_can_turn_on_lock">Puoi attivare SimpleX Lock tramite le impostazioni.</string>
|
||||
<string name="la_could_not_be_verified">Non è stato possibile verificarti, riprova.</string>
|
||||
<string name="confirm_passcode">Conferma il codice di accesso</string>
|
||||
<string name="la_mode_passcode">Codice di accesso</string>
|
||||
<string name="passcode_changed">Codice di accesso cambiato!</string>
|
||||
<string name="passcode_not_changed">Codice di accesso non cambiato!</string>
|
||||
<string name="passcode_set">Codice di accesso impostato!</string>
|
||||
<string name="submit_passcode">Invia</string>
|
||||
<string name="la_mode_system">Sistema</string>
|
||||
<string name="change_lock_mode">Cambia modalità di blocco</string>
|
||||
<string name="authentication_cancelled">Autenticazione annullata</string>
|
||||
<string name="lock_after">Blocca dopo</string>
|
||||
<string name="enable_lock">Attiva blocco</string>
|
||||
<string name="incorrect_passcode">Codice di accesso errato</string>
|
||||
<string name="new_passcode">Nuovo codice di accesso</string>
|
||||
<string name="lock_mode">Modalità di blocco</string>
|
||||
<string name="decryption_error">Errore di decifrazione</string>
|
||||
<string name="alert_text_decryption_error_too_many_skipped"><xliff:g id="message count" example="1">%1$d</xliff:g> messaggi saltati.</string>
|
||||
<string name="alert_title_msg_bad_id">ID del messaggio errato</string>
|
||||
<string name="alert_text_fragment_encryption_out_of_sync_old_database">Può accadere quando tu o il tuo contatto avete usato il backup del database vecchio.</string>
|
||||
<string name="alert_text_msg_bad_id">L\'ID del messaggio successivo non è corretto (inferiore o uguale al precedente).
|
||||
\nPuò accadere a causa di qualche bug o quando la connessione è compromessa.</string>
|
||||
<string name="alert_text_fragment_permanent_error_reconnect">L\'errore è permanente per questa connessione, riconnettiti.</string>
|
||||
<string name="alert_text_decryption_error_header"><xliff:g id="message count" example="1">%1$d</xliff:g> messaggi non decifrati.</string>
|
||||
<string name="alert_title_msg_bad_hash">Hash del messaggio errato</string>
|
||||
<string name="alert_text_msg_bad_hash">L\'hash del messaggio precedente è diverso.</string>
|
||||
<string name="alert_text_fragment_please_report_to_developers">Si prega di segnalarlo agli sviluppatori.</string>
|
||||
<string name="stop_snd_file__message">L\'invio del file verrà interrotto.</string>
|
||||
<string name="stop_file__action">Ferma file</string>
|
||||
<string name="stop_snd_file__title">Fermare l\'invio del file\?</string>
|
||||
<string name="revoke_file__message">Il file verrà eliminato dai server.</string>
|
||||
<string name="stop_rcv_file__message">La ricezione del file verrà interrotta.</string>
|
||||
<string name="revoke_file__confirm">Revoca</string>
|
||||
<string name="revoke_file__action">Revoca file</string>
|
||||
<string name="revoke_file__title">Revocare il file\?</string>
|
||||
<string name="stop_file__confirm">Ferma</string>
|
||||
<string name="stop_rcv_file__title">Fermare la ricezione del file\?</string>
|
||||
<string name="no_spaces">Niente spazi!</string>
|
||||
<string name="allow_calls_only_if">Consenti le chiamate solo se il contatto le consente.</string>
|
||||
<string name="calls_prohibited_with_this_contact">Le chiamate audio/video sono vietate.</string>
|
||||
<string name="both_you_and_your_contact_can_make_calls">Sia tu che il tuo contatto potete effettuare chiamate.</string>
|
||||
<string name="only_you_can_make_calls">Solo tu puoi effettuare chiamate.</string>
|
||||
<string name="only_your_contact_can_make_calls">Solo il tuo contatto può effettuare chiamate.</string>
|
||||
<string name="prohibit_calls">Proibisci le chiamate audio/video.</string>
|
||||
<string name="audio_video_calls">Chiamate audio/video</string>
|
||||
<string name="available_in_v51">"
|
||||
\nDisponibile nella v5.1"</string>
|
||||
<string name="allow_your_contacts_to_call">Consenti ai tuoi contatti di chiamarti.</string>
|
||||
<string name="v5_0_app_passcode">Codice di accesso dell\'app</string>
|
||||
<string name="v5_0_large_files_support_descr">Veloce e senza aspettare che il mittente sia in linea!</string>
|
||||
<string name="v5_0_polish_interface">Interfaccia polacca</string>
|
||||
<string name="v5_0_app_passcode_descr">Impostala al posto dell\'autenticazione di sistema.</string>
|
||||
<string name="v5_0_polish_interface_descr">Grazie agli utenti – contribuite via Weblate!</string>
|
||||
<string name="v5_0_large_files_support">Video e file fino a 1 GB</string>
|
||||
</resources>
|
||||
@@ -540,7 +540,6 @@
|
||||
<string name="markdown_help">マークダウン (書式編集) ガイド</string>
|
||||
<string name="smp_servers_enter_manually">サーバを手動で入力</string>
|
||||
<string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。</string>
|
||||
<string name="core_build_timestamp">コアのビルド@: %s</string>
|
||||
<string name="create_address">アドレスを作成</string>
|
||||
<string name="delete_address__question">アドレスを削除しますか?</string>
|
||||
<string name="display_name__field">表示の名前:</string>
|
||||
@@ -802,7 +801,7 @@
|
||||
<string name="you_sent_group_invitation">グループの招待を送りました</string>
|
||||
<string name="snd_group_event_changed_member_role">%sの役割を次に変えました:%s</string>
|
||||
<string name="button_send_direct_message">ダイレクトメッセージを送信</string>
|
||||
<string name="num_contacts_selected"><xliff:g id="num_contacts">%1$s</xliff:g>連絡先が選択中</string>
|
||||
<string name="num_contacts_selected">%d 連絡先が選択中</string>
|
||||
<string name="snd_group_event_member_deleted">除名しました: <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
|
||||
<string name="switch_verb">切り替える</string>
|
||||
<string name="member_role_will_be_changed_with_notification">役割が「%s」となります。グループの全員に通知が出ます。</string>
|
||||
@@ -812,7 +811,6 @@
|
||||
<string name="network_options_save">保存</string>
|
||||
<string name="update_network_settings_question">ネットワーク設定を更新しますか?</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">設定を更新すると、全サーバにクライントの再接続が行われます。</string>
|
||||
<string name="your_chat_profiles_stored_locally">チャットプロフィールはローカルであなたの端末だけに保存されます。</string>
|
||||
<string name="save_color">色を保存</string>
|
||||
<string name="chat_preferences_you_allow">あなたが次を許可しています:</string>
|
||||
<string name="chat_preferences_yes">はい</string>
|
||||
@@ -844,9 +842,7 @@
|
||||
<string name="this_string_is_not_a_connection_link">このストリングは接続リンクではありません!</string>
|
||||
<string name="chat_with_the_founder">質問やアイデアを送る</string>
|
||||
<string name="star_on_github">GithubでStar</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。
|
||||
\n
|
||||
\n<xliff:g id="appName">SimpleX</xliff:g>のサーバには開示されません。</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">あなたのプロフィールはご自分の端末に保存され、あなたの連絡先のみに共有されます。 <xliff:g id="appName">SimpleX</xliff:g>のサーバには開示されません。</string>
|
||||
<string name="your_calls">あなたの通話</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g>飛ばしたメッセージ</string>
|
||||
<string name="send_link_previews">リンクのプレビューを送信</string>
|
||||
|
||||
@@ -0,0 +1,896 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="server_connected">연결됨</string>
|
||||
<string name="server_connecting">연결 중</string>
|
||||
<string name="connect_via_group_link">그룹 링크를 통해 연결할까요\?</string>
|
||||
<string name="connect_via_invitation_link">초대 링크로 연결할까요\?</string>
|
||||
<string name="display_name_connection_established">연결 수립됨</string>
|
||||
<string name="connection_timeout">연결 시간 초과</string>
|
||||
<string name="cannot_receive_file">파일을 받을 수 없습니다</string>
|
||||
<string name="contact_already_exists">이미 추가된 연락처에요.</string>
|
||||
<string name="smp_server_test_connect">연결</string>
|
||||
<string name="connection_error_auth">연결 오류 (인증)</string>
|
||||
<string name="smp_server_test_create_queue">대기열 만들기</string>
|
||||
<string name="database_initialization_error_title">데이터베이스를 초기화할 수 없어요</string>
|
||||
<string name="notifications_mode_service_desc">앱이 백그라운드에서 항상 실행되어요. 대신 메시지가 도착하자마자 바로 알림이 와요.</string>
|
||||
<string name="notifications_mode_periodic_desc">10분마다 최대 1분간 새 메시지 확인</string>
|
||||
<string name="notification_contact_connected">연결됨</string>
|
||||
<string name="notification_preview_somebody">숨긴 대화 상대 :</string>
|
||||
<string name="notification_preview_mode_contact">대화 상대 이름</string>
|
||||
<string name="allow_verb">허용</string>
|
||||
<string name="auth_confirm_credential">자격 증명 확인</string>
|
||||
<string name="copy_verb">복사</string>
|
||||
<string name="contact_connection_pending">연결 중…</string>
|
||||
<string name="group_connection_pending">연결 중…</string>
|
||||
<string name="attach">첨부파일</string>
|
||||
<string name="icon_descr_cancel_file_preview">파일 미리보기 취소</string>
|
||||
<string name="icon_descr_context">컨텍스트 아이콘</string>
|
||||
<string name="icon_descr_server_status_connected">연결됨</string>
|
||||
<string name="back">뒤로</string>
|
||||
<string name="add_new_contact_to_create_one_time_QR_code"><b>새 연락처 추가</b> : 일회용 QR 코드 만들기</string>
|
||||
<string name="cancel_verb">취소</string>
|
||||
<string name="icon_descr_cancel_live_message">라이브 메시지 취소</string>
|
||||
<string name="choose_file">파일 선택</string>
|
||||
<string name="confirm_verb">확인</string>
|
||||
<string name="connect_via_link_or_qr">링크 / QR 코드를 통해 연결</string>
|
||||
<string name="copied">클립보드로 복사됨</string>
|
||||
<string name="create_group">비밀 그룹 생성</string>
|
||||
<string name="accept_contact_button">수락</string>
|
||||
<string name="clear_chat_warning">모든 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요! 메시지는 나에게서만 삭제돼요.</string>
|
||||
<string name="clear_verb">지우기</string>
|
||||
<string name="clear_chat_menu_action">지우기</string>
|
||||
<string name="clear_chat_button">채팅 지우기</string>
|
||||
<string name="clear_chat_question">채팅을 지울까요\?</string>
|
||||
<string name="connection_request_sent">연결 요청 완료</string>
|
||||
<string name="connect_via_link">링크를 통해 연결</string>
|
||||
<string name="smp_servers_preset_add">프리셋 서버 추가하기</string>
|
||||
<string name="smp_servers_add">서버 추가…</string>
|
||||
<string name="chat_console">채팅 콘솔</string>
|
||||
<string name="smp_servers_check_address">서버 주소를 확인 후 다시 시도해 주세요.</string>
|
||||
<string name="configure_ICE_servers">ICE 서버 설정</string>
|
||||
<string name="contribute">기여</string>
|
||||
<string name="network_settings">고급 네트워크 설정</string>
|
||||
<string name="network_session_mode_user">채팅 프로필</string>
|
||||
<string name="network_session_mode_entity">연결</string>
|
||||
<string name="accept_requests">요청 수락</string>
|
||||
<string name="app_version_code">앱 빌드 : %s</string>
|
||||
<string name="appearance_settings">외관</string>
|
||||
<string name="app_version_title">앱 버전</string>
|
||||
<string name="app_version_name">앱 버전 : v%s</string>
|
||||
<string name="accept_automatically">자동</string>
|
||||
<string name="contact_requests">대화 상대의 요청</string>
|
||||
<string name="core_version">코어 버전 : v%s</string>
|
||||
<string name="callstatus_accepted">전화 받음</string>
|
||||
<string name="bold">굵게</string>
|
||||
<string name="callstatus_in_progress">전화 연결 중</string>
|
||||
<string name="colored">색깔</string>
|
||||
<string name="confirm_password">암호 확인</string>
|
||||
<string name="callstatus_connecting">전화 연결 중</string>
|
||||
<string name="create_profile_button">생성</string>
|
||||
<string name="create_profile">프로필 생성</string>
|
||||
<string name="callstatus_error">통화 오류</string>
|
||||
<string name="callstate_connected">연결됨</string>
|
||||
<string name="callstate_connecting">연결 중…</string>
|
||||
<string name="create_your_profile">내 프로필 생성</string>
|
||||
<string name="onboarding_notifications_mode_periodic_desc"><b>배터리에 좋음</b>. 백그라운드 서비스는 10분마다 새 메시지를 확인합니다. 전화 및 긴급 메시지를 놓칠 수 있습니다.</string>
|
||||
<string name="call_already_ended">전화가 이미 종료되었습니다!</string>
|
||||
<string name="always_use_relay">항상 릴레이 사용</string>
|
||||
<string name="icon_descr_audio_call">음성 전화</string>
|
||||
<string name="settings_audio_video_calls">음성 & 영상 전화</string>
|
||||
<string name="call_on_lock_screen">잠금 화면에서의 전화</string>
|
||||
<string name="status_contact_has_no_e2e_encryption">대화 상대와 종단간 암호화되지 않음</string>
|
||||
<string name="answer_call">응답</string>
|
||||
<string name="icon_descr_audio_on">소리 켜기</string>
|
||||
<string name="icon_descr_audio_off">소리 끄기</string>
|
||||
<string name="icon_descr_call_ended">통화 종료됨</string>
|
||||
<string name="icon_descr_call_connecting">전화 연결 중</string>
|
||||
<string name="auto_accept_images">이미지 자동 다운로드하기</string>
|
||||
<string name="integrity_msg_bad_hash">잘못된 메시지 해시</string>
|
||||
<string name="integrity_msg_bad_id">잘못된 메시지 아이디</string>
|
||||
<string name="settings_section_title_calls">전화</string>
|
||||
<string name="chat_is_running">채팅 기능이 작동하고 있어요</string>
|
||||
<string name="settings_section_title_chats">채팅</string>
|
||||
<string name="chat_database_imported">채팅 데이테베이스를 불러 왔어요</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>주의</b>: 비밀번호를 분실하면 복구나 비밀번호 변경을 할 수 없어요.</string>
|
||||
<string name="change_database_passphrase_question">데이터베이스 암호를 바꾸겠습니까\?</string>
|
||||
<string name="confirm_new_passphrase">새로운 암호 확인…</string>
|
||||
<string name="chat_archive_section">채팅 기록 보관함</string>
|
||||
<string name="rcv_group_event_changed_your_role">내 역할이 %s 역할로 변경됨.</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">주소 바꾸는 중…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing">주소 바꾸는 중…</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_changing_for_member">%s의 주소 바꾸는 중…</string>
|
||||
<string name="rcv_group_event_member_connected">연결됨</string>
|
||||
<string name="group_member_status_complete">완료</string>
|
||||
<string name="group_member_status_connected">연결됨</string>
|
||||
<string name="group_member_status_connecting">연결 중</string>
|
||||
<string name="group_member_status_accepted">연결 중 (수락됨)</string>
|
||||
<string name="group_member_status_announced">연결 중 (알림)</string>
|
||||
<string name="group_member_status_intro_invitation">연결 중(초대 시작)</string>
|
||||
<string name="group_member_status_creator">제작자</string>
|
||||
<string name="invite_prohibited">상대를 초대할 수 없습니다</string>
|
||||
<string name="clear_contacts_selection_button">지우기</string>
|
||||
<string name="icon_descr_contact_checked">대화 상대 확인됨</string>
|
||||
<string name="create_group_link">그룹 링크 생성</string>
|
||||
<string name="button_create_group_link">링크 생성</string>
|
||||
<string name="change_member_role_question">그룹 역할을 바꾸겠습니까\?</string>
|
||||
<string name="info_row_connection">연결</string>
|
||||
<string name="users_add">프로필 추가</string>
|
||||
<string name="incognito_random_profile_description">대화 상대에게 랜덤으로 만들어진 익명 프로필이 보내져요</string>
|
||||
<string name="cant_delete_user_profile">사용자 프로필을 삭제할 수 없습니다</string>
|
||||
<string name="chat_preferences_always">항상</string>
|
||||
<string name="chat_preferences_contact_allows">대화 상대가 허용했어요.</string>
|
||||
<string name="contact_preferences">연락처 개별 설정</string>
|
||||
<string name="allow_voice_messages_only_if">대화 상대도 허용한 경우에만 음성 메시지를 보낼 수 있습니다.</string>
|
||||
<string name="allow_your_contacts_irreversibly_delete">모두에게서 메시지 영구 삭제 허용하기.</string>
|
||||
<string name="allow_your_contacts_to_send_disappearing_messages">대화 상대에게 자동 삭제되는 메시지 허용하기.</string>
|
||||
<string name="allow_your_contacts_to_send_voice_messages">상대가 음성 메시지를 보내는 것을 허용하기.</string>
|
||||
<string name="both_you_and_your_contacts_can_delete">나와 대화 상대 모두 메시지를 영구 삭제할 수 있어요.</string>
|
||||
<string name="both_you_and_your_contact_can_send_voice">나와 대화 상대 모두 음성 메시지를 보낼 수 있어요.</string>
|
||||
<string name="contacts_can_mark_messages_for_deletion">상대가 메시지에 삭제 표시를 할 수 있습니다. 그러나 삭제 표시된 메시지 내용은 여전히 볼 수 있습니다.</string>
|
||||
<string name="allow_to_delete_messages">모두에게서 메시지 영구 삭제 허용하기.</string>
|
||||
<string name="feature_cancelled_item">%s 취소됨</string>
|
||||
<string name="v4_2_auto_accept_contact_requests">대화 상대의 요청 자동 수락</string>
|
||||
<string name="v4_5_transport_isolation_descr">채팅 프로필(기본값) 또는 연결(베타).</string>
|
||||
<string name="v4_4_verify_connection_security_desc">대화 상대와 보안 코드를 비교해 보세요.</string>
|
||||
<string name="v4_6_chinese_spanish_interface">중국어 및 스페인어 인터페이스</string>
|
||||
<string name="about_simplex">SimpleX에 대하여</string>
|
||||
<string name="accept">수락</string>
|
||||
<string name="share_one_time_link">일회용 초대 링크 만들기</string>
|
||||
<string name="create_address">주소 생성</string>
|
||||
<string name="chat_item_ttl_day">1일</string>
|
||||
<string name="about_simplex_chat"><xliff:g id="appNameFull">SimpleX</xliff:g>에 대하여</string>
|
||||
<string name="color_primary">강조 색상</string>
|
||||
<string name="accept_call_on_lock_screen">응답</string>
|
||||
<string name="accept_connection_request__question">연결 요청을 수락할까요\?</string>
|
||||
<string name="accept_feature">수락</string>
|
||||
<string name="network_enable_socks_info">SOCKS 프록시(포트 9050)를 통해 서버에 액세스할까요\? 이 설정을 활성화하기 전에 프록시를 시작해야 해요.</string>
|
||||
<string name="smp_servers_add_to_another_device">다른 기기에 추가</string>
|
||||
<string name="v4_3_improved_server_configuration_desc">QR 코드 스캔으로 서버 추가</string>
|
||||
<string name="button_add_welcome_message">환영 메시지 추가</string>
|
||||
<string name="group_member_role_admin">관리자</string>
|
||||
<string name="v4_2_group_links_desc">관리자는 그룹 가입을 위한 링크를 만들 수 있어요.</string>
|
||||
<string name="allow_to_send_disappearing">자동 삭제되는 메시지 허용하기.</string>
|
||||
<string name="users_delete_all_chats_deleted">모든 채팅과 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
|
||||
<string name="allow_to_send_voice">음성 메시지 허용하기.</string>
|
||||
<string name="allow_voice_messages_question">음성 메시지를 허용하겠습니까\?</string>
|
||||
<string name="allow_disappearing_messages_only_if">상대도 허용하는 경우에만 자동 삭제되는 메시지를 사용할 수 있어요.</string>
|
||||
<string name="allow_direct_messages">그룹 멤버에게 1:1 채팅 허용하기.</string>
|
||||
<string name="all_group_members_will_remain_connected">모든 그룹 멤버는 연결 상태가 계속 유지돼요.</string>
|
||||
<string name="allow_irreversible_message_deletion_only_if">상대도 허용한 경우에만 모두에게서 메시지 영구 삭제가 가능해요.</string>
|
||||
<string name="all_your_contacts_will_remain_connected">모든 연락처와 연결 상태가 계속 유지돼요.</string>
|
||||
<string name="notifications_mode_service">항상 켜기</string>
|
||||
<string name="keychain_is_storing_securely">안드로이드 암호 저장소는 비밀번호를 안전하게 저장하는 데 사용되고 알림이 작동하도록 해요.</string>
|
||||
<string name="keychain_allows_to_receive_ntfs">안드로이드 암호 저장소는 앱을 다시 시작하거나 비밀번호 변경을 하고 나서 비밀번호를 안전하게 저장하는 데 사용되고 알림이 작동되도록 해요.</string>
|
||||
<string name="notifications_mode_off_desc">앱이 실행 중일 때만 알림을 받을 수 있고 백그라운드에서 실행되지 않아요.</string>
|
||||
<string name="full_backup">앱 데이터 백업</string>
|
||||
<string name="settings_section_title_icon">앱 아이콘</string>
|
||||
<string name="incognito_random_profile_from_contact_description">링크를 보낸 사람한테 랜덤으로 만들어진 익명 프로필이 보내져요</string>
|
||||
<string name="network_session_mode_user_description">별도로 분리된 TCP 연결(그리고 SOCKS 자격 증명)이 <b>각각의 채팅 프로필</b>에 사용될 거예요.</string>
|
||||
<string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 대화 상대 및 그룹 구성원</b>에게 사용될 거예요.
|
||||
\n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 엄청 높을 수 있고 일부 연결이 실패할 수 있어요.</string>
|
||||
<string name="icon_descr_asked_to_receive">이미지 수신 요청됨</string>
|
||||
<string name="v4_6_audio_video_calls">음성 및 영상 전화</string>
|
||||
<string name="audio_call_no_encryption">음성 전화 (종단간 암호화 X)</string>
|
||||
<string name="auth_unavailable">인증할 수 없어요</string>
|
||||
<string name="turning_off_service_and_periodic">새 메시지를 수신하기 위해 배터리 최적화 설정을 바꿉니다. 설정에서 언제든지 다시 바꿀 수 있습니다.</string>
|
||||
<string name="onboarding_notifications_mode_off_desc"><b>배터리에 가장 좋음</b>. 앱이 실행 중일 때만 알림을 받게 되며 백그라운드에서 실행되지 않습니다.</string>
|
||||
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>설정을 통해 비활성화할 수 있어요.</b> – 앱이 실행되는 동안 알림이 표시되요.</string>
|
||||
<string name="both_you_and_your_contact_can_send_disappearing">나와 대화 상대 모두 자동 삭제되는 메시지를 보낼 수 있어요.</string>
|
||||
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>QR 코드 스캔</b>: QR 코드를 보여주는 사람과 연결할 수 있어요.</string>
|
||||
<string name="cannot_access_keychain">데이터베이스 암호를 저장하고 있는 암호키 저장소에 접근할 수 없습니다</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><b>배터리 많이 사용</b>! 백그라운드에서 항상 실행돼요. 메시지를 수신하자마자 알림이 떠요.</string>
|
||||
<string name="callstatus_ended">통화 종료됨 <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
|
||||
<string name="callstatus_calling">전화 중…</string>
|
||||
<string name="icon_descr_call_progress">전화 연결 중</string>
|
||||
<string name="icon_descr_cancel_link_preview">링크 미리보기 취소</string>
|
||||
<string name="icon_descr_cancel_image_preview">이미지 미리보기 취소</string>
|
||||
<string name="rcv_group_event_changed_member_role">%s 에서 %s로 역할이 변경됨</string>
|
||||
<string name="chat_database_section">채팅 데이터베이스</string>
|
||||
<string name="alert_title_cant_invite_contacts">대화 상대를 초대할 수 없습니다!</string>
|
||||
<string name="change_verb">변경</string>
|
||||
<string name="chat_archive_header">채팅 기록 보관함</string>
|
||||
<string name="change_role">역할 바꾸기</string>
|
||||
<string name="chat_database_deleted">채팅 데이터베이스를 삭제했어요</string>
|
||||
<string name="chat_is_stopped">채팅 기능이 멈췄어요</string>
|
||||
<string name="chat_is_stopped_indication">채팅 기능이 멈췄어요</string>
|
||||
<string name="chat_preferences">채팅 설정</string>
|
||||
<string name="chat_with_developers">개발자와 대화</string>
|
||||
<string name="connect_via_link_verb">연결</string>
|
||||
<string name="display_name_connecting">연결 중…</string>
|
||||
<string name="icon_descr_close_button">닫기 버튼</string>
|
||||
<string name="connect_button">연결</string>
|
||||
<string name="group_member_status_introduced">연결 중 (도입)</string>
|
||||
<string name="connection_error">연결 오류</string>
|
||||
<string name="connection_local_display_name">연결 <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
|
||||
<string name="connect_via_contact_link">링크를 통해 연결하겠습니까\?</string>
|
||||
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">대화 상대와 메시지가 삭제돼요. 삭제 후 되돌릴 수 없어요!</string>
|
||||
<string name="status_contact_has_e2e_encryption">대화 상대와 종단간 암호화됨</string>
|
||||
<string name="alert_title_contact_connection_pending">대화 상대와 아직 연결되지 않았어요!</string>
|
||||
<string name="archive_created_on_ts"><xliff:g id="archive_ts">%1$s</xliff:g>에 생성 완료</string>
|
||||
<string name="create_one_time_link">일회용 초대 링크 생성</string>
|
||||
<string name="create_secret_group_title">비밀 그룹 생성</string>
|
||||
<string name="accept_contact_incognito_button">익명 수락</string>
|
||||
<string name="chat_item_ttl_month">1개월</string>
|
||||
<string name="chat_item_ttl_week">1주</string>
|
||||
<string name="a_plus_b">a + b</string>
|
||||
<string name="deleted_description">삭제됨</string>
|
||||
<string name="simplex_link_mode_description">설명</string>
|
||||
<string name="smp_server_test_delete_queue">대기열 삭제</string>
|
||||
<string name="delete_verb">삭제</string>
|
||||
<string name="delete_message__question">메시지를 삭제할까요\?</string>
|
||||
<string name="for_me_only">나에게서만 삭제</string>
|
||||
<string name="delete_member_message__question">멤버의 메시지를 삭제할까요\?</string>
|
||||
<string name="maximum_supported_file_size">현재 지원되는 최대 파일 크기는 <xliff:g id="maxFileSize">%1$s</xliff:g> 에요.</string>
|
||||
<string name="image_decoding_exception_title">디코딩 오류</string>
|
||||
<string name="button_delete_contact">대화 상대 삭제</string>
|
||||
<string name="delete_contact_question">연락처를 삭제할까요\?</string>
|
||||
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 데스크톱: <b>QR 코드 스캔</b>을 통해 앱에서 표시된 QR 코드를 스캔해 주세요.</string>
|
||||
<string name="delete_contact_menu_action">삭제</string>
|
||||
<string name="delete_group_menu_action">삭제</string>
|
||||
<string name="delete_pending_connection__question">대기 중인 연결을 삭제할까요\?</string>
|
||||
<string name="clear_verification">인증 취소</string>
|
||||
<string name="database_passphrase_and_export">데이터베이스 비밀번호 & 내보내기</string>
|
||||
<string name="smp_servers_delete_server">서버 삭제</string>
|
||||
<string name="delete_address">주소 삭제</string>
|
||||
<string name="delete_address__question">주소를 삭제할까요\?</string>
|
||||
<string name="delete_image">이미지 삭제</string>
|
||||
<string name="decentralized">탈중앙화</string>
|
||||
<string name="settings_section_title_develop">개발</string>
|
||||
<string name="settings_developer_tools">개발자 도구</string>
|
||||
<string name="settings_section_title_device">기기</string>
|
||||
<string name="database_passphrase">데이터베이스 비밀번호</string>
|
||||
<string name="delete_files_and_media_for_all_users">모든 채팅 프로필 파일 삭제</string>
|
||||
<string name="database_error">데이터베이스 에러</string>
|
||||
<string name="passphrase_is_different">데이터베이스 비밀번호가 암호 저장소에 저장된 것과 일치하지 않아요.</string>
|
||||
<string name="database_passphrase_is_required">채팅을 열려면 데이터베이스 비밀번호가 필요해요.</string>
|
||||
<string name="delete_archive">보관된 채팅 삭제</string>
|
||||
<string name="delete_chat_archive_question">보관된 채팅을 삭제할까요\?</string>
|
||||
<string name="num_contacts_selected">%d 개의 연락처가 선택되었어요.</string>
|
||||
<string name="info_row_database_id">데이터베이스 아이디</string>
|
||||
<string name="users_delete_profile_for">다음 채팅 프로필 삭제</string>
|
||||
<string name="theme_dark">어둡게</string>
|
||||
<string name="delete_after">다음 기간 이후 자동 삭제</string>
|
||||
<string name="above_then_preposition_continuation">위 다음 :</string>
|
||||
<string name="delete_database">데이터베이스 삭제</string>
|
||||
<string name="set_password_to_export_desc">데이터베이스는 임의의 비밀번호로 암호화되었어요. 내보내기 기능 사용 전 비밀번호를 변경해 주세요.</string>
|
||||
<string name="delete_files_and_media_question">파일과 미디어를 삭제할까요\?</string>
|
||||
<string name="current_passphrase">현재 비밀번호…</string>
|
||||
<string name="database_encrypted">데이터베이스 암호화 완료!</string>
|
||||
<string name="database_passphrase_will_be_updated">데이터베이스 비밀번호가 업데이트되요.</string>
|
||||
<string name="encrypted_with_random_passphrase">데이터베이스는 임의의 비밀번호로 암호화되었고, 원하시면 비밀번호를 변경할 수 있어요.</string>
|
||||
<string name="database_will_be_encrypted">데이터베이스는 암호화될 거예요.</string>
|
||||
<string name="delete_messages">메시지 삭제</string>
|
||||
<string name="delete_messages_after">다음 기간 이후 자동 삭제</string>
|
||||
<string name="rcv_group_event_group_deleted">삭제된 그룹</string>
|
||||
<string name="delete_link">링크 삭제</string>
|
||||
<string name="delete_link_question">링크를 삭제할까요\?</string>
|
||||
<string name="chat_preferences_default">기본값 (%s)</string>
|
||||
<string name="ttl_day">%d일</string>
|
||||
<string name="ttl_d">%d일</string>
|
||||
<string name="ttl_days">%d일</string>
|
||||
<string name="button_delete_group">그룹 삭제</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">주소 변경됨</string>
|
||||
<string name="database_encryption_will_be_updated">데이터베이스 비밀번호가 업데이트되고 암호 저장소에 보관됩니다.</string>
|
||||
<string name="database_will_be_encrypted_and_passphrase_stored">데이터베이스는 암호화되고, 비밀번호는 암호 저장소에 보관될 거에요.</string>
|
||||
<string name="users_delete_question">채팅 프로필을 삭제할까요\?</string>
|
||||
<string name="delete_files_and_media_all">모든 파일 삭제</string>
|
||||
<string name="delete_chat_profile_question">채팅 프로필을 삭제할까요\?</string>
|
||||
<string name="full_deletion">모두에게서 삭제</string>
|
||||
<string name="delete_group_question">그룹을 삭제할까요\?</string>
|
||||
<string name="failed_to_create_user_duplicate_title">표시 이름이 중복되어요!</string>
|
||||
<string name="smp_server_test_disconnect">연결 끊기</string>
|
||||
<string name="auth_device_authentication_is_disabled_turning_off">기기 인증이 비활성화되어 SimpleX 잠금 기능이 작동하지 않아요.</string>
|
||||
<string name="auth_disable_simplex_lock">SimpleX 잠금 비활성화</string>
|
||||
<string name="icon_descr_server_status_disconnected">연결 끊김</string>
|
||||
<string name="add_contact">일회용 초대 링크</string>
|
||||
<string name="add_contact_or_create_group">새로운 채팅 시작</string>
|
||||
<string name="display_name__field">표시 이름</string>
|
||||
<string name="display_name_cannot_contain_whitespace">표시 이름에는 공백문자가 쓰일 수 없어요.</string>
|
||||
<string name="display_name">표시 이름</string>
|
||||
<string name="encrypted_audio_call">종단간 암호화된 음성 전화</string>
|
||||
<string name="encrypted_video_call">종단간 암호화된 영상 전화</string>
|
||||
<string name="no_call_on_lock_screen">비활성화</string>
|
||||
<string name="status_e2e_encrypted">종단간 암호화</string>
|
||||
<string name="integrity_msg_duplicate">중복된 메시지</string>
|
||||
<string name="accept_feature_set_1_day">1일로 설정</string>
|
||||
<string name="v4_4_disappearing_messages">자동 삭제되는 메시지</string>
|
||||
<string name="total_files_count_and_size">전체 크기가 %s인 %d개의 파일</string>
|
||||
<string name="conn_level_desc_direct">다이렉트</string>
|
||||
<string name="disappearing_prohibited_in_this_chat">이 채팅에서는 자동 삭제되는 메시지를 사용할 수 없어요.</string>
|
||||
<string name="disappearing_messages_are_prohibited">이 그룹에서는 자동 삭제되는 메시지를 사용할 수 없어요.</string>
|
||||
<string name="ttl_m">%d분</string>
|
||||
<string name="ttl_months">%d 개월</string>
|
||||
<string name="ttl_min">%d 분</string>
|
||||
<string name="ttl_month">%d 개월</string>
|
||||
<string name="ttl_week">%d 주</string>
|
||||
<string name="downgrade_and_open_chat">다운그레이드하고 채팅 열기</string>
|
||||
<string name="direct_messages">1:1 메시지</string>
|
||||
<string name="timed_messages">자동 삭제되는 메시지</string>
|
||||
<string name="direct_messages_are_prohibited_in_chat">이 그룹에서는 멤버들의 1:1 채팅이 금지되어 있어요.</string>
|
||||
<string name="ttl_s">%d초</string>
|
||||
<string name="ttl_sec">%d 초</string>
|
||||
<string name="ttl_h">%d시</string>
|
||||
<string name="ttl_mth">%d개월</string>
|
||||
<string name="ttl_w">%d주</string>
|
||||
<string name="ttl_weeks">%d 주</string>
|
||||
<string name="confirm_database_upgrades">데이터베이스 업그레이드 확인</string>
|
||||
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">기기 인증을 하고 있지 않아요. 기기 인증을 켜면 설정에서 SimpleX 잠금 기능을 사용할 수 있어요.</string>
|
||||
<string name="ttl_hour">%d 시간</string>
|
||||
<string name="ttl_hours">%d 시간</string>
|
||||
<string name="mtr_error_different">앱/데이터베이스의 다른 마이그레이션: %s / %s</string>
|
||||
<string name="v4_5_multiple_chat_profiles_descr">다른 이름, 아바타 그리고 전송 격리.</string>
|
||||
<string name="dont_show_again">다시 보지 않기</string>
|
||||
<string name="connected_to_server_to_receive_messages_from_contact">이 대화 상대로부터의 메시지를 수신할 서버와 연결되었어요.</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="contact_developers">앱 업데이트 후 개발자에게 연락해 주세요.</string>
|
||||
<string name="connection_error_auth_desc">대화 상대가 나갔거나 초대 링크가 이미 사용된 경우가 아니면 버그일 수 있어요. 이 경우 개발자에게 알려주세요.
|
||||
\n대화 상대에게 다른 초대 링크 만들도록 부탁하고 네트워크 연결이 안정적인지 확인하세요.</string>
|
||||
<string name="auth_enable_simplex_lock">SimpleX 잠금 활성화</string>
|
||||
<string name="auth_log_in_using_credential">자격 증명으로 로그인</string>
|
||||
<string name="auth_open_chat_console">채팅 콘솔 열기</string>
|
||||
<string name="auth_stop_chat">채팅 기능 중지하기</string>
|
||||
<string name="auth_unlock">잠금 해제하기</string>
|
||||
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">앱을 사용하지 않는 지 30초가 지나면 다시 인증해야 해요.</string>
|
||||
<string name="contact_wants_to_connect_with_you">님이 연결하고 싶어해요!</string>
|
||||
<string name="callstate_starting">시작…</string>
|
||||
<string name="callstate_waiting_for_answer">응답 대기 중…</string>
|
||||
<string name="callstate_waiting_for_confirmation">확인 대기 중…</string>
|
||||
<string name="alert_title_skipped_messages">읽지 않는 메시지</string>
|
||||
<string name="alert_title_cant_invite_contacts_descr">이 그룹에서 익명 프로필을 사용하고 있어요. 원래의 내 프로필이 노출되는 걸 방지하기 위해 대화 상대 초대가 허용되지 않아요.</string>
|
||||
<string name="button_remove_member">멤버 삭제하기</string>
|
||||
<string name="chat_item_ttl_seconds">%s 초</string>
|
||||
<string name="alert_message_group_invitation_expired">이 링크로 참여할 수 없어요. 이미 삭제된 링크에요.</string>
|
||||
<string name="alert_message_no_group">존재하지 않는 그룹이에요.</string>
|
||||
<string name="alert_title_no_group">그룹을 찾을 수 없어요!</string>
|
||||
<string name="button_add_members">맴버 초대하기</string>
|
||||
<string name="button_welcome_message">환영 메시지</string>
|
||||
<string name="button_edit_group_profile">그룹 프로필 수정</string>
|
||||
<string name="button_leave_group">그룹 나가기</string>
|
||||
<string name="button_send_direct_message">1:1 채팅 시작하기</string>
|
||||
<string name="conn_stats_section_title_servers">서버</string>
|
||||
<string name="conn_level_desc_indirect">인다이렉트 (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="chat_preferences_you_allow">허용함</string>
|
||||
<string name="chat_preferences_off">꺼짐</string>
|
||||
<string name="chat_preferences_on">켜짐</string>
|
||||
<string name="chat_preferences_no">아니요</string>
|
||||
<string name="ask_your_contact_to_enable_voice">대화 상대에게 음성 메시지 기능을 활성화 해달라고 부탁해보세요.</string>
|
||||
<string name="chat_help_tap_button">탭 버튼</string>
|
||||
<string name="connection_you_accepted_will_be_cancelled">수락한 연결이 취소됩니다!</string>
|
||||
<string name="chat_lock">SimpleX 잠금</string>
|
||||
<string name="callstatus_rejected">거절된 전화</string>
|
||||
<string name="callstate_ended">종료됨</string>
|
||||
<string name="call_connection_peer_to_peer">P2P</string>
|
||||
<string name="call_connection_via_relay">릴레이를 경유</string>
|
||||
<string name="alert_title_group_invitation_expired">만료된 초대 링크에요!</string>
|
||||
<string name="chat_preferences_yes">네</string>
|
||||
<string name="contact_wants_to_connect_via_call"><xliff:g id="contactName" example="Alice">%1$s</xliff:g>님이 다음을 통해 연결하려고 해요 :</string>
|
||||
<string name="allow_accepting_calls_from_lock_screen">설정에서 잠금 화면에서 바로 전화를 받을 수 있도록 설정할 수 있어요.</string>
|
||||
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">연결을 완료하려면 대화 상대가 온라인 상태여야 해요.
|
||||
\n연결 요청을 취소하고 대화 상대를 삭제할 수 있어요 (그리고 새 링크로 재시도).</string>
|
||||
<string name="alert_text_skipped_messages_it_can_happen_when">다음과 같은 경우에 발생할 수 있어요.
|
||||
\n1. 대화 상대가 메시지를 보낸 지 30일 지나서 서버에서 삭제된 경우
|
||||
\n2. 메시지를 수신하는 데 사용된 서버가 업데이트되고 재부팅된 경우
|
||||
\n3. 침해된 연결의 경우
|
||||
\n서버 업데이트를 받으려면 설정에서 개발자에게 연락해 주세요.
|
||||
\n저희 개발팀은 메시지 손실을 방지하기 위해 중복된 서버를 추가할 예정이에요.</string>
|
||||
<string name="auth_simplex_lock_turned_on">SimpleX 잠금 켜짐</string>
|
||||
<string name="callstate_received_answer">응답됨…</string>
|
||||
<string name="callstate_received_confirmation">확인 받음…</string>
|
||||
<string name="callstatus_missed">부재 중 전화</string>
|
||||
<string name="chat_item_ttl_none">사용 안 함</string>
|
||||
<string name="chat_with_the_founder">질문이나 아이디어 보내기</string>
|
||||
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(스캔하거나 클립보드에서 붙여넣기)</string>
|
||||
<string name="contact_sent_large_file">대화 상대가 현재 지원되는 최대 크기(<xliff:g id="maxFileSize">%1$s</xliff:g>)보다 큰 파일을 보냈습니다.</string>
|
||||
<string name="display_name_invited_to_connect">초대를 받았어요.</string>
|
||||
<string name="failed_to_create_user_title">프로필 생성 오류!</string>
|
||||
<string name="description_via_group_link_incognito">그룹 링크로 익명 채팅</string>
|
||||
<string name="description_via_group_link">그룹 링크로 채팅</string>
|
||||
<string name="description_via_one_time_link">일회용 링크로 채팅</string>
|
||||
<string name="description_you_shared_one_time_link_incognito">일회용 익명 링크를 공유했어요.</string>
|
||||
<string name="description_you_shared_one_time_link">일회용 링크를 공유했어요.</string>
|
||||
<string name="description_via_contact_address_link_incognito">상대의 연락처 링크로 익명 연결</string>
|
||||
<string name="description_via_contact_address_link">상대의 연락처 링크로 연결</string>
|
||||
<string name="description_via_one_time_link_incognito">일회용 연락처로 익명 연결</string>
|
||||
<string name="ensure_smp_server_address_are_correct_format_and_unique">SMP 서버 주소가 올바른 형식이고 줄로 구분되어 있고 중복이 없는지 확인해 주세요.</string>
|
||||
<string name="error_saving_smp_servers">SMP 서버 저장 오류</string>
|
||||
<string name="error_setting_network_config">네트워크 설정 업데이트 오류</string>
|
||||
<string name="failed_to_active_user_title">프로필 변경 오류!</string>
|
||||
<string name="failed_to_create_user_duplicate_desc">동일한 표시 이름을 가진 채팅 프로필이 있어요. 다른 이름을 선택해 주세요.</string>
|
||||
<string name="failed_to_parse_chats_title">채팅 불러오기 실패</string>
|
||||
<string name="failed_to_parse_chat_title">채팅 불러오기 실패</string>
|
||||
<string name="error_adding_members">멤버 추가 오류</string>
|
||||
<string name="error_joining_group">그룹 참여 오류</string>
|
||||
<string name="error_sending_message">메시지 전송 오류</string>
|
||||
<string name="error_creating_address">주소 생성 오류</string>
|
||||
<string name="error_receiving_file">파일 다운로드 오류</string>
|
||||
<string name="error_accepting_contact_request">상대 요청 수락 오류</string>
|
||||
<string name="error_changing_address">주소 변경 오류</string>
|
||||
<string name="error_deleting_contact">연락처 삭제 오류</string>
|
||||
<string name="error_deleting_contact_request">대화 요청 삭제 오류</string>
|
||||
<string name="error_deleting_group">그룹 삭제 오류</string>
|
||||
<string name="error_deleting_pending_contact_connection">대기 중 대화 요청 삭제 오류</string>
|
||||
<string name="error_smp_test_certificate">서버 주소의 인증서의 지문(fingerprint)이 잘못되었을 수도 있어요.</string>
|
||||
<string name="error_smp_test_failed_at_step">테스트가 %s단계에서 실패했어요.</string>
|
||||
<string name="error_smp_test_server_auth">서버는 대기열을 생성하고 비밀번호를 확인하려면 인증이 필요해요.</string>
|
||||
<string name="enter_passphrase_notification_desc">알림을 받으려면 데이터베이스 암호를 입력해 주세요.</string>
|
||||
<string name="enter_passphrase_notification_title">비밀번호가 필요해요.</string>
|
||||
<string name="error_deleting_user">프로필 삭제 오류</string>
|
||||
<string name="error_updating_user_privacy">사용자 개인정보 업데이트 오류</string>
|
||||
<string name="database_initialization_error_desc">데이터베이스가 올바르게 작동하지 안하요. 자세히 알아보려면 탭하세요.</string>
|
||||
<string name="edit_verb">수정하기</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">메시지가 삭제돼요. 삭제 후 복구할 수 없어요!</string>
|
||||
<string name="delete_message_mark_deleted_warning">메시지가 삭제 표시될 거예요. 대화 상대는 여전히 삭제된 내용을 볼 수 있어요.</string>
|
||||
<string name="ensure_ICE_server_address_are_correct_format_and_unique">WebRTC ICE 서버 주소가 올바른 형식이고 줄로 구분되고 중복이 없는지 확인해 주세요.</string>
|
||||
<string name="enter_one_ICE_server_per_line">ICE 서버(한 줄에 하나씩)</string>
|
||||
<string name="error_saving_ICE_servers">ICE 서버 저장 오류</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="exit_without_saving">저장하지 않고 나가기</string>
|
||||
<string name="encrypt_database">암호화</string>
|
||||
<string name="encrypted_database">암호화된 데이터베이스</string>
|
||||
<string name="feature_off">꺼짐</string>
|
||||
<string name="feature_enabled_for_you">나에게 켜짐</string>
|
||||
<string name="feature_enabled_for_contact">대화 상대에게 켜짐</string>
|
||||
<string name="feature_enabled">켜짐</string>
|
||||
<string name="feature_received_prohibited">수신됨, 금지됨</string>
|
||||
<string name="export_database">데이터베이스 내보내기</string>
|
||||
<string name="enable_automatic_deletion_question">자동 삭제되는 메시지를 사용할까요\?</string>
|
||||
<string name="error_changing_message_deletion">설정 변경 오류</string>
|
||||
<string name="enable_automatic_deletion_message">이 작업은 되돌릴 수 없어요. 선택한 시간보다 일찍 보내거나 받은 메시지는 삭제돼요. 이는 몇 분 걸릴 수 있어요.</string>
|
||||
<string name="error_with_info">오류: %s</string>
|
||||
<string name="enter_correct_passphrase">올바른 비밀번호를 입력해 주세요.</string>
|
||||
<string name="database_backup_can_be_restored">데이터베이스 비밀번호 변경이 완료되지 않았어요.</string>
|
||||
<string name="database_restore_error">데이터베이스 오류 복구</string>
|
||||
<string name="error_creating_link_for_group">그룹 링크 생성 오류</string>
|
||||
<string name="error_updating_link_for_group">그룹 링크 업데이트 오류</string>
|
||||
<string name="error_changing_role">역할 변경 오류</string>
|
||||
<string name="error_removing_member">멤버 삭제 오류</string>
|
||||
<string name="database_downgrade">데이터베이스 다운그레이드</string>
|
||||
<string name="database_migrations">마이그레이션: %s</string>
|
||||
<string name="delete_group_for_all_members_cannot_undo_warning">모든 멤버에게서 그룹이 삭제돼요. 삭제 후 복구할 수 없어요!</string>
|
||||
<string name="delete_group_for_self_cannot_undo_warning">나에게서만 그룹이 삭제되요. 삭제 후 복구할 수 없어요!</string>
|
||||
<string name="file_not_found">파일을 찾을 수 없음</string>
|
||||
<string name="error_saving_user_password">사용자 비밀번호 저장 오류</string>
|
||||
<string name="error_stopping_chat">채팅 정지하기 오류</string>
|
||||
<string name="error_exporting_chat_database">채팅 데이터베이스 내보내기 오류</string>
|
||||
<string name="database_is_not_encrypted">채팅 데이터베이스가 암호화되지 않았어요. 비밀번호를 설정하여 보호해 주세요.</string>
|
||||
<string name="enter_passphrase">비밀번호를 입력해 주세요…</string>
|
||||
<string name="enter_password_to_show">검색에 비밀번호 입력</string>
|
||||
<string name="edit_image">이미지 수정하기</string>
|
||||
<string name="delete_chat_profile_action_cannot_be_undone_warning">이 작업은 실행 취소될 수 없어요. 프로필, 연락처, 메시지 및 파일이 영구적으로 손실돼요.</string>
|
||||
<string name="error_importing_database">채팅 데이터베이스 가져오기 오류</string>
|
||||
<string name="encrypt_database_question">데이터베이스를 암호화할까요\?</string>
|
||||
<string name="developer_options">데이터베이스 ID 및 전송 격리 옵션.</string>
|
||||
<string name="error_starting_chat">채팅 시작하기 오류</string>
|
||||
<string name="error_encrypting_database">데이터베이스 암호화 오류</string>
|
||||
<string name="enter_correct_current_passphrase">올바른 현재 비밀번호를 입력해 주세요.</string>
|
||||
<string name="delete_chat_profile">채팅 프로필 삭제</string>
|
||||
<string name="delete_profile">프로필 삭제</string>
|
||||
<string name="database_downgrade_warning">경고: 일부 데이터가 손실될 수 있어요!</string>
|
||||
<string name="database_upgrade">데이터베이스 업그레이드</string>
|
||||
<string name="delete_files_and_media_desc">이 작업은 실행 취소될 수 없어요. 수신 및 전송된 모든 파일과 미디어가 삭제돼요. 저해상도 사진만 삭제되지 않아요.</string>
|
||||
<string name="error_deleting_database">채팅 데이터베이스 삭제 오류</string>
|
||||
<string name="error_deleting_link_for_group">그룹 링크 삭제 오류</string>
|
||||
<string name="error_saving_file">파일 저장 오류</string>
|
||||
<string name="error_saving_group_profile">그룹 프로필 저장 오류</string>
|
||||
<string name="feature_offered_item">%s 제안</string>
|
||||
<string name="feature_offered_item_with_param">%s 제안: %2s</string>
|
||||
<string name="icon_descr_instant_notifications">즉시 알림</string>
|
||||
<string name="hide_notification">숨기기</string>
|
||||
<string name="hide_verb">숨기기</string>
|
||||
<string name="for_everybody">모두에게</string>
|
||||
<string name="icon_descr_edited">수정됨</string>
|
||||
<string name="icon_descr_image_snd_complete">이미지 보냄</string>
|
||||
<string name="full_name__field">이름 :</string>
|
||||
<string name="hide_profile">프로필 숨기기</string>
|
||||
<string name="icon_descr_flip_camera">카메라 전환</string>
|
||||
<string name="icon_descr_hang_up">전화 끊기</string>
|
||||
<string name="file_with_path">파일 : %s</string>
|
||||
<string name="group_invitation_tap_to_join">탭하여 참여</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">탭하여 익명으로 참여</string>
|
||||
<string name="group_member_status_left">나감</string>
|
||||
<string name="group_member_role_member">멤버</string>
|
||||
<string name="group_member_role_owner">소유자</string>
|
||||
<string name="group_member_status_group_deleted">그룹 삭제됨</string>
|
||||
<string name="group_member_status_invited">초대됨</string>
|
||||
<string name="group_member_status_removed">강퇴됨</string>
|
||||
<string name="icon_descr_expand_role">역할 선택지 펼치기</string>
|
||||
<string name="files_and_media_section">파일 & 미디어</string>
|
||||
<string name="group_invitation_item_description">그룹으로 초대 <xliff:g id="group_name">%1$s</xliff:g></string>
|
||||
<string name="group_link">그룹 링크</string>
|
||||
<string name="group_welcome_title">환영 메시지</string>
|
||||
<string name="group_display_name_field">보여지는 그룹 이름</string>
|
||||
<string name="group_full_name_field">그룹 이름 :</string>
|
||||
<string name="group_is_decentralized">그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요.</string>
|
||||
<string name="group_unsupported_incognito_main_profile_sent">여기에서는 시크릿 모드가 지원되지 않아요. 기본 프로필이 그룹 멤버들에게 전송될 거예요.</string>
|
||||
<string name="group_main_profile_sent">프로필이 그룹 구성원에게 전송될 거예요.</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요.</string>
|
||||
<string name="group_preferences">그룹 설정</string>
|
||||
<string name="group_members_can_send_disappearing">그룹 구성원은 자동 삭제되는 메시지를 보낼 수 있어요.</string>
|
||||
<string name="group_members_can_send_dms">그룹 멤버들끼리 1:1 채팅을 할 수 있어요.</string>
|
||||
<string name="icon_descr_add_members">멤버 초대하기</string>
|
||||
<string name="icon_descr_group_inactive">비활성 그룹</string>
|
||||
<string name="group_member_role_observer">관찰자</string>
|
||||
<string name="group_info_member_you">나 : <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="group_info_section_title_num_members"><xliff:g id="num_members">%1$s</xliff:g> 멤버</string>
|
||||
<string name="file_saved">파일 저장됨</string>
|
||||
<string name="file_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 파일이 저장되어요.</string>
|
||||
<string name="file_will_be_received_when_contact_is_online">대화 상대가 온라인 상태일 때 파일이 전송되어요. 대화 상대가 온라인이 되기를 기다리거나 나중에 다시 확인해 주세요!</string>
|
||||
<string name="icon_descr_record_voice_message">음성 메시지 녹화하기</string>
|
||||
<string name="from_gallery_button">갤러리에서</string>
|
||||
<string name="icon_descr_profile_image_placeholder">프로필 이미지 플레이스 홀더</string>
|
||||
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> 주소</string>
|
||||
<string name="how_to">설명서</string>
|
||||
<string name="how_to_use_your_servers">내 서버 사용법</string>
|
||||
<string name="how_to_use_markdown">마크다운 사용법</string>
|
||||
<string name="how_simplex_works"><xliff:g id="appName">SimpleX</xliff:g> 작동 방식</string>
|
||||
<string name="group_invitation_expired">그룹 초대가 만료되었어요.</string>
|
||||
<string name="group_members_can_delete">그룹 멤버는 보낸 메시지를 영구 삭제할 수 있어요.</string>
|
||||
<string name="group_members_can_send_voice">그룹 멤버는 음성 메시지를 보낼 수 있어요.</string>
|
||||
<string name="hidden_profile_password">숨긴 프로필 비밀번호</string>
|
||||
<string name="full_name_optional__prompt">이름 (선택 사항)</string>
|
||||
<string name="how_it_works">작동 방식</string>
|
||||
<string name="hide_dev_options">숨기기 :</string>
|
||||
<string name="cancel_file__question">파일 전송을 취소할까요\?</string>
|
||||
<string name="file_transfer_will_be_cancelled_warning">파일 전송이 취소될 거예요. 이미 전송이 시작되었다면 중지될 거예요.</string>
|
||||
<string name="icon_descr_sent_msg_status_sent">보냄</string>
|
||||
<string name="group_preview_join_as">%s(으)로 참여</string>
|
||||
<string name="group_preview_you_are_invited">그룹에 초대되었어요.</string>
|
||||
<string name="icon_descr_received_msg_status_unread">잃지 않음</string>
|
||||
<string name="icon_descr_sent_msg_status_send_failed">보내기 실패</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">비인증 전송</string>
|
||||
<string name="icon_descr_help">도움말</string>
|
||||
<string name="first_platform_without_user_ids">개인을 식별할 수 있는 어떠한 정보(임의의 숫자 포함)도 없는 첫 번째 플랫폼. 단순히 약속이 아니라 프로그램 설계상 완전한 익명성을 제공해요.</string>
|
||||
<string name="icon_descr_call_rejected">거절된 전화</string>
|
||||
<string name="icon_descr_call_pending_sent">대기 중인 전화</string>
|
||||
<string name="icon_descr_call_missed">부재중 전화</string>
|
||||
<string name="icon_descr_file">파일</string>
|
||||
<string name="icon_descr_more_button">더 보기</string>
|
||||
<string name="icon_descr_send_message">메시지 보내기</string>
|
||||
<string name="icon_descr_server_status_error">오류</string>
|
||||
<string name="how_to_use_simplex_chat">사용법</string>
|
||||
<string name="icon_descr_email">이메일</string>
|
||||
<string name="icon_descr_server_status_pending">대기 중</string>
|
||||
<string name="icon_descr_settings">설정</string>
|
||||
<string name="icon_descr_waiting_for_image">이미지 기다리는 중</string>
|
||||
<string name="image_decoding_exception_desc">이미지를 디코딩할 수 없어요. 다른 이미지를 시도하거나 개발자에게 문의해 주세요.</string>
|
||||
<string name="image_descr">이미지</string>
|
||||
<string name="image_saved">갤러리에 사진 저장됨</string>
|
||||
<string name="images_limit_desc">동시에 최대 10개까지만 이미지를 보낼 수 있어요.</string>
|
||||
<string name="images_limit_title">이미지 수가 너무 많아요!</string>
|
||||
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">거절해도 상대에게 알림이 전송되지 않아요.</string>
|
||||
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 보여주거나</b> 링크를 공유해 주세요.</string>
|
||||
<string name="icon_descr_video_call">영상 전화</string>
|
||||
<string name="icon_descr_video_off">영상 끄기</string>
|
||||
<string name="icon_descr_speaker_on">스피커 켜기</string>
|
||||
<string name="icon_descr_video_on">영상 켜기</string>
|
||||
<string name="icon_descr_speaker_off">스피커 끄기</string>
|
||||
<string name="import_database">데이터베이스 가져오기</string>
|
||||
<string name="import_database_confirmation">가져오기</string>
|
||||
<string name="incognito">익명 모드</string>
|
||||
<string name="incognito_info_find">익명 채팅에 사용되는 프로필을 확인하려면 채팅 상단에 있는 연락처 또는 그룹 이름을 탭하세요.</string>
|
||||
<string name="image_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 이미지가 수신될 거예요.</string>
|
||||
<string name="image_descr_profile_image">프로필 이미지</string>
|
||||
<string name="incognito_info_allows">하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요.</string>
|
||||
<string name="immune_to_spam_and_abuse">스팸 및 남용에 면역</string>
|
||||
<string name="ignore">무시하기</string>
|
||||
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser"><xliff:g id="appName">SimpleX Chat</xliff:g> 초대 링크를 받으면 브라우저에서 참여할 수 있어요 :</string>
|
||||
<string name="image_descr_link_preview">링크 미리보기 이미지</string>
|
||||
<string name="image_descr_qr_code">QR 코드</string>
|
||||
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> 팀</string>
|
||||
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">직접 만날 수 없다면 <b>영상 통화에서 QR 코드를 스캔</b>하거나 상대에게 초대 링크를 공유할 수 있어요.</string>
|
||||
<string name="icon_descr_video_snd_complete">동영상 보내짐</string>
|
||||
<string name="icon_descr_video_asked_to_receive">동영상 수신 요청됨</string>
|
||||
<string name="icon_descr_waiting_for_video">동영상 기다리는 중</string>
|
||||
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> 로고</string>
|
||||
<string name="image_will_be_received_when_contact_is_online">대화 상대가 온라인 상태일 때 이미지가 수신될 거예요. 기다리거나 나중에 확인하세요!</string>
|
||||
<string name="import_database_question">채팅 데이터베이스를 가져올까요\?</string>
|
||||
<string name="server_error">오류</string>
|
||||
<string name="invalid_chat">유효하지 않는 채팅</string>
|
||||
<string name="invalid_data">잘못된 데이터</string>
|
||||
<string name="invalid_message_format">잘못된 메시지 형식</string>
|
||||
<string name="invalid_connection_link">잘못된 연결 링크</string>
|
||||
<string name="notifications">알림</string>
|
||||
<string name="integrity_msg_skipped"><xliff:g id="connection ID" example="1">%1$d</xliff:g> 읽지 않은 메시지</string>
|
||||
<string name="keychain_error">키체인 오류</string>
|
||||
<string name="invite_to_group_button">그룹에 초대하기</string>
|
||||
<string name="info_row_local_name">로컬 네임</string>
|
||||
<string name="join_group_button">참여</string>
|
||||
<string name="join_group_question">그룹에 참여할까요\?</string>
|
||||
<string name="invite_prohibited_description">이 그룹에서는 기본 프로필을 사용하는 중인데 반해, 익명 프로필로 연락하고 있는 대화 상대를 초대하려고 하셨어요.</string>
|
||||
<string name="initial_member_role">초기 역할</string>
|
||||
<string name="info_row_group">그룹</string>
|
||||
<string name="incompatible_database_version">호환되지 않는 데이터베이스 버전</string>
|
||||
<string name="joining_group">그룹에 참여 중</string>
|
||||
<string name="incognito_info_protects">익명 모드는 기본 프로필 이름과 사진과 같은 개인 정보를 보호해줘요. 새 대화 상대마다 새로운 랜덤 프로필이 만들어져요.</string>
|
||||
<string name="is_verified">%s 은(는) 인증되었어요.</string>
|
||||
<string name="italic">기울게</string>
|
||||
<string name="incognito_info_share">익명 프로필 사용 중 초대받은 그룹에 참여하면, 그 그룹에서도 동일한 익명 프로필이 사용되어요.</string>
|
||||
<string name="incognito_random_profile">내 랜덤 프로필</string>
|
||||
<string name="incoming_audio_call">음성 전화 옴</string>
|
||||
<string name="is_not_verified">%s은(는) 인증되지 않았어요.</string>
|
||||
<string name="install_simplex_chat_for_terminal">터미널용 <xliff:g id="appNameFull">SimpleX Chat</xliff:g>를 설치하세요</string>
|
||||
<string name="incoming_video_call">영상 전화 옴</string>
|
||||
<string name="invalid_migration_confirmation">잘못된 마이그레이션 확인</string>
|
||||
<string name="join_group_incognito_button">익명 모드로 참여</string>
|
||||
<string name="invalid_QR_code">잘못된 QR 코드</string>
|
||||
<string name="incorrect_code">잘못된 보안 코드!</string>
|
||||
<string name="invalid_contact_link">잘못된 링크!</string>
|
||||
<string name="marked_deleted_description">삭제됨으로 표시됨</string>
|
||||
<string name="moderated_item_description">%s에 의해 조정됨</string>
|
||||
<string name="live">라이브</string>
|
||||
<string name="moderated_description">조정됨</string>
|
||||
<string name="network_error_desc"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> 에서 네트워크 연결 상태를 확인한 후 다시 시도하세요.</string>
|
||||
<string name="la_notice_title_simplex_lock">SimpleX 잠금</string>
|
||||
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">정보를 보호하려면 SimpleX 잠금을 켜세요.
|
||||
\n이 기능이 활성화하기 전에 인증을 완료하라는 메시지가 표시될 거예요.</string>
|
||||
<string name="notification_new_contact_request">새로운 대화 요청</string>
|
||||
<string name="notification_preview_mode_hidden">숨겨짐</string>
|
||||
<string name="notification_preview_mode_contact_desc">대화 상대 이름만 표시</string>
|
||||
<string name="notification_preview_new_message">새로운 메시지</string>
|
||||
<string name="notifications_mode_off">앱이 열릴 때 실행</string>
|
||||
<string name="notifications_mode_periodic">주기적으로 실행됨</string>
|
||||
<string name="notification_display_mode_hidden_desc">연락처 이름 및 메시지 숨기기</string>
|
||||
<string name="la_notice_turn_on">켜기</string>
|
||||
<string name="message_delivery_error_desc">대화 상대가 나와의 연결을 삭제했을 가능성이 커요.</string>
|
||||
<string name="message_delivery_error_title">메시지 전달 오류</string>
|
||||
<string name="moderate_verb">조정</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">모든 멤버에게서 메시지가 삭제될 거예요.</string>
|
||||
<string name="moderate_message_will_be_marked_warning">이 메시지는 모든 멤버에게 조정됨으로 표시될 거예요.</string>
|
||||
<string name="no_details">세부 정보 없음</string>
|
||||
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 모바일: <b>모바일 앱에서 열기</b>를 누른 다음 앱에서 <b>연결</b>을 누르세요.</string>
|
||||
<string name="mark_code_verified">확인됨으로 표시</string>
|
||||
<string name="network_and_servers">네트워크 및 서버</string>
|
||||
<string name="new_database_archive">새 데이터베이스 보관함</string>
|
||||
<string name="new_member_role">새 멤버 역할</string>
|
||||
<string name="no_contacts_to_add">추가할 연락처 없음</string>
|
||||
<string name="no_contacts_selected">선택한 연락처 없음</string>
|
||||
<string name="network_status">네트워크 상태</string>
|
||||
<string name="language_system">시스템</string>
|
||||
<string name="messages_section_title">메시지</string>
|
||||
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
|
||||
<string name="member_info_section_title_member">멤버</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
|
||||
<string name="network_options_revert">되돌리기</string>
|
||||
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
|
||||
<string name="leave_group_button">나가기</string>
|
||||
<string name="large_file">큰 파일!</string>
|
||||
<string name="network_settings_title">네트워크 설정</string>
|
||||
<string name="network_use_onion_hosts_required_desc">연결하려면 Onion 호스트가 필요해요.</string>
|
||||
<string name="network_option_ping_count">핑 횟수</string>
|
||||
<string name="network_option_ping_interval">핑 간격</string>
|
||||
<string name="network_option_protocol_timeout">프로토콜 타임아웃</string>
|
||||
<string name="network_option_seconds_label">초</string>
|
||||
<string name="network_option_tcp_connection_timeout">TCP 연결 시간 초과</string>
|
||||
<string name="muted_when_inactive">비활성 시 음소거!</string>
|
||||
<string name="message_deletion_prohibited_in_chat">이 채팅에서는 메시지 영구 삭제가 허용되지 않아요.</string>
|
||||
<string name="make_private_connection">비공개 연결하기</string>
|
||||
<string name="no_received_app_files">수신 또는 전송된 파일 없음</string>
|
||||
<string name="network_options_save">저장하기</string>
|
||||
<string name="make_profile_private">프로필을 비공개로 설정하세요!</string>
|
||||
<string name="live_message">라이브 메시지!</string>
|
||||
<string name="mark_read">읽음으로 표시</string>
|
||||
<string name="mark_unread">읽지 않음으로 표시</string>
|
||||
<string name="mute_chat">음소거</string>
|
||||
<string name="markdown_in_messages">메시지에 사용된 마크다운</string>
|
||||
<string name="network_socks_toggle">SOCKS 프록시 사용 (포트 9050)</string>
|
||||
<string name="network_disable_socks">직접적인 인터넷 연결을 사용할까요\?</string>
|
||||
<string name="network_enable_socks">SOCKS 프록시를 사용할까요\?</string>
|
||||
<string name="network_disable_socks_info">설정하면 메시징 서버에서 내 IP 주소와 내가 연결하려는 서버를 볼 수 있어요.</string>
|
||||
<string name="network_use_onion_hosts">.onion 호스트 사용</string>
|
||||
<string name="network_use_onion_hosts_no">아니요</string>
|
||||
<string name="network_use_onion_hosts_prefer">사용 가능한 경우</string>
|
||||
<string name="network_use_onion_hosts_required">필요함</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
|
||||
<string name="network_use_onion_hosts_no_desc">Onion 호스트가 사용되지 않을 거예요.</string>
|
||||
<string name="network_session_mode_transport_isolation">전송 격리</string>
|
||||
<string name="network_use_onion_hosts_no_desc_in_alert">Onion 호스트가 사용되지 않을 거예요.</string>
|
||||
<string name="network_use_onion_hosts_prefer_desc_in_alert">사용 가능한 경우 Onion 호스트가 사용될 거예요.</string>
|
||||
<string name="network_use_onion_hosts_required_desc_in_alert">연결하려면 Onion 호스트가 필요해요.</string>
|
||||
<string name="next_generation_of_private_messaging">차세대 사생활 보호 메시징</string>
|
||||
<string name="new_passphrase">새 비밀번호…</string>
|
||||
<string name="network_option_enable_tcp_keep_alive">TCP 연결 유지 활성화</string>
|
||||
<string name="new_in_version">%s의 새로운 기능</string>
|
||||
<string name="markdown_help">마크다운 도움말</string>
|
||||
<string name="many_people_asked_how_can_it_deliver">많은 사람들의 질문 : <i><xliff:g id="appName">SimpleX</xliff:g>에는 사용자 식별자가 없는데도 어떻게 메시지를 전달할 수 있어요\?</i></string>
|
||||
<string name="leave_group_question">그룹에서 나갈까요\?</string>
|
||||
<string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만 다음에 대한 다운 마이그레이션 없음: %s</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string>
|
||||
<string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
|
||||
<string name="network_options_reset_to_defaults">기본값으로 재설정</string>
|
||||
<string name="notification_preview_mode_message">메시지 내용</string>
|
||||
<string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string>
|
||||
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string>
|
||||
<string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string>
|
||||
<string name="periodic_notifications">주기적 알림</string>
|
||||
<string name="periodic_notifications_desc">주기적으로 새 메시지를 확인해요 — 하루에 몇 퍼센트의 배터리를 사용할 거예요. 푸시 알림을 사용하지 않아요 — 기기의 데이터가 서버로 전송되지 않아요.</string>
|
||||
<string name="periodic_notifications_disabled">주기적 알림이 비활성화되었어요.</string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat 통화</string>
|
||||
<string name="ntf_channel_messages">SimpleX Chat 메시지</string>
|
||||
<string name="observer_cant_send_message_desc">그룹 관리자에게 문의해 주세요.</string>
|
||||
<string name="observer_cant_send_message_title">메시지를 보낼 수 없습니다!</string>
|
||||
<string name="ok">네</string>
|
||||
<string name="reject_contact_button">거절</string>
|
||||
<string name="password_to_show">비밀번호 표시</string>
|
||||
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">사용자 디바이스에만 <b>2계층 종단 간 암호화</b> 로 전송된 사용자 프로필, 연락처, 그룹 및 메시지를 저장되어요.</string>
|
||||
<string name="read_more_in_github">자세한 내용은 GitHub에서 확인해 주세요.</string>
|
||||
<string name="privacy_and_security">개인 정보 및 보안</string>
|
||||
<string name="notifications_will_be_hidden">알림은 앱이 중지되기 전까지만 전달될 거예요!</string>
|
||||
<string name="only_you_can_send_disappearing">자동 삭제되는 메시지는 나만 보낼 수 있어요.</string>
|
||||
<string name="prohibit_sending_disappearing_messages">자동 삭제되는 메시지 허용되지 않음.</string>
|
||||
<string name="prohibit_sending_voice_messages">음성 메시지 허용되지 않음.</string>
|
||||
<string name="prohibit_sending_disappearing">자동 삭제되는 메시지 허용되지 않음.</string>
|
||||
<string name="old_database_archive">이전 데이터베이스 기록</string>
|
||||
<string name="rcv_group_event_member_added"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> 초대됨</string>
|
||||
<string name="rcv_group_event_member_left">나감</string>
|
||||
<string name="rcv_group_event_user_deleted">강퇴됨</string>
|
||||
<string name="rcv_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> 강퇴됨</string>
|
||||
<string name="only_group_owners_can_change_prefs">그룹 소유자만 그룹 설정을 변경할 수 있어요.</string>
|
||||
<string name="receiving_via">다음을 통해 수신</string>
|
||||
<string name="only_your_contact_can_send_disappearing">대화 상대만 자동 삭제되는 메시지를 보낼 수 있어요.</string>
|
||||
<string name="prohibit_direct_messages">멤버들 간의 1:1 채팅이 허용되지 않음.</string>
|
||||
<string name="only_you_can_send_voice">나만 음성 메시지를 보낼 수 있어요.</string>
|
||||
<string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string>
|
||||
<string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string>
|
||||
<string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string>
|
||||
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string>
|
||||
<string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string>
|
||||
<string name="one_time_link">일회성 초대 링크</string>
|
||||
<string name="paste_button">붙여넣기</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">프로필은 대화 상대들하고만 공유됩니다.</string>
|
||||
<string name="privacy_redefined">프라이버시의 재정의</string>
|
||||
<string name="opensource_protocol_and_code_anybody_can_run_servers">오픈 소스 프로토콜과 코드 - 누구나 자신만의 서버를 구축할 수 있어요.</string>
|
||||
<string name="onboarding_notifications_mode_off">앱이 실행 중일 때</string>
|
||||
<string name="read_more_in_github_with_link">ㅍ자세한 내용은 \u0020<font color="#0088ff">GitHub</font> 에서 확인해 주세요.</string>
|
||||
<string name="relay_server_protects_ip">릴레이 서버는 IP 주소를 숨겨주지만, 통화 시간을 관찰 할 수 있어요.</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">그룹 링크로 초대</string>
|
||||
<string name="onboarding_notifications_mode_subtitle">설정을 통해 나중에 변경할 수 있어요.</string>
|
||||
<string name="onboarding_notifications_mode_title">비공개 알림</string>
|
||||
<string name="reject">거절</string>
|
||||
<string name="open_simplex_chat_to_accept_call"><xliff:g id="appNameFull">SimpleX Chat</xliff:g>을 열어 전화 받기</string>
|
||||
<string name="open_verb">열기</string>
|
||||
<string name="protect_app_screen">앱 잠금</string>
|
||||
<string name="personal_welcome"><xliff:g>%1$s</xliff:g>님, 환영합니다!</string>
|
||||
<string name="only_stored_on_members_devices">(그룹 구성원에게만 저장됨)</string>
|
||||
<string name="paste_connection_link_below_to_connect">아래 칸에 받은 링크를 붙여넣기하여 대화 상대와 연결해 주세요.</string>
|
||||
<string name="rate_the_app">앱 평가하기</string>
|
||||
<string name="onboarding_notifications_mode_periodic">주기적</string>
|
||||
<string name="onboarding_notifications_mode_service">즉시</string>
|
||||
<string name="paste_the_link_you_received">받은 링크 붙여넣기</string>
|
||||
<string name="relay_server_if_necessary">릴레이 서버는 필요한 경우에만 사용되어요. 릴레이 서버가 사용되지 않으면 제3자가 내 IP 주소를 관찰할 수 있어요.</string>
|
||||
<string name="open_chat">채팅 열기</string>
|
||||
<string name="people_can_connect_only_via_links_you_share">공유한 링크를 통해서만 나에게 연결할 수 있어요.</string>
|
||||
<string name="rcv_group_event_updated_group_profile">그룹 프로필 업데이트됨</string>
|
||||
<string name="profile_password">프로필 비밀번호</string>
|
||||
<string name="reveal_verb">보이기</string>
|
||||
<string name="save_verb">저장하기</string>
|
||||
<string name="scan_QR_code">QR 코드 스캔하기</string>
|
||||
<string name="reply_verb">답장</string>
|
||||
<string name="reset_verb">초기화</string>
|
||||
<string name="save_servers_button">저장하기</string>
|
||||
<string name="save_and_notify_contacts">저장하고 모든 대화 상대에게 알리기</string>
|
||||
<string name="save_and_notify_group_members">저장하고 그룹 멤버들에게 알리기</string>
|
||||
<string name="save_and_notify_contact">저장하고 대화 상대에게 알리기</string>
|
||||
<string name="remove_passphrase">지우기</string>
|
||||
<string name="save_archive">아카이브 저장하기</string>
|
||||
<string name="save_color">색상 저장하기</string>
|
||||
<string name="save_passphrase_in_keychain">암호 저장소에 비밀번호 저장하기</string>
|
||||
<string name="restore_database">데이터베이스 백업 복원하기</string>
|
||||
<string name="restore_database_alert_desc">데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요.</string>
|
||||
<string name="restore_database_alert_title">데이터베이스 백업을 복원할까요\?</string>
|
||||
<string name="restore_passphrase_not_found_desc">키스토어에서 암호를 찾을 수 없어요. 직접 입력해 주세요. 백업 도구를 사용하여 복원했을 때 이 문제가 발생할 수 있는데, 그런 경우가 아니라면 개발자에게 알려주세요.</string>
|
||||
<string name="save_and_update_group_profile">저장하고 그룹 프로필 업데이트하기</string>
|
||||
<string name="save_welcome_message_question">환영 메시지를 저장할까요\?</string>
|
||||
<string name="save_group_profile">그룹 프로필 저장하기</string>
|
||||
<string name="reset_color">색상 초기화</string>
|
||||
<string name="remove_member_confirmation">강퇴하기</string>
|
||||
<string name="scan_code">코드 스캔하기</string>
|
||||
<string name="scan_code_from_contacts_app">대화 상대의 앱에서 보안 코드를 스캔해 주세요.</string>
|
||||
<string name="saved_ICE_servers_will_be_removed">저장된 WebRTC ICE 서버가 제거될 거예요.</string>
|
||||
<string name="save_passphrase_and_open_chat">비밀번호 저장하고 채팅 열기</string>
|
||||
<string name="role_in_group">역할</string>
|
||||
<string name="save_preferences_question">설정을 저장할까요\?</string>
|
||||
<string name="save_profile_password">프로필 비밀번호 저장하기</string>
|
||||
<string name="restart_the_app_to_use_imported_chat_database">가져온 채팅 데이터베이스를 사용하려면 앱을 다시 실행해 주세요.</string>
|
||||
<string name="restart_the_app_to_create_a_new_chat_profile">새 프로필을 만드려면 앱을 다시 실행해 주세요.</string>
|
||||
<string name="remove_passphrase_from_keychain">암호 저장소에서 비밀번호를 삭제할까요\?</string>
|
||||
<string name="run_chat_section">채팅 기능 실행하기</string>
|
||||
<string name="restore_database_alert_confirm">복원하기</string>
|
||||
<string name="sender_cancelled_file_transfer">대화 상대가 파일 전송을 취소했어요.</string>
|
||||
<string name="smp_server_test_secure_queue">보안 대기열</string>
|
||||
<string name="smp_server_test_upload_file">파일 업로드하기</string>
|
||||
<string name="smp_server_test_compare_file">파일 비교</string>
|
||||
<string name="smp_server_test_delete_file">파일 삭제하기</string>
|
||||
<string name="smp_server_test_download_file">파일 다운로드하기</string>
|
||||
<string name="share_verb">공유</string>
|
||||
<string name="send_live_message">라이브 메시지 보내기</string>
|
||||
<string name="show_QR_code">QR코드 보기</string>
|
||||
<string name="smp_server_test_create_file">파일 만들기</string>
|
||||
<string name="sender_you_pronoun">나</string>
|
||||
<string name="sending_files_not_yet_supported">파일 보내기는 아직 지원되지 않아요.</string>
|
||||
<string name="simplex_link_connection"><xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g>를 통해</string>
|
||||
<string name="simplex_link_contact">SimpleX 연락처 주소</string>
|
||||
<string name="simplex_link_group">SimpleX 그룹 링크</string>
|
||||
<string name="simplex_link_invitation">SimpleX 일회용 초대 링크</string>
|
||||
<string name="simplex_link_mode">SimpleX 링크</string>
|
||||
<string name="simplex_link_mode_browser">브라우저를 통해</string>
|
||||
<string name="simplex_link_mode_browser_warning">브라우저에서 링크를 열면 익명성 또는 보안에 문제가 생길 수 있어요. 신뢰할 수 없는 SimpleX 링크는 빨간색으로 표시되어요.</string>
|
||||
<string name="simplex_link_mode_full">링크 전체</string>
|
||||
<string name="sender_may_have_deleted_the_connection_request">보낸 사람이 연결 요청을 삭제했을 수 있어요.</string>
|
||||
<string name="service_notifications_disabled">즉각적인 알림이 비활성화되었어요!</string>
|
||||
<string name="settings_notification_preview_mode_title">미리보기 표시</string>
|
||||
<string name="settings_notification_preview_title">알림 미리보기</string>
|
||||
<string name="settings_notifications_mode_title">알림 서비스</string>
|
||||
<string name="simplex_service_notification_text">메시지 받는 중…</string>
|
||||
<string name="simplex_service_notification_title"><xliff:g id="appNameFull">SimpleX 채팅</xliff:g> 서비스</string>
|
||||
<string name="share_file">파일 공유…</string>
|
||||
<string name="share_image">이미지 공유…</string>
|
||||
<string name="share_message">메시지 공유…</string>
|
||||
<string name="send_live_message_desc">라이브 메시지 보내기 - 입력 과정을 실시간으로 상대에게 보여줘요.</string>
|
||||
<string name="send_verb">보내기</string>
|
||||
<string name="share_invitation_link">초대 링크 공유</string>
|
||||
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">연락하고자 하는 사람이 앱에서 QR 코드를 스캔할 수 있어요.</string>
|
||||
<string name="security_code">보안 코드</string>
|
||||
<string name="smp_servers_scan_qr">서버 QR코드 스캔</string>
|
||||
<string name="smp_servers_test_some_failed">일부 서버가 테스트에 통과하지 못했습니다 :</string>
|
||||
<string name="smp_servers_test_failed">서버 테스트 실패!</string>
|
||||
<string name="section_title_welcome_message">환영 메시지</string>
|
||||
<string name="share_link">링크 공유</string>
|
||||
<string name="show_call_on_lock_screen">표시하기</string>
|
||||
<string name="select_contacts">연락처 선택</string>
|
||||
<string name="skip_inviting_button">멤버 초대 건너뛰기</string>
|
||||
<string name="sending_via">다음을 통해 보내기</string>
|
||||
<string name="stop_chat_confirmation">멈추기</string>
|
||||
<string name="snd_group_event_changed_member_role">%s의 역할을 %s로 변경했어요.</string>
|
||||
<string name="section_title_for_console">콘솔용</string>
|
||||
<string name="should_be_at_least_one_visible_profile">적어도 하나의 숨겨지지 않은 사용자 프로필이 있어야 해요.</string>
|
||||
<string name="set_group_preferences">그룹 설정 지정하기</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed_for_member">%s의 주소를 바꿨어요</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">주소를 바꿨어요</string>
|
||||
<string name="snd_group_event_group_profile_updated">그룹 프로필 업데이트됨</string>
|
||||
<string name="should_be_at_least_one_profile">적어도 하나의 사용자 프로필이 있어야 해요.</string>
|
||||
<string name="smp_servers_test_server">서버 테스트하기</string>
|
||||
<string name="smp_servers_use_server">서버 사용하기</string>
|
||||
<string name="smp_servers_use_server_for_new_conn">새로운 대화에 사용</string>
|
||||
<string name="smp_servers_invalid_address">잘못된 서버 주소에요!</string>
|
||||
<string name="secret">비밀</string>
|
||||
<string name="status_no_e2e_encryption">종단 간 암호화 안 됨</string>
|
||||
<string name="settings_section_title_themes">테마</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">내 역할이 %s로 변경되었어요.</string>
|
||||
<string name="snd_group_event_user_left">나감</string>
|
||||
<string name="settings_section_title_help">도움말</string>
|
||||
<string name="settings_section_title_settings">설정</string>
|
||||
<string name="send_link_previews">링크 미리보기 보내기</string>
|
||||
<string name="settings_section_title_socks">SOCKS 프록시</string>
|
||||
<string name="settings_section_title_support">SIMPLEX CHAT 도와주기</string>
|
||||
<string name="settings_section_title_you">나</string>
|
||||
<string name="settings_experimental_features">실험적 기능</string>
|
||||
<string name="show_dev_options">표시 :</string>
|
||||
<string name="set_contact_name">연락처 이름 설정</string>
|
||||
<string name="send_us_an_email">개발자에게 이메일 보내기</string>
|
||||
<string name="smp_servers_enter_manually">서버를 수동으로 입력</string>
|
||||
<string name="smp_servers_preset_server">미리 설정된 서버</string>
|
||||
<string name="smp_servers_save">서버 저장하기</string>
|
||||
<string name="smp_servers_test_servers">서버 테스트하기</string>
|
||||
<string name="smp_servers_per_user">현재 채팅 프로필의 새로운 연결을 위한 서버</string>
|
||||
<string name="smp_save_servers_question">서버를 저장할까요\?</string>
|
||||
<string name="star_on_github">GitHub에서 별 주기</string>
|
||||
<string name="show_developer_options">개발자 옵션 보기</string>
|
||||
<string name="settings_section_title_messages">메시지 및 파일</string>
|
||||
<string name="settings_section_title_incognito">익명 모드</string>
|
||||
<string name="settings_section_title_experimenta">실험적</string>
|
||||
<string name="set_password_to_export">내보낼 비밀번호 설정</string>
|
||||
<string name="smp_servers">SMP 서버</string>
|
||||
<string name="smp_servers_preset_address">미리 설정된 서버 주소</string>
|
||||
<string name="smp_servers_your_server">내 서버</string>
|
||||
<string name="smp_servers_your_server_address">내 서버 주소</string>
|
||||
<string name="snd_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g>을(를) 강퇴했어요.</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">채팅 데이터베이스를 내보내기, 가져오기 또는 삭제 하려면 채팅 기능을 중지해 주세요. 채팅 기능이 중지된 동안에는 메시지를 주고받을 수 없어요.</string>
|
||||
<string name="store_passphrase_securely">비밀번호를 모르면 변경하거나 찾을 수 없으므로 비밀번호를 안전하게 보관해 주세요.</string>
|
||||
<string name="submit_passcode">제출하기</string>
|
||||
<string name="store_passphrase_securely_without_recover">비밀번호를 모르면 채팅에 액세스할 수 없으니 비밀번호를 안전하게 보관해 주세요.</string>
|
||||
<string name="stop_chat_question">채팅 기능을 중지할까요\?</string>
|
||||
<string name="stop_chat_to_enable_database_actions">데이터베이스 작업을 할 수 있도록 채팅 기능을 중지하기</string>
|
||||
<string name="switch_receiving_address">수신 주소 바꾸기</string>
|
||||
</resources>
|
||||
@@ -47,4 +47,312 @@
|
||||
<string name="auth_unavailable">Tapatybės nustatymas neprieinamas</string>
|
||||
<string name="impossible_to_recover_passphrase"><b>Turėkite omenyje</b>: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</string>
|
||||
<string name="cancel_verb">Atsisakyti</string>
|
||||
<string name="callstate_connecting">jungiamasi…</string>
|
||||
<string name="connect_via_link_verb">Prisijungti</string>
|
||||
<string name="server_connected">prisijungta</string>
|
||||
<string name="server_connecting">jungiamasi</string>
|
||||
<string name="display_name_connection_established">ryšys užmegztas</string>
|
||||
<string name="display_name_connecting">jungiasi…</string>
|
||||
<string name="connection_error">Ryšio klaida</string>
|
||||
<string name="smp_server_test_connect">Prisijungti</string>
|
||||
<string name="notification_contact_connected">Prisijungė</string>
|
||||
<string name="connect_button">Prisijungti</string>
|
||||
<string name="group_member_status_connected">prisijungė</string>
|
||||
<string name="contact_connection_pending">jungiasi…</string>
|
||||
<string name="group_connection_pending">jungiasi…</string>
|
||||
<string name="icon_descr_server_status_connected">Prisijungta</string>
|
||||
<string name="confirm_verb">Patvirtinti</string>
|
||||
<string name="configure_ICE_servers">Konfigūruoti ICE serverius</string>
|
||||
<string name="callstate_connected">prisijungta</string>
|
||||
<string name="rcv_group_event_member_connected">prisijungė</string>
|
||||
<string name="info_row_connection">Ryšys</string>
|
||||
<string name="confirm_database_upgrades">Patvirtinti duomenų bazių naujinimus</string>
|
||||
<string name="group_member_status_connecting">jungiasi</string>
|
||||
<string name="network_session_mode_entity">Ryšys</string>
|
||||
<string name="database_passphrase_will_be_updated">Duomenų bazės šifravimo slaptafrazė bus atnaujinta.</string>
|
||||
<string name="core_version">Branduolio versija: v%s</string>
|
||||
<string name="delete_address__question">Ištrinti adresą\?</string>
|
||||
<string name="save_preferences_question">Įrašyti nuostatas\?</string>
|
||||
<string name="create_profile">Sukurti profilį</string>
|
||||
<string name="callstatus_rejected">atmestas skambutis</string>
|
||||
<string name="callstate_ended">užbaigtas</string>
|
||||
<string name="onboarding_notifications_mode_title">Privatūs pranešimai</string>
|
||||
<string name="ignore">Nepaisyti</string>
|
||||
<string name="icon_descr_flip_camera">Apversti kamerą</string>
|
||||
<string name="icon_descr_call_rejected">Atmestas skambutis</string>
|
||||
<string name="privacy_and_security">Privatumas ir saugumas</string>
|
||||
<string name="settings_section_title_device">ĮRENGINYS</string>
|
||||
<string name="settings_section_title_help">PAGALBA</string>
|
||||
<string name="encrypt_database">Šifruoti</string>
|
||||
<string name="remove_passphrase">Šalinti</string>
|
||||
<string name="button_delete_group">Ištrinti grupę</string>
|
||||
<string name="v4_2_group_links">Grupių nuorodos</string>
|
||||
<string name="server_error">klaida</string>
|
||||
<string name="simplex_link_mode_description">Aprašas</string>
|
||||
<string name="error_saving_smp_servers">Klaida įrašant SMP serverius</string>
|
||||
<string name="error_setting_network_config">Klaida atnaujinant tinklo konfigūraciją</string>
|
||||
<string name="failed_to_create_user_title">Klaida kuriant profilį!</string>
|
||||
<string name="failed_to_parse_chat_title">Nepavyko įkelti pokalbio</string>
|
||||
<string name="failed_to_parse_chats_title">Nepavyko įkelti pokalbių</string>
|
||||
<string name="failed_to_active_user_title">Klaida perjungiant profilį!</string>
|
||||
<string name="error_sending_message">Klaida siunčiant žinutę</string>
|
||||
<string name="error_creating_address">Klaida kuriant adresą</string>
|
||||
<string name="error_joining_group">Klaida prisijungiant prie grupės</string>
|
||||
<string name="error_receiving_file">Klaida gaunant failą</string>
|
||||
<string name="error_changing_address">Klaida keičiant adresą</string>
|
||||
<string name="error_deleting_contact">Klaida ištrinant adresatą</string>
|
||||
<string name="error_deleting_group">Klaida ištrinant grupę</string>
|
||||
<string name="smp_server_test_disconnect">Atsijungti</string>
|
||||
<string name="error_deleting_user">Klaida ištrinant naudotojo profilį</string>
|
||||
<string name="notification_display_mode_hidden_desc">Slėpti adresatą ir žinutę</string>
|
||||
<string name="copy_verb">Kopijuoti</string>
|
||||
<string name="reply_verb">Atsakyti</string>
|
||||
<string name="delete_message__question">Ištrinti žinutę\?</string>
|
||||
<string name="icon_descr_file">Failas</string>
|
||||
<string name="maximum_supported_file_size">Šiuo metu didžiausias palaikomas failo dydis yra <xliff:g id="maxFileSize">%1$s</xliff:g>.</string>
|
||||
<string name="file_not_found">Failas nerastas</string>
|
||||
<string name="file_saved">Failas įrašytas</string>
|
||||
<string name="icon_descr_server_status_disconnected">Atsijungta</string>
|
||||
<string name="icon_descr_server_status_error">Klaida</string>
|
||||
<string name="ask_your_contact_to_enable_voice">Paprašykite adresato, kad įjungtų balso žinučių siuntimą.</string>
|
||||
<string name="reset_verb">Atstatyti</string>
|
||||
<string name="delete_group_menu_action">Ištrinti</string>
|
||||
<string name="image_descr_qr_code">QR kodas</string>
|
||||
<string name="icon_descr_help">pagalba</string>
|
||||
<string name="icon_descr_email">El. paštas</string>
|
||||
<string name="create_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
|
||||
<string name="scan_code">Skenuoti kodą</string>
|
||||
<string name="database_passphrase_and_export">Duomenų bazės slaptafrazė ir eksportavimas</string>
|
||||
<string name="smp_servers_delete_server">Ištrinti serverį</string>
|
||||
<string name="error_saving_ICE_servers">Klaida įrašant ICE serverius</string>
|
||||
<string name="save_servers_button">Įrašyti</string>
|
||||
<string name="files_and_media_section">Failai ir medija</string>
|
||||
<string name="delete_files_and_media_all">Ištrinti visus failus</string>
|
||||
<string name="delete_files_and_media_question">Ištrinti failus ir mediją\?</string>
|
||||
<string name="delete_archive">Ištrinti archyvą</string>
|
||||
<string name="delete_chat_archive_question">Ištrinti pokalbio archyvą\?</string>
|
||||
<string name="snd_group_event_group_profile_updated">grupės profilis atnaujintas</string>
|
||||
<string name="info_row_group">Grupė</string>
|
||||
<string name="users_delete_question">Ištrinti pokalbio profilį\?</string>
|
||||
<string name="network_options_revert">Sugrąžinti</string>
|
||||
<string name="network_options_save">Įrašyti</string>
|
||||
<string name="feature_enabled">įjungta</string>
|
||||
<string name="delete_after">Ištrinti po</string>
|
||||
<string name="error_updating_user_privacy">Klaida atnaujinant naudotojo privatumą</string>
|
||||
<string name="simplex_service_notification_text">Gaunamos žinutės…</string>
|
||||
<string name="hide_notification">Slėpti</string>
|
||||
<string name="save_verb">Įrašyti</string>
|
||||
<string name="delete_verb">Ištrinti</string>
|
||||
<string name="edit_verb">Taisyti</string>
|
||||
<string name="hide_verb">Slėpti</string>
|
||||
<string name="reveal_verb">Atskleisti</string>
|
||||
<string name="for_me_only">Ištrinti man</string>
|
||||
<string name="for_everybody">Visiems</string>
|
||||
<string name="icon_descr_edited">taisyta</string>
|
||||
<string name="observer_cant_send_message_desc">Susisiekite su grupės administratoriumi.</string>
|
||||
<string name="button_delete_contact">Ištrinti adresatą</string>
|
||||
<string name="delete_contact_question">Ištrinti adresatą\?</string>
|
||||
<string name="create_group">Sukurti slaptą grupę</string>
|
||||
<string name="from_gallery_button">Iš galerijos</string>
|
||||
<string name="toast_permission_denied">Leidimas atmestas!</string>
|
||||
<string name="reject_contact_button">Atmesti</string>
|
||||
<string name="incorrect_code">Neteisingas saugumo kodas!</string>
|
||||
<string name="smp_servers_save">Įrašyti serverius</string>
|
||||
<string name="smp_save_servers_question">Įrašyti serverius\?</string>
|
||||
<string name="create_address">Sukurti adresą</string>
|
||||
<string name="delete_address">Ištrinti adresą</string>
|
||||
<string name="error_saving_user_password">Klaida įrašant naudotojo slaptažodį</string>
|
||||
<string name="create_profile_button">Sukurti</string>
|
||||
<string name="how_it_works">Kaip tai veikia</string>
|
||||
<string name="incoming_audio_call">Gaunamas garso skambutis</string>
|
||||
<string name="incoming_video_call">Gaunamas vaizdo skambutis</string>
|
||||
<string name="reject">Atmesti</string>
|
||||
<string name="no_call_on_lock_screen">Išjungti</string>
|
||||
<string name="settings_experimental_features">Eksperimentinės ypatybės</string>
|
||||
<string name="database_passphrase">Duomenų bazės slaptafrazė</string>
|
||||
<string name="delete_database">Ištrinti duomenų bazę</string>
|
||||
<string name="export_database">Eksportuoti duomenų bazę</string>
|
||||
<string name="import_database">Importuoti duomenų bazę</string>
|
||||
<string name="set_password_to_export_desc">Duomenų bazė yra šifruota naudojant atsitiktinę slaptafrazę. Prieš eksportuodami duomenų bazę, pakeiskite slaptafrazę.</string>
|
||||
<string name="error_exporting_chat_database">Klaida eksportuojant pokalbio duomenų bazę</string>
|
||||
<string name="error_stopping_chat">Klaida sustabdant pokalbį</string>
|
||||
<string name="import_database_question">Importuoti pokalbio duomenų bazę\?</string>
|
||||
<string name="current_passphrase">Dabartinė slaptafrazė…</string>
|
||||
<string name="encrypted_with_random_passphrase">Duomenų bazė yra šifruota naudojant atsitiktinę slaptafrazę, kurią galite pakeisti.</string>
|
||||
<string name="encrypt_database_question">Šifruoti duomenų bazę\?</string>
|
||||
<string name="error_with_info">Klaida: %s</string>
|
||||
<string name="button_edit_group_profile">Taisyti grupės profilį</string>
|
||||
<string name="group_link">Grupės nuoroda</string>
|
||||
<string name="button_create_group_link">Sukurti nuorodą</string>
|
||||
<string name="info_row_database_id">Duomenų bazės ID</string>
|
||||
<string name="error_changing_role">Klaida keičiant vaidmenį</string>
|
||||
<string name="conn_level_desc_direct">tiesioginis</string>
|
||||
<string name="conn_level_desc_indirect">netiesioginis (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
|
||||
<string name="create_secret_group_title">Sukurti slaptą grupę</string>
|
||||
<string name="error_saving_group_profile">Klaida įrašant grupės profilį</string>
|
||||
<string name="dont_show_again">Daugiau neberodyti</string>
|
||||
<string name="theme_dark">Tamsus</string>
|
||||
<string name="reset_color">Atstatyti spalvas</string>
|
||||
<string name="save_color">Įrašyti spalvą</string>
|
||||
<string name="group_preferences">Grupės nuostatos</string>
|
||||
<string name="full_deletion">Ištrinti visiems</string>
|
||||
<string name="direct_messages">Tiesioginės žinutės</string>
|
||||
<string name="v4_4_disappearing_messages">Išnykstančios žinutės</string>
|
||||
<string name="timed_messages">Išnykstančios žinutės</string>
|
||||
<string name="v4_4_french_interface">Sąsaja prancūzų kalba</string>
|
||||
<string name="v4_3_improved_privacy_and_security">Patobulintas privatumas ir saugumas</string>
|
||||
<string name="num_contacts_selected">Pažymėta adresatų: %d</string>
|
||||
<string name="delete_chat_profile_question">Ištrinti pokalbio profilį\?</string>
|
||||
<string name="error_importing_database">Klaida importuojant pokalbio duomenų bazę</string>
|
||||
<string name="delete_messages_after">Ištrinti žinutes po</string>
|
||||
<string name="database_will_be_encrypted">Duomenų bazė bus šifruota.</string>
|
||||
<string name="alert_title_no_group">Grupė nerasta!</string>
|
||||
<string name="error_deleting_link_for_group">Klaida ištrinant grupės nuorodą</string>
|
||||
<string name="error_updating_link_for_group">Klaida atnaujinant grupės nuorodą</string>
|
||||
<string name="database_upgrade">Duomenų bazės naujinimas</string>
|
||||
<string name="incompatible_database_version">Nesuderinama duomenų bazės versija</string>
|
||||
<string name="group_member_status_group_deleted">grupė ištrinta</string>
|
||||
<string name="save_and_update_group_profile">Įrašyti ir atnaujinti grupės profilį</string>
|
||||
<string name="delete_messages">Ištrinti žinutes</string>
|
||||
<string name="error_changing_message_deletion">Klaida keičiant nustatymą</string>
|
||||
<string name="error_encrypting_database">Klaida šifruojant duomenų bazę</string>
|
||||
<string name="encrypted_database">Šifruota duomenų bazė</string>
|
||||
<string name="restore_database_alert_confirm">Atkurti</string>
|
||||
<string name="create_group_link">Sukurti grupės nuorodą</string>
|
||||
<string name="delete_link">Ištrinti nuorodą</string>
|
||||
<string name="delete_link_question">Ištrinti nuorodą\?</string>
|
||||
<string name="error_creating_link_for_group">Klaida kuriant grupės nuorodą</string>
|
||||
<string name="how_simplex_works">Kaip <xliff:g id="appName">SimpleX</xliff:g> veikia</string>
|
||||
<string name="database_encrypted">Duomenų bazė šifruota!</string>
|
||||
<string name="group_member_status_creator">kūrėjas</string>
|
||||
<string name="delete_group_question">Ištrinti grupę\?</string>
|
||||
<string name="user_hide">Slėpti</string>
|
||||
<string name="error_saving_file">Klaida įrašant failą</string>
|
||||
<string name="hide_dev_options">Slėpti:</string>
|
||||
<string name="database_error">Duomenų bazės klaida</string>
|
||||
<string name="file_with_path">Failas: %s</string>
|
||||
<string name="profile_password">Profilio slaptažodis</string>
|
||||
<string name="remove_member_confirmation">Šalinti</string>
|
||||
<string name="v4_3_improved_server_configuration">Patobulinta serverio konfigūracija</string>
|
||||
<string name="delete_chat_profile">Ištrinti pokalbio profilį</string>
|
||||
<string name="delete_profile">Ištrinti profilį</string>
|
||||
<string name="share_one_time_link">Sukurti vienkartinio pakvietimo nuorodą</string>
|
||||
<string name="delete_contact_menu_action">Ištrinti</string>
|
||||
<string name="save_and_notify_contact">Įrašyti ir pranešti adresatui</string>
|
||||
<string name="enable_automatic_deletion_question">Įjungti automatinį žinučių ištrynimą\?</string>
|
||||
<string name="error_deleting_database">Klaida ištrinant pokalbio duomenų bazę</string>
|
||||
<string name="error_starting_chat">Klaida pradedant pokalbį</string>
|
||||
<string name="hide_profile">Slėpti profilį</string>
|
||||
<string name="import_database_confirmation">Importuoti</string>
|
||||
<string name="save_and_notify_contacts">Įrašyti ir pranešti adresatams</string>
|
||||
<string name="callstate_starting">pradedama…</string>
|
||||
<string name="use_chat">Naudoti pokalbį</string>
|
||||
<string name="icon_descr_speaker_off">Išjungti garsiakalbį</string>
|
||||
<string name="icon_descr_speaker_on">Įjungti garsiakalbį</string>
|
||||
<string name="alert_title_skipped_messages">Praleistos žinutės</string>
|
||||
<string name="settings_section_title_settings">NUSTATYMAI</string>
|
||||
<string name="theme_system">Sistemos</string>
|
||||
<string name="unknown_message_format">nežinomas žinutės formatas</string>
|
||||
<string name="simplex_link_contact">SimpleX adresato adresas</string>
|
||||
<string name="simplex_link_group">SimpleX grupės nuoroda</string>
|
||||
<string name="simplex_link_invitation">SimpleX vienkartinis pakvietimas</string>
|
||||
<string name="simplex_link_mode">SimpleX nuorodos</string>
|
||||
<string name="settings_notification_preview_mode_title">Rodyti peržiūrą</string>
|
||||
<string name="auth_unlock">Atrakinti</string>
|
||||
<string name="share_verb">Bendrinti</string>
|
||||
<string name="tap_to_start_new_chat">Bakstelėkite, norėdami pradėti naują pokalbį</string>
|
||||
<string name="share_file">Bendrinti failą…</string>
|
||||
<string name="icon_descr_settings">Nustatymai</string>
|
||||
<string name="show_QR_code">Rodyti QR kodą</string>
|
||||
<string name="share_invitation_link">Bendrinti pakvietimo nuorodą</string>
|
||||
<string name="smp_servers">SMP serveriai</string>
|
||||
<string name="use_simplex_chat_servers__question">Naudoti <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serverius\?</string>
|
||||
<string name="network_disable_socks">Naudoti tiesioginį interneto ryšį\?</string>
|
||||
<string name="update_network_settings_confirmation">Atnaujinti</string>
|
||||
<string name="update_network_settings_question">Atnaujinti tinklo nustatymus\?</string>
|
||||
<string name="ntf_channel_messages">SimpleX Chat žinutės</string>
|
||||
<string name="ntf_channel_calls">SimpleX Chat skambučiai</string>
|
||||
<string name="notification_preview_mode_message_desc">Rodyti adresatą ir žinutę</string>
|
||||
<string name="notification_preview_mode_contact_desc">Rodyti tik adresatą</string>
|
||||
<string name="auth_stop_chat">Stabdyti pokalbį</string>
|
||||
<string name="share_message">Bendrinti žinutę…</string>
|
||||
<string name="use_camera_button">Naudoti kamerą</string>
|
||||
<string name="thank_you_for_installing_simplex">Dėkojame, kad įdiegėte <xliff:g id="appNameFull">SimpleX Chat</xliff:g>!</string>
|
||||
<string name="smp_servers_use_server">Naudoti serverį</string>
|
||||
<string name="network_socks_toggle">Naudoti SOCKS įgaliotąjį serverį (prievadas 9050)</string>
|
||||
<string name="network_enable_socks">Naudoti SOCKS įgaliotąjį serverį\?</string>
|
||||
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
|
||||
<string name="share_link">Bendrinti nuorodą</string>
|
||||
<string name="tap_to_activate_profile">Bakstelėkite, norėdami aktyvuoti profilį.</string>
|
||||
<string name="language_system">Sistemos</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Bluetooth palaikymas ir kiti patobulinimai.</string>
|
||||
<string name="group_invitation_tap_to_join">Bakstelėkite, norėdami prisijungti</string>
|
||||
<string name="show_call_on_lock_screen">Rodyti</string>
|
||||
<string name="stop_chat_confirmation">Stabdyti</string>
|
||||
<string name="upgrade_and_open_chat">Naujinti ir atverti pokalbį</string>
|
||||
<string name="switch_verb">Perjungti</string>
|
||||
<string name="icon_descr_received_msg_status_unread">neskaityta</string>
|
||||
<string name="show_dev_options">Rodyti:</string>
|
||||
<string name="stop_chat_question">Stabdyti pokalbį\?</string>
|
||||
<string name="update_database">Atnaujinti</string>
|
||||
<string name="unknown_error">Nežinoma klaida</string>
|
||||
<string name="add_contact_or_create_group">Pradėti naują pokalbį</string>
|
||||
<string name="callstate_waiting_for_answer">laukiama, kol bus atsiliepta…</string>
|
||||
<string name="callstate_waiting_for_confirmation">laukiama patvirtinimo…</string>
|
||||
<string name="icon_descr_video_off">Išjungti vaizdą</string>
|
||||
<string name="icon_descr_video_on">Įjungti vaizdą</string>
|
||||
<string name="your_privacy">Jūsų privatumas</string>
|
||||
<string name="settings_section_title_you">JŪS</string>
|
||||
<string name="wrong_passphrase_title">Neteisinga slaptafrazė!</string>
|
||||
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
|
||||
<string name="sender_you_pronoun">jūs</string>
|
||||
<string name="description_via_group_link">per grupės nuorodą</string>
|
||||
<string name="description_via_contact_address_link">per adresato adreso nuorodą</string>
|
||||
<string name="description_via_one_time_link">per vienkartinę nuorodą</string>
|
||||
<string name="simplex_link_connection">per <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
|
||||
<string name="simplex_link_mode_browser">Per naršyklę</string>
|
||||
<string name="waiting_for_file">Laukiama failo</string>
|
||||
<string name="voice_message_with_duration">Balso žinutė (<xliff:g id="duration">%1$s</xliff:g>)</string>
|
||||
<string name="voice_message_send_text">Balso žinutė…</string>
|
||||
<string name="view_security_code">Rodyti saugumo kodą</string>
|
||||
<string name="icon_descr_address"><xliff:g id="appName">SimpleX</xliff:g> adresas</string>
|
||||
<string name="your_SMP_servers">Jūsų SMP serveriai</string>
|
||||
<string name="your_ICE_servers">Jūsų ICE serveriai</string>
|
||||
<string name="users_add">Pridėti profilį</string>
|
||||
<string name="users_delete_all_chats_deleted">Visi pokalbiai ir žinutės bus ištrinti – to neįmanoma bus atšaukti!</string>
|
||||
<string name="welcome">Sveiki!</string>
|
||||
<string name="attach">Pridėti</string>
|
||||
<string name="observer_cant_send_message_title">Jūs negalite siųsti žinučių!</string>
|
||||
<string name="image_descr_simplex_logo"><xliff:g id="appName">SimpleX</xliff:g> logotipas</string>
|
||||
<string name="your_settings">Jūsų nustatymai</string>
|
||||
<string name="smp_servers_your_server">Jūsų serveris</string>
|
||||
<string name="smp_servers_your_server_address">Jūsų serverio adresas</string>
|
||||
<string name="smp_servers_add_to_another_device">Pridėti į kitą įrenginį</string>
|
||||
<string name="using_simplex_chat_servers">Naudojami <xliff:g id="appNameFull">SimpleX Chat</xliff:g> serveriai.</string>
|
||||
<string name="network_settings">Išplėstiniai tinklo nustatymai</string>
|
||||
<string name="your_current_profile">Jūsų dabartinis profilis</string>
|
||||
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Jūsų profilis yra saugomas jūsų įrenginyje ir bendrinamas tik su jūsų adresatais. <xliff:g id="appName">SimpleX</xliff:g> serveriai negali matyti jūsų profilio.</string>
|
||||
<string name="icon_descr_video_call">vaizdo skambutis</string>
|
||||
<string name="your_calls">Jūsų skambučiai</string>
|
||||
<string name="webrtc_ice_servers">WebRTC ICE serveriai</string>
|
||||
<string name="your_ice_servers">Jūsų ICE serveriai</string>
|
||||
<string name="snd_group_event_user_left">jūs išėjote</string>
|
||||
<string name="group_member_role_admin">administratorius</string>
|
||||
<string name="incognito_random_profile">Jūsų atsitiktinis profilis</string>
|
||||
<string name="chat_preferences_you_allow">Jūs leidžiate</string>
|
||||
<string name="your_preferences">Jūsų nuostatos</string>
|
||||
<string name="v4_3_voice_messages">Balso žinutės</string>
|
||||
<string name="voice_messages">Balso žinutės</string>
|
||||
<string name="chat_preferences_yes">taip</string>
|
||||
<string name="you_joined_this_group">Jūs prisijungėte prie šios grupės</string>
|
||||
<string name="database_downgrade_warning">Įspėjimas: galite prarasti tam tikrus duomenis!</string>
|
||||
<string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Jūs nustosite gauti žinutes iš šios grupės. Pokalbio istorija bus išsaugota.</string>
|
||||
<string name="group_info_member_you">jūs: <xliff:g id="group_info_you">%1$s</xliff:g></string>
|
||||
<string name="personal_welcome">Sveiki, <xliff:g>%1$s</xliff:g>!</string>
|
||||
<string name="your_chats">Jūsų pokalbiai</string>
|
||||
<string name="wrong_passphrase">Neteisinga duomenų bazės slaptafrazė</string>
|
||||
<string name="icon_descr_video_snd_complete">Vaizdo įrašas išsiųstas</string>
|
||||
<string name="voice_message">Balso žinutė</string>
|
||||
<string name="icon_descr_simplex_team"><xliff:g id="appName">SimpleX</xliff:g> komanda</string>
|
||||
<string name="whats_new">Kas naujo</string>
|
||||
</resources>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user