Compare commits

..

44 Commits

Author SHA1 Message Date
Evgeny Poberezkin
bcce6a18a1 Delete README.md 2023-04-30 09:17:02 +01:00
lastimeoceanisfall
3bce760e05 Create audio-video-calls.md (#2254) 2023-04-30 09:14:44 +01:00
lastimeoceanisfall
a7c60f0721 Create app-settings.md (#2253) 2023-04-30 09:13:57 +01:00
lastimeoceanisfall
94ad68155f Create WEBRTC.md (#2251) 2023-04-30 09:13:37 +01:00
lastimeoceanisfall
cb2b5ff97b Create TRANSLATIONS.md (#2250) 2023-04-30 09:13:12 +01:00
lastimeoceanisfall
313c40590f Create SQL.md (#2249) 2023-04-30 09:12:53 +01:00
lastimeoceanisfall
d806efc347 Create SIMPLEX.md (#2248) 2023-04-30 09:12:34 +01:00
lastimeoceanisfall
743db49215 Create SERVER.md (#2247) 2023-04-30 09:12:08 +01:00
lastimeoceanisfall
4fdffdb8aa Create README.md (#2246) 2023-04-30 09:11:46 +01:00
lastimeoceanisfall
1699aae906 Create CLI.md (#2242) 2023-04-30 09:10:25 +01:00
lastimeoceanisfall
82eba49b95 Create chat-profiles.md (#2255) 2023-04-30 09:08:48 +01:00
lastimeoceanisfall
218b1a8b7e Create making-connections.md (#2256) 2023-04-30 09:08:22 +01:00
lastimeoceanisfall
6c1bba55b4 Create managing-data.md (#2257) 2023-04-30 09:08:02 +01:00
lastimeoceanisfall
c9fc1547b0 Create privacy-security.md (#2258) 2023-04-30 09:07:44 +01:00
lastimeoceanisfall
8a3a8455d4 Create secret-groups.md (#2260) 2023-04-30 09:07:15 +01:00
lastimeoceanisfall
9c437f6204 Create README.md (#2259) 2023-04-30 09:06:47 +01:00
lastimeoceanisfall
ef698e58d4 Create send-messages.md (#2261) 2023-04-30 09:06:18 +01:00
lastimeoceanisfall
01c0ac3d13 Create CONTRIBUTING.md (#2241) 2023-04-30 09:03:57 +01:00
lastimeoceanisfall
ed7089902c Create README.md (#2272) 2023-04-30 09:02:29 +01:00
lastimeoceanisfall
1ddd4193a0 Create 20201022-simplex-chat.md (#2274) 2023-04-30 09:01:57 +01:00
lastimeoceanisfall
932875fd9d Create 20210512-simplex-chat-terminal-ui.md (#2275) 2023-04-30 09:01:35 +01:00
lastimeoceanisfall
1899c84ee2 Create 20210914-simplex-chat-v0.4-released.md (#2277) 2023-04-30 09:01:07 +01:00
lastimeoceanisfall
91e03a016f Create 20211208-simplex-chat-v0.5-released.md (#2279) 2023-04-30 09:00:48 +01:00
lastimeoceanisfall
a978c12ca3 Create 20220112-simplex-chat-v1-released.md (#2280) 2023-04-30 09:00:30 +01:00
lastimeoceanisfall
da1f2d9ed6 Create 20220214-simplex-chat-ios-public-beta.md (#2282) 2023-04-30 09:00:04 +01:00
lastimeoceanisfall
1ed6179782 Create 20220308-simplex-chat-mobile-apps.md (#2284) 2023-04-30 08:59:44 +01:00
lastimeoceanisfall
e15251e354 Create 20220404-simplex-chat-instant-notifications.md (#2285) 2023-04-30 08:58:55 +01:00
lastimeoceanisfall
c348dd765f Create 20220511-simplex-chat-v2-images-files.md (#2286) 2023-04-30 08:56:02 +01:00
lastimeoceanisfall
78a23e6b02 Create 20220524-simplex-chat-better-privacy.md (#2288) 2023-04-30 08:55:40 +01:00
lastimeoceanisfall
590499684d Create 20220604-simplex-chat-new-privacy-security-settings.md (#2289) 2023-04-30 08:55:16 +01:00
lastimeoceanisfall
59c50f8088 Create 20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md (#2291) 2023-04-30 08:54:59 +01:00
lastimeoceanisfall
ee4c759706 Create 20220723-simplex-chat-v3.1-tor-groups-efficiency.md (#2293) 2023-04-30 08:54:37 +01:00
lastimeoceanisfall
d5bd3a7d68 Create 20220808-simplex-chat-v3.1-chat-groups.md (#2294)
* Create 20220808-simplex-chat-v3.1-chat-groups.md

* Update 20220808-simplex-chat-v3.1-chat-groups.md
2023-04-30 08:54:21 +01:00
lastimeoceanisfall
7159195cb9 Create 20220901-simplex-chat-v3.2-incognito-mode.md (#2295) 2023-04-30 08:54:04 +01:00
lastimeoceanisfall
45d3855d84 Create 20220928-simplex-chat-v4-encrypted-database.md (#2296) 2023-04-30 08:53:48 +01:00
lastimeoceanisfall
650a6f0758 Create 20221108-simplex-chat-v4.2-security-audit-new-website.md (#2297) 2023-04-30 08:53:25 +01:00
lastimeoceanisfall
ae6ba0cfb5 Create 20221206-simplex-chat-v4.3-voice-messages.md (#2298) 2023-04-30 08:53:03 +01:00
lastimeoceanisfall
d9a2317f82 Create 20230103-simplex-chat-v4.4-disappearing-messages.md (#2299) 2023-04-30 08:52:47 +01:00
lastimeoceanisfall
3d8b521c0c Create 20230204-simplex-chat-v4-5-user-chat-profiles.md (#2300) 2023-04-30 08:52:28 +01:00
lastimeoceanisfall
1fd5bbbc4f Create 20230301-simplex-file-transfer-protocol.md (#2301) 2023-04-30 08:52:07 +01:00
lastimeoceanisfall
62299cbf0b Create 20230328-simplex-chat-v4-6-hidden-profiles.md (#2302) 2023-04-30 08:51:47 +01:00
lastimeoceanisfall
3eb969d3c5 Create 20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md (#2303) 2023-04-30 08:51:09 +01:00
lastimeoceanisfall
85dba0f36b Create README.md (#2304) 2023-04-30 08:50:39 +01:00
lastimeoceanisfall
cb6490ed59 Create ANDROID.md (#2240) 2023-04-30 08:50:18 +01:00
429 changed files with 11910 additions and 50607 deletions

View File

@@ -1,7 +1,7 @@
name: Bug
description: File a bug report/issue
title: "[Bug]: "
labels: ["bug", "triage"]
labels: ["type:bug", "type:triage"]
body:
- type: checkboxes
attributes:

View File

@@ -1,7 +1,7 @@
name: Feature
description: Suggest your feature
title: "[Feature]: "
labels: ["enhancement", "triage"]
labels: ["type:enhancement", "type:triage"]
body:
- type: checkboxes
attributes:

View File

@@ -1,7 +1,7 @@
name: Question
description: Ask your question
title: "[Q]: "
labels: ["question", "triage"]
labels: ["type:question", "type:triage"]
body:
- type: markdown
attributes:

View File

@@ -62,14 +62,6 @@ jobs:
cache_path: C:/cabal
asset_name: simplex-chat-windows-x86-64
steps:
- name: Configure pagefile (Windows)
if: matrix.os == 'windows-latest'
uses: al-cheb/configure-pagefile-action@v1.3
with:
minimum-size: 16GB
maximum-size: 16GB
disk-root: "C:"
- name: Clone project
uses: actions/checkout@v2
@@ -119,6 +111,12 @@ jobs:
cabal build --enable-tests
echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)"
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30
shell: bash
run: cabal test --test-show-details=direct
- name: Unix upload binary to release
if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest'
uses: svenstaro/upload-release-action@v2
@@ -128,12 +126,6 @@ jobs:
asset_name: ${{ matrix.asset_name }}
tag: ${{ github.ref }}
- name: Unix test
if: matrix.os != 'windows-latest'
timeout-minutes: 30
shell: bash
run: cabal test --test-show-details=direct
# Unix /
# / Windows

View File

@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
node-version: [16.x]
node-version: [12.x]
steps:
- uses: actions/checkout@v2

2
.gitignore vendored
View File

@@ -49,8 +49,8 @@ logs/
# for website
website/node_modules/
website/src/blog/
website/src/docs/
website/translations.json
website/src/_data/supported_languages.json
website/src/img/images/
website/src/images/
# Generated files

View File

@@ -68,7 +68,7 @@ You can join an English-speaking users group if you want to ask any questions: [
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:
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
@@ -101,10 +101,8 @@ Join our translators to help SimpleX grow!
|🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)||
|🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)|
|🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)||
|🇯🇵 ja|Japanese ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|||
|🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)||
|🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)|||
|🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)<br>-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)||
|🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)|||
|🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)<br><br>[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)<br>[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)<br>&nbsp;|<br><br>[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)||
@@ -114,7 +112,6 @@ Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https:/
We would love to have you join the development! You can help us with:
- [share the color theme](./docs/THEMES.md) you use in Android app!
- writing a tutorial or recipes about hosting servers, chat bot automations, etc.
- contributing to SimpleX Chat knowledge-base.
- developing features - please connect to us via chat so we can help you get started.
@@ -200,8 +197,6 @@ You can use SimpleX with your own servers and still communicate with people usin
Recent updates:
[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md).
[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.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).
@@ -250,15 +245,13 @@ See [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md) for the format of m
SimpleX Chat is a work in progress we are releasing improvements as they are ready. You have to decide if the current state is good enough for your usage scenario.
We compiled a [glossary of terms](./docs/GLOSSARY.md) used to describe communication systems to help understand some terms below and to help compare advantages and disadvantages of various communication systems.
What is already implemented:
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
2. [End-to-end encryption](./docs/GLOSSARY.md#end-to-end-encryption) in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
1. Instead of user profile identifiers used by all other platforms, even the most private ones, SimpleX uses pairwise per-queue identifiers (2 addresses for each unidirectional message queue, with an optional 3rd address for push notifications on iOS, 2 queues in each connection between the users). It makes observing the network graph on the application level more difficult, as for `n` users there can be up to `n * (n-1)` message queues.
2. End-to-end encryption in each message queue using [NaCl cryptobox](https://nacl.cr.yp.to/box.html). This is added to allow redundancy in the future (passing each message via several servers), to avoid having the same ciphertext in different queues (that would only be visible to the attacker if TLS is compromised). The encryption keys used for this encryption are not rotated, instead we are planning to rotate the queues. Curve25519 keys are used for key negotiation.
3. [Double ratchet](https://signal.org/docs/specifications/doubleratchet/) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with forward secrecy (each message is encrypted by its own ephemeral key), break-in recovery (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial key agreement, initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message.
4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well).
5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks.
5. Several levels of content padding to frustrate message size attacks.
6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed.
7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448.
8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key.
@@ -323,27 +316,25 @@ If you are considering developing with SimpleX platform please get in touch for
- ✅ Sending and receiving large files via [XFTP protocol](./blog/20230301-simplex-file-transfer-protocol.md).
- ✅ Video messages.
- ✅ App access passcode.
- Improved Android app UI design.
- ✅ Optional alternative access password.
- ✅ Message reactions
- ✅ Message editing history
- ✅ Reduced battery and traffic usage in large groups.
- 🏗 Desktop client.
- 🏗 Message delivery confirmation (with sender opt-in or opt-out per contact, TBC).
- SMP queue redundancy and rotation (manual is supported).
- 🏗 Improved Android app UI design.
- 🏗 SMP queue redundancy and rotation (manual is supported).
- 🏗 Reduced battery and traffic usage in large groups.
- Include optional message into connection request sent via contact address.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- Optional alternative access password.
- Local app files encryption.
- Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.).
- Large groups, communities and public channels.
- Feeds/broadcasts.
- Ephemeral/disappearing/OTR conversations with the existing contacts.
- 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.).
- Supporting the same profile on multiple devices.
- Desktop client.
- Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages:
- keep all your contacts and groups even if you lose the domain.
- the server doesn't have information about your contacts and groups.
- Hosting server for large groups, communities and public channels.
- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic).
- High capacity multi-node SMP relays.

View File

@@ -4,6 +4,7 @@ This readme is currently a stub and as such is in development.
Ultimately, this readme will act as a guide to contributing to the develop of the SimpleX android app.
## Gotchas
#### SHA Signature for verification for app links/deep links
@@ -22,15 +23,3 @@ To find your SHA certificate fingerprint perform the following steps.
More information is available [here](https://developer.android.com/training/app-links/verify-site-associations#manual-verification). If there is no response when running the `pm get-app-links` command, the intents in `AndroidManifest.xml` are likely misspecified. A verification attempt can be triggered using `adb shell pm verify-app-links --re-verify chat.simplex.app`.
Note that this is not an issue for the app store build of the app as this is signed with our app store credentials and thus there is a stable signature over users. Developers do not have general access to these credentials for development and testing.
## Adding icons
1. Find a [Material symbol](https://fonts.google.com/icons?icon.style=Rounded) in Rounded category.
2. Set weight to 400, grade to -25 and size to 48px.
3. Click on the icon, choose Android and download XML file.
4. Update the color to black (#FF000000) and the size to "24.dp", as in other icons.
For example, this is [add reaction icon](https://fonts.google.com/icons?selected=Material+Symbols+Rounded:add_reaction:FILL@0;wght@300;GRAD@-25;opsz@24&icon.style=Rounded).

View File

@@ -11,10 +11,8 @@ android {
applicationId "chat.simplex.app"
minSdk 26
targetSdk 32
// !!!
// skip version code after release to F-Droid, as it uses two version codes
versionCode 127
versionName "5.1.3"
versionCode 117
versionName "5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -86,10 +84,8 @@ android {
"es",
"fr",
"it",
"ja",
"nl",
"pl",
"pt-rBR",
"ru",
"zh-rCN"
)
@@ -162,9 +158,6 @@ dependencies {
// Video support
implementation "com.google.android.exoplayer:exoplayer:2.17.1"
// Wheel picker
implementation 'com.github.zj565061763:compose-wheel-picker:1.0.0-alpha10'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

View File

@@ -10,12 +10,14 @@ import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.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.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -30,12 +32,10 @@ import chat.simplex.app.views.SplashView
import chat.simplex.app.views.call.ActiveCallView
import chat.simplex.app.views.call.IncomingCallAlertView
import chat.simplex.app.views.chat.ChatView
import chat.simplex.app.views.chat.group.ProgressIndicator
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.helpers.DatabaseUtils.ksSelfDestructPassword
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.onboarding.*
@@ -61,7 +61,6 @@ class MainActivity: FragmentActivity() {
}
}
private val vm by viewModels<SimplexViewModel>()
private val destroyedAfterBackPress = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -84,17 +83,14 @@ class MainActivity: FragmentActivity() {
}
setContent {
SimpleXTheme {
Surface(color = MaterialTheme.colors.background) {
MainPage(
m,
userAuthorized,
laFailed,
destroyedAfterBackPress,
::runAuthenticate,
::setPerformLA,
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
)
}
MainPage(
m,
userAuthorized,
laFailed,
::runAuthenticate,
::setPerformLA,
showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown, this) }
)
}
}
SimplexApp.context.schedulePeriodicServiceRestartWorker()
@@ -112,12 +108,7 @@ class MainActivity: FragmentActivity() {
val enteredBackgroundVal = enteredBackground.value
val delay = vm.chatModel.controller.appPrefs.laLockDelay.get()
if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) {
if (userAuthorized.value != false) {
/** [runAuthenticate] will be called in [MainPage] if needed. Making like this prevents double showing of passcode on start */
setAuthState()
} else if (!vm.chatModel.activeCallViewIsVisible.value) {
runAuthenticate()
}
runAuthenticate()
}
}
@@ -151,7 +142,6 @@ class MainActivity: FragmentActivity() {
// When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch
clearAuthState()
laFailed.value = true
destroyedAfterBackPress.value = true
}
if (!onBackPressedDispatcher.hasEnabledCallbacks()) {
// Drop shared content
@@ -159,14 +149,13 @@ class MainActivity: FragmentActivity() {
}
}
private fun setAuthState() {
userAuthorized.value = !vm.chatModel.controller.appPrefs.performLA.get()
}
private fun runAuthenticate() {
val m = vm.chatModel
setAuthState()
if (userAuthorized.value == false) {
if (!m.controller.appPrefs.performLA.get()) {
userAuthorized.value = true
} else {
userAuthorized.value = false
ModalManager.shared.closeModals()
// To make Main thread free in order to allow to Compose to show blank view that hiding content underneath of it faster on slow devices
CoroutineScope(Dispatchers.Default).launch {
delay(50)
@@ -180,7 +169,6 @@ class MainActivity: FragmentActivity() {
generalGetString(R.string.auth_log_in_using_credential)
else
generalGetString(R.string.auth_unlock),
selfDestruct = true,
this@MainActivity,
completed = { laResult ->
when (laResult) {
@@ -250,7 +238,7 @@ class MainActivity: FragmentActivity() {
authenticate(
generalGetString(R.string.auth_enable_simplex_lock),
generalGetString(R.string.auth_confirm_credential),
activity = activity,
activity,
completed = { laResult ->
when (laResult) {
LAResult.Success -> {
@@ -291,8 +279,7 @@ class MainActivity: FragmentActivity() {
appPrefs.performLA.set(false)
laPasscodeNotSetAlert()
},
close = close
)
close)
}
}
}
@@ -317,7 +304,7 @@ class MainActivity: FragmentActivity() {
generalGetString(R.string.auth_confirm_credential)
else
"",
activity = activity,
activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
when (laResult) {
@@ -353,17 +340,14 @@ class MainActivity: FragmentActivity() {
generalGetString(R.string.auth_confirm_credential)
else
generalGetString(R.string.auth_disable_simplex_lock),
activity = activity,
activity,
completed = { laResult ->
val prefPerformLA = m.controller.appPrefs.performLA
val selfDestructPref = m.controller.appPrefs.selfDestruct
when (laResult) {
LAResult.Success -> {
m.performLA.value = false
prefPerformLA.set(false)
ksAppPassword.remove()
selfDestructPref.set(false)
ksSelfDestructPassword.remove()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
is LAResult.Error -> {
@@ -392,7 +376,6 @@ fun MainPage(
chatModel: ChatModel,
userAuthorized: MutableState<Boolean?>,
laFailed: MutableState<Boolean>,
destroyedAfterBackPress: MutableState<Boolean>,
runAuthenticate: () -> Unit,
setPerformLA: (Boolean, FragmentActivity) -> Unit,
showLANotice: () -> Unit
@@ -429,38 +412,43 @@ fun MainPage(
}
@Composable
fun AuthView() {
Surface(color = MaterialTheme.colors.background) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_unlock),
icon = painterResource(R.drawable.ic_lock),
click = {
laFailed.value = false
runAuthenticate()
}
)
}
fun authView() {
Box(
Modifier.fillMaxSize().background(MaterialTheme.colors.background),
contentAlignment = Alignment.Center
) {
SimpleButton(
stringResource(R.string.auth_unlock),
icon = painterResource(R.drawable.ic_lock),
click = {
laFailed.value = false
runAuthenticate()
}
)
}
}
Box {
val onboarding = chatModel.onboardingStage.value
val userCreated = chatModel.userCreated.value
var showInitializationView by remember { mutableStateOf(false) }
when {
chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView()
showChatDatabaseError -> {
chatModel.chatDbStatus.value?.let {
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
}
}
onboarding == null || userCreated == null -> SplashView()
userAuthorized.value != true -> {
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
authView()
} else {
SplashView()
}
}
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
Box {
if (chatModel.showCallView.value) ActiveCallView(chatModel)
else {
showAdvertiseLAAlert = true
BoxWithConstraints {
var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) }
@@ -506,41 +494,16 @@ fun MainPage(
}
}
}
}
}
onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true)
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
onboarding == OnboardingStage.Step3_SetNotificationsMode -> SetNotificationsMode(chatModel)
}
ModalManager.shared.showInView()
val unauthorized = remember { derivedStateOf { userAuthorized.value != true } }
if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) {
LaunchedEffect(Unit) {
// With these constrains when user presses back button while on ChatList, activity destroys and shows auth request
// while the screen moves to a launcher. Detect it and prevent showing the auth
if (!(destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) {
runAuthenticate()
}
}
if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) {
AuthView()
} else {
SplashView()
}
} else if (chatModel.showCallView.value) {
ActiveCallView(chatModel)
}
ModalManager.shared.showPasscodeInView()
val invitation = chatModel.activeCallInvitation.value
if (invitation != null) IncomingCallAlertView(invitation, chatModel)
AlertManager.shared.showInView()
LaunchedEffect(Unit) {
delay(1000)
if (chatModel.chatDbStatus.value == null) {
showInitializationView = true
}
}
}
DisposableEffectOnRotate {
@@ -552,22 +515,6 @@ fun MainPage(
}
}
@Composable
private fun InitializationView() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
CircularProgressIndicator(
Modifier
.padding(bottom = DEFAULT_PADDING)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 2.5.dp
)
Text(stringResource(R.string.opening_database))
}
}
}
fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
val userId = getUserIdFromIntent(intent)
when (intent?.action) {
@@ -582,7 +529,7 @@ fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) {
}
val cInfo = chatModel.getChat(chatId)?.chatInfo
chatModel.clearOverlays.value = true
if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel)
if (cInfo != null) openChat(cInfo, chatModel)
}
}
}

View File

@@ -36,11 +36,13 @@ external fun chatParseServer(str: String): String
external fun chatPasswordHash(pwd: String, salt: String): String
class SimplexApp: Application(), LifecycleEventObserver {
lateinit var chatController: ChatController
var isAppOnForeground: Boolean = false
val defaultLocale: Locale = Locale.getDefault()
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = 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 confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp
@@ -51,7 +53,11 @@ class SimplexApp: Application(), LifecycleEventObserver {
val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long
} else null
chatController.ctrl = ctrl
if (::chatController.isInitialized) {
chatController.ctrl = ctrl
} else {
chatController = ChatController(ctrl, ntfManager, applicationContext, appPreferences)
}
chatModel.chatDbEncrypted.value = dbKey != ""
chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) {
@@ -59,22 +65,12 @@ class SimplexApp: Application(), LifecycleEventObserver {
} else if (startChat) {
// If we migrated successfully means previous re-encryption process on database level finished successfully too
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
val user = chatController.apiGetActiveUser()
if (user == null) {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
chatModel.currentUser.value = null
chatModel.users.clear()
} else {
val savedOnboardingStage = appPreferences.onboardingStage.get()
chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
OnboardingStage.Step3_CreateSimpleXAddress
withApi {
val user = chatController.apiGetActiveUser()
if (user == null) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
savedOnboardingStage
}
chatController.startChat(user)
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
chatController.startChat(user)
chatController.showBackgroundServiceNoticeIfNeeded()
if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name)
SimplexService.start(applicationContext)
@@ -94,20 +90,13 @@ class SimplexApp: Application(), LifecycleEventObserver {
AppPreferences(applicationContext)
}
val chatController: ChatController by lazy {
ChatController(0L, ntfManager, applicationContext, appPreferences)
}
override fun onCreate() {
super.onCreate()
context = this
initChatController()
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
context.getDir("temp", MODE_PRIVATE).deleteRecursively()
withBGApi {
initChatController()
runMigrations()
}
ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp)
runMigrations()
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {

View File

@@ -9,9 +9,9 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import chat.simplex.app.model.ChatController
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
// based on:
// https://robertohuertas.com/2019/06/29/android_foreground_services/
@@ -86,7 +86,6 @@ class SimplexService: Service() {
isStartingService = true
withApi {
val chatController = (application as SimplexApp).chatController
waitDbMigrationEnds(chatController)
try {
Log.w(TAG, "Starting foreground service")
val chatDbStatus = chatController.chatModel.chatDbStatus.value
@@ -325,18 +324,6 @@ class SimplexService: Service() {
notificationManager.cancel(PASSPHRASE_NOTIFICATION_ID)
}
/*
* When the app starts the database is in migration process. It can take from seconds to tens of seconds.
* It happens in background thread, so other places that relies on database should wait til the end of migration
* */
suspend fun waitDbMigrationEnds(chatController: ChatController) {
var maxWaitTime = 50_000
while (chatController.chatModel.chatDbStatus.value == null && maxWaitTime > 0) {
delay(50)
maxWaitTime -= 50
}
}
private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
}
}

View File

@@ -4,6 +4,8 @@ import android.net.Uri
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration
@@ -17,16 +19,12 @@ import chat.simplex.app.views.usersettings.NotificationPreviewMode
import chat.simplex.app.views.usersettings.NotificationsMode
import kotlinx.coroutines.*
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
import java.io.File
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.random.Random
import kotlin.time.*
@@ -43,6 +41,7 @@ class ChatModel(val controller: ChatController) {
val chatDbChanged = mutableStateOf<Boolean>(false)
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val chatDbDeleted = mutableStateOf(false)
val chats = mutableStateListOf<Chat>()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
@@ -76,7 +75,6 @@ class ChatModel(val controller: ChatController) {
val callInvitations = mutableStateMapOf<String, RcvCallInvitation>()
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
val activeCall = mutableStateOf<Call?>(null)
val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
val callCommand = mutableStateOf<WCallCommand?>(null)
val showCallView = mutableStateOf(false)
val switchingCall = mutableStateOf(false)
@@ -240,17 +238,6 @@ class ChatModel(val controller: ChatController) {
}
}
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
if (chatId.value == cInfo.id) {
withContext(Dispatchers.Main) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) {
chatItems[itemIndex] = cItem
}
}
}
}
fun removeChatItem(cInfo: ChatInfo, cItem: ChatItem) {
if (cItem.isRcvNew) {
decreaseCounterInChat(cInfo.id)
@@ -474,8 +461,6 @@ data class User(
val showNotifications: Boolean = activeUser || showNtfs
val addressShared: Boolean = profile.contactLink != null
companion object {
val sampleData = User(
userId = 1,
@@ -742,7 +727,6 @@ data class Contact(
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser
ChatFeature.FullDelete -> mergedPreferences.fullDelete.enabled.forUser
ChatFeature.Reactions -> mergedPreferences.reactions.enabled.forUser
ChatFeature.Voice -> mergedPreferences.voice.enabled.forUser
ChatFeature.Calls -> mergedPreferences.calls.enabled.forUser
}
@@ -750,7 +734,6 @@ data class Contact(
override val displayName get() = localAlias.ifEmpty { profile.displayName }
override val fullName get() = profile.fullName
override val image get() = profile.image
val contactLink: String? = profile.contactLink
override val localAlias get() = profile.localAlias
val verified get() = activeConn.connectionCode != null
@@ -764,14 +747,12 @@ data class Contact(
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO
ChatFeature.FullDelete -> mergedPreferences.fullDelete.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.contactPreference.allow != FeatureAllowed.NO
ChatFeature.Reactions -> mergedPreferences.reactions.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.Reactions -> mergedPreferences.reactions.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Voice -> mergedPreferences.voice.userPreference.pref.allow != FeatureAllowed.NO
ChatFeature.Calls -> mergedPreferences.calls.userPreference.pref.allow != FeatureAllowed.NO
}
@@ -833,7 +814,6 @@ data class Profile(
override val fullName: String,
override val image: String? = null,
override val localAlias : String = "",
val contactLink: String? = null,
val preferences: ChatPreferences? = null
): NamedChat {
val profileViewName: String
@@ -841,7 +821,7 @@ data class Profile(
return if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)"
}
fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, image, localAlias, contactLink, preferences)
fun toLocalProfile(profileId: Long): LocalProfile = LocalProfile(profileId, displayName, fullName, image, localAlias, preferences)
companion object {
val sampleData = Profile(
@@ -852,18 +832,17 @@ data class Profile(
}
@Serializable
data class LocalProfile(
class LocalProfile(
val profileId: Long,
override val displayName: String,
override val fullName: String,
override val image: String? = null,
override val localAlias: String,
val contactLink: String? = null,
val preferences: ChatPreferences? = null
): NamedChat {
val profileViewName: String = localAlias.ifEmpty { if (fullName == "" || displayName == fullName) displayName else "$displayName ($fullName)" }
fun toProfile(): Profile = Profile(displayName, fullName, image, localAlias, contactLink, preferences)
fun toProfile(): Profile = Profile(displayName, fullName, image, localAlias, preferences)
companion object {
val sampleData = LocalProfile(
@@ -904,7 +883,6 @@ data class GroupInfo (
override fun featureEnabled(feature: ChatFeature) = when (feature) {
ChatFeature.TimedMessages -> fullGroupPreferences.timedMessages.on
ChatFeature.FullDelete -> fullGroupPreferences.fullDelete.on
ChatFeature.Reactions -> fullGroupPreferences.reactions.on
ChatFeature.Voice -> fullGroupPreferences.voice.on
ChatFeature.Calls -> false
}
@@ -974,7 +952,6 @@ data class GroupMember (
val displayName: String get() = memberProfile.localAlias.ifEmpty { memberProfile.displayName }
val fullName: String get() = memberProfile.fullName
val image: String? get() = memberProfile.image
val contactLink: String? = memberProfile.contactLink
val verified get() = activeConn?.connectionCode != null
val chatViewName: String
@@ -1274,20 +1251,6 @@ class AChatItem (
val chatItem: ChatItem
)
@Serializable
class ACIReaction(
val chatInfo: ChatInfo,
val chatReaction: CIReaction
)
@Serializable
class CIReaction(
val chatDir: CIDirection,
val chatItem: ChatItem,
val sentAt: Instant,
val reaction: MsgReaction
)
@Serializable @Stable
data class ChatItem (
val chatDir: CIDirection,
@@ -1295,7 +1258,6 @@ data class ChatItem (
val content: CIContent,
val formattedText: List<FormattedText>? = null,
val quotedItem: CIQuote? = null,
val reactions: List<CIReactionCount>,
val file: CIFile? = null
) {
val id: Long get() = meta.itemId
@@ -1312,11 +1274,6 @@ data class ChatItem (
val isRcvNew: Boolean get() = meta.isRcvNew
val allowAddReaction: Boolean get() =
meta.itemDeleted == null && !isLiveDummy && (reactions.count { it.userReacted } < 3)
private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID
val memberDisplayName: String? get() =
if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName
else null
@@ -1406,7 +1363,6 @@ data class ChatItem (
meta = CIMeta.getSample(id, ts, text, status, itemDeleted, itemEdited, itemTimed, editable),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)),
quotedItem = quotedItem,
reactions = listOf(),
file = file
)
@@ -1422,7 +1378,6 @@ data class ChatItem (
meta = CIMeta.getSample(id, Clock.System.now(), text, CIStatus.RcvRead()),
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile(text)),
quotedItem = null,
reactions = listOf(),
file = CIFile.getSample(fileName = fileName, fileSize = fileSize, fileStatus = fileStatus)
)
@@ -1438,7 +1393,6 @@ data class ChatItem (
meta = CIMeta.getSample(id, ts, text, status),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null,
reactions = listOf(),
file = null
)
@@ -1448,7 +1402,6 @@ data class ChatItem (
meta = CIMeta.getSample(1, Clock.System.now(), "received invitation to join group team as admin", CIStatus.RcvRead()),
content = CIContent.RcvGroupInvitation(groupInvitation = CIGroupInvitation.getSample(status = status), memberRole = GroupMemberRole.Admin),
quotedItem = null,
reactions = listOf(),
file = null
)
@@ -1458,7 +1411,6 @@ data class ChatItem (
meta = CIMeta.getSample(1, Clock.System.now(), "group event text", CIStatus.RcvRead()),
content = CIContent.RcvGroupEventContent(rcvGroupEvent = RcvGroupEvent.MemberAdded(groupMemberId = 1, profile = Profile.sampleData)),
quotedItem = null,
reactions = listOf(),
file = null
)
@@ -1469,7 +1421,6 @@ data class ChatItem (
meta = CIMeta.getSample(1, Clock.System.now(), content.text, CIStatus.RcvRead()),
content = content,
quotedItem = null,
reactions = listOf(),
file = null
)
}
@@ -1495,7 +1446,6 @@ data class ChatItem (
),
content = CIContent.RcvDeleted(deleteMode = CIDeleteMode.cidmBroadcast),
quotedItem = null,
reactions = listOf(),
file = null
)
@@ -1516,7 +1466,6 @@ data class ChatItem (
),
content = CIContent.SndMsgContent(MsgContent.MCText("")),
quotedItem = null,
reactions = listOf(),
file = null
)
@@ -1526,7 +1475,6 @@ data class ChatItem (
meta = meta ?: CIMeta.invalidJSON(),
content = CIContent.InvalidJSON(json),
quotedItem = null,
reactions = listOf(),
file = null
)
}
@@ -1624,31 +1572,10 @@ fun getTimestampText(t: Instant): String {
val tz = TimeZone.currentSystemDefault()
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
val time: LocalDateTime = t.toLocalDateTime(tz)
val period = now.date.minus(time.date)
val recent = now.date == time.date ||
(period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 )
val dateFormatter =
if (recent) {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
} else {
DateTimeFormatter.ofPattern(
when (Locale.getDefault().country) {
"US" -> "M/dd"
"DE" -> "dd.MM"
"RU" -> "dd.MM"
else -> "dd/MM"
}
)
// DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
}
return time.toJavaLocalDateTime().format(dateFormatter)
}
fun localTimestamp(t: Instant): String {
val tz = TimeZone.currentSystemDefault()
val ts: LocalDateTime = t.toLocalDateTime(tz)
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
return ts.toJavaLocalDateTime().format(dateFormatter)
(now.date.minus(time.date).days == 1 && now.hour < 12 && time.hour >= 18 )
return if (recent) String.format("%02d:%02d", time.hour, time.minute)
else String.format("%02d/%02d", time.dayOfMonth, time.monthNumber)
}
@Serializable
@@ -1663,8 +1590,8 @@ sealed class CIStatus {
@Serializable
sealed class CIDeleted {
@Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted()
@Serializable @SerialName("moderated") class Moderated(val deletedTs: Instant?, val byGroupMember: GroupMember): CIDeleted()
@Serializable @SerialName("deleted") class Deleted: CIDeleted()
@Serializable @SerialName("moderated") class Moderated(val byGroupMember: GroupMember): CIDeleted()
}
@Serializable
@@ -1738,18 +1665,18 @@ sealed class CIContent: ItemContent {
companion object {
fun featureText(feature: Feature, enabled: String, param: Int?): String =
if (feature.hasParam) {
"${feature.text}: ${timeText(param)}"
"${feature.text}: ${TimedMessagesPreference.ttlText(param)}"
} else {
"${feature.text}: $enabled"
}
fun preferenceText(feature: Feature, allowed: FeatureAllowed, param: Int?): String = when {
allowed != FeatureAllowed.NO && feature.hasParam && param != null ->
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, timeText(param))
String.format(generalGetString(R.string.feature_offered_item_with_param), feature.text, TimedMessagesPreference.ttlText(param))
allowed != FeatureAllowed.NO ->
String.format(generalGetString(R.string.feature_offered_item), feature.text, timeText(param))
String.format(generalGetString(R.string.feature_offered_item), feature.text, TimedMessagesPreference.ttlText(param))
else ->
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, timeText(param))
String.format(generalGetString(R.string.feature_cancelled_item), feature.text, TimedMessagesPreference.ttlText(param))
}
}
}
@@ -1796,75 +1723,6 @@ class CIQuote (
}
}
@Serializable
class CIReactionCount(val reaction: MsgReaction, val userReacted: Boolean, val totalReacted: Int)
@Serializable(with = MsgReactionSerializer::class)
sealed class MsgReaction {
@Serializable(with = MsgReactionSerializer::class) class Emoji(val emoji: MREmojiChar): MsgReaction()
@Serializable(with = MsgReactionSerializer::class) class Unknown(val type: String? = null, val json: JsonElement): MsgReaction()
val text: String get() = when (this) {
is Emoji -> when (emoji) {
MREmojiChar.Heart -> "❤️"
else -> emoji.value
}
is Unknown -> ""
}
companion object {
val values: List<MsgReaction> get() = MREmojiChar.values().map(::Emoji)
}
}
object MsgReactionSerializer : KSerializer<MsgReaction> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgReaction", PolymorphicKind.SEALED) {
element("Emoji", buildClassSerialDescriptor("Emoji") {
element<String>("emoji")
})
element("Unknown", buildClassSerialDescriptor("Unknown"))
}
override fun deserialize(decoder: Decoder): MsgReaction {
require(decoder is JsonDecoder)
val json = decoder.decodeJsonElement()
return if (json is JsonObject && "type" in json) {
when(val t = json["type"]?.jsonPrimitive?.content ?: "") {
"emoji" -> {
val emoji = Json.decodeFromString<MREmojiChar>(json["emoji"].toString())
if (emoji == null) MsgReaction.Unknown(t, json) else MsgReaction.Emoji(emoji)
}
else -> MsgReaction.Unknown(t, json)
}
} else {
MsgReaction.Unknown("", json)
}
}
override fun serialize(encoder: Encoder, value: MsgReaction) {
require(encoder is JsonEncoder)
val json = when (value) {
is MsgReaction.Emoji ->
buildJsonObject {
put("type", "emoji")
put("emoji", json.encodeToJsonElement(value.emoji))
}
is MsgReaction.Unknown -> value.json
}
encoder.encodeJsonElement(json)
}
}
@Serializable
enum class MREmojiChar(val value: String) {
@SerialName("👍") ThumbsUp("👍"),
@SerialName("👎") ThumbsDown("👎"),
@SerialName("😀") Smile("😀"),
@SerialName("😢") Sad("😢"),
@SerialName("") Heart(""),
@SerialName("🚀") Launch("🚀");
}
@Serializable
class CIFile(
val fileId: Long,
@@ -2385,17 +2243,3 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> {
}
}
}
@Serializable
class ChatItemInfo(
val itemVersions: List<ChatItemVersion>,
)
@Serializable
data class ChatItemVersion(
val chatItemVersionId: Long,
val msgContent: MsgContent,
val formattedText: List<FormattedText>?,
val itemVersionTs: Instant,
val createdAt: Instant,
)

View File

@@ -231,10 +231,6 @@ class NtfManager(val context: Context, private val appPreferences: AppPreference
manager.cancel(CallNotificationId)
}
fun cancelAllNotifications() {
manager.cancelAll()
}
fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() }
private fun hideSecrets(cItem: ChatItem) : String {

View File

@@ -141,19 +141,14 @@ class AppPreferences(val context: Context) {
val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true)
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } }
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
val encryptedAppPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE, null)
val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null)
val encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE, null)
val initializationVectorSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE, null)
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false)
val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false)
val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null)
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name)
@@ -165,7 +160,6 @@ class AppPreferences(val context: Context) {
val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null)
val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0)
val customDisappearingMessageTime = mkIntPreference(SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME, 300)
private fun mkIntPreference(prefName: String, default: Int) =
SharedPreference(
@@ -252,7 +246,6 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName"
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy"
@@ -279,18 +272,13 @@ class AppPreferences(val context: Context) {
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
private const val SHARED_PREFS_ENCRYPTED_APP_PASSPHRASE = "EncryptedAppPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE = "InitializationVectorAppPassphrase"
private const val SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE = "EncryptedSelfDestructPassphrase"
private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase"
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades"
private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct"
private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName"
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme"
private const val SHARED_PREFS_THEMES = "Themes"
private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion"
private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode"
private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime"
}
}
@@ -338,6 +326,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
chatModel.userCreated.value = true
apiSetIncognito(chatModel.incognito.value)
getUserChatData()
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now())
chatModel.chatRunning.value = true
startReceiver()
@@ -444,8 +433,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiCreateActiveUser(p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false): User? {
val r = sendCmd(CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp))
suspend fun apiCreateActiveUser(p: Profile): User? {
val r = sendCmd(CC.CreateActiveUser(p))
if (r is CR.ActiveUser) return r.user
else if (
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName ||
@@ -545,9 +534,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
throw Error("failed to export archive: ${r.responseType} ${r.details}")
}
suspend fun apiImportArchive(config: ArchiveConfig): List<ArchiveError> {
suspend fun apiImportArchive(config: ArchiveConfig) {
val r = sendCmd(CC.ApiImportArchive(config))
if (r is CR.ArchiveImported) return r.archiveErrors
if (r is CR.CmdOk) return
throw Error("failed to import archive: ${r.responseType} ${r.details}")
}
@@ -581,8 +570,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl)
suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false): AChatItem? {
val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live)
val r = sendCmd(cmd)
return when (r) {
is CR.NewChatItem -> r.chatItem
@@ -595,16 +584,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiGetChatItemInfo(type: ChatType, id: Long, itemId: Long): ChatItemInfo? {
return when (val r = sendCmd(CC.ApiGetChatItemInfo(type, id, itemId))) {
is CR.ApiChatItemInfo -> r.chatItemInfo
else -> {
apiErrorAlert("apiGetChatItemInfo", generalGetString(R.string.error_loading_details), r)
null
}
}
}
suspend fun apiUpdateChatItem(type: ChatType, id: Long, itemId: Long, mc: MsgContent, live: Boolean = false): AChatItem? {
val r = sendCmd(CC.ApiUpdateChatItem(type, id, itemId, mc, live))
if (r is CR.ChatItemUpdated) return r.chatItem
@@ -612,13 +591,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiChatItemReaction(type: ChatType, id: Long, itemId: Long, add: Boolean, reaction: MsgReaction): ChatItem? {
val r = sendCmd(CC.ApiChatItemReaction(type, id, itemId, add, reaction))
if (r is CR.ChatItemReaction) return r.reaction.chatReaction.chatItem
Log.e(TAG, "apiUpdateChatItem bad response: ${r.responseType} ${r.details}")
return null
}
suspend fun apiDeleteChatItem(type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? {
val r = sendCmd(CC.ApiDeleteChatItem(type, id, itemId, mode))
if (r is CR.ChatItemDeleted) return r
@@ -883,15 +855,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
return null
}
suspend fun apiSetProfileAddress(on: Boolean): User? {
val userId = try { currentUserId("apiSetProfileAddress") } catch (e: Exception) { return null }
return when (val r = sendCmd(CC.ApiSetProfileAddress(userId, on))) {
is CR.UserProfileNoChange -> null
is CR.UserProfileUpdated -> r.user
else -> throw Exception("failed to set profile address: ${r.responseType} ${r.details}")
}
}
suspend fun apiSetContactPrefs(contactId: Long, prefs: ChatPreferences): Contact? {
val r = sendCmd(CC.ApiSetContactPrefs(contactId, prefs))
if (r is CR.ContactPrefsUpdated) return r.toContact
@@ -927,12 +890,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
}
suspend fun apiDeleteUserAddress(): User? {
val userId = try { currentUserId("apiDeleteUserAddress") } catch (e: Exception) { return null }
suspend fun apiDeleteUserAddress(): Boolean {
val userId = kotlin.runCatching { currentUserId("apiDeleteUserAddress") }.getOrElse { return false }
val r = sendCmd(CC.ApiDeleteMyAddress(userId))
if (r is CR.UserContactLinkDeleted) return r.user
if (r is CR.UserContactLinkDeleted) return true
Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}")
return null
return false
}
private suspend fun apiGetUserAddress(): UserContactLinkRec? {
@@ -1374,12 +1337,12 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
val file = cItem.file
val mc = cItem.content.msgContent
if (file != null &&
appPrefs.privacyAcceptImages.get() &&
((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV)
|| (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) {
withApi { receiveFile(r.user, file.fileId) }
if (file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) {
val acceptImages = appPrefs.privacyAcceptImages.get()
if ((mc is MsgContent.MCImage && acceptImages)
|| (mc is MsgContent.MCVoice && ((file.fileSize > MAX_VOICE_SIZE_FOR_SENDING && acceptImages) || cInfo is ChatInfo.Group))) {
withApi { receiveFile(r.user, file.fileId) } // TODO check inlineFileMode != IFMSent
}
}
if (cItem.showNotification && (!SimplexApp.context.isAppOnForeground || chatModel.chatId.value != cInfo.id)) {
ntfManager.notifyMessageReceived(r.user, cInfo, cItem)
@@ -1397,11 +1360,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
}
is CR.ChatItemUpdated ->
chatItemSimpleUpdate(r.user, r.chatItem)
is CR.ChatItemReaction -> {
if (active(r.user)) {
chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
}
is CR.ChatItemDeleted -> {
if (!active(r.user)) {
if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) {
@@ -1480,14 +1438,10 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
if (active(r.user)) {
chatModel.upsertGroupMember(r.groupInfo, r.member)
}
is CR.ConnectedToGroupMember -> {
is CR.ConnectedToGroupMember ->
if (active(r.user)) {
chatModel.upsertGroupMember(r.groupInfo, r.member)
}
if (r.memberContact != null) {
chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected())
}
}
is CR.GroupUpdated ->
if (active(r.user)) {
chatModel.updateGroup(r.toGroup)
@@ -1849,9 +1803,6 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
)
}
/**
* [AppPreferences.networkProxyHostPort] is not changed here, use appPrefs to set it
* */
fun setNetCfg(cfg: NetCfg) {
appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy)
appPrefs.networkHostMode.set(cfg.hostMode.name)
@@ -1889,7 +1840,7 @@ class SharedPreference<T>(val get: () -> T, set: (T) -> Unit) {
sealed class CC {
class Console(val cmd: String): CC()
class ShowActiveUser: CC()
class CreateActiveUser(val profile: Profile?, val sameServers: Boolean, val pastTimestamp: Boolean): CC()
class CreateActiveUser(val profile: Profile): CC()
class ListUsers: CC()
class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC()
class ApiHideUser(val userId: Long, val viewPwd: String): CC()
@@ -1909,12 +1860,10 @@ sealed class CC {
class ApiStorageEncryption(val config: DBEncryptionConfig): CC()
class ApiGetChats(val userId: Long): CC()
class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC()
class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC()
class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean): CC()
class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC()
class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC()
class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC()
class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC()
class ApiNewGroup(val userId: Long, val groupProfile: GroupProfile): CC()
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
@@ -1956,7 +1905,6 @@ sealed class CC {
class ApiCreateMyAddress(val userId: Long): CC()
class ApiDeleteMyAddress(val userId: Long): CC()
class ApiShowMyAddress(val userId: Long): CC()
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
class ApiAddressAutoAccept(val userId: Long, val autoAccept: AutoAccept?): CC()
class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC()
class ApiRejectCall(val contact: Contact): CC()
@@ -1976,10 +1924,7 @@ sealed class CC {
val cmdString: String get() = when (this) {
is Console -> cmd
is ShowActiveUser -> "/u"
is CreateActiveUser -> {
val user = NewUser(profile, sameServers = sameServers, pastTimestamp = pastTimestamp)
"/_create user ${json.encodeToString(user)}"
}
is CreateActiveUser -> "/create user ${profile.displayName} ${profile.fullName}"
is ListUsers -> "/users"
is ApiSetActiveUser -> "/_user $userId${maybePwd(viewPwd)}"
is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}"
@@ -1999,15 +1944,10 @@ sealed class CC {
is ApiStorageEncryption -> "/_db encryption ${json.encodeToString(config)}"
is ApiGetChats -> "/_get chats $userId pcc=on"
is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search")
is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId"
is ApiSendMessage -> {
val ttlStr = if (ttl != null) "$ttl" else "default"
"/_send ${chatRef(type, id)} live=${onOff(live)} ttl=${ttlStr} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
}
is ApiSendMessage -> "/_send ${chatRef(type, id)} live=${onOff(live)} json ${json.encodeToString(ComposedMessage(file, quotedItemId, mc))}"
is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}"
is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}"
is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId"
is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}"
is ApiNewGroup -> "/_group $userId ${json.encodeToString(groupProfile)}"
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
@@ -2049,7 +1989,6 @@ sealed class CC {
is ApiCreateMyAddress -> "/_address $userId"
is ApiDeleteMyAddress -> "/_delete_address $userId"
is ApiShowMyAddress -> "/_show_address $userId"
is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}"
is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}"
is ApiAcceptContact -> "/_accept $contactReqId"
is ApiRejectContact -> "/_reject $contactReqId"
@@ -2090,12 +2029,10 @@ sealed class CC {
is ApiStorageEncryption -> "apiStorageEncryption"
is ApiGetChats -> "apiGetChats"
is ApiGetChat -> "apiGetChat"
is ApiGetChatItemInfo -> "apiGetChatItemInfo"
is ApiSendMessage -> "apiSendMessage"
is ApiUpdateChatItem -> "apiUpdateChatItem"
is ApiDeleteChatItem -> "apiDeleteChatItem"
is ApiDeleteMemberChatItem -> "apiDeleteMemberChatItem"
is ApiChatItemReaction -> "apiChatItemReaction"
is ApiNewGroup -> "apiNewGroup"
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
@@ -2137,7 +2074,6 @@ sealed class CC {
is ApiCreateMyAddress -> "apiCreateMyAddress"
is ApiDeleteMyAddress -> "apiDeleteMyAddress"
is ApiShowMyAddress -> "apiShowMyAddress"
is ApiSetProfileAddress -> "apiSetProfileAddress"
is ApiAddressAutoAccept -> "apiAddressAutoAccept"
is ApiAcceptContact -> "apiAcceptContact"
is ApiRejectContact -> "apiRejectContact"
@@ -2192,13 +2128,6 @@ sealed class CC {
}
}
@Serializable
data class NewUser(
val profile: Profile?,
val sameServers: Boolean,
val pastTimestamp: Boolean
)
sealed class ChatPagination {
class Last(val count: Int): ChatPagination()
class After(val chatItemId: Long, val count: Int): ChatPagination()
@@ -2414,15 +2343,6 @@ data class NetCfg(
val useSocksProxy: Boolean get() = socksProxy != null
val enableKeepAlive: Boolean get() = tcpKeepAlive != null
fun withHostPort(hostPort: String?, default: String? = ":9050"): NetCfg {
val socksProxy = if (hostPort?.startsWith("localhost:") == true) {
hostPort.removePrefix("localhost")
} else {
hostPort ?: default
}
return copy(socksProxy = socksProxy)
}
companion object {
val defaults: NetCfg =
NetCfg(
@@ -2510,23 +2430,15 @@ data class ChatSettings(
data class FullChatPreferences(
val timedMessages: TimedMessagesPreference,
val fullDelete: SimpleChatPreference,
val reactions: SimpleChatPreference,
val voice: SimpleChatPreference,
val calls: SimpleChatPreference,
) {
fun toPreferences(): ChatPreferences = ChatPreferences(
timedMessages = timedMessages,
fullDelete = fullDelete,
reactions = reactions,
voice = voice,
calls = calls
)
fun toPreferences(): ChatPreferences = ChatPreferences(timedMessages = timedMessages, fullDelete = fullDelete, voice = voice, calls = calls)
companion object {
val sampleData = FullChatPreferences(
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
reactions = SimpleChatPreference(allow = FeatureAllowed.YES),
voice = SimpleChatPreference(allow = FeatureAllowed.YES),
calls = SimpleChatPreference(allow = FeatureAllowed.YES),
)
@@ -2537,7 +2449,6 @@ data class FullChatPreferences(
data class ChatPreferences(
val timedMessages: TimedMessagesPreference?,
val fullDelete: SimpleChatPreference?,
val reactions: SimpleChatPreference?,
val voice: SimpleChatPreference?,
val calls: SimpleChatPreference?,
) {
@@ -2545,7 +2456,6 @@ data class ChatPreferences(
when (feature) {
ChatFeature.TimedMessages -> this.copy(timedMessages = TimedMessagesPreference(allow = allowed, ttl = param ?: this.timedMessages?.ttl))
ChatFeature.FullDelete -> this.copy(fullDelete = SimpleChatPreference(allow = allowed))
ChatFeature.Reactions -> this.copy(reactions = SimpleChatPreference(allow = allowed))
ChatFeature.Voice -> this.copy(voice = SimpleChatPreference(allow = allowed))
ChatFeature.Calls -> this.copy(calls = SimpleChatPreference(allow = allowed))
}
@@ -2554,7 +2464,6 @@ data class ChatPreferences(
val sampleData = ChatPreferences(
timedMessages = TimedMessagesPreference(allow = FeatureAllowed.NO),
fullDelete = SimpleChatPreference(allow = FeatureAllowed.NO),
reactions = SimpleChatPreference(allow = FeatureAllowed.YES),
voice = SimpleChatPreference(allow = FeatureAllowed.YES),
calls = SimpleChatPreference(allow = FeatureAllowed.YES),
)
@@ -2577,115 +2486,62 @@ data class TimedMessagesPreference(
): ChatPreference {
companion object {
val ttlValues: List<Int?>
get() = listOf(600, 3600, 86400, 7 * 86400, 30 * 86400, 3 * 30 * 86400, null)
}
}
get() = listOf(30, 300, 3600, 8 * 3600, 86400, 7 * 86400, 30 * 86400, null)
sealed class CustomTimeUnit {
object Second: CustomTimeUnit()
object Minute: CustomTimeUnit()
object Hour: CustomTimeUnit()
object Day: CustomTimeUnit()
object Week: CustomTimeUnit()
object Month: CustomTimeUnit()
val toSeconds: Int
get() =
when (this) {
Second -> 1
Minute -> 60
Hour -> 3600
Day -> 86400
Week -> 7 * 86400
Month -> 30 * 86400
}
val text: String
get() =
when (this) {
Second -> generalGetString(R.string.custom_time_unit_seconds)
Minute -> generalGetString(R.string.custom_time_unit_minutes)
Hour -> generalGetString(R.string.custom_time_unit_hours)
Day -> generalGetString(R.string.custom_time_unit_days)
Week -> generalGetString(R.string.custom_time_unit_weeks)
Month -> generalGetString(R.string.custom_time_unit_months)
}
companion object {
fun toTimeUnit(seconds: Int): Pair<CustomTimeUnit, Int> {
val tryUnits = listOf(Month, Week, Day, Hour, Minute)
var selectedUnit: Pair<CustomTimeUnit, Int>? = null
for (unit in tryUnits) {
val (v, r) = divMod(seconds, unit.toSeconds)
if (r == 0) {
selectedUnit = Pair(unit, v)
break
}
}
return selectedUnit ?: Pair(Second, seconds)
fun ttlText(ttl: Int?): String {
ttl ?: return generalGetString(R.string.feature_off)
if (ttl == 0) return String.format(generalGetString(R.string.ttl_sec), 0)
val (m_, s) = divMod(ttl, 60)
val (h_, m) = divMod(m_, 60)
val (d_, h) = divMod(h_, 24)
val (mm, d) = divMod(d_, 30)
return maybe(mm, if (mm == 1) String.format(generalGetString(R.string.ttl_month), 1) else String.format(generalGetString(R.string.ttl_months), mm)) +
maybe(d, if (d == 1) String.format(generalGetString(R.string.ttl_day), 1) else if (d == 7) String.format(generalGetString(R.string.ttl_week), 1) else if (d == 14) String.format(generalGetString(R.string.ttl_weeks), 2) else String.format(generalGetString(R.string.ttl_days), d)) +
maybe(h, if (h == 1) String.format(generalGetString(R.string.ttl_hour), 1) else String.format(generalGetString(R.string.ttl_hours), h)) +
maybe(m, String.format(generalGetString(R.string.ttl_min), m)) +
maybe(s, String.format(generalGetString(R.string.ttl_sec), s))
}
private fun divMod(n: Int, d: Int): Pair<Int, Int> =
fun shortTtlText(ttl: Int?): String {
ttl ?: return generalGetString(R.string.feature_off)
val m = ttl / 60
if (m == 0) {
return String.format(generalGetString(R.string.ttl_s), ttl)
}
val h = m / 60
if (h == 0) {
return String.format(generalGetString(R.string.ttl_m), m)
}
val d = h / 24
if (d == 0) {
return String.format(generalGetString(R.string.ttl_h), h)
}
val mm = d / 30
if (mm > 0) {
return String.format(generalGetString(R.string.ttl_mth), mm)
}
val w = d / 7
return if (w == 0 || d % 7 != 0) String.format(generalGetString(R.string.ttl_d), d) else String.format(generalGetString(R.string.ttl_w), w)
}
fun divMod(n: Int, d: Int): Pair<Int, Int> =
n / d to n % d
fun toText(seconds: Int): String {
val (unit, value) = toTimeUnit(seconds)
return when (unit) {
Second -> String.format(generalGetString(R.string.ttl_sec), value)
Minute -> String.format(generalGetString(R.string.ttl_min), value)
Hour -> if (value == 1) String.format(generalGetString(R.string.ttl_hour), 1) else String.format(generalGetString(R.string.ttl_hours), value)
Day -> if (value == 1) String.format(generalGetString(R.string.ttl_day), 1) else String.format(generalGetString(R.string.ttl_days), value)
Week -> if (value == 1) String.format(generalGetString(R.string.ttl_week), 1) else String.format(generalGetString(R.string.ttl_weeks), value)
Month -> if (value == 1) String.format(generalGetString(R.string.ttl_month), 1) else String.format(generalGetString(R.string.ttl_months), value)
}
}
fun toShortText(seconds: Int): String {
val (unit, value) = toTimeUnit(seconds)
return when (unit) {
Second -> String.format(generalGetString(R.string.ttl_s), value)
Minute -> String.format(generalGetString(R.string.ttl_m), value)
Hour -> String.format(generalGetString(R.string.ttl_h), value)
Day -> String.format(generalGetString(R.string.ttl_d), value)
Week -> String.format(generalGetString(R.string.ttl_w), value)
Month -> String.format(generalGetString(R.string.ttl_mth), value)
}
}
fun maybe(n: Int, s: String): String =
if (n == 0) "" else s
}
}
fun timeText(seconds: Int?): String {
if (seconds == null) {
return generalGetString(R.string.feature_off)
}
if (seconds == 0) {
String.format(generalGetString(R.string.ttl_sec), 0)
}
return CustomTimeUnit.toText(seconds)
}
fun shortTimeText(seconds: Int?): String {
if (seconds == null) {
return generalGetString(R.string.feature_off)
}
if (seconds == 0) {
String.format(generalGetString(R.string.ttl_s), 0)
}
return CustomTimeUnit.toShortText(seconds)
}
@Serializable
data class ContactUserPreferences(
val timedMessages: ContactUserPreferenceTimed,
val fullDelete: ContactUserPreference,
val reactions: ContactUserPreference,
val voice: ContactUserPreference,
val calls: ContactUserPreference,
) {
fun toPreferences(): ChatPreferences = ChatPreferences(
timedMessages = timedMessages.userPreference.pref,
fullDelete = fullDelete.userPreference.pref,
reactions = reactions.userPreference.pref,
voice = voice.userPreference.pref,
calls = calls.userPreference.pref
)
@@ -2702,11 +2558,6 @@ data class ContactUserPreferences(
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.NO)),
contactPreference = SimpleChatPreference(allow = FeatureAllowed.NO)
),
reactions = ContactUserPreference(
enabled = FeatureEnabled(forUser = true, forContact = true),
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
contactPreference = SimpleChatPreference(allow = FeatureAllowed.YES)
),
voice = ContactUserPreference(
enabled = FeatureEnabled(forUser = true, forContact = true),
userPreference = ContactUserPref.User(preference = SimpleChatPreference(allow = FeatureAllowed.YES)),
@@ -2803,7 +2654,6 @@ interface Feature {
enum class ChatFeature: Feature {
@SerialName("timedMessages") TimedMessages,
@SerialName("fullDelete") FullDelete,
@SerialName("reactions") Reactions,
@SerialName("voice") Voice,
@SerialName("calls") Calls;
@@ -2821,7 +2671,6 @@ enum class ChatFeature: Feature {
get() = when(this) {
TimedMessages -> generalGetString(R.string.timed_messages)
FullDelete -> generalGetString(R.string.full_deletion)
Reactions -> generalGetString(R.string.message_reactions)
Voice -> generalGetString(R.string.voice_messages)
Calls -> generalGetString(R.string.audio_video_calls)
}
@@ -2830,7 +2679,6 @@ enum class ChatFeature: Feature {
@Composable get() = when(this) {
TimedMessages -> painterResource(R.drawable.ic_timer)
FullDelete -> painterResource(R.drawable.ic_delete_forever)
Reactions -> painterResource(R.drawable.ic_add_reaction)
Voice -> painterResource(R.drawable.ic_keyboard_voice)
Calls -> painterResource(R.drawable.ic_call)
}
@@ -2839,7 +2687,6 @@ enum class ChatFeature: Feature {
override fun iconFilled(): Painter = when(this) {
TimedMessages -> painterResource(R.drawable.ic_timer_filled)
FullDelete -> painterResource(R.drawable.ic_delete_forever_filled)
Reactions -> painterResource(R.drawable.ic_add_reaction_filled)
Voice -> painterResource(R.drawable.ic_keyboard_voice_filled)
Calls -> painterResource(R.drawable.ic_call_filled)
}
@@ -2856,12 +2703,7 @@ enum class ChatFeature: Feature {
FeatureAllowed.YES -> generalGetString(R.string.allow_irreversible_message_deletion_only_if)
FeatureAllowed.NO -> generalGetString(R.string.contacts_can_mark_messages_for_deletion)
}
Reactions -> when (allowed) {
FeatureAllowed.ALWAYS -> generalGetString(R.string.allow_your_contacts_adding_message_reactions)
FeatureAllowed.YES -> generalGetString(R.string.allow_message_reactions_only_if)
FeatureAllowed.NO -> generalGetString(R.string.prohibit_message_reactions)
}
Voice -> when (allowed) {
Voice -> when (allowed) {
FeatureAllowed.ALWAYS -> generalGetString(R.string.allow_your_contacts_to_send_voice_messages)
FeatureAllowed.YES -> generalGetString(R.string.allow_voice_messages_only_if)
FeatureAllowed.NO -> generalGetString(R.string.prohibit_sending_voice_messages)
@@ -2887,12 +2729,6 @@ enum class ChatFeature: Feature {
enabled.forContact -> generalGetString(R.string.only_your_contact_can_delete)
else -> generalGetString(R.string.message_deletion_prohibited)
}
Reactions -> when {
enabled.forUser && enabled.forContact -> generalGetString(R.string.both_you_and_your_contact_can_add_message_reactions)
enabled.forUser -> generalGetString(R.string.only_you_can_add_message_reactions)
enabled.forContact -> generalGetString(R.string.only_your_contact_can_add_message_reactions)
else -> generalGetString(R.string.message_reactions_prohibited_in_this_chat)
}
Voice -> when {
enabled.forUser && enabled.forContact -> generalGetString(R.string.both_you_and_your_contact_can_send_voice)
enabled.forUser -> generalGetString(R.string.only_you_can_send_voice)
@@ -2913,7 +2749,6 @@ enum class GroupFeature: Feature {
@SerialName("timedMessages") TimedMessages,
@SerialName("directMessages") DirectMessages,
@SerialName("fullDelete") FullDelete,
@SerialName("reactions") Reactions,
@SerialName("voice") Voice;
override val hasParam: Boolean get() = when(this) {
@@ -2926,7 +2761,6 @@ enum class GroupFeature: Feature {
TimedMessages -> generalGetString(R.string.timed_messages)
DirectMessages -> generalGetString(R.string.direct_messages)
FullDelete -> generalGetString(R.string.full_deletion)
Reactions -> generalGetString(R.string.message_reactions)
Voice -> generalGetString(R.string.voice_messages)
}
@@ -2935,7 +2769,6 @@ enum class GroupFeature: Feature {
TimedMessages -> painterResource(R.drawable.ic_timer)
DirectMessages -> painterResource(R.drawable.ic_swap_horizontal_circle)
FullDelete -> painterResource(R.drawable.ic_delete_forever)
Reactions -> painterResource(R.drawable.ic_add_reaction)
Voice -> painterResource(R.drawable.ic_keyboard_voice)
}
@@ -2944,7 +2777,6 @@ enum class GroupFeature: Feature {
TimedMessages -> painterResource(R.drawable.ic_timer_filled)
DirectMessages -> painterResource(R.drawable.ic_swap_horizontal_circle_filled)
FullDelete -> painterResource(R.drawable.ic_delete_forever_filled)
Reactions -> painterResource(R.drawable.ic_add_reaction_filled)
Voice -> painterResource(R.drawable.ic_keyboard_voice_filled)
}
@@ -2963,10 +2795,6 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_delete_messages)
GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_message_deletion)
}
Reactions -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(R.string.allow_message_reactions)
GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_message_reactions_group)
}
Voice -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(R.string.allow_to_send_voice)
GroupFeatureEnabled.OFF -> generalGetString(R.string.prohibit_sending_voice)
@@ -2986,10 +2814,6 @@ enum class GroupFeature: Feature {
GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_delete)
GroupFeatureEnabled.OFF -> generalGetString(R.string.message_deletion_prohibited_in_chat)
}
Reactions -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_add_message_reactions)
GroupFeatureEnabled.OFF -> generalGetString(R.string.message_reactions_are_prohibited)
}
Voice -> when(enabled) {
GroupFeatureEnabled.ON -> generalGetString(R.string.group_members_can_send_voice)
GroupFeatureEnabled.OFF -> generalGetString(R.string.voice_messages_are_prohibited)
@@ -3030,7 +2854,6 @@ data class ContactFeaturesAllowed(
val timedMessagesAllowed: Boolean,
val timedMessagesTTL: Int?,
val fullDelete: ContactFeatureAllowed,
val reactions: ContactFeatureAllowed,
val voice: ContactFeatureAllowed,
val calls: ContactFeatureAllowed,
) {
@@ -3039,7 +2862,6 @@ data class ContactFeaturesAllowed(
timedMessagesAllowed = false,
timedMessagesTTL = null,
fullDelete = ContactFeatureAllowed.UserDefault(FeatureAllowed.NO),
reactions = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
voice = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
calls = ContactFeatureAllowed.UserDefault(FeatureAllowed.YES),
)
@@ -3053,7 +2875,6 @@ fun contactUserPrefsToFeaturesAllowed(contactUserPreferences: ContactUserPrefere
timedMessagesAllowed = allow == FeatureAllowed.YES || allow == FeatureAllowed.ALWAYS,
timedMessagesTTL = pref.pref.ttl,
fullDelete = contactUserPrefToFeatureAllowed(contactUserPreferences.fullDelete),
reactions = contactUserPrefToFeatureAllowed(contactUserPreferences.reactions),
voice = contactUserPrefToFeatureAllowed(contactUserPreferences.voice),
calls = contactUserPrefToFeatureAllowed(contactUserPreferences.calls),
)
@@ -3073,7 +2894,6 @@ fun contactFeaturesAllowedToPrefs(contactFeaturesAllowed: ContactFeaturesAllowed
ChatPreferences(
timedMessages = TimedMessagesPreference(if (contactFeaturesAllowed.timedMessagesAllowed) FeatureAllowed.YES else FeatureAllowed.NO, contactFeaturesAllowed.timedMessagesTTL),
fullDelete = contactFeatureAllowedToPref(contactFeaturesAllowed.fullDelete),
reactions = contactFeatureAllowedToPref(contactFeaturesAllowed.reactions),
voice = contactFeatureAllowedToPref(contactFeaturesAllowed.voice),
calls = contactFeatureAllowedToPref(contactFeaturesAllowed.calls),
)
@@ -3105,24 +2925,16 @@ data class FullGroupPreferences(
val timedMessages: TimedMessagesGroupPreference,
val directMessages: GroupPreference,
val fullDelete: GroupPreference,
val reactions: GroupPreference,
val voice: GroupPreference
) {
fun toGroupPreferences(): GroupPreferences =
GroupPreferences(
timedMessages = timedMessages,
directMessages = directMessages,
fullDelete = fullDelete,
reactions = reactions,
voice = voice
)
GroupPreferences(timedMessages = timedMessages, directMessages = directMessages, fullDelete = fullDelete, voice = voice)
companion object {
val sampleData = FullGroupPreferences(
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
reactions = GroupPreference(GroupFeatureEnabled.ON),
voice = GroupPreference(GroupFeatureEnabled.ON)
)
}
@@ -3133,7 +2945,6 @@ data class GroupPreferences(
val timedMessages: TimedMessagesGroupPreference?,
val directMessages: GroupPreference?,
val fullDelete: GroupPreference?,
val reactions: GroupPreference?,
val voice: GroupPreference?
) {
companion object {
@@ -3141,7 +2952,6 @@ data class GroupPreferences(
timedMessages = TimedMessagesGroupPreference(GroupFeatureEnabled.OFF),
directMessages = GroupPreference(GroupFeatureEnabled.OFF),
fullDelete = GroupPreference(GroupFeatureEnabled.OFF),
reactions = GroupPreference(GroupFeatureEnabled.ON),
voice = GroupPreference(GroupFeatureEnabled.ON)
)
}
@@ -3271,7 +3081,6 @@ sealed class CR {
@Serializable @SerialName("chatStopped") class ChatStopped: CR()
@Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List<Chat>): CR()
@Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR()
@Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: User, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR()
@Serializable @SerialName("userProtoServers") class UserProtoServers(val user: User, val servers: UserProtocolServers): CR()
@Serializable @SerialName("serverTestResult") class ServerTestResult(val user: User, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR()
@Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR()
@@ -3314,7 +3123,6 @@ sealed class CR {
@Serializable @SerialName("newChatItem") class NewChatItem(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: User, val chatItem: AChatItem): CR()
@Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: User, val added: Boolean, val reaction: ACIReaction): CR()
@Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: User, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR()
@Serializable @SerialName("contactsList") class ContactsList(val user: User, val contacts: List<Contact>): CR()
// group events
@@ -3337,7 +3145,7 @@ sealed class CR {
@Serializable @SerialName("groupInvitation") class GroupInvitation(val user: User, val groupInfo: GroupInfo): CR() // unused
@Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: User, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR()
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
@@ -3373,7 +3181,6 @@ sealed class CR {
@Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR()
@Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR()
@Serializable @SerialName("chatError") class ChatRespError(val user_: User?, val chatError: ChatError): CR()
@Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR()
@Serializable class Response(val type: String, val json: String): CR()
@Serializable class Invalid(val str: String): CR()
@@ -3385,7 +3192,6 @@ sealed class CR {
is ChatStopped -> "chatStopped"
is ApiChats -> "apiChats"
is ApiChat -> "apiChat"
is ApiChatItemInfo -> "chatItemInfo"
is UserProtoServers -> "userProtoServers"
is ServerTestResult -> "serverTestResult"
is ChatItemTTL -> "chatItemTTL"
@@ -3428,7 +3234,6 @@ sealed class CR {
is NewChatItem -> "newChatItem"
is ChatItemStatusUpdated -> "chatItemStatusUpdated"
is ChatItemUpdated -> "chatItemUpdated"
is ChatItemReaction -> "chatItemReaction"
is ChatItemDeleted -> "chatItemDeleted"
is ContactsList -> "contactsList"
is GroupCreated -> "groupCreated"
@@ -3483,7 +3288,6 @@ sealed class CR {
is CmdOk -> "cmdOk"
is ChatCmdError -> "chatCmdError"
is ChatRespError -> "chatError"
is ArchiveImported -> "archiveImported"
is Response -> "* $type"
is Invalid -> "* invalid json"
}
@@ -3496,7 +3300,6 @@ sealed class CR {
is ChatStopped -> noDetails()
is ApiChats -> withUser(user, json.encodeToString(chats))
is ApiChat -> withUser(user, json.encodeToString(chat))
is ApiChatItemInfo -> withUser(user, "chatItem: ${json.encodeToString(AChatItem)}\n${json.encodeToString(chatItemInfo)}")
is UserProtoServers -> withUser(user, "servers: ${json.encodeToString(servers)}")
is ServerTestResult -> withUser(user, "server: $testServer\nresult: ${json.encodeToString(testFailure)}")
is ChatItemTTL -> withUser(user, json.encodeToString(chatItemTTL))
@@ -3540,7 +3343,6 @@ sealed class CR {
is NewChatItem -> withUser(user, json.encodeToString(chatItem))
is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem))
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}")
is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser")
is ContactsList -> withUser(user, json.encodeToString(contacts))
is GroupCreated -> withUser(user, json.encodeToString(groupInfo))
@@ -3562,7 +3364,7 @@ sealed class CR {
is GroupInvitation -> withUser(user, json.encodeToString(groupInfo))
is UserJoinedGroup -> withUser(user, json.encodeToString(groupInfo))
is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact")
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
@@ -3596,7 +3398,6 @@ sealed class CR {
is CmdOk -> withUser(user, noDetails())
is ChatCmdError -> withUser(user_, chatError.string)
is ChatRespError -> withUser(user_, chatError.string)
is ArchiveImported -> "${archiveErrors.map { it.string } }"
is Response -> json
is Invalid -> str
}
@@ -3685,7 +3486,6 @@ sealed class ChatErrorType {
is InvalidConnReq -> "invalidConnReq"
is FileAlreadyReceiving -> "fileAlreadyReceiving"
is СommandError -> "commandError $message"
is CEException -> "exception $message"
}
@Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType()
@Serializable @SerialName("differentActiveUser") class DifferentActiveUser: ChatErrorType()
@@ -3693,7 +3493,6 @@ sealed class ChatErrorType {
@Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType()
@Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving: ChatErrorType()
@Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
}
@Serializable
@@ -3905,13 +3704,3 @@ sealed class XFTPErrorType {
@Serializable @SerialName("FILE_IO") object FILE_IO: XFTPErrorType()
@Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType()
}
@Serializable
sealed class ArchiveError {
val string: String get() = when (this) {
is ArchiveErrorImport -> "import ${chatError.string}"
is ArchiveErrorImportFile -> "importFile $file ${chatError.string}"
}
@Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError()
@Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError()
}

View File

@@ -21,6 +21,7 @@ val ToolbarDark = Color(80, 80, 80, 12)
val SettingsSecondaryLight = Color(200, 196, 195, 90)
val GroupDark = Color(80, 80, 80, 60)
val IncomingCallLight = Color(239, 237, 236, 255)
val IncomingCallDark = Color(34, 30, 29, 255)
val WarningOrange = Color(255, 127, 0, 255)
val WarningYellow = Color(255, 192, 0, 255)
val FileLight = Color(183, 190, 199, 255)

View File

@@ -16,7 +16,6 @@ import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okhttp3.internal.toHexString
enum class DefaultTheme {
SYSTEM, LIGHT, DARK, SIMPLEX;
@@ -129,29 +128,11 @@ data class ThemeColors(
receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage,
)
}
fun withFilledColors(base: DefaultTheme): ThemeColors {
val c = toColors(base)
val ac = toAppColors(base)
return ThemeColors(
primary = c.primary.toReadableHex(),
primaryVariant = c.primaryVariant.toReadableHex(),
secondary = c.secondary.toReadableHex(),
secondaryVariant = c.secondaryVariant.toReadableHex(),
background = c.background.toReadableHex(),
surface = c.surface.toReadableHex(),
title = ac.title.toReadableHex(),
sentMessage = ac.sentMessage.toReadableHex(),
receivedMessage = ac.receivedMessage.toReadableHex()
)
}
}
private fun String.colorFromReadableHex(): Color =
Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong())
private fun Color.toReadableHex(): String = "#" + toArgb().toHexString()
@Serializable
data class ThemeOverrides (
val base: DefaultTheme,
@@ -172,6 +153,9 @@ data class ThemeOverrides (
}
}
@Serializable
data class ThemeData (val colors: ThemeColors)
fun Modifier.themedBackground(baseTheme: DefaultTheme = CurrentColors.value.base, shape: Shape = RectangleShape): Modifier {
return if (baseTheme == DefaultTheme.SIMPLEX) {
this.background(brush = Brush.linearGradient(

View File

@@ -44,16 +44,19 @@ object ThemeManager {
return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base))
}
fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides {
val themeName = appPrefs.currentTheme.get()!!
val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) {
themeName
} else {
if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name
}
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base))
fun currentThemeData(darkForSystemTheme: Boolean): ThemeData {
val t = currentColors(darkForSystemTheme)
return ThemeData(colors = ThemeColors(
primary = t.colors.primary.toReadableHex(),
primaryVariant = t.colors.primaryVariant.toReadableHex(),
secondary = t.colors.secondary.toReadableHex(),
secondaryVariant = t.colors.secondaryVariant.toReadableHex(),
background = t.colors.background.toReadableHex(),
surface = t.colors.surface.toReadableHex(),
title = t.appColors.title.toReadableHex(),
sentMessage = t.appColors.sentMessage.toReadableHex(),
receivedMessage = t.appColors.receivedMessage.toReadableHex()
))
}
// colors, default theme enum, localized name of theme
@@ -125,12 +128,11 @@ object ThemeManager {
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}
fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) {
fun saveAndApplyThemeData(name: String, theme: ThemeData, darkForSystemTheme: Boolean) {
val overrides = appPrefs.themeOverrides.get().toMutableMap()
val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
overrides[theme.base.name] = prevValue.copy(colors = theme.colors)
val prevValue = overrides[name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors())
overrides[name] = prevValue.copy(colors = theme.colors)
appPrefs.themeOverrides.set(overrides)
appPrefs.currentTheme.set(theme.base.name)
CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight)
}

View File

@@ -33,11 +33,6 @@ val Typography = Typography(
fontWeight = FontWeight.Normal,
fontSize = 18.5.sp
),
h4 = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,
fontSize = 17.5.sp
),
body1 = TextStyle(
fontFamily = Inter,
fontWeight = FontWeight.Normal,

View File

@@ -86,7 +86,7 @@ fun TerminalLayout(
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
sendMessage = { sendCommand() },
sendMessage = sendCommand,
sendLiveMessage = null,
updateLiveMessage = null,
onMessageChange = ::onMessageChange,
@@ -99,8 +99,8 @@ fun TerminalLayout(
Surface(
modifier = Modifier
.padding(contentPadding)
.fillMaxWidth(),
color = MaterialTheme.colors.background
.fillMaxWidth()
.themedBackground()
) {
TerminalLog(terminalItems)
}

View File

@@ -44,75 +44,77 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
val fullName = rememberSaveable { mutableStateOf("") }
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
/*CloseSheetBar(close = {
if (chatModel.users.isEmpty()) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
close()
}
})*/
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
AppBarTitle(stringResource(R.string.create_profile), bottomPadding = DEFAULT_PADDING)
ReadableText(R.string.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1)
ReadableText(R.string.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1)
Spacer(Modifier.height(DEFAULT_PADDING))
Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text(
stringResource(R.string.display_name),
fontSize = 16.sp
)
if (!isValidDisplayName(displayName.value)) {
Surface(Modifier.background(MaterialTheme.colors.onBackground)) {
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
/*CloseSheetBar(close = {
if (chatModel.users.isEmpty()) {
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
} else {
close()
}
})*/
Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) {
AppBarTitle(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.no_spaces),
fontSize = 16.sp,
color = Color.Red
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()) {
SimpleButtonDecorated(
text = stringResource(R.string.about_simplex),
icon = painterResource(R.drawable.ic_arrow_back_ios_new),
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
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
createColor = MaterialTheme.colors.secondary
}
Surface(shape = RoundedCornerShape(20.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(R.drawable.ic_arrow_forward_ios), stringResource(R.string.create_profile_button), tint = createColor)
}
}
}
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()) {
SimpleButtonDecorated(
text = stringResource(R.string.about_simplex),
icon = painterResource(R.drawable.ic_arrow_back_ios_new),
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
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
createColor = MaterialTheme.colors.secondary
}
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium)
Icon(painterResource(R.drawable.ic_arrow_forward_ios), stringResource(R.string.create_profile_button), tint = createColor)
}
}
}
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
}
}
}
}
@@ -125,17 +127,13 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress
chatModel.onboardingStage.value = OnboardingStage.Step3_SetNotificationsMode
SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert()
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
// this will get it unstuck.
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
close()
}
}

View File

@@ -26,6 +26,7 @@ class CallManager(val chatModel: ChatModel) {
}
fun acceptIncomingCall(invitation: RcvCallInvitation) {
ModalManager.shared.closeModals()
val call = chatModel.activeCall.value
if (call == null) {
justAcceptIncomingCall(invitation = invitation)

View File

@@ -185,12 +185,10 @@ fun ActiveCallView(chatModel: ChatModel) {
activity.volumeControlStream = AudioManager.STREAM_VOICE_CALL
// Lock orientation to portrait in order to have good experience with calls
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
chatModel.activeCallViewIsVisible.value = true
onDispose {
activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
chatModel.activeCallViewIsVisible.value = false
}
}
}

View File

@@ -97,9 +97,8 @@ fun IncomingCallActivityView(m: ChatModel) {
SimpleXTheme {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background
) {
.themedBackground()
.fillMaxSize()) {
if (showCallView) {
Box {
ActiveCallView(m)
@@ -227,9 +226,8 @@ fun PreviewIncomingCallLockScreenAlert() {
SimpleXTheme(true) {
Surface(
Modifier
.fillMaxSize(),
color = MaterialTheme.colors.background
) {
.themedBackground()
.fillMaxSize()) {
IncomingCallLockScreenAlertLayout(
invitation = RcvCallInvitation(
user = User.sampleData,

View File

@@ -50,13 +50,13 @@ fun IncomingCallAlertLayout(
ignoreCall: () -> Unit,
acceptCall: () -> Unit
) {
val color = if (isInDarkTheme()) MaterialTheme.colors.surface else IncomingCallLight
val color = if (isInDarkTheme()) IncomingCallDark else IncomingCallLight
Column(Modifier.fillMaxWidth().background(color).padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING, start = DEFAULT_PADDING, end = 8.dp)) {
IncomingCallInfo(invitation, chatModel)
Spacer(Modifier.height(8.dp))
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
Row(Modifier.fillMaxWidth().weight(1f), verticalAlignment = Alignment.CenterVertically) {
ProfilePreview(profileOf = invitation.contact, size = 64.dp)
ProfilePreview(profileOf = invitation.contact, size = 64.dp, color = Color.White)
}
Row(verticalAlignment = Alignment.CenterVertically) {
CallButton(stringResource(R.string.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall)
@@ -78,7 +78,7 @@ fun IncomingCallInfo(invitation: RcvCallInvitation, chatModel: ChatModel) {
if (invitation.callType.media == CallMediaType.Video) CallIcon(painterResource(R.drawable.ic_videocam_filled), stringResource(R.string.icon_descr_video_call))
else CallIcon(painterResource(R.drawable.ic_call_filled), stringResource(R.string.icon_descr_audio_call))
Spacer(Modifier.width(4.dp))
Text(invitation.callTypeText, color = MaterialTheme.colors.onBackground)
Text(invitation.callTypeText)
}
}

View File

@@ -5,11 +5,8 @@ import InfoRowEllipsis
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionItemViewWithIcon
import SectionSpacer
import SectionTextFooter
import SectionView
import TextIconSpaced
import android.widget.Toast
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
@@ -21,7 +18,8 @@ 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.platform.*
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -36,7 +34,6 @@ import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import chat.simplex.app.views.usersettings.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
@@ -198,15 +195,6 @@ fun ChatInfoLayout(
}
SectionDividerSpaced()
if (contact.contactLink != null) {
val context = LocalContext.current
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(context, contact.contactLink) }
SectionTextFooter(stringResource(R.string.you_can_share_this_address_with_your_contacts).format(contact.displayName))
}
SectionDividerSpaced()
}
SectionView(title = stringResource(R.string.conn_stats_section_title_servers)) {
SwitchAddressButton(switchContactAddress)
@@ -428,17 +416,6 @@ private fun DeleteContactButton(onClick: () -> Unit) {
)
}
@Composable
fun ShareAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_share_filled),
stringResource(R.string.share_address),
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
)
}
private fun setContactAlias(contactApiId: Long, localAlias: String, chatModel: ChatModel) = withApi {
chatModel.controller.apiSetContactAlias(contactApiId, localAlias)?.let {
chatModel.updateContact(it)

View File

@@ -1,186 +0,0 @@
package chat.simplex.app.views.chat
import InfoRow
import SectionBottomSpacer
import SectionDividerSpaced
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
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.CurrentColors
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.views.helpers.*
@Composable
fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) {
val sent = ci.chatDir.sent
val appColors = CurrentColors.collectAsState().value.appColors
val itemColor = if (sent) appColors.sentMessage else appColors.receivedMessage
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
@Composable
fun ItemVersionView(ciVersion: ChatItemVersion, current: Boolean) {
val showMenu = remember { mutableStateOf(false) }
val text = ciVersion.msgContent.text
@Composable
fun VersionText() {
if (text != "") {
MarkdownText(
text, if (text.isEmpty()) emptyList() else ciVersion.formattedText,
linkMode = SimplexLinkMode.DESCRIPTION, uriHandler = uriHandler,
onLinkLongClick = { showMenu.value = true }
)
} else {
Text(
generalGetString(R.string.item_info_no_text),
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp, fontStyle = FontStyle.Italic)
)
}
}
Column {
Box(
Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp)
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = {})
) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
VersionText()
}
}
Row(Modifier.padding(start = 12.dp, top = 3.dp, bottom = 16.dp)) {
Text(
localTimestamp(ciVersion.itemVersionTs),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary,
modifier = Modifier.padding(end = 6.dp)
)
if (current && ci.meta.itemDeleted == null) {
Text(
stringResource(R.string.item_info_current),
fontSize = 12.sp,
color = MaterialTheme.colors.secondary
)
}
}
if (text != "") {
DefaultDropdownMenu(showMenu) {
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
shareText(context, text)
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, text)
showMenu.value = false
})
}
}
}
}
Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
AppBarTitle(stringResource(if (sent) R.string.sent_message else R.string.received_message))
SectionView {
InfoRow(stringResource(R.string.info_row_sent_at), localTimestamp(ci.meta.itemTs))
if (!sent) {
InfoRow(stringResource(R.string.info_row_received_at), localTimestamp(ci.meta.createdAt))
}
when (val itemDeleted = ci.meta.itemDeleted) {
is CIDeleted.Deleted ->
if (itemDeleted.deletedTs != null) {
InfoRow(stringResource(R.string.info_row_deleted_at), localTimestamp(itemDeleted.deletedTs))
}
is CIDeleted.Moderated ->
if (itemDeleted.deletedTs != null) {
InfoRow(stringResource(R.string.info_row_moderated_at), localTimestamp(itemDeleted.deletedTs))
}
else -> {}
}
val deleteAt = ci.meta.itemTimed?.deleteAt
if (deleteAt != null) {
InfoRow(stringResource(R.string.info_row_disappears_at), localTimestamp(deleteAt))
}
if (devTools) {
InfoRow(stringResource(R.string.info_row_database_id), ci.meta.itemId.toString())
InfoRow(stringResource(R.string.info_row_updated_at), localTimestamp(ci.meta.updatedAt))
}
}
val versions = ciInfo.itemVersions
if (versions.isNotEmpty()) {
SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false)
SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) {
Text(stringResource(R.string.edit_history), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING))
versions.forEachIndexed { i, ciVersion ->
ItemVersionView(ciVersion, current = i == 0)
}
}
}
SectionBottomSpacer()
}
}
fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String {
val meta = ci.meta
val sent = ci.chatDir.sent
val shareText = mutableListOf<String>(generalGetString(if (sent) R.string.sent_message else R.string.received_message), "")
shareText.add(String.format(generalGetString(R.string.share_text_sent_at), localTimestamp(meta.itemTs)))
if (!ci.chatDir.sent) {
shareText.add(String.format(generalGetString(R.string.share_text_received_at), localTimestamp(meta.createdAt)))
}
when (val itemDeleted = ci.meta.itemDeleted) {
is CIDeleted.Deleted ->
if (itemDeleted.deletedTs != null) {
shareText.add(String.format(generalGetString(R.string.share_text_deleted_at), localTimestamp(itemDeleted.deletedTs)))
}
is CIDeleted.Moderated ->
if (itemDeleted.deletedTs != null) {
shareText.add(String.format(generalGetString(R.string.share_text_moderated_at), localTimestamp(itemDeleted.deletedTs)))
}
else -> {}
}
val deleteAt = ci.meta.itemTimed?.deleteAt
if (deleteAt != null) {
shareText.add(String.format(generalGetString(R.string.share_text_disappears_at), localTimestamp(deleteAt)))
}
if (devTools) {
shareText.add(String.format(generalGetString(R.string.share_text_database_id), meta.itemId))
shareText.add(String.format(generalGetString(R.string.share_text_updated_at), meta.updatedAt))
}
val versions = chatItemInfo.itemVersions
if (versions.isNotEmpty()) {
shareText.add("")
shareText.add(generalGetString(R.string.edit_history))
versions.forEachIndexed { index, itemVersion ->
val ts = localTimestamp(itemVersion.itemVersionTs)
shareText.add("")
shareText.add(
if (index == 0 && ci.meta.itemDeleted == null) {
String.format(generalGetString(R.string.current_version_timestamp), ts)
} else {
localTimestamp(itemVersion.itemVersionTs)
}
)
val t = itemVersion.msgContent.text
shareText.add(if (t != "") t else generalGetString(R.string.item_info_no_text))
}
}
return shareText.joinToString(separator = "\n")
}

View File

@@ -101,7 +101,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
}
}
val view = LocalView.current
val context = LocalContext.current
if (activeChat.value == null || user == null) {
chatModel.chatId.value = null
} else {
@@ -259,32 +258,6 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) {
chatModel.controller.allowFeatureToContact(contact, feature, param)
}
},
setReaction = { cInfo, cItem, add, reaction ->
withApi {
val updatedCI = chatModel.controller.apiChatItemReaction(
type = cInfo.chatType,
id = cInfo.apiId,
itemId = cItem.id,
add = add,
reaction = reaction
)
if (updatedCI != null) {
chatModel.updateChatItem(cInfo, updatedCI)
}
}
},
showItemDetails = { cInfo, cItem ->
withApi {
val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
ModalManager.shared.showModal(endButtons = { ShareButton {
shareText(context, itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get()))
} }) {
ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get())
}
}
}
},
addMembers = { groupInfo ->
hideKeyboard(view)
withApi {
@@ -343,8 +316,6 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
addMembers: (GroupInfo) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
@@ -355,6 +326,7 @@ fun ChatLayout(
Box(
Modifier
.fillMaxWidth()
.themedBackground()
) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
ModalBottomSheetLayout(
@@ -380,14 +352,11 @@ fun ChatLayout(
modifier = Modifier.navigationBarsWithImePadding(),
floatingActionButton = { floatingButton.value() },
) { contentPadding ->
BoxWithConstraints(Modifier
.fillMaxHeight()
.padding(contentPadding)
) {
BoxWithConstraints(Modifier.fillMaxHeight().padding(contentPadding)) {
ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, markRead, setFloatingButton, onComposed,
)
}
}
@@ -560,8 +529,6 @@ fun BoxWithConstraintsScope.ChatItemsList(
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit,
setFloatingButton: (@Composable () -> Unit) -> Unit,
onComposed: () -> Unit,
@@ -649,13 +616,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
}
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null
if (chat.chatInfo is ChatInfo.Group) {
if (cItem.chatDir is CIDirection.GroupRcv) {
val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null
val member = cItem.chatDir.groupMember
val showMember = showMemberImage(member, prevItem)
Row(Modifier.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp).then(swipeableModifier)) {
Row(Modifier.padding(start = 8.dp, end = 66.dp).then(swipeableModifier)) {
if (showMember) {
val contactId = member.memberContactId
if (contactId == null) {
@@ -675,22 +641,22 @@ 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, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
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 = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
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, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
} else { // direct message
val sent = cItem.chatDir.sent
Box(
Modifier.padding(
start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp,
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
start = if (sent) 76.dp else 12.dp,
end = if (sent) 12.dp else 76.dp,
).then(swipeableModifier)
) {
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, scrollToItem = scrollToItem)
}
}
@@ -889,7 +855,6 @@ private fun TopEndFloatingButton(
FloatingActionButton(
{}, // no action here
modifier.size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp),
interactionSource = interactionSource,
) {
@@ -916,8 +881,7 @@ private fun bottomEndFloatingButton(
FloatingActionButton(
onClick = onClickCounter,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant,
modifier = Modifier.size(48.dp)
) {
Text(
unreadCountStr(unreadCount),
@@ -932,8 +896,7 @@ private fun bottomEndFloatingButton(
FloatingActionButton(
onClick = onClickArrowDown,
elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp, 0.dp, 0.dp),
modifier = Modifier.size(48.dp),
backgroundColor = MaterialTheme.colors.secondaryVariant,
modifier = Modifier.size(48.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_keyboard_arrow_down),
@@ -1113,8 +1076,6 @@ fun PreviewChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },
@@ -1175,8 +1136,6 @@ fun PreviewGroupChatLayout() {
startCall = {},
acceptCall = { _ -> },
acceptFeature = { _, _, _ -> },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
addMembers = { _ -> },
markRead = { _, _ -> },
changeNtfsState = { _, _ -> },

View File

@@ -118,7 +118,7 @@ data class ComposeState(
val attachmentDisabled: Boolean
get() {
if (editing || liveMessage != null || inProgress) return true
if (editing || liveMessage != null) return true
return when (preview) {
ComposePreview.NoPreview -> false
is ComposePreview.CLinkPreview -> false
@@ -366,27 +366,22 @@ fun ComposeView(
chatModel.filesToDelete.clear()
}
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false, ttl: Int?): ChatItem? {
suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false): ChatItem? {
val aChatItem = chatModel.controller.apiSendMessage(
type = cInfo.chatType,
id = cInfo.apiId,
file = file,
quotedItemId = quoted,
mc = mc,
live = live,
ttl = ttl
live = live
)
if (aChatItem != null) {
chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem.chatItem
}
if (file != null) removeFile(context, file)
return null
if (aChatItem != null) chatModel.addChatItem(cInfo, aChatItem.chatItem)
return aChatItem?.chatItem
}
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
suspend fun sendMessageAsync(text: String?, live: Boolean): ChatItem? {
val cInfo = chat.chatInfo
val cs = composeState.value
var sent: ChatItem?
@@ -500,8 +495,7 @@ fun ComposeView(
msgs.forEachIndexed { index, content ->
if (index > 0) delay(100)
sent = send(cInfo, content, if (index == 0) quotedItemId else null, files.getOrNull(index),
live = if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false,
ttl = ttl
if (content !is MsgContent.MCVoice && index == msgs.lastIndex) live else false
)
}
if (sent == null &&
@@ -509,16 +503,16 @@ fun ComposeView(
cs.preview is ComposePreview.FilePreview ||
cs.preview is ComposePreview.VoicePreview)
) {
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl)
sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live)
}
}
clearState(live)
return sent
}
fun sendMessage(ttl: Int?) {
fun sendMessage() {
withBGApi {
sendMessageAsync(null, false, ttl)
sendMessageAsync(null, false)
}
}
@@ -594,7 +588,7 @@ fun ComposeView(
val cs = composeState.value
val typedMsg = cs.message
if ((cs.sendEnabled() || cs.contextItem is ComposeContextItem.QuotedItem) && (cs.liveMessage == null || !cs.liveMessage?.sent)) {
val ci = sendMessageAsync(typedMsg, live = true, ttl = null)
val ci = sendMessageAsync(typedMsg, live = true)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = typedMsg, sent = true))
}
@@ -615,7 +609,7 @@ fun ComposeView(
if (liveMessage != null) {
val sentMsg = liveMessageToSend(liveMessage, typedMsg)
if (sentMsg != null) {
val ci = sendMessageAsync(sentMsg, live = true, ttl = null)
val ci = sendMessageAsync(sentMsg, live = true)
if (ci != null) {
composeState.value = composeState.value.copy(liveMessage = LiveMessage(ci, typedMsg = typedMsg, sentMsg = sentMsg, sent = true))
}
@@ -629,27 +623,23 @@ fun ComposeView(
fun previewView() {
when (val preview = composeState.value.preview) {
ComposePreview.NoPreview -> {}
is ComposePreview.CLinkPreview -> ComposeLinkView(
preview.linkPreview,
::cancelLinkPreview,
cancelEnabled = !composeState.value.inProgress
)
is ComposePreview.CLinkPreview -> ComposeLinkView(preview.linkPreview, ::cancelLinkPreview)
is ComposePreview.MediaPreview -> ComposeImageView(
preview,
::cancelImages,
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress
cancelEnabled = !composeState.value.editing
)
is ComposePreview.VoicePreview -> ComposeVoiceView(
preview.voice,
preview.durationMs,
preview.finished,
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress,
cancelEnabled = !composeState.value.editing,
::cancelVoice
)
is ComposePreview.FilePreview -> ComposeFileView(
preview.fileName,
::cancelFile,
cancelEnabled = !composeState.value.editing && !composeState.value.inProgress
cancelEnabled = !composeState.value.editing
)
}
}
@@ -667,14 +657,6 @@ fun ComposeView(
}
}
// In case a user sent something, state is in progress, the user rotates a screen to different orientation.
// Without clearing the state the user will be unable to send anything until re-enters ChatView
LaunchedEffect(Unit) {
if (composeState.value.inProgress) {
clearState()
}
}
LaunchedEffect(chatModel.sharedContent.value) {
// Important. If it's null, don't do anything, chat is not closed yet but will be after a moment
if (chatModel.chatId.value == null) return@LaunchedEffect
@@ -692,22 +674,11 @@ fun ComposeView(
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
Column {
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
contextItemView()
when {
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
composeState.value.editing && composeState.value.preview is ComposePreview.FilePreview -> {}
else -> previewView()
}
} else {
Box {
Box(Modifier.align(Alignment.TopStart).padding(bottom = 69.dp)) {
contextItemView()
}
Box(Modifier.align(Alignment.BottomStart)) {
previewView()
}
}
contextItemView()
when {
composeState.value.editing && composeState.value.preview is ComposePreview.VoicePreview -> {}
composeState.value.editing && composeState.value.preview is ComposePreview.FilePreview -> {}
else -> previewView()
}
Row(
modifier = Modifier.padding(end = 8.dp),
@@ -769,12 +740,10 @@ fun ComposeView(
if (orientation == activity.resources.configuration.orientation) {
val cs = composeState.value
if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) {
sendMessage(null)
sendMessage()
resetLinkPreview()
clearCurrentDraft()
deleteUnusedFiles()
} else if (composeState.value.inProgress) {
clearCurrentDraft()
} else if (!composeState.value.empty) {
if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) {
composeState.value = cs.copy(preview = cs.preview.copy(finished = true))
@@ -790,9 +759,6 @@ fun ComposeView(
}
}
// TODO in 5.2 - allow if ttl is not configured
// val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) }
val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) && chat.chatInfo.timedMessagesTTL != null }
SendMsgView(
composeState,
showVoiceRecordIcon = true,
@@ -804,10 +770,8 @@ fun ComposeView(
allowVoiceToContact = ::allowVoiceToContact,
userIsObserver = userIsObserver.value,
userCanSend = userCanSend.value,
timedMessageAllowed = timedMessageAllowed,
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
sendMessage = { ttl ->
sendMessage(ttl)
sendMessage = {
sendMessage()
resetLinkPreview()
},
sendLiveMessage = ::sendLiveMessage,

View File

@@ -6,10 +6,6 @@ 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.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -29,135 +25,99 @@ fun ComposeVoiceView(
cancelEnabled: Boolean,
cancelVoice: () -> Unit
) {
val progress = rememberSaveable { mutableStateOf(0) }
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Box {
Box(
BoxWithConstraints(Modifier
.fillMaxWidth()
) {
val audioPlaying = rememberSaveable { mutableStateOf(false) }
val progress = rememberSaveable { mutableStateOf(0) }
val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) }
val progressBarWidth = remember { Animatable(0f) }
LaunchedEffect(recordedDurationMs, finishedRecording) {
snapshotFlow { progress.value }
.distinctUntilChanged()
.collect {
val startTime = when {
finishedRecording -> progress.value
else -> recordedDurationMs
}
val endTime = when {
finishedRecording -> duration.value
audioPlaying.value -> recordedDurationMs
else -> MAX_VOICE_MILLIS_FOR_SENDING
}
val to = ((startTime.toDouble() / endTime) * maxWidth.value).dp
progressBarWidth.animateTo(to.value, audioProgressBarAnimationSpec())
}
}
Spacer(
Modifier
.fillMaxWidth().padding(top = 22.dp)
.requiredWidth(progressBarWidth.value.dp)
.padding(top = 58.dp)
.height(3.dp)
.background(MaterialTheme.colors.primary)
)
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row(
Modifier
.height(60.dp)
.fillMaxWidth()
.padding(top = 8.dp)
.background(sentColor),
verticalAlignment = Alignment.CenterVertically
) {
val audioPlaying = rememberSaveable { mutableStateOf(false) }
Row(
Modifier
.height(57.dp)
.fillMaxWidth()
.background(sentColor)
.padding(top = 3.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(
onClick = {
if (!audioPlaying.value) {
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
} else {
AudioPlayer.pause(audioPlaying, progress)
}
},
enabled = finishedRecording) {
Icon(
if (audioPlaying.value) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
stringResource(R.string.icon_descr_file),
Modifier
.padding(start = 4.dp, end = 2.dp)
.size(36.dp),
tint = if (finishedRecording) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
val numberInText = remember(recordedDurationMs, progress.value) {
derivedStateOf {
when {
finishedRecording && progress.value == 0 && !audioPlaying.value -> duration.value / 1000
finishedRecording -> progress.value / 1000
else -> recordedDurationMs / 1000
}
}
}
Text(
durationText(numberInText.value),
fontSize = 18.sp,
color = MaterialTheme.colors.secondary,
)
Spacer(Modifier.weight(1f))
if (cancelEnabled) {
IconButton(
onClick = {
if (!audioPlaying.value) {
AudioPlayer.play(filePath, audioPlaying, progress, duration, false)
} else {
AudioPlayer.pause(audioPlaying, progress)
}
AudioPlayer.stop(filePath)
cancelVoice()
},
enabled = finishedRecording
modifier = Modifier.padding(0.dp)
) {
Icon(
if (audioPlaying.value) painterResource(R.drawable.ic_pause_filled) else painterResource(R.drawable.ic_play_arrow_filled),
stringResource(R.string.icon_descr_file),
Modifier
.padding(start = 4.dp, end = 2.dp)
.size(36.dp),
tint = if (finishedRecording) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
}
val numberInText = remember(recordedDurationMs, progress.value) {
derivedStateOf {
when {
finishedRecording && progress.value == 0 && !audioPlaying.value -> duration.value / 1000
finishedRecording -> progress.value / 1000
else -> recordedDurationMs / 1000
}
}
}
Text(
durationText(numberInText.value),
fontSize = 18.sp,
color = MaterialTheme.colors.secondary,
)
Spacer(Modifier.weight(1f))
if (cancelEnabled) {
IconButton(
onClick = {
AudioPlayer.stop(filePath)
cancelVoice()
},
modifier = Modifier.padding(0.dp)
) {
Icon(
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_file_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
}
}
}
}
if (finishedRecording) {
FinishedRecordingSlider(sentColor, progress, duration, filePath)
} else {
RecordingInProgressSlider(recordedDurationMs)
}
}
}
@Composable
fun FinishedRecordingSlider(backgroundColor: Color, progress: MutableState<Int>, duration: MutableState<Int>, filePath: String) {
val dp4 = with(LocalDensity.current) { 4.dp.toPx() }
val dp10 = with(LocalDensity.current) { 10.dp.toPx() }
val primary = MaterialTheme.colors.primary
val inactiveTrackColor = MaterialTheme.colors.primary.mixWith(
backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha),
0.24f)
Slider(
progress.value.toFloat(),
onValueChange = { AudioPlayer.seekTo(it.toInt(), progress, filePath) },
Modifier
.fillMaxWidth()
.drawBehind {
drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
drawRect(inactiveTrackColor, Offset(size.width - dp10, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
},
colors = SliderDefaults.colors(inactiveTrackColor = inactiveTrackColor),
valueRange = 0f..duration.value.toFloat()
)
}
@Composable
fun RecordingInProgressSlider(recordedDurationMs: Int) {
val thumbPosition = remember { Animatable(0f) }
val recDuration = rememberUpdatedState(recordedDurationMs)
LaunchedEffect(Unit) {
snapshotFlow { recDuration.value }
.distinctUntilChanged()
.collect {
thumbPosition.animateTo(it.toFloat(), audioProgressBarAnimationSpec())
}
}
val dp4 = with(LocalDensity.current) { 4.dp.toPx() }
val dp10 = with(LocalDensity.current) { 10.dp.toPx() }
val primary = MaterialTheme.colors.primary
val inactiveTrackColor = Color.Transparent
Slider(
thumbPosition.value,
onValueChange = {},
Modifier
.fillMaxWidth()
.drawBehind {
drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
},
colors = SliderDefaults.colors(disabledInactiveTrackColor = inactiveTrackColor, disabledActiveTrackColor = primary, thumbColor = Color.Transparent, disabledThumbColor = Color.Transparent),
enabled = false,
valueRange = 0f..MAX_VOICE_MILLIS_FOR_SENDING.toFloat()
)
}
@Preview
@Composable
fun PreviewComposeAudioView() {

View File

@@ -95,11 +95,6 @@ private fun ContactPreferencesLayout(
applyPrefs(featuresAllowed.copy(fullDelete = it))
}
SectionDividerSpaced(true, maxBottomPadding = false)
// val allowReactions: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) }
// FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) {
// applyPrefs(featuresAllowed.copy(reactions = it))
// }
// SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice: MutableState<ContactFeatureAllowed> = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) }
FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) {
applyPrefs(featuresAllowed.copy(voice = it))
@@ -144,6 +139,7 @@ private fun FeatureSection(
ContactFeatureAllowed.values(userDefault).map { it to it.text },
allowFeature,
icon = null,
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected
)
InfoRow(
@@ -151,7 +147,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
@@ -186,17 +182,9 @@ private fun TimedMessagesFeatureSection(
)
if (featuresAllowed.timedMessagesAllowed) {
val ttl = rememberSaveable(featuresAllowed.timedMessagesTTL) { mutableStateOf(featuresAllowed.timedMessagesTTL) }
DropdownCustomTimePickerSettingRow(
selection = ttl,
propagateExternalSelectionUpdate = true, // for Reset
label = generalGetString(R.string.delete_after),
dropdownValues = TimedMessagesPreference.ttlValues,
customPickerTitle = generalGetString(R.string.delete_after),
customPickerConfirmButtonText = generalGetString(R.string.custom_time_picker_select),
onSelected = onTTLUpdated
)
TimedMessagesTTLPicker(ttl, onTTLUpdated)
} else if (pref.contactPreference.allow == FeatureAllowed.YES || pref.contactPreference.allow == FeatureAllowed.ALWAYS) {
InfoRow(generalGetString(R.string.delete_after), timeText(pref.contactPreference.ttl))
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(pref.contactPreference.ttl))
}
}
SectionTextFooter(ChatFeature.TimedMessages.enabledDescription(enabled))
@@ -214,6 +202,18 @@ private fun ResetSaveButtons(reset: () -> Unit, save: () -> Unit, disabled: Bool
}
}
@Composable
fun TimedMessagesTTLPicker(selection: MutableState<Int?>, onSelected: (Int?) -> Unit) {
val ttlValues = TimedMessagesPreference.ttlValues
val values = ttlValues + if (ttlValues.contains(selection.value)) listOf() else listOf(selection.value)
ExposedDropDownSettingRow(
generalGetString(R.string.delete_after),
values.map { it to TimedMessagesPreference.ttlText(it) },
selection,
onSelected = onSelected
)
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_preferences_question),

View File

@@ -18,7 +18,7 @@ import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
@@ -29,14 +29,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
@@ -44,7 +44,8 @@ import androidx.core.widget.*
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.ui.theme.CurrentColors
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.item.ItemAction
import chat.simplex.app.views.helpers.*
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@@ -63,25 +64,13 @@ fun SendMsgView(
userIsObserver: Boolean,
userCanSend: Boolean,
allowVoiceToContact: () -> Unit,
timedMessageAllowed: Boolean = false,
customDisappearingMessageTimePref: SharedPreference<Int>? = null,
sendMessage: (Int?) -> Unit,
sendMessage: () -> Unit,
sendLiveMessage: (suspend () -> Unit)? = null,
updateLiveMessage: (suspend () -> Unit)? = null,
cancelLiveMessage: (() -> Unit)? = null,
onMessageChange: (String) -> Unit,
textStyle: MutableState<TextStyle>
) {
val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) }
if (showCustomDisappearingMessageDialog.value) {
CustomDisappearingMessageDialog(
sendMessage = sendMessage,
setShowDialog = { showCustomDisappearingMessageDialog.value = it },
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
}
Box(Modifier.padding(vertical = 8.dp)) {
val cs = composeState.value
val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview)
@@ -90,16 +79,15 @@ fun SendMsgView(
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange)
// Disable clicks on text field
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
Box(
Modifier
.matchParentSize()
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.observer_cant_send_message_title),
text = generalGetString(R.string.observer_cant_send_message_desc)
)
})
if (cs.preview is ComposePreview.VoicePreview || !userCanSend) {
Box(Modifier
.matchParentSize()
.clickable(enabled = !userCanSend, indication = null, interactionSource = remember { MutableInteractionSource() }, onClick = {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.observer_cant_send_message_title),
text = generalGetString(R.string.observer_cant_send_message_desc)
)
})
)
}
if (showDeleteTextButton.value) {
@@ -136,11 +124,10 @@ fun SendMsgView(
else ->
RecordVoiceView(recState, stopRecOnNextClick)
}
if (sendLiveMessage != null
&& updateLiveMessage != null
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem
) {
if (sendLiveMessage != null
&& updateLiveMessage != null
&& (cs.preview !is ComposePreview.VoicePreview || !stopRecOnNextClick.value)
&& cs.contextItem is ComposeContextItem.NoContextItem) {
Spacer(Modifier.width(10.dp))
StartLiveMessageButton(userCanSend) {
if (composeState.value.preview is ComposePreview.NoPreview) {
@@ -159,53 +146,27 @@ fun SendMsgView(
val cs = composeState.value
val icon = if (cs.editing || cs.liveMessage != null) painterResource(R.drawable.ic_check_filled) else painterResource(R.drawable.ic_arrow_upward)
val disabled = !cs.sendEnabled() ||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
val showDropdown = rememberSaveable { mutableStateOf(false) }
@Composable
fun MenuItems(): List<@Composable () -> Unit> {
val menuItems = mutableListOf<@Composable () -> Unit>()
if (cs.liveMessage == null && !cs.editing) {
if (
cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
menuItems.add {
ItemAction(
generalGetString(R.string.send_live_message),
BoltFilled,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown.value = false
}
)
}
}
if (timedMessageAllowed) {
menuItems.add {
ItemAction(
generalGetString(R.string.disappearing_message),
painterResource(R.drawable.ic_timer),
onClick = {
showCustomDisappearingMessageDialog.value = true
showDropdown.value = false
}
)
}
}
}
return menuItems
}
val menuItems = MenuItems()
if (menuItems.isNotEmpty()) {
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled
if (cs.liveMessage == null &&
cs.preview !is ComposePreview.VoicePreview && !cs.editing &&
cs.contextItem is ComposeContextItem.NoContextItem &&
sendLiveMessage != null && updateLiveMessage != null
) {
val showDropdown = rememberSaveable { mutableStateOf(false) }
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true }
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() }
DefaultDropdownMenu(
showDropdown,
) {
ItemAction(
generalGetString(R.string.send_live_message),
BoltFilled,
onClick = {
startLiveMessage(scope, sendLiveMessage, updateLiveMessage, sendButtonSize, sendButtonAlpha, composeState, liveMessageAlertShown)
showDropdown.value = false
}
)
}
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage)
@@ -216,99 +177,6 @@ fun SendMsgView(
}
}
@Composable
private fun CustomDisappearingMessageDialog(
sendMessage: (Int?) -> Unit,
setShowDialog: (Boolean) -> Unit,
customDisappearingMessageTimePref: SharedPreference<Int>?
) {
val showCustomTimePicker = remember { mutableStateOf(false) }
if (showCustomTimePicker.value) {
val selectedDisappearingMessageTime = remember {
mutableStateOf(customDisappearingMessageTimePref?.get?.invoke() ?: 300)
}
CustomTimePickerDialog(
selectedDisappearingMessageTime,
title = generalGetString(R.string.delete_after),
confirmButtonText = generalGetString(R.string.send_disappearing_message_send),
confirmButtonAction = { ttl ->
sendMessage(ttl)
customDisappearingMessageTimePref?.set?.invoke(ttl)
setShowDialog(false)
},
cancel = { setShowDialog(false) }
)
} else {
@Composable
fun ChoiceButton(
text: String,
onClick: () -> Unit
) {
TextButton(onClick) {
Text(
text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
Dialog(onDismissRequest = { setShowDialog(false) }) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) {
Box(
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
generalGetString(R.string.send_disappearing_message),
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(R.drawable.ic_close),
generalGetString(R.string.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { setShowDialog(false) }
)
}
ChoiceButton(generalGetString(R.string.send_disappearing_message_30_seconds)) {
sendMessage(30)
setShowDialog(false)
}
ChoiceButton(generalGetString(R.string.send_disappearing_message_1_minute)) {
sendMessage(60)
setShowDialog(false)
}
ChoiceButton(generalGetString(R.string.send_disappearing_message_5_minutes)) {
sendMessage(300)
setShowDialog(false)
}
ChoiceButton(generalGetString(R.string.send_disappearing_message_custom_time)) {
showCustomTimePicker.value = true
}
}
}
}
}
}
}
@Composable
private fun NativeKeyboard(
composeState: MutableState<ComposeState>,
@@ -382,20 +250,14 @@ private fun NativeKeyboard(
Log.e(chat.simplex.app.TAG, e.stackTraceToString())
}
}
editText.doOnTextChanged { text, _, _, _ ->
if (!composeState.value.inProgress) {
onMessageChange(text.toString())
} else if (text.toString() != composeState.value.message) {
editText.setText(composeState.value.message)
}
}
editText.doOnTextChanged { text, _, _, _ -> onMessageChange(text.toString()) }
editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") }
editText
}) {
it.setTextColor(textColor.toArgb())
it.textSize = textStyle.value.fontSize.value
DrawableCompat.setTint(it.background, tintColor.toArgb())
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview && !cs.inProgress
it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview
it.isFocusableInTouchMode = it.isFocusable
if (cs.message != it.text.toString()) {
it.setText(cs.message)
@@ -408,7 +270,7 @@ private fun NativeKeyboard(
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false
}
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
showDeleteTextButton.value = it.lineCount >= 4
}
if (composeState.value.preview is ComposePreview.VoicePreview) {
ComposeOverlay(R.string.voice_message_send_text, textStyle, padding)
@@ -439,7 +301,7 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>)
@Composable
private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) {
val rec: Recorder = remember { RecorderNative() }
val rec: Recorder = remember { RecorderNative(MAX_VOICE_SIZE_FOR_SENDING) }
DisposableEffect(Unit) { onDispose { rec.stop() } }
val stopRecordingAndAddAudio: () -> Unit = {
recState.value.filePathNullable?.let {
@@ -587,14 +449,14 @@ private fun SendMsgButton(
sizeDp: Animatable<Float, AnimationVector1D>,
alpha: Animatable<Float, AnimationVector1D>,
enabled: Boolean,
sendMessage: (Int?) -> Unit,
sendMessage: () -> Unit,
onLongClick: (() -> Unit)? = null
) {
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = Modifier.requiredSize(36.dp)
.combinedClickable(
onClick = { sendMessage(null) },
onClick = sendMessage,
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
@@ -739,7 +601,6 @@ fun PreviewSendMsgView() {
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
sendMessage = {},
onMessageChange = { _ -> },
textStyle = textStyle
@@ -770,7 +631,6 @@ fun PreviewSendMsgViewEditing() {
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
sendMessage = {},
onMessageChange = { _ -> },
textStyle = textStyle
@@ -801,7 +661,6 @@ fun PreviewSendMsgViewInProgress() {
userIsObserver = false,
userCanSend = true,
allowVoiceToContact = {},
timedMessageAllowed = false,
sendMessage = {},
onMessageChange = { _ -> },
textStyle = textStyle

View File

@@ -175,8 +175,6 @@ fun GroupChatInfoLayout(
SectionView {
if (groupInfo.canEdit) {
EditGroupProfileButton(editGroupProfile)
}
if (groupInfo.groupProfile.description != null || groupInfo.canEdit) {
AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage)
}
GroupPreferencesButton(openPreferences)

View File

@@ -4,10 +4,7 @@ import InfoRow
import SectionBottomSpacer
import SectionDividerSpaced
import SectionSpacer
import SectionTextFooter
import SectionView
import android.net.Uri
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
@@ -16,20 +13,17 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.*
import chat.simplex.app.views.usersettings.SettingsActionItem
import kotlinx.datetime.Clock
@@ -70,15 +64,6 @@ fun GroupMemberInfoView(
}
}
},
connectViaAddress = { connReqUri ->
val uri = Uri.parse(connReqUri)
withUriAction(uri) { linkType ->
withApi {
Log.d(TAG, "connectViaUri: connecting")
connectViaUri(chatModel, linkType, uri)
}
}
},
removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) },
onRoleSelected = {
if (it == newRole.value) return@GroupMemberInfoLayout
@@ -158,21 +143,11 @@ fun GroupMemberInfoLayout(
connectionCode: String?,
getContactChat: (Long) -> Chat?,
openDirectChat: (Long) -> Unit,
connectViaAddress: (String) -> Unit,
removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
verifyClicked: () -> Unit,
) {
fun knownDirectChat(contactId: Long): Chat? {
val chat = getContactChat(contactId)
return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) {
chat
} else {
null
}
}
Column(
Modifier
.fillMaxWidth()
@@ -186,39 +161,22 @@ fun GroupMemberInfoLayout(
}
SectionSpacer()
val contactId = member.memberContactId
if (member.memberActive) {
val contactId = member.memberContactId
if (contactId != null) {
SectionView {
if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) {
val chat = getContactChat(contactId)
if ((chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) || groupInfo.fullGroupPreferences.directMessages.on) {
OpenChatButton(onClick = { openDirectChat(contactId) })
}
if (connectionCode != null) {
VerifyCodeButton(member.verified, verifyClicked)
}
}
SectionDividerSpaced()
SectionSpacer()
}
}
if (member.contactLink != null) {
val context = LocalContext.current
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { shareText(context, member.contactLink) }
if (contactId != null) {
if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
} else {
ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) })
}
SectionTextFooter(stringResource(R.string.you_can_share_this_address_with_your_contacts).format(member.displayName))
}
SectionDividerSpaced()
}
SectionView(title = stringResource(R.string.member_info_section_title_member)) {
InfoRow(stringResource(R.string.info_row_group), groupInfo.displayName)
val roles = remember { member.canChangeRoleTo(groupInfo) }
@@ -323,17 +281,6 @@ fun OpenChatButton(onClick: () -> Unit) {
)
}
@Composable
fun ConnectViaAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_link),
stringResource(R.string.connect_button),
click = onClick,
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary,
)
}
@Composable
private fun RoleSelectionRow(
roles: List<GroupMemberRole>,
@@ -393,7 +340,6 @@ fun PreviewGroupMemberInfoLayout() {
connectionCode = "123",
getContactChat = { Chat.sampleData },
openDirectChat = {},
connectViaAddress = {},
removeMember = {},
onRoleSelected = {},
switchMemberAddress = {},

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.TimedMessagesTTLPicker
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon
@@ -94,11 +95,6 @@ private fun GroupPreferencesLayout(
applyPrefs(preferences.copy(fullDelete = GroupPreference(enable = it)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
// val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.enable) }
// FeatureSection(GroupFeature.Reactions, allowReactions, groupInfo, preferences, onTTLUpdated) {
// applyPrefs(preferences.copy(reactions = GroupPreference(enable = it)))
// }
// SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.enable) }
FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) {
applyPrefs(preferences.copy(voice = GroupPreference(enable = it)))
@@ -140,15 +136,7 @@ private fun FeatureSection(
}
if (timedOn) {
val ttl = rememberSaveable(preferences.timedMessages) { mutableStateOf(preferences.timedMessages.ttl) }
DropdownCustomTimePickerSettingRow(
selection = ttl,
propagateExternalSelectionUpdate = true, // for Reset
label = generalGetString(R.string.delete_after),
dropdownValues = TimedMessagesPreference.ttlValues.filterNotNull(), // TODO in 5.2 - allow "off"
customPickerTitle = generalGetString(R.string.delete_after),
customPickerConfirmButtonText = generalGetString(R.string.custom_time_picker_select),
onSelected = onTTLUpdated
)
TimedMessagesTTLPicker(ttl, onTTLUpdated)
}
} else {
InfoRow(
@@ -158,7 +146,7 @@ private fun FeatureSection(
iconTint = iconTint,
)
if (timedOn) {
InfoRow(generalGetString(R.string.delete_after), timeText(preferences.timedMessages.ttl))
InfoRow(generalGetString(R.string.delete_after), TimedMessagesPreference.ttlText(preferences.timedMessages.ttl))
}
}
}

View File

@@ -1,35 +1,27 @@
package chat.simplex.app.views.chat.group
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionSpacer
import SectionView
import TextIconSpaced
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.delay
@Composable
fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
var gInfo by remember { mutableStateOf(groupInfo) }
val welcomeText = remember { mutableStateOf(gInfo.groupProfile.description ?: "") }
var groupInfo by remember { mutableStateOf(groupInfo) }
val welcomeText = remember { mutableStateOf(groupInfo.groupProfile.description ?: "") }
fun save(afterSave: () -> Unit = {}) {
withApi {
@@ -37,10 +29,10 @@ fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
if (welcome?.length == 0) {
welcome = null
}
val groupProfileUpdated = gInfo.groupProfile.copy(description = welcome)
val res = m.controller.apiUpdateGroup(gInfo.groupId, groupProfileUpdated)
val groupProfileUpdated = groupInfo.groupProfile.copy(description = welcome)
val res = m.controller.apiUpdateGroup(groupInfo.groupId, groupProfileUpdated)
if (res != null) {
gInfo = res
groupInfo = res
m.updateGroup(res)
welcomeText.value = welcome ?: ""
}
@@ -50,14 +42,13 @@ fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
ModalView(
close = {
if (welcomeText.value == gInfo.groupProfile.description || (welcomeText.value == "" && gInfo.groupProfile.description == null)) close()
if (welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)) close()
else showUnsavedChangesAlert({ save(close) }, close)
},
) {
GroupWelcomeLayout(
welcomeText,
gInfo,
m.controller.appPrefs.simplexLinkMode.get(),
groupInfo,
save = ::save
)
}
@@ -67,66 +58,23 @@ fun GroupWelcomeView(m: ChatModel, groupInfo: GroupInfo, close: () -> Unit) {
private fun GroupWelcomeLayout(
welcomeText: MutableState<String>,
groupInfo: GroupInfo,
linkMode: SimplexLinkMode,
save: () -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
val editMode = remember { mutableStateOf(true) }
AppBarTitle(stringResource(R.string.group_welcome_title))
val wt = rememberSaveable { welcomeText }
if (groupInfo.canEdit) {
if (editMode.value) {
val focusRequester = remember { FocusRequester() }
TextEditor(
wt,
Modifier.height(140.dp), stringResource(R.string.enter_welcome_message),
focusRequester = focusRequester
)
LaunchedEffect(Unit) {
delay(300)
focusRequester.requestFocus()
}
} else {
TextPreview(wt.value, linkMode)
}
ChangeModeButton(
editMode.value,
click = {
editMode.value = !editMode.value
},
wt.value.isEmpty()
)
CopyTextButton { copyText(SimplexApp.context, wt.value) }
SectionDividerSpaced(maxBottomPadding = false)
SaveButton(
save = save,
disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null)
)
} else {
TextPreview(wt.value, linkMode)
CopyTextButton { copyText(SimplexApp.context, wt.value) }
}
val welcomeText = remember { welcomeText }
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
SectionSpacer()
SaveButton(
save = save,
disabled = welcomeText.value == groupInfo.groupProfile.description || (welcomeText.value == "" && groupInfo.groupProfile.description == null)
)
SectionBottomSpacer()
}
}
@Composable
private fun TextPreview(text: String, linkMode: SimplexLinkMode, markdown: Boolean = true) {
Column {
SelectionContainer(Modifier.fillMaxWidth()) {
MarkdownText(
text,
formattedText = if (markdown) remember(text) { parseToMarkdown(text) } else null,
modifier = Modifier.fillMaxHeight().padding(horizontal = DEFAULT_PADDING),
linkMode = linkMode,
style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp)
)
}
}
}
@Composable
private fun SaveButton(save: () -> Unit, disabled: Boolean) {
SectionView {
@@ -136,35 +84,6 @@ private fun SaveButton(save: () -> Unit, disabled: Boolean) {
}
}
@Composable
private fun ChangeModeButton(editMode: Boolean, click: () -> Unit, disabled: Boolean) {
SectionItemView(click, disabled = disabled) {
Icon(
painterResource(if (editMode) R.drawable.ic_visibility else R.drawable.ic_edit),
contentDescription = generalGetString(R.string.edit_verb),
tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
)
TextIconSpaced()
Text(
stringResource(if (editMode) R.string.group_welcome_preview else R.string.edit_verb),
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
)
}
}
@Composable
private fun CopyTextButton(click: () -> Unit) {
SectionItemView(click) {
Icon(
painterResource(R.drawable.ic_content_copy),
contentDescription = generalGetString(R.string.copy_verb),
tint = MaterialTheme.colors.primary,
)
TextIconSpaced()
Text(stringResource(R.string.copy_verb), color = MaterialTheme.colors.primary)
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_welcome_message_question),

View File

@@ -211,7 +211,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.SndSent(), itemEdited = true),
content = CIContent.SndMsgContent(msgContent = MsgContent.MCFile("")),
quotedItem = null,
reactions = listOf(),
file = CIFile.getSample(fileStatus = CIFileStatus.SndComplete)
)
private val fileChatItemWtFile = ChatItem(
@@ -219,7 +218,6 @@ class ChatItemProvider: PreviewParameterProvider<ChatItem> {
meta = CIMeta.getSample(1, Clock.System.now(), "", CIStatus.RcvRead(), ),
content = CIContent.RcvMsgContent(msgContent = MsgContent.MCFile("")),
quotedItem = null,
reactions = listOf(),
file = null
)
override val values = listOf(

View File

@@ -24,7 +24,7 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma
Text(
chatItem.timestampText,
color = metaColor,
fontSize = 12.sp,
fontSize = 14.sp,
modifier = Modifier.padding(start = 3.dp)
)
} else {
@@ -44,7 +44,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
StatusIconText(painterResource(R.drawable.ic_timer), color)
val ttl = meta.itemTimed?.ttl
if (ttl != chatTTL) {
Text(shortTimeText(ttl), color = color, fontSize = 12.sp)
Text(TimedMessagesPreference.shortTtlText(ttl), color = color, fontSize = 13.sp)
}
Spacer(Modifier.width(4.dp))
}
@@ -57,7 +57,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) {
StatusIconText(painterResource(R.drawable.ic_circle_filled), Color.Transparent)
Spacer(Modifier.width(4.dp))
}
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
Text(meta.timestampText, color = color, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
// the conditions in this function should match CIMetaText
@@ -69,7 +69,7 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String {
res += iconSpace
val ttl = meta.itemTimed.ttl
if (ttl != chatTTL) {
res += shortTimeText(ttl)
res += TimedMessagesPreference.shortTtlText(ttl)
}
}
if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) {

View File

@@ -1,6 +1,5 @@
package chat.simplex.app.views.chat.item
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
@@ -10,19 +9,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.draw.*
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.*
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import kotlinx.coroutines.flow.distinctUntilChanged
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
@@ -36,10 +36,9 @@ fun CIVoiceView(
ci: ChatItem,
timedMessagesTTL: Int?,
longClick: () -> Unit,
receiveFile: (Long) -> Unit,
) {
Row(
Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp),
Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = 6.dp, end = 6.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (file != null) {
@@ -65,11 +64,9 @@ fun CIVoiceView(
durationText(time / 1000)
}
}
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) {
AudioPlayer.seekTo(it, progress, filePath)
}
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick)
} else {
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile)
VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick)
val metaReserve = if (edited)
" "
else
@@ -93,89 +90,39 @@ private fun VoiceLayout(
timedMessagesTTL: Int?,
play: () -> Unit,
pause: () -> Unit,
longClick: () -> Unit,
receiveFile: (Long) -> Unit,
onProgressChanged: (Int) -> Unit,
longClick: () -> Unit
) {
@Composable
fun RowScope.Slider(backgroundColor: Color, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) {
var movedManuallyTo by rememberSaveable(file.fileId) { mutableStateOf(-1) }
if (audioPlaying.value || progress.value > 0 || movedManuallyTo == progress.value) {
val dp4 = with(LocalDensity.current) { 4.dp.toPx() }
val dp10 = with(LocalDensity.current) { 10.dp.toPx() }
val primary = MaterialTheme.colors.primary
val inactiveTrackColor =
MaterialTheme.colors.primary.mixWith(
backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha),
0.24f)
val width = with(LocalDensity.current) { LocalView.current.width.toDp() }
val colors = SliderDefaults.colors(
inactiveTrackColor = inactiveTrackColor
)
Slider(
progress.value.toFloat(),
onValueChange = {
onProgressChanged(it.toInt())
movedManuallyTo = it.toInt()
},
Modifier
.size(width, 48.dp)
.weight(1f)
.padding(padding)
.drawBehind {
drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
drawRect(inactiveTrackColor, Offset(size.width - dp10, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4))
},
valueRange = 0f..duration.value.toFloat(),
colors = colors
)
LaunchedEffect(Unit) {
snapshotFlow { audioPlaying.value }
.distinctUntilChanged()
.collect {
movedManuallyTo = -1
}
}
}
}
when {
hasText -> {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
Spacer(Modifier.width(6.dp))
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
Row(verticalAlignment = Alignment.CenterVertically) {
DurationText(text, PaddingValues(start = 12.dp))
Slider(if (ci.chatDir.sent) sentColor else receivedColor)
}
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
DurationText(text, PaddingValues(start = 12.dp))
}
sent -> {
Column(horizontalAlignment = Alignment.End) {
Row {
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) {
Spacer(Modifier.height(56.dp))
Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp))
DurationText(text, PaddingValues(end = 12.dp))
}
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.height(56.dp))
DurationText(text, PaddingValues(end = 12.dp))
}
Box(Modifier.padding(top = 6.dp, end = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
}
}
}
}
else -> {
Column(horizontalAlignment = Alignment.Start) {
Row {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile)
Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) {
DurationText(text, PaddingValues(start = 12.dp))
Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp))
Spacer(Modifier.height(56.dp))
Row {
Column {
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick)
Box(Modifier.align(Alignment.CenterHorizontally).padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
}
}
Box(Modifier.padding(top = 6.dp)) {
CIMetaView(ci, timedMessagesTTL)
Row(verticalAlignment = Alignment.CenterVertically) {
DurationText(text, PaddingValues(start = 12.dp))
Spacer(Modifier.height(56.dp))
}
}
}
@@ -246,8 +193,7 @@ private fun VoiceMsgIndicator(
error: Boolean,
play: () -> Unit,
pause: () -> Unit,
longClick: () -> Unit,
receiveFile: (Long) -> Unit,
longClick: () -> Unit
) {
val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() }
val strokeColor = MaterialTheme.colors.primary
@@ -266,9 +212,8 @@ private fun VoiceMsgIndicator(
PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick)
}
} else {
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick)
} else if (file?.fileStatus is CIFileStatus.RcvTransfer
if (file?.fileStatus is CIFileStatus.RcvInvitation
|| file?.fileStatus is CIFileStatus.RcvTransfer
|| file?.fileStatus is CIFileStatus.RcvAccepted
) {
Box(

View File

@@ -2,8 +2,7 @@ package chat.simplex.app.views.chat.item
import android.Manifest
import android.os.Build
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
@@ -17,17 +16,15 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.chat.*
import chat.simplex.app.views.chat.ComposeContextItem
import chat.simplex.app.views.chat.ComposeState
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.IncognitoView
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.datetime.Clock
@@ -48,9 +45,7 @@ fun ChatItemView(
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
@@ -80,256 +75,186 @@ fun ChatItemView(
else -> {}
}
}
Column(
Modifier
.clip(RoundedCornerShape(18.dp))
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
) {
@Composable
fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
}
@Composable
fun ChatItemReactions() {
Row {
cItem.reactions.forEach { r ->
var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp))
if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) {
modifier = modifier.clickable {
setReaction(cInfo, cItem, !r.userReacted, r.reaction)
fun deleteMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.delete_message_cannot_be_undone_warning)
} else {
generalGetString(R.string.delete_message_mark_deleted_warning)
}
}
fun moderateMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.moderate_message_will_be_deleted_warning)
} else {
generalGetString(R.string.moderate_message_will_be_marked_warning)
}
}
@Composable
fun MsgContentItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
showMenu.value = false
})
}
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when {
filePath != null -> shareFile(context, cItem.text, filePath)
else -> shareText(context, cItem.content.text)
}
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, cItem.content.text)
showMenu.value = false
})
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)
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
saveImage(context, cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
}
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
else -> {}
}
showMenu.value = false
})
}
}
Row(modifier.padding(2.dp)) {
Text(r.reaction.text, fontSize = 12.sp)
if (r.totalReacted > 1) {
Spacer(Modifier.width(4.dp))
Text("${r.totalReacted}",
fontSize = 11.5.sp,
fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal,
color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
)
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
if (cItem.meta.itemDeleted != null && revealed.value) {
ItemAction(
stringResource(R.string.hide_verb),
painterResource(R.drawable.ic_visibility_off),
onClick = {
revealed.value = false
showMenu.value = false
}
)
}
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)
}
val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
}
}
}
}
Column(horizontalAlignment = if (cItem.chatDir.sent) Alignment.End else Alignment.Start) {
Column(
Modifier
.clip(RoundedCornerShape(18.dp))
.combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick),
) {
@Composable
fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem)
}
fun deleteMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.delete_message_cannot_be_undone_warning)
} else {
generalGetString(R.string.delete_message_mark_deleted_warning)
}
}
fun moderateMessageQuestionText(): String {
return if (fullDeleteAllowed) {
generalGetString(R.string.moderate_message_will_be_deleted_warning)
} else {
generalGetString(R.string.moderate_message_will_be_marked_warning)
}
}
@Composable
fun MsgReactionsMenu() {
val rs = MsgReaction.values.mapNotNull { r ->
if (null == cItem.reactions.find { it.userReacted && it.reaction.text == r.text }) {
r
} else {
null
}
}
if (rs.isNotEmpty()) {
Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState())) {
rs.forEach() { r ->
Box(
Modifier.size(36.dp).clickable {
setReaction(cInfo, cItem, true, r)
showMenu.value = false
},
contentAlignment = Alignment.Center
) {
Text(r.text)
}
}
}
}
}
@Composable
fun MsgContentItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) {
MsgReactionsMenu()
}
if (cItem.meta.itemDeleted == null && !live) {
ItemAction(stringResource(R.string.reply_verb), painterResource(R.drawable.ic_reply), onClick = {
if (composeState.value.editing) {
composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews)
} else {
composeState.value = composeState.value.copy(contextItem = ComposeContextItem.QuotedItem(cItem))
}
@Composable
fun MarkedDeletedItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(R.string.reveal_verb),
painterResource(R.drawable.ic_visibility),
onClick = {
revealed.value = true
showMenu.value = false
})
}
ItemAction(stringResource(R.string.share_verb), painterResource(R.drawable.ic_share), onClick = {
val filePath = getLoadedFilePath(SimplexApp.context, cItem.file)
when {
filePath != null -> shareFile(context, cItem.text, filePath)
else -> shareText(context, cItem.content.text)
}
showMenu.value = false
})
ItemAction(stringResource(R.string.copy_verb), painterResource(R.drawable.ic_content_copy), onClick = {
copyText(context, cItem.content.text)
showMenu.value = false
})
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)
ItemAction(stringResource(R.string.save_verb), painterResource(R.drawable.ic_download), onClick = {
when (cItem.content.msgContent) {
is MsgContent.MCImage -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) {
saveImage(context, cItem.file)
} else {
writePermissionState.launchPermissionRequest()
}
}
is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName)
else -> {}
}
showMenu.value = false
})
}
}
if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) {
ItemAction(stringResource(R.string.edit_verb), painterResource(R.drawable.ic_edit_filled), onClick = {
composeState.value = ComposeState(editingItem = cItem, useLinkPreviews = useLinkPreviews)
showMenu.value = false
})
}
if (cItem.meta.itemDeleted != null && revealed.value) {
ItemAction(
stringResource(R.string.hide_verb),
painterResource(R.drawable.ic_visibility_off),
onClick = {
revealed.value = false
showMenu.value = false
}
)
}
if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null) {
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
}
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
if (!(live && cItem.meta.isLive)) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage)
}
)
}
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable
fun MarkedDeletedItemDropdownMenu() {
DefaultDropdownMenu(showMenu) {
if (!cItem.isDeletedContent) {
ItemAction(
stringResource(R.string.reveal_verb),
painterResource(R.drawable.ic_visibility),
onClick = {
revealed.value = true
showMenu.value = false
}
)
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
}
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && !revealed.value) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
MarkedDeletedItemDropdownMenu()
@Composable
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && !revealed.value) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
MarkedDeletedItemDropdownMenu()
} else if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL)
MsgContentItemDropdownMenu()
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") })
MsgContentItemDropdownMenu()
} else {
if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL)
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile)
} else {
framedItemView()
}
} else {
framedItemView()
}
framedItemView()
MsgContentItemDropdownMenu()
}
}
@Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable fun CallItem(status: CICallStatus, duration: Int) {
CICallItemView(cInfo, cItem, status, duration, acceptCall)
}
@Composable
fun ModeratedItem() {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, showMenu, questionText = generalGetString(R.string.delete_message_cannot_be_undone_warning), deleteMessage)
}
}
when (val c = cItem.content) {
is CIContent.SndMsgContent -> ContentItem()
is CIContent.RcvMsgContent -> ContentItem()
is CIContent.SndDeleted -> DeletedItem()
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.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)
is CIContent.SndGroupEventContent -> CIEventView(cItem)
is CIContent.RcvConnEventContent -> CIEventView(cItem)
is CIContent.SndConnEventContent -> CIEventView(cItem)
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,)
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
is CIContent.SndModerated -> ModeratedItem()
is CIContent.RcvModerated -> ModeratedItem()
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
} else {
framedItemView()
MsgContentItemDropdownMenu()
}
}
if (cItem.content.msgContent != null && (cItem.meta.itemDeleted == null || revealed.value) && cItem.reactions.isNotEmpty()) {
ChatItemReactions()
@Composable fun DeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage)
}
}
@Composable fun CallItem(status: CICallStatus, duration: Int) {
CICallItemView(cInfo, cItem, status, duration, acceptCall)
}
when (val c = cItem.content) {
is CIContent.SndMsgContent -> ContentItem()
is CIContent.RcvMsgContent -> ContentItem()
is CIContent.SndDeleted -> DeletedItem()
is CIContent.RcvDeleted -> DeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.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)
is CIContent.SndGroupEventContent -> CIEventView(cItem)
is CIContent.RcvConnEventContent -> CIEventView(cItem)
is CIContent.SndConnEventContent -> CIEventView(cItem)
is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor)
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
}
is CIContent.SndChatPreference -> CIChatFeatureView(cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon,)
is CIContent.RcvGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.SndGroupFeature -> CIChatFeatureView(cItem, c.groupFeature, c.preference.enable.iconColor)
is CIContent.RcvChatFeatureRejected -> CIChatFeatureView(cItem, c.feature, Color.Red)
is CIContent.RcvGroupFeatureRejected -> CIChatFeatureView(cItem, c.groupFeature, Color.Red)
is CIContent.SndModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.RcvModerated -> MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember)
is CIContent.InvalidJSON -> CIInvalidJSONView(c.json)
}
}
}
@@ -353,24 +278,6 @@ fun CancelFileItemAction(
)
}
@Composable
fun ItemInfoAction(
cInfo: ChatInfo,
cItem: ChatItem,
showItemDetails: (ChatInfo, ChatItem) -> Unit,
showMenu: MutableState<Boolean>
) {
ItemAction(
stringResource(R.string.info_menu),
painterResource(R.drawable.ic_info),
onClick = {
showItemDetails(cInfo, cItem)
showMenu.value = false
}
)
}
@Composable
fun DeleteItemAction(
cItem: ChatItem,
@@ -523,9 +430,7 @@ fun PreviewChatItemView() {
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
acceptFeature = { _, _, _ -> }
)
}
}
@@ -546,9 +451,7 @@ fun PreviewChatItemViewDeletedContent() {
joinGroup = {},
acceptCall = { _ -> },
scrollToItem = {},
acceptFeature = { _, _, _ -> },
setReaction = { _, _, _, _ -> },
showItemDetails = { _, _ -> },
acceptFeature = { _, _, _ -> }
)
}
}

View File

@@ -221,7 +221,7 @@ fun FramedItemView(
}
}
is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile)
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") })
if (mc.text != "") {
CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler)
}

View File

@@ -19,7 +19,6 @@ import chat.simplex.app.model.CIDeleted
import chat.simplex.app.model.ChatItem
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.Clock
@Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) {
@@ -68,7 +67,7 @@ private fun MarkedDeletedText(text: String) {
fun PreviewMarkedDeletedItemView() {
SimpleXTheme {
DeletedItemView(
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted()),
null
)
}

View File

@@ -8,6 +8,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.runtime.Composable
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.SpanStyle
import androidx.compose.ui.text.font.FontWeight
@@ -17,7 +18,7 @@ import androidx.compose.ui.unit.sp
import chat.simplex.app.R
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.annotatedStringResource
import chat.simplex.app.views.onboarding.ReadableTextWithLink
import chat.simplex.app.views.helpers.openUriCatching
import chat.simplex.app.views.usersettings.MarkdownHelpView
import chat.simplex.app.views.usersettings.simplexTeamUri
@@ -28,8 +29,17 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
val uriHandler = LocalUriHandler.current
Text(stringResource(R.string.thank_you_for_installing_simplex), lineHeight = 22.sp)
ReadableTextWithLink(R.string.you_can_connect_to_simplex_chat_founder, simplexTeamUri)
Text(
annotatedStringResource(R.string.you_can_connect_to_simplex_chat_founder),
modifier = Modifier.clickable(onClick = {
uriHandler.openUriCatching(simplexTeamUri)
}),
lineHeight = 22.sp
)
Column(
Modifier.padding(top = 24.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)

View File

@@ -159,14 +159,6 @@ fun GroupMenuItems(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showM
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
}
}
GroupMemberStatus.MemAccepted -> {
if (groupInfo.membership.memberCurrent) {
LeaveGroupAction(groupInfo, chatModel, showMenu)
}
if (groupInfo.canDelete) {
DeleteGroupAction(chat, groupInfo, chatModel, showMenu)
}
}
else -> {
if (showMarkRead) {
MarkReadChatAction(chat, chatModel, showMenu)

View File

@@ -176,7 +176,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user
BackHandler(onBack = hideSearchOnBack)
}
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
if (chatModel.chats.size > 0) {
if (chatModel.chats.size >= 8) {
barButtons.add {
IconButton({ showSearch = true }) {
Icon(painterResource(R.drawable.ic_search_500), stringResource(android.R.string.search_go).capitalize(Locale.current), tint = MaterialTheme.colors.primary)

View File

@@ -35,6 +35,7 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) {
Column(
modifier = Modifier
.fillMaxSize()
.themedBackground()
) {
if (chatModel.chats.isNotEmpty()) {
ShareList(chatModel, search = searchInList)

View File

@@ -64,6 +64,7 @@ fun DatabaseView(
importArchiveAlert(m, context, uri, appFilesCountAndSize, progressIndicator)
}
}
val chatDbDeleted = remember { m.chatDbDeleted }
LaunchedEffect(m.chatRunning) {
runChat.value = m.chatRunning.value ?: true
}
@@ -82,6 +83,7 @@ fun DatabaseView(
chatArchiveName,
chatArchiveTime,
chatLastStart,
chatDbDeleted.value,
m.controller.appPrefs.privacyFullBackup,
appFilesCountAndSize,
chatItemTTL,
@@ -132,6 +134,7 @@ fun DatabaseLayout(
chatArchiveName: MutableState<String?>,
chatArchiveTime: MutableState<Instant?>,
chatLastStart: MutableState<Instant?>,
chatDbDeleted: Boolean,
privacyFullBackup: SharedPreference<Boolean>,
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
chatItemTTL: MutableState<ChatItemTTL>,
@@ -170,7 +173,7 @@ fun DatabaseLayout(
SectionDividerSpaced(maxTopPadding = true)
SectionView(stringResource(R.string.run_chat_section)) {
RunChatSetting(runChat, stopped, startChat, stopChatAlert)
RunChatSetting(runChat, stopped, chatDbDeleted, startChat, stopChatAlert)
}
SectionDividerSpaced()
@@ -178,7 +181,8 @@ fun DatabaseLayout(
val unencrypted = chatDbEncrypted == false
SettingsActionItem(
if (unencrypted) painterResource(R.drawable.ic_lock_open) else if (useKeyChain) painterResource(R.drawable.ic_vpn_key_filled)
else painterResource(R.drawable.ic_lock),
else painterResource(R
.drawable.ic_lock),
stringResource(R.string.database_passphrase),
click = showSettingsModal() { DatabaseEncryptionView(it) },
iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary,
@@ -326,6 +330,7 @@ private fun TtlOptions(current: State<ChatItemTTL>, enabled: State<Boolean>, onS
fun RunChatSetting(
runChat: Boolean,
stopped: Boolean,
chatDbDeleted: Boolean,
startChat: () -> Unit,
stopChatAlert: () -> Unit
) {
@@ -336,6 +341,7 @@ fun RunChatSetting(
iconColor = if (stopped) Color.Red else MaterialTheme.colors.primary,
) {
DefaultSwitch(
enabled = !chatDbDeleted,
checked = runChat,
onCheckedChange = { runChatSwitch ->
if (runChatSwitch) {
@@ -365,14 +371,9 @@ private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastSta
ModalManager.shared.closeModals()
return@withApi
}
if (m.currentUser.value == null) {
ModalManager.shared.closeModals()
return@withApi
} else {
m.controller.apiStartChat()
runChat.value = true
m.chatRunning.value = true
}
m.controller.apiStartChat()
runChat.value = true
m.chatRunning.value = true
val ts = Clock.System.now()
m.controller.appPrefs.chatLastStart.set(ts)
chatLastStart.value = ts
@@ -409,7 +410,7 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context:
authenticate(
generalGetString(R.string.auth_stop_chat),
generalGetString(R.string.auth_log_in_using_credential),
activity = context as FragmentActivity,
context as FragmentActivity,
completed = { laResult ->
when (laResult) {
LAResult.Success, is LAResult.Unavailable -> {
@@ -432,28 +433,18 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>, context:
private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>, context: Context) {
withApi {
try {
m.controller.apiStopChat()
runChat.value = false
stopChatAsync(m)
SimplexService.safeStopService(SimplexApp.context)
m.chatRunning.value = false
SimplexService.safeStopService(context)
MessagesFetcherWorker.cancelAll()
} catch (e: Error) {
runChat.value = true
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_stopping_chat), e.toString())
AlertManager.shared.showAlertMsg(generalGetString(R.string.error_starting_chat), e.toString())
}
}
}
suspend fun stopChatAsync(m: ChatModel) {
m.controller.apiStopChat()
m.chatRunning.value = false
}
suspend fun deleteChatAsync(m: ChatModel) {
m.controller.apiDeleteStorage()
DatabaseUtils.ksDatabasePassword.remove()
m.controller.appPrefs.storeDBPassphrase.set(true)
}
private fun exportArchive(
context: Context,
m: ChatModel,
@@ -573,17 +564,11 @@ private fun importArchive(
m.controller.apiDeleteStorage()
try {
val config = ArchiveConfig(archivePath, parentTempDirectory = context.cacheDir.toString())
val archiveErrors = m.controller.apiImportArchive(config)
m.controller.apiImportArchive(config)
DatabaseUtils.ksDatabasePassword.remove()
appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory(context))
if (archiveErrors.isEmpty()) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
}
} else {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), text = generalGetString(R.string.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(R.string.non_fatal_errors_occured_during_import))
}
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(R.string.chat_database_imported), generalGetString(R.string.restart_the_app_to_use_imported_chat_database))
}
} catch (e: Error) {
operationEnded(m, progressIndicator) {
@@ -634,7 +619,10 @@ private fun deleteChat(m: ChatModel, progressIndicator: MutableState<Boolean>) {
progressIndicator.value = true
withApi {
try {
deleteChatAsync(m)
m.controller.apiDeleteStorage()
m.chatDbDeleted.value = true
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))
}
@@ -729,6 +717,7 @@ fun PreviewDatabaseLayout() {
chatArchiveName = remember { mutableStateOf("dummy_archive") },
chatArchiveTime = remember { mutableStateOf(Clock.System.now()) },
chatLastStart = remember { mutableStateOf(Clock.System.now()) },
chatDbDeleted = false,
privacyFullBackup = SharedPreference({ true }, {}),
appFilesCountAndSize = remember { mutableStateOf(0 to 0L) },
chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) },

View File

@@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.*
@@ -44,7 +43,7 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U
}
@Composable
fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) {
fun AppBarTitle(title: String, withPadding: Boolean = true) {
val theme = CurrentColors.collectAsState()
val titleColor = CurrentColors.collectAsState().value.appColors.title
val brush = if (theme.value.base == DefaultTheme.SIMPLEX)
@@ -55,7 +54,7 @@ fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp =
title,
Modifier
.fillMaxWidth()
.padding(bottom = bottomPadding, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
.padding(bottom = DEFAULT_PADDING * 1.5f, start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h1.copy(brush = brush),
color = MaterialTheme.colors.primaryVariant,

View File

@@ -1,290 +0,0 @@
package chat.simplex.app.views.helpers
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.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import com.sd.lib.compose.wheel_picker.*
@Composable
fun CustomTimePicker(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits
) {
fun getUnitValues(unit: CustomTimeUnit, selectedValue: Int): List<Int> {
val unitLimits = timeUnitsLimits.firstOrNull { it.timeUnit == unit } ?: TimeUnitLimits.defaultUnitLimits(unit)
val regularUnitValues = (unitLimits.minValue..unitLimits.maxValue).toList()
return regularUnitValues + if (regularUnitValues.contains(selectedValue)) emptyList() else listOf(selectedValue)
}
val (unit, duration) = CustomTimeUnit.toTimeUnit(selection.value)
val selectedUnit: MutableState<CustomTimeUnit> = remember { mutableStateOf(unit) }
val selectedDuration = remember { mutableStateOf(duration) }
val selectedUnitValues = remember { mutableStateOf(getUnitValues(selectedUnit.value, selectedDuration.value)) }
val isTriggered = remember { mutableStateOf(false) }
LaunchedEffect(selectedUnit.value) {
// on initial composition, if passed selection doesn't fit into picker bounds, so that selectedDuration is bigger than selectedUnit maxValue
// (e.g., for selection = 121 seconds: selectedUnit would be Second, selectedDuration would be 121 > selectedUnit maxValue of 120),
// selectedDuration would've been replaced by maxValue - isTriggered check prevents this by skipping LaunchedEffect on initial composition
if (isTriggered.value) {
val maxValue = timeUnitsLimits.firstOrNull { it.timeUnit == selectedUnit.value }?.maxValue
if (maxValue != null && selectedDuration.value > maxValue) {
selectedDuration.value = maxValue
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
} else {
selectedUnitValues.value = getUnitValues(selectedUnit.value, selectedDuration.value)
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
} else {
isTriggered.value = true
}
}
LaunchedEffect(selectedDuration.value) {
selection.value = selectedUnit.value.toSeconds * selectedDuration.value
}
Row(
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.spacedBy(0.dp)
) {
Column(Modifier.weight(1f)) {
val durationPickerState = rememberFWheelPickerState(selectedUnitValues.value.indexOf(selectedDuration.value))
FVerticalWheelPicker(
count = selectedUnitValues.value.count(),
state = durationPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
selectedUnitValues.value[index].toString(),
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(durationPickerState) {
snapshotFlow { durationPickerState.currentIndex }
.collect {
selectedDuration.value = selectedUnitValues.value[it]
}
}
}
Column(Modifier.weight(1f)) {
val unitPickerState = rememberFWheelPickerState(timeUnitsLimits.indexOfFirst { it.timeUnit == selectedUnit.value })
FVerticalWheelPicker(
count = timeUnitsLimits.count(),
state = unitPickerState,
unfocusedCount = 2,
focus = {
FWheelPickerFocusVertical(dividerColor = MaterialTheme.colors.primary)
}
) { index ->
Text(
timeUnitsLimits[index].timeUnit.text,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
LaunchedEffect(unitPickerState) {
snapshotFlow { unitPickerState.currentIndex }
.collect {
selectedUnit.value = timeUnitsLimits[it].timeUnit
}
}
}
}
}
data class TimeUnitLimits(
val timeUnit: CustomTimeUnit,
val minValue: Int = 1,
val maxValue: Int
) {
companion object {
fun defaultUnitLimits(unit: CustomTimeUnit): TimeUnitLimits {
return when (unit) {
CustomTimeUnit.Second -> TimeUnitLimits(CustomTimeUnit.Second, maxValue = 120)
CustomTimeUnit.Minute -> TimeUnitLimits(CustomTimeUnit.Minute, maxValue = 120)
CustomTimeUnit.Hour -> TimeUnitLimits(CustomTimeUnit.Hour, maxValue = 72)
CustomTimeUnit.Day -> TimeUnitLimits(CustomTimeUnit.Day, maxValue = 60)
CustomTimeUnit.Week -> TimeUnitLimits(CustomTimeUnit.Week, maxValue = 12) // TODO in 5.2 - 54
CustomTimeUnit.Month -> TimeUnitLimits(CustomTimeUnit.Month, maxValue = 3) // TODO in 5.2 - 12
}
}
val defaultUnitsLimits: List<TimeUnitLimits>
get() = listOf(
defaultUnitLimits(CustomTimeUnit.Second),
defaultUnitLimits(CustomTimeUnit.Minute),
defaultUnitLimits(CustomTimeUnit.Hour),
defaultUnitLimits(CustomTimeUnit.Day),
defaultUnitLimits(CustomTimeUnit.Week),
defaultUnitLimits(CustomTimeUnit.Month)
)
}
}
@Composable
fun CustomTimePickerDialog(
selection: MutableState<Int>,
timeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
title: String,
confirmButtonText: String,
confirmButtonAction: (Int) -> Unit,
cancel: () -> Unit
) {
Dialog(onDismissRequest = cancel) {
Surface(
shape = RoundedCornerShape(corner = CornerSize(25.dp))
) {
Box(
contentAlignment = Alignment.Center
) {
Column(
modifier = Modifier.padding(DEFAULT_PADDING),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(" ") // centers title
Text(
title,
fontSize = 16.sp,
color = MaterialTheme.colors.secondary
)
Icon(
painterResource(R.drawable.ic_close),
generalGetString(R.string.icon_descr_close_button),
tint = MaterialTheme.colors.secondary,
modifier = Modifier
.size(25.dp)
.clickable { cancel() }
)
}
CustomTimePicker(
selection,
timeUnitsLimits
)
TextButton(onClick = { confirmButtonAction(selection.value) }) {
Text(
confirmButtonText,
fontSize = 18.sp,
color = MaterialTheme.colors.primary
)
}
}
}
}
}
}
@Composable
fun DropdownCustomTimePickerSettingRow(
selection: MutableState<Int?>,
propagateExternalSelectionUpdate: Boolean = false,
label: String,
dropdownValues: List<Int?>,
customPickerTitle: String,
customPickerConfirmButtonText: String,
customPickerTimeUnitsLimits: List<TimeUnitLimits> = TimeUnitLimits.defaultUnitsLimits,
onSelected: (Int?) -> Unit
) {
fun getValues(selectedValue: Int?): List<DropdownSelection> =
dropdownValues.map { DropdownSelection.DropdownValue(it) } +
(if (dropdownValues.contains(selectedValue)) listOf() else listOf(DropdownSelection.DropdownValue(selectedValue))) +
listOf(DropdownSelection.Custom)
val dropdownSelection: MutableState<DropdownSelection> = remember { mutableStateOf(DropdownSelection.DropdownValue(selection.value)) }
val values: MutableState<List<DropdownSelection>> = remember { mutableStateOf(getValues(selection.value)) }
val showCustomTimePicker = remember { mutableStateOf(false) }
fun updateValue(selectedValue: Int?) {
values.value = getValues(selectedValue)
dropdownSelection.value = DropdownSelection.DropdownValue(selectedValue)
onSelected(selectedValue)
}
if (propagateExternalSelectionUpdate) {
LaunchedEffect(selection.value) {
values.value = getValues(selection.value)
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
}
}
ExposedDropDownSettingRow(
label,
values.value.map { sel: DropdownSelection ->
when (sel) {
is DropdownSelection.DropdownValue -> sel to timeText(sel.value)
DropdownSelection.Custom -> sel to generalGetString(R.string.custom_time_picker_custom)
}
},
dropdownSelection,
onSelected = { sel: DropdownSelection ->
when (sel) {
is DropdownSelection.DropdownValue -> updateValue(sel.value)
DropdownSelection.Custom -> showCustomTimePicker.value = true
}
}
)
if (showCustomTimePicker.value) {
val selectedCustomTime = remember { mutableStateOf(selection.value ?: 86400) }
CustomTimePickerDialog(
selectedCustomTime,
timeUnitsLimits = customPickerTimeUnitsLimits,
title = customPickerTitle,
confirmButtonText = customPickerConfirmButtonText,
confirmButtonAction = { time ->
updateValue(time)
showCustomTimePicker.value = false
},
cancel = {
dropdownSelection.value = DropdownSelection.DropdownValue(selection.value)
showCustomTimePicker.value = false
}
)
}
}
private sealed class DropdownSelection {
data class DropdownValue(val value: Int?): DropdownSelection()
object Custom: DropdownSelection()
override fun equals(other: Any?): Boolean =
other is DropdownSelection &&
when (other) {
is DropdownValue -> this is DropdownValue && this.value == other.value
is Custom -> this is Custom
}
override fun hashCode(): Int =
// DO NOT REMOVE the as? cast as it will turn them into recursive hashCode calls
// https://youtrack.jetbrains.com/issue/KT-31239
when (this) {
is DropdownValue -> (this as? DropdownValue).hashCode()
is Custom -> (this as? Custom).hashCode()
}
}

View File

@@ -18,11 +18,9 @@ object DatabaseUtils {
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
private const val APP_PASSWORD_ALIAS: String = "appPassword"
private const val SELF_DESTRUCT_PASSWORD_ALIAS: String = "selfDestructPassword"
val ksDatabasePassword = KeyStoreItem(DATABASE_PASSWORD_ALIAS, appPreferences.encryptedDBPassphrase, appPreferences.initializationVectorDBPassphrase)
val ksAppPassword = KeyStoreItem(APP_PASSWORD_ALIAS, appPreferences.encryptedAppPassphrase, appPreferences.initializationVectorAppPassphrase)
val ksSelfDestructPassword = KeyStoreItem(SELF_DESTRUCT_PASSWORD_ALIAS, appPreferences.encryptedSelfDestructPassphrase, appPreferences.initializationVectorSelfDestructPassphrase)
class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) {
fun get(): String? {

View File

@@ -77,7 +77,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? {
}
@Composable
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) {
fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit) {
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
Row(
Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor),
@@ -109,15 +109,13 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel
)
}
}
if (cancelEnabled) {
IconButton(onClick = cancelPreview, modifier = Modifier.padding(0.dp)) {
Icon(
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_link_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
}
IconButton(onClick = cancelPreview, modifier = Modifier.padding(0.dp)) {
Icon(
painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.icon_descr_cancel_link_preview),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
}
}
}
@@ -195,7 +193,7 @@ fun PreviewChatItemLinkView() {
@Composable
fun PreviewComposeLinkView() {
SimpleXTheme {
ComposeLinkView(LinkPreview.sampleData, cancelPreview = { -> }, true)
ComposeLinkView(LinkPreview.sampleData) { -> }
}
}
@@ -203,6 +201,6 @@ fun PreviewComposeLinkView() {
@Composable
fun PreviewComposeLinkViewLoading() {
SimpleXTheme {
ComposeLinkView(null, cancelPreview = { -> }, true)
ComposeLinkView(null) { -> }
}
}

View File

@@ -28,18 +28,16 @@ data class LocalAuthRequest (
val title: String?,
val reason: String,
val password: String,
val selfDestruct: Boolean,
val completed: (LAResult) -> Unit
) {
companion object {
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "", selfDestruct = false) { }
val sample = LocalAuthRequest(generalGetString(R.string.la_enter_app_passcode), generalGetString(R.string.la_authenticate), "") { }
}
}
fun authenticate(
promptTitle: String,
promptSubtitle: String,
selfDestruct: Boolean = false,
activity: FragmentActivity,
usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(),
completed: (LAResult) -> Unit
@@ -55,13 +53,13 @@ fun authenticate(
}
LAMode.PASSCODE -> {
val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(R.string.la_no_app_password)))
ModalManager.shared.showPasscodeCustomModal { close ->
ModalManager.shared.showCustomModal(animated = false) { close ->
BackHandler {
close()
completed(LAResult.Error(generalGetString(R.string.authentication_cancelled)))
}
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && SimplexApp.context.chatModel.controller.appPrefs.selfDestruct.get()) {
LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password) {
close()
completed(it)
})

View File

@@ -56,7 +56,6 @@ class MessagesFetcherWork(
try {
withTimeout(durationSeconds * 1000L) {
val chatController = (applicationContext as SimplexApp).chatController
SimplexService.waitDbMigrationEnds(chatController)
val chatDbStatus = chatController.chatModel.chatDbStatus.value
if (chatDbStatus != DBMigrationResult.OK) {
Log.w(TAG, "Worker: problem with the database: $chatDbStatus")

View File

@@ -37,7 +37,6 @@ class ModalManager {
private val modalCount = mutableStateOf(0)
private val toRemove = mutableSetOf<Int>()
private var oldViewChanging = AtomicBoolean(false)
private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null)
fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) {
showCustomModal { close ->
@@ -52,7 +51,7 @@ class ModalManager {
}
fun showCustomModal(animated: Boolean = true, modal: @Composable (close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showCustomModal")
Log.d(TAG, "ModalManager.showModal")
// Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen.
// This is useful when invoking close() and ShowCustomModal one after another without delay. Otherwise, screen will hold prev view
if (toRemove.isNotEmpty()) {
@@ -62,11 +61,6 @@ class ModalManager {
modalCount.value = modalViews.size - toRemove.size
}
fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) {
Log.d(TAG, "ModalManager.showPasscodeCustomModal")
passcodeView.value = modal
}
fun hasModalsOpen() = modalCount.value > 0
fun closeModal() {
@@ -78,9 +72,7 @@ class ModalManager {
}
fun closeModals() {
modalViews.clear()
toRemove.clear()
modalCount.value = 0
while (modalCount.value > 0) closeModal()
}
@OptIn(ExperimentalAnimationApi::class)
@@ -108,11 +100,6 @@ class ModalManager {
}
}
@Composable
fun showPasscodeInView() {
remember { passcodeView }.value?.invoke { passcodeView.value = null }
}
/**
* Allows to modify a list without getting [ConcurrentModificationException]
* */

View File

@@ -21,7 +21,7 @@ interface Recorder {
fun stop(): Int
}
class RecorderNative(): Recorder {
class RecorderNative(private val recordedBytesLimit: Long): Recorder {
companion object {
// Allows to stop the recorder from outside without having the recorder in a variable
var stopRecording: (() -> Unit)? = null
@@ -48,8 +48,9 @@ class RecorderNative(): Recorder {
rec.setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
rec.setAudioChannels(1)
rec.setAudioSamplingRate(16000)
rec.setAudioEncodingBitRate(32000)
rec.setAudioEncodingBitRate(16000)
rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING)
rec.setMaxFileSize(recordedBytesLimit)
val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE)
val fileToSave = File.createTempFile(generateNewFileName(SimplexApp.context, "voice", "${extension}_"), ".tmp", tmpDir)
fileToSave.deleteOnExit()
@@ -273,13 +274,6 @@ object AudioPlayer {
audioPlaying.value = false
}
fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {
pro.value = ms
if (this.currentlyPlaying.value?.first == filePath) {
player.seekTo(ms)
}
}
fun duration(filePath: String): Int? {
var res: Int? = null
kotlin.runCatching {

View File

@@ -202,9 +202,9 @@ fun SectionDividerSpaced(maxTopPadding: Boolean = false, maxBottomPadding: Boole
Divider(
Modifier.padding(
start = DEFAULT_PADDING_HALF,
top = if (maxTopPadding) 37.dp else 27.dp,
top = if (maxTopPadding) 40.dp else 30.dp,
end = DEFAULT_PADDING_HALF,
bottom = if (maxBottomPadding) 37.dp else 27.dp)
bottom = if (maxBottomPadding) 40.dp else 30.dp)
)
}

View File

@@ -4,7 +4,6 @@ import android.Manifest
import android.content.*
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.compose.ManagedActivityResultLauncher
@@ -49,17 +48,6 @@ fun copyText(cxt: Context, text: String) {
clipboard?.setPrimaryClip(ClipData.newPlainText("text", text))
}
fun sendEmail(context: Context, subject: String, body: CharSequence) {
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
emailIntent.putExtra(Intent.EXTRA_TEXT, body)
try {
context.startActivity(emailIntent)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No activity was found for handling email intent")
}
}
@Composable
fun rememberSaveFileLauncher(cxt: Context, ciFile: CIFile?): ManagedActivityResultLauncher<String, Uri?> =
rememberLauncherForActivityResult(

View File

@@ -79,7 +79,7 @@ fun SimpleButtonIconEnded(
}
@Composable
fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled: Boolean = false, content: @Composable RowScope.() -> Unit) {
fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled: Boolean = false, content: @Composable () -> Unit) {
Box(Modifier.clip(RoundedCornerShape(20.dp))) {
val modifier = if (disabled) modifier else modifier.clickable { click() }
Row(

View File

@@ -1,117 +1,64 @@
package chat.simplex.app.views.helpers
import android.util.Log
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
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.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.*
import chat.simplex.app.TAG
import chat.simplex.app.chatParseMarkdown
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.MarkdownText
import com.google.accompanist.insets.navigationBarsWithImePadding
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.Serializable
import java.lang.Exception
import chat.simplex.app.ui.theme.DEFAULT_PADDING
@Composable
fun TextEditor(
value: MutableState<String>,
modifier: Modifier,
placeholder: String? = null,
contentPadding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING),
isValid: (String) -> Boolean = { true },
focusRequester: FocusRequester? = null
text: MutableState<String>,
border: Boolean = true,
fontSize: TextUnit = 14.sp,
background: Color = MaterialTheme.colors.background,
onChange: ((String) -> Unit)? = null
) {
var valid by rememberSaveable { mutableStateOf(true) }
var focused by rememberSaveable { mutableStateOf(false) }
val strokeColor by remember {
derivedStateOf {
if (valid) {
if (focused) {
CurrentColors.value.colors.secondary.copy(alpha = 0.6f)
} else {
CurrentColors.value.colors.secondary.copy(alpha = 0.3f)
BasicTextField(
value = text.value,
onValueChange = { text.value = it; onChange?.invoke(it) },
textStyle = TextStyle(
fontFamily = FontFamily.Monospace, fontSize = fontSize,
color = MaterialTheme.colors.onBackground
),
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
modifier = modifier,
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = { innerTextField ->
Surface(
shape = if (border) RoundedCornerShape(10.dp) else RectangleShape,
border = if (border) BorderStroke(1.dp, MaterialTheme.colors.secondaryVariant) else null
) {
Row(
Modifier.background(background),
verticalAlignment = Alignment.Top
) {
Box(
Modifier
.weight(1f)
.padding(vertical = 5.dp, horizontal = if (border) 7.dp else DEFAULT_PADDING)
) {
innerTextField()
}
}
} else Color.Red
}
}
}
Box(
Modifier
.fillMaxWidth()
.padding(contentPadding)
.heightIn(min = 52.dp),
// .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(26.dp)),
contentAlignment = Alignment.Center,
) {
val modifier = modifier
.fillMaxWidth()
.navigationBarsWithImePadding()
.onFocusChanged { focused = it.isFocused }
BasicTextField(
value = value.value,
onValueChange = { value.value = it },
modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester),
textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp),
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrect = false
),
singleLine = false,
maxLines = 5,
cursorBrush = SolidColor(MaterialTheme.colors.secondary),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.TextFieldDecorationBox(
value = value.value,
innerTextField = innerTextField,
placeholder = if (placeholder != null) {{ Text(placeholder, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp)) }} else null,
contentPadding = PaddingValues(),
label = null,
visualTransformation = VisualTransformation.None,
leadingIcon = null,
trailingIcon = null,
singleLine = false,
enabled = true,
isError = false,
interactionSource = remember { MutableInteractionSource() },
)
}
)
}
LaunchedEffect(Unit) {
snapshotFlow { value.value }
.distinctUntilChanged()
.collect {
valid = isValid(it)
}
}
}
@Serializable
data class ParsedFormattedText(
val formattedText: List<FormattedText>? = null
)
fun parseToMarkdown(text: String): List<FormattedText>? {
val formatted = chatParseMarkdown(text)
return try {
json.decodeFromString(ParsedFormattedText.serializer(), formatted).formattedText
} catch (e: Exception) {
Log.e(TAG, "Failed to parse into markdown: " + e.stackTraceToString())
null
}
)
}

View File

@@ -23,11 +23,9 @@ import android.view.View
import android.view.ViewTreeObserver
import android.view.inputmethod.InputMethodManager
import androidx.annotation.StringRes
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.*
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.*
@@ -35,11 +33,11 @@ import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.*
import androidx.core.content.FileProvider
import androidx.core.graphics.ColorUtils
import androidx.core.text.HtmlCompat
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.ThemeData
import chat.simplex.app.ui.theme.ThemeOverrides
import com.charleskorn.kaml.decodeFromStream
import kotlinx.coroutines.*
@@ -236,16 +234,16 @@ private fun spannableStringToAnnotatedString(
}
// maximum image file size to be auto-accepted
const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB
const val MAX_IMAGE_SIZE: Long = 236700
const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2
const val MAX_VIDEO_SIZE_AUTO_RCV: Long = 1_047_552 // 1023KB
const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE
const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 300_000
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_SMP: Long = 8000000
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB
const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824
fun getFilesDirectory(context: Context): String {
return context.filesDir.toString()
@@ -393,10 +391,10 @@ fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable
}
}
fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeOverrides? {
fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeData? {
SimplexApp.context.contentResolver.openInputStream(uri).use {
runCatching {
return yaml.decodeFromStream<ThemeOverrides>(it!!)
return yaml.decodeFromStream<ThemeData>(it!!)
}.onFailure {
if (withAlertOnException) {
AlertManager.shared.showAlertMsg(
@@ -590,9 +588,6 @@ fun Color.darker(factor: Float = 0.1f): Color =
fun Color.lighter(factor: Float = 0.1f): Color =
Color(min(red * (1 + factor), 1f), min(green * (1 + factor), 1f), min(blue * (1 + factor), 1f), alpha)
fun Color.mixWith(color: Color, alpha: Float): Color =
Color(ColorUtils.blendARGB(color.toArgb(), toArgb(), alpha))
fun ByteArray.toBase64String() = Base64.encodeToString(this, Base64.DEFAULT)
fun String.toByteArrayFromBase64() = Base64.decode(this, Base64.DEFAULT)

View File

@@ -1,80 +1,22 @@
package chat.simplex.app.views.localauth
import android.util.Log
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.*
import chat.simplex.app.model.*
import chat.simplex.app.views.database.deleteChatAsync
import chat.simplex.app.views.database.stopChatAsync
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.onboarding.OnboardingStage
import kotlinx.coroutines.delay
@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 sdPassword = ksSelfDestructPassword.get()
if (sdPassword == passcode.value && authRequest.selfDestruct) {
deleteStorageAndRestart(m, sdPassword) { r ->
authRequest.completed(r)
}
} else {
val r: LAResult = if (passcode.value == authRequest.password) LAResult.Success else LAResult.Error(generalGetString(R.string.incorrect_passcode))
authRequest.completed(r)
}
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)))
})
}
private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (LAResult) -> Unit) {
withBGApi {
try {
stopChatAsync(m)
deleteChatAsync(m)
ksAppPassword.set(password)
ksSelfDestructPassword.remove()
m.controller.ntfManager.cancelAllNotifications()
val selfDestructPref = m.controller.appPrefs.selfDestruct
val displayNamePref = m.controller.appPrefs.selfDestructDisplayName
val displayName = displayNamePref.get()
selfDestructPref.set(false)
displayNamePref.set(null)
m.chatDbChanged.value = true
m.chatDbStatus.value = null
try {
SimplexApp.context.initChatController(startChat = true)
} catch (e: Exception) {
Log.d(TAG, "initializeChat ${e.stackTraceToString()}")
}
m.chatDbChanged.value = false
if (m.currentUser.value != null) {
return@withBGApi
}
var profile: Profile? = null
if (!displayName.isNullOrEmpty()) {
profile = Profile(displayName = displayName, fullName = "")
}
val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true)
m.currentUser.value = createdUser
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
m.onboardingStage.value = OnboardingStage.OnboardingComplete
if (createdUser != null) {
m.controller.startChat(createdUser)
}
ModalManager.shared.closeModals()
AlertManager.shared.hideAlert()
completed(LAResult.Success)
} catch (e: Exception) {
completed(LAResult.Error(generalGetString(R.string.incorrect_passcode)))
}
}
}

View File

@@ -4,15 +4,11 @@ import androidx.activity.compose.BackHandler
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import chat.simplex.app.R
import chat.simplex.app.views.helpers.DatabaseUtils
import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.helpers.generalGetString
@Composable
fun SetAppPasscodeView(
passcodeKeychain: DatabaseUtils.KeyStoreItem = ksAppPassword,
title: String = generalGetString(R.string.new_passcode),
reason: String? = null,
submit: () -> Unit,
cancel: () -> Unit,
close: () -> Unit
@@ -27,7 +23,7 @@ fun SetAppPasscodeView(
close()
cancel()
}
PasscodeView(passcode, title = title, reason = reason, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) {
PasscodeView(passcode, title = title, submitLabel = submitLabel, submitEnabled = submitEnabled, submit = submit) {
close()
cancel()
}
@@ -40,7 +36,7 @@ fun SetAppPasscodeView(
submitEnabled = { pwd -> pwd == enteredPassword }
) {
if (passcode.value == enteredPassword) {
passcodeKeychain.set(passcode.value)
ksAppPassword.set(passcode.value)
enteredPassword = ""
passcode.value = ""
close()
@@ -48,7 +44,7 @@ fun SetAppPasscodeView(
}
}
} else {
SetPasswordView(title, generalGetString(R.string.save_verb)) {
SetPasswordView(generalGetString(R.string.new_passcode), generalGetString(R.string.save_verb)) {
enteredPassword = passcode.value
passcode.value = ""
confirming = true

View File

@@ -1,23 +0,0 @@
package chat.simplex.app.views.newchat
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.views.helpers.AppBarTitle
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.onboarding.ReadableTextWithLink
@Composable
fun AddContactLearnMore() {
Column(
Modifier.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.one_time_link))
ReadableText(R.string.scan_qr_to_connect_to_contact)
ReadableText(R.string.if_you_cant_meet_in_person)
ReadableTextWithLink(R.string.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/readme.html#connect-to-friends")
}
}

View File

@@ -1,8 +1,6 @@
package chat.simplex.app.views.newchat
import SectionBottomSpacer
import SectionSpacer
import SectionView
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -17,10 +15,10 @@ 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
import chat.simplex.app.R
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.usersettings.SettingsActionItem
@Composable
fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
@@ -28,92 +26,62 @@ fun AddContactView(connReqInvitation: String, connIncognito: Boolean) {
AddContactLayout(
connReq = connReqInvitation,
connIncognito = connIncognito,
share = { shareText(cxt, connReqInvitation) },
learnMore = {
ModalManager.shared.showModal {
Column(
Modifier
.fillMaxHeight()
.padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
AddContactLearnMore()
}
}
}
share = { shareText(cxt, connReqInvitation) }
)
}
@Composable
fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit, learnMore: () -> Unit) {
Column(
Modifier
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
) {
AppBarTitle(stringResource(R.string.add_contact))
OneTimeLinkProfileText(connIncognito)
SectionSpacer()
SectionView(stringResource(R.string.one_time_link_short).uppercase()) {
OneTimeLinkSection(connReq, share, learnMore)
}
SectionBottomSpacer()
}
}
@Composable
fun OneTimeLinkProfileText(connIncognito: Boolean) {
Row(Modifier.padding(horizontal = DEFAULT_PADDING)) {
InfoAboutIncognito(
connIncognito,
true,
generalGetString(R.string.incognito_random_profile_description),
generalGetString(R.string.your_profile_will_be_sent)
)
}
}
@Composable
fun ColumnScope.OneTimeLinkSection(connReq: String, share: () -> Unit, learnMore: () -> Unit) {
if (connReq.isNotEmpty()) {
QRCode(
connReq, Modifier
.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)
.aspectRatio(1f)
)
} else {
CircularProgressIndicator(
fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit) {
BoxWithConstraints {
val screenHeight = maxHeight
Column(
Modifier
.size(36.dp)
.padding(4.dp)
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.SpaceBetween,
) {
AppBarTitle(stringResource(R.string.add_contact), false)
Text(
stringResource(R.string.show_QR_code_for_your_contact_to_scan_from_the_app__multiline),
)
Row {
InfoAboutIncognito(
connIncognito,
true,
generalGetString(R.string.incognito_random_profile_description),
generalGetString(R.string.your_profile_will_be_sent)
)
}
if (connReq.isNotEmpty()) {
QRCode(
connReq, Modifier
.aspectRatio(1f)
.padding(vertical = 3.dp)
)
} else {
CircularProgressIndicator(
Modifier
.size(36.dp)
.padding(4.dp)
.align(Alignment.CenterHorizontally),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
}
Text(
annotatedStringResource(R.string.if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel),
lineHeight = 22.sp,
modifier = Modifier
.padding(top = DEFAULT_PADDING, bottom = if (screenHeight > 600.dp) DEFAULT_PADDING else 0.dp)
)
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
SimpleButton(stringResource(R.string.share_invitation_link), icon = painterResource(R.drawable.ic_share), click = share)
}
SectionBottomSpacer()
}
}
ShareLinkButton(share)
OneTimeLinkLearnMoreButton(learnMore)
}
@Composable
fun ShareLinkButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_share),
stringResource(R.string.share_invitation_link),
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
)
}
@Composable
fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_info),
stringResource(R.string.learn_more),
onClick,
)
}
@Composable
@@ -165,8 +133,7 @@ fun PreviewAddContactView() {
AddContactLayout(
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
connIncognito = false,
share = {},
learnMore = {},
share = {}
)
}
}

View File

@@ -159,7 +159,7 @@ fun CreateGroupButton(color: Color, modifier: Modifier) {
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent) {
Surface(shape = RoundedCornerShape(20.dp)) {
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
Text(stringResource(R.string.create_profile_button), style = MaterialTheme.typography.caption, color = color, fontWeight = FontWeight.Bold)
Icon(painterResource(R.drawable.ic_arrow_forward_ios), stringResource(R.string.create_profile_button), tint = color)

View File

@@ -1,7 +1,6 @@
package chat.simplex.app.views.newchat
import SectionBottomSpacer
import SectionDividerSpaced
import SectionView
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
@@ -11,7 +10,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -45,16 +43,13 @@ fun ContactConnectionInfoView(
}
}
}
val context = LocalContext.current
ContactConnectionInfoLayout(
connReq = connReqInvitation,
contactConnection,
connIncognito = contactConnection.incognito,
focusAlias,
deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) shareText(context, connReqInvitation) },
learnMore = {
showQr = {
ModalManager.shared.showModal {
Column(
Modifier
@@ -62,7 +57,7 @@ fun ContactConnectionInfoView(
.padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
AddContactLearnMore()
AddContactView(connReqInvitation ?: return@showModal, contactConnection.incognito)
}
}
}
@@ -73,12 +68,10 @@ fun ContactConnectionInfoView(
private fun ContactConnectionInfoLayout(
connReq: String?,
contactConnection: PendingContactConnection,
connIncognito: Boolean,
focusAlias: Boolean,
deleteConnection: () -> Unit,
onLocalAliasChanged: (String) -> Unit,
share: () -> Unit,
learnMore: () -> Unit,
showQr: () -> Unit,
) {
Column(
Modifier
@@ -90,6 +83,11 @@ private fun ContactConnectionInfoLayout(
else R.string.you_accepted_connection
)
)
if (contactConnection.groupLinkId == null) {
Row(Modifier.padding(bottom = DEFAULT_PADDING)) {
LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
}
}
Text(
stringResource(
if (contactConnection.viaContactUri)
@@ -99,28 +97,27 @@ private fun ContactConnectionInfoLayout(
),
Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING)
)
OneTimeLinkProfileText(connIncognito)
if (contactConnection.groupLinkId == null) {
LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
}
SectionView {
if (!connReq.isNullOrEmpty() && contactConnection.initiated) {
OneTimeLinkSection(connReq, share, learnMore)
} else {
OneTimeLinkLearnMoreButton(learnMore)
ShowQrButton(contactConnection.incognito, showQr)
}
DeleteButton(deleteConnection)
}
SectionDividerSpaced(maxBottomPadding = false)
DeleteButton(deleteConnection)
SectionBottomSpacer()
}
}
@Composable
fun ShowQrButton(incognito: Boolean, onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_qr_code),
stringResource(R.string.show_QR_code),
click = onClick,
textColor = if (incognito) Indigo else MaterialTheme.colors.primary,
iconColor = if (incognito) Indigo else MaterialTheme.colors.primary,
)
}
@Composable
fun DeleteButton(onClick: () -> Unit) {
SettingsActionItem(
@@ -150,12 +147,10 @@ private fun PreviewContactConnectionInfoView() {
ContactConnectionInfoLayout(
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D",
PendingContactConnection.getSampleData(),
connIncognito = false,
focusAlias = false,
deleteConnection = {},
onLocalAliasChanged = {},
share = {},
learnMore = {}
showQr = {},
)
}
}

View File

@@ -45,13 +45,14 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
when {
it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() -> stringResource(R.string.create_one_time_link)
it == CreateLinkTab.ONE_TIME -> stringResource(R.string.one_time_link)
it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_simplex_contact_address)
it == CreateLinkTab.LONG_TERM -> stringResource(R.string.your_contact_address)
else -> ""
}
}
Column(
Modifier
.fillMaxHeight(),
.fillMaxHeight()
.padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(Modifier.weight(1f)) {
@@ -60,7 +61,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) {
AddContactView(connReqInvitation.value ?: "", m.incognito.value)
}
CreateLinkTab.LONG_TERM -> {
UserAddressView(m, viaCreateLinkView = true, close = {})
UserAddressView(m)
}
}
}

View File

@@ -173,7 +173,7 @@ fun ActionButton(
disabled: Boolean = false,
click: () -> Unit = {}
) {
Surface(shape = RoundedCornerShape(18.dp), color = Color.Transparent) {
Surface(shape = RoundedCornerShape(18.dp)) {
Column(
Modifier
.clickable(onClick = click)

View File

@@ -85,7 +85,7 @@ fun PasteToConnectLayout(
)
Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) {
TextEditor(connectionLink, Modifier.height(180.dp), contentPadding = PaddingValues())
TextEditor(Modifier.height(180.dp), text = connectionLink)
}
Row(

View File

@@ -1,174 +0,0 @@
package chat.simplex.app.views.onboarding
import SectionBottomSpacer
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
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.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.unit.dp
import chat.simplex.app.*
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.UserContactLinkRec
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun CreateSimpleXAddress(m: ChatModel) {
val context = LocalContext.current
var progressIndicator by remember { mutableStateOf(false) }
val userAddress = remember { m.userAddress }
CreateSimpleXAddressLayout(
userAddress.value,
share = { address: String -> shareText(context, address) },
sendEmail = { address ->
sendEmail(
context,
generalGetString(R.string.email_invite_subject),
generalGetString(R.string.email_invite_body).format(address.connReqContact)
)
},
createAddress = {
withApi {
progressIndicator = true
val connReqContact = m.controller.apiCreateUserAddress()
if (connReqContact != null) {
m.userAddress.value = UserContactLinkRec(connReqContact)
// TODO uncomment in v5.2
// try {
// val u = m.controller.apiSetProfileAddress(true)
// if (u != null) {
// m.updateUser(u)
// }
// } catch (e: Exception) {
// Log.e(TAG, "CreateSimpleXAddress apiSetProfileAddress: ${e.stackTraceToString()}")
// }
progressIndicator = false
}
}
},
nextStep = {
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode)
m.onboardingStage.value = OnboardingStage.Step4_SetNotificationsMode
},
)
if (progressIndicator) {
ProgressIndicator()
}
}
@Composable
private fun CreateSimpleXAddressLayout(
userAddress: UserContactLinkRec?,
share: (String) -> Unit,
sendEmail: (UserContactLinkRec) -> Unit,
createAddress: () -> Unit,
nextStep: () -> Unit,
) {
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarTitle(stringResource(R.string.simplex_address))
Spacer(Modifier.weight(1f))
if (userAddress != null) {
QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { share(userAddress.connReqContact) }
Spacer(Modifier.weight(1f))
ShareViaEmailButton { sendEmail(userAddress) }
Spacer(Modifier.weight(1f))
ContinueButton(nextStep)
} else {
CreateAddressButton(createAddress)
// TODO remove color in v5.2
TextBelowButton(stringResource(R.string.your_contacts_will_see_it), color = Color.Transparent)
Spacer(Modifier.weight(1f))
SkipButton(nextStep)
}
SectionBottomSpacer()
}
}
@Composable
private fun CreateAddressButton(onClick: () -> Unit) {
TextButton(onClick) {
Text(stringResource(R.string.create_simplex_address), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary)
}
}
@Composable
fun ShareAddressButton(onClick: () -> Unit) {
SimpleButtonFrame(onClick) {
Icon(
painterResource(R.drawable.ic_share_filled), generalGetString(R.string.share_verb), tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 8.dp).size(18.dp)
)
Text(stringResource(R.string.share_verb), style = MaterialTheme.typography.caption, color = MaterialTheme.colors.primary)
}
}
@Composable
fun ShareViaEmailButton(onClick: () -> Unit) {
SimpleButtonFrame(onClick) {
Icon(
painterResource(R.drawable.ic_mail), generalGetString(R.string.share_verb), tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(end = 8.dp).size(30.dp)
)
Text(stringResource(R.string.invite_friends), style = MaterialTheme.typography.h6, color = MaterialTheme.colors.primary)
}
}
@Composable
private fun ContinueButton(onClick: () -> Unit) {
SimpleButtonIconEnded(stringResource(R.string.continue_to_next_step), painterResource(R.drawable.ic_chevron_right), click = onClick)
}
@Composable
private fun SkipButton(onClick: () -> Unit) {
SimpleButtonIconEnded(stringResource(R.string.dont_create_address), painterResource(R.drawable.ic_chevron_right), click = onClick)
TextBelowButton(stringResource(R.string.you_can_create_it_later))
}
@Composable
private fun TextBelowButton(text: String, color: Color = Color.Unspecified) {
// TODO remove color in v5.2
Text(
text,
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING * 3),
style = MaterialTheme.typography.subtitle1,
textAlign = TextAlign.Center,
color = color
)
}
@Composable
private fun ProgressIndicator() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
}
}

View File

@@ -4,22 +4,21 @@ import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
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.*
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.SimplexApp
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.item.MarkdownText
import chat.simplex.app.ui.theme.DEFAULT_PADDING
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.helpers.*
@Composable
@@ -34,7 +33,12 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
ReadableText(R.string.you_control_servers_to_receive_your_contacts_to_send)
ReadableText(R.string.only_client_devices_store_contacts_groups_e2e_encrypted_messages)
if (onboardingStage == null) {
ReadableTextWithLink(R.string.read_more_in_github_with_link, "https://github.com/simplex-chat/simplex-chat#readme")
val uriHandler = LocalUriHandler.current
Text(
annotatedStringResource(R.string.read_more_in_github_with_link),
modifier = Modifier.padding(bottom = 12.dp).clickable { uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#readme") },
lineHeight = 22.sp
)
} else {
ReadableText(R.string.read_more_in_github)
}
@@ -51,28 +55,8 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n
}
@Composable
fun ReadableText(@StringRes stringResId: Int, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), style: TextStyle = LocalTextStyle.current) {
Text(annotatedStringResource(stringResId), modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp, style = style)
}
@Composable
fun ReadableTextWithLink(@StringRes stringResId: Int, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
val annotated = annotatedStringResource(stringResId)
val primary = MaterialTheme.colors.primary
// This replaces links in text highlighted with specific color, e.g. SimplexBlue
val newStyles = remember(stringResId) {
val newStyles = ArrayList<AnnotatedString.Range<SpanStyle>>()
annotated.spanStyles.forEach {
if (it.item.color == SimplexBlue) {
newStyles.add(it.copy(item = it.item.copy(primary)))
} else {
newStyles.add(it)
}
}
newStyles
}
val uriHandler = LocalUriHandler.current
Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { uriHandler.openUriCatching(link) }, textAlign = textAlign, 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
@@ -80,17 +64,6 @@ fun ReadableText(text: String, textAlign: TextAlign = TextAlign.Start, padding:
Text(text, modifier = Modifier.padding(padding), textAlign = textAlign, lineHeight = 22.sp)
}
@Composable
fun ReadableMarkdownText(text: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) {
MarkdownText(
text,
formattedText = remember(text) { parseToMarkdown(text) },
modifier = Modifier.padding(padding),
style = TextStyle(textAlign = textAlign, lineHeight = 22.sp, fontSize = 16.sp),
linkMode = SimplexApp.context.chatModel.controller.appPrefs.simplexLinkMode.get(),
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,

View File

@@ -1,7 +1,9 @@
package chat.simplex.app.views.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@@ -14,8 +16,7 @@ import kotlinx.coroutines.launch
enum class OnboardingStage {
Step1_SimpleXInfo,
Step2_CreateProfile,
Step3_CreateSimpleXAddress,
Step4_SetNotificationsMode,
Step3_SetNotificationsMode,
OnboardingComplete
}
@@ -30,7 +31,8 @@ fun CreateProfile(chatModel: ChatModel, close: () -> Unit) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 20.dp)
.background(color = MaterialTheme.colors.background)
.padding(20.dp)
) {
CreateProfilePanel(chatModel, close)
LaunchedEffect(Unit) {

View File

@@ -40,16 +40,13 @@ fun SetNotificationsMode(m: ChatModel) {
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 = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
changeNotificationsMode(currentMode.value, m)
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
OnboardingActionButton(R.string.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
changeNotificationsMode(currentMode.value, m)
}
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
Spacer(Modifier.fillMaxHeight().weight(1f))
}
LaunchedEffect(Unit) {
m.controller.ntfManager.createNtfChannelsMaybeShowAlert()
}
}
@Composable
@@ -59,10 +56,10 @@ private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mod
border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)),
shape = RoundedCornerShape(35.dp),
) {
Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp)) {
Column(Modifier.padding(horizontal = 14.dp).padding(top = 4.dp, bottom = 8.dp)) {
Text(
stringResource(title),
style = MaterialTheme.typography.h3,
style = MaterialTheme.typography.h2,
fontWeight = FontWeight.Medium,
color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally),
@@ -70,7 +67,6 @@ private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mod
)
Text(annotatedStringResource(description),
Modifier.align(Alignment.CenterHorizontally),
fontSize = 15.sp,
color = MaterialTheme.colors.onBackground,
lineHeight = 24.sp,
textAlign = TextAlign.Center

View File

@@ -18,7 +18,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import chat.simplex.app.R
import chat.simplex.app.SimplexApp
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.User
import chat.simplex.app.ui.theme.*
@@ -43,13 +42,13 @@ fun SimpleXInfoLayout(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(start = DEFAULT_PADDING , end = DEFAULT_PADDING, top = 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, bottom = 10.dp), contentAlignment = Alignment.Center) {
SimpleXLogo()
}
Text(stringResource(R.string.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 48.dp).padding(horizontal = 36.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)
@@ -67,11 +66,12 @@ fun SimpleXInfoLayout(
Box(
Modifier
.fillMaxWidth()
.padding(bottom = DEFAULT_PADDING.times(1.5f), top = DEFAULT_PADDING), contentAlignment = Alignment.Center
.padding(bottom = DEFAULT_PADDING, top = DEFAULT_PADDING), contentAlignment = Alignment.Center
) {
SimpleButtonDecorated(text = stringResource(R.string.how_it_works), icon = painterResource(R.drawable.ic_info),
click = showModal { HowItWorks(user, onboardingStage) })
}
Spacer(Modifier.weight(1f))
}
}
@@ -120,7 +120,7 @@ fun OnboardingActionButton(
Modifier
.border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50))
.padding(
horizontal = DEFAULT_PADDING * 2,
horizontal = DEFAULT_PADDING * 3,
vertical = 4.dp
)
} else {
@@ -130,14 +130,13 @@ fun OnboardingActionButton(
SimpleButtonFrame(click = {
onclick?.invoke()
onboardingStage.value = onboarding
if (onboarding != null) {
SimplexApp.context.chatModel.controller.appPrefs.onboardingStage.set(onboarding)
}
}, modifier) {
Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp)
Icon(
painterResource(R.drawable.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(start = DEFAULT_PADDING.div(4)).size(20.dp)
modifier = Modifier
.padding(start = DEFAULT_PADDING, top = 5.dp)
.size(15.dp)
)
}
}

View File

@@ -20,7 +20,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.app.BuildConfig
import chat.simplex.app.R
import chat.simplex.app.model.ChatModel
@@ -43,7 +42,9 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
)
}
Column(modifier = Modifier.padding(bottom = 12.dp)) {
Column(
modifier = Modifier.padding(bottom = 12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -52,16 +53,16 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
Icon(icon, stringResource(titleId), tint = MaterialTheme.colors.secondary)
Text(
generalGetString(titleId),
maxLines = 2,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.h4,
style = MaterialTheme.typography.h3,
fontWeight = FontWeight.Medium
)
if (link != null) {
linkButton(link)
}
}
Text(generalGetString(descrId), fontSize = 15.sp)
Text(generalGetString(descrId))
}
}
@@ -113,23 +114,14 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) {
.fillMaxSize()
.padding(horizontal = DEFAULT_PADDING)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING.times(0.75f))
verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING)
) {
AppBarTitle(String.format(generalGetString(R.string.new_in_version), v.version), bottomPadding = DEFAULT_PADDING)
AppBarTitle(String.format(generalGetString(R.string.new_in_version), v.version))
v.features.forEach { feature ->
featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link)
}
val uriHandler = LocalUriHandler.current
if (v.post != null) {
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) {
Text(stringResource(R.string.whats_new_read_more), color = MaterialTheme.colors.primary,
modifier = Modifier.clickable { uriHandler.openUriCatching(v.post) })
Icon(painterResource(R.drawable.ic_open_in_new), stringResource(R.string.whats_new_read_more), tint = MaterialTheme.colors.primary)
}
}
if (!viaSettings) {
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(
@@ -161,14 +153,12 @@ private data class FeatureDescription(
private data class VersionDescription(
val version: String,
val features: List<FeatureDescription>,
val post: String? = null,
val features: List<FeatureDescription>
)
private val versionDescriptions: List<VersionDescription> = listOf(
VersionDescription(
version = "v4.2",
post = "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_verified_user,
@@ -190,7 +180,6 @@ private val versionDescriptions: List<VersionDescription> = listOf(
),
VersionDescription(
version = "v4.3",
post = "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_mic,
@@ -216,7 +205,6 @@ private val versionDescriptions: List<VersionDescription> = listOf(
),
VersionDescription(
version = "v4.4",
post = "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_timer,
@@ -242,7 +230,6 @@ private val versionDescriptions: List<VersionDescription> = listOf(
),
VersionDescription(
version = "v4.5",
post = "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_manage_accounts,
@@ -274,13 +261,12 @@ private val versionDescriptions: List<VersionDescription> = listOf(
icon = R.drawable.ic_translate,
titleId = R.string.v4_5_italian_interface,
descrId = R.string.v4_5_italian_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat"
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
),
VersionDescription(
version = "v4.6",
post = "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_lock,
@@ -311,13 +297,12 @@ private val versionDescriptions: List<VersionDescription> = listOf(
icon = R.drawable.ic_translate,
titleId = R.string.v4_6_chinese_spanish_interface,
descrId = R.string.v4_6_chinese_spanish_interface_descr,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat"
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
),
VersionDescription(
version = "v5.0",
post = "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_upload_file,
@@ -333,45 +318,7 @@ private val versionDescriptions: List<VersionDescription> = listOf(
icon = R.drawable.ic_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#help-translating-simplex-chat"
)
)
),
// Also in v5.1
// preference to disable calls per contact
// configurable SOCKS proxy port
// access welcome message via a group profile
// improve calls on lock screen
// better formatting of times and dates
VersionDescription(
version = "v5.1",
post = "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html",
features = listOf(
FeatureDescription(
icon = R.drawable.ic_add_reaction,
titleId = R.string.v5_1_message_reactions,
descrId = R.string.v5_1_message_reactions_descr
),
FeatureDescription(
icon = R.drawable.ic_chat,
titleId = R.string.v5_1_better_messages,
descrId = R.string.v5_1_better_messages_descr
),
FeatureDescription(
icon = R.drawable.ic_light_mode,
titleId = R.string.v5_1_custom_themes,
descrId = R.string.v5_1_custom_themes_descr
),
FeatureDescription(
icon = R.drawable.ic_lock,
titleId = R.string.v5_1_self_destruct_passcode,
descrId = R.string.v5_1_self_destruct_passcode_descr
),
FeatureDescription(
icon = R.drawable.ic_translate,
titleId = R.string.v5_1_japanese_portuguese_interface,
descrId = R.string.whats_new_thanks_to_users_contribute_weblate,
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat"
link = "https://github.com/simplex-chat/simplex-chat/tree/stable#translate-the-apps"
)
)
)

View File

@@ -0,0 +1,160 @@
package chat.simplex.app.views.usersettings
import SectionBottomSpacer
import SectionCustomFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.ui.res.painterResource
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
fun AcceptRequestsView(m: ChatModel, contactLink: UserContactLinkRec) {
var contactLink by remember { mutableStateOf(contactLink) }
AcceptRequestsLayout(
contactLink,
saveState = { new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState> ->
withApi {
val link = m.controller.userAddressAutoAccept(new.value.autoAccept)
if (link != null) {
contactLink = link
m.userAddress.value = link
old.value = new.value
}
}
}
)
}
@Composable
private fun AcceptRequestsLayout(
contactLink: UserContactLinkRec,
saveState: (new: MutableState<AutoAcceptState>, old: MutableState<AutoAcceptState>) -> Unit,
) {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.contact_requests))
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(contactLink)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
SectionView(stringResource(R.string.accept_requests).uppercase()) {
PreferenceToggleWithIcon(stringResource(R.string.accept_automatically), painterResource(R.drawable.ic_check), checked = autoAcceptState.value.enable) {
autoAcceptState.value = if (!it)
AutoAcceptState()
else
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText)
}
if (autoAcceptState.value.enable) {
PreferenceToggleWithIcon(
stringResource(R.string.incognito),
if (autoAcceptState.value.incognito) painterResource(R.drawable.ic_theater_comedy_filled) else painterResource(R.drawable.ic_theater_comedy),
if (autoAcceptState.value.incognito) Indigo else MaterialTheme.colors.secondary,
autoAcceptState.value.incognito,
) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText)
}
}
}
val welcomeText = remember { mutableStateOf(autoAcceptState.value.welcomeText) }
SectionCustomFooter(PaddingValues(horizontal = DEFAULT_PADDING)) {
ButtonsFooter(
cancel = {
autoAcceptState.value = autoAcceptStateSaved.value
welcomeText.value = autoAcceptStateSaved.value.welcomeText
},
save = { saveState(autoAcceptState, autoAcceptStateSaved) },
disabled = autoAcceptState.value == autoAcceptStateSaved.value
)
}
Spacer(Modifier.height(DEFAULT_PADDING))
if (autoAcceptState.value.enable) {
Text(
stringResource(R.string.section_title_welcome_message), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2,
modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp
)
TextEditor(Modifier.padding(horizontal = DEFAULT_PADDING).height(160.dp), text = welcomeText)
LaunchedEffect(welcomeText.value) {
if (welcomeText.value != autoAcceptState.value.welcomeText) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value)
}
}
}
SectionBottomSpacer()
}
}
@Composable
private fun ButtonsFooter(cancel: () -> Unit, save: () -> Unit, disabled: Boolean) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
FooterButton(painterResource(R.drawable.ic_replay), stringResource(R.string.cancel_verb), cancel, disabled)
FooterButton(painterResource(R.drawable.ic_check), stringResource(R.string.save_verb), save, disabled)
}
}
private class AutoAcceptState {
var enable: Boolean = false
private set
var incognito: Boolean = false
private set
var welcomeText: String = ""
private set
constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") {
this.enable = enable
this.incognito = incognito
this.welcomeText = welcomeText
}
constructor(contactLink: UserContactLinkRec) {
contactLink.autoAccept?.let { aa ->
enable = true
incognito = aa.acceptIncognito
aa.autoReply?.let { msg ->
welcomeText = msg.text
} ?: run {
welcomeText = ""
}
}
}
val autoAccept: AutoAccept?
get() {
if (enable) {
var autoReply: MsgContent? = null
val s = welcomeText.trim()
if (s != "") {
autoReply = MsgContent.MCText(s)
}
return AutoAccept(incognito, autoReply)
}
return null
}
override fun equals(other: Any?): Boolean {
if (other !is AutoAcceptState) return false
return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText
}
override fun hashCode(): Int {
var result = enable.hashCode()
result = 31 * result + incognito.hashCode()
result = 31 * result + welcomeText.hashCode()
return result
}
}

View File

@@ -240,8 +240,8 @@ fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) {
val theme = remember { mutableStateOf(null as String?) }
val exportThemeLauncher = rememberSaveThemeLauncher(context, theme)
SectionItemView({
val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme)
theme.value = yaml.encodeToString<ThemeOverrides>(overrides)
val themeData = ThemeManager.currentThemeData(isInDarkTheme)
theme.value = yaml.encodeToString<ThemeData>(themeData)
exportThemeLauncher.launch("simplex.theme")
}) {
Text(generalGetString(R.string.export_theme), color = colors.primary)
@@ -251,7 +251,7 @@ fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) {
if (uri != null) {
val theme = getThemeFromUri(uri)
if (theme != null) {
ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme)
ThemeManager.saveAndApplyThemeData(currentTheme.name, theme, isInDarkTheme)
}
}
}
@@ -320,10 +320,8 @@ private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) {
"es" to "Español",
"fr" to "Français",
"it" to "Italiano",
"ja" to "日本語",
"nl" to "Nederlands",
"pl" to "Polski",
"pt-BR" to "Português (Brasil)",
"ru" to "Русский",
"zh-CN" to "简体中文"
)

View File

@@ -126,30 +126,6 @@ fun SharedPreferenceToggleWithIcon(
}
}
@Composable
fun SharedPreferenceToggleWithIcon(
text: String,
icon: Painter,
onClickInfo: () -> Unit,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text(text, Modifier.padding(end = 4.dp))
Icon(
icon,
null,
Modifier.clickable(onClick = onClickInfo),
tint = MaterialTheme.colors.primary
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = checked,
onCheckedChange = onCheckedChange,
)
}
}
@Composable
fun <T>SharedPreferenceRadioButton(text: String, prefState: MutableState<T>, preference: SharedPreference<T>, value: T) {
Row(verticalAlignment = Alignment.CenterVertically) {

View File

@@ -48,13 +48,12 @@ fun NetworkAndServersView(
chatModel.userSMPServersUnsaved.value = null
}
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
NetworkAndServersLayout(
developerTools = developerTools,
networkUseSocksProxy = networkUseSocksProxy,
onionHosts = onionHosts,
sessionMode = sessionMode,
proxyPort = proxyPort,
proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } },
showModal = showModal,
showSettingsModal = showSettingsModal,
showCustomModal = showCustomModal,
@@ -62,15 +61,14 @@ fun NetworkAndServersView(
if (enable) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.network_enable_socks),
text = generalGetString(R.string.network_enable_socks_info).format(proxyPort.value),
text = generalGetString(R.string.network_enable_socks_info),
confirmText = generalGetString(R.string.confirm_verb),
onConfirm = {
withApi {
val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get())
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
chatModel.controller.apiSetNetworkConfig(NetCfg.proxyDefaults)
chatModel.controller.setNetCfg(NetCfg.proxyDefaults)
networkUseSocksProxy.value = true
onionHosts.value = conf.onionHosts
onionHosts.value = NetCfg.proxyDefaults.onionHosts
}
}
)
@@ -81,11 +79,10 @@ fun NetworkAndServersView(
confirmText = generalGetString(R.string.confirm_verb),
onConfirm = {
withApi {
val conf = NetCfg.defaults
chatModel.controller.apiSetNetworkConfig(conf)
chatModel.controller.setNetCfg(conf)
chatModel.controller.apiSetNetworkConfig(NetCfg.defaults)
chatModel.controller.setNetCfg(NetCfg.defaults)
networkUseSocksProxy.value = false
onionHosts.value = conf.onionHosts
onionHosts.value = NetCfg.defaults.onionHosts
}
}
)
@@ -209,31 +206,35 @@ fun UseSocksProxySwitch(
) {
Icon(
painterResource(R.drawable.ic_settings_ethernet),
stringResource(R.string.network_socks_toggle_use_socks_proxy),
stringResource(R.string.network_socks_toggle),
tint = MaterialTheme.colors.secondary
)
TextIconSpaced(false)
val text = buildAnnotatedString {
append(generalGetString(R.string.network_socks_toggle_use_socks_proxy) + " (")
val style = SpanStyle(color = MaterialTheme.colors.primary)
withAnnotation(tag = "PORT", annotation = generalGetString(R.string.network_proxy_port).format(proxyPort.value)) {
withStyle(style) { append(generalGetString(R.string.network_proxy_port).format(proxyPort.value)) }
}
append(")")
}
ClickableText(
text,
style = TextStyle(color = MaterialTheme.colors.onBackground, fontSize = 16.sp, fontFamily = FontFamily(Font(R.font.inter_regular))),
onClick = { offset ->
text.getStringAnnotations(tag = "PORT", start = offset, end = offset)
.firstOrNull()?.let { _ ->
showSettingsModal { SockProxySettings(it) }()
if (networkUseSocksProxy.value) {
val text = buildAnnotatedString {
append(generalGetString(R.string.network_socks_toggle_use_socks_proxy) + " (")
val style = SpanStyle(color = MaterialTheme.colors.primary)
withAnnotation(tag = "PORT", annotation = generalGetString(R.string.network_proxy_port).format(proxyPort.value)) {
withStyle(style) { append(generalGetString(R.string.network_proxy_port).format(proxyPort.value)) }
}
},
shouldConsumeEvent = { offset ->
text.getStringAnnotations(tag = "PORT", start = offset, end = offset).any()
}
)
append(")")
}
ClickableText(
text,
style = TextStyle(color = MaterialTheme.colors.onBackground, fontSize = 16.sp, fontFamily = FontFamily(Font(R.font.inter_regular))),
onClick = { offset ->
text.getStringAnnotations(tag = "PORT", start = offset, end = offset)
.firstOrNull()?.let { _ ->
showSettingsModal { SockProxySettings(it) }()
}
},
shouldConsumeEvent = { offset ->
text.getStringAnnotations(tag = "PORT", start = offset, end = offset).any()
}
)
} else {
Text(stringResource(R.string.network_socks_toggle))
}
}
DefaultSwitch(
checked = networkUseSocksProxy.value,
@@ -261,15 +262,13 @@ fun SockProxySettings(m: ChatModel) {
val save = {
withBGApi {
m.controller.appPrefs.networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
if (m.controller.appPrefs.networkUseSocksProxy.get()) {
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
}
m.controller.apiSetNetworkConfig(m.controller.getNetCfg())
}
}
SectionView {
SectionItemView {
ResetToDefaultsButton({
val reset = {
showUpdateNetworkSettingsDialog {
m.controller.appPrefs.networkProxyHostPort.set(defaultHostPort)
val newHost = defaultHostPort.split(":").first()
val newPort = defaultHostPort.split(":").last()
@@ -277,13 +276,6 @@ fun SockProxySettings(m: ChatModel) {
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
save()
}
if (m.controller.appPrefs.networkUseSocksProxy.get()) {
showUpdateNetworkSettingsDialog {
reset()
}
} else {
reset()
}
}, disabled = hostPort == defaultHostPort)
}
SectionItemView {
@@ -315,7 +307,7 @@ fun SockProxySettings(m: ChatModel) {
hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length))
portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length))
},
save = { if (m.controller.appPrefs.networkUseSocksProxy.get()) showUpdateNetworkSettingsDialog { save() } else save() },
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 ||

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.model.*
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
@Composable
@@ -71,11 +72,6 @@ private fun PreferencesLayout(
applyPrefs(preferences.copy(fullDelete = SimpleChatPreference(allow = it)))
}
SectionDividerSpaced(true, maxBottomPadding = false)
// val allowReactions = remember(preferences) { mutableStateOf(preferences.reactions.allow) }
// FeatureSection(ChatFeature.Reactions, allowReactions) {
// applyPrefs(preferences.copy(reactions = SimpleChatPreference(allow = it)))
// }
// SectionDividerSpaced(true, maxBottomPadding = false)
val allowVoice = remember(preferences) { mutableStateOf(preferences.voice.allow) }
FeatureSection(ChatFeature.Voice, allowVoice) {
applyPrefs(preferences.copy(voice = SimpleChatPreference(allow = it)))
@@ -103,10 +99,11 @@ private fun FeatureSection(feature: ChatFeature, allowFeature: State<FeatureAllo
FeatureAllowed.values().map { it to it.text },
allowFeature,
icon = feature.icon,
enabled = remember { mutableStateOf(feature != ChatFeature.Calls) },
onSelected = onSelected,
)
}
SectionTextFooter(feature.allowDescription(allowFeature.value))
SectionTextFooter(feature.allowDescription(allowFeature.value) + (if (feature == ChatFeature.Calls) generalGetString(R.string.available_in_v51) else ""))
}
@Composable

View File

@@ -6,8 +6,9 @@ import SectionItemView
import SectionTextFooter
import SectionView
import android.view.WindowManager
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -16,18 +17,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
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.helpers.DatabaseUtils.ksAppPassword
import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword
import chat.simplex.app.views.isValidDisplayName
import chat.simplex.app.views.localauth.SetAppPasscodeView
import chat.simplex.app.views.onboarding.ReadableText
enum class LAMode {
SYSTEM,
@@ -116,9 +111,6 @@ fun SimplexLockView(
val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay }
val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } }
val activity = LocalContext.current as FragmentActivity
val selfDestructPref = remember { chatModel.controller.appPrefs.selfDestruct }
val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") }
val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName }
fun resetLAEnabled(onOff: Boolean) {
chatModel.controller.appPrefs.performLA.set(onOff)
@@ -131,11 +123,6 @@ fun SimplexLockView(
laUnavailableInstructionAlert()
}
fun resetSelfDestruct() {
selfDestructPref.set(false)
ksSelfDestructPassword.remove()
}
fun toggleLAMode(toLAMode: LAMode) {
authenticate(
if (toLAMode == LAMode.SYSTEM) {
@@ -143,7 +130,7 @@ fun SimplexLockView(
} else {
generalGetString(R.string.chat_lock)
},
generalGetString(R.string.change_lock_mode), activity = activity
generalGetString(R.string.change_lock_mode), activity
) { laResult ->
when (laResult) {
is LAResult.Error -> {
@@ -153,15 +140,16 @@ fun SimplexLockView(
LAResult.Success -> {
when (toLAMode) {
LAMode.SYSTEM -> {
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity = activity, usingLAMode = toLAMode) { laResult ->
authenticate(generalGetString(R.string.auth_enable_simplex_lock), promptSubtitle = "", activity, toLAMode) { laResult ->
when (laResult) {
LAResult.Success -> {
currentLAMode.set(toLAMode)
ksAppPassword.remove()
resetSelfDestruct()
laTurnedOnAlert()
}
is LAResult.Unavailable, is LAResult.Error -> laFailedAlert()
is LAResult.Unavailable, is LAResult.Error -> {
laFailedAlert()
}
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
}
}
@@ -176,7 +164,7 @@ fun SimplexLockView(
passcodeAlert(generalGetString(R.string.passcode_set))
},
cancel = {},
close = close
close
)
}
}
@@ -188,27 +176,8 @@ fun SimplexLockView(
}
}
fun toggleSelfDestruct(selfDestruct: SharedPreference<Boolean>) {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_mode), activity = activity) { laResult ->
when (laResult) {
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> { /* Can be called multiple times on every failure */ }
LAResult.Success -> {
if (!selfDestruct.get()) {
ModalManager.shared.showCustomModal { close ->
EnableSelfDestruct(selfDestruct, close)
}
} else {
resetSelfDestruct()
}
}
is LAResult.Unavailable -> disableUnavailableLA()
}
}
}
fun changeLAPassword() {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.la_change_app_passcode), activity = activity) { laResult ->
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 ->
@@ -218,32 +187,7 @@ fun SimplexLockView(
passcodeAlert(generalGetString(R.string.passcode_changed))
}, cancel = {
passcodeAlert(generalGetString(R.string.passcode_not_changed))
}, close = close
)
}
}
}
is LAResult.Error -> laFailedAlert()
is LAResult.Failed -> {}
is LAResult.Unavailable -> disableUnavailableLA()
}
}
}
fun changeSelfDestructPassword() {
authenticate(generalGetString(R.string.la_current_app_passcode), generalGetString(R.string.change_self_destruct_passcode), activity = activity) { laResult ->
when (laResult) {
LAResult.Success -> {
ModalManager.shared.showCustomModal { close ->
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword,
submit = {
selfDestructPasscodeAlert(generalGetString(R.string.self_destruct_passcode_changed))
}, cancel = {
passcodeAlert(generalGetString(R.string.passcode_not_changed))
},
close = close
}, close
)
}
}
@@ -279,8 +223,7 @@ fun SimplexLockView(
},
cancel = {
resetLAEnabled(false)
},
close = close
}, close
)
}
}
@@ -303,53 +246,7 @@ fun SimplexLockView(
LockDelaySelector(remember { laLockDelay.state }) { laLockDelay.set(it) }
if (showChangePasscode.value && laMode.value == LAMode.PASSCODE) {
SectionItemView({ changeLAPassword() }) {
Text(
generalGetString(R.string.la_change_app_passcode),
color = MaterialTheme.colors.primary
)
}
}
}
if (performLA.value && laMode.value == LAMode.PASSCODE) {
SectionDividerSpaced()
SectionView(stringResource(R.string.self_destruct_passcode).uppercase()) {
val openInfo = {
ModalManager.shared.showModal {
SelfDestructInfoView()
}
}
SettingsActionItemWithContent(null, null, click = openInfo) {
SharedPreferenceToggleWithIcon(
stringResource(R.string.enable_self_destruct),
painterResource(R.drawable.ic_info),
openInfo,
remember { selfDestructPref.state }.value
) {
toggleSelfDestruct(selfDestructPref)
}
}
if (remember { selfDestructPref.state }.value) {
Column(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF)) {
Text(
stringResource(R.string.self_destruct_new_display_name),
fontSize = 16.sp,
modifier = Modifier.padding(bottom = DEFAULT_PADDING_HALF)
)
ProfileNameField(selfDestructDisplayName, "", ::isValidDisplayName)
LaunchedEffect(selfDestructDisplayName.value) {
val new = selfDestructDisplayName.value
if (isValidDisplayName(new) && selfDestructDisplayNamePref.get() != new) {
selfDestructDisplayNamePref.set(new)
}
}
}
SectionItemView({ changeSelfDestructPassword() }) {
Text(
stringResource(R.string.change_self_destruct_passcode),
color = MaterialTheme.colors.primary
)
}
Text(generalGetString(R.string.la_change_app_passcode))
}
}
}
@@ -358,40 +255,6 @@ fun SimplexLockView(
}
}
@Composable
private fun SelfDestructInfoView() {
Column(
Modifier.fillMaxWidth().verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING),
) {
AppBarTitle(stringResource(R.string.self_destruct), withPadding = false)
ReadableText(stringResource(R.string.if_you_enter_self_destruct_code))
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
TextListItem("1.", stringResource(R.string.all_app_data_will_be_cleared))
TextListItem("2.", stringResource(R.string.app_passcode_replaced_with_self_destruct))
TextListItem("3.", stringResource(R.string.empty_chat_profile_is_created))
}
SectionBottomSpacer()
}
}
@Composable
private fun EnableSelfDestruct(
selfDestruct: SharedPreference<Boolean>,
close: () -> Unit
) {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
SetAppPasscodeView(
passcodeKeychain = ksSelfDestructPassword, title = generalGetString(R.string.set_passcode), reason = generalGetString(R.string.enabled_self_destruct_passcode),
submit = {
selfDestruct.set(true)
selfDestructPasscodeAlert(generalGetString(R.string.self_destruct_passcode_enabled))
},
cancel = {},
close = close
)
}
}
@Composable
private fun EnableLock(performLA: MutableState<Boolean>, onCheckedChange: (Boolean) -> Unit) {
SectionItemView {
@@ -437,14 +300,6 @@ private fun LockDelaySelector(state: State<Int>, onSelected: (Int) -> Unit) {
)
}
@Composable
private fun TextListItem(n: String, text: String) {
Box {
Text(n)
Text(text, Modifier.padding(start = 20.dp))
}
}
private fun laDelayText(t: Int): String {
val m = t / 60
val s = t % 60
@@ -464,7 +319,3 @@ private fun passcodeAlert(title: String) {
text = generalGetString(R.string.la_please_remember_to_store_password)
)
}
private fun selfDestructPasscodeAlert(title: String) {
AlertManager.shared.showAlertMsg(title, generalGetString(R.string.if_you_enter_passcode_data_removed))
}

View File

@@ -27,8 +27,6 @@ import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -142,16 +140,14 @@ private fun CustomServer(
) {
val testedPreviously = remember { mutableMapOf<String, Boolean?>() }
TextEditor(
serverAddress,
Modifier.height(144.dp)
)
LaunchedEffect(Unit) {
snapshotFlow { serverAddress.value }
.distinctUntilChanged()
.collect {
testedPreviously[server.server] = server.tested
onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value]))
}
Modifier.height(144.dp),
text = serverAddress,
border = false,
fontSize = 16.sp,
background = if (isInDarkTheme()) GroupDark else MaterialTheme.colors.background
) {
testedPreviously[server.server] = server.tested
onUpdate(server.copy(server = it, tested = testedPreviously[serverAddress.value]))
}
}
SectionDividerSpaced()

View File

@@ -266,7 +266,7 @@ private fun HowToButton() {
SettingsActionItem(
painterResource(R.drawable.ic_open_in_new),
stringResource(R.string.how_to_use_your_servers),
{ uriHandler.openUriCatching("https://simplex.chat/docs/server.html") },
{ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md") },
textColor = MaterialTheme.colors.primary,
iconColor = MaterialTheme.colors.primary
)

View File

@@ -120,7 +120,7 @@ fun RTCServersLayout(
} else {
Text(stringResource(R.string.enter_one_ICE_server_per_line))
if (editRTCServers) {
TextEditor(userRTCServersStr, Modifier.height(160.dp), contentPadding = PaddingValues())
TextEditor(Modifier.height(160.dp), text = userRTCServersStr)
Row(
Modifier.fillMaxWidth(),
@@ -197,7 +197,7 @@ private fun howToButton() {
val uriHandler = LocalUriHandler.current
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.clickable { uriHandler.openUriCatching("https://simplex.chat/docs/webrtc.html#configure-mobile-apps") }
modifier = Modifier.clickable { uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat/blob/stable/docs/WEBRTC.md#configure-mobile-apps") }
) {
Text(stringResource(R.string.how_to), color = MaterialTheme.colors.primary)
Icon(

View File

@@ -8,6 +8,7 @@ import SectionView
import TextIconSpaced
import android.content.Context
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
@@ -150,7 +151,7 @@ fun SettingsLayout(
val profileHidden = rememberSaveable { mutableStateOf(false) }
SettingsActionItem(painterResource(R.drawable.ic_manage_accounts), stringResource(R.string.your_chat_profiles), { withAuth(generalGetString(R.string.auth_open_chat_profiles), generalGetString(R.string.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true)
SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() }
SettingsActionItem(painterResource(R.drawable.ic_qr_code), stringResource(R.string.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(R.drawable.ic_qr_code), stringResource(R.string.your_simplex_contact_address), showModal { CreateLinkView(it, CreateLinkTab.LONG_TERM) }, disabled = stopped, extraPadding = true)
ChatPreferencesItem(showCustomModal, stopped = stopped)
}
SectionDividerSpaced()
@@ -361,15 +362,15 @@ fun ChatLockItem(
Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, textColor: Color = MaterialTheme.colors.onBackground, stopped: Boolean = false) {
ProfileImage(size = size, image = profileOf.image, color = iconColor)
@Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, color: Color = MaterialTheme.colors.secondaryVariant, stopped: Boolean = false) {
ProfileImage(size = size, image = profileOf.image, color = color)
Spacer(Modifier.padding(horizontal = 8.dp))
Column(Modifier.height(size), verticalArrangement = Arrangement.Center) {
Text(
profileOf.displayName,
style = MaterialTheme.typography.caption,
fontWeight = FontWeight.Bold,
color = if (stopped) MaterialTheme.colors.secondary else textColor,
color = if (stopped) MaterialTheme.colors.secondary else Color.Unspecified,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -377,7 +378,7 @@ fun ChatLockItem(
Text(
profileOf.fullName,
Modifier.padding(vertical = 5.dp),
color = if (stopped) MaterialTheme.colors.secondary else textColor,
color = if (stopped) MaterialTheme.colors.secondary else Color.Unspecified,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
@@ -483,7 +484,7 @@ private fun runAuth(title: String, desc: String, context: Context, onFinish: (su
authenticate(
title,
desc,
activity = context as FragmentActivity,
context as FragmentActivity,
completed = { laResult ->
onFinish(laResult == LAResult.Success || laResult is LAResult.Unavailable)
}

View File

@@ -1,24 +0,0 @@
package chat.simplex.app.views.usersettings
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import chat.simplex.app.R
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.onboarding.ReadableText
import chat.simplex.app.views.onboarding.ReadableTextWithLink
@Composable
fun UserAddressLearnMore() {
Column(
Modifier.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.simplex_address))
ReadableText(R.string.you_can_share_your_address)
ReadableText(R.string.you_wont_lose_your_contacts_if_delete_address)
ReadableText(R.string.you_can_accept_or_reject_connection)
ReadableTextWithLink(R.string.read_more_in_user_guide_with_link, "https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address")
}
}

View File

@@ -1,425 +1,118 @@
package chat.simplex.app.views.usersettings
import SectionBottomSpacer
import SectionDividerSpaced
import SectionItemView
import SectionTextFooter
import SectionView
import android.content.res.Configuration
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.TAG
import chat.simplex.app.model.*
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.UserContactLinkRec
import chat.simplex.app.ui.theme.*
import chat.simplex.app.views.chat.ShareAddressButton
import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.QRCode
@Composable
fun UserAddressView(
chatModel: ChatModel,
viaCreateLinkView: Boolean = false,
shareViaProfile: Boolean = false,
close: () -> Unit
) {
val context = LocalContext.current
val shareViaProfile = remember { mutableStateOf(shareViaProfile) }
var progressIndicator by remember { mutableStateOf(false) }
val onCloseHandler: MutableState<(close: () -> Unit) -> Unit> = remember { mutableStateOf({ _ -> }) }
fun setProfileAddress(on: Boolean) {
progressIndicator = true
withBGApi {
try {
val u = chatModel.controller.apiSetProfileAddress(on)
if (u != null) {
chatModel.updateUser(u)
fun UserAddressView(chatModel: ChatModel) {
val cxt = LocalContext.current
UserAddressLayout(
userAddress = remember { chatModel.userAddress }.value,
createAddress = {
withApi {
val connReqContact = chatModel.controller.apiCreateUserAddress()
if (connReqContact != null) {
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
}
} catch (e: Exception) {
Log.e(TAG, "UserAddressView apiSetProfileAddress: ${e.stackTraceToString()}")
} finally {
progressIndicator = false
}
}
}
val userAddress = remember { chatModel.userAddress }
val showLayout = @Composable {
UserAddressLayout(
userAddress = userAddress.value,
shareViaProfile,
onCloseHandler,
createAddress = {
withApi {
progressIndicator = true
val connReqContact = chatModel.controller.apiCreateUserAddress()
if (connReqContact != null) {
chatModel.userAddress.value = UserContactLinkRec(connReqContact)
// TODO uncomment in v5.2
// AlertManager.shared.showAlertDialog(
// title = generalGetString(R.string.share_address_with_contacts_question),
// text = generalGetString(R.string.add_address_to_your_profile),
// confirmText = generalGetString(R.string.share_verb),
// onConfirm = {
// setProfileAddress(true)
// shareViaProfile.value = true
// }
// )
}
progressIndicator = false
}
},
learnMore = {
ModalManager.shared.showModal {
Column(
Modifier
.fillMaxHeight()
.padding(horizontal = DEFAULT_PADDING),
verticalArrangement = Arrangement.SpaceBetween
) {
UserAddressLearnMore()
}
}
},
share = { userAddress: String -> shareText(context, userAddress) },
sendEmail = { userAddress ->
sendEmail(
context,
generalGetString(R.string.email_invite_subject),
generalGetString(R.string.email_invite_body).format(userAddress.connReqContact)
)
},
setProfileAddress = ::setProfileAddress,
deleteAddress = {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_address__question),
text = if (shareViaProfile.value) generalGetString(R.string.all_your_contacts_will_remain_connected_update_sent) else generalGetString(R.string.all_your_contacts_will_remain_connected),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
progressIndicator = true
withApi {
val u = chatModel.controller.apiDeleteUserAddress()
if (u != null) {
chatModel.userAddress.value = null
chatModel.updateUser(u)
shareViaProfile.value = false
progressIndicator = false
}
}
},
destructive = true,
)
},
saveAas = { aas: AutoAcceptState, savedAAS: MutableState<AutoAcceptState> ->
withBGApi {
val address = chatModel.controller.userAddressAutoAccept(aas.autoAccept)
if (address != null) {
chatModel.userAddress.value = address
savedAAS.value = aas
}
}
},
)
}
if (viaCreateLinkView) {
showLayout()
} else {
ModalView(close = { onCloseHandler.value(close) }) {
showLayout()
}
}
if (progressIndicator) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
if (userAddress.value != null) {
Surface(Modifier.size(50.dp), color = MaterialTheme.colors.background.copy(0.9f), shape = RoundedCornerShape(50)){}
},
share = { userAddress: String -> shareText(cxt, userAddress) },
acceptRequests = {
chatModel.userAddress.value?.let { address ->
ModalManager.shared.showModal(settings = true) { AcceptRequestsView(chatModel, address) }
}
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
},
deleteAddress = {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.delete_address__question),
text = generalGetString(R.string.all_your_contacts_will_remain_connected),
confirmText = generalGetString(R.string.delete_verb),
onConfirm = {
withApi {
chatModel.controller.apiDeleteUserAddress()
chatModel.userAddress.value = null
}
},
destructive = true,
)
}
}
)
}
@Composable
private fun UserAddressLayout(
fun UserAddressLayout(
userAddress: UserContactLinkRec?,
shareViaProfile: MutableState<Boolean>,
onCloseHandler: MutableState<(close: () -> Unit) -> Unit>,
createAddress: () -> Unit,
learnMore: () -> Unit,
share: (String) -> Unit,
sendEmail: (UserContactLinkRec) -> Unit,
setProfileAddress: (Boolean) -> Unit,
deleteAddress: () -> Unit,
saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit,
acceptRequests: () -> Unit,
deleteAddress: () -> Unit
) {
Column(
Modifier.verticalScroll(rememberScrollState()),
) {
AppBarTitle(stringResource(R.string.simplex_address), false)
AppBarTitle(stringResource(R.string.your_contact_address), false)
Text(
stringResource(R.string.you_can_share_your_address_anybody_will_be_able_to_connect),
Modifier.padding(bottom = 12.dp),
lineHeight = 22.sp
)
Column(
Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceEvenly
) {
if (userAddress == null) {
SectionView {
CreateAddressButton(createAddress)
SectionTextFooter(stringResource(R.string.create_address_and_let_people_connect))
}
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
LearnMoreButton(learnMore)
}
LaunchedEffect(Unit) {
onCloseHandler.value = { close -> close() }
}
SimpleButton(stringResource(R.string.create_address), icon = painterResource(R.drawable.ic_qr_code), click = createAddress)
} else {
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
SectionView(stringResource(R.string.address_section_title).uppercase()) {
QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
ShareAddressButton { share(userAddress.connReqContact) }
ShareViaEmailButton { sendEmail(userAddress) }
// TODO uncomment in v5.2
// ShareWithContactsButton(shareViaProfile, setProfileAddress)
AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) }
LearnMoreButton(learnMore)
}
if (autoAcceptState.value.enable) {
SectionDividerSpaced()
AutoAcceptSection(autoAcceptState, autoAcceptStateSaved, saveAas)
}
SectionDividerSpaced(maxBottomPadding = false)
SectionView {
DeleteAddressButton(deleteAddress)
SectionTextFooter(stringResource(R.string.your_contacts_will_remain_connected))
}
LaunchedEffect(Unit) {
onCloseHandler.value = { close ->
if (autoAcceptState.value == autoAcceptStateSaved.value) close()
else showUnsavedChangesAlert({ saveAas(autoAcceptState.value, autoAcceptStateSaved); close() }, close)
}
QRCode(userAddress.connReqContact, Modifier.aspectRatio(1f))
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = DEFAULT_PADDING)
) {
SimpleButton(
stringResource(R.string.share_link),
icon = painterResource(R.drawable.ic_share),
click = { share(userAddress.connReqContact) })
SimpleButtonIconEnded(
stringResource(R.string.contact_requests),
icon = painterResource(R.drawable.ic_chevron_right),
click = acceptRequests
)
}
SimpleButton(
stringResource(R.string.delete_address),
icon = painterResource(R.drawable.ic_delete),
color = Color.Red,
click = deleteAddress
)
}
}
SectionBottomSpacer()
}
}
@Composable
private fun CreateAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_qr_code),
stringResource(R.string.create_simplex_address),
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
)
}
@Composable
private fun LearnMoreButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_info),
stringResource(R.string.learn_more_about_address),
onClick,
)
}
@Composable
fun ShareViaEmailButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_mail),
stringResource(R.string.invite_friends),
onClick,
iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary,
)
}
@Composable
fun ShareWithContactsButton(shareViaProfile: MutableState<Boolean>, setProfileAddress: (Boolean) -> Unit) {
PreferenceToggleWithIcon(
stringResource(R.string.share_with_contacts),
painterResource(R.drawable.ic_person),
checked = shareViaProfile.value,
) { on ->
shareViaProfile.value = on
if (on) {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.share_address_with_contacts_question),
text = generalGetString(R.string.profile_update_will_be_sent_to_contacts),
confirmText = generalGetString(R.string.share_verb),
onConfirm = {
setProfileAddress(on)
},
onDismiss = {
shareViaProfile.value = !on
},
onDismissRequest = {
shareViaProfile.value = !on
})
} else {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.stop_sharing_address),
text = generalGetString(R.string.profile_update_will_be_sent_to_contacts),
confirmText = generalGetString(R.string.stop_sharing),
onConfirm = {
setProfileAddress(on)
},
onDismiss = {
shareViaProfile.value = !on
},
onDismissRequest = {
shareViaProfile.value = !on
})
}
}
}
@Composable
private fun AutoAcceptToggle(autoAcceptState: MutableState<AutoAcceptState>, saveAas: (AutoAcceptState) -> Unit) {
PreferenceToggleWithIcon(stringResource(R.string.auto_accept_contact), painterResource(R.drawable.ic_check), checked = autoAcceptState.value.enable) {
autoAcceptState.value = if (!it)
AutoAcceptState()
else
AutoAcceptState(it, autoAcceptState.value.incognito, autoAcceptState.value.welcomeText)
saveAas(autoAcceptState.value)
}
}
@Composable
private fun DeleteAddressButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(R.drawable.ic_delete),
stringResource(R.string.delete_address),
onClick,
iconColor = MaterialTheme.colors.error,
textColor = MaterialTheme.colors.error,
)
}
private class AutoAcceptState {
var enable: Boolean = false
private set
var incognito: Boolean = false
private set
var welcomeText: String = ""
private set
constructor(enable: Boolean = false, incognito: Boolean = false, welcomeText: String = "") {
this.enable = enable
this.incognito = incognito
this.welcomeText = welcomeText
}
constructor(contactLink: UserContactLinkRec) {
contactLink.autoAccept?.let { aa ->
enable = true
incognito = aa.acceptIncognito
aa.autoReply?.let { msg ->
welcomeText = msg.text
} ?: run {
welcomeText = ""
}
}
}
val autoAccept: AutoAccept?
get() {
if (enable) {
var autoReply: MsgContent? = null
val s = welcomeText.trim()
if (s != "") {
autoReply = MsgContent.MCText(s)
}
return AutoAccept(incognito, autoReply)
}
return null
}
override fun equals(other: Any?): Boolean {
if (other !is AutoAcceptState) return false
return this.enable == other.enable && this.incognito == other.incognito && this.welcomeText == other.welcomeText
}
override fun hashCode(): Int {
var result = enable.hashCode()
result = 31 * result + incognito.hashCode()
result = 31 * result + welcomeText.hashCode()
return result
}
}
@Composable
private fun AutoAcceptSection(
autoAcceptState: MutableState<AutoAcceptState>,
savedAutoAcceptState: MutableState<AutoAcceptState>,
saveAas: (AutoAcceptState, MutableState<AutoAcceptState>) -> Unit
) {
SectionView(stringResource(R.string.auto_accept_contact).uppercase()) {
AcceptIncognitoToggle(autoAcceptState)
WelcomeMessageEditor(autoAcceptState)
SaveAASButton(autoAcceptState.value == savedAutoAcceptState.value) { saveAas(autoAcceptState.value, savedAutoAcceptState) }
}
}
@Composable
private fun AcceptIncognitoToggle(autoAcceptState: MutableState<AutoAcceptState>) {
PreferenceToggleWithIcon(
stringResource(R.string.accept_contact_incognito_button),
if (autoAcceptState.value.incognito) painterResource(R.drawable.ic_theater_comedy_filled) else painterResource(R.drawable.ic_theater_comedy),
if (autoAcceptState.value.incognito) Indigo else MaterialTheme.colors.secondary,
autoAcceptState.value.incognito,
) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, it, autoAcceptState.value.welcomeText)
}
}
@Composable
private fun WelcomeMessageEditor(autoAcceptState: MutableState<AutoAcceptState>) {
val welcomeText = rememberSaveable { mutableStateOf(autoAcceptState.value.welcomeText) }
TextEditor(welcomeText, Modifier.height(100.dp), placeholder = stringResource(R.string.enter_welcome_message_optional))
LaunchedEffect(welcomeText.value) {
if (welcomeText.value != autoAcceptState.value.welcomeText) {
autoAcceptState.value = AutoAcceptState(autoAcceptState.value.enable, autoAcceptState.value.incognito, welcomeText.value)
}
}
}
@Composable
private fun SaveAASButton(disabled: Boolean, onClick: () -> Unit) {
SectionItemView(onClick, disabled = disabled) {
Text(stringResource(R.string.save_verb), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
}
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -433,27 +126,12 @@ fun PreviewUserAddressLayoutNoAddress() {
userAddress = null,
createAddress = {},
share = { _ -> },
acceptRequests = {},
deleteAddress = {},
saveAas = { _, _ -> },
setProfileAddress = { _ -> },
learnMore = {},
shareViaProfile = remember { mutableStateOf(false) },
onCloseHandler = remember { mutableStateOf({}) },
sendEmail = {},
)
}
}
private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) {
AlertManager.shared.showAlertDialogStacked(
title = generalGetString(R.string.save_settings_question),
confirmText = generalGetString(R.string.save_auto_accept_settings),
dismissText = generalGetString(R.string.exit_without_saving),
onConfirm = save,
onDismiss = revert,
)
}
@Preview(showBackground = true)
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
@@ -467,13 +145,8 @@ fun PreviewUserAddressLayoutAddressCreated() {
userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"),
createAddress = {},
share = { _ -> },
acceptRequests = {},
deleteAddress = {},
saveAas = { _, _ -> },
setProfileAddress = { _ -> },
learnMore = {},
shareViaProfile = remember { mutableStateOf(false) },
onCloseHandler = remember { mutableStateOf({}) },
sendEmail = {},
)
}
}

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.dp"
android:height="24.dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480.33,875Q398.63,875 326.7,843.74Q254.78,812.47 201.14,758.74Q147.5,705 116.25,633.21Q85,561.42 85,479.8Q85,398.09 116.36,325.93Q147.73,253.78 201.13,200.39Q254.54,147 326.79,116Q399.04,85 479.92,85Q526.76,85 570.69,95.51Q614.62,106.03 654,125Q650.5,133.5 649.25,142.08Q648,150.67 648,160Q648,167.9 648.75,174.95Q649.5,182 652,189Q614,166.5 570.93,154.5Q527.86,142.5 479.9,142.5Q339.69,142.5 241.09,240.75Q142.5,339 142.5,479.51Q142.5,620.03 241.24,718.76Q339.97,817.5 480.49,817.5Q621,817.5 719.25,718.91Q817.5,620.31 817.5,480Q817.5,441.47 809.25,404.99Q801,368.5 786,336Q796.96,343.76 810.93,347.88Q824.9,352 840,352Q843.36,352 846.75,352Q850.14,352 853.5,351.5Q864,382 869.5,413.93Q875,445.87 875,479.82Q875,560.91 843.99,633.3Q812.97,705.68 759.49,758.99Q706,812.3 633.98,843.65Q561.95,875 480.33,875ZM624.45,426.5Q647.4,426.5 662.45,411.5Q677.5,396.49 677.5,373.55Q677.5,350.6 662.5,335.55Q647.49,320.5 624.55,320.5Q601.6,320.5 586.55,335.5Q571.5,350.51 571.5,373.45Q571.5,396.4 586.5,411.45Q601.51,426.5 624.45,426.5ZM335.45,426.5Q358.4,426.5 373.45,411.5Q388.5,396.49 388.5,373.55Q388.5,350.6 373.5,335.55Q358.49,320.5 335.55,320.5Q312.6,320.5 297.55,335.5Q282.5,350.51 282.5,373.45Q282.5,396.4 297.5,411.45Q312.51,426.5 335.45,426.5ZM480,696Q544.5,696 599.25,661.5Q654,627 679,566.5L281,566.5Q307,627 361.25,661.5Q415.5,696 480,696ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM811.5,188.5L760,188.5Q747.75,188.5 739.63,180.33Q731.5,172.15 731.5,159.82Q731.5,147.5 739.63,139.25Q747.75,131 760,131L811.5,131L811.5,80Q811.5,67.75 819.67,59.38Q827.85,51 840.17,51Q852.5,51 860.75,59.38Q869,67.75 869,80L869,131L920,131Q932.25,131 940.63,139.43Q949,147.85 949,160.17Q949,172.5 940.63,180.5Q932.25,188.5 920,188.5L869,188.5L869,240Q869,252.25 860.58,260.38Q852.15,268.5 839.83,268.5Q827.5,268.5 819.5,260.38Q811.5,252.25 811.5,240L811.5,188.5Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24.dp"
android:height="24.dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M811.5,188.5L760,188.5Q748.5,188.5 740,180.33Q731.5,172.15 731.5,159.82Q731.5,147.5 739.63,139.25Q747.75,131 760,131L811.5,131L811.5,80Q811.5,67.75 819.67,59.38Q827.85,51 840.17,51Q852.5,51 860.75,59.38Q869,67.75 869,80L869,131L920,131Q932.25,131 940.63,139.43Q949,147.85 949,160.17Q949,172.5 940.63,180.5Q932.25,188.5 920,188.5L869,188.5L869,240Q869,251.5 860.58,260Q852.15,268.5 839.83,268.5Q827.5,268.5 819.5,260.38Q811.5,252.25 811.5,240L811.5,188.5ZM480.33,875Q398.63,875 326.7,843.74Q254.78,812.47 201.14,758.74Q147.5,705 116.25,633.21Q85,561.42 85,479.8Q85,398.09 116.36,325.93Q147.73,253.78 201.13,200.39Q254.54,147 326.79,116Q399.04,85 479.92,85Q526.76,85 570.69,95.51Q614.62,106.03 654,125Q650.5,133.5 649.25,142.08Q648,150.67 648,159.57Q648,198 671.44,228.46Q694.88,258.93 732.5,267.5Q741.57,305.17 772.03,328.58Q802.48,352 840.28,352Q843.36,352 846.75,352Q850.14,352 853.5,351.5Q864,382 869.5,413.93Q875,445.87 875,479.82Q875,560.91 843.99,633.3Q812.97,705.68 759.49,758.99Q706,812.3 633.98,843.65Q561.95,875 480.33,875ZM624.45,426.5Q647.4,426.5 662.45,411.5Q677.5,396.49 677.5,373.55Q677.5,350.6 662.5,335.55Q647.49,320.5 624.55,320.5Q601.6,320.5 586.55,335.5Q571.5,350.51 571.5,373.45Q571.5,396.4 586.5,411.45Q601.51,426.5 624.45,426.5ZM335.45,426.5Q358.4,426.5 373.45,411.5Q388.5,396.49 388.5,373.55Q388.5,350.6 373.5,335.55Q358.49,320.5 335.55,320.5Q312.6,320.5 297.55,335.5Q282.5,350.51 282.5,373.45Q282.5,396.4 297.5,411.45Q312.51,426.5 335.45,426.5ZM480,696Q544.5,696 599.25,661.5Q654,627 679,566.5L281,566.5Q307,627 361.25,661.5Q415.5,696 480,696Z"/>
</vector>

View File

@@ -5,5 +5,5 @@
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480,520.5L271,729.5Q262,738.5 250.75,738.5Q239.5,738.5 230.5,729.5Q221.5,720.5 221.5,709.25Q221.5,698 230.5,689L440,479.5L230.5,270Q221.5,261.5 221.5,250.25Q221.5,239 230.5,230Q239.5,221 250.75,221Q262,221 271,230L480,439.5L689,230.5Q698,221.5 709.25,221.5Q720.5,221.5 729.5,230.5Q738.5,239.5 738.5,250.75Q738.5,262 729.5,271L520.5,480L730,689.5Q738.5,698.5 738.5,709.75Q738.5,721 730,729.5Q721,738.5 709.75,738.5Q698.5,738.5 689.5,729.5L480,520.5Z"/>
android:pathData="m480,520.5 l-209,209q-9,9 -20.25,9t-20.25,-9q-9,-9 -9,-20.25t9,-20.25L440,479.5 230.5,270q-9,-8.5 -9,-19.75t9,-20.25q9,-9 20.25,-9t20.25,9l209,209.5 209,-209q9,-9 20.25,-9t20.25,9q9,9 9,20.25t-9,20.25l-209,209L730,689.5q8.5,9 8.5,20.25T730,729.5q-9,9 -20.25,9t-20.25,-9L480,520.5Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M480.02,479.5q-65.52,0 -106.52,-40.98t-41,-106.5q0,-65.52 40.98,-106.52t106.5,-41q65.52,0 106.52,40.98t41,106.5q0,65.52 -40.98,106.52t-106.5,41ZM738,793.5L222,793.5q-23.72,0 -40.61,-16.89Q164.5,759.72 164.5,736v-33.51q0,-37.49 18.75,-63.99t48.43,-40.17Q298,569 358.5,554T480,539q61,0 121,15.25t126.4,44.43q30.82,13.64 49.46,39.85Q795.5,664.75 795.5,702.47v33.77q0,23.45 -16.89,40.36Q761.72,793.5 738,793.5ZM222,736h516v-33.37q0,-16.32 -9.75,-30.97Q718.5,657 705,650q-64,-30.5 -116.29,-42t-108.57,-11.5Q423.5,596.5 370.5,608t-116,42q-14,7 -23.25,21.73T222,702.74L222,736ZM480,422q39,0 64.5,-25.5T570,332q0,-39 -25.5,-64.5T480,242q-39,0 -64.5,25.5T390,332q0,39 25.5,64.5T480,422ZM480,332ZM480,736Z"/>
</vector>

View File

@@ -30,7 +30,8 @@
<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 على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار.</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>

View File

@@ -9,17 +9,18 @@
<string name="network_settings">Pokročilá nastavení sítě</string>
<string name="accept">Přijmout</string>
<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 %d\? Před povolením této možnosti musí být spuštěna proxy.</string>
<string name="network_enable_socks_info">Přistupovat k serverům přes SOCKS proxy na portu 9050\? Před povolením této možnosti musí být spuštěna proxy.</string>
<string name="accept_feature">Přijmout</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Povolte svým kontaktům odesílat mizící zprávy.</string>
<string name="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>
<string name="allow_verb">Povolit</string>
<string name="allow_voice_messages_question">Povolit hlasové zprávy\?</string>
<string name="about_simplex">O SimpleX</string>
<string name="a_plus_b">a + b</string>
<string name="accept_call_on_lock_screen">Přijmout</string>
<string name="chat_item_ttl_day">1 den</string>
<string name="chat_item_ttl_day">1 dni</string>
<string name="group_member_role_admin">správce</string>
<string name="users_add">Přidat profil</string>
<string name="users_delete_all_chats_deleted">Všechny chaty a zprávy budou smazány tuto akci nelze vrátit zpět!</string>
@@ -78,7 +79,7 @@
<string name="ttl_s">%ds</string>
<string name="ttl_min">%d min</string>
<string name="ttl_hour">%d hodinu</string>
<string name="feature_offered_item_with_param">nabízeno %s: %2s</string>
<string name="feature_offered_item_with_param">offered %s: %2s</string>
<string name="v4_2_group_links">Odkazy na skupiny</string>
<string name="v4_3_voice_messages">Hlasové zprávy</string>
<string name="v4_3_irreversible_message_deletion_desc">Vaše kontakty mohou povolit úplné mazání zpráv.</string>
@@ -128,7 +129,7 @@
<string name="error_sending_message">Chyba odesílání zprávy</string>
<string name="error_adding_members">Chyba přidávání členů</string>
<string name="contact_already_exists">Kontakt již existuje</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Jste již připojeni k <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="invalid_connection_link">Neplatný odkaz na spojení</string>
<string name="error_accepting_contact_request">Chyba příjmu požadavku od kontaktu</string>
<string name="error_changing_address">Chyba změny adresy</string>
@@ -240,6 +241,8 @@
<string name="network_session_mode_entity">Připojení</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="create_address">Vytvořit adresu</string>
<string name="accept_automatically">Automaticky</string>
<string name="section_title_welcome_message">UVÍTACÍ ZPRÁVA</string>
<string name="save_and_notify_group_members">Uložit a upozornit členy skupiny</string>
<string name="exit_without_saving">Ukončit bez uložení</string>
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Platforma pro zasílání zpráv a aplikace chránící vaše soukromí a bezpečnost.</string>
@@ -375,6 +378,7 @@
<string name="delete_pending_connection__question">Smazat čekající připojení\?</string>
<string name="icon_descr_settings">Nastavení</string>
<string name="image_descr_qr_code">QR kód</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Váš kontakt může z aplikace naskenovat QR kód.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Pokud se nemůžete setkat osobně, ukažte ve <b>videohovoru QR kód</b> nebo sdílejte odkaz.</string>
<string name="scan_code">Skenovat kód</string>
<string name="incorrect_code">Nesprávný bezpečnostní kód!</string>
@@ -383,7 +387,7 @@
<string name="clear_verification">Jasné ověření</string>
<string name="to_verify_compare">Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</string>
<string name="your_settings">Vaše nastavení</string>
<string name="your_simplex_contact_address">Vaše SimpleX adresa</string>
<string name="your_simplex_contact_address">Vaše adresa <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="database_passphrase_and_export">Heslo databáze a export</string>
<string name="your_chat_profiles">Chat profily</string>
<string name="chat_with_the_founder">Zaslat otázky a nápady</string>
@@ -392,6 +396,7 @@
<string name="network_use_onion_hosts_required_desc">Pro připojení budou vyžadováni Onion hostitelé.</string>
<string name="update_network_session_mode_question">Aktualizovat režim dopravní izolace\?</string>
<string name="app_version_code">Sestavení aplikace: %s</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Můžete sdílet svou adresu jako odkaz nebo jako QR kód - kdokoli se k vám bude moci připojit. O své kontakty nepřijdete, pokud ji později smažete.</string>
<string name="share_link">Sdílet odkaz</string>
<string name="delete_address">Smazat adresu</string>
<string name="full_name__field">Celé jméno:</string>
@@ -568,7 +573,7 @@
<string name="you_have_no_chats">Nemáte žádné konverzace</string>
<string name="icon_descr_cancel_image_preview">Zrušit náhled obrázku</string>
<string name="share_message">Sdílet zprávu…</string>
<string name="share_image">Sdílet média</string>
<string name="share_image">Sdílet obrázek</string>
<string name="icon_descr_cancel_file_preview">Zrušit náhled souboru</string>
<string name="images_limit_title">Příliš mnoho obrázků!</string>
<string name="waiting_for_file">Čekání na soubor</string>
@@ -635,6 +640,7 @@
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní.</string>
<string name="save_servers_button">Uložit</string>
<string name="network_and_servers">Síť a servery</string>
<string name="network_socks_toggle">Použít proxy server SOCKS (port 9050)</string>
<string name="update_onion_hosts_settings_question">Aktualizovat nastavení hostitelů .onion\?</string>
<string name="network_use_onion_hosts">Použít hostitele .onion</string>
<string name="network_use_onion_hosts_prefer">Když bude dostupný</string>
@@ -653,6 +659,7 @@
<string name="core_version">Verze jádra: v%s</string>
<string name="delete_address__question">Smazat adresu\?</string>
<string name="all_your_contacts_will_remain_connected">Všechny vaše kontakty zůstanou připojeny.</string>
<string name="contact_requests">Žádosti o kontakt</string>
<string name="display_name__field">Zobrazované jméno:</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Váš profil je uložen v zařízení a je sdílen pouze s vašimi kontakty. <xliff:g id="appName">SimpleX</xliff:g> servery váš profil vidět nemohou.</string>
<string name="save_preferences_question">Uložit předvolby\?</string>
@@ -690,9 +697,9 @@
<string name="onboarding_notifications_mode_subtitle">Lze změnit později v nastavení.</string>
<string name="onboarding_notifications_mode_off">Když aplikace běží</string>
<string name="onboarding_notifications_mode_service">Okamžité</string>
<string name="onboarding_notifications_mode_off_desc"><b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží (žádná služba na pozadí).</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Dobré pro baterii</b>. Služba na pozadí bude kontrolovat každých 10 minut. Můžete zmeškat hovory nebo naléhavé zprávy.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Využívá více baterie</b>! Služba na pozadí je spuštěna vždy - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
<string name="onboarding_notifications_mode_off_desc"><b>Nejlepší pro baterii</b>. Budete přijímat oznámení pouze když aplikace běží, služba na pozadí NEBUDE použita.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Dobré pro baterii</b>. Služba na pozadí bude kontrolovat nové zprávy každých 10 minut. Můžete zmeškat hovory a naléhavé zprávy.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Využívá více baterie</b>! Služba na pozadí je vždy spuštěna - oznámení se zobrazí, jakmile jsou zprávy k dispozici.</string>
<string name="paste_the_link_you_received">Vložení přijatého odkazu</string>
<string name="incoming_video_call">Příchozí videohovor</string>
<string name="incoming_audio_call">Příchozí zvukový hovor</string>
@@ -809,7 +816,7 @@
<string name="you_joined_this_group">Připojili jste se k této skupině</string>
<string name="you_rejected_group_invitation">Odmítli jste pozvánku do skupiny</string>
<string name="group_invitation_expired">Platnost pozvánky do skupiny vypršela</string>
<string name="rcv_group_event_member_added">pozval <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_added">pozva <xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g></string>
<string name="rcv_group_event_member_connected">připojen</string>
<string name="rcv_group_event_changed_member_role">změnil roli %s na %s</string>
<string name="rcv_group_event_changed_your_role">změnil svou roli na %s</string>
@@ -950,11 +957,12 @@
<string name="v4_5_italian_interface">Italské rozhraní</string>
<string name="v4_5_italian_interface_descr">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Budete připojeni, jakmile bude zařízení vašeho kontaktu online, vyčkejte prosím nebo se podívejte později!</string>
<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_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 jednorázovou pozvánku</string>
<string name="share_invitation_link">Sdílet pozvánku</string>
<string name="status_e2e_encrypted">koncově šifrované</string>
<string name="moderated_description">moderované</string>
<string name="moderated_item_description">moderovaný %s</string>
@@ -1129,137 +1137,4 @@
<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>
<string name="learn_more_about_address">O adrese SimpleX</string>
<string name="one_time_link_short">Jednorázový odkaz</string>
<string name="message_reactions">Reakce na zprávy</string>
<string name="send_disappearing_message_send">Poslat</string>
<string name="self_destruct_passcode">Samodestrukční heslo</string>
<string name="self_destruct_passcode_changed">Heslo pro sebedestrukci změněno!</string>
<string name="color_primary_variant">Další zbarvení</string>
<string name="color_secondary">Sekundární</string>
<string name="empty_chat_profile_is_created">Vytvořit prázdný chat profil se zadaným názvem a otevřít aplikaci jako obvykle.</string>
<string name="if_you_enter_passcode_data_removed">Pokud tento přístupový kód zadáte při otevření aplikace, všechna data budou nenávratně smazána!</string>
<string name="color_secondary_variant">Další sekundární</string>
<string name="color_background">Pozadí</string>
<string name="color_surface">Menu a upozornění</string>
<string name="custom_time_picker_custom">vlastní</string>
<string name="custom_time_picker_select">Vybrat</string>
<string name="error_loading_details">Chyba načítání podrobností</string>
<string name="info_menu">Info</string>
<string name="auth_open_chat_profiles">Otevřít chat profily</string>
<string name="edit_history">Historie</string>
<string name="received_message">Přijatá zpráva</string>
<string name="sent_message">Poslaná zpráva</string>
<string name="disappearing_message">Mizící zpráva</string>
<string name="send_disappearing_message">Poslat mizící zprávu</string>
<string name="send_disappearing_message_1_minute">1 minutu</string>
<string name="send_disappearing_message_30_seconds">30 vteřin</string>
<string name="send_disappearing_message_custom_time">Vlastní čas</string>
<string name="all_your_contacts_will_remain_connected_update_sent">Všechny vaše kontakty zůstanou připojeny. Aktualizace profilu bude odeslána vašim kontaktům.</string>
<string name="invite_friends">Pozvat přátele</string>
<string name="enabled_self_destruct_passcode">Povolit sebedestrukční heslo</string>
<string name="self_destruct">Sebedestrukce</string>
<string name="enable_self_destruct">Povolit sebedestrukci</string>
<string name="if_you_enter_self_destruct_code">Pokud při otevření aplikace zadáte sebedestrukční heslo:</string>
<string name="self_destruct_passcode_enabled">Sebedestrukční heslo povoleno!</string>
<string name="set_passcode">Nastavit heslo</string>
<string name="info_row_updated_at">Záznam aktualizován v</string>
<string name="info_row_sent_at">Posláno v</string>
<string name="share_address">Sdílet adresu</string>
<string name="info_row_moderated_at">Upraveno v</string>
<string name="info_row_received_at">Přijato v</string>
<string name="share_text_received_at">Přijato: %s</string>
<string name="share_text_updated_at">Záznam aktualizován: %s</string>
<string name="share_text_sent_at">Posláno: %s</string>
<string name="share_text_disappears_at">Zmizí: %s</string>
<string name="share_text_moderated_at">Upraveno: %s</string>
<string name="current_version_timestamp">%s (aktuální)</string>
<string name="dark_theme">Tmavý motiv</string>
<string name="import_theme">Import motivu</string>
<string name="theme_simplex">SimpleX</string>
<string name="export_theme">Export motivu</string>
<string name="color_sent_message">Poslaná zpráva</string>
<string name="color_title">Titul</string>
<string name="color_received_message">Přijatá zpráva</string>
<string name="allow_your_contacts_adding_message_reactions">Povolit kontaktům přidávat reakce na zprávy.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Vy i váš kontakt můžete přidávat reakce na zprávy.</string>
<string name="message_reactions_prohibited_in_this_chat">Reakce na zprávy jsou v tomto chatu zakázány.</string>
<string name="prohibit_message_reactions">Zakázat reakce na zprávy.</string>
<string name="prohibit_message_reactions_group">Zakázat reakce na zprávy.</string>
<string name="group_members_can_add_message_reactions">Členové skupin mohou přidávat reakce na zprávy.</string>
<string name="message_reactions_are_prohibited">Reakce na zprávy jsou v této skupině zakázány.</string>
<string name="custom_time_unit_months">měsíců</string>
<string name="learn_more">Zjistit více</string>
<string name="share_with_contacts">Sdílet s kontakty</string>
<string name="group_welcome_preview">Náhled</string>
<string name="opening_database">Otvírání databáze…</string>
<string name="error_setting_address">Chyba nastavení adresy</string>
<string name="scan_qr_to_connect_to_contact">Pro připojení může váš kontakt naskenovat QR kód, nebo použít odkaz v aplikaci.</string>
<string name="you_can_accept_or_reject_connection">Když někdo požádá o připojení, můžete žádost přijmout nebo odmítnout.</string>
<string name="read_more_in_user_guide_with_link">Přečtěte si více v <font color="#0088ff">Uživatelské příručce</font>.</string>
<string name="simplex_address">Adresa SimpleX</string>
<string name="theme_colors_section_title">BARVY MOTIVU</string>
<string name="customize_theme_title">Přizpůsobit motiv</string>
<string name="profile_update_will_be_sent_to_contacts">Aktualizace profilu bude zaslána vašim kontaktům.</string>
<string name="share_address_with_contacts_question">Sdílet adresu s kontakty\?</string>
<string name="stop_sharing_address">Přestat sdílet adresu\?</string>
<string name="create_address_and_let_people_connect">Vytvořit adresu, aby se s vámi lidé mohli spojit.</string>
<string name="save_auto_accept_settings">Uložit nastavení automatického přijímání</string>
<string name="stop_sharing">Přestat sdílet</string>
<string name="auto_accept_contact">Automaticky přijmout</string>
<string name="you_can_create_it_later">Můžete vytvořit později</string>
<string name="dont_create_address">Nevytvářet adresu</string>
<string name="email_invite_body">Ahoj!
\nSpojte se se mnou přes SimpleX Chat: %s</string>
<string name="email_invite_subject">Promluvme si v SimpleX Chatu</string>
<string name="whats_new_read_more">Přečíst více</string>
<string name="v5_1_self_destruct_passcode_descr">Všechna data se při zadání vymažou.</string>
<string name="v5_1_better_messages">Lepší zprávy</string>
<string name="v5_1_custom_themes_descr">Přizpůsobit a sdílet barevné motivy.</string>
<string name="v5_1_custom_themes">Vlastní motiv</string>
<string name="v5_1_message_reactions_descr">Konečně je máme! 🚀</string>
<string name="v5_1_message_reactions">Reakce na zprávy</string>
<string name="v5_1_self_destruct_passcode">Samodestrukční heslo</string>
<string name="v5_1_japanese_portuguese_interface">Japonské a portugalské uživatelské rozhraní</string>
<string name="custom_time_unit_minutes">minut</string>
<string name="custom_time_unit_seconds">vteřin</string>
<string name="whats_new_thanks_to_users_contribute_weblate">Díky uživatelům - překládejte prostřednictvím Weblate!</string>
<string name="v5_1_better_messages_descr">- 5 minutové hlasové zprávy.
\n- vlastní čas mizení.
\n- historie úprav.</string>
<string name="custom_time_unit_days">dní</string>
<string name="custom_time_unit_hours">hodin</string>
<string name="custom_time_unit_weeks">týdnů</string>
<string name="send_disappearing_message_5_minutes">5 minut</string>
<string name="add_address_to_your_profile">Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům.</string>
<string name="all_app_data_will_be_cleared">Všechna data aplikace jsou smazána.</string>
<string name="address_section_title">Adresa</string>
<string name="allow_message_reactions">Povolit reakce na zprávy.</string>
<string name="allow_message_reactions_only_if">Povolit reakce na zprávy, pokud je váš kontakt povolí.</string>
<string name="create_simplex_address">Vytvořit SimpleX adresu</string>
<string name="continue_to_next_step">Pokračovat</string>
<string name="enter_welcome_message_optional">Zadat uvítací zprávu... (volitelně)</string>
<string name="if_you_cant_meet_in_person">Pokud se nemůžete setkat osobně, zobrazte QR kód ve videohovoru nebo sdílejte odkaz.</string>
<string name="change_self_destruct_mode">Změnit režim sebedestrukce</string>
<string name="change_self_destruct_passcode">Změnit sebedestrukční heslo</string>
<string name="item_info_current">(aktuální)</string>
<string name="share_text_database_id">ID databáze: %d</string>
<string name="info_row_deleted_at">Smazáno v</string>
<string name="share_text_deleted_at">Smazáno: %s</string>
<string name="info_row_disappears_at">Zmizí v</string>
<string name="enter_welcome_message">Zadat uvítací zprávu…</string>
<string name="import_theme_error">Chyba importu motivu</string>
<string name="import_theme_error_desc">Ujistěte se, že soubor má správnou syntaxi YAML. Exportujte motiv, abyste měli příklad struktury souboru.</string>
<string name="self_destruct_new_display_name">Nově zobrazované jméno:</string>
<string name="only_you_can_add_message_reactions">Reakce na zprávy můžete přidávat pouze vy.</string>
<string name="save_settings_question">Uložit nastavení\?</string>
<string name="only_your_contact_can_add_message_reactions">Reakce na zprávy může přidávat pouze váš kontakt.</string>
<string name="your_contacts_will_remain_connected">Vaše kontakty zůstanou připojeny.</string>
<string name="you_can_share_this_address_with_your_contacts">Tuto adresu můžete sdílet se svými kontakty, aby se mohli připojit k %s.</string>
<string name="your_contacts_will_see_it">Vaše kontakty v SimpleX ji uvidí.
\nMůžete ji změnit v Nastavení.</string>
<string name="you_can_share_your_address">Svou adresu můžete sdílet jako odkaz nebo QR kód - kdokoli se k vám může připojit.</string>
<string name="you_wont_lose_your_contacts_if_delete_address">Pokud později adresu odstraníte, o kontakty nepřijdete.</string>
<string name="app_passcode_replaced_with_self_destruct">Přístupový kód aplikace je nahrazen sebedestrukčním přístupovým heslem.</string>
<string name="item_info_no_text">žádný text</string>
</resources>

View File

@@ -63,7 +63,7 @@
<string name="error_receiving_file">Fehler beim Empfangen der Datei</string>
<string name="error_creating_address">Fehler beim Erstellen der Adresse</string>
<string name="contact_already_exists">Kontakt ist bereits vorhanden</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit <xliff:g id="contactName" example="Alice">%1$s</xliff:g> verbunden.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit <xliff:g id="contactName" example="Alice">%1$s!</xliff:g> verbunden.</string>
<string name="invalid_connection_link">Ungültiger Verbindungslink</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string>
<string name="connection_error_auth">Verbindungsfehler (AUTH)</string>
@@ -328,11 +328,12 @@
<string name="you_will_be_connected_when_group_host_device_is_online">Sie werden mit der Gruppe verbunden, sobald das Endgerät des Gruppen-Hosts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Sie werden verbunden, sobald Ihre Verbindungsanfrage akzeptiert wird. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Sie werden verbunden, sobald das Endgerät Ihres Kontakts online ist. Bitte warten oder schauen Sie später nochmal nach!</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Zeigen Sie Ihrem Kontakt den QR-Code aus der App zum Scannen.</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs anzeigen</b> oder einen Einladungslink über einen anderen Kanal mit Ihrem Kontakt teilen.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Ihr Chat-Profil wird
\nan Ihren Kontakt gesendet</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.</string>
<string name="share_invitation_link">Einmal-Link teilen</string>
<string name="share_invitation_link">Einladungslink teilen</string>
<string name="paste_connection_link_below_to_connect">Fügen Sie den erhaltenen Link in das Feld unten ein, um sich mit Ihrem Kontakt zu verbinden.</string>
<string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet</string>
<!-- PasteToConnect.kt -->
@@ -344,9 +345,10 @@
<!-- CreateLinkView.kt -->
<string name="create_one_time_link">Link / QR-Code erstellen</string>
<string name="one_time_link">Einmaliger Einladungs-Link</string>
<string name="your_contact_address">Meine Kontaktadresse</string>
<!-- settings - SettingsView.kt -->
<string name="your_settings">Meine Einstellungen</string>
<string name="your_simplex_contact_address">Meine SimpleX-Adresse</string>
<string name="your_simplex_contact_address">Meine <xliff:g id="appName">SimpleX</xliff:g> Kontaktadresse</string>
<string name="database_passphrase_and_export">Datenbank-Passwort &amp; -Export</string>
<string name="about_simplex_chat">Über <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">Wie man SimpleX nutzt</string>
@@ -395,8 +397,9 @@
<string name="network_and_servers">Netzwerk &amp; Server</string>
<string name="network_settings">Erweiterte Netzwerkeinstellungen</string>
<string name="network_settings_title">Netzwerkeinstellungen</string>
<string name="network_socks_toggle">SOCKS-Proxy verwenden (Port 9050)</string>
<string name="network_enable_socks">SOCKS-Proxy verwenden?</string>
<string name="network_enable_socks_info">Zugriff auf die Server über SOCKS-Proxy auf Port %d? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird.</string>
<string name="network_enable_socks_info">Zugriff auf die Server über SOCKS-Proxy auf Port 9050? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird.</string>
<string name="network_disable_socks">Direkte Internetverbindung verwenden?</string>
<string name="network_disable_socks_info">Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse und Ihren Provider sehen und mit welchen Servern Sie sich verbinden.</string>
<string name="update_onion_hosts_settings_question">Einstellung für .onion-Hosts aktualisieren?</string>
@@ -415,8 +418,14 @@
<string name="create_address">Adresse erstellen</string>
<string name="delete_address__question">Adresse löschen?</string>
<string name="all_your_contacts_will_remain_connected">Alle Ihre Kontakte bleiben verbunden.</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Sie können Ihre Adresse als Link oder als QR-Code teilen Jede Person kann sich darüber mit Ihnen verbinden. Sie werden Ihre mit dieser Adresse verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen.</string>
<string name="share_link">Link teilen</string>
<string name="delete_address">Adresse löschen</string>
<!-- AcceptRequestsView.kt -->
<string name="contact_requests">Kontaktanfragen</string>
<string name="accept_requests">Anfragen annehmen</string>
<string name="accept_automatically">Automatisch</string>
<string name="section_title_welcome_message">Begrüßungsmeldung</string>
<!-- User profile details - UserProfileView.kt -->
<string name="display_name__field">Angezeigter Name:</string>
<string name="full_name__field">"Vollständiger Name:</string>
@@ -905,8 +914,8 @@
<string name="live">LIVE</string>
<string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string>
<string name="onboarding_notifications_mode_service">Sofort</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Gute Option für die Batterieausdauer</b>. Der Hintergrundservice überprüft alle 10 Minuten nach Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.</string>
<string name="onboarding_notifications_mode_off_desc"><b>Beste Option für die Batterieausdauer</b>. Sie empfangen Benachrichtigungen nur solange die App abläuft (kein aktiver Hintergrundservice).</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Gute Option für die Batterieausdauer</b>. Der Hintergrundservice überprüft alle 10 Minuten nach neuen Nachrichten. Sie können eventuell Anrufe und dringende Nachrichten verpassen.</string>
<string name="onboarding_notifications_mode_off_desc"><b>Beste Option für die Batterieausdauer</b>. Sie empfangen Benachrichtigungen nur solange die App abläuft. Der Hintergrundservice wird nicht genutzt!</string>
<string name="send_verb">Senden</string>
<string name="is_verified">%s wurde erfolgreich überprüft</string>
<string name="clear_verification">Überprüfung zurücknehmen</string>
@@ -942,7 +951,7 @@
<string name="failed_to_parse_chat_title">Fehler beim Laden des Chats</string>
<string name="failed_to_parse_chats_title">Fehler beim Laden der Chats</string>
<string name="contact_developers">Bitte aktualisieren Sie die App und nehmen Sie Kontakt mit den Entwicklern auf.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Benötigt mehr Leistung Ihrer Batterie</b>! Der Hintergrundservice läuft permanent ab. Benachrichtigungen werden Ihnen angezeigt, sobald Sie neue Nachrichten erhalten haben.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Benötigt mehr Leistung Ihrer Batterie</b>! Der Hintergrundservice läuft die ganze Zeit ab. Benachrichtigungen werden Ihnen sofort angezeigt, nachdem Sie neue Nachrichten erhalten haben.</string>
<string name="create_group_link">Gruppenlink erstellen</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten.</string>
<string name="prohibit_sending_disappearing_messages">Das Senden von verschwindenden Nachrichten verbieten.</string>
@@ -1175,7 +1184,7 @@
<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 unterscheidet sich.</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>
@@ -1209,137 +1218,4 @@
<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>
<string name="address_section_title">Adresse</string>
<string name="share_address">Adresse teilen</string>
<string name="export_theme">Design exportieren</string>
<string name="import_theme_error">Fehler beim Importieren des Designs</string>
<string name="color_title">Bezeichnung</string>
<string name="opening_database">Öffne Datenbank …</string>
<string name="error_setting_address">Fehler bei der Adresseinstellung</string>
<string name="learn_more">Mehr erfahren</string>
<string name="scan_qr_to_connect_to_contact">Um eine Verbindung herzustellen, kann Ihr Kontakt den QR-Code scannen oder den Link in der App verwenden.</string>
<string name="simplex_address">SimpleX-Adresse</string>
<string name="you_can_accept_or_reject_connection">Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen.</string>
<string name="you_wont_lose_your_contacts_if_delete_address">Sie werden Ihre Kontakte nicht verlieren, falls Sie Ihre Adresse später löschen.</string>
<string name="customize_theme_title">Design anpassen</string>
<string name="theme_colors_section_title">DESIGN-FARBEN</string>
<string name="add_address_to_your_profile">Fügen Sie die Adresse zu Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</string>
<string name="all_your_contacts_will_remain_connected_update_sent">Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet.</string>
<string name="create_address_and_let_people_connect">Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können.</string>
<string name="create_simplex_address">SimpleX-Adresse erstellen</string>
<string name="share_with_contacts">Mit Kontakten teilen</string>
<string name="your_contacts_will_remain_connected">Ihre Kontakte bleiben verbunden.</string>
<string name="auto_accept_contact">Automatisch akzeptieren</string>
<string name="enter_welcome_message_optional">Geben Sie eine Begrüßungsmeldung ein … (optional)</string>
<string name="invite_friends">Freunde einladen</string>
<string name="email_invite_subject">Lassen Sie uns in SimpleX Chat kommunizieren</string>
<string name="profile_update_will_be_sent_to_contacts">Profil-Aktualisierung wird an Ihre Kontakte gesendet.</string>
<string name="save_auto_accept_settings">Einstellungen von \"Automatisch akzeptieren\" speichern</string>
<string name="save_settings_question">Einstellungen speichern\?</string>
<string name="share_address_with_contacts_question">Die Adresse mit Kontakten teilen\?</string>
<string name="stop_sharing">Teilen beenden</string>
<string name="stop_sharing_address">Das Teilen der Adresse beenden\?</string>
<string name="dont_create_address">Keine Adresse erstellt</string>
<string name="email_invite_body">Hallo!
\nVerbinden Sie sich per SimpleX Chat mit mir: %s</string>
<string name="you_can_create_it_later">Sie können dies später erstellen</string>
<string name="enter_welcome_message">Geben Sie eine Begrüßungsmeldung ein …</string>
<string name="group_welcome_preview">Vorschau</string>
<string name="you_can_share_this_address_with_your_contacts">Sie können diese Adresse mit Ihren Kontakten teilen, um sie mit %s verbinden zu lassen.</string>
<string name="color_secondary_variant">Zweite Akzentfarbe</string>
<string name="color_background">Hintergrund-Farbe</string>
<string name="import_theme">Design importieren</string>
<string name="color_surface">Menüs &amp; Benachrichtigungen</string>
<string name="color_received_message">Empfangene Nachricht</string>
<string name="color_secondary">Zweite Farbe</string>
<string name="color_sent_message">Gesendete Nachricht</string>
<string name="theme_simplex">SimpleX</string>
<string name="learn_more_about_address">Über die SimpleX-Adresse</string>
<string name="one_time_link_short">Einmal-Link</string>
<string name="color_primary_variant">Erste Akzentfarbe</string>
<string name="continue_to_next_step">Weiter</string>
<string name="dark_theme">Dunkles Design</string>
<string name="if_you_cant_meet_in_person">Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.</string>
<string name="read_more_in_user_guide_with_link">Lesen Sie mehr dazu in der <font color="#0088ff">Benutzeranleitung</font>.</string>
<string name="import_theme_error_desc">Stellen Sie sicher, dass die Datei die korrekte YAML-Syntax hat. Exportieren sie das Design, um ein Beispiel für die Dateistruktur des Designs zu erhalten.</string>
<string name="auth_open_chat_profiles">Offene Chat-Profile</string>
<string name="you_can_share_your_address">Sie können Ihre Adresse als Link oder QR-Code teilen - jede Person kann sich mit Ihnen verbinden.</string>
<string name="your_contacts_will_see_it">Ihre Kontakte in SimpleX werden es sehen.
\nSie können es in den Einstellungen ändern.</string>
<string name="all_app_data_will_be_cleared">App-Daten werden komplett gelöscht.</string>
<string name="empty_chat_profile_is_created">Es wurde ein leeres Chat-Profil mit dem eingegebenen Namen erstellt und die App öffnet wie gewohnt.</string>
<string name="if_you_enter_self_destruct_code">Wenn Sie Ihr Selbstzerstörungspasswort während des Öffnens der App eingeben:</string>
<string name="self_destruct_new_display_name">Neuer angezeigter Name:</string>
<string name="self_destruct_passcode_changed">Das Selbstzerstörungspasswort wurde geändert!</string>
<string name="change_self_destruct_mode">Selbstzerstörungs-Modus ändern</string>
<string name="change_self_destruct_passcode">Selbstzerstörungspasswort ändern</string>
<string name="enabled_self_destruct_passcode">Selbstzerstörungspasswort aktivieren</string>
<string name="self_destruct">Selbstzerstörung</string>
<string name="self_destruct_passcode_enabled">Das Selbstzerstörungspasswort wurde aktiviert!</string>
<string name="app_passcode_replaced_with_self_destruct">Das App Passwort wurde durch das Selbstzerstörungspasswort ersetzt.</string>
<string name="enable_self_destruct">Selbstzerstörung aktivieren</string>
<string name="if_you_enter_passcode_data_removed">Wenn Sie dieses Passwort während des Öffnens der App eingeben, werden alle App-Daten unwiederbringlich gelöscht!</string>
<string name="self_destruct_passcode">Selbstzerstörungspasswort</string>
<string name="set_passcode">Passwort eingeben</string>
<string name="message_reactions_are_prohibited">In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt.</string>
<string name="error_loading_details">Fehler beim Laden von Details</string>
<string name="received_message">Empfangene Nachricht</string>
<string name="info_menu">Information</string>
<string name="sent_message">Gesendete Nachricht</string>
<string name="send_disappearing_message_custom_time">Zeit anpassen</string>
<string name="disappearing_message">Verschwindende Nachricht</string>
<string name="send_disappearing_message_send">Senden</string>
<string name="send_disappearing_message_1_minute">1 Minute</string>
<string name="send_disappearing_message">Verschwindende Nachricht senden</string>
<string name="info_row_moderated_at">Moderiert um</string>
<string name="info_row_deleted_at">Gelöscht um</string>
<string name="info_row_received_at">Empfangen um</string>
<string name="info_row_updated_at">Datensatz aktualisiert um</string>
<string name="info_row_sent_at">Gesendet um</string>
<string name="share_text_moderated_at">Moderiert um: %s</string>
<string name="share_text_database_id">Datenbank-ID: %d</string>
<string name="share_text_received_at">Empfangen um: %s</string>
<string name="share_text_updated_at">Datensatz aktualisiert um: %s</string>
<string name="current_version_timestamp">%s (aktuell)</string>
<string name="share_text_sent_at">Gesendet um: %s</string>
<string name="message_reactions">Reaktionen auf Nachrichten</string>
<string name="allow_your_contacts_adding_message_reactions">Erlauben Sie Ihren Kontakten Reaktionen auf Nachrichten zu geben.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben.</string>
<string name="only_you_can_add_message_reactions">Nur Sie können Reaktionen auf Nachrichten geben.</string>
<string name="prohibit_message_reactions">Reaktionen auf Nachrichten nicht erlauben.</string>
<string name="allow_message_reactions_only_if">Reaktionen auf Nachrichten sind nur möglich, falls Ihr Kontakt dies erlaubt.</string>
<string name="only_your_contact_can_add_message_reactions">Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben.</string>
<string name="allow_message_reactions">Reaktionen auf Nachrichten erlauben.</string>
<string name="prohibit_message_reactions_group">Reaktionen auf Nachrichten nicht erlauben.</string>
<string name="group_members_can_add_message_reactions">Gruppenmitglieder können eine Reaktion auf Nachrichten geben.</string>
<string name="whats_new_read_more">Mehr erfahren</string>
<string name="v5_1_message_reactions_descr">Endlich haben wir sie! 🚀</string>
<string name="v5_1_message_reactions">Reaktionen auf Nachrichten</string>
<string name="v5_1_self_destruct_passcode">Selbstzerstörungspasswort</string>
<string name="v5_1_self_destruct_passcode_descr">Sobald es eingegeben wird, werden alle Daten gelöscht.</string>
<string name="v5_1_custom_themes">Benutzerdefinierte Designs</string>
<string name="v5_1_japanese_portuguese_interface">Japanische und Portugiesische Bedienoberfläche</string>
<string name="custom_time_unit_minutes">Minuten</string>
<string name="custom_time_unit_seconds">Sekunden</string>
<string name="whats_new_thanks_to_users_contribute_weblate">Dank der Nutzer - Tragen Sie per Weblate bei!</string>
<string name="v5_1_better_messages">Verbesserungen bei Nachrichten</string>
<string name="v5_1_custom_themes_descr">Farbdesigns anpassen und weitergeben.</string>
<string name="custom_time_unit_days">Tage</string>
<string name="custom_time_unit_hours">Stunden</string>
<string name="v5_1_better_messages_descr">- Bis zu 5 Minuten lange Sprachnachrichten
\n- Zeit für verschwindende Nachrichten anpassen
\n- Vergangenheit bearbeiten</string>
<string name="custom_time_picker_custom">benutzerdefiniert</string>
<string name="custom_time_unit_months">Monate</string>
<string name="custom_time_picker_select">Auswählen</string>
<string name="custom_time_unit_weeks">Wochen</string>
<string name="send_disappearing_message_5_minutes">5 Minuten</string>
<string name="item_info_current">(aktuell)</string>
<string name="send_disappearing_message_30_seconds">30 Sekunden</string>
<string name="share_text_deleted_at">Gelöscht um: %s</string>
<string name="info_row_disappears_at">Verschwindet um</string>
<string name="share_text_disappears_at">Verschwindet um: %s</string>
<string name="edit_history">Vergangenheit</string>
<string name="message_reactions_prohibited_in_this_chat">In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt.</string>
<string name="item_info_no_text">Kein Text</string>
</resources>

View File

@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="chat_item_ttl_day">1 μέρα</string>
<string name="chat_item_ttl_month">1 μήνας</string>
<string name="about_simplex">Για το SimpleX</string>
<string name="scan_QR_code">Σαρώστε τον QR κωδικό</string>
<string name="a_plus_b">α + β</string>
<string name="about_simplex_chat">Για το <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="scan_code_from_contacts_app">Σαρώστε τον κωδικό ασφαλείας από την εφαρμογή επαφών σας</string>
<string name="smp_server_test_secure_queue">Ασφαλή ουρά</string>
<string name="network_option_seconds_label">δε</string>
<string name="security_code">Κωδικός ασφαλείας</string>
<string name="smp_servers_scan_qr">Σαρώστε τον κωδικό QR διακομιστή</string>
<string name="secret">μυστικό</string>
<string name="chat_item_ttl_week">1 εβδομάδα</string>
<string name="v4_2_security_assessment">αξιολόγηση ασφαλείας</string>
<string name="allow_verb">Συναινώ</string>
<string name="accept_contact_button">Αποδοχή</string>
<string name="accept_contact_incognito_button">Αποδοχή ανώνυμης περιήγησης</string>
<string name="smp_servers_preset_add">Προσθήκη προκαθορισμένου διακομιστή</string>
<string name="smp_servers_add_to_another_device">Προσθήκη σε άλλη συσκευή</string>
<string name="all_your_contacts_will_remain_connected">Όλες οι επαφές σας θα παραμείνουν ενεργές.</string>
<string name="accept_call_on_lock_screen">Αποδοχή</string>
<string name="group_member_role_admin">διαχειριστής</string>
<string name="button_add_welcome_message">Προσθέστε μήνυμα καλωσορίσματος</string>
<string name="all_group_members_will_remain_connected">Όλα τα μέλη της ομάδας θα παραμήνουν συνδεδεμένα.</string>
<string name="users_add">Προσθήκη προφίλ</string>
<string name="color_primary">Προφορά</string>
<string name="chat_preferences_always">πάντα</string>
<string name="accept_feature">Αποδοχή</string>
<string name="allow_disappearing_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_voice_messages_only_if">Επιτρέπονται τα φωνητικά μηνύματα μόνο εάν τα επιτρέπει η επαφή σας.</string>
<string name="allow_your_contacts_to_call">Επιτρέψτε στις επαφές σας να σας καλέσουν.</string>
<string name="allow_your_contacts_to_send_voice_messages">Επιτρέψτε στις επαφές σας να στέλνουν φωνητικά μηνύματα.</string>
<string name="allow_direct_messages">Να επιτρέπεται η αποστολή άμεσων μηνυμάτων στα μέλη.</string>
<string name="allow_to_send_disappearing">Επιτρέπεται η αποστολή μηνυμάτων που εξαφανίζονται.</string>
<string name="allow_to_send_voice">Επιτρέπεται η αποστολή φωνητικών μηνυμάτων.</string>
<string name="app_name"><xliff:g id="appName">SimpleX</xliff:g></string>
<string name="accept">Αποδοχή</string>
<string name="accept_connection_request__question">Αποδοχή αιτήματος σύνδεσης;</string>
<string name="callstatus_accepted">αποδεκτή κλήση</string>
<string name="network_enable_socks_info">Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα 9050; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση.</string>
<string name="accept_requests">Αποδοχή αιτημάτων</string>
<string name="smp_servers_add">Προσθήκη διακομιστή…</string>
<string name="network_settings">Προχωρημένες ρυθμίσεις δικτύου</string>
<string name="v4_3_improved_server_configuration_desc">Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών.</string>
<string name="v4_2_group_links_desc">Οι διαχειριστές μπορούν να δημιουργήσουν τους συνδέσμους συμμετοχής σε ομάδες.</string>
<string name="users_delete_all_chats_deleted">Όλες οι συνομιλίες και τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί!</string>
<string name="clear_chat_warning">Όλα τα μηνύματα θα διαγραφούν - αυτή η ενέργεια δεν μπορεί να αντιστραφεί! Τα μηνύματα θα διαγραφούν ΜΟΝΟ για εσάς.</string>
<string name="allow_irreversible_message_deletion_only_if">Επιτρέψτε τη μη αναστρέψιμη διαγραφή μηνυμάτων μόνο εάν σας το επιτρέπει η επαφή σας.</string>
<string name="allow_calls_only_if">Επιτρέπονται οι κλήσεις μόνο εάν η επαφή σας τις επιτρέπει.</string>
<string name="allow_to_delete_messages">Επιτρέψτε τη μη αναστρέψιμη διαγραφή των απεσταλμένων μηνυμάτων.</string>
<string name="allow_voice_messages_question">Να επιτρέπονται τα φωνητικά μηνύματα;</string>
<string name="notifications_mode_service">Πάντα ενεργό</string>
<string name="always_use_relay">Να χρησιμοποιείται πάντα αναμεταδότη</string>
</resources>

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@
<string name="invalid_connection_link">Lien de connection invalide</string>
<string name="connection_timeout">Délai de connexion</string>
<string name="error_sending_message">Erreur lors de l\'envoi du message</string>
<string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Vous êtes déjà connecté à <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="connection_error_auth">Erreur de connexion (AUTH)</string>
<string name="connection_error_auth_desc">A moins que votre contact ait supprimé la connexion ou que ce lien ait déjà été utilisé, il peut s\'agir d\'un bug - veuillez le signaler.
\nPour vous connecter, veuillez demander à votre contact de créer un autre lien de connexion et vérifiez que vous disposez d\'une connexion réseau stable.</string>
@@ -240,9 +240,10 @@
<string name="this_QR_code_is_not_a_link">Ce code QR n\'est pas un lien !</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Vous serez connecté·e lorsque votre demande de connexion sera acceptée, veuillez attendre ou vérifier plus tard !</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">Vous serez connecté·e lorsque l\'appareil de votre contact sera en ligne, veuillez attendre ou vérifier plus tard !</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Votre contact peut scanner le code QR depuis l\'app.</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Votre profil de chat sera envoyé
\nà votre contact</string>
<string name="share_invitation_link">Partager un lien unique</string>
<string name="share_invitation_link">Partager le lien d\'invitation</string>
<string name="your_profile_will_be_sent">Votre profil de chat sera envoyé à votre contact</string>
<string name="paste_button">Coller</string>
<string name="this_string_is_not_a_connection_link">Cette chaîne n\'est pas un lien de connexion !</string>
@@ -259,6 +260,7 @@
<string name="connect_via_link">Se connecter via un lien</string>
<string name="clear_verification">Retirer la vérification</string>
<string name="one_time_link">Lien d\'invitation unique</string>
<string name="your_contact_address">Votre adresse de contact</string>
<string name="scan_code">Scanner le code</string>
<string name="incorrect_code">Code de sécurité incorrect !</string>
<string name="security_code">Code de sécurité</string>
@@ -324,7 +326,7 @@
<string name="use_simplex_chat_servers__question">Utiliser les serveurs <xliff:g id="appNameFull">SimpleX Chat</xliff:g> \?</string>
<string name="smp_servers_delete_server">Supprimer le serveur</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne.</string>
<string name="network_enable_socks_info">Accéder aux serveurs via un proxy SOCKS sur le port %d \? Le proxy doit être démarré avant d\'activer cette option.</string>
<string name="network_enable_socks_info">Accéder aux serveurs via un proxy SOCKS sur le port 9050 \? Le proxy doit être démarré avant d\'activer cette option.</string>
<string name="network_use_onion_hosts">Utiliser les hôtes .onions</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
<string name="network_use_onion_hosts_required_desc_in_alert">Les hôtes .onion seront nécessaires pour la connexion.</string>
@@ -350,6 +352,7 @@
<string name="configure_ICE_servers">Configurer les serveurs ICE</string>
<string name="network_settings">Paramètres réseau avancés</string>
<string name="network_settings_title">Paramètres réseau</string>
<string name="network_socks_toggle">Utiliser un proxy SOCKS (port 9050)</string>
<string name="network_enable_socks">Utiliser un proxy SOCKS \?</string>
<string name="network_disable_socks">Utiliser une connexion Internet directe \?</string>
<string name="network_disable_socks_info">Si vous confirmez, les serveurs de messagerie seront en mesure de voir votre adresse IP, votre fournisseur ainsi que les serveurs auxquels vous vous connectez.</string>
@@ -358,6 +361,7 @@
<string name="network_use_onion_hosts_prefer_desc">Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles.</string>
<string name="appearance_settings">Apparence</string>
<string name="create_address">Créer une adresse</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Vous pouvez partager votre adresse sous forme de lien ou de code QR - n\'importe qui pourra se connecter à vous. Vous ne perdrez pas vos contacts si vous la supprimez par la suite.</string>
<string name="your_current_profile">Votre profil de chat</string>
<string name="edit_image">Modifier l\'image</string>
<string name="save_and_notify_contacts">Sauvegarder et notifier les contacts</string>
@@ -388,7 +392,7 @@
<string name="onboarding_notifications_mode_off">Quand l\'application fonctionne</string>
<string name="onboarding_notifications_mode_periodic">Périodique</string>
<string name="onboarding_notifications_mode_service">Instantanée</string>
<string name="onboarding_notifications_mode_off_desc"><b>Économie de batterie</b>. Vous recevrez des notifications uniquement lorsque l\'application est en cours d\'exécution (PAS de service d\'arrière-plan)</string>
<string name="onboarding_notifications_mode_off_desc"><b>Économie de batterie</b>. Vous recevrez des notifications uniquement lorsque l\'application est en cours d\'exécution, le service de fond ne sera PAS utilisé.</string>
<string name="about_simplex_chat">À propos de <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="how_to_use_simplex_chat">Comment l\'utiliser</string>
<string name="markdown_help">Aide Markdown</string>
@@ -402,7 +406,7 @@
<string name="callstate_starting">lancement…</string>
<string name="is_verified">%s est vérifié·e</string>
<string name="is_not_verified">%s n\'est pas vérifié·e</string>
<string name="your_simplex_contact_address">Votre adresse SimpleX</string>
<string name="your_simplex_contact_address">Votre adresse de contact <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="database_passphrase_and_export">Phrase secrète et exportation de la base de données</string>
<string name="chat_with_the_founder">Envoyez vos questions et idées</string>
<string name="send_us_an_email">Envoyez nous un e-mail</string>
@@ -429,6 +433,10 @@
<string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string>
<string name="share_link">Partager le lien</string>
<string name="delete_address">Supprimer l\'adresse</string>
<string name="contact_requests">Demandes de contact</string>
<string name="accept_requests">Accepter les demandes</string>
<string name="accept_automatically">Automatiquement</string>
<string name="section_title_welcome_message">MESSAGE DE BIENVENUE</string>
<string name="display_name__field">Nom affiché :</string>
<string name="full_name__field">Nom complet :</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Votre profil est stocké sur votre appareil et partagé uniquement avec vos contacts. Les serveurs <xliff:g id="appName">SimpleX</xliff:g> ne peuvent pas voir votre profil.</string>
@@ -462,8 +470,8 @@
<string name="many_people_asked_how_can_it_deliver">Beaucoup se demandent : <i>si <xliff:g id="appName">SimpleX</xliff:g> n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?</i></string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un <b>chiffrement de bout en bout à deux couches</b>.</string>
<string name="read_more_in_github_with_link">Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Batterie peu utilisée</b>. Le service de fond vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Batterie plus utilisée </b> ! Le service de fond est toujours en cours d\'exécution - les notifications s\'affichent dès que les messages sont disponibles.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Batterie peu utilisée</b>. Le service de fond vérifie les nouveaux messages toutes les 10 minutes. Vous risquez de manquer des appels et des messages urgents.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Batterie plus utilisée </b> ! Le service de fond est toujours en cours d\'exécution - les notifications s\'afficheront dès que les messages seront disponibles.</string>
<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>
@@ -959,7 +967,7 @@
<string name="moderated_description">modéré</string>
<string name="moderated_item_description">modéré par %s</string>
<string name="delete_member_message__question">Supprimer le message de ce membre \?</string>
<string name="moderate_verb">Modérer</string>
<string name="moderate_verb">Modéré</string>
<string name="moderate_message_will_be_deleted_warning">Le message sera supprimé pour tous les membres.</string>
<string name="moderate_message_will_be_marked_warning">Le message sera marqué comme modéré pour tous les membres.</string>
<string name="you_are_observer">vous êtes observateur</string>
@@ -1129,137 +1137,4 @@
<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>
<string name="opening_database">Ouverture de la base de données…</string>
<string name="learn_more_about_address">À propos de l\'adresse SimpleX</string>
<string name="learn_more">En savoir plus</string>
<string name="you_can_share_your_address">Vous pouvez partager votre adresse sous la forme d\'un lien ou d\'un code QR - tout le monde peut se connecter à vous.</string>
<string name="you_wont_lose_your_contacts_if_delete_address">Vous ne perdrez pas vos contacts si vous supprimez votre adresse ultérieurement.</string>
<string name="simplex_address">Adresse SimpleX</string>
<string name="you_can_accept_or_reject_connection">Lorsque des personnes demandent à se connecter, vous pouvez les accepter ou les refuser.</string>
<string name="theme_colors_section_title">COULEURS DU THÈME</string>
<string name="your_contacts_will_remain_connected">Vos contacts resteront connectés.</string>
<string name="share_address_with_contacts_question">Partager l\'adresse avec vos contacts \?</string>
<string name="share_with_contacts">Partager avec vos contacts</string>
<string name="enter_welcome_message_optional">Entrez un message de bienvenue… (facultatif)</string>
<string name="stop_sharing">Cesser le partage</string>
<string name="stop_sharing_address">Cesser le partage d\'adresse \?</string>
<string name="invite_friends">Inviter des amis</string>
<string name="email_invite_subject">Discutons sur SimpleX Chat</string>
<string name="save_settings_question">Sauvegarder les paramètres \?</string>
<string name="dont_create_address">Ne pas créer d\'adresse</string>
<string name="address_section_title">Adresse</string>
<string name="share_address">Partager l\'adresse</string>
<string name="you_can_share_this_address_with_your_contacts">Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec %s.</string>
<string name="group_welcome_preview">Aperçu</string>
<string name="color_background">Fond d\'écran</string>
<string name="dark_theme">Thème sombre</string>
<string name="export_theme">Exporter le thème</string>
<string name="import_theme">Importer un thème</string>
<string name="import_theme_error">Erreur lors de l\'importation d\'un thème</string>
<string name="color_secondary">Secondaire</string>
<string name="theme_simplex">SimpleX</string>
<string name="color_sent_message">Message envoyé</string>
<string name="color_title">Titre</string>
<string name="one_time_link_short">Lien à usage unique</string>
<string name="color_primary_variant">Accentuation supplémentaire</string>
<string name="add_address_to_your_profile">Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d\'autres personnes. La mise à jour du profil sera envoyée à vos contacts.</string>
<string name="color_secondary_variant">Secondaire supplémentaire</string>
<string name="all_your_contacts_will_remain_connected_update_sent">Tous vos contacts resteront connectés. La mise à jour du profil sera envoyée à vos contacts.</string>
<string name="auto_accept_contact">Auto-acceptation</string>
<string name="create_simplex_address">Créer une adresse SimpleX</string>
<string name="customize_theme_title">Personnaliser le thème</string>
<string name="continue_to_next_step">Continuer</string>
<string name="error_setting_address">Erreur lors du réglage de l\'adresse</string>
<string name="create_address_and_let_people_connect">Créez une adresse pour permettre aux gens de vous contacter.</string>
<string name="enter_welcome_message">Entrez un message de bienvenue…</string>
<string name="you_can_create_it_later">Vous pouvez la créer plus tard</string>
<string name="email_invite_body">Bonjour !
\nContactez-moi via SimpleX Chat : %s</string>
<string name="if_you_cant_meet_in_person">Si vous ne pouvez pas vous rencontrer en personne, montrez le code QR lors d\'un appel vidéo ou partagez le lien.</string>
<string name="auth_open_chat_profiles">Ouvrir les profils de chat</string>
<string name="color_surface">Menus et alertes</string>
<string name="color_received_message">Message reçu</string>
<string name="import_theme_error_desc">Assurez-vous que le fichier a une syntaxe YAML correcte. Exporter le thème pour avoir un exemple de la structure du fichier du thème.</string>
<string name="profile_update_will_be_sent_to_contacts">La mise à jour du profil sera envoyée à vos contacts.</string>
<string name="read_more_in_user_guide_with_link">Pour en savoir plus, consultez le <font color="#0088ff">Guide de l\'utilisateur</font>.</string>
<string name="save_auto_accept_settings">Sauvegarder les paramètres d\'acceptation automatique</string>
<string name="scan_qr_to_connect_to_contact">Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l\'application.</string>
<string name="your_contacts_will_see_it">Vos contacts dans SimpleX la verront.
\nVous pouvez modifier ce choix dans les Paramètres.</string>
<string name="app_passcode_replaced_with_self_destruct">Le code d\'accès de l\'application est remplacé par un code d\'autodestruction.</string>
<string name="enable_self_destruct">Activer l\'autodestruction</string>
<string name="empty_chat_profile_is_created">Un profil de chat vierge portant le nom fourni est créé et l\'application s\'ouvre normalement.</string>
<string name="change_self_destruct_passcode">Modifier le code d\'autodestruction</string>
<string name="enabled_self_destruct_passcode">Activer le code d\'autodestruction</string>
<string name="self_destruct">Autodestruction</string>
<string name="change_self_destruct_mode">Modifier le mode d\'autodestruction</string>
<string name="self_destruct_new_display_name">Nouveau nom affiché :</string>
<string name="self_destruct_passcode">Code d\'autodestruction</string>
<string name="self_destruct_passcode_changed">Le code d\'autodestruction a été modifié !</string>
<string name="self_destruct_passcode_enabled">Code d\'autodestruction activé !</string>
<string name="all_app_data_will_be_cleared">Toutes les données de l\'application sont supprimées.</string>
<string name="if_you_enter_self_destruct_code">Si vous entrez votre code d\'autodestruction à l\'ouverture de l\'application :</string>
<string name="if_you_enter_passcode_data_removed">Si vous saisissez ce code à l\'ouverture de l\'application, toutes les données de l\'application seront irréversiblement supprimées !</string>
<string name="set_passcode">Définir le code d\'accès</string>
<string name="send_disappearing_message_30_seconds">30 secondes</string>
<string name="send_disappearing_message_custom_time">Délai personnalisé</string>
<string name="disappearing_message">Message éphémère</string>
<string name="send_disappearing_message">Envoyer un message éphémère</string>
<string name="send_disappearing_message_send">Envoyer</string>
<string name="allow_message_reactions_only_if">Autoriser les réactions aux messages uniquement si votre contact les autorise.</string>
<string name="allow_your_contacts_adding_message_reactions">Autoriser vos contacts à ajouter des réactions aux messages.</string>
<string name="message_reactions">Réactions aux messages</string>
<string name="prohibit_message_reactions">Interdire les réactions aux messages.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Vous et votre contact pouvez ajouter des réactions aux messages.</string>
<string name="message_reactions_prohibited_in_this_chat">Les réactions aux messages sont interdites dans ce chat.</string>
<string name="only_you_can_add_message_reactions">Vous seul pouvez ajouter des réactions aux messages.</string>
<string name="allow_message_reactions">Autoriser les réactions aux messages.</string>
<string name="prohibit_message_reactions_group">Interdire les réactions aux messages.</string>
<string name="group_members_can_add_message_reactions">Les membres du groupe peuvent ajouter des réactions aux messages.</string>
<string name="message_reactions_are_prohibited">Les réactions aux messages sont interdites dans ce groupe.</string>
<string name="custom_time_unit_hours">heures</string>
<string name="custom_time_unit_minutes">minutes</string>
<string name="custom_time_unit_seconds">secondes</string>
<string name="custom_time_unit_weeks">semaines</string>
<string name="send_disappearing_message_1_minute">1 minute</string>
<string name="send_disappearing_message_5_minutes">5 minutes</string>
<string name="custom_time_unit_days">jours</string>
<string name="custom_time_unit_months">mois</string>
<string name="only_your_contact_can_add_message_reactions">Seul votre contact peut ajouter des réactions aux messages.</string>
<string name="error_loading_details">Erreur de chargement des détails</string>
<string name="edit_history">Historique</string>
<string name="info_menu">Info</string>
<string name="received_message">Message reçu</string>
<string name="custom_time_picker_custom">personnalisé</string>
<string name="custom_time_picker_select">Choisir</string>
<string name="whats_new_read_more">En savoir plus</string>
<string name="v5_1_better_messages_descr">- messages vocaux pouvant durer jusqu\'à 5 minutes.
\n- délai personnalisé de disparition.
\n- l\'historique de modification.</string>
<string name="share_text_received_at">Reçu le : %s</string>
<string name="v5_1_better_messages">Meilleurs messages</string>
<string name="v5_1_custom_themes">Thèmes personnalisés</string>
<string name="v5_1_self_destruct_passcode_descr">Toutes les données sont effacées lorsqu\'il est saisi.</string>
<string name="v5_1_custom_themes_descr">Personnalisez et partagez des thèmes de couleurs.</string>
<string name="item_info_current">(actuel)</string>
<string name="info_row_deleted_at">Supprimé à</string>
<string name="info_row_received_at">Reçu le</string>
<string name="info_row_sent_at">Envoyé le</string>
<string name="share_text_database_id">ID de base de données : %d</string>
<string name="share_text_deleted_at">Supprimé à : %s</string>
<string name="share_text_disappears_at">Disparaîtra le : %s</string>
<string name="info_row_disappears_at">Disparaîtra le</string>
<string name="v5_1_japanese_portuguese_interface">Interface en japonais et en portugais</string>
<string name="whats_new_thanks_to_users_contribute_weblate">Merci aux utilisateurs - contribuez via Weblate !</string>
<string name="v5_1_message_reactions_descr">Enfin, les voilà ! 🚀</string>
<string name="info_row_moderated_at">Modéré à</string>
<string name="share_text_updated_at">Enregistrement mis à jour le : %s</string>
<string name="v5_1_message_reactions">Réactions aux messages</string>
<string name="share_text_moderated_at">Modéré à : %s</string>
<string name="info_row_updated_at">Enregistrement mis à jour le</string>
<string name="current_version_timestamp">%s (actuel)</string>
<string name="v5_1_self_destruct_passcode">Code d\'autodestruction</string>
<string name="share_text_sent_at">Envoyé le : %s</string>
<string name="sent_message">Message envoyé</string>
<string name="item_info_no_text">aucun texte</string>
</resources>

View File

@@ -10,6 +10,7 @@
<string name="above_then_preposition_continuation">ऊपर,तब:</string>
<string name="accept_contact_button">स्वीकार करना</string>
<string name="connect_button">जुडिये</string>
<string name="your_contact_address">आपका संपर्क पता</string>
<string name="smp_servers_add_to_another_device">दूसरे उपकरण में जोड़ें</string>
<string name="bold">निडर</string>
<string name="answer_call">कॉल का उत्तर दें</string>
@@ -34,6 +35,7 @@
<string name="accept_connection_request__question">संबंध अनुरोध स्वीकार करें\?</string>
<string name="callstatus_accepted">स्वीकृत कॉल</string>
<string name="accept_contact_incognito_button">गुप्त स्वीकार करें</string>
<string name="accept_requests">निवेदन स्वीकार करो</string>
<string name="smp_servers_preset_add">पूर्वनिर्धारित सर्वर जोड़ें</string>
<string name="users_add">प्रोफ़ाइल जोड़ें</string>
<string name="smp_servers_add">सर्वर जोड़े…</string>
@@ -65,6 +67,7 @@
<string name="chat_preferences_you_allow">आप आज्ञा दें</string>
<string name="welcome">स्वागत!</string>
<string name="la_notice_turn_on">चालू करो</string>
<string name="section_title_welcome_message">स्वागत संदेश</string>
<string name="unknown_message_format">अज्ञात संदेश प्रारूप</string>
<string name="personal_welcome">स्वागत <xliff:g>%1$s</xliff:g>!</string>
<string name="callstate_starting">शुरुआत</string>
@@ -232,6 +235,7 @@
<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>

View File

@@ -207,7 +207,7 @@
<string name="simplex_link_mode_description">Descrizione</string>
<string name="simplex_link_connection">via <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g></string>
<string name="simplex_link_mode_browser_warning">Aprire il link nel browser può ridurre la privacy e la sicurezza della connessione. I link SimpleX non fidati saranno in rosso.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a <xliff:g id="contactName" example="Alice">%1$s</xliff:g>.</string>
<string name="you_are_already_connected_to_vName_via_this_link">Sei già connesso a <xliff:g id="contactName" example="Alice">%1$s!</xliff:g>.</string>
<string name="connection_error_auth_desc">A meno che il tuo contatto non abbia eliminato la connessione o che questo link non sia già stato usato, potrebbe essere un errore; per favore segnalalo.
\nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string>
<string name="error_smp_test_certificate">Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata</string>
@@ -239,7 +239,8 @@
<string name="allow_disappearing_messages_only_if">Consenti i messaggi a tempo solo se il tuo contatto li consente.</string>
<string name="allow_to_delete_messages">Permetti di eliminare irreversibilmente i messaggi inviati.</string>
<string name="allow_your_contacts_to_send_disappearing_messages">Permetti ai tuoi contatti di inviare messaggi a tempo.</string>
<string name="network_enable_socks_info">Accedere ai server via proxy SOCKS sulla porta %d\? Il proxy deve essere avviato prima di attivare questa opzione.</string>
<string name="accept_requests">Accetta le richieste</string>
<string name="network_enable_socks_info">Accedere ai server via proxy SOCKS sulla porta 9050\? Il proxy deve essere avviato prima di attivare questa opzione.</string>
<string name="v4_3_improved_server_configuration_desc">Aggiungi server scansionando codici QR.</string>
<string name="all_group_members_will_remain_connected">Tutti i membri del gruppo resteranno connessi.</string>
<string name="allow_irreversible_message_deletion_only_if">Consenti l\'eliminazione irreversibile dei messaggi solo se il contatto la consente a te.</string>
@@ -272,8 +273,8 @@
<string name="settings_section_title_icon">ICONA APP</string>
<string name="incognito_random_profile_from_contact_description">Verrà inviato un profilo casuale al contatto da cui hai ricevuto questo link</string>
<string name="incognito_random_profile_description">Verrà inviato un profilo casuale al tuo contatto</string>
<string name="onboarding_notifications_mode_off_desc"><b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione (NO servizio in secondo piano).</string>
<string name="onboarding_notifications_mode_service_desc"><b>Consuma più batteria</b>! Servizio in secondo piano sempre attivo: le notifiche sono mostrate non appena i messaggi sono disponibili.</string>
<string name="onboarding_notifications_mode_off_desc"><b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione, il servizio in secondo piano NON verrà usato.</string>
<string name="onboarding_notifications_mode_service_desc"><b>Consuma più batteria</b>! Il servizio in secondo piano è sempre attivo: le notifiche verranno mostrate non appena i messaggi saranno disponibili.</string>
<string name="callstatus_calling">chiamata…</string>
<string name="icon_descr_cancel_link_preview">annulla anteprima link</string>
<string name="cannot_access_keychain">Impossibile accedere al Keystore per salvare la password del database</string>
@@ -283,6 +284,7 @@
<string name="snd_conn_event_switch_queue_phase_changing">cambio indirizzo…</string>
<string name="chat_is_stopped">Chat fermata</string>
<string name="group_member_status_introduced">connessione (presentato)</string>
<string name="contact_requests">Richieste del contatto</string>
<string name="connection_request_sent">Richiesta di connessione inviata!</string>
<string name="delete_link_question">Eliminare il link\?</string>
<string name="delete_link">Elimina link</string>
@@ -337,6 +339,7 @@
<string name="how_to">Come si fa</string>
<string name="how_to_use_your_servers">Come usare i tuoi server</string>
<string name="enter_one_ICE_server_per_line">Server ICE (uno per riga)</string>
<string name="accept_automatically">Automaticamente</string>
<string name="bold">grassetto</string>
<string name="callstatus_ended">chiamata terminata <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_error">errore di chiamata</string>
@@ -357,7 +360,7 @@
<string name="how_to_use_markdown">Come usare il markdown</string>
<string name="icon_descr_audio_call">chiamata audio</string>
<string name="audio_call_no_encryption">chiamata audio (non crittografata e2e)</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Buono per la batteria</b>. Il servizio in secondo piano cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti.</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>Buono per la batteria</b>. Il servizio in secondo piano controlla nuovi messaggi ogni 10 minuti. Potresti perdere chiamate e messaggi urgenti.</string>
<string name="call_already_ended">Chiamata già terminata!</string>
<string name="create_your_profile">Crea il tuo profilo</string>
<string name="decentralized">Decentralizzato</string>
@@ -572,6 +575,7 @@
<string name="you_invited_your_contact">Hai invitato il contatto</string>
<string name="your_chat_profile_will_be_sent_to_your_contact">Il tuo profilo di chat verrà inviato
\nal tuo contatto</string>
<string name="show_QR_code_for_your_contact_to_scan_from_the_app__multiline">Il tuo contatto può scansionare il codice QR dall\'app.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Il tuo contatto deve essere in linea per completare la connessione.
\nPuoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo).</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">Verrai connesso/a quando la tua richiesta di connessione verrà accettata, attendi o controlla più tardi!</string>
@@ -594,23 +598,24 @@
<string name="chat_with_the_founder">Invia domande e idee</string>
<string name="send_us_an_email">Inviaci un\'email</string>
<string name="smp_servers_test_failed">Test del server fallito!</string>
<string name="share_invitation_link">Condividi link una tantum</string>
<string name="share_invitation_link">Condividi link di invito</string>
<string name="chat_lock">SimpleX Lock</string>
<string name="is_not_verified">%s non è verificato/a</string>
<string name="is_verified">%s è verificato/a</string>
<string name="smp_servers">Server SMP</string>
<string name="smp_servers_test_some_failed">Alcuni server hanno fallito il test:</string>
<string name="smp_servers_test_server">Prova server</string>
<string name="smp_servers_test_servers">Prova i server</string>
<string name="smp_servers_test_server">Testa server</string>
<string name="smp_servers_test_servers">Testa i server</string>
<string name="this_string_is_not_a_connection_link">Questa stringa non è un link di connessione!</string>
<string name="to_verify_compare">Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi.</string>
<string name="smp_servers_use_server_for_new_conn">Usa per connessioni nuove</string>
<string name="smp_servers_use_server">Usa il server</string>
<string name="you_can_also_connect_by_clicking_the_link">Puoi anche connetterti cliccando il link. Se si apre nel browser, clicca il pulsante <b>Apri nell\'app mobile</b>.</string>
<string name="your_profile_will_be_sent">Il tuo profilo di chat verrà inviato al tuo contatto</string>
<string name="your_contact_address">Il tuo indirizzo di contatto</string>
<string name="smp_servers_your_server">Il tuo server</string>
<string name="smp_servers_your_server_address">L\'indirizzo del tuo server</string>
<string name="your_simplex_contact_address">Il tuo indirizzo SimpleX</string>
<string name="your_simplex_contact_address">Il tuo indirizzo di contatto di <xliff:g id="appName">SimpleX</xliff:g></string>
<string name="network_disable_socks_info">Se confermi, i server di messaggistica saranno in grado di vedere il tuo indirizzo IP e il tuo fornitore, a quali server ti stai connettendo.</string>
<string name="install_simplex_chat_for_terminal">Installa <xliff:g id="appNameFull">SimpleX Chat</xliff:g> per terminale</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi.</string>
@@ -633,9 +638,11 @@
<string name="network_disable_socks">Usare una connessione internet diretta\?</string>
<string name="network_use_onion_hosts">Usa gli host .onion</string>
<string name="network_enable_socks">Usare il proxy SOCKS\?</string>
<string name="network_socks_toggle">Usa il proxy SOCKS (porta 9050)</string>
<string name="use_simplex_chat_servers__question">Usare i server di <xliff:g id="appNameFull">SimpleX Chat</xliff:g>\?</string>
<string name="using_simplex_chat_servers">Stai usando i server di <xliff:g id="appNameFull">SimpleX Chat</xliff:g>.</string>
<string name="network_use_onion_hosts_prefer">Quando disponibili</string>
<string name="you_can_share_your_address_anybody_will_be_able_to_connect">Puoi condividere il tuo indirizzo come link o come codice QR: chiunque potrà connettersi a te. Non perderai i tuoi contatti se in seguito lo elimini.</string>
<string name="your_ICE_servers">I tuoi server ICE</string>
<string name="your_SMP_servers">I tuoi server SMP</string>
<string name="italic">corsivo</string>
@@ -655,6 +662,7 @@
<string name="callstate_waiting_for_answer">in attesa di risposta…</string>
<string name="callstate_waiting_for_confirmation">in attesa di conferma…</string>
<string name="we_do_not_store_contacts_or_messages_on_servers">Non memorizziamo nessuno dei tuoi contatti o messaggi (una volta recapitati) sui server.</string>
<string name="section_title_welcome_message">MESSAGGIO DI BENVENUTO</string>
<string name="you_can_use_markdown_to_format_messages__prompt">Puoi usare il markdown per formattare i messaggi:</string>
<string name="you_control_your_chat">Sei tu a controllare la tua chat!</string>
<string name="your_current_profile">Il tuo profilo attuale</string>
@@ -1113,7 +1121,7 @@
<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 tuo contatto le consente.</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>
@@ -1126,139 +1134,7 @@
<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">Impostalo al posto dell\'autenticazione di sistema.</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>
<string name="auto_accept_contact">Accetta automaticamente</string>
<string name="auth_open_chat_profiles">Apri i profili di chat</string>
<string name="learn_more">Maggiori informazioni</string>
<string name="scan_qr_to_connect_to_contact">Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app.</string>
<string name="you_can_accept_or_reject_connection">Quando le persone chiedono di connettersi, puoi accettare o rifiutare.</string>
<string name="simplex_address">Indirizzo SimpleX</string>
<string name="theme_colors_section_title">COLORI DEL TEMA</string>
<string name="your_contacts_will_remain_connected">I tuoi contatti resteranno connessi.</string>
<string name="add_address_to_your_profile">Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti.</string>
<string name="create_address_and_let_people_connect">Crea un indirizzo per consentire alle persone di connettersi con te.</string>
<string name="create_simplex_address">Crea indirizzo SimpleX</string>
<string name="share_with_contacts">Condividi con i contatti</string>
<string name="share_address_with_contacts_question">Condividere l\'indirizzo con i contatti\?</string>
<string name="stop_sharing">Smetti di condividere</string>
<string name="enter_welcome_message_optional">Inserisci il messaggio di benvenuto… (facoltativo)</string>
<string name="email_invite_body">Ciao!
\nConnettiti a me tramite SimpleX Chat: %s</string>
<string name="invite_friends">Invita amici</string>
<string name="save_auto_accept_settings">Salva le impostazioni di accettazione automatica</string>
<string name="you_can_create_it_later">Puoi crearlo più tardi</string>
<string name="your_contacts_will_see_it">I tuoi contatti in SimpleX lo vedranno.
\nPuoi modificarlo nelle impostazioni.</string>
<string name="share_address">Condividi indirizzo</string>
<string name="enter_welcome_message">Inserisci il messaggio di benvenuto…</string>
<string name="group_welcome_preview">Anteprima</string>
<string name="you_can_share_this_address_with_your_contacts">Puoi condividere questo indirizzo con i contatti per consentire loro di connettersi con %s.</string>
<string name="import_theme">Importa tema</string>
<string name="theme_simplex">SimpleX</string>
<string name="color_primary_variant">Principale aggiuntivo</string>
<string name="color_secondary_variant">Secondario aggiuntivo</string>
<string name="export_theme">Esporta tema</string>
<string name="import_theme_error">Errore di importazione del tema</string>
<string name="import_theme_error_desc">Assicurati che il file abbia una sintassi YAML corretta. Esporta il tema per avere un esempio della struttura del file del tema.</string>
<string name="color_secondary">Secondario</string>
<string name="color_received_message">Messaggio ricevuto</string>
<string name="color_sent_message">Messaggio inviato</string>
<string name="color_title">Titolo</string>
<string name="one_time_link_short">Link una tantum</string>
<string name="learn_more_about_address">Info sull\'indirizzo SimpleX</string>
<string name="all_your_contacts_will_remain_connected_update_sent">Tutti i tuoi contatti resteranno connessi. L\'aggiornamento del profilo verrà inviato ai tuoi contatti.</string>
<string name="address_section_title">Indirizzo</string>
<string name="color_background">Sfondo</string>
<string name="continue_to_next_step">Continua</string>
<string name="error_setting_address">Errore di impostazione dell\'indirizzo</string>
<string name="dont_create_address">Non creare un indirizzo</string>
<string name="customize_theme_title">Personalizza il tema</string>
<string name="dark_theme">Tema scuro</string>
<string name="if_you_cant_meet_in_person">Se non potete incontrarvi di persona, mostra il codice QR in una videochiamata o condividi il link.</string>
<string name="email_invite_subject">Parliamo in SimpleX Chat</string>
<string name="profile_update_will_be_sent_to_contacts">L\'aggiornamento del profilo verrà inviato ai tuoi contatti.</string>
<string name="read_more_in_user_guide_with_link">Maggiori informazioni nella <font color="#0088ff">Guida per l\'utente</font>.</string>
<string name="color_surface">Menu e avvisi</string>
<string name="stop_sharing_address">Smettere di condividere l\'indirizzo\?</string>
<string name="save_settings_question">Salvare le impostazioni\?</string>
<string name="you_wont_lose_your_contacts_if_delete_address">Non perderai i contatti se in seguito elimini il tuo indirizzo.</string>
<string name="you_can_share_your_address">Puoi condividere il tuo indirizzo come link o codice QR: chiunque può connettersi a te.</string>
<string name="opening_database">Apertura del database…</string>
<string name="change_self_destruct_mode">Cambia modalità di autodistruzione</string>
<string name="enabled_self_destruct_passcode">Attiva il codice di autodistruzione</string>
<string name="self_destruct">Autodistruzione</string>
<string name="self_destruct_passcode_changed">Codice di autodistruzione modificato!</string>
<string name="self_destruct_passcode_enabled">Codice di autodistruzione attivato!</string>
<string name="all_app_data_will_be_cleared">Tutti i dati dell\'app vengono eliminati.</string>
<string name="app_passcode_replaced_with_self_destruct">Il codice di accesso dell\'app viene sostituito da un codice di autodistruzione.</string>
<string name="enable_self_destruct">Attiva l\'autodistruzione</string>
<string name="if_you_enter_self_destruct_code">Se inserisci il tuo codice di autodistruzione mentre apri l\'app:</string>
<string name="self_destruct_new_display_name">Nome da mostrare nuovo:</string>
<string name="self_destruct_passcode">Codice di autodistruzione</string>
<string name="empty_chat_profile_is_created">Viene creato un profilo di chat vuoto con il nome scelto e l\'app si apre come al solito.</string>
<string name="if_you_enter_passcode_data_removed">Se inserisci questo codice all\'apertura dell\'app, tutti i dati di essa verranno rimossi in modo irreversibile!</string>
<string name="set_passcode">Imposta codice</string>
<string name="change_self_destruct_passcode">Cambia codice di autodistruzione</string>
<string name="message_reactions">Reazioni ai messaggi</string>
<string name="message_reactions_prohibited_in_this_chat">Le reazioni ai messaggi sono vietate in questa chat.</string>
<string name="message_reactions_are_prohibited">Le reazioni ai messaggi sono vietate in questo gruppo.</string>
<string name="only_you_can_add_message_reactions">Solo tu puoi aggiungere reazioni ai messaggi.</string>
<string name="prohibit_message_reactions">Proibisci le reazioni ai messaggi.</string>
<string name="prohibit_message_reactions_group">Proibisci le reazioni ai messaggi.</string>
<string name="allow_message_reactions">Consenti reazioni ai messaggi.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi.</string>
<string name="only_your_contact_can_add_message_reactions">Solo il tuo contatto può aggiungere reazioni ai messaggi.</string>
<string name="allow_your_contacts_adding_message_reactions">Consenti ai tuoi contatti di aggiungere reazioni ai messaggi.</string>
<string name="allow_message_reactions_only_if">Consenti reazioni ai messaggi solo se il tuo contatto le consente.</string>
<string name="group_members_can_add_message_reactions">I membri del gruppo possono aggiungere reazioni ai messaggi.</string>
<string name="send_disappearing_message_30_seconds">30 secondi</string>
<string name="send_disappearing_message_send">Invia</string>
<string name="send_disappearing_message">Invia messaggio a tempo</string>
<string name="custom_time_unit_days">giorni</string>
<string name="custom_time_unit_hours">ore</string>
<string name="custom_time_unit_minutes">minuti</string>
<string name="custom_time_unit_months">mesi</string>
<string name="custom_time_unit_seconds">secondi</string>
<string name="custom_time_unit_weeks">settimane</string>
<string name="custom_time_picker_select">Seleziona</string>
<string name="send_disappearing_message_1_minute">1 minuto</string>
<string name="send_disappearing_message_custom_time">Tempo personalizzato</string>
<string name="send_disappearing_message_5_minutes">5 minuti</string>
<string name="custom_time_picker_custom">personalizzato</string>
<string name="disappearing_message">Messaggio a tempo</string>
<string name="error_loading_details">Errore caricamento dettagli</string>
<string name="info_menu">Informazioni</string>
<string name="edit_history">Cronologia</string>
<string name="received_message">Messaggio ricevuto</string>
<string name="sent_message">Messaggio inviato</string>
<string name="share_text_database_id">ID database: %d</string>
<string name="info_row_moderated_at">Moderato il</string>
<string name="share_text_moderated_at">Moderato il: %s</string>
<string name="info_row_received_at">Ricevuto il</string>
<string name="share_text_received_at">Ricevuto il: %s</string>
<string name="info_row_updated_at">Registro aggiornato il</string>
<string name="share_text_updated_at">Registro aggiornato il: %s</string>
<string name="info_row_sent_at">Inviato il</string>
<string name="share_text_sent_at">Inviato il: %s</string>
<string name="current_version_timestamp">%s (attuale)</string>
<string name="whats_new_read_more">Leggi tutto</string>
<string name="v5_1_self_destruct_passcode_descr">Tutti i dati vengono cancellati quando inserito.</string>
<string name="v5_1_custom_themes_descr">Personalizza e condividi temi di colore.</string>
<string name="v5_1_custom_themes">Temi personalizzati</string>
<string name="v5_1_message_reactions_descr">Finalmente le abbiamo! 🚀</string>
<string name="v5_1_message_reactions">Reazioni ai messaggi</string>
<string name="v5_1_self_destruct_passcode">Codice di autodistruzione</string>
<string name="whats_new_thanks_to_users_contribute_weblate">Grazie agli utenti contribuite via Weblate!</string>
<string name="v5_1_better_messages_descr">- messaggi vocali fino a 5 minuti.
\n- tempo di scomparsa personalizzato.
\n- cronologia delle modifiche.</string>
<string name="v5_1_better_messages">Messaggi migliorati</string>
<string name="v5_1_japanese_portuguese_interface">Interfaccia giapponese e portoghese</string>
<string name="item_info_current">(attuale)</string>
<string name="info_row_deleted_at">Eliminato il</string>
<string name="share_text_deleted_at">Eliminato il: %s</string>
<string name="info_row_disappears_at">Scompare il</string>
<string name="share_text_disappears_at">Scompare il: %s</string>
</resources>

View File

@@ -1,840 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="color_primary">הדגשה</string>
<string name="accept_contact_button">אשר</string>
<string name="accept_connection_request__question">לאשר בקשת חיבור\?</string>
<string name="about_simplex_chat">אודות <xliff:g id="appNameFull">SimpleX Chat</xliff:g></string>
<string name="accept_requests">אשר בקשות</string>
<string name="about_simplex">אודות SimpleX</string>
<string name="accept_call_on_lock_screen">ענה</string>
<string name="chat_item_ttl_week">שבוע</string>
<string name="accept_feature">קבל</string>
<string name="chat_item_ttl_day">יום</string>
<string name="chat_item_ttl_month">חודש</string>
<string name="a_plus_b">a + b</string>
<string name="above_then_preposition_continuation">למעלה, אז:</string>
<string name="accept">אשר</string>
<string name="callstatus_accepted">שיחה שהתקבלה</string>
<string name="allow_verb">אפשר</string>
<string name="clear_chat_warning">כל ההודעות יימחקו לא ניתן לבטל זאת! ההודעות יימחקו רק עבורך.</string>
<string name="accept_contact_incognito_button">אשר זהות נסתרת</string>
<string name="smp_servers_preset_add">הוסף שרתים מוגדרים מראש</string>
<string name="smp_servers_add">הוסף שרת…</string>
<string name="network_enable_socks_info">לגשת לשרתים דרך פרוקסי SOCKS בפורט %d\? הפרוקסי חייב לפעול לפני הפעלת אפשרות זו.</string>
<string name="network_settings">הגדרות רשת מתקדמות</string>
<string name="appearance_settings">מראה</string>
<string name="app_version_name">גרסת האפליקציה: v%s</string>
<string name="app_version_code">גרסת אפליקציה: %s</string>
<string name="section_title_welcome_message">הודעת פתיחה</string>
<string name="all_your_contacts_will_remain_connected">כל אנשי הקשר יישארו מחוברים.</string>
<string name="always_use_relay">תמיד להשתמש בממסר</string>
<string name="answer_call">ענה לשיחה</string>
<string name="all_group_members_will_remain_connected">כל חברי הקבוצה יישארו מחוברים.</string>
<string name="button_add_welcome_message">הוסף הודעת פתיחה</string>
<string name="button_welcome_message">הודעת פתיחה</string>
<string name="group_welcome_title">הודעת פתיחה</string>
<string name="chat_preferences_always">תמיד</string>
<string name="allow_disappearing_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_voice_messages_only_if">אפשר הודעות קוליות רק אם איש הקשר מאפשר אותן.</string>
<string name="allow_your_contacts_to_call">אפשר לאנשי קשר להתקשר אליך.</string>
<string name="allow_to_delete_messages">אפשר מחיקה בלתי הפיכה של הודעות שנשלחו.</string>
<string name="allow_to_send_disappearing">אפשר שליחת הודעות נעלמות.</string>
<string name="allow_to_send_voice">אפשר שליחת הודעות קוליות.</string>
<string name="group_member_role_admin">מנהל</string>
<string name="v4_2_group_links_desc">מנהלים יכולים ליצור קישורי הצטרפות לקבוצות.</string>
<string name="users_delete_all_chats_deleted">כל הצ׳אטים וההודעות יימחקו לא ניתן לבטל זאת!</string>
<string name="users_add">הוסף פרופיל</string>
<string name="v4_3_improved_server_configuration_desc">הוסף שרתים על ידי סריקת קוד QR.</string>
<string name="smp_servers_add_to_another_device">הוסף למכשיר אחר</string>
<string name="allow_calls_only_if">אפשר שיחות רק אם איש הקשר מאפשר אותן.</string>
<string name="allow_irreversible_message_deletion_only_if">אפשר לאנשי קשר מחיקת הודעות בלתי הפיכה רק אם הם מאפשרים לך לעשות זאת.</string>
<string name="allow_direct_messages">אפשר שליחת הודעות ישירות לחברי הקבוצה.</string>
<string name="allow_voice_messages_question">לאפשר הודעות קוליות\?</string>
<string name="allow_your_contacts_to_send_voice_messages">אפשר לאנשי קשר לשלוח הודעות קוליות.</string>
<string name="notifications_mode_service">תמיד פעיל</string>
<string name="keychain_is_storing_securely">ישנו שימוש ב־Android Keystore כדי לאחסן בבטחה את הסיסמה דבר המאפשר לשירות ההתראות לעבוד.</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore יאחסן בבטחה את הסיסמה לאחר הפעלה מחדש של האפליקציה או שינוי הסיסמה דבר המאפשר קבלת התראות.</string>
<string name="full_backup">גיבוי נתוני האפליקציה</string>
<string name="settings_section_title_icon">סמל האפליקציה</string>
<string name="notifications_mode_off_desc">האפליקציה יכולה לקבל התראות רק כאשר היא מופעלת, לא יופעל שירות ברקע.</string>
<string name="v5_0_app_passcode">קוד גישה לאפליקציה</string>
<string name="app_version_title">גרסת האפליקציה</string>
<string name="incognito_random_profile_description">פרופיל אקראי יישלח לאיש הקשר</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_video_asked_to_receive">הנמען התבקש לקבל את הסרטון</string>
<string name="icon_descr_asked_to_receive">הנמען התבקש לקבל את התמונה</string>
<string name="attach">צרף</string>
<string name="icon_descr_audio_call">שיחת שמע</string>
<string name="icon_descr_audio_off">שמע כבוי</string>
<string name="icon_descr_audio_on">שמע פעיל</string>
<string name="calls_prohibited_with_this_contact">שיחות שמע/וידאו אסורות.</string>
<string name="v4_6_audio_video_calls">שיחות שמע ווידאו</string>
<string name="audio_call_no_encryption">שיחת שמע (לא מוצפנת מקצה־לקצה)</string>
<string name="audio_video_calls">שיחות שמע/וידאו</string>
<string name="settings_audio_video_calls">שיחות שמע ווידאו</string>
<string name="turning_off_service_and_periodic">מיטוב הסוללה פעיל, מכבה את שירות הרקע ובקשות תקופתיות לקבלת הודעות חדשות. ניתן להפעיל אותם מחדש בהגדרות.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><b>ניתן להשבית זאת בהגדרות</b> התראות עדיין יוצגו בזמן שהאפליקציה פועלת.</string>
<string name="notifications_mode_service_desc">שירות רקע תמיד מופעל התראות יוצגו מיד כאשר הודעות מגיעות.</string>
<string name="la_authenticate">אימות</string>
<string name="la_auth_failed">אימות נכשל</string>
<string name="back">חזרה</string>
<string name="accept_automatically">אוטומטית</string>
<string name="bold">מודגש</string>
<string name="integrity_msg_bad_hash">גיבוב הודעה שגוי</string>
<string name="integrity_msg_bad_id">מזהה הודעה שגוי</string>
<string name="alert_title_msg_bad_hash">גיבוב הודעה שגוי</string>
<string name="alert_title_msg_bad_id">מזהה הודעה שגוי</string>
<string name="auto_accept_images">קבל אוטומטית תמונות</string>
<string name="impossible_to_recover_passphrase"><b>שימו לב</b>: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה.</string>
<string name="available_in_v51">"
\nזמין מ־v5.1"</string>
<string name="both_you_and_your_contact_can_send_voice">גם אתם וגם איש הקשר יכולים לשלוח הודעות קוליות.</string>
<string name="both_you_and_your_contact_can_make_calls">גם אתם וגם איש הקשר יכולים לבצע שיחות.</string>
<string name="v4_2_auto_accept_contact_requests">אשר אוטומטית בקשות ליצירת קשר.</string>
<string name="authentication_cancelled">אימות בוטל</string>
<string name="auth_unavailable">אימות לא זמין</string>
<string name="add_new_contact_to_create_one_time_QR_code"><b>הוסף איש קשר חדש</b>: ליצירת קוד QR חד פעמי עבור איש הקשר שלך.</string>
<string name="onboarding_notifications_mode_off_desc"><b>הטוב ביותר לסוללה</b>. התראות יוצגו רק כאשר האפליקציה מופעלת (ללא שירות רקע).</string>
<string name="onboarding_notifications_mode_periodic_desc"><b>טוב לסוללה</b>. שירות הרקע ייבדוק הודעות כל 10 דקות. שיחות או הודעות דחופות עלולות להתפספס.</string>
<string name="both_you_and_your_contacts_can_delete">גם אתם וגם איש הקשר יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</string>
<string name="both_you_and_your_contact_can_send_disappearing">גם אתם וגם איש הקשר יכולים לשלוח הודעות נעלמות.</string>
<string name="cannot_receive_file">לא ניתן לקבל את הקובץ</string>
<string name="icon_descr_cancel_image_preview">בטל תצוגה מקדימה של תמונות</string>
<string name="cancel_verb">ביטול</string>
<string name="icon_descr_cancel_live_message">בטל הודעה חיה</string>
<string name="use_camera_button">מצלמה</string>
<string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>סריקת קוד QR</b>: כדי להתחבר לאיש קשר המציג לכם קוד QR.</string>
<string name="icon_descr_cancel_link_preview">בטל תצוגה מקדימה של קישורים</string>
<string name="callstatus_error">שגיאת שיחה</string>
<string name="callstatus_in_progress">שיחה מתמשכת</string>
<string name="onboarding_notifications_mode_service_desc"><b>צורך יותר סוללה</b>! שירות רקע תמיד מופעל התראות יוצגו מיד כאשר הודעות מגיעות.</string>
<string name="call_already_ended">השיחה כבר הסתיימה!</string>
<string name="call_on_lock_screen">שיחות במסך הנעילה:</string>
<string name="icon_descr_call_ended">השיחה הסתיימה</string>
<string name="icon_descr_call_progress">שיחה מתמשכת</string>
<string name="settings_section_title_calls">שיחות</string>
<string name="cannot_access_keychain">לא ניתן לגשת ל־Keystore כדי לאחסן את סיסמת מסד הנתונים</string>
<string name="cant_delete_user_profile">לא ניתן למחוק פרופיל משתמש!</string>
<string name="feature_cancelled_item">בוטל %s</string>
<string name="v4_5_transport_isolation_descr">לפי פרופיל צ׳אט (ברירת מחדל) או לפי חיבור (בביטא).</string>
<string name="callstatus_calling">מתקשר…</string>
<string name="callstatus_ended">השיחה הסתיימה <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="icon_descr_cancel_file_preview">בטל תצוגה מקדימה של קבצים</string>
<string name="connect_via_contact_link">להתחבר באמצעות קישור ליצירת קשר\?</string>
<string name="connect_via_link_verb">התחבר</string>
<string name="connect_via_group_link">להתחבר באמצעות קישור קבוצה\?</string>
<string name="server_connected">מחובר</string>
<string name="server_connecting">מתחבר</string>
<string name="display_name_connecting">מתחבר…</string>
<string name="connection_error">שגיאת חיבור</string>
<string name="connection_timeout">תם זמן ניסיון החיבור</string>
<string name="connection_error_auth">שגיאת חיבור (אימות)</string>
<string name="smp_server_test_connect">התחבר</string>
<string name="smp_server_test_compare_file">השווה קובץ</string>
<string name="database_initialization_error_title">לא ניתן לאתחל את מסד הנתונים</string>
<string name="notifications_mode_periodic_desc">בודק הודעות חדשות כל 10 דקות למשך עד דקה אחת.</string>
<string name="notification_contact_connected">מחובר</string>
<string name="la_change_app_passcode">שנה קוד גישה</string>
<string name="auth_confirm_credential">אימות אישורך</string>
<string name="chat_with_developers">צ׳אט עם המפתחים</string>
<string name="contact_connection_pending">מתחבר…</string>
<string name="group_connection_pending">מתחבר…</string>
<string name="confirm_verb">אשר</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="icon_descr_close_button">לחצן סגירה</string>
<string name="connection_request_sent">בקשת חיבור נשלחה!</string>
<string name="clear_verification">נקה אימות</string>
<string name="connect_button">התחבר</string>
<string name="chat_console">מסוף צ׳אט</string>
<string name="smp_servers_check_address">בידקו את כתובת השרת ונסו שוב.</string>
<string name="configure_ICE_servers">הגדר שרתי ICE</string>
<string name="network_session_mode_user">פרופיל צ׳אט</string>
<string name="network_session_mode_entity">חיבור</string>
<string name="confirm_password">אימות סיסמה</string>
<string name="colored">צבעוני</string>
<string name="callstatus_connecting">מתחבר לשיחה…</string>
<string name="callstate_connected">מחובר</string>
<string name="callstate_connecting">מתחבר…</string>
<string name="icon_descr_call_connecting">מתחבר לשיחה</string>
<string name="confirm_passcode">אימות קוד גישה</string>
<string name="change_lock_mode">שנה מצב נעילה</string>
<string name="settings_section_title_chats">צ׳אטים</string>
<string name="chat_database_section">מסד נתונים</string>
<string name="chat_is_running">צ׳אט פעיל</string>
<string name="chat_is_stopped">צ׳אט מופסק</string>
<string name="chat_database_deleted">מסד הנתונים של הצ׳אט נמחק</string>
<string name="chat_database_imported">‬מסד הנתונים של הצ׳אט יובא</string>
<string name="confirm_database_upgrades">אשר שדרוגי מסד נתונים</string>
<string name="chat_archive_header">ארכיון צ׳אט</string>
<string name="chat_archive_section">ארכיון צ׳אט</string>
<string name="chat_is_stopped_indication">צ׳אט מופסק</string>
<string name="alert_title_cant_invite_contacts">לא ניתן להזמין את אנשי הקשר!</string>
<string name="rcv_group_event_changed_your_role">שונה תפקידך ל%s</string>
<string name="rcv_group_event_member_connected">מחובר</string>
<string name="rcv_conn_event_switch_queue_phase_completed">כתובתך שונתה</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="group_member_status_connected">מחובר</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_introduced">מתחבר (בוצעה היכרות)</string>
<string name="invite_prohibited">לא ניתן להזמין את איש הקשר!</string>
<string name="clear_contacts_selection_button">נקה</string>
<string name="group_member_status_complete">חיבור הושלם</string>
<string name="group_member_status_connecting">מתחבר</string>
<string name="change_verb">שנה</string>
<string name="change_member_role_question">לשנות תפקיד בקבוצה\?</string>
<string name="change_role">שנה תפקיד</string>
<string name="info_row_connection">חיבור</string>
<string name="chat_preferences">העדפות צ׳אט</string>
<string name="v4_6_chinese_spanish_interface">ממשק סינית וספרדית</string>
<string name="v4_4_verify_connection_security_desc">השוואת קודי אבטחה עם אנשי הקשר שלך.</string>
<string name="change_database_passphrase_question">לשנות את סיסמת מסד הנתונים\?</string>
<string name="rcv_group_event_changed_member_role">שונה התפקיד של %s ל%s</string>
<string name="confirm_new_passphrase">אימות סיסמה חדשה…</string>
<string name="icon_descr_server_status_connected">מחובר</string>
<string name="display_name_connection_established">חיבור נוצר</string>
<string name="connection_local_display_name">חיבור <xliff:g id="connection ID" example="1">%1$d</xliff:g></string>
<string name="connect_via_invitation_link">להתחבר באמצעות קישור הזמנה\?</string>
<string name="contact_already_exists">איש הקשר כבר קיים</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">איש הקשר וכל ההודעות יימחקו לא ניתן לבטל זאת!</string>
<string name="connect_via_link_or_qr">התחברות באמצעות קישור / קוד QR</string>
<string name="connect_via_link">התחברות באמצעות קישור</string>
<string name="chat_preferences_contact_allows">איש הקשר מאפשר</string>
<string name="notification_preview_somebody">איש קשר מוסתר:</string>
<string name="notification_preview_mode_contact">שם איש קשר</string>
<string name="alert_title_contact_connection_pending">איש הקשר עוד לא מחובר!</string>
<string name="status_contact_has_e2e_encryption">לאיש הקשר יש הצפנה מקצה־לקצה</string>
<string name="icon_descr_contact_checked">איש קשר נבדק</string>
<string name="status_contact_has_no_e2e_encryption">לאיש הקשר אין הצפנה מקצה־לקצה</string>
<string name="smp_server_test_create_queue">צור תור</string>
<string name="smp_server_test_create_file">צור קובץ</string>
<string name="copy_verb">העתק</string>
<string name="icon_descr_context">סמל מידע נוסף</string>
<string name="copied">הועתק ללוח</string>
<string name="share_one_time_link">צור קישור הזמנה חד־פעמי</string>
<string name="create_group">צור קבוצה סודית</string>
<string name="create_one_time_link">צור קישור הזמנה חד־פעמי</string>
<string name="contribute">תרומה</string>
<string name="core_version">גרסת ליבה: v%s</string>
<string name="create_address">צור כתובת</string>
<string name="contact_requests">בקשות ליצירת קשר</string>
<string name="create_profile_button">צור</string>
<string name="create_profile">צור פרופיל</string>
<string name="create_your_profile">יצירת הפרופיל שלך</string>
<string name="archive_created_on_ts">נוצר ב־<xliff:g id="archive_ts">%1$s</xliff:g></string>
<string name="create_group_link">צור קישור קבוצה</string>
<string name="button_create_group_link">צור קישור</string>
<string name="group_member_status_creator">יוצר הקבוצה</string>
<string name="create_secret_group_title">צור קבוצה סודית</string>
<string name="contact_preferences">העדפות איש קשר</string>
<string name="contacts_can_mark_messages_for_deletion">אנשי קשר יכולים לסמן הודעות למחיקה; ניתן יהיה לראות אותן.</string>
<string name="smp_server_test_delete_queue">מחק תור</string>
<string name="la_current_app_passcode">קוד גישה נוכחי</string>
<string name="delete_verb">מחק</string>
<string name="button_delete_contact">מחק איש קשר</string>
<string name="scan_QR_code">סריקת קוד QR</string>
<string name="developer_options">מזהי מסד נתונים ואפשרות בידוד תעבורה.</string>
<string name="delete_address">מחק כתובת</string>
<string name="settings_developer_tools">כלי מפתחים</string>
<string name="database_passphrase">סיסמת מסד הנתונים</string>
<string name="delete_messages_after">מחק הודעות אחרי</string>
<string name="encrypted_with_random_passphrase">מסד הנתונים מוצפן באמצעות סיסמה אקראית, באפשרותך לשנות אותה.</string>
<string name="database_passphrase_will_be_updated">סיסמת הצפנת מסד הנתונים תעודכן.</string>
<string name="database_encryption_will_be_updated">סיסמת הצפנת מסד הנתונים תעודכן ותאוחסן ב־Keystore.</string>
<string name="passphrase_is_different">סיסמת מסד הנתונים שונה מזו המאוחסנת ב־Keystore.</string>
<string name="mtr_error_no_down_migration">גרסת מסד הנתונים חדשה יותר מהאפליקציה, אך אין העברת נתונים עבור: %s</string>
<string name="users_delete_profile_for">מחיקת פרופיל צ׳אט עבור</string>
<string name="delete_profile">מחק פרופיל</string>
<string name="delete_contact_question">למחוק איש קשר\?</string>
<string name="delete_contact_menu_action">מחק</string>
<string name="delete_group_menu_action">מחק</string>
<string name="smp_servers_delete_server">מחק שרת</string>
<string name="create_address_and_let_people_connect">צרו כתובת כדי לאפשר לאנשים להתחבר אליכם.</string>
<string name="decentralized">מבוזר</string>
<string name="set_password_to_export_desc">מסד הנתונים מוצפן באמצעות סיסמה אקראית. אנא שנו אותה לפני הייצוא.</string>
<string name="delete_chat_archive_question">למחוק ארכיון צ׳אט\?</string>
<string name="delete_chat_profile">מחק פרופיל צ׳אט</string>
<string name="chat_preferences_default">ברירת מחדל (%s)</string>
<string name="ttl_day">%d יום</string>
<string name="ttl_h">%d שעות</string>
<string name="ttl_hour">%d שעה</string>
<string name="ttl_d">%d ימים</string>
<string name="ttl_days">%d ימים</string>
<string name="ttl_hours">%d שעות</string>
<string name="decryption_error">שגיאת פענוח הצפנה</string>
<string name="deleted_description">נמחק</string>
<string name="simplex_link_mode_description">תיאור</string>
<string name="delete_member_message__question">למחוק הודעת חבר קבוצה\?</string>
<string name="delete_message__question">למחוק הודעה\?</string>
<string name="image_decoding_exception_title">שגיאת פענוח קידוד</string>
<string name="delete_pending_connection__question">למחוק חיבור ממתין\?</string>
<string name="one_time_link_short">קישור חד־פעמי</string>
<string name="network_session_mode_transport_isolation">בידוד תעבורה</string>
<string name="all_your_contacts_will_remain_connected_update_sent">כל אנשי הקשר יישארו מחוברים. עדכון הפרופיל יישלח לאנשי הקשר.</string>
<string name="create_simplex_address">צור כתובת SimpleX</string>
<string name="delete_address__question">למחוק כתובת\?</string>
<string name="auto_accept_contact">אשר אוטומטית</string>
<string name="continue_to_next_step">המשך</string>
<string name="delete_image">מחק תמונה</string>
<string name="delete_chat_profile_question">למחוק פרופיל צ׳אט\?</string>
<string name="delete_files_and_media_question">למחוק קבצים ומדיה\?</string>
<string name="delete_files_and_media_for_all_users">מחק קבצים עבור כל פרופילי הצ׳אט</string>
<string name="current_passphrase">סיסמה נוכחית…</string>
<string name="database_encrypted">מסד הנתונים מוצפן!</string>
<string name="delete_messages">מחק הודעות</string>
<string name="database_will_be_encrypted">מסד הנתונים יוצפן.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">מסד הנתונים יוצפן והסיסמה תאוחסן ב־Keystore.</string>
<string name="database_error">שגיאת מסד נתונים</string>
<string name="database_passphrase_is_required">סיסמת מסד הנתונים נדרשת כדי לפתוח את הצ׳אט.</string>
<string name="database_downgrade">שדרוג לאחור של מסד הנתונים</string>
<string name="database_upgrade">שדרוג מסד הנתונים</string>
<string name="num_contacts_selected">%d אנשי קשר נבחרו</string>
<string name="delete_link_question">למחוק קישור\?</string>
<string name="theme_dark">כהה</string>
<string name="color_secondary">משני</string>
<string name="color_secondary_variant">משני נוסף</string>
<string name="color_background">רקע</string>
<string name="learn_more_about_address">אודות כתובת SimpleX</string>
<string name="add_address_to_your_profile">הוסיפו את הכתובת לפרופיל שלכם, כך שאנשי קשר יוכלו לשתף אותה עם אנשים אחרים. עדכון הפרופיל יישלח לאנשי הקשר.</string>
<string name="color_primary_variant">הדגשה נוספת</string>
<string name="address_section_title">כתובת</string>
<string name="maximum_supported_file_size">גודל הקובץ המרבי הנתמך כרגע הוא <xliff:g id="maxFileSize">%1$s</xliff:g></string>
<string name="customize_theme_title">התאמה אישית</string>
<string name="dark_theme">מצב כהה</string>
<string name="info_row_database_id">מזהה מסד נתונים</string>
<string name="v4_5_transport_isolation">בידוד תעבורה</string>
<string name="database_passphrase_and_export">סיסמה וייצוא של מסד הנתונים</string>
<string name="delete_after">מחק אחרי</string>
<string name="delete_files_and_media_all">מחק את כל הקבצים</string>
<string name="delete_archive">מחק ארכיון</string>
<string name="for_me_only">מחק עבורי</string>
<string name="delete_link">מחק קישור</string>
<string name="users_delete_question">למחוק פרופיל צ׳אט\?</string>
<string name="delete_database">מחק מסד נתונים</string>
<string name="rcv_group_event_group_deleted">קבוצה נמחקה</string>
<string name="smp_server_test_delete_file">מחק קובץ</string>
<string name="full_deletion">מחק לכולם</string>
<string name="button_delete_group">מחק קבוצה</string>
<string name="delete_group_question">למחוק קבוצה\?</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 שולחן עבודה: סירקו את קוד ה־QR המוצג באפליקציה, באמצעות <b>סריקת קוד QR</b>.</string>
<string name="settings_section_title_develop">פיתוח</string>
<string name="settings_section_title_device">מכשיר</string>
<string name="auth_device_authentication_is_disabled_turning_off">נעילת המכשיר מושבתת. מכבה נעילת SimpleX.</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">נעילת המכשיר אינה מופעלת. ניתן להפעיל את נעילת SimpleX, לאחר שתפעילו את נעילת המכשיר.</string>
<string name="total_files_count_and_size">%d קבצים בגודל כולל של %s</string>
<string name="server_error">שגיאה</string>
<string name="error_loading_smp_servers">שגיאה בטעינת שרתי SMP</string>
<string name="failed_to_create_user_title">שגיאה ביצירת פרופיל!</string>
<string name="failed_to_create_user_duplicate_title">שם תצוגה כבר קיים!</string>
<string name="error_creating_address">שגיאה ביצירת כתובת</string>
<string name="error_accepting_contact_request">שגיאה באישור בקשה ליצירת קשר</string>
<string name="la_minutes">%d דקות</string>
<string name="la_seconds">%d שניות</string>
<string name="la_enter_app_passcode">הזינו קוד גישה</string>
<string name="auth_enable_simplex_lock">הפעלת נעילת SimpleX</string>
<string name="edit_verb">ערוך</string>
<string name="display_name__field">שם תצוגה:</string>
<string name="edit_image">ערוך תמונה</string>
<string name="enter_correct_passphrase">הזינו סיסמה נכונה.</string>
<string name="mtr_error_different">העברת נתונים שונה באפליקציה/מסד נתונים: %s / %s</string>
<string name="downgrade_and_open_chat">שדרג לאחור ופתח צ׳אט</string>
<string name="error_changing_role">שגיאה בשינוי תפקיד</string>
<string name="network_option_enable_tcp_keep_alive">הפעל TCP keep-alive</string>
<string name="disappearing_prohibited_in_this_chat">הודעות נעלמות אסורות בצ׳אט זה.</string>
<string name="icon_descr_server_status_disconnected">מנותק</string>
<string name="icon_descr_server_status_error">שגיאה</string>
<string name="smp_servers_enter_manually">הזנת שרת ידנית</string>
<string name="callstate_ended">הסתיימה</string>
<string name="enable_lock">הפעל נעילה</string>
<string name="error_exporting_chat_database">שגיאה בייצוא מסד הנתונים של הצ׳אט</string>
<string name="enable_automatic_deletion_question">לאפשר מחיקת הודעות אוטומטית\?</string>
<string name="error_encrypting_database">שגיאה בהצפנת מסד הנתונים</string>
<string name="enter_password_to_show">הזינו סיסמה בחיפוש</string>
<string name="error_adding_members">שגיאה בהוספת חברי קבוצה</string>
<string name="error_deleting_contact_request">שגיאה במחיקת בקשה ליצירת קשר</string>
<string name="error_deleting_group">שגיאה במחיקת קבוצה</string>
<string name="error_joining_group">שגיאה בהצטרפות לקבוצה</string>
<string name="smp_server_test_disconnect">התנתק</string>
<string name="smp_server_test_download_file">הורד קובץ</string>
<string name="error_changing_address">שגיאה בשינוי כתובת</string>
<string name="error_deleting_user">שגיאה במחיקת פרופיל משתמש</string>
<string name="auth_disable_simplex_lock">השבתת נעילת SimpleX</string>
<string name="dont_create_address">לא ליצור כתובת</string>
<string name="no_call_on_lock_screen">מושבת</string>
<string name="integrity_msg_duplicate">הודעה כפולה</string>
<string name="error_deleting_database">שגיאה במחיקת מסד הנתונים של הצ׳אט</string>
<string name="error_importing_database">שגיאה בייבוא מסד הנתונים של הצ׳אט</string>
<string name="encrypt_database">הצפן</string>
<string name="error_changing_message_deletion">שגיאה בשינוי הגדרה</string>
<string name="encrypt_database_question">להצפין מסד נתונים\?</string>
<string name="encrypted_database">מסד נתונים מוצפן</string>
<string name="button_edit_group_profile">ערוך פרופיל קבוצה</string>
<string name="error_creating_link_for_group">שגיאה ביצירת קישור קבוצה</string>
<string name="error_deleting_link_for_group">שגיאה במחיקת קישור קבוצה</string>
<string name="dont_show_again">אל תציג שוב</string>
<string name="direct_messages">הודעות ישירות</string>
<string name="timed_messages">הודעות נעלמות</string>
<string name="feature_enabled">מופעל</string>
<string name="feature_enabled_for_contact">מופעל עבור איש הקשר</string>
<string name="feature_enabled_for_you">מופעל עבורך</string>
<string name="disappearing_messages_are_prohibited">הודעות נעלמות אסורות בקבוצה זו.</string>
<string name="ttl_min">%d דקה</string>
<string name="ttl_s">%d שניות</string>
<string name="v4_4_disappearing_messages">הודעות נעלמות</string>
<string name="ttl_m">%d דקות</string>
<string name="ttl_month">%d חודש</string>
<string name="ttl_w">%d שבועות</string>
<string name="ttl_week">%d שבוע</string>
<string name="ttl_weeks">%d שבועות</string>
<string name="v4_5_multiple_chat_profiles_descr">שמות שונים, אווטארים ובידוד תעבורה.</string>
<string name="conn_level_desc_direct">ישיר</string>
<string name="direct_messages_are_prohibited_in_chat">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</string>
<string name="display_name">שם תצוגה</string>
<string name="display_name_cannot_contain_whitespace">שם תצוגה אינו יכול להכיל רווחים.</string>
<string name="ttl_months">%d חודשים</string>
<string name="ttl_mth">%d חודשים</string>
<string name="ttl_sec">%d שנייה</string>
<string name="status_e2e_encrypted">הצפנה מקצה־לקצה</string>
<string name="encrypted_video_call">שיחת וידאו מוצפנת מקצה־לקצה</string>
<string name="icon_descr_edited">הודעה נערכה</string>
<string name="encrypted_audio_call">שיחת שמע מוצפנת מקצה־לקצה</string>
<string name="icon_descr_email">אימייל</string>
<string name="allow_accepting_calls_from_lock_screen">אפשרו שיחות ממסך הנעילה דרך ההגדרות.</string>
<string name="enter_passphrase">הזינו סיסמה…</string>
<string name="enter_welcome_message_optional">הזינו הודעת פתיחה (אופציונלי)</string>
<string name="enter_welcome_message">הזינו הודעת פתיחה…</string>
<string name="error_deleting_contact">שגיאה במחיקת איש קשר</string>
<string name="error_deleting_pending_contact_connection">שגיאה במחיקת חיבור איש קשר ממתין</string>
<string name="error_loading_xftp_servers">שגיאה בטעינת שרתי XFTP</string>
<string name="error_sending_message">שגיאה בשליחת הודעה</string>
<string name="error_receiving_file">שגיאה בקבלת קובץ</string>
<string name="error_saving_ICE_servers">שגיאה בשמירת שרתי ICE</string>
<string name="settings_experimental_features">תכונות ניסיוניות</string>
<string name="export_database">ייצא מסד נתונים</string>
<string name="error_removing_member">שגיאה בהסרת חבר קבוצה</string>
<string name="error_saving_group_profile">שגיאה בשמירת פרופיל קבוצה</string>
<string name="error_saving_file">שגיאה בשמירת קובץ</string>
<string name="exit_without_saving">יציאה ללא שמירה</string>
<string name="error_stopping_chat">שגיאה בעצירת צ׳אט</string>
<string name="error_saving_smp_servers">שגיאה בשמירת שרתי SMP</string>
<string name="error_saving_xftp_servers">שגיאה בשמירת שרתי XFTP</string>
<string name="failed_to_active_user_title">שגיאה בהחלפת פרופיל!</string>
<string name="error_setting_network_config">שגיאה בעדכון תצורת הרשת</string>
<string name="failed_to_parse_chat_title">טעינת הצ׳אט נכשלה</string>
<string name="failed_to_parse_chats_title">טעינת הצ׳אטים נכשלה</string>
<string name="error_setting_address">שגיאה בהגדרת כתובת</string>
<string name="error_updating_user_privacy">שגיאה בעדכון פרטיות משתמש</string>
<string name="error_saving_user_password">שגיאה בשמירת סיסמת משתמש</string>
<string name="settings_section_title_experimenta">ניסיוני</string>
<string name="error_starting_chat">שגיאה בהפעלת צ׳אט</string>
<string name="error_with_info">שגיאה: %s</string>
<string name="error_updating_link_for_group">שגיאה בעדכון קישור קבוצה</string>
<string name="export_theme">ייצא ערכת צבעים</string>
<string name="icon_descr_expand_role">הרחב בחירת תפקיד</string>
<string name="file_will_be_received_when_contact_is_online">הקובץ ייתקבל כאשר איש הקשר יהיה מקוון, אנא חכו או בידקו מאוחר יותר!</string>
<string name="full_name__field">שם מלא:</string>
<string name="file_with_path">קובץ: %s</string>
<string name="icon_descr_group_inactive">קבוצה לא פעילה</string>
<string name="group_invitation_expired">פג תוקפה של ההזמנה לקבוצה</string>
<string name="group_display_name_field">שם תצוגה של הקבוצה:</string>
<string name="group_full_name_field">שם מלא של הקבוצה:</string>
<string name="v4_2_group_links">קישורי קבוצה</string>
<string name="icon_descr_file">קובץ</string>
<string name="file_will_be_received_when_contact_completes_uploading">הקובץ ייתקבל כאשר איש הקשר יסיים להעלות אותו.</string>
<string name="file_not_found">קובץ לא נמצא</string>
<string name="file_saved">קובץ נשמר</string>
<string name="alert_message_group_invitation_expired">ההזמנה לקבוצה כבר לא תקפה, היא הוסרה על ידי השולח.</string>
<string name="group_link">קישור קבוצה</string>
<string name="info_row_group">קבוצה</string>
<string name="simplex_link_mode_full">קישור מלא</string>
<string name="for_everybody">עבור כולם</string>
<string name="choose_file">קובץ</string>
<string name="from_gallery_button">מתוך גלריה</string>
<string name="full_name_optional__prompt">שם מלא (אופציונלי)</string>
<string name="files_and_media_section">קבצים ומדיה</string>
<string name="group_member_status_group_deleted">קבוצה נמחקה</string>
<string name="section_title_for_console">עבור מסוף הצ׳אט</string>
<string name="v5_0_large_files_support_descr">מהיר וללא המתנה עד שהשולח יהיה מקוון!</string>
<string name="v4_4_french_interface">ממשק צרפתית</string>
<string name="v4_6_reduced_battery_usage">הפחתה נוספת בשימוש בסוללה</string>
<string name="revoke_file__message">הקובץ יימחק מהשרתים.</string>
<string name="icon_descr_flip_camera">הפוך מצלמה</string>
<string name="icon_descr_help">עזרה</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">אם לא ניתן להיפגש פנים אל פנים, <b>הציגו את קוד ה־QR בשיחת וידאו</b>, או שתפו את הקישור.</string>
<string name="how_to">איך</string>
<string name="how_to_use_your_servers">איך להשתמש בשרתים שלך</string>
<string name="enter_one_ICE_server_per_line">שרתי ICE (אחד בכל שורה)</string>
<string name="host_verb">לארח</string>
<string name="hide_dev_options">מוסתר:</string>
<string name="snd_group_event_group_profile_updated">פרופיל הקבוצה עודכן</string>
<string name="group_profile_is_stored_on_members_devices">פרופיל הקבוצה מאוחסן במכשירי חברי הקבוצה, לא על השרתים.</string>
<string name="all_app_data_will_be_cleared">כל נתוני האפליקציה יימחקו.</string>
<string name="empty_chat_profile_is_created">פרופיל צ׳אט ריק עם השם שסופק ייווצר, והאפליקציה תיפתח כרגיל.</string>
<string name="app_passcode_replaced_with_self_destruct">קוד הגישה לאפליקציה יוחלף עם קוד הגישה להשמדה עצמית.</string>
<string name="v4_3_improved_privacy_and_security_desc">ניתן להסתיר את מסך האפליקציה במסך האפליקציות האחרונות.</string>
<string name="v4_6_hidden_chat_profiles">פרופילי צ׳אט מוסתרים</string>
<string name="v4_6_group_moderation">ניהול קבוצה</string>
<string name="v4_6_group_welcome_message">הודעת פתיחה בקבוצה</string>
<string name="alert_title_no_group">קבוצה לא נמצאה!</string>
<string name="hide_notification">הסתר</string>
<string name="notification_preview_mode_hidden">מוסתר</string>
<string name="notification_display_mode_hidden_desc">הסתר איש קשר והודעה</string>
<string name="hide_verb">הסתר</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="if_you_cant_meet_in_person">אם לא ניתן להיפגש פנים אל פנים, הציגו את קוד ה־QR בשיחת וידאו, או שתפו את הקישור.</string>
<string name="how_to_use_simplex_chat">איך להשתמש בזה</string>
<string name="email_invite_body">היי!
\nאפשר להתחבר אליי דרך SimpleX Chat: %s</string>
<string name="hidden_profile_password">סיסמת פרופיל מוסתרת</string>
<string name="hide_profile">הסתר פרופיל</string>
<string name="how_to_use_markdown">איך להשתמש במרקדאון</string>
<string name="how_it_works">איך זה עובד</string>
<string name="how_simplex_works">איך <xliff:g id="appName">SimpleX</xliff:g> עובדת</string>
<string name="icon_descr_hang_up">נתק</string>
<string name="settings_section_title_help">עזרה</string>
<string name="delete_group_for_all_members_cannot_undo_warning">הקבוצה תימחק עבור כל חברי הקבוצה לא ניתן לבטל זאת!</string>
<string name="delete_group_for_self_cannot_undo_warning">הקבוצה תימחק עבורך לא ניתן לבטל זאת!</string>
<string name="user_hide">הסתר</string>
<string name="group_preferences">העדפות קבוצה</string>
<string name="group_members_can_delete">חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</string>
<string name="group_members_can_send_disappearing">חברי הקבוצה יכולים לשלוח הודעות נעלמות.</string>
<string name="group_members_can_send_dms">חברי הקבוצה יכולים לשלוח הודעות ישירות.</string>
<string name="group_members_can_send_voice">חברי הקבוצה יכולים לשלוח הודעות קוליות.</string>
<string name="enable_self_destruct">אפשר השמדה עצמית</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">אם תבחרו לדחות השולח לא יקבל התראה על כך.</string>
<string name="network_disable_socks_info">אם תאשרו, שרתי העברת ההודעות יוכלו לראות את ה־IP שלכם, וספק האינטרנט שלכם את השרתים אליהם אתם מחוברים.</string>
<string name="image_descr">תמונה</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="if_you_enter_self_destruct_code">אם תזינו את קוד הגישה להשמדה עצמית בעת פתיחת האפליקציה:</string>
<string name="if_you_enter_passcode_data_removed">אם תזינו קוד גישה זה בעת פתיחת האפליקציה, כל נתוני האפליקציה יימחקו באופן בלתי הפיך!</string>
<string name="image_saved">תמונה נשמרה בגלריה</string>
<string name="gallery_image_button">תמונה</string>
<string name="ignore">התעלם</string>
<string name="la_immediately">מיד</string>
<string name="import_database">ייבא מסד נתונים</string>
<string name="immune_to_spam_and_abuse">חסין מפני ספאם ושימוש לרעה</string>
<string name="import_database_question">לייבא מסד נתונים של צ׳אט\?</string>
<string name="icon_descr_image_snd_complete">תמונה נשלחה</string>
<string name="image_will_be_received_when_contact_completes_uploading">התמונה תתקבל כאשר איש הקשר יסיים להעלות אותה.</string>
<string name="image_will_be_received_when_contact_is_online">התמונה תתקבל כאשר איש הקשר יהיה מקוון, אנא המתינו או בידקו מאוחר יותר!</string>
<string name="import_database_confirmation">ייבא</string>
<string name="import_theme">ייבא ערכת צבעים</string>
<string name="import_theme_error">שגיאה בייבוא ערכת צבעים</string>
<string name="group_unsupported_incognito_main_profile_sent">מצב זהות נסתרת אינו נתמך כאן הפרופיל הראשי שלך יישלח לחברי הקבוצה</string>
<string name="v4_3_improved_server_configuration">תצורת שרתים משופרת</string>
<string name="v4_3_improved_privacy_and_security">פרטיות ואבטחה משופרים</string>
<string name="settings_section_title_incognito">מצב זהות נסתרת</string>
<string name="incognito">זהות נסתרת</string>
<string name="description_via_contact_address_link_incognito">זהות נסתרת באמצעות קישור כתובת איש קשר</string>
<string name="description_via_group_link_incognito">זהות נסתרת באמצעות קישור קבוצה</string>
<string name="description_via_one_time_link_incognito">זהות נסתרת באמצעות קישור חד־פעמי</string>
<string name="invalid_connection_link">קישור חיבור לא תקין</string>
<string name="turn_off_battery_optimization">על מנת להשתמש בזה, אנא <b>השביתו מיטוב סוללה</b> עבור <xliff:g id="appName">SimpleX</xliff:g> בתיבת הדו־שיח הבאה. אחרת, ההתראות יושבתו.</string>
<string name="service_notifications_disabled">התראות מיידיות מושבתות!</string>
<string name="icon_descr_add_members">הזמן חברי קבוצה</string>
<string name="group_member_status_invited">הוזמן</string>
<string name="conn_level_desc_indirect">עקיף (<xliff:g id="conn_level">%1$s</xliff:g>)</string>
<string name="incognito_info_protects">מצב זהות נסתרת מגן על התמונה והפרטיות של שם הפרופיל הראשי שלך עבור כל איש קשר חדש נוצר פרופיל חדש אקראי.</string>
<string name="incompatible_database_version">גרסת מסד נתונים לא תואמת</string>
<string name="invalid_migration_confirmation">אישור העברת נתונים לא תקין</string>
<string name="v4_3_irreversible_message_deletion">מחיקה בלתי הפיכה של הודעות</string>
<string name="v4_5_italian_interface">ממשק איטלקית</string>
<string name="smp_servers_invalid_address">כתובת שרת לא תקינה!</string>
<string name="install_simplex_chat_for_terminal">התקנת <xliff:g id="appNameFull">SimpleX Chat</xliff:g> עבור מסוף הפקודה</string>
<string name="invite_friends">הזמן חברים</string>
<string name="italic">נטוי</string>
<string name="invalid_chat">צ׳אט לא תקין</string>
<string name="invalid_data">נתונים לא תקינים</string>
<string name="invalid_message_format">פורמט הודעה לא תקין</string>
<string name="display_name_invited_to_connect">הוזמן להתחבר</string>
<string name="icon_descr_instant_notifications">התראות מיידיות</string>
<string name="service_notifications">התראות מיידיות!</string>
<string name="invalid_contact_link">קישור לא תקין!</string>
<string name="invalid_QR_code">קוד QR לא תקין</string>
<string name="incorrect_code">קוד אבטחה שגוי!</string>
<string name="onboarding_notifications_mode_service">מיידית</string>
<string name="incoming_audio_call">שיחת שמע נכנסת</string>
<string name="incoming_video_call">שיחת וידאו נכנסת</string>
<string name="incorrect_passcode">קוד גישה שגוי</string>
<string name="group_invitation_item_description">הזמנה לקבוצה <xliff:g id="group_name">%1$s</xliff:g></string>
<string name="alert_title_group_invitation_expired">פג תוקף ההזמנה!</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_invited_via_your_group_link">הוזמן באמצעות קישור הקבוצה שלך</string>
<string name="initial_member_role">תפקיד ראשוני</string>
<string name="invite_to_group_button">הזמן לקבוצה</string>
<string name="button_add_members">הזמן חברי קבוצה</string>
<string name="message_deletion_prohibited">מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה.</string>
<string name="message_deletion_prohibited_in_chat">מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו.</string>
<string name="group_preview_join_as">להצטרף בתור %s</string>
<string name="incognito_info_allows">זה מאפשר חיבורים אנונימיים רבים ללא שום נתונים משותפים ביניהם בפרופיל צ׳אט יחיד.</string>
<string name="alert_text_skipped_messages_it_can_happen_when">זה יכול לקרות כאשר:
\n1. פג תוקפן של ההודעות בלקוח השולח לאחר 2 ימים או בשרת לאחר 30 ימים.
\n2. פיענוח הצפנת הודעה נכשל, מכיוון שאתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים.
\n3. החיבור נפגע.</string>
<string name="onboarding_notifications_mode_subtitle">ניתן לשנות זאת מאוחר יותר באמצעות ההגדרות.</string>
<string name="alert_text_fragment_encryption_out_of_sync_old_database">זה יכול לקרות כאשר אתם או איש הקשר שלכם השתמשתם בגיבוי ישן של מסד הנתונים.</string>
<string name="join_group_question">להצטרף לקבוצה\?</string>
<string name="join_group_button">הצטרף</string>
<string name="large_file">קובץ גדול!</string>
<string name="thousand_abbreviation">אלף</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">ודאו שכתובות שרתי ה־WebRTC ICE הן בפורמט הנכון, מופרדות בשורה ולא משוכפלות.</string>
<string name="v4_4_live_messages">הודעות חיות</string>
<string name="live_message">הודעה חיה!</string>
<string name="image_descr_link_preview">תצוגה מקדימה של קישור</string>
<string name="make_private_connection">צור חיבור פרטי</string>
<string name="lock_after">נעל אחרי</string>
<string name="lock_mode">מצב נעילה</string>
<string name="joining_group">מצטרף לקבוצה</string>
<string name="leave_group_button">עזוב</string>
<string name="make_profile_private">הפוך את הפרופיל לפרטי!</string>
<string name="live">הודעה חיה</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">ודאו שכתובות שרתי ה־SMP הן בפורמט הנכון, מופרדות בשורה ולא משוכפלות.</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">ודאו שכתובות שרתי ה־XFTP הן בפורמט הנכון, מופרדות בשורה ולא משוכפלות.</string>
<string name="auth_log_in_using_credential">התחבר באמצעות האישור שלך</string>
<string name="learn_more">למדו עוד</string>
<string name="markdown_help">עזרה במרקדאון</string>
<string name="email_invite_subject">בואו נדבר ב־Simplex Chat</string>
<string name="many_people_asked_how_can_it_deliver">אנשים רבים שאלו: <i>אם ל־<xliff:g id="appName">SimpleX</xliff:g> אין מזהי משתמש, איך ניתן להעביר הודעות\?</i></string>
<string name="keychain_error">שגיאת Keychain</string>
<string name="join_group_incognito_button">הצטרף עם זהות נסתרת</string>
<string name="leave_group_question">לעזוב קבוצה\?</string>
<string name="rcv_group_event_member_left">עזב</string>
<string name="group_member_status_left">עזב</string>
<string name="button_leave_group">עזוב קבוצה</string>
<string name="info_row_local_name">שם מקומי</string>
<string name="users_delete_data_only">נתוני פרופיל מקומיים בלבד</string>
<string name="theme_light">בהיר</string>
<string name="import_theme_error_desc">ודאו שלקובץ יש תחביר YAML תקין. ייצאו ערכת צבעים כדי לקבל דוגמה למבנה תקין של קובץ ערכת צבעים.</string>
<string name="message_delivery_error_desc">ככל הנראה איש קשר זה מחק את החיבור איתך.</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="moderated_item_description">מנוהל על ידי %s</string>
<string name="la_no_app_password">אין קוד גישה לאפליקציה</string>
<string name="videos_limit_desc">ניתן לשלוח רק 10 סרטונים בו־זמנית</string>
<string name="images_limit_desc">ניתן לשלוח רק 10 תמונות בו־זמנית</string>
<string name="notifications">התראות</string>
<string name="only_group_owners_can_enable_voice">רק בעלי הקבוצות יכולים לאפשר הודעות קוליות.</string>
<string name="no_details">ללא פרטים</string>
<string name="ok">אישור</string>
<string name="add_contact">קישור הזמנה חד־פעמי</string>
<string name="markdown_in_messages">מרקדאון בהודעות</string>
<string name="network_and_servers">רשת ושרתים</string>
<string name="network_settings_title">הגדרות רשת</string>
<string name="no_spaces">בלי רווחים!</string>
<string name="new_database_archive">ארכיון מסד נתונים חדש</string>
<string name="messages_section_title">הודעות</string>
<string name="group_member_role_member">חבר קבוצה</string>
<string name="group_member_role_observer">צופה</string>
<string name="only_group_owners_can_change_prefs">רק בעלי הקבוצות יכולים לשנות העדפות קבוצה.</string>
<string name="network_status">מצב רשת</string>
<string name="chat_preferences_on">פעיל</string>
<string name="chat_preferences_no">לא</string>
<string name="chat_preferences_off">כבוי</string>
<string name="only_you_can_delete_messages">רק אתם יכולים למחוק הודעות באופן בלתי הפיך (איש הקשר שלכם יכול לסמן אותן למחיקה).</string>
<string name="only_you_can_send_voice">רק אתם יכולים לשלוח הודעות קוליות.</string>
<string name="only_your_contact_can_send_voice">רק איש הקשר שלכם יכול לשלוח הודעות קוליות.</string>
<string name="only_you_can_make_calls">רק אתם יכולים לבצע שיחות.</string>
<string name="only_your_contact_can_make_calls">רק איש הקשר שלכם יכול לבצע שיחות.</string>
<string name="new_in_version">חדש ב־%s</string>
<string name="self_destruct_new_display_name">שם תצוגה חדש:</string>
<string name="notifications_will_be_hidden">התראות יוצגו רק עד שהאפליקציה תופסק!</string>
<string name="new_passphrase">סיסמה חדשה…</string>
<string name="database_migrations">העברות נתונים: %s</string>
<string name="no_contacts_to_add">אין אנשי קשר להוסיף</string>
<string name="only_you_can_send_disappearing">רק אתם יכולים לשלוח הודעות נעלמות.</string>
<string name="only_your_contact_can_send_disappearing">רק איש הקשר שלכם יכול לשלוח הודעות נעלמות.</string>
<string name="only_your_contact_can_delete">רק איש הקשר שלכם יכול למחוק הודעות באופן בלתי הפיך (אתם יכולים לסמן אותן למחיקה).</string>
<string name="v4_5_message_draft">טיוטת הודעה</string>
<string name="v4_5_multiple_chat_profiles">פרופילי צ׳אט מרובים</string>
<string name="v4_5_reduced_battery_usage_descr">שיפורים נוספים יגיעו בקרוב!</string>
<string name="v4_6_reduced_battery_usage_descr">שיפורים נוספים יגיעו בקרוב!</string>
<string name="v4_6_group_moderation_descr">כעת מנהלים יכולים:
\n- למחוק הודעות של חברי קבוצה.
\n- להשבית חברי קבוצה (תפקיד ”צופה”)</string>
<string name="notification_new_contact_request">בקשה חדשה ליצירת קשר</string>
<string name="notification_preview_new_message">הודעה חדשה</string>
<string name="mark_read">סמן כנקרא</string>
<string name="mark_unread">סמן כלא נקרא</string>
<string name="one_time_link">קישור הזמנה חד־פעמי</string>
<string name="muted_when_inactive">מושתק כאשר אין פעילות!</string>
<string name="feature_offered_item_with_param">הוצע %s: %2s</string>
<string name="marked_deleted_description">מסומן כנמחק</string>
<string name="moderated_description">מנוהל</string>
<string name="settings_notifications_mode_title">שירות התראות</string>
<string name="settings_notification_preview_title">תצוגה מקדימה של התראות</string>
<string name="notification_preview_mode_message">טקסט הודעה</string>
<string name="message_delivery_error_title">שגיאת מסירת הודעה</string>
<string name="moderate_verb">נהל</string>
<string name="delete_message_cannot_be_undone_warning">ההודעה תימחק לא ניתן לבטל זאת!</string>
<string name="delete_message_mark_deleted_warning">ההודעה תסומן למחיקה. הנמענים יוכלו לחשוף הודעה זו.</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 נייד: הקישו <b>פתח באפליקציה</b>, ואז הקישו <b>התחבר</b> באפליקציה.</string>
<string name="only_stored_on_members_devices">(מאוחסן רק על ידי חברי הקבוצה)</string>
<string name="mute_chat">השתק</string>
<string name="icon_descr_more_button">עוד</string>
<string name="mark_code_verified">סמן מאומת</string>
<string name="network_use_onion_hosts_no">לא</string>
<string name="network_use_onion_hosts_prefer_desc">ייעשה שימוש במארחי Onion כאשר יהיו זמינים.</string>
<string name="network_use_onion_hosts_required_desc">מארחי Onion יידרשו לחיבור.</string>
<string name="network_use_onion_hosts_no_desc">לא ייעשה שימוש במארחי Onion.</string>
<string name="network_use_onion_hosts_no_desc_in_alert">לא ייעשה שימוש במארחי Onion.</string>
<string name="callstatus_missed">שיחה שלא נענתה</string>
<string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages">רק מכשירי לקוח מאחסנים פרופילי משתמש, אנשי קשר, קבוצות, והודעות שנשלחו עם <b>הצפנה מקצה־לקצה דו־שכבתית</b>.</string>
<string name="status_no_e2e_encryption">ללא הצפנה מקצה־לקצה</string>
<string name="icon_descr_call_missed">שיחה שלא נענתה</string>
<string name="new_passcode">קוד גישה חדש</string>
<string name="settings_section_title_messages">הודעות וקבצים</string>
<string name="old_database_archive">ארכיון מסד נתונים ישן</string>
<string name="chat_item_ttl_none">לעולם לא</string>
<string name="no_received_app_files">לא התקבלו או נשלחו קבצים</string>
<string name="new_member_role">תפקיד חבר קבוצה</string>
<string name="no_contacts_selected">לא נבחרו אנשי קשר</string>
<string name="member_info_section_title_member">חבר קבוצה</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">חבר הקבוצה יוסר מהקבוצה לא ניתן לבטל זאת!</string>
<string name="user_mute">השתק</string>
<string name="color_surface">תפריטים והתראות</string>
<string name="feature_off">כבוי</string>
<string name="v4_3_voice_messages_desc">מקסימום 40 שניות, מתקבל מיידית.</string>
<string name="feature_offered_item">הוצע %s</string>
<string name="both_you_and_your_contact_can_add_message_reactions">גם אתם וגם איש הקשר יכולים להוסיף תגובות אמוג׳י להודעות.</string>
<string name="message_reactions">תגובות אמוג׳י להודעות</string>
<string name="allow_message_reactions">אפשר תגובות אמוג׳י להודעות.</string>
<string name="message_reactions_prohibited_in_this_chat">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</string>
<string name="message_reactions_are_prohibited">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</string>
<string name="allow_your_contacts_adding_message_reactions">אפשר לאנשי הקשר להוסיף תגובות אמוג׳י להודעות.</string>
<string name="allow_message_reactions_only_if">אפשר תגובות אמוג׳י להודעות רק אם איש הקשר מאפשר אותן.</string>
<string name="group_members_can_add_message_reactions">חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות.</string>
<string name="only_you_can_add_message_reactions">רק אתם יכולים להוסיף תגובות אמוג׳י להודעות.</string>
<string name="only_your_contact_can_add_message_reactions">רק איש הקשר שלכם יכול להוסיף תגובות אמוג׳י להודעות.</string>
<string name="open_verb">פתח</string>
<string name="prohibit_message_reactions">לאסור תגובות אמוג׳י להודעות.</string>
<string name="send_disappearing_message_1_minute">דקה</string>
<string name="send_disappearing_message_30_seconds">30 שניות</string>
<string name="send_disappearing_message_5_minutes">5 דקות</string>
<string name="send_disappearing_message_custom_time">זמן מותאם אישית</string>
<string name="disappearing_message">הודעה נעלמת</string>
<string name="toast_permission_denied">ההרשאה נדחתה!</string>
<string name="image_descr_profile_image">תמונת פרופיל</string>
<string name="profile_update_will_be_sent_to_contacts">עדכון הפרופיל יישלח לאנשי הקשר שלך.</string>
<string name="open_simplex_chat_to_accept_call">פיתחו את <xliff:g id="appNameFull">Simplex Chat</xliff:g> כדי לענות לשיחה.</string>
<string name="v4_5_message_draft_descr">שמירת טיוטת ההודעה האחרונה, עם קבצים מצורפים.</string>
<string name="prohibit_message_reactions_group">לאסור תגובות אמוג׳י להודעות.</string>
<string name="v4_5_private_filenames">שמות קבצים פרטיים</string>
<string name="custom_time_unit_hours">שעות</string>
<string name="custom_time_unit_minutes">דקות</string>
<string name="custom_time_unit_days">ימים</string>
<string name="custom_time_unit_months">חודשים</string>
<string name="restore_passphrase_not_found_desc">סיסמת מסד הנתונים לא נמצאה ב־Keystore, יש להזין אותה ידנית. זה יכול לקרות אם נתוני האפליקציה שוחזרו על ידי כלי גיבוי. אם זה לא המקרה, אנא, תיצרו קשר עם המפתחים.</string>
<string name="contact_developers">אנא עדכנו את האפליקציה וצרו קשר עם המפתחים.</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">אנא ודאו שהשתמשתם בקישור הנכון או בקשו מאיש הקשר שלכם לשלוח קישור נוסף.</string>
<string name="periodic_notifications">התראות תקופתיות</string>
<string name="enter_passphrase_notification_title">נדרשת סיסמה</string>
<string name="la_lock_mode_passcode">הזנת קוד גישה</string>
<string name="paste_button">הדבק</string>
<string name="port_verb">פורט</string>
<string name="network_proxy_port">פורט %d</string>
<string name="privacy_and_security">פרטיות ואבטחה</string>
<string name="passcode_set">קוד גישה הוגדר!</string>
<string name="passcode_not_changed">קוד גישה לא השתנה!</string>
<string name="passcode_changed">קוד גישה השתנה!</string>
<string name="network_option_ping_count">ספירת PING</string>
<string name="prohibit_sending_disappearing_messages">לאסור שליחת הודעות נעלמות.</string>
<string name="prohibit_calls">לאסור שיחות שמע/וידאו.</string>
<string name="enter_correct_current_passphrase">נא להזין את הסיסמה הנכונה הנוכחית.</string>
<string name="periodic_notifications_disabled">התראות תקופתיות מושבתות!</string>
<string name="network_option_ping_interval">מרווח PING</string>
<string name="la_please_remember_to_store_password">אנא זיכרו או שימרו את הסיסמה בצורה מאובטחת אין דרך לשחזר סיסמה אבודה!</string>
<string name="observer_cant_send_message_desc">אנא צרו קשר עם מנהל הקבוצה.</string>
<string name="ask_your_contact_to_enable_voice">אנא בקשו מאיש הקשר שלכם לאפשר שליחת הודעות קוליות.</string>
<string name="icon_descr_profile_image_placeholder">שומר מקום לתמונת פרופיל</string>
<string name="smp_servers_preset_server">שרת מוגדר מראש</string>
<string name="privacy_redefined">פרטיות מוגדרת מחדש</string>
<string name="people_can_connect_only_via_links_you_share">אנשים יכולים להתחבר אליכם רק דרך הקישורים שאתם משתפים.</string>
<string name="opensource_protocol_and_code_anybody_can_run_servers">פרוטוקול וקוד פתוחים כל אחד יכול להריץ את השרתים.</string>
<string name="onboarding_notifications_mode_periodic">תקופתי</string>
<string name="restore_database_alert_desc">נא להזין את הסיסמה הקודמת לאחר שחזור גיבוי מסד הנתונים, לא ניתן לבטל פעולה זו.</string>
<string name="prohibit_message_deletion">לאסור מחיקה בלתי הפיכה של הודעות.</string>
<string name="opening_database">פותח מסד נתונים…</string>
<string name="simplex_link_mode_browser_warning">פתיחת הקישור בדפדפן עלולה להפחית את פרטיות ואבטחת החיבור. קישורי SimpleX לא מהימנים יהיו אדומים.</string>
<string name="network_error_desc">אנא בידקו את חיבור האינטרנט שלכם עם <xliff:g id="serverHost" example="smp.simplex.im">%1$s</xliff:g> ונסו שוב.</string>
<string name="error_smp_test_certificate">ייתכן שטביעת האצבע של התעודה בכתובת השרת שגויה</string>
<string name="auth_open_chat_console">פתיחת מסוף צ׳אט</string>
<string name="auth_open_chat_profiles">פתיחת פרופילי צ׳אט</string>
<string name="icon_descr_server_status_pending">ממתין</string>
<string name="paste_connection_link_below_to_connect">הדביקו את הקישור שקיבלתם בתיבה למטה כדי להתחבר לאיש הקשר שלכם.</string>
<string name="smp_servers_preset_address">כתובת שרת מוגדר מראש</string>
<string name="password_to_show">סיסמה להצגה</string>
<string name="onboarding_notifications_mode_title">התראות פרטיות</string>
<string name="paste_the_link_you_received">הדבק קישור שהתקבל</string>
<string name="call_connection_peer_to_peer">עמית־לעמית</string>
<string name="icon_descr_call_pending_sent">שיחה ממתינה</string>
<string name="alert_text_fragment_please_report_to_developers">אנא דווחו על כך למפתחים.</string>
<string name="la_mode_passcode">קוד גישה</string>
<string name="store_passphrase_securely_without_recover">אנא שימרו את הסיסמה בצורה מאובטחת, לא תוכלו לגשת לצ׳אט אם תאבדו אותה.</string>
<string name="store_passphrase_securely">אנא שימרו את הסיסמה בצורה מאובטחת, לא תוכלו לשנות אותה אם תאבדו אותה.</string>
<string name="open_chat">פתח צ׳אט</string>
<string name="group_member_role_owner">בעלים</string>
<string name="group_welcome_preview">תצוגה מקדימה</string>
<string name="users_delete_with_connections">פרופיל וחיבורי שרתים</string>
<string name="profile_password">סיסמת פרופיל</string>
<string name="prohibit_direct_messages">לאסור שליחת הודעות ישירות לחברי הקבוצה.</string>
<string name="v5_0_polish_interface">ממשק פולנית</string>
<string name="v4_6_hidden_chat_profiles_descr">הגנו על פרופילי הצ׳אט שלכם באמצעות סיסמה!</string>
<string name="protect_app_screen">הגנה על מסך האפליקציה</string>
<string name="prohibit_sending_voice">לאסור שליחת הודעות קוליות.</string>
<string name="prohibit_sending_voice_messages">לאסור שליחת הודעות קוליות.</string>
<string name="prohibit_sending_disappearing">לאסור שליחת הודעות נעלמות.</string>
<string name="receiving_files_not_yet_supported">קבלת קבצים אינה נתמכת עדיין</string>
<string name="image_descr_qr_code">קוד QR</string>
<string name="callstatus_rejected">שיחה נדחתה</string>
<string name="callstate_received_answer">התקבלה תשובה…</string>
<string name="callstate_received_confirmation">התקבל אישור…</string>
<string name="reject">דחיה</string>
<string name="remove_passphrase">הסר</string>
<string name="receiving_via">מקבל באמצעות</string>
<string name="network_option_protocol_timeout">תום זמן הפרוטוקול</string>
<string name="rcv_group_event_member_deleted"><xliff:g id="member profile" example="alice (Alice)">%1$s</xliff:g> הוסר/ה</string>
<string name="color_received_message">הודעה שהתקבלה</string>
<string name="v4_4_live_messages_desc">נמענים רואים הודעות תוך כדי הקלדתן.</string>
<string name="v4_5_reduced_battery_usage">שימוש מופחת בסוללה</string>
<string name="custom_time_picker_custom">מותאם אישית</string>
<string name="simplex_service_notification_text">מקבל הודעות…</string>
<string name="stop_rcv_file__message">קבלת הקובץ תופסק.</string>
<string name="icon_descr_record_voice_message">הקלט הודעה קולית</string>
<string name="reject_contact_button">דחיה</string>
<string name="read_more_in_user_guide_with_link">קראו עוד ב<font color="#0088ff">מדריך למשתמש</font>.</string>
<string name="rate_the_app">דרגו את האפליקציה</string>
<string name="read_more_in_github">קראו עוד ב־GitHub repository שלנו.</string>
<string name="read_more_in_github_with_link">קראו עוד ב־<font color="#0088ff">GitHub repository</font> שלנו.</string>
<string name="relay_server_if_necessary">יבוצע שימוש בשרת ממסר רק במידת הצורך. גורם אחר יכול לצפות בכתובת ה־IP שלך.</string>
<string name="relay_server_protects_ip">שרת ממסר מגן על כתובת ה־IP שלך, אך הוא יכול לראות את משך השיחה.</string>
<string name="icon_descr_call_rejected">שיחה נדחתה</string>
<string name="rcv_group_event_user_deleted">הסירו אותך</string>
<string name="group_member_status_removed">הוסר</string>
<string name="button_remove_member">הסר חבר קבוצה</string>
<string name="remove_member_confirmation">הסר</string>
<string name="feature_received_prohibited">התקבל, לא מאופשר</string>
<string name="enabled_self_destruct_passcode">יצירת קוד גישה להשמדה עצמית</string>
<string name="change_self_destruct_mode">שינוי מצב השמדה עצמית</string>
<string name="change_self_destruct_passcode">שנה קוד גישה להשמדה עצמית</string>
<string name="share_text_database_id">מזהה מסד נתונים: %d</string>
<string name="share_text_deleted_at">נמחק ב: %s</string>
<string name="share_text_disappears_at">ייעלם ב: %s</string>
<string name="v5_1_self_destruct_passcode_descr">כל הנתונים נמחקים בהזנת הקוד.</string>
<string name="v5_1_better_messages">הודעות משופרות</string>
<string name="v5_1_custom_themes_descr">התאימו אישית ושתפו ערכות צבעים.</string>
<string name="v5_1_custom_themes">ערכות צבעים מותאמות אישית</string>
<string name="item_info_current">(נוכחי)</string>
<string name="info_row_deleted_at">נמחק</string>
<string name="info_row_disappears_at">ייעלם</string>
</resources>

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