mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
381 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c2d357147 | |||
| 6a05cc24aa | |||
| c1a0943448 | |||
| fb4f0503a1 | |||
| c1c17d1f19 | |||
| 009d13210f | |||
| 892f6498be | |||
| 3fa1d7b07c | |||
| ee146cdc7b | |||
| 219381f941 | |||
| 89f380400e | |||
| 4f1cf6e79f | |||
| 6ff7d4a73c | |||
| 3acc69c6d8 | |||
| 61ab15bbb5 | |||
| b9777c92a5 | |||
| f3be723cde | |||
| a182cf5730 | |||
| 247d12fa40 | |||
| 6593de89c2 | |||
| a1e25620f7 | |||
| 29b9abf241 | |||
| a0b8cf62be | |||
| 43fa4c43a2 | |||
| 85e7a13dba | |||
| 9d992735f4 | |||
| e61babdc8f | |||
| c04e952620 | |||
| 92967dfe0c | |||
| bc96000131 | |||
| a588e7003d | |||
| 665501026d | |||
| a62ce9168e | |||
| f6b611aa30 | |||
| 5a59fdd91c | |||
| 5f01dc1a3f | |||
| c488c4fcd5 | |||
| c53d56ba00 | |||
| 3143cc960e | |||
| b8442d92a4 | |||
| 98a3437f43 | |||
| 4aaff20fd8 | |||
| 8f32c6a61a | |||
| 79d5573169 | |||
| a9e7635e00 | |||
| 2c0de36439 | |||
| 94377d0b7a | |||
| e9853fe3fc | |||
| 4738286f4e | |||
| 961bdbfc59 | |||
| 879c117269 | |||
| 03bc4e5d01 | |||
| 80bd4cd337 | |||
| b0f3f0a523 | |||
| 9915e5572e | |||
| 9c6e0a7051 | |||
| 68be4b4ba5 | |||
| 13efdf2595 | |||
| c3991aad87 | |||
| 614846465f | |||
| 096dec2c7b | |||
| d19708ed77 | |||
| 22d7db89d8 | |||
| ba7abcf6f7 | |||
| b512d3a743 | |||
| 9fa968a593 | |||
| 41b7ad01f9 | |||
| 15fae29e5b | |||
| 8c1abcccfb | |||
| 25893177d0 | |||
| 345e0acdec | |||
| 1f04984a34 | |||
| 7a91ed2ab2 | |||
| d912fe07a1 | |||
| cfc21dfb51 | |||
| e5c83b20c9 | |||
| 97b472fd9c | |||
| d40d690f86 | |||
| 6581e27524 | |||
| 909edac64f | |||
| 30a24df9c0 | |||
| b42cde2519 | |||
| 7bcb514baf | |||
| bda84b08a1 | |||
| 2f0fe50f88 | |||
| 4f640c96d1 | |||
| 9b71702ac8 | |||
| 76aedb4a15 | |||
| a6f5ba541b | |||
| e47b16f3b4 | |||
| 494ef6e671 | |||
| bff2d7d3b6 | |||
| 2adfa0c18b | |||
| b5170684ad | |||
| 396fa7f988 | |||
| ea9ee987cf | |||
| 49d1b26bba | |||
| bab63d8f27 | |||
| 1083a0727a | |||
| 78b3b12ec1 | |||
| 61d7df8906 | |||
| 522f99aadd | |||
| 927a04d45f | |||
| 2b155db57d | |||
| f3cef7ce12 | |||
| 29b54ec5b2 | |||
| 313acefb19 | |||
| e5534c0402 | |||
| 4e37efdc4a | |||
| 58c92ed004 | |||
| 181f72fa1f | |||
| 4b9c618ae3 | |||
| 70a29512b7 | |||
| fcae5e9925 | |||
| 619985730e | |||
| 3d4a47cdae | |||
| d1ae3ba2d3 | |||
| feb4ecbb6b | |||
| e645dd99e7 | |||
| a17bfc52ce | |||
| 2b0c229dbb | |||
| 6843269cff | |||
| b605ebfd2a | |||
| feb687d3b8 | |||
| ff8e29c0eb | |||
| 1fbf21d395 | |||
| d42cab8e22 | |||
| 807f698cf2 | |||
| 9e54ae7295 | |||
| 457e12880c | |||
| ef0f21a11c | |||
| 8396e70e7b | |||
| 2da89c2cf1 | |||
| 3b0205b25f | |||
| bdaec30fa0 | |||
| 97df069730 | |||
| 9a1166f097 | |||
| ddd639a86c | |||
| ab5bd911c2 | |||
| b75602f248 | |||
| 37b78edb91 | |||
| 5d599a59b7 | |||
| af6b57e453 | |||
| 6bd2794548 | |||
| 59d86274cd | |||
| b6a9b25acd | |||
| 0e3b71866b | |||
| 3e9e7e29c0 | |||
| 531248f61a | |||
| 2defbdc361 | |||
| 8ce3d335b0 | |||
| 2295c2a85f | |||
| aa06b08e69 | |||
| 0b681d900b | |||
| a87153714f | |||
| a08200b3ab | |||
| 810f05d88f | |||
| 00aa615df7 | |||
| 6c3fd9b428 | |||
| c9f5066c60 | |||
| bc96e95849 | |||
| 8a301567e1 | |||
| afa6083cb3 | |||
| 160c253966 | |||
| 3d7434c52f | |||
| c78df15cd6 | |||
| 701bef2a50 | |||
| 560d9b1b03 | |||
| b82349164f | |||
| 30df42d20d | |||
| 01345cd423 | |||
| 38a2f3b458 | |||
| f739c7f226 | |||
| 28fb67fe92 | |||
| d2b643de01 | |||
| bae9685950 | |||
| ccd34f5ab1 | |||
| dd8081a9d4 | |||
| d65909cb53 | |||
| e99b543c2f | |||
| 312d315d73 | |||
| cdf5520084 | |||
| 170e8ddeaf | |||
| 68f7956512 | |||
| e424b3eb66 | |||
| fb7a414e3c | |||
| 5966d6d648 | |||
| c6ebc9a4d9 | |||
| 05ec8b0676 | |||
| 6980f643ff | |||
| b60dd3e3e8 | |||
| 17a5770442 | |||
| 439bbc15a5 | |||
| 3688141972 | |||
| d79f8f8345 | |||
| 923ad7470c | |||
| 833abb252e | |||
| c60207f211 | |||
| d5665a421f | |||
| b2aa0ffc3c | |||
| e50a5cfaa1 | |||
| 73ec44a9be | |||
| 59fe6ab8ce | |||
| ec64be7c89 | |||
| 41984e56c0 | |||
| c435510b2e | |||
| de65d0e0a2 | |||
| 9cde37c6f5 | |||
| 9514acf6aa | |||
| 40d54f3f16 | |||
| 3249c5e463 | |||
| e440fba582 | |||
| 43cedbce26 | |||
| ebad009553 | |||
| 35cf113d98 | |||
| e82d2cbea2 | |||
| a0e056994e | |||
| e4d8ea17fc | |||
| e69db17804 | |||
| 9282cca396 | |||
| ad2eaeb004 | |||
| 3f44c9af24 | |||
| 4296515473 | |||
| 88830a2e09 | |||
| c8a269e391 | |||
| a45897007c | |||
| 42ea1df342 | |||
| 47214e33ea | |||
| ff239d81e5 | |||
| 0a6e47dd61 | |||
| ee75219ed3 | |||
| 2e337a753f | |||
| 9918d91def | |||
| 82dd5751c1 | |||
| de637fab50 | |||
| cb49f6f01d | |||
| 6d0a83aa58 | |||
| 05f57e98ad | |||
| 6a66525927 | |||
| 9530a6055a | |||
| f5ed8debcc | |||
| 355d2449c5 | |||
| f7382cdd6f | |||
| dc8d10d068 | |||
| 951245d33f | |||
| 521b901cc9 | |||
| 6311ba451b | |||
| 754c76d6fd | |||
| aea7ff1c89 | |||
| 77ac972e09 | |||
| 256f85024f | |||
| 870f9e42dd | |||
| 0a4a3a24e1 | |||
| 8a66390a78 | |||
| 3d48eded3d | |||
| 6546426ec0 | |||
| 4a7ceb00fb | |||
| 53560378bb | |||
| cdb3b6aafd | |||
| 9f3d3e8ba4 | |||
| 047aad592e | |||
| 087acd9180 | |||
| 0b822e4a5c | |||
| f8a469488e | |||
| 3b5e806418 | |||
| 79e208193a | |||
| ef5c13b1c1 | |||
| 38533213d2 | |||
| 5f1aa6fa9d | |||
| b0002fe07d | |||
| ef21fd1d26 | |||
| 4a311b9578 | |||
| b8da5e225b | |||
| f27de052cf | |||
| 5cc537f14c | |||
| dc8ca4cf89 | |||
| b62dd801f1 | |||
| 0c096e2c89 | |||
| cc127e56fe | |||
| 1781495ee3 | |||
| 831231d8e6 | |||
| 45102442f4 | |||
| f323c8e112 | |||
| 3bdc6b5e28 | |||
| d8373262bc | |||
| 3597d34716 | |||
| bd4259e89e | |||
| 55ead740cc | |||
| 5ef0eda2d7 | |||
| 49a9b0e7d6 | |||
| 45ada450a2 | |||
| 307a1b3c5e | |||
| ed6b3bbead | |||
| 901610eec5 | |||
| 7d4127c51d | |||
| 13215d91d7 | |||
| e1a8099474 | |||
| daa8d9bb21 | |||
| 5fcbade1bc | |||
| 3937ffa9a6 | |||
| 80ddb50e1c | |||
| f6e66f1c53 | |||
| 0c23ff9ae3 | |||
| 1570bc2b99 | |||
| 1e2104cabf | |||
| f3014f258d | |||
| f0991cc0ba | |||
| 74b78a8d7b | |||
| 82cd70a75c | |||
| fe4eb7b5af | |||
| c459e71d02 | |||
| 2516d5a393 | |||
| 477d98d75a | |||
| 4253cd7fb9 | |||
| ca78958667 | |||
| 1f5b80d560 | |||
| 2de111e76c | |||
| 8343285d93 | |||
| 5dbe2b2745 | |||
| fb9485190d | |||
| 6881600e06 | |||
| 9ed723bafa | |||
| 9ded1c9821 | |||
| bb374c68b1 | |||
| c3e82a6a4e | |||
| 7c12e82042 | |||
| e7e66ff873 | |||
| c4d7e5307c | |||
| d6b9a45a39 | |||
| 7fd3b4d6ba | |||
| 4004aafbc5 | |||
| 95008eeeaf | |||
| c7a8992043 | |||
| ea2b5f2ccf | |||
| ed9f277421 | |||
| 5c14c3b349 | |||
| d8fb31f167 | |||
| 02db38ffd3 | |||
| 7692195bfa | |||
| c435cbdc7b | |||
| effc281271 | |||
| 41eb2e5689 | |||
| 67d74a0a27 | |||
| f66405e79b | |||
| 74d186af16 | |||
| 187fef0c5a | |||
| 4782cab507 | |||
| bcbee67709 | |||
| 2501cbe55d | |||
| 2bd049db87 | |||
| 6b8b9ab4fd | |||
| 30db24265e | |||
| 316d605899 | |||
| b4257f7767 | |||
| a3f2d5c919 | |||
| cf46469cd5 | |||
| 0312fde818 | |||
| 9defa44f0c | |||
| 915b53054c | |||
| f81557b4fd | |||
| e273bd1239 | |||
| a63caf4640 | |||
| e7f0234134 | |||
| 340552321e | |||
| 98a3fc214d | |||
| 6a578cfe3c | |||
| dacc075fe8 | |||
| 55418e2bc0 | |||
| f2b5c0f3a8 | |||
| 5ebdf5dba9 | |||
| 8e045764df | |||
| 503d3d77e6 | |||
| 81bd7d97c5 | |||
| 8f57925067 | |||
| 9bf99db82e | |||
| 5615cdbf1a | |||
| d802ae0058 | |||
| 8f2278198c | |||
| 10937a5a4e | |||
| 6aff6e9804 | |||
| 95477cae7e |
+11
-11
@@ -124,12 +124,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "ignore-project: False" >> cabal.project.local
|
echo "ignore-project: False" >> cabal.project.local
|
||||||
echo "package simplexmq" >> cabal.project.local
|
echo "package simplexmq" >> cabal.project.local
|
||||||
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
|
echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local
|
||||||
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
|
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local
|
||||||
echo "" >> cabal.project.local
|
echo "" >> cabal.project.local
|
||||||
echo "package direct-sqlcipher" >> cabal.project.local
|
echo "package direct-sqlcipher" >> cabal.project.local
|
||||||
echo " extra-include-dirs: /opt/homebrew/opt/openssl@1.1/include" >> cabal.project.local
|
echo " extra-include-dirs: /opt/homebrew/opt/openssl@3.0/include" >> cabal.project.local
|
||||||
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@1.1/lib" >> cabal.project.local
|
echo " extra-lib-dirs: /opt/homebrew/opt/openssl@3.0/lib" >> cabal.project.local
|
||||||
echo " flags: +openssl" >> cabal.project.local
|
echo " flags: +openssl" >> cabal.project.local
|
||||||
|
|
||||||
- name: Unix prepare cabal.project.local for Mac
|
- name: Unix prepare cabal.project.local for Mac
|
||||||
@@ -138,21 +138,21 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "ignore-project: False" >> cabal.project.local
|
echo "ignore-project: False" >> cabal.project.local
|
||||||
echo "package simplexmq" >> cabal.project.local
|
echo "package simplexmq" >> cabal.project.local
|
||||||
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
|
echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local
|
||||||
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
|
echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local
|
||||||
echo "" >> cabal.project.local
|
echo "" >> cabal.project.local
|
||||||
echo "package direct-sqlcipher" >> cabal.project.local
|
echo "package direct-sqlcipher" >> cabal.project.local
|
||||||
echo " extra-include-dirs: /usr/local/opt/openssl@1.1/include" >> cabal.project.local
|
echo " extra-include-dirs: /usr/local/opt/openssl@3.0/include" >> cabal.project.local
|
||||||
echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local
|
echo " extra-lib-dirs: /usr/local/opt/openssl@3.0/lib" >> cabal.project.local
|
||||||
echo " flags: +openssl" >> cabal.project.local
|
echo " flags: +openssl" >> cabal.project.local
|
||||||
|
|
||||||
- name: Install AppImage dependencies
|
- name: Install AppImage dependencies
|
||||||
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
|
if: startsWith(github.ref, 'refs/tags/v') && matrix.asset_name && matrix.os == 'ubuntu-20.04'
|
||||||
run: sudo apt install -y desktop-file-utils
|
run: sudo apt install -y desktop-file-utils
|
||||||
|
|
||||||
- name: Install pkg-config for Mac
|
- name: Install openssl for Mac
|
||||||
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
|
if: matrix.os == 'macos-latest' || matrix.os == 'macos-13'
|
||||||
run: brew install pkg-config
|
run: brew install openssl@3.0
|
||||||
|
|
||||||
- name: Unix prepare cabal.project.local for Ubuntu
|
- name: Unix prepare cabal.project.local for Ubuntu
|
||||||
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04'
|
||||||
@@ -334,7 +334,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
|
export PATH=$PATH:/c/ghcup/bin:$(echo /c/tools/ghc-*/bin || echo)
|
||||||
scripts/desktop/prepare-openssl-windows.sh
|
scripts/desktop/prepare-openssl-windows.sh
|
||||||
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-1.1.1w | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
|
openssl_windows_style_path=$(echo `pwd`/dist-newstyle/openssl-3.0.15 | sed 's#/\([a-zA-Z]\)#\1:#' | sed 's#/#\\#g')
|
||||||
rm cabal.project.local 2>/dev/null || true
|
rm cabal.project.local 2>/dev/null || true
|
||||||
echo "ignore-project: False" >> cabal.project.local
|
echo "ignore-project: False" >> cabal.project.local
|
||||||
echo "package direct-sqlcipher" >> cabal.project.local
|
echo "package direct-sqlcipher" >> cabal.project.local
|
||||||
|
|||||||
@@ -233,6 +233,8 @@ You can use SimpleX with your own servers and still communicate with people usin
|
|||||||
|
|
||||||
Recent and important updates:
|
Recent and important updates:
|
||||||
|
|
||||||
|
[Nov 25, 2025. Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md)
|
||||||
|
|
||||||
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
|
[Oct 14, 2024. SimpleX network: security review of protocols design by Trail of Bits, v6.1 released with better calls and user experience.](./blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md)
|
||||||
|
|
||||||
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
|
[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||||||
application.registerForRemoteNotifications()
|
application.registerForRemoteNotifications()
|
||||||
removePasscodesIfReinstalled()
|
removePasscodesIfReinstalled()
|
||||||
prepareForLaunch()
|
prepareForLaunch()
|
||||||
|
deleteOldChatArchive()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Flux_logo_blue_white.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Flux_logo_blue.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "Flux_symbol_blue-white.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -9,6 +9,18 @@ import SwiftUI
|
|||||||
import Intents
|
import Intents
|
||||||
import SimpleXChat
|
import SimpleXChat
|
||||||
|
|
||||||
|
private enum NoticesSheet: Identifiable {
|
||||||
|
case whatsNew(updatedConditions: Bool)
|
||||||
|
case updatedConditions
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .whatsNew: return "whatsNew"
|
||||||
|
case .updatedConditions: return "updatedConditions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
@ObservedObject var alertManager = AlertManager.shared
|
@ObservedObject var alertManager = AlertManager.shared
|
||||||
@@ -30,7 +42,8 @@ struct ContentView: View {
|
|||||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
@AppStorage(DEFAULT_NOTIFICATION_ALERT_SHOWN) private var notificationAlertShown = false
|
||||||
@State private var showWhatsNew = false
|
@State private var noticesShown = false
|
||||||
|
@State private var noticesSheetItem: NoticesSheet? = nil
|
||||||
@State private var showChooseLAMode = false
|
@State private var showChooseLAMode = false
|
||||||
@State private var showSetPasscode = false
|
@State private var showSetPasscode = false
|
||||||
@State private var waitingForOrPassedAuth = true
|
@State private var waitingForOrPassedAuth = true
|
||||||
@@ -261,8 +274,15 @@ struct ContentView: View {
|
|||||||
alertManager.showAlert(laNoticeAlert())
|
alertManager.showAlert(laNoticeAlert())
|
||||||
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
|
} else if !chatModel.showCallView && CallController.shared.activeCallInvitation == nil {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
if !showWhatsNew {
|
if !noticesShown {
|
||||||
showWhatsNew = shouldShowWhatsNew()
|
let showWhatsNew = shouldShowWhatsNew()
|
||||||
|
let showUpdatedConditions = chatModel.conditions.conditionsAction?.showNotice ?? false
|
||||||
|
noticesShown = showWhatsNew || showUpdatedConditions
|
||||||
|
if showWhatsNew {
|
||||||
|
noticesSheetItem = .whatsNew(updatedConditions: showUpdatedConditions)
|
||||||
|
} else if showUpdatedConditions {
|
||||||
|
noticesSheetItem = .updatedConditions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,8 +290,22 @@ struct ContentView: View {
|
|||||||
connectViaUrl()
|
connectViaUrl()
|
||||||
}
|
}
|
||||||
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
|
.onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() }
|
||||||
.sheet(isPresented: $showWhatsNew) {
|
.sheet(item: $noticesSheetItem) { item in
|
||||||
WhatsNewView()
|
switch item {
|
||||||
|
case let .whatsNew(updatedConditions):
|
||||||
|
WhatsNewView(updatedConditions: updatedConditions)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
.if(updatedConditions) { v in
|
||||||
|
v.task { await setConditionsNotified_() }
|
||||||
|
}
|
||||||
|
case .updatedConditions:
|
||||||
|
UsageConditionsView(
|
||||||
|
currUserServers: Binding.constant([]),
|
||||||
|
userServers: Binding.constant([])
|
||||||
|
)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
.task { await setConditionsNotified_() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if chatModel.setDeliveryReceipts {
|
if chatModel.setDeliveryReceipts {
|
||||||
SetDeliveryReceiptsView()
|
SetDeliveryReceiptsView()
|
||||||
@@ -283,6 +317,15 @@ struct ContentView: View {
|
|||||||
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
|
.onContinueUserActivity("INStartVideoCallIntent", perform: processUserActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setConditionsNotified_() async {
|
||||||
|
do {
|
||||||
|
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
|
||||||
|
try await setConditionsNotified(conditionsId: conditionsId)
|
||||||
|
} catch let error {
|
||||||
|
logger.error("setConditionsNotified error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func processUserActivity(_ activity: NSUserActivity) {
|
private func processUserActivity(_ activity: NSUserActivity) {
|
||||||
let intent = activity.interaction?.intent
|
let intent = activity.interaction?.intent
|
||||||
if let intent = intent as? INStartCallIntent {
|
if let intent = intent as? INStartCallIntent {
|
||||||
|
|||||||
@@ -193,6 +193,8 @@ final class ChatModel: ObservableObject {
|
|||||||
@Published var draft: ComposeState?
|
@Published var draft: ComposeState?
|
||||||
@Published var draftChatId: String?
|
@Published var draftChatId: String?
|
||||||
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
|
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
|
||||||
|
// usage conditions
|
||||||
|
@Published var conditions: ServerOperatorConditions = .empty
|
||||||
|
|
||||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||||
|
|
||||||
@@ -843,7 +845,7 @@ final class ChatModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func dismissConnReqView(_ id: String) {
|
func dismissConnReqView(_ id: String) {
|
||||||
if id == showingInvitation?.connId {
|
if id == showingInvitation?.pcc.id {
|
||||||
markShowingInvitationUsed()
|
markShowingInvitationUsed()
|
||||||
dismissAllSheets()
|
dismissAllSheets()
|
||||||
}
|
}
|
||||||
@@ -896,7 +898,7 @@ final class ChatModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ShowingInvitation {
|
struct ShowingInvitation {
|
||||||
var connId: String
|
var pcc: PendingContactConnection
|
||||||
var connChatUsed: Bool
|
var connChatUsed: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -446,6 +446,13 @@ func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, re
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func apiGetReactionMembers(groupId: Int64, itemId: Int64, reaction: MsgReaction) async throws -> [MemberReaction] {
|
||||||
|
let userId = try currentUserId("apiGetReactionMemebers")
|
||||||
|
let r = await chatSendCmd(.apiGetReactionMembers(userId: userId, groupId: groupId, itemId: itemId, reaction: reaction ))
|
||||||
|
if case let .reactionMembers(_, memberReactions) = r { return memberReactions }
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] {
|
||||||
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay)
|
||||||
if case let .chatItemsDeleted(_, items, _) = r { return items }
|
if case let .chatItemsDeleted(_, items, _) = r { return items }
|
||||||
@@ -500,18 +507,6 @@ func apiDeleteToken(token: DeviceToken) async throws {
|
|||||||
try await sendCommandOkResp(.apiDeleteToken(token: token))
|
try await sendCommandOkResp(.apiDeleteToken(token: token))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserProtoServers(_ serverProtocol: ServerProtocol) throws -> UserProtoServers {
|
|
||||||
let userId = try currentUserId("getUserProtoServers")
|
|
||||||
let r = chatSendCmdSync(.apiGetUserProtoServers(userId: userId, serverProtocol: serverProtocol))
|
|
||||||
if case let .userProtoServers(_, servers) = r { return servers }
|
|
||||||
throw r
|
|
||||||
}
|
|
||||||
|
|
||||||
func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg]) async throws {
|
|
||||||
let userId = try currentUserId("setUserProtoServers")
|
|
||||||
try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> {
|
func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> {
|
||||||
let userId = try currentUserId("testProtoServer")
|
let userId = try currentUserId("testProtoServer")
|
||||||
let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server))
|
let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server))
|
||||||
@@ -524,6 +519,65 @@ func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFail
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getServerOperators() throws -> ServerOperatorConditions {
|
||||||
|
let r = chatSendCmdSync(.apiGetServerOperators)
|
||||||
|
if case let .serverOperatorConditions(conditions) = r { return conditions }
|
||||||
|
logger.error("getServerOperators error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func setServerOperators(operators: [ServerOperator]) async throws -> ServerOperatorConditions {
|
||||||
|
let r = await chatSendCmd(.apiSetServerOperators(operators: operators))
|
||||||
|
if case let .serverOperatorConditions(conditions) = r { return conditions }
|
||||||
|
logger.error("setServerOperators error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserServers() async throws -> [UserOperatorServers] {
|
||||||
|
let userId = try currentUserId("getUserServers")
|
||||||
|
let r = await chatSendCmd(.apiGetUserServers(userId: userId))
|
||||||
|
if case let .userServers(_, userServers) = r { return userServers }
|
||||||
|
logger.error("getUserServers error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUserServers(userServers: [UserOperatorServers]) async throws {
|
||||||
|
let userId = try currentUserId("setUserServers")
|
||||||
|
let r = await chatSendCmd(.apiSetUserServers(userId: userId, userServers: userServers))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
|
logger.error("setUserServers error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateServers(userServers: [UserOperatorServers]) async throws -> [UserServersError] {
|
||||||
|
let userId = try currentUserId("validateServers")
|
||||||
|
let r = await chatSendCmd(.apiValidateServers(userId: userId, userServers: userServers))
|
||||||
|
if case let .userServersValidation(_, serverErrors) = r { return serverErrors }
|
||||||
|
logger.error("validateServers error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUsageConditions() async throws -> (UsageConditions, String?, UsageConditions?) {
|
||||||
|
let r = await chatSendCmd(.apiGetUsageConditions)
|
||||||
|
if case let .usageConditions(usageConditions, conditionsText, acceptedConditions) = r { return (usageConditions, conditionsText, acceptedConditions) }
|
||||||
|
logger.error("getUsageConditions error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConditionsNotified(conditionsId: Int64) async throws {
|
||||||
|
let r = await chatSendCmd(.apiSetConditionsNotified(conditionsId: conditionsId))
|
||||||
|
if case .cmdOk = r { return }
|
||||||
|
logger.error("setConditionsNotified error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptConditions(conditionsId: Int64, operatorIds: [Int64]) async throws -> ServerOperatorConditions {
|
||||||
|
let r = await chatSendCmd(.apiAcceptConditions(conditionsId: conditionsId, operatorIds: operatorIds))
|
||||||
|
if case let .serverOperatorConditions(conditions) = r { return conditions }
|
||||||
|
logger.error("acceptConditions error: \(String(describing: r))")
|
||||||
|
throw r
|
||||||
|
}
|
||||||
|
|
||||||
func getChatItemTTL() throws -> ChatItemTTL {
|
func getChatItemTTL() throws -> ChatItemTTL {
|
||||||
let userId = try currentUserId("getChatItemTTL")
|
let userId = try currentUserId("getChatItemTTL")
|
||||||
return try chatItemTTLResponse(chatSendCmdSync(.apiGetChatItemTTL(userId: userId)))
|
return try chatItemTTLResponse(chatSendCmdSync(.apiGetChatItemTTL(userId: userId)))
|
||||||
@@ -1014,8 +1068,8 @@ func apiRejectContactRequest(contactReqId: Int64) async throws {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) async throws {
|
func apiChatRead(type: ChatType, id: Int64) async throws {
|
||||||
try await sendCommandOkResp(.apiChatRead(type: type, id: id, itemRange: itemRange))
|
try await sendCommandOkResp(.apiChatRead(type: type, id: id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
|
func apiChatItemsRead(type: ChatType, id: Int64, itemIds: [Int64]) async throws {
|
||||||
@@ -1321,15 +1375,13 @@ func apiGetNetworkStatuses() throws -> [ConnNetworkStatus] {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func markChatRead(_ chat: Chat, aboveItem: ChatItem? = nil) async {
|
func markChatRead(_ chat: Chat) async {
|
||||||
do {
|
do {
|
||||||
if chat.chatStats.unreadCount > 0 {
|
if chat.chatStats.unreadCount > 0 {
|
||||||
let minItemId = chat.chatStats.minUnreadItemId
|
|
||||||
let itemRange = (minItemId, aboveItem?.id ?? chat.chatItems.last?.id ?? minItemId)
|
|
||||||
let cInfo = chat.chatInfo
|
let cInfo = chat.chatInfo
|
||||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: itemRange)
|
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
withAnimation { ChatModel.shared.markChatItemsRead(cInfo, aboveItem: aboveItem) }
|
withAnimation { ChatModel.shared.markChatItemsRead(cInfo) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if chat.chatStats.unreadChat {
|
if chat.chatStats.unreadChat {
|
||||||
@@ -1352,17 +1404,6 @@ func markChatUnread(_ chat: Chat, unreadChat: Bool = true) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
|
|
||||||
do {
|
|
||||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id))
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
ChatModel.shared.markChatItemsRead(cInfo, [cItem.id])
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
logger.error("apiChatRead error: \(responseError(error))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async {
|
func apiMarkChatItemsRead(_ cInfo: ChatInfo, _ itemIds: [ChatItem.ID]) async {
|
||||||
do {
|
do {
|
||||||
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
|
try await apiChatItemsRead(type: cInfo.chatType, id: cInfo.apiId, itemIds: itemIds)
|
||||||
@@ -1558,6 +1599,16 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
|||||||
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get())
|
||||||
m.chatInitialized = true
|
m.chatInitialized = true
|
||||||
m.currentUser = try apiGetActiveUser()
|
m.currentUser = try apiGetActiveUser()
|
||||||
|
m.conditions = try getServerOperators()
|
||||||
|
if shouldImportAppSettingsDefault.get() {
|
||||||
|
do {
|
||||||
|
let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
|
appSettings.importIntoApp()
|
||||||
|
shouldImportAppSettingsDefault.set(false)
|
||||||
|
} catch {
|
||||||
|
logger.error("Error while importing app settings: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
if m.currentUser == nil {
|
if m.currentUser == nil {
|
||||||
onboardingStageDefault.set(.step1_SimpleXInfo)
|
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||||
privacyDeliveryReceiptsSet.set(true)
|
privacyDeliveryReceiptsSet.set(true)
|
||||||
@@ -1602,7 +1653,7 @@ private func chatInitialized(start: Bool, refreshInvitations: Bool) throws {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startChat(refreshInvitations: Bool = true) throws {
|
func startChat(refreshInvitations: Bool = true, onboarding: Bool = false) throws {
|
||||||
logger.debug("startChat")
|
logger.debug("startChat")
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
try setNetworkConfig(getNetCfg())
|
try setNetworkConfig(getNetCfg())
|
||||||
@@ -1621,13 +1672,15 @@ func startChat(refreshInvitations: Bool = true) throws {
|
|||||||
if let token = m.deviceToken {
|
if let token = m.deviceToken {
|
||||||
registerToken(token: token)
|
registerToken(token: token)
|
||||||
}
|
}
|
||||||
withAnimation {
|
if !onboarding {
|
||||||
let savedOnboardingStage = onboardingStageDefault.get()
|
withAnimation {
|
||||||
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
|
let savedOnboardingStage = onboardingStageDefault.get()
|
||||||
? .step3_CreateSimpleXAddress
|
m.onboardingStage = [.step1_SimpleXInfo, .step2_CreateProfile].contains(savedOnboardingStage) && m.users.count == 1
|
||||||
: savedOnboardingStage
|
? .step3_ChooseServerOperators
|
||||||
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
|
: savedOnboardingStage
|
||||||
m.setDeliveryReceipts = true
|
if m.onboardingStage == .onboardingComplete && !privacyDeliveryReceiptsSet.get() {
|
||||||
|
m.setDeliveryReceipts = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1961,6 +2014,18 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
|||||||
m.removeChat(hostConn.id)
|
m.removeChat(hostConn.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case let .businessLinkConnecting(user, groupInfo, _, fromContact):
|
||||||
|
if !active(user) { return }
|
||||||
|
|
||||||
|
await MainActor.run {
|
||||||
|
m.updateGroup(groupInfo)
|
||||||
|
}
|
||||||
|
if m.chatId == fromContact.id {
|
||||||
|
ItemsModel.shared.loadOpenChat(groupInfo.id)
|
||||||
|
}
|
||||||
|
await MainActor.run {
|
||||||
|
m.removeChat(fromContact.id)
|
||||||
|
}
|
||||||
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
case let .joinedGroupMemberConnecting(user, groupInfo, _, member):
|
||||||
if active(user) {
|
if active(user) {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct ChatInfoToolbar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var contactVerifiedShield: Text {
|
private var contactVerifiedShield: Text {
|
||||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.baselineOffset(1)
|
.baselineOffset(1)
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ struct ChatInfoView: View {
|
|||||||
Text(Image(systemName: "checkmark.shield"))
|
Text(Image(systemName: "checkmark.shield"))
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
+ Text(" ")
|
+ textSpace
|
||||||
+ Text(contact.profile.displayName)
|
+ Text(contact.profile.displayName)
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ struct CIFeaturePreferenceView: View {
|
|||||||
+ Text(acceptText)
|
+ Text(acceptText)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(theme.colors.primary)
|
.foregroundColor(theme.colors.primary)
|
||||||
+ Text(" ")
|
+ Text(verbatim: " ")
|
||||||
}
|
}
|
||||||
r = r + chatItem.timestampText
|
r = r + chatItem.timestampText
|
||||||
.fontWeight(.light)
|
.fontWeight(.light)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct CIGroupInvitationView: View {
|
|||||||
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
Text(chatIncognito ? "Tap to join incognito" : "Tap to join")
|
||||||
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
+ Text(" ")
|
+ Text(verbatim: " ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
)
|
)
|
||||||
.overlay(DetermineWidth())
|
.overlay(DetermineWidth())
|
||||||
@@ -53,7 +53,7 @@ struct CIGroupInvitationView: View {
|
|||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
groupInvitationText()
|
groupInvitationText()
|
||||||
+ Text(" ")
|
+ Text(verbatim: " ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
)
|
)
|
||||||
.overlay(DetermineWidth())
|
.overlay(DetermineWidth())
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct CIMemberCreatedContactView: View {
|
|||||||
+ Text(openText)
|
+ Text(openText)
|
||||||
.fontWeight(.medium)
|
.fontWeight(.medium)
|
||||||
.foregroundColor(theme.colors.primary)
|
.foregroundColor(theme.colors.primary)
|
||||||
+ Text(" ")
|
+ Text(verbatim: " ")
|
||||||
}
|
}
|
||||||
r = r + chatItem.timestampText
|
r = r + chatItem.timestampText
|
||||||
.fontWeight(.light)
|
.fontWeight(.light)
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ enum MetaColorMode {
|
|||||||
? Image("checkmark.wide")
|
? Image("checkmark.wide")
|
||||||
: Image(systemName: "circlebadge.fill")
|
: Image(systemName: "circlebadge.fill")
|
||||||
).foregroundColor(.clear)
|
).foregroundColor(.clear)
|
||||||
case .invertedMaterial: Text(" ").kerning(13)
|
case .invertedMaterial: textSpace.kerning(13)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ func ciMetaText(
|
|||||||
if ttl != chatTTL {
|
if ttl != chatTTL {
|
||||||
r = r + colored(Text(shortTimeText(ttl)), resolved)
|
r = r + colored(Text(shortTimeText(ttl)), resolved)
|
||||||
}
|
}
|
||||||
space = Text(" ")
|
space = textSpace
|
||||||
}
|
}
|
||||||
if showViaProxy, meta.sentViaProxy == true {
|
if showViaProxy, meta.sentViaProxy == true {
|
||||||
appendSpace()
|
appendSpace()
|
||||||
@@ -138,12 +138,12 @@ func ciMetaText(
|
|||||||
} else if !meta.disappearing {
|
} else if !meta.disappearing {
|
||||||
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
|
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
|
||||||
}
|
}
|
||||||
space = Text(" ")
|
space = textSpace
|
||||||
}
|
}
|
||||||
if let enc = encrypted {
|
if let enc = encrypted {
|
||||||
appendSpace()
|
appendSpace()
|
||||||
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
|
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
|
||||||
space = Text(" ")
|
space = textSpace
|
||||||
}
|
}
|
||||||
if showTimesamp {
|
if showTimesamp {
|
||||||
appendSpace()
|
appendSpace()
|
||||||
|
|||||||
@@ -121,11 +121,11 @@ struct CIRcvDecryptionError: View {
|
|||||||
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath"))
|
||||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
+ Text(" ")
|
+ textSpace
|
||||||
+ Text("Fix connection")
|
+ Text("Fix connection")
|
||||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
+ Text(" ")
|
+ Text(verbatim: " ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ struct CIRcvDecryptionError: View {
|
|||||||
Text(chatItem.content.text)
|
Text(chatItem.content.text)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
.italic()
|
.italic()
|
||||||
+ Text(" ")
|
+ Text(verbatim: " ")
|
||||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ struct MarkedDeletedItemView: View {
|
|||||||
var chatItem: ChatItem
|
var chatItem: ChatItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
(Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText)
|
(Text(mergedMarkedDeletedText).italic() + textSpace + chatItem.timestampText)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SimpleXChat
|
|||||||
|
|
||||||
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
let uiLinkColor = UIColor(red: 0, green: 0.533, blue: 1, alpha: 1)
|
||||||
|
|
||||||
private let noTyping = Text(" ")
|
private let noTyping = Text(verbatim: " ")
|
||||||
|
|
||||||
private let typingIndicators: [Text] = [
|
private let typingIndicators: [Text] = [
|
||||||
(typing(.black) + typing() + typing()),
|
(typing(.black) + typing() + typing()),
|
||||||
@@ -85,7 +85,7 @@ struct MsgContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
(rightToLeft ? Text("\n") : Text(verbatim: " ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let i = icon {
|
if let i = icon {
|
||||||
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + Text(" ") + res
|
res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + textSpace + res
|
||||||
}
|
}
|
||||||
|
|
||||||
if let s = sender {
|
if let s = sender {
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
|
|
||||||
private func eventItemViewText(_ secondaryColor: Color) -> Text {
|
private func eventItemViewText(_ secondaryColor: Color) -> Text {
|
||||||
if !revealed, let t = mergedGroupEventText {
|
if !revealed, let t = mergedGroupEventText {
|
||||||
return chatEventText(t + Text(" ") + chatItem.timestampText, secondaryColor)
|
return chatEventText(t + textSpace + chatItem.timestampText, secondaryColor)
|
||||||
} else if let member = chatItem.memberDisplayName {
|
} else if let member = chatItem.memberDisplayName {
|
||||||
return Text(member + " ")
|
return Text(member + " ")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -203,7 +203,7 @@ struct ChatItemContentView<Content: View>: View {
|
|||||||
} else if ns.count == 0 {
|
} else if ns.count == 0 {
|
||||||
Text("\(count) group events")
|
Text("\(count) group events")
|
||||||
} else if count > ns.count {
|
} else if count > ns.count {
|
||||||
Text(members) + Text(" ") + Text("and \(count - ns.count) other events")
|
Text(members) + textSpace + Text("and \(count - ns.count) other events")
|
||||||
} else {
|
} else {
|
||||||
Text(members)
|
Text(members)
|
||||||
}
|
}
|
||||||
@@ -234,7 +234,7 @@ func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text {
|
func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text {
|
||||||
chatEventText(Text(eventText) + Text(" ") + ts, secondaryColor)
|
chatEventText(Text(eventText) + textSpace + ts, secondaryColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text {
|
||||||
|
|||||||
@@ -901,7 +901,7 @@ struct ChatView: View {
|
|||||||
@State private var showChatItemInfoSheet: Bool = false
|
@State private var showChatItemInfoSheet: Bool = false
|
||||||
@State private var chatItemInfo: ChatItemInfo?
|
@State private var chatItemInfo: ChatItemInfo?
|
||||||
@State private var msgWidth: CGFloat = 0
|
@State private var msgWidth: CGFloat = 0
|
||||||
|
|
||||||
@Binding var selectedChatItems: Set<Int64>?
|
@Binding var selectedChatItems: Set<Int64>?
|
||||||
@Binding var forwardedChatItems: [ChatItem]
|
@Binding var forwardedChatItems: [ChatItem]
|
||||||
|
|
||||||
@@ -987,7 +987,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
} else if chatItem.isRcvNew {
|
} else if chatItem.isRcvNew {
|
||||||
waitToMarkRead {
|
waitToMarkRead {
|
||||||
await apiMarkChatItemRead(chat.chatInfo, chatItem)
|
await apiMarkChatItemsRead(chat.chatInfo, [chatItem.id])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1117,14 +1117,12 @@ struct ChatView: View {
|
|||||||
HStack(alignment: .top, spacing: 10) {
|
HStack(alignment: .top, spacing: 10) {
|
||||||
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
|
MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if let member = m.getGroupMember(member.groupMemberId) {
|
if let mem = m.getGroupMember(member.groupMemberId) {
|
||||||
selectedMember = member
|
selectedMember = mem
|
||||||
} else {
|
} else {
|
||||||
Task {
|
let mem = GMember.init(member)
|
||||||
await m.loadGroupMembers(groupInfo) {
|
m.groupMembers.append(mem)
|
||||||
selectedMember = m.getGroupMember(member.groupMemberId)
|
selectedMember = mem
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
chatItemWithMenu(ci, range, maxWidth, itemSeparation)
|
||||||
@@ -1244,11 +1242,20 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
|
.if(chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted)) { v in
|
||||||
if chat.chatInfo.featureEnabled(.reactions) && (ci.allowAddReaction || r.userReacted) {
|
|
||||||
v.onTapGesture {
|
v.onTapGesture {
|
||||||
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
|
setReaction(ci, add: !r.userReacted, reaction: r.reaction)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if case let .group(groupInfo) = chat.chatInfo {
|
||||||
|
v.contextMenu {
|
||||||
|
ReactionContextMenu(
|
||||||
|
groupInfo: groupInfo,
|
||||||
|
itemId: ci.id,
|
||||||
|
reactionCount: r,
|
||||||
|
selectedMember: $selectedMember
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
v
|
v
|
||||||
}
|
}
|
||||||
@@ -1838,6 +1845,108 @@ private func buildTheme() -> AppTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ReactionContextMenu: View {
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
let groupInfo: GroupInfo
|
||||||
|
var itemId: Int64
|
||||||
|
var reactionCount: CIReactionCount
|
||||||
|
@Binding var selectedMember: GMember?
|
||||||
|
@State private var memberReactions: [MemberReaction] = []
|
||||||
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
groupMemberReactionList()
|
||||||
|
.task {
|
||||||
|
logger.debug("ReactionContextMenu task \(radius)")
|
||||||
|
await loadChatItemReaction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func groupMemberReactionList() -> some View {
|
||||||
|
if memberReactions.isEmpty {
|
||||||
|
ForEach(Array(repeating: 0, count: reactionCount.totalReacted), id: \.self) { _ in
|
||||||
|
Text(verbatim: " ")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ForEach(memberReactions, id: \.groupMember.groupMemberId) { mr in
|
||||||
|
let mem = mr.groupMember
|
||||||
|
let userMember = mem.groupMemberId == groupInfo.membership.groupMemberId
|
||||||
|
Button {
|
||||||
|
if let member = m.getGroupMember(mem.groupMemberId) {
|
||||||
|
selectedMember = member
|
||||||
|
} else {
|
||||||
|
let member = GMember.init(mem)
|
||||||
|
m.groupMembers.append(member)
|
||||||
|
selectedMember = member
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text(mem.displayName)
|
||||||
|
if let img = cropImage(mem.image) {
|
||||||
|
Image(uiImage: img)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "person.crop.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(userMember)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cropImage(_ img: String?) -> UIImage? {
|
||||||
|
return if let originalImage = imageFromBase64(img) {
|
||||||
|
maskToCustomShape(originalImage, size: 30, radius: radius)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadChatItemReaction() async {
|
||||||
|
do {
|
||||||
|
let memberReactions = try await apiGetReactionMembers(
|
||||||
|
groupId: groupInfo.groupId,
|
||||||
|
itemId: itemId,
|
||||||
|
reaction: reactionCount.reaction
|
||||||
|
)
|
||||||
|
await MainActor.run {
|
||||||
|
self.memberReactions = memberReactions
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("apiGetReactionMembers error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskToCustomShape(_ image: UIImage, size: CGFloat, radius: CGFloat) -> UIImage {
|
||||||
|
let path = Path { path in
|
||||||
|
if radius >= 50 {
|
||||||
|
path.addEllipse(in: CGRect(x: 0, y: 0, width: size, height: size))
|
||||||
|
} else if radius <= 0 {
|
||||||
|
path.addRect(CGRect(x: 0, y: 0, width: size, height: size))
|
||||||
|
} else {
|
||||||
|
let cornerRadius = size * CGFloat(radius) / 100
|
||||||
|
path.addRoundedRect(
|
||||||
|
in: CGRect(x: 0, y: 0, width: size, height: size),
|
||||||
|
cornerSize: CGSize(width: cornerRadius, height: cornerRadius),
|
||||||
|
style: .continuous
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIGraphicsImageRenderer(size: CGSize(width: size, height: size)).image { context in
|
||||||
|
context.cgContext.addPath(path.cgPath)
|
||||||
|
context.cgContext.clip()
|
||||||
|
let scale = size / max(image.size.width, image.size.height)
|
||||||
|
let imageSize = CGSize(width: image.size.width * scale, height: image.size.height * scale)
|
||||||
|
let imageOrigin = CGPoint(
|
||||||
|
x: (size - imageSize.width) / 2,
|
||||||
|
y: (size - imageSize.height) / 2
|
||||||
|
)
|
||||||
|
image.draw(in: CGRect(origin: imageOrigin, size: imageSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ToggleNtfsButton: View {
|
struct ToggleNtfsButton: View {
|
||||||
@ObservedObject var chat: Chat
|
@ObservedObject var chat: Chat
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ struct ContextItemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func image(_ s: String) -> Text {
|
func image(_ s: String) -> Text {
|
||||||
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ")
|
Text(Image(systemName: s)).foregroundColor(Color(uiColor: .tertiaryLabel)) + textSpace
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,12 +140,13 @@ struct AddGroupMembersViewCommon: View {
|
|||||||
return dummy
|
return dummy
|
||||||
}()
|
}()
|
||||||
|
|
||||||
private func inviteMembersButton() -> some View {
|
@ViewBuilder private func inviteMembersButton() -> some View {
|
||||||
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Invite to group" : "Invite to chat"
|
||||||
Button {
|
Button {
|
||||||
inviteMembers()
|
inviteMembers()
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Invite to group")
|
Text(label)
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,6 +232,7 @@ func searchFieldView(text: Binding<String>, focussed: FocusState<Bool>.Binding,
|
|||||||
.focused(focussed)
|
.focused(focussed)
|
||||||
.foregroundColor(onBackgroundColor)
|
.foregroundColor(onBackgroundColor)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
Image(systemName: "xmark.circle.fill")
|
Image(systemName: "xmark.circle.fill")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
|
|||||||
@@ -81,10 +81,10 @@ struct GroupChatInfoView: View {
|
|||||||
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
if groupInfo.canEdit {
|
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
||||||
editGroupButton()
|
editGroupButton()
|
||||||
}
|
}
|
||||||
if groupInfo.groupProfile.description != nil || groupInfo.canEdit {
|
if groupInfo.groupProfile.description != nil || (groupInfo.isOwner && groupInfo.businessChat == nil) {
|
||||||
addOrEditWelcomeMessage()
|
addOrEditWelcomeMessage()
|
||||||
}
|
}
|
||||||
groupPreferencesButton($groupInfo)
|
groupPreferencesButton($groupInfo)
|
||||||
@@ -101,13 +101,20 @@ struct GroupChatInfoView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("")
|
Text("")
|
||||||
} footer: {
|
} footer: {
|
||||||
Text("Only group owners can change group preferences.")
|
let label: LocalizedStringKey = (
|
||||||
|
groupInfo.businessChat == nil
|
||||||
|
? "Only group owners can change group preferences."
|
||||||
|
: "Only chat owners can change preferences."
|
||||||
|
)
|
||||||
|
Text(label)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
|
Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) {
|
||||||
if groupInfo.canAddMembers {
|
if groupInfo.canAddMembers {
|
||||||
groupLinkButton()
|
if groupInfo.businessChat == nil {
|
||||||
|
groupLinkButton()
|
||||||
|
}
|
||||||
if (chat.chatInfo.incognito) {
|
if (chat.chatInfo.incognito) {
|
||||||
Label("Invite members", systemImage: "plus")
|
Label("Invite members", systemImage: "plus")
|
||||||
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
.foregroundColor(Color(uiColor: .tertiaryLabel))
|
||||||
@@ -276,10 +283,15 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func addMembersButton() -> some View {
|
private func addMembersButton() -> some View {
|
||||||
NavigationLink {
|
let label: LocalizedStringKey = switch groupInfo.businessChat?.chatType {
|
||||||
|
case .customer: "Add team members"
|
||||||
|
case .business: "Add friends"
|
||||||
|
case .none: "Invite members"
|
||||||
|
}
|
||||||
|
return NavigationLink {
|
||||||
addMembersDestinationView()
|
addMembersDestinationView()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Invite members", systemImage: "plus")
|
Label(label, systemImage: "plus")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,7 +430,7 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var memberVerifiedShield: Text {
|
private var memberVerifiedShield: Text {
|
||||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.baselineOffset(2)
|
.baselineOffset(2)
|
||||||
.kerning(-2)
|
.kerning(-2)
|
||||||
@@ -487,11 +499,12 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteGroupButton() -> some View {
|
@ViewBuilder private func deleteGroupButton() -> some View {
|
||||||
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group" : "Delete chat"
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
alert = .deleteGroupAlert
|
alert = .deleteGroupAlert
|
||||||
} label: {
|
} label: {
|
||||||
Label("Delete group", systemImage: "trash")
|
Label(label, systemImage: "trash")
|
||||||
.foregroundColor(Color.red)
|
.foregroundColor(Color.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -505,20 +518,22 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func leaveGroupButton() -> some View {
|
@ViewBuilder private func leaveGroupButton() -> some View {
|
||||||
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group" : "Leave chat"
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
alert = .leaveGroupAlert
|
alert = .leaveGroupAlert
|
||||||
} label: {
|
} label: {
|
||||||
Label("Leave group", systemImage: "rectangle.portrait.and.arrow.right")
|
Label(label, systemImage: "rectangle.portrait.and.arrow.right")
|
||||||
.foregroundColor(Color.red)
|
.foregroundColor(Color.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO reuse this and clearChatAlert with ChatInfoView
|
// TODO reuse this and clearChatAlert with ChatInfoView
|
||||||
private func deleteGroupAlert() -> Alert {
|
private func deleteGroupAlert() -> Alert {
|
||||||
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Delete group?"),
|
title: Text(label),
|
||||||
message: deleteGroupAlertMessage(),
|
message: deleteGroupAlertMessage(groupInfo),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@@ -537,10 +552,6 @@ struct GroupChatInfoView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteGroupAlertMessage() -> Text {
|
|
||||||
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearChatAlert() -> Alert {
|
private func clearChatAlert() -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Clear conversation?"),
|
title: Text("Clear conversation?"),
|
||||||
@@ -556,9 +567,15 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func leaveGroupAlert() -> Alert {
|
private func leaveGroupAlert() -> Alert {
|
||||||
Alert(
|
let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
|
||||||
title: Text("Leave group?"),
|
let messageLabel: LocalizedStringKey = (
|
||||||
message: Text("You will stop receiving messages from this group. Chat history will be preserved."),
|
groupInfo.businessChat == nil
|
||||||
|
? "You will stop receiving messages from this group. Chat history will be preserved."
|
||||||
|
: "You will stop receiving messages from this chat. Chat history will be preserved."
|
||||||
|
)
|
||||||
|
return Alert(
|
||||||
|
title: Text(titleLabel),
|
||||||
|
message: Text(messageLabel),
|
||||||
primaryButton: .destructive(Text("Leave")) {
|
primaryButton: .destructive(Text("Leave")) {
|
||||||
Task {
|
Task {
|
||||||
await leaveGroup(chat.chatInfo.apiId)
|
await leaveGroup(chat.chatInfo.apiId)
|
||||||
@@ -602,9 +619,14 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
||||||
Alert(
|
let messageLabel: LocalizedStringKey = (
|
||||||
|
groupInfo.businessChat == nil
|
||||||
|
? "Member will be removed from group - this cannot be undone!"
|
||||||
|
: "Member will be removed from chat - this cannot be undone!"
|
||||||
|
)
|
||||||
|
return Alert(
|
||||||
title: Text("Remove member?"),
|
title: Text("Remove member?"),
|
||||||
message: Text("Member will be removed from group - this cannot be undone!"),
|
message: Text(messageLabel),
|
||||||
primaryButton: .destructive(Text("Remove")) {
|
primaryButton: .destructive(Text("Remove")) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@@ -624,22 +646,31 @@ struct GroupChatInfoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
|
||||||
|
groupInfo.businessChat == nil ? (
|
||||||
|
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
||||||
|
) : (
|
||||||
|
groupInfo.membership.memberCurrent ? Text("Chat will be deleted for all members - this cannot be undone!") : Text("Chat will be deleted for you - this cannot be undone!")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
|
func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View {
|
||||||
NavigationLink {
|
let label: LocalizedStringKey = groupInfo.wrappedValue.businessChat == nil ? "Group preferences" : "Chat preferences"
|
||||||
|
return NavigationLink {
|
||||||
GroupPreferencesView(
|
GroupPreferencesView(
|
||||||
groupInfo: groupInfo,
|
groupInfo: groupInfo,
|
||||||
preferences: groupInfo.wrappedValue.fullGroupPreferences,
|
preferences: groupInfo.wrappedValue.fullGroupPreferences,
|
||||||
currentPreferences: groupInfo.wrappedValue.fullGroupPreferences,
|
currentPreferences: groupInfo.wrappedValue.fullGroupPreferences,
|
||||||
creatingGroup: creatingGroup
|
creatingGroup: creatingGroup
|
||||||
)
|
)
|
||||||
.navigationBarTitle("Group preferences")
|
.navigationBarTitle(label)
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
if creatingGroup {
|
if creatingGroup {
|
||||||
Text("Set group preferences")
|
Text("Set group preferences")
|
||||||
} else {
|
} else {
|
||||||
Label("Group preferences", systemImage: "switch.2")
|
Label(label, systemImage: "switch.2")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ struct GroupMemberInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
|
Section(header: Text("Member").foregroundColor(theme.colors.secondary)) {
|
||||||
infoRow("Group", groupInfo.displayName)
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Group" : "Chat"
|
||||||
|
infoRow(label, groupInfo.displayName)
|
||||||
|
|
||||||
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
if let roles = member.canChangeRoleTo(groupInfo: groupInfo) {
|
||||||
Picker("Change role", selection: $newRole) {
|
Picker("Change role", selection: $newRole) {
|
||||||
@@ -305,10 +306,15 @@ struct GroupMemberInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) {
|
func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) {
|
||||||
|
let messageLabel: LocalizedStringKey = (
|
||||||
|
groupInfo.businessChat == nil
|
||||||
|
? "Direct messages between members are prohibited."
|
||||||
|
: "Direct messages between members are prohibited in this chat."
|
||||||
|
)
|
||||||
alert = .someAlert(alert: SomeAlert(
|
alert = .someAlert(alert: SomeAlert(
|
||||||
alert: mkAlert(
|
alert: mkAlert(
|
||||||
title: title,
|
title: title,
|
||||||
message: "Direct messages between members are prohibited in this group."
|
message: messageLabel
|
||||||
),
|
),
|
||||||
id: "can't message member, direct messages prohibited"
|
id: "can't message member, direct messages prohibited"
|
||||||
))
|
))
|
||||||
@@ -388,7 +394,7 @@ struct GroupMemberInfoView: View {
|
|||||||
Text(Image(systemName: "checkmark.shield"))
|
Text(Image(systemName: "checkmark.shield"))
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
+ Text(" ")
|
+ textSpace
|
||||||
+ Text(mem.displayName)
|
+ Text(mem.displayName)
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
)
|
)
|
||||||
@@ -537,9 +543,14 @@ struct GroupMemberInfoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
private func removeMemberAlert(_ mem: GroupMember) -> Alert {
|
||||||
Alert(
|
let label: LocalizedStringKey = (
|
||||||
|
groupInfo.businessChat == nil
|
||||||
|
? "Member will be removed from group - this cannot be undone!"
|
||||||
|
: "Member will be removed from chat - this cannot be undone!"
|
||||||
|
)
|
||||||
|
return Alert(
|
||||||
title: Text("Remove member?"),
|
title: Text("Remove member?"),
|
||||||
message: Text("Member will be removed from group - this cannot be undone!"),
|
message: Text(label),
|
||||||
primaryButton: .destructive(Text("Remove")) {
|
primaryButton: .destructive(Text("Remove")) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@@ -562,7 +573,15 @@ struct GroupMemberInfoView: View {
|
|||||||
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
|
private func changeMemberRoleAlert(_ mem: GroupMember) -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Change member role?"),
|
title: Text("Change member role?"),
|
||||||
message: mem.memberCurrent ? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.") : Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation."),
|
message: (
|
||||||
|
mem.memberCurrent
|
||||||
|
? (
|
||||||
|
groupInfo.businessChat == nil
|
||||||
|
? Text("Member role will be changed to \"\(newRole.text)\". All group members will be notified.")
|
||||||
|
: Text("Member role will be changed to \"\(newRole.text)\". All chat members will be notified.")
|
||||||
|
)
|
||||||
|
: Text("Member role will be changed to \"\(newRole.text)\". The member will receive a new invitation.")
|
||||||
|
),
|
||||||
primaryButton: .default(Text("Change")) {
|
primaryButton: .default(Text("Change")) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
@@ -570,7 +589,7 @@ struct GroupMemberInfoView: View {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
_ = chatModel.upsertGroupMember(groupInfo, updatedMember)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch let error {
|
} catch let error {
|
||||||
newRole = mem.memberRole
|
newRole = mem.memberRole
|
||||||
logger.error("apiMemberRole error: \(responseError(error))")
|
logger.error("apiMemberRole error: \(responseError(error))")
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ struct GroupPreferencesView: View {
|
|||||||
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role)
|
||||||
featureSection(.history, $preferences.history.enable)
|
featureSection(.history, $preferences.history.enable)
|
||||||
|
|
||||||
if groupInfo.canEdit {
|
if groupInfo.isOwner {
|
||||||
Section {
|
Section {
|
||||||
Button("Reset") { preferences = currentPreferences }
|
Button("Reset") { preferences = currentPreferences }
|
||||||
Button(saveText) { savePreferences() }
|
Button(saveText) { savePreferences() }
|
||||||
@@ -77,7 +77,7 @@ struct GroupPreferencesView: View {
|
|||||||
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
|
let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary
|
||||||
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
|
let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon
|
||||||
let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on
|
let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on
|
||||||
if groupInfo.canEdit {
|
if groupInfo.isOwner {
|
||||||
let enable = Binding(
|
let enable = Binding(
|
||||||
get: { enableFeature.wrappedValue == .on },
|
get: { enableFeature.wrappedValue == .on },
|
||||||
set: { on, _ in enableFeature.wrappedValue = on ? .on : .off }
|
set: { on, _ in enableFeature.wrappedValue = on ? .on : .off }
|
||||||
@@ -123,7 +123,7 @@ struct GroupPreferencesView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit))
|
Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.isOwner))
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
.onChange(of: enableFeature.wrappedValue) { enabled in
|
.onChange(of: enableFeature.wrappedValue) { enabled in
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct GroupWelcomeView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
if groupInfo.canEdit {
|
if groupInfo.isOwner && groupInfo.businessChat == nil {
|
||||||
editorView()
|
editorView()
|
||||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
if welcomeTextUnchanged() {
|
if welcomeTextUnchanged() {
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ struct ChatHelp: View {
|
|||||||
Text("above, then choose:")
|
Text("above, then choose:")
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("**Add contact**: to create a new invitation link, or connect via a link you received.")
|
Text("**Create 1-time link**: to create and share a new invitation link.")
|
||||||
|
Text("**Scan / Paste link**: to connect via a link you received.")
|
||||||
Text("**Create group**: to create a new group.")
|
Text("**Create group**: to create a new group.")
|
||||||
}
|
}
|
||||||
.padding(.top, 24)
|
.padding(.top, 24)
|
||||||
|
|||||||
@@ -404,8 +404,9 @@ struct ChatListNavLink: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||||
Alert(
|
let label: LocalizedStringKey = groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
|
||||||
title: Text("Delete group?"),
|
return Alert(
|
||||||
|
title: Text(label),
|
||||||
message: deleteGroupAlertMessage(groupInfo),
|
message: deleteGroupAlertMessage(groupInfo),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
Task { await deleteChat(chat) }
|
Task { await deleteChat(chat) }
|
||||||
@@ -414,10 +415,6 @@ struct ChatListNavLink: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteGroupAlertMessage(_ groupInfo: GroupInfo) -> Text {
|
|
||||||
groupInfo.membership.memberCurrent ? Text("Group will be deleted for all members - this cannot be undone!") : Text("Group will be deleted for you - this cannot be undone!")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearChatAlert() -> Alert {
|
private func clearChatAlert() -> Alert {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Clear conversation?"),
|
title: Text("Clear conversation?"),
|
||||||
@@ -441,9 +438,15 @@ struct ChatListNavLink: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
private func leaveGroupAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||||
Alert(
|
let titleLabel: LocalizedStringKey = groupInfo.businessChat == nil ? "Leave group?" : "Leave chat?"
|
||||||
title: Text("Leave group?"),
|
let messageLabel: LocalizedStringKey = (
|
||||||
message: Text("You will stop receiving messages from this group. Chat history will be preserved."),
|
groupInfo.businessChat == nil
|
||||||
|
? "You will stop receiving messages from this group. Chat history will be preserved."
|
||||||
|
: "You will stop receiving messages from this chat. Chat history will be preserved."
|
||||||
|
)
|
||||||
|
return Alert(
|
||||||
|
title: Text(titleLabel),
|
||||||
|
message: Text(messageLabel),
|
||||||
primaryButton: .destructive(Text("Leave")) {
|
primaryButton: .destructive(Text("Leave")) {
|
||||||
Task { await leaveGroup(groupInfo.groupId) }
|
Task { await leaveGroup(groupInfo.groupId) }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,9 +31,21 @@ enum UserPickerSheet: Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SaveableSettings: ObservableObject {
|
||||||
|
@Published var servers: ServerSettings = ServerSettings(currUserServers: [], userServers: [], serverErrors: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServerSettings {
|
||||||
|
public var currUserServers: [UserOperatorServers]
|
||||||
|
public var userServers: [UserOperatorServers]
|
||||||
|
public var serverErrors: [UserServersError]
|
||||||
|
}
|
||||||
|
|
||||||
struct UserPickerSheetView: View {
|
struct UserPickerSheetView: View {
|
||||||
let sheet: UserPickerSheet
|
let sheet: UserPickerSheet
|
||||||
@EnvironmentObject var chatModel: ChatModel
|
@EnvironmentObject var chatModel: ChatModel
|
||||||
|
@StateObject private var ss = SaveableSettings()
|
||||||
|
|
||||||
@State private var loaded = false
|
@State private var loaded = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -76,6 +88,21 @@ struct UserPickerSheetView: View {
|
|||||||
{ loaded = true }
|
{ loaded = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if serversCanBeSaved(
|
||||||
|
ss.servers.currUserServers,
|
||||||
|
ss.servers.userServers,
|
||||||
|
ss.servers.serverErrors
|
||||||
|
) {
|
||||||
|
showAlert(
|
||||||
|
title: NSLocalizedString("Save servers?", comment: "alert title"),
|
||||||
|
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
|
||||||
|
buttonAction: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) },
|
||||||
|
cancelButton: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.environmentObject(ss)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +121,7 @@ struct ChatListView: View {
|
|||||||
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
@AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false
|
||||||
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
@AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true
|
||||||
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
|
@AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false
|
||||||
|
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
|
||||||
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
@AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -276,12 +304,6 @@ struct ChatListView: View {
|
|||||||
.padding(.top, oneHandUI ? 8 : 0)
|
.padding(.top, oneHandUI ? 8 : 0)
|
||||||
.id("searchBar")
|
.id("searchBar")
|
||||||
}
|
}
|
||||||
if !oneHandUICardShown {
|
|
||||||
OneHandUICard()
|
|
||||||
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
|
||||||
.listRowSeparator(.hidden)
|
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
}
|
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
ForEach(cs, id: \.viewId) { chat in
|
ForEach(cs, id: \.viewId) { chat in
|
||||||
ChatListNavLink(chat: chat)
|
ChatListNavLink(chat: chat)
|
||||||
@@ -307,6 +329,20 @@ struct ChatListView: View {
|
|||||||
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
.disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !oneHandUICardShown {
|
||||||
|
OneHandUICard()
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
}
|
||||||
|
if !addressCreationCardShown {
|
||||||
|
AddressCreationCard()
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.onChange(of: chatModel.chatId) { currentChatId in
|
.onChange(of: chatModel.chatId) { currentChatId in
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ struct ChatPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var verifiedIcon: Text {
|
private var verifiedIcon: Text {
|
||||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.baselineOffset(1)
|
.baselineOffset(1)
|
||||||
.kerning(-2)
|
.kerning(-2)
|
||||||
@@ -232,12 +232,12 @@ struct ChatPreviewView: View {
|
|||||||
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
+ messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary)
|
||||||
|
|
||||||
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text {
|
||||||
Text(Image(systemName: s)).foregroundColor(color) + Text(" ")
|
Text(Image(systemName: s)).foregroundColor(color) + textSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachment() -> Text {
|
func attachment() -> Text {
|
||||||
switch draft.preview {
|
switch draft.preview {
|
||||||
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ")
|
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + textSpace
|
||||||
case .mediaPreviews: return image("photo")
|
case .mediaPreviews: return image("photo")
|
||||||
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
|
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
|
||||||
default: return Text("")
|
default: return Text("")
|
||||||
@@ -367,11 +367,11 @@ struct ChatPreviewView: View {
|
|||||||
case .sndErrorAuth, .sndError:
|
case .sndErrorAuth, .sndError:
|
||||||
return Text(Image(systemName: "multiply"))
|
return Text(Image(systemName: "multiply"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.red) + Text(" ")
|
.foregroundColor(.red) + textSpace
|
||||||
case .sndWarning:
|
case .sndWarning:
|
||||||
return Text(Image(systemName: "exclamationmark.triangle.fill"))
|
return Text(Image(systemName: "exclamationmark.triangle.fill"))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.orange) + Text(" ")
|
.foregroundColor(.orange) + textSpace
|
||||||
default: return Text("")
|
default: return Text("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ struct OneHandUICard: View {
|
|||||||
.background(theme.appColors.sentMessage)
|
.background(theme.appColors.sentMessage)
|
||||||
.cornerRadius(12)
|
.cornerRadius(12)
|
||||||
.frame(height: dynamicSize(userFont).rowHeight)
|
.frame(height: dynamicSize(userFont).rowHeight)
|
||||||
.padding(.vertical, 12)
|
|
||||||
.alert(isPresented: $showOneHandUIAlert) {
|
.alert(isPresented: $showOneHandUIAlert) {
|
||||||
Alert(
|
Alert(
|
||||||
title: Text("Reachable chat toolbar"),
|
title: Text("Reachable chat toolbar"),
|
||||||
|
|||||||
@@ -491,15 +491,6 @@ struct SMPServerSummaryView: View {
|
|||||||
Section("Server address") {
|
Section("Server address") {
|
||||||
Text(summary.smpServer)
|
Text(summary.smpServer)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
if summary.known == true {
|
|
||||||
NavigationLink {
|
|
||||||
ProtocolServersView(serverProtocol: .smp)
|
|
||||||
.navigationTitle("Your SMP servers")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text("Open server settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let stats = summary.stats {
|
if let stats = summary.stats {
|
||||||
@@ -679,15 +670,6 @@ struct XFTPServerSummaryView: View {
|
|||||||
Section("Server address") {
|
Section("Server address") {
|
||||||
Text(summary.xftpServer)
|
Text(summary.xftpServer)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
if summary.known == true {
|
|
||||||
NavigationLink {
|
|
||||||
ProtocolServersView(serverProtocol: .xftp)
|
|
||||||
.navigationTitle("Your XFTP servers")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text("Open server settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let stats = summary.stats {
|
if let stats = summary.stats {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ struct ContactListNavLink: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var verifiedIcon: Text {
|
private var verifiedIcon: Text {
|
||||||
(Text(Image(systemName: "checkmark.shield")) + Text(" "))
|
(Text(Image(systemName: "checkmark.shield")) + textSpace)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
.baselineOffset(1)
|
.baselineOffset(1)
|
||||||
.kerning(-2)
|
.kerning(-2)
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
//
|
|
||||||
// ChatArchiveView.swift
|
|
||||||
// SimpleXChat
|
|
||||||
//
|
|
||||||
// Created by Evgeny on 23/06/2022.
|
|
||||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SimpleXChat
|
|
||||||
|
|
||||||
struct ChatArchiveView: View {
|
|
||||||
@EnvironmentObject var theme: AppTheme
|
|
||||||
var archiveName: String
|
|
||||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
|
||||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
|
||||||
@State private var showDeleteAlert = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName)
|
|
||||||
let fileTs = chatArchiveTimeDefault.get()
|
|
||||||
List {
|
|
||||||
Section {
|
|
||||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
|
||||||
Button {
|
|
||||||
showShareSheet(items: [fileUrl])
|
|
||||||
} label: {
|
|
||||||
Text("Save archive")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsRow("trash", color: theme.colors.secondary) {
|
|
||||||
Button {
|
|
||||||
showDeleteAlert = true
|
|
||||||
} label: {
|
|
||||||
Text("Delete archive").foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Chat archive")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
} footer: {
|
|
||||||
Text("Created on \(fileTs)")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert(isPresented: $showDeleteAlert) {
|
|
||||||
Alert(
|
|
||||||
title: Text("Delete chat archive?"),
|
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
|
||||||
do {
|
|
||||||
try FileManager.default.removeItem(atPath: fileUrl.path)
|
|
||||||
chatArchiveName = nil
|
|
||||||
chatArchiveTime = 0
|
|
||||||
} catch let error {
|
|
||||||
logger.error("removeItem error \(String(describing: error))")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondaryButton: .cancel()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ChatArchiveView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ChatArchiveView(archiveName: "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,6 +48,8 @@ struct DatabaseEncryptionView: View {
|
|||||||
@State private var confirmNewKey = ""
|
@State private var confirmNewKey = ""
|
||||||
@State private var currentKeyShown = false
|
@State private var currentKeyShown = false
|
||||||
|
|
||||||
|
let stopChatRunBlockStartChat: (Binding<Bool>, @escaping () async throws -> Bool) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
List {
|
List {
|
||||||
@@ -134,46 +136,61 @@ struct DatabaseEncryptionView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
||||||
}
|
}
|
||||||
.disabled(m.chatRunning != false)
|
.disabled(progressIndicator)
|
||||||
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
|
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func encryptDatabase() {
|
private func encryptDatabaseAsync() async -> Bool {
|
||||||
progressIndicator = true
|
await MainActor.run {
|
||||||
Task {
|
progressIndicator = true
|
||||||
do {
|
}
|
||||||
encryptionStartedDefault.set(true)
|
do {
|
||||||
encryptionStartedAtDefault.set(Date.now)
|
encryptionStartedDefault.set(true)
|
||||||
if !m.chatDbChanged {
|
encryptionStartedAtDefault.set(Date.now)
|
||||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
if !m.chatDbChanged {
|
||||||
}
|
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
}
|
||||||
encryptionStartedDefault.set(false)
|
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||||
initialRandomDBPassphraseGroupDefault.set(false)
|
encryptionStartedDefault.set(false)
|
||||||
if migration {
|
initialRandomDBPassphraseGroupDefault.set(false)
|
||||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
if migration {
|
||||||
}
|
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||||
if useKeychain {
|
}
|
||||||
if kcDatabasePassword.set(newKey) {
|
if useKeychain {
|
||||||
await resetFormAfterEncryption(true)
|
if kcDatabasePassword.set(newKey) {
|
||||||
await operationEnded(.databaseEncrypted)
|
await resetFormAfterEncryption(true)
|
||||||
} else {
|
|
||||||
await resetFormAfterEncryption()
|
|
||||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if migration {
|
|
||||||
removePassphraseFromKeyChain()
|
|
||||||
}
|
|
||||||
await resetFormAfterEncryption()
|
|
||||||
await operationEnded(.databaseEncrypted)
|
await operationEnded(.databaseEncrypted)
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
|
||||||
await operationEnded(.currentPassphraseError)
|
|
||||||
} else {
|
} else {
|
||||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
await resetFormAfterEncryption()
|
||||||
|
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if migration {
|
||||||
|
removePassphraseFromKeyChain()
|
||||||
|
}
|
||||||
|
await resetFormAfterEncryption()
|
||||||
|
await operationEnded(.databaseEncrypted)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch let error {
|
||||||
|
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||||
|
await operationEnded(.currentPassphraseError)
|
||||||
|
} else {
|
||||||
|
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func encryptDatabase() {
|
||||||
|
// it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped
|
||||||
|
if migration {
|
||||||
|
Task {
|
||||||
|
await encryptDatabaseAsync()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopChatRunBlockStartChat($progressIndicator) {
|
||||||
|
return await encryptDatabaseAsync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,6 +388,6 @@ func validKey(_ s: String) -> Bool {
|
|||||||
|
|
||||||
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false)
|
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false, stopChatRunBlockStartChat: { _, _ in true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import SimpleXChat
|
|||||||
|
|
||||||
struct DatabaseErrorView: View {
|
struct DatabaseErrorView: View {
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
@State var status: DBMigrationResult
|
@State var status: DBMigrationResult
|
||||||
@State private var dbKey = ""
|
@State private var dbKey = ""
|
||||||
@State private var storedDBKey = kcDatabasePassword.get()
|
@State private var storedDBKey = kcDatabasePassword.get()
|
||||||
@@ -28,23 +29,39 @@ struct DatabaseErrorView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func databaseErrorView() -> some View {
|
@ViewBuilder private func databaseErrorView() -> some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .center, spacing: 20) {
|
||||||
switch status {
|
switch status {
|
||||||
case let .errorNotADatabase(dbFile):
|
case let .errorNotADatabase(dbFile):
|
||||||
if useKeychain && storedDBKey != nil && storedDBKey != "" {
|
if useKeychain && storedDBKey != nil && storedDBKey != "" {
|
||||||
titleText("Wrong database passphrase")
|
titleText("Wrong database passphrase")
|
||||||
Text("Database passphrase is different from saved in the keychain.")
|
Text("Database passphrase is different from saved in the keychain.")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
|
||||||
databaseKeyField(onSubmit: saveAndRunChat)
|
databaseKeyField(onSubmit: saveAndRunChat)
|
||||||
saveAndOpenButton()
|
Spacer()
|
||||||
fileNameText(dbFile)
|
VStack(spacing: 10) {
|
||||||
|
saveAndOpenButton()
|
||||||
|
fileNameText(dbFile)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
titleText("Encrypted database")
|
titleText("Encrypted database")
|
||||||
Text("Database passphrase is required to open chat.")
|
Text("Database passphrase is required to open chat.")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.bottom, 5)
|
||||||
|
|
||||||
if useKeychain {
|
if useKeychain {
|
||||||
databaseKeyField(onSubmit: saveAndRunChat)
|
databaseKeyField(onSubmit: saveAndRunChat)
|
||||||
|
Spacer()
|
||||||
saveAndOpenButton()
|
saveAndOpenButton()
|
||||||
} else {
|
} else {
|
||||||
databaseKeyField(onSubmit: { runChat() })
|
databaseKeyField(onSubmit: { runChat() })
|
||||||
|
Spacer()
|
||||||
openChatButton()
|
openChatButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,73 +69,105 @@ struct DatabaseErrorView: View {
|
|||||||
switch migrationError {
|
switch migrationError {
|
||||||
case let .upgrade(upMigrations):
|
case let .upgrade(upMigrations):
|
||||||
titleText("Database upgrade")
|
titleText("Database upgrade")
|
||||||
Button("Upgrade and open chat") { runChat(confirmMigrations: .yesUp) }
|
|
||||||
fileNameText(dbFile)
|
|
||||||
migrationsText(upMigrations.map(\.upName))
|
migrationsText(upMigrations.map(\.upName))
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Button("Upgrade and open chat") {
|
||||||
|
runChat(confirmMigrations: .yesUp)
|
||||||
|
}.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
|
fileNameText(dbFile)
|
||||||
|
}
|
||||||
case let .downgrade(downMigrations):
|
case let .downgrade(downMigrations):
|
||||||
titleText("Database downgrade")
|
titleText("Database downgrade")
|
||||||
Text("Warning: you may lose some data!").bold()
|
Text("Warning: you may lose some data!")
|
||||||
Button("Downgrade and open chat") { runChat(confirmMigrations: .yesUpDown) }
|
.bold()
|
||||||
fileNameText(dbFile)
|
.padding(.horizontal, 25)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
migrationsText(downMigrations)
|
migrationsText(downMigrations)
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
Button("Downgrade and open chat") {
|
||||||
|
runChat(confirmMigrations: .yesUpDown)
|
||||||
|
}.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
|
fileNameText(dbFile)
|
||||||
|
}
|
||||||
case let .migrationError(mtrError):
|
case let .migrationError(mtrError):
|
||||||
titleText("Incompatible database version")
|
titleText("Incompatible database version")
|
||||||
fileNameText(dbFile)
|
fileNameText(dbFile, font: .callout)
|
||||||
Text("Error: ") + Text(mtrErrorDescription(mtrError))
|
errorView(Text(mtrErrorDescription(mtrError)))
|
||||||
}
|
}
|
||||||
case let .errorSQL(dbFile, migrationSQLError):
|
case let .errorSQL(dbFile, migrationSQLError):
|
||||||
titleText("Database error")
|
titleText("Database error")
|
||||||
fileNameText(dbFile)
|
fileNameText(dbFile, font: .callout)
|
||||||
Text("Error: \(migrationSQLError)")
|
errorView(Text("Error: \(migrationSQLError)"))
|
||||||
case .errorKeychain:
|
case .errorKeychain:
|
||||||
titleText("Keychain error")
|
titleText("Keychain error")
|
||||||
Text("Cannot access keychain to save database password")
|
errorView(Text("Cannot access keychain to save database password"))
|
||||||
case .invalidConfirmation:
|
case .invalidConfirmation:
|
||||||
// this can only happen if incorrect parameter is passed
|
// this can only happen if incorrect parameter is passed
|
||||||
Text(String("Invalid migration confirmation")).font(.title)
|
titleText("Invalid migration confirmation")
|
||||||
|
errorView()
|
||||||
|
|
||||||
case let .unknown(json):
|
case let .unknown(json):
|
||||||
titleText("Database error")
|
titleText("Database error")
|
||||||
Text("Unknown database error: \(json)")
|
errorView(Text("Unknown database error: \(json)"))
|
||||||
case .ok:
|
case .ok:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
if showRestoreDbButton {
|
if showRestoreDbButton {
|
||||||
Spacer().frame(height: 10)
|
Spacer()
|
||||||
Text("The attempt to change database passphrase was not completed.")
|
Text("The attempt to change database passphrase was not completed.")
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.font(.footnote)
|
||||||
|
|
||||||
restoreDbButton()
|
restoreDbButton()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.top, 75)
|
||||||
|
.padding(.bottom, 25)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
.onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() }
|
.onAppear() { showRestoreDbButton = shouldShowRestoreDbButton() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private func titleText(_ s: LocalizedStringKey) -> Text {
|
private func titleText(_ s: LocalizedStringKey) -> some View {
|
||||||
Text(s).font(.title)
|
Text(s).font(.largeTitle).bold().multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fileNameText(_ f: String) -> Text {
|
private func fileNameText(_ f: String, font: Font = .caption) -> Text {
|
||||||
Text("File: \((f as NSString).lastPathComponent)")
|
Text("File: \((f as NSString).lastPathComponent)").font(font)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func migrationsText(_ ms: [String]) -> Text {
|
private func migrationsText(_ ms: [String]) -> some View {
|
||||||
Text("Migrations: \(ms.joined(separator: ", "))")
|
(Text("Migrations:").font(.subheadline) + Text(verbatim: "\n") + Text(ms.joined(separator: "\n")).font(.caption))
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
|
private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View {
|
||||||
PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
|
PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit)
|
||||||
|
.padding(.vertical, 10)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||||
|
.fill(Color(uiColor: .tertiarySystemFill))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveAndOpenButton() -> some View {
|
private func saveAndOpenButton() -> some View {
|
||||||
Button("Save passphrase and open chat") {
|
Button("Save passphrase and open chat") {
|
||||||
saveAndRunChat()
|
saveAndRunChat()
|
||||||
}
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openChatButton() -> some View {
|
private func openChatButton() -> some View {
|
||||||
Button("Open chat") {
|
Button("Open chat") {
|
||||||
runChat()
|
runChat()
|
||||||
}
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func saveAndRunChat() {
|
private func saveAndRunChat() {
|
||||||
@@ -192,8 +241,9 @@ struct DatabaseErrorView: View {
|
|||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
))
|
))
|
||||||
} label: {
|
} label: {
|
||||||
Text("Restore database backup").foregroundColor(.red)
|
Text("Restore database backup")
|
||||||
}
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func restoreDb() {
|
private func restoreDb() {
|
||||||
@@ -208,6 +258,23 @@ struct DatabaseErrorView: View {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func errorView(_ s: Text? = nil) -> some View {
|
||||||
|
VStack(spacing: 35) {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 50, height: 50)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
|
||||||
|
if let text = s {
|
||||||
|
text
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DatabaseErrorView_Previews: PreviewProvider {
|
struct DatabaseErrorView_Previews: PreviewProvider {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ struct DatabaseView: View {
|
|||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
let dismissSettingsSheet: DismissAction
|
let dismissSettingsSheet: DismissAction
|
||||||
@State private var runChat = false
|
@State private var runChat = false
|
||||||
|
@State private var stoppingChat = false
|
||||||
@State private var alert: DatabaseAlert? = nil
|
@State private var alert: DatabaseAlert? = nil
|
||||||
@State private var showFileImporter = false
|
@State private var showFileImporter = false
|
||||||
@State private var importedArchivePath: URL?
|
@State private var importedArchivePath: URL?
|
||||||
@@ -57,6 +58,8 @@ struct DatabaseView: View {
|
|||||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||||
@State private var appFilesCountAndSize: (Int, Int)?
|
@State private var appFilesCountAndSize: (Int, Int)?
|
||||||
|
|
||||||
|
@State private var showDatabaseEncryptionView = false
|
||||||
|
|
||||||
@State var chatItemTTL: ChatItemTTL
|
@State var chatItemTTL: ChatItemTTL
|
||||||
@State private var currentChatItemTTL: ChatItemTTL = .none
|
@State private var currentChatItemTTL: ChatItemTTL = .none
|
||||||
|
|
||||||
@@ -69,7 +72,20 @@ struct DatabaseView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
private func chatDatabaseView() -> some View {
|
private func chatDatabaseView() -> some View {
|
||||||
|
NavigationLink(isActive: $showDatabaseEncryptionView) {
|
||||||
|
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
|
||||||
|
stopChatRunBlockStartChat(false, progressIndicator, block)
|
||||||
|
})
|
||||||
|
.navigationTitle("Database passphrase")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
|
|
||||||
List {
|
List {
|
||||||
let stopped = m.chatRunning == false
|
let stopped = m.chatRunning == false
|
||||||
Section {
|
Section {
|
||||||
@@ -101,9 +117,10 @@ struct DatabaseView: View {
|
|||||||
isOn: $runChat
|
isOn: $runChat
|
||||||
)
|
)
|
||||||
.onChange(of: runChat) { _ in
|
.onChange(of: runChat) { _ in
|
||||||
if (runChat) {
|
if runChat {
|
||||||
startChat()
|
DatabaseView.startChat($runChat, $progressIndicator)
|
||||||
} else {
|
} else if !stoppingChat {
|
||||||
|
stoppingChat = false
|
||||||
alert = .stopChat
|
alert = .stopChat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +140,9 @@ struct DatabaseView: View {
|
|||||||
let color: Color = unencrypted ? .orange : theme.colors.secondary
|
let color: Color = unencrypted ? .orange : theme.colors.secondary
|
||||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
|
||||||
|
stopChatRunBlockStartChat(false, progressIndicator, block)
|
||||||
|
})
|
||||||
.navigationTitle("Database passphrase")
|
.navigationTitle("Database passphrase")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
} label: {
|
} label: {
|
||||||
@@ -133,9 +152,14 @@ struct DatabaseView: View {
|
|||||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||||
Button("Export database") {
|
Button("Export database") {
|
||||||
if initialRandomDBPassphraseGroupDefault.get() && !unencrypted {
|
if initialRandomDBPassphraseGroupDefault.get() && !unencrypted {
|
||||||
alert = .exportProhibited
|
showDatabaseEncryptionView = true
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
alert = .exportProhibited
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
exportArchive()
|
stopChatRunBlockStartChat(stopped, $progressIndicator) {
|
||||||
|
await exportArchive()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,20 +168,6 @@ struct DatabaseView: View {
|
|||||||
showFileImporter = true
|
showFileImporter = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let archiveName = chatArchiveName {
|
|
||||||
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
|
|
||||||
? "Old database archive"
|
|
||||||
: "New database archive"
|
|
||||||
settingsRow("archivebox", color: theme.colors.secondary) {
|
|
||||||
NavigationLink {
|
|
||||||
ChatArchiveView(archiveName: archiveName)
|
|
||||||
.navigationTitle(title)
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text(title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsRow("trash.slash", color: theme.colors.secondary) {
|
settingsRow("trash.slash", color: theme.colors.secondary) {
|
||||||
Button("Delete database", role: .destructive) {
|
Button("Delete database", role: .destructive) {
|
||||||
alert = .deleteChat
|
alert = .deleteChat
|
||||||
@@ -167,14 +177,10 @@ struct DatabaseView: View {
|
|||||||
Text("Chat database")
|
Text("Chat database")
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
} footer: {
|
} footer: {
|
||||||
Text(
|
Text("You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.")
|
||||||
stopped
|
|
||||||
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
|
|
||||||
: "Stop chat to enable database actions"
|
|
||||||
)
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
.disabled(!stopped)
|
.disabled(progressIndicator)
|
||||||
|
|
||||||
if case .group = dbContainer, legacyDatabase {
|
if case .group = dbContainer, legacyDatabase {
|
||||||
Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) {
|
Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) {
|
||||||
@@ -190,7 +196,7 @@ struct DatabaseView: View {
|
|||||||
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
|
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
|
||||||
alert = .deleteFilesAndMedia
|
alert = .deleteFilesAndMedia
|
||||||
}
|
}
|
||||||
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
|
.disabled(progressIndicator || appFilesCountAndSize?.0 == 0)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Files & media")
|
Text("Files & media")
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
@@ -255,7 +261,10 @@ struct DatabaseView: View {
|
|||||||
title: Text("Import chat database?"),
|
title: Text("Import chat database?"),
|
||||||
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||||
primaryButton: .destructive(Text("Import")) {
|
primaryButton: .destructive(Text("Import")) {
|
||||||
importArchive(fileURL)
|
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||||
|
_ = await DatabaseView.importArchive(fileURL, $progressIndicator, $alert)
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
@@ -263,19 +272,15 @@ struct DatabaseView: View {
|
|||||||
return Alert(title: Text("Error: no database file"))
|
return Alert(title: Text("Error: no database file"))
|
||||||
}
|
}
|
||||||
case .archiveImported:
|
case .archiveImported:
|
||||||
return Alert(
|
let (title, message) = archiveImportedAlertText()
|
||||||
title: Text("Chat database imported"),
|
return Alert(title: Text(title), message: Text(message))
|
||||||
message: Text("Restart the app to use imported chat database")
|
|
||||||
)
|
|
||||||
case let .archiveImportedWithErrors(errs):
|
case let .archiveImportedWithErrors(errs):
|
||||||
return Alert(
|
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||||
title: Text("Chat database imported"),
|
return Alert(title: Text(title), message: Text(message))
|
||||||
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
|
|
||||||
)
|
|
||||||
case let .archiveExportedWithErrors(archivePath, errs):
|
case let .archiveExportedWithErrors(archivePath, errs):
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Chat database exported"),
|
title: Text("Chat database exported"),
|
||||||
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
|
||||||
dismissButton: .default(Text("Continue")) {
|
dismissButton: .default(Text("Continue")) {
|
||||||
showShareSheet(items: [archivePath])
|
showShareSheet(items: [archivePath])
|
||||||
}
|
}
|
||||||
@@ -285,15 +290,17 @@ struct DatabaseView: View {
|
|||||||
title: Text("Delete chat profile?"),
|
title: Text("Delete chat profile?"),
|
||||||
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
deleteChat()
|
let wasStopped = m.chatRunning == false
|
||||||
|
stopChatRunBlockStartChat(wasStopped, $progressIndicator) {
|
||||||
|
_ = await deleteChat()
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
case .chatDeleted:
|
case .chatDeleted:
|
||||||
return Alert(
|
let (title, message) = chatDeletedAlertText()
|
||||||
title: Text("Chat database deleted"),
|
return Alert(title: Text(title), message: Text(message))
|
||||||
message: Text("Restart the app to create a new chat profile")
|
|
||||||
)
|
|
||||||
case .deleteLegacyDatabase:
|
case .deleteLegacyDatabase:
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Delete old database?"),
|
title: Text("Delete old database?"),
|
||||||
@@ -308,7 +315,10 @@ struct DatabaseView: View {
|
|||||||
title: Text("Delete files and media?"),
|
title: Text("Delete files and media?"),
|
||||||
message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."),
|
message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
deleteFiles()
|
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||||
|
deleteFiles()
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
secondaryButton: .cancel()
|
secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
@@ -328,95 +338,180 @@ struct DatabaseView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func authStopChat() {
|
private func authStopChat(_ onStop: (() -> Void)? = nil) {
|
||||||
if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) {
|
if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) {
|
||||||
authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in
|
authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in
|
||||||
switch laResult {
|
switch laResult {
|
||||||
case .success: stopChat()
|
case .success: stopChat(onStop)
|
||||||
case .unavailable: stopChat()
|
case .unavailable: stopChat(onStop)
|
||||||
case .failed: withAnimation { runChat = true }
|
case .failed: withAnimation { runChat = true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stopChat()
|
stopChat(onStop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopChat() {
|
private func stopChat(_ onStop: (() -> Void)? = nil) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
try await stopChatAsync()
|
try await stopChatAsync()
|
||||||
|
onStop?()
|
||||||
} catch let error {
|
} catch let error {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
runChat = true
|
runChat = true
|
||||||
alert = .error(title: "Error stopping chat", error: responseError(error))
|
showAlert("Error stopping chat", message: responseError(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func exportArchive() {
|
func stopChatRunBlockStartChat(
|
||||||
progressIndicator = true
|
_ stopped: Bool,
|
||||||
Task {
|
_ progressIndicator: Binding<Bool>,
|
||||||
do {
|
_ block: @escaping () async throws -> Bool
|
||||||
let (archivePath, archiveErrors) = try await exportChatArchive()
|
) {
|
||||||
if archiveErrors.isEmpty {
|
// if the chat was running, the sequence is: stop chat, run block, start chat.
|
||||||
showShareSheet(items: [archivePath])
|
// Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not
|
||||||
await MainActor.run { progressIndicator = false }
|
if stopped {
|
||||||
} else {
|
Task {
|
||||||
await MainActor.run {
|
do {
|
||||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
_ = try await block()
|
||||||
progressIndicator = false
|
} catch {
|
||||||
|
logger.error("Error while executing block: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
authStopChat {
|
||||||
|
stoppingChat = true
|
||||||
|
runChat = false
|
||||||
|
Task {
|
||||||
|
// if it throws, let's start chat again anyway
|
||||||
|
var canStart = false
|
||||||
|
do {
|
||||||
|
canStart = try await block()
|
||||||
|
} catch {
|
||||||
|
logger.error("Error executing block: \(error)")
|
||||||
|
canStart = true
|
||||||
|
}
|
||||||
|
if canStart {
|
||||||
|
await MainActor.run {
|
||||||
|
DatabaseView.startChat($runChat, $progressIndicator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func startChat(_ runChat: Binding<Bool>, _ progressIndicator: Binding<Bool>) {
|
||||||
|
progressIndicator.wrappedValue = true
|
||||||
|
let m = ChatModel.shared
|
||||||
|
if m.chatDbChanged {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
resetChatCtrl()
|
||||||
|
do {
|
||||||
|
let hadDatabase = hasDatabase()
|
||||||
|
try initializeChat(start: true)
|
||||||
|
m.chatDbChanged = false
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
|
if m.chatDbStatus != .ok || !hadDatabase {
|
||||||
|
// Hide current view and show `DatabaseErrorView`
|
||||||
|
dismissAllSheets(animated: true)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
fatalError("Error starting chat \(responseError(error))")
|
||||||
|
}
|
||||||
|
progressIndicator.wrappedValue = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
_ = try apiStartChat()
|
||||||
|
runChat.wrappedValue = true
|
||||||
|
m.chatRunning = true
|
||||||
|
ChatReceiver.shared.start()
|
||||||
|
chatLastStartGroupDefault.set(Date.now)
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
} catch let error {
|
} catch let error {
|
||||||
|
runChat.wrappedValue = false
|
||||||
|
showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error))
|
||||||
|
}
|
||||||
|
progressIndicator.wrappedValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func exportArchive() async -> Bool {
|
||||||
|
await MainActor.run {
|
||||||
|
progressIndicator = true
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let (archivePath, archiveErrors) = try await exportChatArchive()
|
||||||
|
if archiveErrors.isEmpty {
|
||||||
|
showShareSheet(items: [archivePath])
|
||||||
|
await MainActor.run { progressIndicator = false }
|
||||||
|
} else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
||||||
progressIndicator = false
|
progressIndicator = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||||
|
progressIndicator = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func importArchive(_ archivePath: URL) {
|
static func importArchive(
|
||||||
|
_ archivePath: URL,
|
||||||
|
_ progressIndicator: Binding<Bool>,
|
||||||
|
_ alert: Binding<DatabaseAlert?>
|
||||||
|
) async -> Bool {
|
||||||
if archivePath.startAccessingSecurityScopedResource() {
|
if archivePath.startAccessingSecurityScopedResource() {
|
||||||
progressIndicator = true
|
await MainActor.run {
|
||||||
Task {
|
progressIndicator.wrappedValue = true
|
||||||
do {
|
|
||||||
try await apiDeleteStorage()
|
|
||||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
|
||||||
do {
|
|
||||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
|
||||||
let archiveErrors = try await apiImportArchive(config: config)
|
|
||||||
_ = kcDatabasePassword.remove()
|
|
||||||
if archiveErrors.isEmpty {
|
|
||||||
await operationEnded(.archiveImported)
|
|
||||||
} else {
|
|
||||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors))
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)))
|
|
||||||
}
|
|
||||||
archivePath.stopAccessingSecurityScopedResource()
|
|
||||||
}
|
}
|
||||||
|
do {
|
||||||
|
try await apiDeleteStorage()
|
||||||
|
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||||
|
do {
|
||||||
|
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||||
|
let archiveErrors = try await apiImportArchive(config: config)
|
||||||
|
shouldImportAppSettingsDefault.set(true)
|
||||||
|
_ = kcDatabasePassword.remove()
|
||||||
|
if archiveErrors.isEmpty {
|
||||||
|
await operationEnded(.archiveImported, progressIndicator, alert)
|
||||||
|
} else {
|
||||||
|
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch let error {
|
||||||
|
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert)
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert)
|
||||||
|
}
|
||||||
|
archivePath.stopAccessingSecurityScopedResource()
|
||||||
} else {
|
} else {
|
||||||
alert = .error(title: "Error accessing database file")
|
showAlert("Error accessing database file")
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func deleteChat() {
|
private func deleteChat() async -> Bool {
|
||||||
progressIndicator = true
|
await MainActor.run {
|
||||||
Task {
|
progressIndicator = true
|
||||||
do {
|
}
|
||||||
try await deleteChatAsync()
|
do {
|
||||||
await operationEnded(.chatDeleted)
|
try await deleteChatAsync()
|
||||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||||
} catch let error {
|
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
|
||||||
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
|
return true
|
||||||
}
|
} catch let error {
|
||||||
|
await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,39 +523,28 @@ struct DatabaseView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func operationEnded(_ dbAlert: DatabaseAlert) async {
|
private static func operationEnded(_ dbAlert: DatabaseAlert, _ progressIndicator: Binding<Bool>, _ alert: Binding<DatabaseAlert?>) async {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
|
let m = ChatModel.shared
|
||||||
m.chatDbChanged = true
|
m.chatDbChanged = true
|
||||||
m.chatInitialized = false
|
m.chatInitialized = false
|
||||||
progressIndicator = false
|
progressIndicator.wrappedValue = false
|
||||||
alert = dbAlert
|
|
||||||
}
|
}
|
||||||
}
|
await withCheckedContinuation { cont in
|
||||||
|
let okAlertActionWaiting = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default, handler: { _ in cont.resume() })
|
||||||
private func startChat() {
|
// show these alerts globally so they are visible when all sheets will be hidden
|
||||||
if m.chatDbChanged {
|
if case .archiveImported = dbAlert {
|
||||||
dismissSettingsSheet()
|
let (title, message) = archiveImportedAlertText()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||||
resetChatCtrl()
|
} else if case .archiveImportedWithErrors(let errs) = dbAlert {
|
||||||
do {
|
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||||
try initializeChat(start: true)
|
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||||
m.chatDbChanged = false
|
} else if case .chatDeleted = dbAlert {
|
||||||
AppChatState.shared.set(.active)
|
let (title, message) = chatDeletedAlertText()
|
||||||
} catch let error {
|
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||||
fatalError("Error starting chat \(responseError(error))")
|
} else {
|
||||||
}
|
alert.wrappedValue = dbAlert
|
||||||
}
|
cont.resume()
|
||||||
} else {
|
|
||||||
do {
|
|
||||||
_ = try apiStartChat()
|
|
||||||
runChat = true
|
|
||||||
m.chatRunning = true
|
|
||||||
ChatReceiver.shared.start()
|
|
||||||
chatLastStartGroupDefault.set(Date.now)
|
|
||||||
AppChatState.shared.set(.active)
|
|
||||||
} catch let error {
|
|
||||||
runChat = false
|
|
||||||
alert = .error(title: "Error starting chat", error: responseError(error))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -503,8 +587,28 @@ struct DatabaseView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func archiveErrorsText(_ errs: [ArchiveError]) -> Text {
|
private func archiveImportedAlertText() -> (String, String) {
|
||||||
return Text("\n" + errs.map(showArchiveError).joined(separator: "\n"))
|
(
|
||||||
|
NSLocalizedString("Chat database imported", comment: ""),
|
||||||
|
NSLocalizedString("Restart the app to use imported chat database", comment: "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
private func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) {
|
||||||
|
(
|
||||||
|
NSLocalizedString("Chat database imported", comment: ""),
|
||||||
|
NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func chatDeletedAlertText() -> (String, String) {
|
||||||
|
(
|
||||||
|
NSLocalizedString("Chat database deleted", comment: ""),
|
||||||
|
NSLocalizedString("Restart the app to create a new chat profile", comment: "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func archiveErrorsText(_ errs: [ArchiveError]) -> String {
|
||||||
|
return "\n" + errs.map(showArchiveError).joined(separator: "\n")
|
||||||
|
|
||||||
func showArchiveError(_ err: ArchiveError) -> String {
|
func showArchiveError(_ err: ArchiveError) -> String {
|
||||||
switch err {
|
switch err {
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ struct MigrateToAppGroupView: View {
|
|||||||
setV3DBMigration(.migration_error)
|
setV3DBMigration(.migration_error)
|
||||||
migrationError = "Error starting chat: \(responseError(error))"
|
migrationError = "Error starting chat: \(responseError(error))"
|
||||||
}
|
}
|
||||||
deleteOldArchive()
|
deleteOldChatArchive()
|
||||||
} label: {
|
} label: {
|
||||||
Text("Start chat")
|
Text("Start chat")
|
||||||
.font(.title)
|
.font(.title)
|
||||||
@@ -235,14 +235,16 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [Archive
|
|||||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||||
let errs = try await apiExportArchive(config: config)
|
let errs = try await apiExportArchive(config: config)
|
||||||
if storagePath == nil {
|
if storagePath == nil {
|
||||||
deleteOldArchive()
|
deleteOldChatArchive()
|
||||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||||
chatArchiveTimeDefault.set(archiveTime)
|
chatArchiveTimeDefault.set(archiveTime)
|
||||||
}
|
}
|
||||||
return (archivePath, errs)
|
return (archivePath, errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteOldArchive() {
|
/// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then.
|
||||||
|
/// Remove DEFAULT_CHAT_ARCHIVE_NAME and DEFAULT_CHAT_ARCHIVE_TIME as well
|
||||||
|
func deleteOldChatArchive() {
|
||||||
let d = UserDefaults.standard
|
let d = UserDefaults.standard
|
||||||
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
|
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ struct MigrateFromDevice: View {
|
|||||||
case let .archiveExportedWithErrors(archivePath, errs):
|
case let .archiveExportedWithErrors(archivePath, errs):
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Chat database exported"),
|
title: Text("Chat database exported"),
|
||||||
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
|
||||||
dismissButton: .default(Text("Continue")) {
|
dismissButton: .default(Text("Continue")) {
|
||||||
Task { await uploadArchive(path: archivePath) }
|
Task { await uploadArchive(path: archivePath) }
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,8 @@ struct MigrateFromDevice: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func passphraseNotSetView() -> some View {
|
private func passphraseNotSetView() -> some View {
|
||||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true)
|
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in
|
||||||
|
})
|
||||||
.onChange(of: initialRandomDBPassphrase) { initial in
|
.onChange(of: initialRandomDBPassphrase) { initial in
|
||||||
if !initial {
|
if !initial {
|
||||||
migrationState = .uploadConfirmation
|
migrationState = .uploadConfirmation
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ struct MigrateToDevice: View {
|
|||||||
@State private var showQRCodeScanner: Bool = true
|
@State private var showQRCodeScanner: Bool = true
|
||||||
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||||
|
|
||||||
|
@State private var importingArchiveFromFileProgressIndicator = false
|
||||||
|
@State private var showFileImporter = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
switch migrationState {
|
switch migrationState {
|
||||||
@@ -200,6 +203,12 @@ struct MigrateToDevice: View {
|
|||||||
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
||||||
pasteLinkView()
|
pasteLinkView()
|
||||||
}
|
}
|
||||||
|
Section(header: Text("Or import archive file").foregroundColor(theme.colors.secondary)) {
|
||||||
|
archiveImportFromFileView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if importingArchiveFromFileProgressIndicator {
|
||||||
|
progressView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,6 +229,34 @@ struct MigrateToDevice: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func archiveImportFromFileView() -> some View {
|
||||||
|
Button {
|
||||||
|
showFileImporter = true
|
||||||
|
} label: {
|
||||||
|
Label("Import database", systemImage: "square.and.arrow.down")
|
||||||
|
}
|
||||||
|
.disabled(importingArchiveFromFileProgressIndicator)
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $showFileImporter,
|
||||||
|
allowedContentTypes: [.zip],
|
||||||
|
allowsMultipleSelection: false
|
||||||
|
) { result in
|
||||||
|
if case let .success(files) = result, let fileURL = files.first {
|
||||||
|
Task {
|
||||||
|
let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, Binding.constant(nil))
|
||||||
|
if success {
|
||||||
|
DatabaseView.startChat(
|
||||||
|
Binding.constant(false),
|
||||||
|
$importingArchiveFromFileProgressIndicator
|
||||||
|
)
|
||||||
|
hideView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func linkDownloadingView(_ link: String) -> some View {
|
private func linkDownloadingView(_ link: String) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
List {
|
List {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ struct NewChatMenuButton: View {
|
|||||||
// @EnvironmentObject var chatModel: ChatModel
|
// @EnvironmentObject var chatModel: ChatModel
|
||||||
@State private var showNewChatSheet = false
|
@State private var showNewChatSheet = false
|
||||||
@State private var alert: SomeAlert? = nil
|
@State private var alert: SomeAlert? = nil
|
||||||
@State private var pendingConnection: PendingContactConnection? = nil
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button {
|
Button {
|
||||||
@@ -30,12 +29,8 @@ struct NewChatMenuButton: View {
|
|||||||
.frame(width: 24, height: 24)
|
.frame(width: 24, height: 24)
|
||||||
}
|
}
|
||||||
.appSheet(isPresented: $showNewChatSheet) {
|
.appSheet(isPresented: $showNewChatSheet) {
|
||||||
NewChatSheet(pendingConnection: $pendingConnection)
|
NewChatSheet()
|
||||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||||
.onDisappear {
|
|
||||||
alert = cleanupPendingConnection(contactConnection: pendingConnection)
|
|
||||||
pendingConnection = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.alert(item: $alert) { a in
|
.alert(item: $alert) { a in
|
||||||
return a.alert
|
return a.alert
|
||||||
@@ -55,7 +50,6 @@ struct NewChatSheet: View {
|
|||||||
@State private var searchShowingSimplexLink = false
|
@State private var searchShowingSimplexLink = false
|
||||||
@State private var searchChatFilteredBySimplexLink: String? = nil
|
@State private var searchChatFilteredBySimplexLink: String? = nil
|
||||||
@State private var alert: SomeAlert?
|
@State private var alert: SomeAlert?
|
||||||
@Binding var pendingConnection: PendingContactConnection?
|
|
||||||
|
|
||||||
// Sheet height management
|
// Sheet height management
|
||||||
@State private var isAddContactActive = false
|
@State private var isAddContactActive = false
|
||||||
@@ -110,17 +104,17 @@ struct NewChatSheet: View {
|
|||||||
if (searchText.isEmpty) {
|
if (searchText.isEmpty) {
|
||||||
Section {
|
Section {
|
||||||
NavigationLink(isActive: $isAddContactActive) {
|
NavigationLink(isActive: $isAddContactActive) {
|
||||||
NewChatView(selection: .invite, parentAlert: $alert, contactConnection: $pendingConnection)
|
NewChatView(selection: .invite)
|
||||||
.navigationTitle("New chat")
|
.navigationTitle("New chat")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
} label: {
|
} label: {
|
||||||
navigateOnTap(Label("Add contact", systemImage: "link.badge.plus")) {
|
navigateOnTap(Label("Create 1-time link", systemImage: "link.badge.plus")) {
|
||||||
isAddContactActive = true
|
isAddContactActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink(isActive: $isScanPasteLinkActive) {
|
NavigationLink(isActive: $isScanPasteLinkActive) {
|
||||||
NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert, contactConnection: $pendingConnection)
|
NewChatView(selection: .connect, showQRCodeScanner: true)
|
||||||
.navigationTitle("New chat")
|
.navigationTitle("New chat")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
|||||||
@@ -45,32 +45,33 @@ enum NewChatOption: Identifiable {
|
|||||||
var id: Self { self }
|
var id: Self { self }
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupPendingConnection(contactConnection: PendingContactConnection?) -> SomeAlert? {
|
func showKeepInvitationAlert() {
|
||||||
var alert: SomeAlert? = nil
|
if let showingInvitation = ChatModel.shared.showingInvitation,
|
||||||
|
!showingInvitation.connChatUsed {
|
||||||
if !(ChatModel.shared.showingInvitation?.connChatUsed ?? true),
|
showAlert(
|
||||||
let conn = contactConnection {
|
NSLocalizedString("Keep unused invitation?", comment: "alert title"),
|
||||||
alert = SomeAlert(
|
message: NSLocalizedString("You can view invitation link again in connection details.", comment: "alert message"),
|
||||||
alert: Alert(
|
actions: {[
|
||||||
title: Text("Keep unused invitation?"),
|
UIAlertAction(
|
||||||
message: Text("You can view invitation link again in connection details."),
|
title: NSLocalizedString("Keep", comment: "alert action"),
|
||||||
primaryButton: .default(Text("Keep")) {},
|
style: .default
|
||||||
secondaryButton: .destructive(Text("Delete")) {
|
),
|
||||||
Task {
|
UIAlertAction(
|
||||||
await deleteChat(Chat(
|
title: NSLocalizedString("Delete", comment: "alert action"),
|
||||||
chatInfo: .contactConnection(contactConnection: conn),
|
style: .destructive,
|
||||||
chatItems: []
|
handler: { _ in
|
||||||
))
|
Task {
|
||||||
|
await deleteChat(Chat(
|
||||||
|
chatInfo: .contactConnection(contactConnection: showingInvitation.pcc),
|
||||||
|
chatItems: []
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
),
|
]}
|
||||||
id: "keepUnusedInvitation"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel.shared.showingInvitation = nil
|
ChatModel.shared.showingInvitation = nil
|
||||||
|
|
||||||
return alert
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NewChatView: View {
|
struct NewChatView: View {
|
||||||
@@ -84,13 +85,12 @@ struct NewChatView: View {
|
|||||||
@State var choosingProfile = false
|
@State var choosingProfile = false
|
||||||
@State private var pastedLink: String = ""
|
@State private var pastedLink: String = ""
|
||||||
@State private var alert: NewChatViewAlert?
|
@State private var alert: NewChatViewAlert?
|
||||||
@Binding var parentAlert: SomeAlert?
|
@State private var contactConnection: PendingContactConnection? = nil
|
||||||
@Binding var contactConnection: PendingContactConnection?
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Picker("New chat", selection: $selection) {
|
Picker("New chat", selection: $selection) {
|
||||||
Label("Add contact", systemImage: "link")
|
Label("1-time link", systemImage: "link")
|
||||||
.tag(NewChatOption.invite)
|
.tag(NewChatOption.invite)
|
||||||
Label("Connect via link", systemImage: "qrcode")
|
Label("Connect via link", systemImage: "qrcode")
|
||||||
.tag(NewChatOption.connect)
|
.tag(NewChatOption.connect)
|
||||||
@@ -157,7 +157,7 @@ struct NewChatView: View {
|
|||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
if !choosingProfile {
|
if !choosingProfile {
|
||||||
parentAlert = cleanupPendingConnection(contactConnection: contactConnection)
|
showKeepInvitationAlert()
|
||||||
contactConnection = nil
|
contactConnection = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +197,7 @@ struct NewChatView: View {
|
|||||||
if let (connReq, pcc) = r {
|
if let (connReq, pcc) = r {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
m.updateContactConnection(pcc)
|
m.updateContactConnection(pcc)
|
||||||
m.showingInvitation = ShowingInvitation(connId: pcc.id, connChatUsed: false)
|
m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false)
|
||||||
connReqInvitation = connReq
|
connReqInvitation = connReq
|
||||||
contactConnection = pcc
|
contactConnection = pcc
|
||||||
}
|
}
|
||||||
@@ -916,11 +916,17 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (
|
|||||||
)
|
)
|
||||||
case let .groupLinkConnecting(_, groupInfo):
|
case let .groupLinkConnecting(_, groupInfo):
|
||||||
if let groupInfo = groupInfo {
|
if let groupInfo = groupInfo {
|
||||||
return Alert(
|
return groupInfo.businessChat == nil
|
||||||
|
? Alert(
|
||||||
title: Text("Group already exists!"),
|
title: Text("Group already exists!"),
|
||||||
message: Text("You are already joining the group \(groupInfo.displayName)."),
|
message: Text("You are already joining the group \(groupInfo.displayName)."),
|
||||||
dismissButton: .default(Text("OK")) { cleanup?() }
|
dismissButton: .default(Text("OK")) { cleanup?() }
|
||||||
)
|
)
|
||||||
|
: Alert(
|
||||||
|
title: Text("Chat already exists!"),
|
||||||
|
message: Text("You are already connecting to \(groupInfo.displayName)."),
|
||||||
|
dismissButton: .default(Text("OK")) { cleanup?() }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Already joining the group!"),
|
title: Text("Already joining the group!"),
|
||||||
@@ -1237,10 +1243,15 @@ func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
|
func groupAlreadyExistsAlert(_ groupInfo: GroupInfo) -> Alert {
|
||||||
mkAlert(
|
groupInfo.businessChat == nil
|
||||||
|
? mkAlert(
|
||||||
title: "Group already exists",
|
title: "Group already exists",
|
||||||
message: "You are already in group \(groupInfo.displayName)."
|
message: "You are already in group \(groupInfo.displayName)."
|
||||||
)
|
)
|
||||||
|
: mkAlert(
|
||||||
|
title: "Chat already exists",
|
||||||
|
message: "You are already connected with \(groupInfo.displayName)."
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ConnReqType: Equatable {
|
enum ConnReqType: Equatable {
|
||||||
@@ -1278,9 +1289,7 @@ struct NewChatView_Previews: PreviewProvider {
|
|||||||
@State var contactConnection: PendingContactConnection? = nil
|
@State var contactConnection: PendingContactConnection? = nil
|
||||||
|
|
||||||
NewChatView(
|
NewChatView(
|
||||||
selection: .invite,
|
selection: .invite
|
||||||
parentAlert: $parentAlert,
|
|
||||||
contactConnection: $contactConnection
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
//
|
||||||
|
// AddressCreationCard.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Diogo Cunha on 13/11/2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
struct AddressCreationCard: View {
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@EnvironmentObject private var chatModel: ChatModel
|
||||||
|
@Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize
|
||||||
|
@AppStorage(DEFAULT_ADDRESS_CREATION_CARD_SHOWN) private var addressCreationCardShown = false
|
||||||
|
@State private var showAddressCreationAlert = false
|
||||||
|
@State private var showAddressSheet = false
|
||||||
|
@State private var showAddressInfoSheet = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let addressExists = chatModel.userAddress != nil
|
||||||
|
let chats = chatModel.chats.filter { chat in
|
||||||
|
!chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card
|
||||||
|
}
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
HStack(alignment: .top, spacing: 16) {
|
||||||
|
let envelopeSize = dynamicSize(userFont).profileImageSize
|
||||||
|
Image(systemName: "envelope.circle.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: envelopeSize, height: envelopeSize)
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Your SimpleX address")
|
||||||
|
.font(.title3)
|
||||||
|
Spacer()
|
||||||
|
Text("How to use it") + textSpace + Text(Image(systemName: "info.circle")).foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Image(systemName: "multiply")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.onTapGesture {
|
||||||
|
showAddressCreationAlert = true
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Text("Create")
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
.onTapGesture {
|
||||||
|
showAddressSheet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
showAddressInfoSheet = true
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(theme.appColors.sentMessage)
|
||||||
|
.cornerRadius(12)
|
||||||
|
.frame(height: dynamicSize(userFont).rowHeight)
|
||||||
|
.alert(isPresented: $showAddressCreationAlert) {
|
||||||
|
Alert(
|
||||||
|
title: Text("SimpleX address"),
|
||||||
|
message: Text("Tap Create SimpleX address in the menu to create it later."),
|
||||||
|
dismissButton: .default(Text("Ok")) {
|
||||||
|
withAnimation {
|
||||||
|
addressCreationCardShown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showAddressSheet) {
|
||||||
|
NavigationView {
|
||||||
|
UserAddressView(autoCreate: true)
|
||||||
|
.navigationTitle("SimpleX address")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showAddressInfoSheet) {
|
||||||
|
NavigationView {
|
||||||
|
UserAddressLearnMore(showCreateAddressButton: true)
|
||||||
|
.navigationTitle("Address or 1-time link?")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: addressExists) { exists in
|
||||||
|
if exists, !addressCreationCardShown {
|
||||||
|
addressCreationCardShown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: chats.count) { size in
|
||||||
|
if size >= 3, !addressCreationCardShown {
|
||||||
|
addressCreationCardShown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if addressExists, !addressCreationCardShown {
|
||||||
|
addressCreationCardShown = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
AddressCreationCard()
|
||||||
|
}
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
//
|
||||||
|
// ChooseServerOperators.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 31.10.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
let conditionsURL = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md")!
|
||||||
|
|
||||||
|
struct OnboardingButtonStyle: ButtonStyle {
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
var isDisabled: Bool = false
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
configuration.label
|
||||||
|
.font(.system(size: 17, weight: .semibold))
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(
|
||||||
|
isDisabled
|
||||||
|
? (
|
||||||
|
theme.colors.isLight
|
||||||
|
? .gray.opacity(0.17)
|
||||||
|
: .gray.opacity(0.27)
|
||||||
|
)
|
||||||
|
: theme.colors.primary
|
||||||
|
)
|
||||||
|
.foregroundColor(
|
||||||
|
isDisabled
|
||||||
|
? (
|
||||||
|
theme.colors.isLight
|
||||||
|
? .gray.opacity(0.4)
|
||||||
|
: .white.opacity(0.2)
|
||||||
|
)
|
||||||
|
: .white
|
||||||
|
)
|
||||||
|
.cornerRadius(16)
|
||||||
|
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ChooseServerOperatorsSheet: Identifiable {
|
||||||
|
case showInfo
|
||||||
|
case showConditions
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .showInfo: return "showInfo"
|
||||||
|
case .showConditions: return "showConditions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChooseServerOperators: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
var onboarding: Bool
|
||||||
|
@State private var serverOperators: [ServerOperator] = []
|
||||||
|
@State private var selectedOperatorIds = Set<Int64>()
|
||||||
|
@State private var reviewConditionsNavLinkActive = false
|
||||||
|
@State private var sheetItem: ChooseServerOperatorsSheet? = nil
|
||||||
|
@State private var notificationsModeNavLinkActive = false
|
||||||
|
@State private var justOpened = true
|
||||||
|
|
||||||
|
var selectedOperators: [ServerOperator] { serverOperators.filter { selectedOperatorIds.contains($0.operatorId) } }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { g in
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
let title = Text("Server operators")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.bold()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
|
if onboarding {
|
||||||
|
title.padding(.top, 50)
|
||||||
|
} else {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
|
infoText()
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
ForEach(serverOperators) { srvOperator in
|
||||||
|
operatorCheckView(srvOperator)
|
||||||
|
}
|
||||||
|
Text("You can configure servers via settings.")
|
||||||
|
.font(.footnote)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
let reviewForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
|
||||||
|
let canReviewLater = reviewForOperators.allSatisfy { $0.conditionsAcceptance.usageAllowed }
|
||||||
|
let currEnabledOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId })
|
||||||
|
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
if !reviewForOperators.isEmpty {
|
||||||
|
reviewConditionsButton()
|
||||||
|
} else if selectedOperatorIds != currEnabledOperatorIds && !selectedOperatorIds.isEmpty {
|
||||||
|
setOperatorsButton()
|
||||||
|
} else {
|
||||||
|
continueButton()
|
||||||
|
}
|
||||||
|
if onboarding {
|
||||||
|
Group {
|
||||||
|
if reviewForOperators.isEmpty {
|
||||||
|
Button("Conditions of use") {
|
||||||
|
sheetItem = .showConditions
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Conditions of use")
|
||||||
|
.foregroundColor(.clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.font(.system(size: 17, weight: .semibold))
|
||||||
|
.frame(minHeight: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !onboarding && !reviewForOperators.isEmpty {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
reviewLaterButton()
|
||||||
|
(
|
||||||
|
Text("Conditions will be accepted for enabled operators after 30 days.")
|
||||||
|
+ textSpace
|
||||||
|
+ Text("You can configure operators in Network & servers settings.")
|
||||||
|
)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.font(.footnote)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.disabled(!canReviewLater)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minHeight: g.size.height)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if justOpened {
|
||||||
|
serverOperators = ChatModel.shared.conditions.serverOperators
|
||||||
|
selectedOperatorIds = Set(serverOperators.filter { $0.enabled }.map { $0.operatorId })
|
||||||
|
justOpened = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(item: $sheetItem) { item in
|
||||||
|
switch item {
|
||||||
|
case .showInfo:
|
||||||
|
ChooseServerOperatorsInfoView()
|
||||||
|
case .showConditions:
|
||||||
|
UsageConditionsView(
|
||||||
|
currUserServers: Binding.constant([]),
|
||||||
|
userServers: Binding.constant([])
|
||||||
|
)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
.padding(onboarding ? 25 : 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func infoText() -> some View {
|
||||||
|
Button {
|
||||||
|
sheetItem = .showInfo
|
||||||
|
} label: {
|
||||||
|
Label("How it helps privacy", systemImage: "info.circle")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func operatorCheckView(_ serverOperator: ServerOperator) -> some View {
|
||||||
|
let checked = selectedOperatorIds.contains(serverOperator.operatorId)
|
||||||
|
let icon = checked ? "checkmark.circle.fill" : "circle"
|
||||||
|
let iconColor = checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme)
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Image(serverOperator.largeLogo(colorScheme))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(height: 48)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: icon)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 26, height: 26)
|
||||||
|
.foregroundColor(iconColor)
|
||||||
|
}
|
||||||
|
.background(theme.colors.background)
|
||||||
|
.padding()
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 18))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 18)
|
||||||
|
.stroke(Color(uiColor: .secondarySystemFill), lineWidth: 2)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 2)
|
||||||
|
.onTapGesture {
|
||||||
|
if checked {
|
||||||
|
selectedOperatorIds.remove(serverOperator.operatorId)
|
||||||
|
} else {
|
||||||
|
selectedOperatorIds.insert(serverOperator.operatorId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reviewConditionsButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Button {
|
||||||
|
reviewConditionsNavLinkActive = true
|
||||||
|
} label: {
|
||||||
|
Text("Review conditions")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||||
|
.disabled(selectedOperatorIds.isEmpty)
|
||||||
|
|
||||||
|
NavigationLink(isActive: $reviewConditionsNavLinkActive) {
|
||||||
|
reviewConditionsDestinationView()
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setOperatorsButton() -> some View {
|
||||||
|
notificationsModeNavLinkButton {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
if let enabledOperators = enabledOperators(serverOperators) {
|
||||||
|
let r = try await setServerOperators(operators: enabledOperators)
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.conditions = r
|
||||||
|
continueToNextStep()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await MainActor.run {
|
||||||
|
continueToNextStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Update")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||||
|
.disabled(selectedOperatorIds.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func continueButton() -> some View {
|
||||||
|
notificationsModeNavLinkButton {
|
||||||
|
Button {
|
||||||
|
continueToNextStep()
|
||||||
|
} label: {
|
||||||
|
Text("Continue")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: selectedOperatorIds.isEmpty))
|
||||||
|
.disabled(selectedOperatorIds.isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reviewLaterButton() -> some View {
|
||||||
|
notificationsModeNavLinkButton {
|
||||||
|
Button {
|
||||||
|
continueToNextStep()
|
||||||
|
} label: {
|
||||||
|
Text("Review later")
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func continueToNextStep() {
|
||||||
|
if onboarding {
|
||||||
|
onboardingStageDefault.set(.step4_SetNotificationsMode)
|
||||||
|
notificationsModeNavLinkActive = true
|
||||||
|
} else {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func notificationsModeNavLinkButton(_ button: @escaping (() -> some View)) -> some View {
|
||||||
|
ZStack {
|
||||||
|
button()
|
||||||
|
|
||||||
|
NavigationLink(isActive: $notificationsModeNavLinkActive) {
|
||||||
|
notificationsModeDestinationView()
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func notificationsModeDestinationView() -> some View {
|
||||||
|
SetNotificationsMode()
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reviewConditionsDestinationView() -> some View {
|
||||||
|
reviewConditionsView()
|
||||||
|
.navigationTitle("Conditions of use")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func reviewConditionsView() -> some View {
|
||||||
|
let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted }
|
||||||
|
let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
if !operatorsWithConditionsAccepted.isEmpty {
|
||||||
|
Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
Text("Same conditions will apply to operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
} else {
|
||||||
|
Text("Conditions will be accepted for operator(s): **\(acceptForOperators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
}
|
||||||
|
ConditionsTextView()
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
acceptConditionsButton()
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func acceptConditionsButton() -> some View {
|
||||||
|
notificationsModeNavLinkButton {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
|
||||||
|
let acceptForOperators = selectedOperators.filter { !$0.conditionsAcceptance.conditionsAccepted }
|
||||||
|
let operatorIds = acceptForOperators.map { $0.operatorId }
|
||||||
|
let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.conditions = r
|
||||||
|
}
|
||||||
|
if let enabledOperators = enabledOperators(r.serverOperators) {
|
||||||
|
let r2 = try await setServerOperators(operators: enabledOperators)
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.conditions = r2
|
||||||
|
continueToNextStep()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await MainActor.run {
|
||||||
|
continueToNextStep()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error accepting conditions", comment: "alert title"),
|
||||||
|
message: responseError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Accept conditions")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func enabledOperators(_ operators: [ServerOperator]) -> [ServerOperator]? {
|
||||||
|
var ops = operators
|
||||||
|
if !ops.isEmpty {
|
||||||
|
for i in 0..<ops.count {
|
||||||
|
var op = ops[i]
|
||||||
|
op.enabled = selectedOperatorIds.contains(op.operatorId)
|
||||||
|
ops[i] = op
|
||||||
|
}
|
||||||
|
let haveSMPStorage = ops.contains(where: { $0.enabled && $0.smpRoles.storage })
|
||||||
|
let haveSMPProxy = ops.contains(where: { $0.enabled && $0.smpRoles.proxy })
|
||||||
|
let haveXFTPStorage = ops.contains(where: { $0.enabled && $0.xftpRoles.storage })
|
||||||
|
let haveXFTPProxy = ops.contains(where: { $0.enabled && $0.xftpRoles.proxy })
|
||||||
|
if haveSMPStorage && haveSMPProxy && haveXFTPStorage && haveXFTPProxy {
|
||||||
|
return ops
|
||||||
|
} else if let firstEnabledIndex = ops.firstIndex(where: { $0.enabled }) {
|
||||||
|
var op = ops[firstEnabledIndex]
|
||||||
|
if !haveSMPStorage { op.smpRoles.storage = true }
|
||||||
|
if !haveSMPProxy { op.smpRoles.proxy = true }
|
||||||
|
if !haveXFTPStorage { op.xftpRoles.storage = true }
|
||||||
|
if !haveXFTPProxy { op.xftpRoles.proxy = true }
|
||||||
|
ops[firstEnabledIndex] = op
|
||||||
|
return ops
|
||||||
|
} else { // Shouldn't happen - view doesn't let to proceed if no operators are enabled
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let operatorsPostLink = URL(string: "https://simplex.chat/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.html")!
|
||||||
|
|
||||||
|
struct ChooseServerOperatorsInfoView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Server operators")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.bold()
|
||||||
|
.padding(.vertical)
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Group {
|
||||||
|
Text("The app protects your privacy by using different operators in each conversation.")
|
||||||
|
Text("When more than one operator is enabled, none of them has metadata to learn who communicates with whom.")
|
||||||
|
Text("For example, if your contact receives messages via a SimpleX Chat server, your app will deliver them via a Flux server.")
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ChooseServerOperators(onboarding: true)
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ struct CreateProfile: View {
|
|||||||
TextField("Enter your name…", text: $displayName)
|
TextField("Enter your name…", text: $displayName)
|
||||||
.focused($focusDisplayName)
|
.focused($focusDisplayName)
|
||||||
Button {
|
Button {
|
||||||
createProfile(displayName, showAlert: { alert = $0 }, dismiss: dismiss)
|
createProfile()
|
||||||
} label: {
|
} label: {
|
||||||
Label("Create profile", systemImage: "checkmark")
|
Label("Create profile", systemImage: "checkmark")
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,35 @@ struct CreateProfile: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createProfile() {
|
||||||
|
hideKeyboard()
|
||||||
|
let profile = Profile(
|
||||||
|
displayName: displayName.trimmingCharacters(in: .whitespaces),
|
||||||
|
fullName: ""
|
||||||
|
)
|
||||||
|
let m = ChatModel.shared
|
||||||
|
do {
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
|
m.currentUser = try apiCreateActiveUser(profile)
|
||||||
|
// .isEmpty check is redundant here, but it makes it clearer what is going on
|
||||||
|
if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) {
|
||||||
|
try startChat()
|
||||||
|
withAnimation {
|
||||||
|
onboardingStageDefault.set(.step3_ChooseServerOperators)
|
||||||
|
m.onboardingStage = .step3_ChooseServerOperators
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onboardingStageDefault.set(.onboardingComplete)
|
||||||
|
m.onboardingStage = .onboardingComplete
|
||||||
|
dismiss()
|
||||||
|
m.users = try listUsers()
|
||||||
|
try getUserChatData()
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
showCreateProfileAlert(showAlert: { alert = $0 }, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateFirstProfile: View {
|
struct CreateFirstProfile: View {
|
||||||
@@ -86,131 +115,151 @@ struct CreateFirstProfile: View {
|
|||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State private var displayName: String = ""
|
@State private var displayName: String = ""
|
||||||
@FocusState private var focusDisplayName
|
@FocusState private var focusDisplayName
|
||||||
|
@State private var nextStepNavLinkActive = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
Group {
|
VStack(alignment: .center, spacing: 20) {
|
||||||
Text("Create your profile")
|
Text("Create your profile")
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.bold()
|
.bold()
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Text("Your profile, contacts and delivered messages are stored on your device.")
|
Text("Your profile, contacts and delivered messages are stored on your device.")
|
||||||
|
.font(.callout)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Text("The profile is only shared with your contacts.")
|
Text("The profile is only shared with your contacts.")
|
||||||
|
.font(.callout)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.padding(.bottom)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
.padding(.bottom)
|
.frame(maxWidth: .infinity) // Ensures it takes up the full width
|
||||||
|
.padding(.top, 25)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
|
||||||
ZStack(alignment: .topLeading) {
|
HStack {
|
||||||
let name = displayName.trimmingCharacters(in: .whitespaces)
|
let name = displayName.trimmingCharacters(in: .whitespaces)
|
||||||
let validName = mkValidName(name)
|
let validName = mkValidName(name)
|
||||||
if name != validName {
|
ZStack(alignment: .trailing) {
|
||||||
Button {
|
TextField("Enter your name…", text: $displayName)
|
||||||
showAlert(.invalidNameError(validName: validName))
|
.focused($focusDisplayName)
|
||||||
} label: {
|
.padding(.horizontal)
|
||||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
.padding(.vertical, 10)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||||
|
.fill(Color(uiColor: .tertiarySystemFill))
|
||||||
|
)
|
||||||
|
if name != validName {
|
||||||
|
Button {
|
||||||
|
showAlert(.invalidNameError(validName: validName))
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "exclamationmark.circle")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Image(systemName: "exclamationmark.circle").foregroundColor(.clear)
|
|
||||||
}
|
}
|
||||||
TextField("Enter your name…", text: $displayName)
|
|
||||||
.focused($focusDisplayName)
|
|
||||||
.padding(.leading, 32)
|
|
||||||
}
|
}
|
||||||
.padding(.bottom)
|
.padding(.top)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
onboardingButtons()
|
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
createProfileButton()
|
||||||
|
if !focusDisplayName {
|
||||||
|
onboardingButtonPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
focusDisplayName = true
|
focusDisplayName = true
|
||||||
setLastVersionDefault()
|
setLastVersionDefault()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.top, 10)
|
||||||
|
.padding(.bottom, 25)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onboardingButtons() -> some View {
|
func createProfileButton() -> some View {
|
||||||
HStack {
|
ZStack {
|
||||||
Button {
|
Button {
|
||||||
hideKeyboard()
|
createProfile()
|
||||||
withAnimation {
|
|
||||||
m.onboardingStage = .step1_SimpleXInfo
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
Text("Create profile")
|
||||||
Image(systemName: "lessthan")
|
|
||||||
Text("About SimpleX")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button {
|
|
||||||
createProfile(displayName, showAlert: showAlert, dismiss: dismiss)
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Text("Create")
|
|
||||||
Image(systemName: "greaterthan")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: !canCreateProfile(displayName)))
|
||||||
.disabled(!canCreateProfile(displayName))
|
.disabled(!canCreateProfile(displayName))
|
||||||
|
|
||||||
|
NavigationLink(isActive: $nextStepNavLinkActive) {
|
||||||
|
nextStepDestinationView()
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showAlert(_ alert: UserProfileAlert) {
|
private func showAlert(_ alert: UserProfileAlert) {
|
||||||
AlertManager.shared.showAlert(userProfileAlert(alert, $displayName))
|
AlertManager.shared.showAlert(userProfileAlert(alert, $displayName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func nextStepDestinationView() -> some View {
|
||||||
|
ChooseServerOperators(onboarding: true)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createProfile() {
|
||||||
|
hideKeyboard()
|
||||||
|
let profile = Profile(
|
||||||
|
displayName: displayName.trimmingCharacters(in: .whitespaces),
|
||||||
|
fullName: ""
|
||||||
|
)
|
||||||
|
let m = ChatModel.shared
|
||||||
|
do {
|
||||||
|
AppChatState.shared.set(.active)
|
||||||
|
m.currentUser = try apiCreateActiveUser(profile)
|
||||||
|
try startChat(onboarding: true)
|
||||||
|
onboardingStageDefault.set(.step3_ChooseServerOperators)
|
||||||
|
nextStepNavLinkActive = true
|
||||||
|
} catch let error {
|
||||||
|
showCreateProfileAlert(showAlert: showAlert, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createProfile(_ displayName: String, showAlert: (UserProfileAlert) -> Void, dismiss: DismissAction) {
|
private func showCreateProfileAlert(
|
||||||
hideKeyboard()
|
showAlert: (UserProfileAlert) -> Void,
|
||||||
let profile = Profile(
|
_ error: Error
|
||||||
displayName: displayName.trimmingCharacters(in: .whitespaces),
|
) {
|
||||||
fullName: ""
|
|
||||||
)
|
|
||||||
let m = ChatModel.shared
|
let m = ChatModel.shared
|
||||||
do {
|
switch error as? ChatResponse {
|
||||||
AppChatState.shared.set(.active)
|
case .chatCmdError(_, .errorStore(.duplicateName)),
|
||||||
m.currentUser = try apiCreateActiveUser(profile)
|
.chatCmdError(_, .error(.userExists)):
|
||||||
// .isEmpty check is redundant here, but it makes it clearer what is going on
|
if m.currentUser == nil {
|
||||||
if m.users.isEmpty || m.users.allSatisfy({ $0.user.hidden }) {
|
AlertManager.shared.showAlert(duplicateUserAlert)
|
||||||
try startChat()
|
|
||||||
withAnimation {
|
|
||||||
onboardingStageDefault.set(.step3_CreateSimpleXAddress)
|
|
||||||
m.onboardingStage = .step3_CreateSimpleXAddress
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
onboardingStageDefault.set(.onboardingComplete)
|
showAlert(.duplicateUserError)
|
||||||
m.onboardingStage = .onboardingComplete
|
|
||||||
dismiss()
|
|
||||||
m.users = try listUsers()
|
|
||||||
try getUserChatData()
|
|
||||||
}
|
}
|
||||||
} catch let error {
|
case .chatCmdError(_, .error(.invalidDisplayName)):
|
||||||
switch error as? ChatResponse {
|
if m.currentUser == nil {
|
||||||
case .chatCmdError(_, .errorStore(.duplicateName)),
|
AlertManager.shared.showAlert(invalidDisplayNameAlert)
|
||||||
.chatCmdError(_, .error(.userExists)):
|
} else {
|
||||||
if m.currentUser == nil {
|
showAlert(.invalidDisplayNameError)
|
||||||
AlertManager.shared.showAlert(duplicateUserAlert)
|
}
|
||||||
} else {
|
default:
|
||||||
showAlert(.duplicateUserError)
|
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
||||||
}
|
if m.currentUser == nil {
|
||||||
case .chatCmdError(_, .error(.invalidDisplayName)):
|
AlertManager.shared.showAlert(creatUserErrorAlert(err))
|
||||||
if m.currentUser == nil {
|
} else {
|
||||||
AlertManager.shared.showAlert(invalidDisplayNameAlert)
|
showAlert(.createUserError(error: err))
|
||||||
} else {
|
|
||||||
showAlert(.invalidDisplayNameError)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
let err: LocalizedStringKey = "Error: \(responseError(error))"
|
|
||||||
if m.currentUser == nil {
|
|
||||||
AlertManager.shared.showAlert(creatUserErrorAlert(err))
|
|
||||||
} else {
|
|
||||||
showAlert(.createUserError(error: err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
logger.error("Failed to create user or start chat: \(responseError(error))")
|
|
||||||
}
|
}
|
||||||
|
logger.error("Failed to create user or start chat: \(responseError(error))")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func canCreateProfile(_ displayName: String) -> Bool {
|
private func canCreateProfile(_ displayName: String) -> Bool {
|
||||||
|
|||||||
@@ -9,24 +9,24 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct HowItWorks: View {
|
struct HowItWorks: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
var onboarding: Bool
|
var onboarding: Bool
|
||||||
|
@Binding var createProfileNavLinkActive: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("How SimpleX works")
|
Text("How SimpleX works")
|
||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
|
.bold()
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Group {
|
Group {
|
||||||
Text("Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*")
|
Text("To protect your privacy, SimpleX uses separate IDs for each of your contacts.")
|
||||||
Text("To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.")
|
Text("Only client devices store user profiles, contacts, groups, and messages.")
|
||||||
Text("You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.")
|
Text("All messages and files are sent **end-to-end encrypted**, with post-quantum security in direct messages.")
|
||||||
Text("Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.")
|
if !onboarding {
|
||||||
if onboarding {
|
|
||||||
Text("Read more in our GitHub repository.")
|
|
||||||
} else {
|
|
||||||
Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).")
|
Text("Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,19 +37,34 @@ struct HowItWorks: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if onboarding {
|
if onboarding {
|
||||||
OnboardingActionButton()
|
VStack(spacing: 10) {
|
||||||
.padding(.bottom, 8)
|
createFirstProfileButton()
|
||||||
|
onboardingButtonPlaceholder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.lineLimit(10)
|
.lineLimit(10)
|
||||||
.padding()
|
.padding(onboarding ? 25 : 16)
|
||||||
.frame(maxHeight: .infinity, alignment: .top)
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
.modifier(ThemedBackground())
|
.modifier(ThemedBackground())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createFirstProfileButton() -> some View {
|
||||||
|
Button {
|
||||||
|
dismiss()
|
||||||
|
createProfileNavLinkActive = true
|
||||||
|
} label: {
|
||||||
|
Text("Create your profile")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HowItWorks_Previews: PreviewProvider {
|
struct HowItWorks_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
HowItWorks(onboarding: true)
|
HowItWorks(
|
||||||
|
onboarding: true,
|
||||||
|
createProfileNavLinkActive: Binding.constant(false)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,39 @@ struct OnboardingView: View {
|
|||||||
var onboarding: OnboardingStage
|
var onboarding: OnboardingStage
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch onboarding {
|
NavigationView {
|
||||||
case .step1_SimpleXInfo: SimpleXInfo(onboarding: true)
|
switch onboarding {
|
||||||
case .step2_CreateProfile: CreateFirstProfile()
|
case .step1_SimpleXInfo:
|
||||||
case .step3_CreateSimpleXAddress: CreateSimpleXAddress()
|
SimpleXInfo(onboarding: true)
|
||||||
case .step4_SetNotificationsMode: SetNotificationsMode()
|
.modifier(ThemedBackground())
|
||||||
case .onboardingComplete: EmptyView()
|
case .step2_CreateProfile: // deprecated
|
||||||
|
CreateFirstProfile()
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
case .step3_CreateSimpleXAddress: // deprecated
|
||||||
|
CreateSimpleXAddress()
|
||||||
|
case .step3_ChooseServerOperators:
|
||||||
|
ChooseServerOperators(onboarding: true)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
case .step4_SetNotificationsMode:
|
||||||
|
SetNotificationsMode()
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
case .onboardingComplete: EmptyView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func onboardingButtonPlaceholder() -> some View {
|
||||||
|
Spacer().frame(height: 40)
|
||||||
|
}
|
||||||
|
|
||||||
enum OnboardingStage: String, Identifiable {
|
enum OnboardingStage: String, Identifiable {
|
||||||
case step1_SimpleXInfo
|
case step1_SimpleXInfo
|
||||||
case step2_CreateProfile
|
case step2_CreateProfile // deprecated
|
||||||
case step3_CreateSimpleXAddress
|
case step3_CreateSimpleXAddress // deprecated
|
||||||
|
case step3_ChooseServerOperators
|
||||||
case step4_SetNotificationsMode
|
case step4_SetNotificationsMode
|
||||||
case onboardingComplete
|
case onboardingComplete
|
||||||
|
|
||||||
|
|||||||
@@ -13,42 +13,54 @@ struct SetNotificationsMode: View {
|
|||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@State private var notificationMode = NotificationsMode.instant
|
@State private var notificationMode = NotificationsMode.instant
|
||||||
@State private var showAlert: NotificationAlert?
|
@State private var showAlert: NotificationAlert?
|
||||||
|
@State private var showInfo: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
GeometryReader { g in
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
ScrollView {
|
||||||
Text("Push notifications")
|
VStack(alignment: .center, spacing: 20) {
|
||||||
.font(.largeTitle)
|
Text("Push Notifications")
|
||||||
.bold()
|
.font(.largeTitle)
|
||||||
.frame(maxWidth: .infinity)
|
.bold()
|
||||||
|
.padding(.top, 50)
|
||||||
|
|
||||||
|
infoText()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
Text("Send notifications:")
|
ForEach(NotificationsMode.values) { mode in
|
||||||
ForEach(NotificationsMode.values) { mode in
|
NtfModeSelector(mode: mode, selection: $notificationMode)
|
||||||
NtfModeSelector(mode: mode, selection: $notificationMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button {
|
|
||||||
if let token = m.deviceToken {
|
|
||||||
setNotificationsMode(token, notificationMode)
|
|
||||||
} else {
|
|
||||||
AlertManager.shared.showAlertMsg(title: "No device token!")
|
|
||||||
}
|
}
|
||||||
onboardingStageDefault.set(.onboardingComplete)
|
|
||||||
m.onboardingStage = .onboardingComplete
|
Spacer()
|
||||||
} label: {
|
|
||||||
if case .off = notificationMode {
|
VStack(spacing: 10) {
|
||||||
Text("Use chat")
|
Button {
|
||||||
} else {
|
if let token = m.deviceToken {
|
||||||
Text("Enable notifications")
|
setNotificationsMode(token, notificationMode)
|
||||||
|
} else {
|
||||||
|
AlertManager.shared.showAlertMsg(title: "No device token!")
|
||||||
|
}
|
||||||
|
onboardingStageDefault.set(.onboardingComplete)
|
||||||
|
m.onboardingStage = .onboardingComplete
|
||||||
|
} label: {
|
||||||
|
if case .off = notificationMode {
|
||||||
|
Text("Use chat")
|
||||||
|
} else {
|
||||||
|
Text("Enable notifications")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle())
|
||||||
|
onboardingButtonPlaceholder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.font(.title)
|
.padding(25)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(minHeight: g.size.height)
|
||||||
}
|
}
|
||||||
.padding()
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
.frame(maxHeight: .infinity)
|
||||||
|
.sheet(isPresented: $showInfo) {
|
||||||
|
NotificationsInfoView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +87,15 @@ struct SetNotificationsMode: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func infoText() -> some View {
|
||||||
|
Button {
|
||||||
|
showInfo = true
|
||||||
|
} label: {
|
||||||
|
Label("How it affects privacy", systemImage: "info.circle")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct NtfModeSelector: View {
|
struct NtfModeSelector: View {
|
||||||
@@ -85,15 +106,24 @@ struct NtfModeSelector: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
HStack(spacing: 16) {
|
||||||
Text(mode.label)
|
Image(systemName: mode.icon)
|
||||||
.font(.headline)
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: mode.icon == "bolt" ? 14 : 18, height: 18)
|
||||||
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
||||||
Text(ntfModeDescription(mode))
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
.lineLimit(10)
|
Text(mode.label)
|
||||||
.font(.subheadline)
|
.font(.headline)
|
||||||
|
.foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary)
|
||||||
|
Text(ntfModeShortDescription(mode))
|
||||||
|
.lineLimit(2)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(12)
|
.padding(.vertical, 12)
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
.padding(.leading, 16)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
.background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background)
|
||||||
@@ -109,6 +139,37 @@ struct NtfModeSelector: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NotificationsInfoView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Notifications privacy")
|
||||||
|
.font(.largeTitle)
|
||||||
|
.bold()
|
||||||
|
.padding(.vertical)
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Group {
|
||||||
|
ForEach(NotificationsMode.values) { mode in
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
(Text(Image(systemName: mode.icon)) + textSpace + Text(mode.label))
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(ntfModeDescription(mode))
|
||||||
|
.lineLimit(10)
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct NotificationsModeView_Previews: PreviewProvider {
|
struct NotificationsModeView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
SetNotificationsMode()
|
SetNotificationsMode()
|
||||||
|
|||||||
@@ -13,58 +13,58 @@ struct SimpleXInfo: View {
|
|||||||
@EnvironmentObject var m: ChatModel
|
@EnvironmentObject var m: ChatModel
|
||||||
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
@State private var showHowItWorks = false
|
@State private var showHowItWorks = false
|
||||||
|
@State private var createProfileNavLinkActive = false
|
||||||
var onboarding: Bool
|
var onboarding: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { g in
|
GeometryReader { g in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Image(colorScheme == .light ? "logo" : "logo-light")
|
VStack(alignment: .center, spacing: 10) {
|
||||||
.resizable()
|
Image(colorScheme == .light ? "logo" : "logo-light")
|
||||||
.aspectRatio(contentMode: .fit)
|
.resizable()
|
||||||
.frame(width: g.size.width * 0.67)
|
.aspectRatio(contentMode: .fit)
|
||||||
.padding(.bottom, 8)
|
.frame(width: g.size.width * 0.67)
|
||||||
.frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
|
.padding(.bottom, 8)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 48, alignment: .top)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showHowItWorks = true
|
||||||
|
} label: {
|
||||||
|
Label("The future of messaging", systemImage: "info.circle")
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
Text("The next generation of private messaging")
|
onboardingInfoRow("privacy", "Privacy redefined",
|
||||||
.font(.title2)
|
"No user identifiers.", width: 48)
|
||||||
.padding(.bottom, 30)
|
onboardingInfoRow("shield", "Immune to spam",
|
||||||
.padding(.horizontal, 40)
|
"You decide who can connect.", width: 46)
|
||||||
.frame(maxWidth: .infinity)
|
onboardingInfoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
|
||||||
.multilineTextAlignment(.center)
|
"Anybody can host servers.", width: 46)
|
||||||
infoRow("privacy", "Privacy redefined",
|
|
||||||
"The 1st platform without any user identifiers – private by design.", width: 48)
|
|
||||||
infoRow("shield", "Immune to spam and abuse",
|
|
||||||
"People can connect to you only via the links you share.", width: 46)
|
|
||||||
infoRow(colorScheme == .light ? "decentralized" : "decentralized-light", "Decentralized",
|
|
||||||
"Open-source protocol and code – anybody can run the servers.", width: 44)
|
|
||||||
}
|
}
|
||||||
|
.padding(.leading, 16)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if onboarding {
|
if onboarding {
|
||||||
OnboardingActionButton()
|
VStack(spacing: 10) {
|
||||||
Spacer()
|
createFirstProfileButton()
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
m.migrationState = .pasteOrScanLink
|
m.migrationState = .pasteOrScanLink
|
||||||
} label: {
|
} label: {
|
||||||
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
|
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
|
||||||
.font(.subheadline)
|
.font(.system(size: 17, weight: .semibold))
|
||||||
|
.frame(minHeight: 40)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 8)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
|
||||||
showHowItWorks = true
|
|
||||||
} label: {
|
|
||||||
Label("How it works", systemImage: "info.circle")
|
|
||||||
.font(.subheadline)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 8)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.frame(minHeight: g.size.height)
|
.frame(minHeight: g.size.height)
|
||||||
}
|
}
|
||||||
@@ -83,76 +83,59 @@ struct SimpleXInfo: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showHowItWorks) {
|
.sheet(isPresented: $showHowItWorks) {
|
||||||
HowItWorks(onboarding: onboarding)
|
HowItWorks(
|
||||||
|
onboarding: onboarding,
|
||||||
|
createProfileNavLinkActive: $createProfileNavLinkActive
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
.padding()
|
.padding(.horizontal, 25)
|
||||||
|
.padding(.top, 75)
|
||||||
|
.padding(.bottom, 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func infoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
|
private func onboardingInfoRow(_ image: String, _ title: LocalizedStringKey, _ text: LocalizedStringKey, width: CGFloat) -> some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Image(image)
|
Image(image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFit()
|
.scaledToFit()
|
||||||
.frame(width: width, height: 54)
|
.frame(width: width, height: 54)
|
||||||
.frame(width: 54)
|
.frame(width: 54)
|
||||||
.padding(.top, 4)
|
|
||||||
.padding(.leading, 4)
|
|
||||||
.padding(.trailing, 10)
|
.padding(.trailing, 10)
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title).font(.headline)
|
Text(title).font(.headline)
|
||||||
Text(text).frame(minHeight: 40, alignment: .top)
|
Text(text).frame(minHeight: 40, alignment: .top)
|
||||||
|
.font(.callout)
|
||||||
|
.lineLimit(3)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createFirstProfileButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Button {
|
||||||
|
createProfileNavLinkActive = true
|
||||||
|
} label: {
|
||||||
|
Text("Create your profile")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle(isDisabled: false))
|
||||||
|
|
||||||
|
NavigationLink(isActive: $createProfileNavLinkActive) {
|
||||||
|
CreateFirstProfile()
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
}
|
}
|
||||||
.padding(.bottom, 20)
|
|
||||||
.padding(.trailing, 6)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct OnboardingActionButton: View {
|
let textSpace = Text(verbatim: " ")
|
||||||
@EnvironmentObject var m: ChatModel
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if m.currentUser == nil {
|
|
||||||
actionButton("Create your profile", onboarding: .step2_CreateProfile)
|
|
||||||
} else {
|
|
||||||
actionButton("Make a private connection", onboarding: .onboardingComplete)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func actionButton(_ label: LocalizedStringKey, onboarding: OnboardingStage) -> some View {
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
onboardingStageDefault.set(onboarding)
|
|
||||||
m.onboardingStage = onboarding
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Text(label).font(.title2)
|
|
||||||
Image(systemName: "greaterthan")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Text(label).font(.title2)
|
|
||||||
Image(systemName: "greaterthan")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.padding(.bottom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SimpleXInfo_Previews: PreviewProvider {
|
struct SimpleXInfo_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
|
|||||||
@@ -7,190 +7,209 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
private struct VersionDescription {
|
private struct VersionDescription {
|
||||||
var version: String
|
var version: String
|
||||||
var post: URL?
|
var post: URL?
|
||||||
var features: [FeatureDescription]
|
var features: [Feature]
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct FeatureDescription {
|
private enum Feature: Identifiable {
|
||||||
var icon: String?
|
case feature(Description)
|
||||||
var title: LocalizedStringKey
|
case view(FeatureView)
|
||||||
var description: LocalizedStringKey?
|
|
||||||
|
var id: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case let .feature(d): d.title
|
||||||
|
case let .view(v): v.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct Description {
|
||||||
|
let icon: String?
|
||||||
|
let title: LocalizedStringKey
|
||||||
|
let description: LocalizedStringKey?
|
||||||
var subfeatures: [(icon: String, description: LocalizedStringKey)] = []
|
var subfeatures: [(icon: String, description: LocalizedStringKey)] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct FeatureView {
|
||||||
|
let icon: String?
|
||||||
|
let title: LocalizedStringKey
|
||||||
|
let view: () -> any View
|
||||||
|
}
|
||||||
|
|
||||||
private let versionDescriptions: [VersionDescription] = [
|
private let versionDescriptions: [VersionDescription] = [
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v4.2",
|
version: "v4.2",
|
||||||
post: URL(string: "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"),
|
post: URL(string: "https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "checkmark.shield",
|
icon: "checkmark.shield",
|
||||||
title: "Security assessment",
|
title: "Security assessment",
|
||||||
description: "SimpleX Chat security was audited by Trail of Bits."
|
description: "SimpleX Chat security was audited by Trail of Bits."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "person.2",
|
icon: "person.2",
|
||||||
title: "Group links",
|
title: "Group links",
|
||||||
description: "Admins can create the links to join groups."
|
description: "Admins can create the links to join groups."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "checkmark",
|
icon: "checkmark",
|
||||||
title: "Auto-accept contact requests",
|
title: "Auto-accept contact requests",
|
||||||
description: "With optional welcome message."
|
description: "With optional welcome message."
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v4.3",
|
version: "v4.3",
|
||||||
post: URL(string: "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html"),
|
post: URL(string: "https://simplex.chat/blog/20221206-simplex-chat-v4.3-voice-messages.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "mic",
|
icon: "mic",
|
||||||
title: "Voice messages",
|
title: "Voice messages",
|
||||||
description: "Max 30 seconds, received instantly."
|
description: "Max 30 seconds, received instantly."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "trash.slash",
|
icon: "trash.slash",
|
||||||
title: "Irreversible message deletion",
|
title: "Irreversible message deletion",
|
||||||
description: "Your contacts can allow full message deletion."
|
description: "Your contacts can allow full message deletion."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "externaldrive.connected.to.line.below",
|
icon: "externaldrive.connected.to.line.below",
|
||||||
title: "Improved server configuration",
|
title: "Improved server configuration",
|
||||||
description: "Add servers by scanning QR codes."
|
description: "Add servers by scanning QR codes."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "eye.slash",
|
icon: "eye.slash",
|
||||||
title: "Improved privacy and security",
|
title: "Improved privacy and security",
|
||||||
description: "Hide app screen in the recent apps."
|
description: "Hide app screen in the recent apps."
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v4.4",
|
version: "v4.4",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html"),
|
post: URL(string: "https://simplex.chat/blog/20230103-simplex-chat-v4.4-disappearing-messages.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "stopwatch",
|
icon: "stopwatch",
|
||||||
title: "Disappearing messages",
|
title: "Disappearing messages",
|
||||||
description: "Sent messages will be deleted after set time."
|
description: "Sent messages will be deleted after set time."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "ellipsis.circle",
|
icon: "ellipsis.circle",
|
||||||
title: "Live messages",
|
title: "Live messages",
|
||||||
description: "Recipients see updates as you type them."
|
description: "Recipients see updates as you type them."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "checkmark.shield",
|
icon: "checkmark.shield",
|
||||||
title: "Verify connection security",
|
title: "Verify connection security",
|
||||||
description: "Compare security codes with your contacts."
|
description: "Compare security codes with your contacts."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "camera",
|
icon: "camera",
|
||||||
title: "GIFs and stickers",
|
title: "GIFs and stickers",
|
||||||
description: "Send them from gallery or custom keyboards."
|
description: "Send them from gallery or custom keyboards."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "French interface",
|
title: "French interface",
|
||||||
description: "Thanks to the users – contribute via Weblate!"
|
description: "Thanks to the users – contribute via Weblate!"
|
||||||
)
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v4.5",
|
version: "v4.5",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html"),
|
post: URL(string: "https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "person.crop.rectangle.stack",
|
icon: "person.crop.rectangle.stack",
|
||||||
title: "Multiple chat profiles",
|
title: "Multiple chat profiles",
|
||||||
description: "Different names, avatars and transport isolation."
|
description: "Different names, avatars and transport isolation."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "rectangle.and.pencil.and.ellipsis",
|
icon: "rectangle.and.pencil.and.ellipsis",
|
||||||
title: "Message draft",
|
title: "Message draft",
|
||||||
description: "Preserve the last message draft, with attachments."
|
description: "Preserve the last message draft, with attachments."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "network.badge.shield.half.filled",
|
icon: "network.badge.shield.half.filled",
|
||||||
title: "Transport isolation",
|
title: "Transport isolation",
|
||||||
description: "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."
|
description: "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "lock.doc",
|
icon: "lock.doc",
|
||||||
title: "Private filenames",
|
title: "Private filenames",
|
||||||
description: "To protect timezone, image/voice files use UTC."
|
description: "To protect timezone, image/voice files use UTC."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "battery.25",
|
icon: "battery.25",
|
||||||
title: "Reduced battery usage",
|
title: "Reduced battery usage",
|
||||||
description: "More improvements are coming soon!"
|
description: "More improvements are coming soon!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "Italian interface",
|
title: "Italian interface",
|
||||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
)
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v4.6",
|
version: "v4.6",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html"),
|
post: URL(string: "https://simplex.chat/blog/20230328-simplex-chat-v4-6-hidden-profiles.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "lock",
|
icon: "lock",
|
||||||
title: "Hidden chat profiles",
|
title: "Hidden chat profiles",
|
||||||
description: "Protect your chat profiles with a password!"
|
description: "Protect your chat profiles with a password!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "phone.arrow.up.right",
|
icon: "phone.arrow.up.right",
|
||||||
title: "Audio and video calls",
|
title: "Audio and video calls",
|
||||||
description: "Fully re-implemented - work in background!"
|
description: "Fully re-implemented - work in background!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "flag",
|
icon: "flag",
|
||||||
title: "Group moderation",
|
title: "Group moderation",
|
||||||
description: "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)"
|
description: "Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "plus.message",
|
icon: "plus.message",
|
||||||
title: "Group welcome message",
|
title: "Group welcome message",
|
||||||
description: "Set the message shown to new members!"
|
description: "Set the message shown to new members!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "battery.50",
|
icon: "battery.50",
|
||||||
title: "Further reduced battery usage",
|
title: "Further reduced battery usage",
|
||||||
description: "More improvements are coming soon!"
|
description: "More improvements are coming soon!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "Chinese and Spanish interface",
|
title: "Chinese and Spanish interface",
|
||||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.0",
|
version: "v5.0",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html"),
|
post: URL(string: "https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "arrow.up.doc",
|
icon: "arrow.up.doc",
|
||||||
title: "Videos and files up to 1gb",
|
title: "Videos and files up to 1gb",
|
||||||
description: "Fast and no wait until the sender is online!"
|
description: "Fast and no wait until the sender is online!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "lock",
|
icon: "lock",
|
||||||
title: "App passcode",
|
title: "App passcode",
|
||||||
description: "Set it instead of system authentication."
|
description: "Set it instead of system authentication."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "Polish interface",
|
title: "Polish interface",
|
||||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
// Also
|
// Also
|
||||||
@@ -200,240 +219,240 @@ private let versionDescriptions: [VersionDescription] = [
|
|||||||
version: "v5.1",
|
version: "v5.1",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html"),
|
post: URL(string: "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "face.smiling",
|
icon: "face.smiling",
|
||||||
title: "Message reactions",
|
title: "Message reactions",
|
||||||
description: "Finally, we have them! 🚀"
|
description: "Finally, we have them! 🚀"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "arrow.up.message",
|
icon: "arrow.up.message",
|
||||||
title: "Better messages",
|
title: "Better messages",
|
||||||
description: "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history."
|
description: "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "lock",
|
icon: "lock",
|
||||||
title: "Self-destruct passcode",
|
title: "Self-destruct passcode",
|
||||||
description: "All data is erased when it is entered."
|
description: "All data is erased when it is entered."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "Japanese interface",
|
title: "Japanese interface",
|
||||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.2",
|
version: "v5.2",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html"),
|
post: URL(string: "https://simplex.chat/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "checkmark",
|
icon: "checkmark",
|
||||||
title: "Message delivery receipts!",
|
title: "Message delivery receipts!",
|
||||||
description: "The second tick we missed! ✅"
|
description: "The second tick we missed! ✅"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "star",
|
icon: "star",
|
||||||
title: "Find chats faster",
|
title: "Find chats faster",
|
||||||
description: "Filter unread and favorite chats."
|
description: "Filter unread and favorite chats."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "exclamationmark.arrow.triangle.2.circlepath",
|
icon: "exclamationmark.arrow.triangle.2.circlepath",
|
||||||
title: "Keep your connections",
|
title: "Keep your connections",
|
||||||
description: "Fix encryption after restoring backups."
|
description: "Fix encryption after restoring backups."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "stopwatch",
|
icon: "stopwatch",
|
||||||
title: "Make one message disappear",
|
title: "Make one message disappear",
|
||||||
description: "Even when disabled in the conversation."
|
description: "Even when disabled in the conversation."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "gift",
|
icon: "gift",
|
||||||
title: "A few more things",
|
title: "A few more things",
|
||||||
description: "- more stable message delivery.\n- a bit better groups.\n- and more!"
|
description: "- more stable message delivery.\n- a bit better groups.\n- and more!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.3",
|
version: "v5.3",
|
||||||
post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"),
|
post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "desktopcomputer",
|
icon: "desktopcomputer",
|
||||||
title: "New desktop app!",
|
title: "New desktop app!",
|
||||||
description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻"
|
description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "lock",
|
icon: "lock",
|
||||||
title: "Encrypt stored files & media",
|
title: "Encrypt stored files & media",
|
||||||
description: "App encrypts new local files (except videos)."
|
description: "App encrypts new local files (except videos)."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "magnifyingglass",
|
icon: "magnifyingglass",
|
||||||
title: "Discover and join groups",
|
title: "Discover and join groups",
|
||||||
description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable."
|
description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "theatermasks",
|
icon: "theatermasks",
|
||||||
title: "Simplified incognito mode",
|
title: "Simplified incognito mode",
|
||||||
description: "Toggle incognito when connecting."
|
description: "Toggle incognito when connecting."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "\(4) new interface languages",
|
title: "\(4) new interface languages",
|
||||||
description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.4",
|
version: "v5.4",
|
||||||
post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"),
|
post: URL(string: "https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "desktopcomputer",
|
icon: "desktopcomputer",
|
||||||
title: "Link mobile and desktop apps! 🔗",
|
title: "Link mobile and desktop apps! 🔗",
|
||||||
description: "Via secure quantum resistant protocol."
|
description: "Via secure quantum resistant protocol."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "person.2",
|
icon: "person.2",
|
||||||
title: "Better groups",
|
title: "Better groups",
|
||||||
description: "Faster joining and more reliable messages."
|
description: "Faster joining and more reliable messages."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "theatermasks",
|
icon: "theatermasks",
|
||||||
title: "Incognito groups",
|
title: "Incognito groups",
|
||||||
description: "Create a group using a random profile."
|
description: "Create a group using a random profile."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "hand.raised",
|
icon: "hand.raised",
|
||||||
title: "Block group members",
|
title: "Block group members",
|
||||||
description: "To hide unwanted messages."
|
description: "To hide unwanted messages."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "gift",
|
icon: "gift",
|
||||||
title: "A few more things",
|
title: "A few more things",
|
||||||
description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!"
|
description: "- optionally notify deleted contacts.\n- profile names with spaces.\n- and more!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.5",
|
version: "v5.5",
|
||||||
post: URL(string: "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"),
|
post: URL(string: "https://simplex.chat/blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "folder",
|
icon: "folder",
|
||||||
title: "Private notes",
|
title: "Private notes",
|
||||||
description: "With encrypted files and media."
|
description: "With encrypted files and media."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "link",
|
icon: "link",
|
||||||
title: "Paste link to connect!",
|
title: "Paste link to connect!",
|
||||||
description: "Search bar accepts invitation links."
|
description: "Search bar accepts invitation links."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "bubble.left.and.bubble.right",
|
icon: "bubble.left.and.bubble.right",
|
||||||
title: "Join group conversations",
|
title: "Join group conversations",
|
||||||
description: "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."
|
description: "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "battery.50",
|
icon: "battery.50",
|
||||||
title: "Improved message delivery",
|
title: "Improved message delivery",
|
||||||
description: "With reduced battery usage."
|
description: "With reduced battery usage."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "Turkish interface",
|
title: "Turkish interface",
|
||||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.6",
|
version: "v5.6",
|
||||||
post: URL(string: "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html"),
|
post: URL(string: "https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "key",
|
icon: "key",
|
||||||
title: "Quantum resistant encryption",
|
title: "Quantum resistant encryption",
|
||||||
description: "Enable in direct chats (BETA)!"
|
description: "Enable in direct chats (BETA)!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "tray.and.arrow.up",
|
icon: "tray.and.arrow.up",
|
||||||
title: "App data migration",
|
title: "App data migration",
|
||||||
description: "Migrate to another device via QR code."
|
description: "Migrate to another device via QR code."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "phone",
|
icon: "phone",
|
||||||
title: "Picture-in-picture calls",
|
title: "Picture-in-picture calls",
|
||||||
description: "Use the app while in the call."
|
description: "Use the app while in the call."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "hand.raised",
|
icon: "hand.raised",
|
||||||
title: "Safer groups",
|
title: "Safer groups",
|
||||||
description: "Admins can block a member for all."
|
description: "Admins can block a member for all."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "character",
|
icon: "character",
|
||||||
title: "Hungarian interface",
|
title: "Hungarian interface",
|
||||||
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
description: "Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.7",
|
version: "v5.7",
|
||||||
post: URL(string: "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"),
|
post: URL(string: "https://simplex.chat/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "key",
|
icon: "key",
|
||||||
title: "Quantum resistant encryption",
|
title: "Quantum resistant encryption",
|
||||||
description: "Will be enabled in direct chats!"
|
description: "Will be enabled in direct chats!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "arrowshape.turn.up.forward",
|
icon: "arrowshape.turn.up.forward",
|
||||||
title: "Forward and save messages",
|
title: "Forward and save messages",
|
||||||
description: "Message source remains private."
|
description: "Message source remains private."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "music.note",
|
icon: "music.note",
|
||||||
title: "In-call sounds",
|
title: "In-call sounds",
|
||||||
description: "When connecting audio and video calls."
|
description: "When connecting audio and video calls."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "person.crop.square",
|
icon: "person.crop.square",
|
||||||
title: "Shape profile images",
|
title: "Shape profile images",
|
||||||
description: "Square, circle, or anything in between."
|
description: "Square, circle, or anything in between."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "antenna.radiowaves.left.and.right",
|
icon: "antenna.radiowaves.left.and.right",
|
||||||
title: "Network management",
|
title: "Network management",
|
||||||
description: "More reliable network connection."
|
description: "More reliable network connection."
|
||||||
)
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v5.8",
|
version: "v5.8",
|
||||||
post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"),
|
post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "arrow.forward",
|
icon: "arrow.forward",
|
||||||
title: "Private message routing 🚀",
|
title: "Private message routing 🚀",
|
||||||
description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings."
|
description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "network.badge.shield.half.filled",
|
icon: "network.badge.shield.half.filled",
|
||||||
title: "Safely receive files",
|
title: "Safely receive files",
|
||||||
description: "Confirm files from unknown servers."
|
description: "Confirm files from unknown servers."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "battery.50",
|
icon: "battery.50",
|
||||||
title: "Improved message delivery",
|
title: "Improved message delivery",
|
||||||
description: "With reduced battery usage."
|
description: "With reduced battery usage."
|
||||||
)
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v6.0",
|
version: "v6.0",
|
||||||
post: URL(string: "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html"),
|
post: URL(string: "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: nil,
|
icon: nil,
|
||||||
title: "New chat experience 🎉",
|
title: "New chat experience 🎉",
|
||||||
description: nil,
|
description: nil,
|
||||||
@@ -444,8 +463,8 @@ private let versionDescriptions: [VersionDescription] = [
|
|||||||
("platter.filled.bottom.and.arrow.down.iphone", "Use the app with one hand."),
|
("platter.filled.bottom.and.arrow.down.iphone", "Use the app with one hand."),
|
||||||
("paintpalette", "Color chats with the new themes."),
|
("paintpalette", "Color chats with the new themes."),
|
||||||
]
|
]
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: nil,
|
icon: nil,
|
||||||
title: "New media options",
|
title: "New media options",
|
||||||
description: nil,
|
description: nil,
|
||||||
@@ -454,39 +473,39 @@ private let versionDescriptions: [VersionDescription] = [
|
|||||||
("play.circle", "Play from the chat list."),
|
("play.circle", "Play from the chat list."),
|
||||||
("circle.filled.pattern.diagonalline.rectangle", "Blur for better privacy.")
|
("circle.filled.pattern.diagonalline.rectangle", "Blur for better privacy.")
|
||||||
]
|
]
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "arrow.forward",
|
icon: "arrow.forward",
|
||||||
title: "Private message routing 🚀",
|
title: "Private message routing 🚀",
|
||||||
description: "It protects your IP address and connections."
|
description: "It protects your IP address and connections."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "network",
|
icon: "network",
|
||||||
title: "Better networking",
|
title: "Better networking",
|
||||||
description: "Connection and servers status."
|
description: "Connection and servers status."
|
||||||
)
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
VersionDescription(
|
VersionDescription(
|
||||||
version: "v6.1",
|
version: "v6.1",
|
||||||
post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"),
|
post: URL(string: "https://simplex.chat/blog/20241014-simplex-network-v6-1-security-review-better-calls-user-experience.html"),
|
||||||
features: [
|
features: [
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "checkmark.shield",
|
icon: "checkmark.shield",
|
||||||
title: "Better security ✅",
|
title: "Better security ✅",
|
||||||
description: "SimpleX protocols reviewed by Trail of Bits."
|
description: "SimpleX protocols reviewed by Trail of Bits."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "video",
|
icon: "video",
|
||||||
title: "Better calls",
|
title: "Better calls",
|
||||||
description: "Switch audio and video during the call."
|
description: "Switch audio and video during the call."
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: "bolt",
|
icon: "bolt",
|
||||||
title: "Better notifications",
|
title: "Better notifications",
|
||||||
description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!"
|
description: "Improved delivery, reduced traffic usage.\nMore improvements are coming soon!"
|
||||||
),
|
)),
|
||||||
FeatureDescription(
|
.feature(Description(
|
||||||
icon: nil,
|
icon: nil,
|
||||||
title: "Better user experience",
|
title: "Better user experience",
|
||||||
description: nil,
|
description: nil,
|
||||||
@@ -497,9 +516,30 @@ private let versionDescriptions: [VersionDescription] = [
|
|||||||
("arrowshape.turn.up.right", "Forward up to 20 messages at once."),
|
("arrowshape.turn.up.right", "Forward up to 20 messages at once."),
|
||||||
("flag", "Delete or moderate up to 200 messages.")
|
("flag", "Delete or moderate up to 200 messages.")
|
||||||
]
|
]
|
||||||
),
|
)),
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
VersionDescription(
|
||||||
|
version: "v6.2",
|
||||||
|
post: URL(string: "https://simplex.chat/blog/20241210-simplex-network-v6-2-servers-by-flux-business-chats.html"),
|
||||||
|
features: [
|
||||||
|
.view(FeatureView(
|
||||||
|
icon: nil,
|
||||||
|
title: "Network decentralization",
|
||||||
|
view: { NewOperatorsView() }
|
||||||
|
)),
|
||||||
|
.feature(Description(
|
||||||
|
icon: "briefcase",
|
||||||
|
title: "Business chats",
|
||||||
|
description: "Privacy for your customers."
|
||||||
|
)),
|
||||||
|
.feature(Description(
|
||||||
|
icon: "bolt",
|
||||||
|
title: "More reliable notifications",
|
||||||
|
description: "Delivered even when Apple drops them."
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
private let lastVersion = versionDescriptions.last!.version
|
private let lastVersion = versionDescriptions.last!.version
|
||||||
@@ -514,14 +554,68 @@ func shouldShowWhatsNew() -> Bool {
|
|||||||
return v != lastVersion
|
return v != lastVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate struct NewOperatorsView: View {
|
||||||
|
@State private var showOperatorsSheet = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Image((operatorsInfo[.flux] ?? ServerOperator.dummyOperatorInfo).largeLogo)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(height: 48)
|
||||||
|
Text("The second preset operator in the app!")
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.lineLimit(10)
|
||||||
|
HStack {
|
||||||
|
Button("Enable Flux") {
|
||||||
|
showOperatorsSheet = true
|
||||||
|
}
|
||||||
|
Text("for better metadata privacy.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showOperatorsSheet) {
|
||||||
|
NavigationView {
|
||||||
|
ChooseServerOperators(onboarding: false)
|
||||||
|
.modifier(ThemedBackground())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum WhatsNewViewSheet: Identifiable {
|
||||||
|
case showConditions
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .showConditions: return "showConditions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct WhatsNewView: View {
|
struct WhatsNewView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
@State var currentVersion = versionDescriptions.count - 1
|
@State var currentVersion = versionDescriptions.count - 1
|
||||||
@State var currentVersionNav = versionDescriptions.count - 1
|
@State var currentVersionNav = versionDescriptions.count - 1
|
||||||
var viaSettings = false
|
var viaSettings = false
|
||||||
|
var updatedConditions: Bool
|
||||||
|
@State private var sheetItem: WhatsNewViewSheet? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
whatsNewView()
|
||||||
|
.sheet(item: $sheetItem) { item in
|
||||||
|
switch item {
|
||||||
|
case .showConditions:
|
||||||
|
UsageConditionsView(
|
||||||
|
currUserServers: Binding.constant([]),
|
||||||
|
userServers: Binding.constant([])
|
||||||
|
)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func whatsNewView() -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
TabView(selection: $currentVersion) {
|
TabView(selection: $currentVersion) {
|
||||||
ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in
|
ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in
|
||||||
@@ -532,9 +626,11 @@ struct WhatsNewView: View {
|
|||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
ForEach(v.features, id: \.title) { f in
|
ForEach(v.features) { f in
|
||||||
featureDescription(f)
|
switch f {
|
||||||
.padding(.bottom, 8)
|
case let .feature(d): featureDescription(d).padding(.bottom, 8)
|
||||||
|
case let .view(v): AnyView(v.view()).padding(.bottom, 8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let post = v.post {
|
if let post = v.post {
|
||||||
Link(destination: post) {
|
Link(destination: post) {
|
||||||
@@ -544,13 +640,20 @@ struct WhatsNewView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if updatedConditions {
|
||||||
|
Button("View updated conditions") {
|
||||||
|
sheetItem = .showConditions
|
||||||
|
}
|
||||||
|
}
|
||||||
if !viaSettings {
|
if !viaSettings {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button("Ok") {
|
Button("Ok") {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
.font(.title3)
|
.font(.title3)
|
||||||
.frame(maxWidth: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,20 +671,24 @@ struct WhatsNewView: View {
|
|||||||
currentVersionNav = currentVersion
|
currentVersionNav = currentVersion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func featureDescription(_ f: FeatureDescription) -> some View {
|
@ViewBuilder private func featureHeader(_ icon: String?, _ title: LocalizedStringKey) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
if let icon {
|
||||||
if let icon = f.icon {
|
HStack(alignment: .center, spacing: 4) {
|
||||||
HStack(alignment: .center, spacing: 4) {
|
Image(systemName: icon)
|
||||||
Image(systemName: icon)
|
.symbolRenderingMode(.monochrome)
|
||||||
.symbolRenderingMode(.monochrome)
|
.foregroundColor(theme.colors.secondary)
|
||||||
.foregroundColor(theme.colors.secondary)
|
.frame(minWidth: 30, alignment: .center)
|
||||||
.frame(minWidth: 30, alignment: .center)
|
Text(title).font(.title3).bold()
|
||||||
Text(f.title).font(.title3).bold()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text(f.title).font(.title3).bold()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Text(title).font(.title3).bold()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func featureDescription(_ f: Description) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
featureHeader(f.icon, f.title)
|
||||||
if let d = f.description {
|
if let d = f.description {
|
||||||
Text(d)
|
Text(d)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
@@ -636,6 +743,6 @@ struct WhatsNewView: View {
|
|||||||
|
|
||||||
struct NewFeaturesView_Previews: PreviewProvider {
|
struct NewFeaturesView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
WhatsNewView()
|
WhatsNewView(updatedConditions: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ struct ConnectDesktopView: View {
|
|||||||
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text {
|
||||||
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
|
var t = Text(rc?.deviceViewName ?? session.ctrlAppInfo?.deviceName ?? "")
|
||||||
if (rc == nil) {
|
if (rc == nil) {
|
||||||
t = t + Text(" ") + Text("(new)").italic()
|
t = t + textSpace + Text("(new)").italic()
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ struct ConnectDesktopView: View {
|
|||||||
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
|
let v = session.ctrlAppInfo?.appVersionRange.maxVersion
|
||||||
var t = Text("v\(v ?? "")")
|
var t = Text("v\(v ?? "")")
|
||||||
if v != session.appVersion {
|
if v != session.appVersion {
|
||||||
t = t + Text(" ") + Text("(this device v\(session.appVersion))").italic()
|
t = t + textSpace + Text("(this device v\(session.appVersion))").italic()
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ extension AppSettings {
|
|||||||
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
|
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
|
||||||
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
|
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
|
||||||
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
|
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
|
||||||
|
if let val = chatBottomBar { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static var current: AppSettings {
|
public static var current: AppSettings {
|
||||||
@@ -100,6 +101,7 @@ extension AppSettings {
|
|||||||
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
|
c.uiCurrentThemeIds = currentThemeIdsDefault.get()
|
||||||
c.uiThemes = themeOverridesDefault.get()
|
c.uiThemes = themeOverridesDefault.get()
|
||||||
c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI)
|
c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI)
|
||||||
|
c.chatBottomBar = groupDefaults.bool(forKey: GROUP_DEFAULT_CHAT_BOTTOM_BAR)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ struct DeveloperView: View {
|
|||||||
} header: {
|
} header: {
|
||||||
Text("")
|
Text("")
|
||||||
} footer: {
|
} footer: {
|
||||||
((developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option."))
|
((developerTools ? Text("Show:") : Text("Hide:")) + textSpace + Text("Database IDs and Transport isolation option."))
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
//
|
|
||||||
// NetworkServersView.swift
|
|
||||||
// SimpleX (iOS)
|
|
||||||
//
|
|
||||||
// Created by Evgeny on 02/08/2022.
|
|
||||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SimpleXChat
|
|
||||||
|
|
||||||
private enum NetworkAlert: Identifiable {
|
|
||||||
case error(err: String)
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
switch self {
|
|
||||||
case let .error(err): return "error \(err)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NetworkAndServers: View {
|
|
||||||
@EnvironmentObject var m: ChatModel
|
|
||||||
@EnvironmentObject var theme: AppTheme
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
List {
|
|
||||||
Section {
|
|
||||||
NavigationLink {
|
|
||||||
ProtocolServersView(serverProtocol: .smp)
|
|
||||||
.navigationTitle("Your SMP servers")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text("Message servers")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink {
|
|
||||||
ProtocolServersView(serverProtocol: .xftp)
|
|
||||||
.navigationTitle("Your XFTP servers")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text("Media & file servers")
|
|
||||||
}
|
|
||||||
|
|
||||||
NavigationLink {
|
|
||||||
AdvancedNetworkSettings()
|
|
||||||
.navigationTitle("Advanced settings")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text("Advanced network settings")
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Messages & files")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) {
|
|
||||||
NavigationLink {
|
|
||||||
RTCServers()
|
|
||||||
.navigationTitle("Your ICE servers")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
} label: {
|
|
||||||
Text("WebRTC ICE servers")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section(header: Text("Network connection").foregroundColor(theme.colors.secondary)) {
|
|
||||||
HStack {
|
|
||||||
Text(m.networkInfo.networkType.text)
|
|
||||||
Spacer()
|
|
||||||
Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NetworkServersView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NetworkAndServers()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
//
|
||||||
|
// ConditionsWebView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Stanislav Dmitrenko on 26.11.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import WebKit
|
||||||
|
|
||||||
|
struct ConditionsWebView: UIViewRepresentable {
|
||||||
|
@State var html: String
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@State var pageLoaded = false
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> WKWebView {
|
||||||
|
let view = WKWebView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isOpaque = false
|
||||||
|
view.navigationDelegate = context.coordinator
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
|
// just to make sure that even if updateUIView will not be called for any reason, the page
|
||||||
|
// will be rendered anyway
|
||||||
|
if !pageLoaded {
|
||||||
|
loadPage(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ view: WKWebView, context: Context) {
|
||||||
|
loadPage(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPage(_ webView: WKWebView) {
|
||||||
|
let styles = """
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
color: \(theme.colors.onBackground.toHTMLHex());
|
||||||
|
font-family: Helvetica;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: \(theme.colors.primary.toHTMLHex());
|
||||||
|
}
|
||||||
|
code, pre {
|
||||||
|
font-family: Menlo;
|
||||||
|
background: \(theme.colors.secondary.opacity(theme.colors.isLight ? 0.2 : 0.3).toHTMLHex());
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
let head = "<head><meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, user-scalable=no'>\(styles)</head>"
|
||||||
|
webView.loadHTMLString(head + html, baseURL: nil)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
pageLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Cordinator {
|
||||||
|
Cordinator()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Cordinator: NSObject, WKNavigationDelegate {
|
||||||
|
func webView(_ webView: WKWebView,
|
||||||
|
decidePolicyFor navigationAction: WKNavigationAction,
|
||||||
|
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||||
|
|
||||||
|
guard let url = navigationAction.request.url else { return decisionHandler(.allow) }
|
||||||
|
|
||||||
|
switch navigationAction.navigationType {
|
||||||
|
case .linkActivated:
|
||||||
|
decisionHandler(.cancel)
|
||||||
|
if url.absoluteString.starts(with: "https://simplex.chat/contact#") {
|
||||||
|
ChatModel.shared.appOpenUrl = url
|
||||||
|
} else {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
decisionHandler(.allow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,435 @@
|
|||||||
|
//
|
||||||
|
// NetworkServersView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Evgeny on 02/08/2022.
|
||||||
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
private enum NetworkAlert: Identifiable {
|
||||||
|
case error(err: String)
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case let .error(err): return "error \(err)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum NetworkAndServersSheet: Identifiable {
|
||||||
|
case showConditions
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .showConditions: return "showConditions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NetworkAndServers: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject var m: ChatModel
|
||||||
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@EnvironmentObject var ss: SaveableSettings
|
||||||
|
@State private var sheetItem: NetworkAndServersSheet? = nil
|
||||||
|
@State private var justOpened = true
|
||||||
|
@State private var showSaveDialog = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
List {
|
||||||
|
let conditionsAction = m.conditions.conditionsAction
|
||||||
|
let anyOperatorEnabled = ss.servers.userServers.contains(where: { $0.operator?.enabled ?? false })
|
||||||
|
Section {
|
||||||
|
ForEach(ss.servers.userServers.enumerated().map { $0 }, id: \.element.id) { idx, userOperatorServers in
|
||||||
|
if let serverOperator = userOperatorServers.operator {
|
||||||
|
serverOperatorView(idx, serverOperator)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let conditionsAction = conditionsAction, anyOperatorEnabled {
|
||||||
|
conditionsButton(conditionsAction)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Preset servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
switch conditionsAction {
|
||||||
|
case let .review(_, deadline, _):
|
||||||
|
if let deadline = deadline, anyOperatorEnabled {
|
||||||
|
Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
if let idx = ss.servers.userServers.firstIndex(where: { $0.operator == nil }) {
|
||||||
|
NavigationLink {
|
||||||
|
YourServersView(
|
||||||
|
userServers: $ss.servers.userServers,
|
||||||
|
serverErrors: $ss.servers.serverErrors,
|
||||||
|
operatorIndex: idx
|
||||||
|
)
|
||||||
|
.navigationTitle("Your servers")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("Your servers")
|
||||||
|
|
||||||
|
if ss.servers.userServers[idx] != ss.servers.currUserServers[idx] {
|
||||||
|
Spacer()
|
||||||
|
unsavedChangesIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
AdvancedNetworkSettings()
|
||||||
|
.navigationTitle("Advanced settings")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
Text("Advanced network settings")
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Messages & files")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button("Save servers", action: { saveServers($ss.servers.currUserServers, $ss.servers.userServers) })
|
||||||
|
.disabled(!serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors))
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalServersError(ss.servers.serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
} else if !ss.servers.serverErrors.isEmpty {
|
||||||
|
ServersErrorView(errStr: NSLocalizedString("Errors in servers configuration.", comment: "servers error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) {
|
||||||
|
NavigationLink {
|
||||||
|
RTCServers()
|
||||||
|
.navigationTitle("Your ICE servers")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
Text("WebRTC ICE servers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: Text("Network connection").foregroundColor(theme.colors.secondary)) {
|
||||||
|
HStack {
|
||||||
|
Text(m.networkInfo.networkType.text)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "circle.fill").foregroundColor(m.networkInfo.online ? .green : .red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
// this condition is needed to prevent re-setting the servers when exiting single server view
|
||||||
|
if justOpened {
|
||||||
|
do {
|
||||||
|
ss.servers.currUserServers = try await getUserServers()
|
||||||
|
ss.servers.userServers = ss.servers.currUserServers
|
||||||
|
ss.servers.serverErrors = []
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error loading servers", comment: "alert title"),
|
||||||
|
message: responseError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
justOpened = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
|
if serversCanBeSaved(ss.servers.currUserServers, ss.servers.userServers, ss.servers.serverErrors) {
|
||||||
|
showSaveDialog = true
|
||||||
|
} else {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) {
|
||||||
|
Button("Save") {
|
||||||
|
saveServers($ss.servers.currUserServers, $ss.servers.userServers)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
Button("Exit without saving") { dismiss() }
|
||||||
|
}
|
||||||
|
.sheet(item: $sheetItem) { item in
|
||||||
|
switch item {
|
||||||
|
case .showConditions:
|
||||||
|
UsageConditionsView(
|
||||||
|
currUserServers: $ss.servers.currUserServers,
|
||||||
|
userServers: $ss.servers.userServers
|
||||||
|
)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverOperatorView(_ operatorIndex: Int, _ serverOperator: ServerOperator) -> some View {
|
||||||
|
NavigationLink() {
|
||||||
|
OperatorView(
|
||||||
|
currUserServers: $ss.servers.currUserServers,
|
||||||
|
userServers: $ss.servers.userServers,
|
||||||
|
serverErrors: $ss.servers.serverErrors,
|
||||||
|
operatorIndex: operatorIndex,
|
||||||
|
useOperator: serverOperator.enabled
|
||||||
|
)
|
||||||
|
.navigationBarTitle("\(serverOperator.tradeName) servers")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(serverOperator.logo(colorScheme))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.grayscale(serverOperator.enabled ? 0.0 : 1.0)
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
|
Text(serverOperator.tradeName)
|
||||||
|
.foregroundColor(serverOperator.enabled ? theme.colors.onBackground : theme.colors.secondary)
|
||||||
|
|
||||||
|
if ss.servers.userServers[operatorIndex] != ss.servers.currUserServers[operatorIndex] {
|
||||||
|
Spacer()
|
||||||
|
unsavedChangesIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func unsavedChangesIndicator() -> some View {
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.symbolRenderingMode(.monochrome)
|
||||||
|
.frame(maxWidth: 24, maxHeight: 24, alignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func conditionsButton(_ conditionsAction: UsageConditionsAction) -> some View {
|
||||||
|
Button {
|
||||||
|
sheetItem = .showConditions
|
||||||
|
} label: {
|
||||||
|
switch conditionsAction {
|
||||||
|
case .review:
|
||||||
|
Text("Review conditions")
|
||||||
|
case .accepted:
|
||||||
|
Text("Accepted conditions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UsageConditionsView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Binding var currUserServers: [UserOperatorServers]
|
||||||
|
@Binding var userServers: [UserOperatorServers]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
HStack {
|
||||||
|
Text("Conditions of use").font(.largeTitle).bold()
|
||||||
|
Spacer()
|
||||||
|
conditionsLinkButton()
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
.padding(.top)
|
||||||
|
|
||||||
|
switch ChatModel.shared.conditions.conditionsAction {
|
||||||
|
|
||||||
|
case .none:
|
||||||
|
ConditionsTextView()
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
case let .review(operators, deadline, _):
|
||||||
|
Text("Conditions will be accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
ConditionsTextView()
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
acceptConditionsButton(operators.map { $0.operatorId })
|
||||||
|
if let deadline = deadline {
|
||||||
|
Text("Conditions will be automatically accepted for enabled operators on: \(conditionsTimestamp(deadline)).")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.font(.footnote)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .center)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
|
||||||
|
case let .accepted(operators):
|
||||||
|
Text("Conditions are accepted for the operator(s): **\(operators.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
ConditionsTextView()
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func acceptConditionsButton(_ operatorIds: [Int64]) -> some View {
|
||||||
|
Button {
|
||||||
|
acceptForOperators(operatorIds)
|
||||||
|
} label: {
|
||||||
|
Text("Accept conditions")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptForOperators(_ operatorIds: [Int64]) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
|
||||||
|
let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.conditions = r
|
||||||
|
updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators)
|
||||||
|
updateOperatorsConditionsAcceptance($userServers, r.serverOperators)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error accepting conditions", comment: "alert title"),
|
||||||
|
message: responseError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateServers_(_ userServers: Binding<[UserOperatorServers]>, _ serverErrors: Binding<[UserServersError]>) {
|
||||||
|
let userServersToValidate = userServers.wrappedValue
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let errs = try await validateServers(userServers: userServersToValidate)
|
||||||
|
await MainActor.run {
|
||||||
|
serverErrors.wrappedValue = errs
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("validateServers error: \(responseError(error))")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serversCanBeSaved(
|
||||||
|
_ currUserServers: [UserOperatorServers],
|
||||||
|
_ userServers: [UserOperatorServers],
|
||||||
|
_ serverErrors: [UserServersError]
|
||||||
|
) -> Bool {
|
||||||
|
return userServers != currUserServers && serverErrors.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServersErrorView: View {
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
var errStr: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.circle")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(errStr)
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalServersError(_ serverErrors: [UserServersError]) -> String? {
|
||||||
|
for err in serverErrors {
|
||||||
|
if let errStr = err.globalError {
|
||||||
|
return errStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalSMPServersError(_ serverErrors: [UserServersError]) -> String? {
|
||||||
|
for err in serverErrors {
|
||||||
|
if let errStr = err.globalSMPError {
|
||||||
|
return errStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func globalXFTPServersError(_ serverErrors: [UserServersError]) -> String? {
|
||||||
|
for err in serverErrors {
|
||||||
|
if let errStr = err.globalXFTPError {
|
||||||
|
return errStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findDuplicateHosts(_ serverErrors: [UserServersError]) -> Set<String> {
|
||||||
|
let duplicateHostsList = serverErrors.compactMap { err in
|
||||||
|
if case let .duplicateServer(_, _, duplicateHost) = err {
|
||||||
|
return duplicateHost
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Set(duplicateHostsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveServers(_ currUserServers: Binding<[UserOperatorServers]>, _ userServers: Binding<[UserOperatorServers]>) {
|
||||||
|
let userServersToSave = userServers.wrappedValue
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await setUserServers(userServers: userServersToSave)
|
||||||
|
// Get updated servers to learn new server ids (otherwise it messes up delete of newly added and saved servers)
|
||||||
|
do {
|
||||||
|
let updatedServers = try await getUserServers()
|
||||||
|
let updatedOperators = try await getServerOperators()
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.conditions = updatedOperators
|
||||||
|
currUserServers.wrappedValue = updatedServers
|
||||||
|
userServers.wrappedValue = updatedServers
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("saveServers getUserServers error: \(responseError(error))")
|
||||||
|
await MainActor.run {
|
||||||
|
currUserServers.wrappedValue = userServersToSave
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("saveServers setUserServers error: \(responseError(error))")
|
||||||
|
await MainActor.run {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error saving servers", comment: "alert title"),
|
||||||
|
message: responseError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateOperatorsConditionsAcceptance(_ usvs: Binding<[UserOperatorServers]>, _ updatedOperators: [ServerOperator]) {
|
||||||
|
for i in 0..<usvs.wrappedValue.count {
|
||||||
|
if let updatedOperator = updatedOperators.first(where: { $0.operatorId == usvs.wrappedValue[i].operator?.operatorId }) {
|
||||||
|
usvs.wrappedValue[i].operator?.conditionsAcceptance = updatedOperator.conditionsAcceptance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NetworkServersView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NetworkAndServers()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
//
|
||||||
|
// NewServerView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 13.11.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
struct NewServerView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Binding var userServers: [UserOperatorServers]
|
||||||
|
@Binding var serverErrors: [UserServersError]
|
||||||
|
@State private var serverToEdit: UserServer = .empty
|
||||||
|
@State private var showTestFailure = false
|
||||||
|
@State private var testing = false
|
||||||
|
@State private var testFailure: ProtocolTestFailure?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
customServer()
|
||||||
|
if testing {
|
||||||
|
ProgressView().scaleEffect(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modifier(BackButton(disabled: Binding.constant(false)) {
|
||||||
|
addServer(serverToEdit, $userServers, $serverErrors, dismiss)
|
||||||
|
})
|
||||||
|
.alert(isPresented: $showTestFailure) {
|
||||||
|
Alert(
|
||||||
|
title: Text("Server test failed!"),
|
||||||
|
message: Text(testFailure?.localizedDescription ?? "")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Possibly refactor - similar functions in ProtocolServerView
|
||||||
|
private func customServer() -> some View {
|
||||||
|
VStack {
|
||||||
|
let serverAddress = parseServerAddress(serverToEdit.server)
|
||||||
|
let valid = serverAddress?.valid == true
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
TextEditor(text: $serverToEdit.server)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.allowsTightening(true)
|
||||||
|
.lineLimit(10)
|
||||||
|
.frame(height: 144)
|
||||||
|
.padding(-6)
|
||||||
|
} header: {
|
||||||
|
HStack {
|
||||||
|
Text("Your server address")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
if !valid {
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useServerSection(valid)
|
||||||
|
if valid {
|
||||||
|
Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) {
|
||||||
|
MutableQRCode(uri: $serverToEdit.server)
|
||||||
|
.listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func useServerSection(_ valid: Bool) -> some View {
|
||||||
|
Section(header: Text("Use server").foregroundColor(theme.colors.secondary)) {
|
||||||
|
HStack {
|
||||||
|
Button("Test server") {
|
||||||
|
testing = true
|
||||||
|
serverToEdit.tested = nil
|
||||||
|
Task {
|
||||||
|
if let f = await testServerConnection(server: $serverToEdit) {
|
||||||
|
showTestFailure = true
|
||||||
|
testFailure = f
|
||||||
|
}
|
||||||
|
await MainActor.run { testing = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disabled(!valid || testing)
|
||||||
|
Spacer()
|
||||||
|
showTestStatus(server: serverToEdit)
|
||||||
|
}
|
||||||
|
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverProtocolAndOperator(_ server: UserServer, _ userServers: [UserOperatorServers]) -> (ServerProtocol, ServerOperator?)? {
|
||||||
|
if let serverAddress = parseServerAddress(server.server) {
|
||||||
|
let serverProtocol = serverAddress.serverProtocol
|
||||||
|
let hostnames = serverAddress.hostnames
|
||||||
|
let matchingOperator = userServers.compactMap { $0.operator }.first { op in
|
||||||
|
op.serverDomains.contains { domain in
|
||||||
|
hostnames.contains { hostname in
|
||||||
|
hostname.hasSuffix(domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (serverProtocol, matchingOperator)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addServer(
|
||||||
|
_ server: UserServer,
|
||||||
|
_ userServers: Binding<[UserOperatorServers]>,
|
||||||
|
_ serverErrors: Binding<[UserServersError]>,
|
||||||
|
_ dismiss: DismissAction
|
||||||
|
) {
|
||||||
|
if let (serverProtocol, matchingOperator) = serverProtocolAndOperator(server, userServers.wrappedValue) {
|
||||||
|
if let i = userServers.wrappedValue.firstIndex(where: { $0.operator?.operatorId == matchingOperator?.operatorId }) {
|
||||||
|
switch serverProtocol {
|
||||||
|
case .smp: userServers[i].wrappedValue.smpServers.append(server)
|
||||||
|
case .xftp: userServers[i].wrappedValue.xftpServers.append(server)
|
||||||
|
}
|
||||||
|
validateServers_(userServers, serverErrors)
|
||||||
|
dismiss()
|
||||||
|
if let op = matchingOperator {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Operator server", comment: "alert title"),
|
||||||
|
message: String.localizedStringWithFormat(NSLocalizedString("Server added to operator %@.", comment: "alert message"), op.tradeName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else { // Shouldn't happen
|
||||||
|
dismiss()
|
||||||
|
showAlert(NSLocalizedString("Error adding server", comment: "alert title"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dismiss()
|
||||||
|
if server.server.trimmingCharacters(in: .whitespaces) != "" {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Invalid server address!", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Check server address and try again.", comment: "alert title")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NewServerView(
|
||||||
|
userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]),
|
||||||
|
serverErrors: Binding.constant([])
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,606 @@
|
|||||||
|
//
|
||||||
|
// OperatorView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by spaced4ndy on 28.10.2024.
|
||||||
|
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
import Ink
|
||||||
|
|
||||||
|
struct OperatorView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.editMode) private var editMode
|
||||||
|
@Binding var currUserServers: [UserOperatorServers]
|
||||||
|
@Binding var userServers: [UserOperatorServers]
|
||||||
|
@Binding var serverErrors: [UserServersError]
|
||||||
|
var operatorIndex: Int
|
||||||
|
@State var useOperator: Bool
|
||||||
|
@State private var useOperatorToggleReset: Bool = false
|
||||||
|
@State private var showConditionsSheet: Bool = false
|
||||||
|
@State private var selectedServer: String? = nil
|
||||||
|
@State private var testing = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
operatorView()
|
||||||
|
.opacity(testing ? 0.4 : 1)
|
||||||
|
.overlay {
|
||||||
|
if testing {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(2)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.allowsHitTesting(!testing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func operatorView() -> some View {
|
||||||
|
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||||
|
VStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
infoViewLink()
|
||||||
|
useOperatorToggle()
|
||||||
|
} header: {
|
||||||
|
Text("Operator")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
} else {
|
||||||
|
switch (userServers[operatorIndex].operator_.conditionsAcceptance) {
|
||||||
|
case let .accepted(acceptedAt):
|
||||||
|
if let acceptedAt = acceptedAt {
|
||||||
|
Text("Conditions accepted on: \(conditionsTimestamp(acceptedAt)).")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
case let .required(deadline):
|
||||||
|
if userServers[operatorIndex].operator_.enabled, let deadline = deadline {
|
||||||
|
Text("Conditions will be accepted on: \(conditionsTimestamp(deadline)).")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if userServers[operatorIndex].operator_.enabled {
|
||||||
|
if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty {
|
||||||
|
Section {
|
||||||
|
Toggle("To receive", isOn: $userServers[operatorIndex].operator_.smpRoles.storage)
|
||||||
|
.onChange(of: userServers[operatorIndex].operator_.smpRoles.storage) { _ in
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
Toggle("For private routing", isOn: $userServers[operatorIndex].operator_.smpRoles.proxy)
|
||||||
|
.onChange(of: userServers[operatorIndex].operator_.smpRoles.proxy) { _ in
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Use for messages")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalSMPServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset servers can't be deleted
|
||||||
|
if !userServers[operatorIndex].smpServers.filter({ $0.preset }).isEmpty {
|
||||||
|
Section {
|
||||||
|
ForEach($userServers[operatorIndex].smpServers) { srv in
|
||||||
|
if srv.wrappedValue.preset {
|
||||||
|
ProtocolServerViewLink(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
duplicateHosts: duplicateHosts,
|
||||||
|
server: srv,
|
||||||
|
serverProtocol: .smp,
|
||||||
|
backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers",
|
||||||
|
selectedServer: $selectedServer
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Message servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalSMPServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
} else {
|
||||||
|
Text("The servers for new connections of your current chat profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.lineLimit(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userServers[operatorIndex].smpServers.filter({ !$0.preset && !$0.deleted }).isEmpty {
|
||||||
|
Section {
|
||||||
|
ForEach($userServers[operatorIndex].smpServers) { srv in
|
||||||
|
if !srv.wrappedValue.preset && !srv.wrappedValue.deleted {
|
||||||
|
ProtocolServerViewLink(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
duplicateHosts: duplicateHosts,
|
||||||
|
server: srv,
|
||||||
|
serverProtocol: .smp,
|
||||||
|
backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers",
|
||||||
|
selectedServer: $selectedServer
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
deleteSMPServer($userServers, operatorIndex, indexSet)
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Added message servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty {
|
||||||
|
Section {
|
||||||
|
Toggle("To send", isOn: $userServers[operatorIndex].operator_.xftpRoles.storage)
|
||||||
|
.onChange(of: userServers[operatorIndex].operator_.xftpRoles.storage) { _ in
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Use for files")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalXFTPServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset servers can't be deleted
|
||||||
|
if !userServers[operatorIndex].xftpServers.filter({ $0.preset }).isEmpty {
|
||||||
|
Section {
|
||||||
|
ForEach($userServers[operatorIndex].xftpServers) { srv in
|
||||||
|
if srv.wrappedValue.preset {
|
||||||
|
ProtocolServerViewLink(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
duplicateHosts: duplicateHosts,
|
||||||
|
server: srv,
|
||||||
|
serverProtocol: .xftp,
|
||||||
|
backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers",
|
||||||
|
selectedServer: $selectedServer
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Media & file servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalXFTPServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
} else {
|
||||||
|
Text("The servers for new files of your current chat profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.lineLimit(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userServers[operatorIndex].xftpServers.filter({ !$0.preset && !$0.deleted }).isEmpty {
|
||||||
|
Section {
|
||||||
|
ForEach($userServers[operatorIndex].xftpServers) { srv in
|
||||||
|
if !srv.wrappedValue.preset && !srv.wrappedValue.deleted {
|
||||||
|
ProtocolServerViewLink(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
duplicateHosts: duplicateHosts,
|
||||||
|
server: srv,
|
||||||
|
serverProtocol: .xftp,
|
||||||
|
backLabel: "\(userServers[operatorIndex].operator_.tradeName) servers",
|
||||||
|
selectedServer: $selectedServer
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
deleteXFTPServer($userServers, operatorIndex, indexSet)
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Added media & file servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
TestServersButton(
|
||||||
|
smpServers: $userServers[operatorIndex].smpServers,
|
||||||
|
xftpServers: $userServers[operatorIndex].xftpServers,
|
||||||
|
testing: $testing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
if (
|
||||||
|
!userServers[operatorIndex].smpServers.filter({ !$0.preset && !$0.deleted }).isEmpty ||
|
||||||
|
!userServers[operatorIndex].xftpServers.filter({ !$0.preset && !$0.deleted }).isEmpty
|
||||||
|
) {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showConditionsSheet, onDismiss: onUseToggleSheetDismissed) {
|
||||||
|
SingleOperatorUsageConditionsView(
|
||||||
|
currUserServers: $currUserServers,
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
operatorIndex: operatorIndex
|
||||||
|
)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func infoViewLink() -> some View {
|
||||||
|
NavigationLink() {
|
||||||
|
OperatorInfoView(serverOperator: userServers[operatorIndex].operator_)
|
||||||
|
.navigationBarTitle("Network operator")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
Image(userServers[operatorIndex].operator_.largeLogo(colorScheme))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.grayscale(userServers[operatorIndex].operator_.enabled ? 0.0 : 1.0)
|
||||||
|
.frame(height: 40)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func useOperatorToggle() -> some View {
|
||||||
|
Toggle("Use servers", isOn: $useOperator)
|
||||||
|
.onChange(of: useOperator) { useOperatorToggle in
|
||||||
|
if useOperatorToggleReset {
|
||||||
|
useOperatorToggleReset = false
|
||||||
|
} else if useOperatorToggle {
|
||||||
|
switch userServers[operatorIndex].operator_.conditionsAcceptance {
|
||||||
|
case .accepted:
|
||||||
|
userServers[operatorIndex].operator_.enabled = true
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
case let .required(deadline):
|
||||||
|
if deadline == nil {
|
||||||
|
showConditionsSheet = true
|
||||||
|
} else {
|
||||||
|
userServers[operatorIndex].operator_.enabled = true
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
userServers[operatorIndex].operator_.enabled = false
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func onUseToggleSheetDismissed() {
|
||||||
|
if useOperator && !userServers[operatorIndex].operator_.conditionsAcceptance.usageAllowed {
|
||||||
|
useOperatorToggleReset = true
|
||||||
|
useOperator = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionsTimestamp(_ date: Date) -> String {
|
||||||
|
let localDateFormatter = DateFormatter()
|
||||||
|
localDateFormatter.dateStyle = .medium
|
||||||
|
localDateFormatter.timeStyle = .none
|
||||||
|
return localDateFormatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OperatorInfoView: View {
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.colorScheme) var colorScheme: ColorScheme
|
||||||
|
var serverOperator: ServerOperator
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Image(serverOperator.largeLogo(colorScheme))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(height: 48)
|
||||||
|
if let legalName = serverOperator.legalName {
|
||||||
|
Text(legalName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
ForEach(serverOperator.info.description, id: \.self) { d in
|
||||||
|
Text(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
Link("\(serverOperator.info.website)", destination: URL(string: serverOperator.info.website)!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConditionsTextView: View {
|
||||||
|
@State private var conditionsData: (UsageConditions, String?, UsageConditions?)?
|
||||||
|
@State private var failedToLoad: Bool = false
|
||||||
|
@State private var conditionsHTML: String? = nil
|
||||||
|
|
||||||
|
let defaultConditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md"
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
viewBody()
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.task {
|
||||||
|
do {
|
||||||
|
let conditions = try await getUsageConditions()
|
||||||
|
let conditionsText = conditions.1
|
||||||
|
let parentLink = "https://github.com/simplex-chat/simplex-chat/blob/\(conditions.0.conditionsCommit)"
|
||||||
|
let preparedText: String?
|
||||||
|
if let conditionsText {
|
||||||
|
let prepared = prepareMarkdown(conditionsText.trimmingCharacters(in: .whitespacesAndNewlines), parentLink)
|
||||||
|
conditionsHTML = MarkdownParser().html(from: prepared)
|
||||||
|
preparedText = prepared
|
||||||
|
} else {
|
||||||
|
preparedText = nil
|
||||||
|
}
|
||||||
|
conditionsData = (conditions.0, preparedText, conditions.2)
|
||||||
|
} catch let error {
|
||||||
|
logger.error("ConditionsTextView getUsageConditions error: \(responseError(error))")
|
||||||
|
failedToLoad = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Diff rendering
|
||||||
|
@ViewBuilder private func viewBody() -> some View {
|
||||||
|
if let (usageConditions, _, _) = conditionsData {
|
||||||
|
if let conditionsHTML {
|
||||||
|
ConditionsWebView(html: conditionsHTML)
|
||||||
|
.padding(6)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color(uiColor: .secondarySystemGroupedBackground))
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let conditionsLink = "https://github.com/simplex-chat/simplex-chat/blob/\(usageConditions.conditionsCommit)/PRIVACY.md"
|
||||||
|
conditionsLinkView(conditionsLink)
|
||||||
|
}
|
||||||
|
} else if failedToLoad {
|
||||||
|
conditionsLinkView(defaultConditionsLink)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func conditionsLinkView(_ conditionsLink: String) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
Text("Current conditions text couldn't be loaded, you can review conditions via this link:")
|
||||||
|
Link(destination: URL(string: conditionsLink)!) {
|
||||||
|
Text(conditionsLink)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func prepareMarkdown(_ text: String, _ parentLink: String) -> String {
|
||||||
|
let localLinkRegex = try! NSRegularExpression(pattern: "\\[([^\\(]*)\\]\\(#.*\\)")
|
||||||
|
let h1Regex = try! NSRegularExpression(pattern: "^# ")
|
||||||
|
var text = localLinkRegex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "$1")
|
||||||
|
text = h1Regex.stringByReplacingMatches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count), withTemplate: "")
|
||||||
|
return text
|
||||||
|
.replacingOccurrences(of: "](/", with: "](\(parentLink)/")
|
||||||
|
.replacingOccurrences(of: "](./", with: "](\(parentLink)/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SingleOperatorUsageConditionsView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Binding var currUserServers: [UserOperatorServers]
|
||||||
|
@Binding var userServers: [UserOperatorServers]
|
||||||
|
@Binding var serverErrors: [UserServersError]
|
||||||
|
var operatorIndex: Int
|
||||||
|
@State private var usageConditionsNavLinkActive: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
viewBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func viewBody() -> some View {
|
||||||
|
let operatorsWithConditionsAccepted = ChatModel.shared.conditions.serverOperators.filter { $0.conditionsAcceptance.conditionsAccepted }
|
||||||
|
if case .accepted = userServers[operatorIndex].operator_.conditionsAcceptance {
|
||||||
|
|
||||||
|
// In current UI implementation this branch doesn't get shown - as conditions can't be opened from inside operator once accepted
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
Group {
|
||||||
|
viewHeader()
|
||||||
|
ConditionsTextView()
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
|
} else if !operatorsWithConditionsAccepted.isEmpty {
|
||||||
|
|
||||||
|
NavigationView {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
Group {
|
||||||
|
viewHeader()
|
||||||
|
Text("Conditions are already accepted for following operator(s): **\(operatorsWithConditionsAccepted.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
Text("Same conditions will apply to operator **\(userServers[operatorIndex].operator_.legalName_)**.")
|
||||||
|
conditionsAppliedToOtherOperatorsText()
|
||||||
|
usageConditionsNavLinkButton()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
acceptConditionsButton()
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
Group {
|
||||||
|
viewHeader()
|
||||||
|
Text("To use the servers of **\(userServers[operatorIndex].operator_.legalName_)**, accept conditions of use.")
|
||||||
|
conditionsAppliedToOtherOperatorsText()
|
||||||
|
ConditionsTextView()
|
||||||
|
acceptConditionsButton()
|
||||||
|
.padding(.bottom)
|
||||||
|
.padding(.bottom)
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func viewHeader() -> some View {
|
||||||
|
HStack {
|
||||||
|
Text("Use \(userServers[operatorIndex].operator_.tradeName)").font(.largeTitle).bold()
|
||||||
|
Spacer()
|
||||||
|
conditionsLinkButton()
|
||||||
|
}
|
||||||
|
.padding(.top)
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func conditionsAppliedToOtherOperatorsText() -> some View {
|
||||||
|
let otherOperatorsToApply = ChatModel.shared.conditions.serverOperators.filter {
|
||||||
|
$0.enabled &&
|
||||||
|
!$0.conditionsAcceptance.conditionsAccepted &&
|
||||||
|
$0.operatorId != userServers[operatorIndex].operator_.operatorId
|
||||||
|
}
|
||||||
|
if !otherOperatorsToApply.isEmpty {
|
||||||
|
Text("These conditions will also apply for: **\(otherOperatorsToApply.map { $0.legalName_ }.joined(separator: ", "))**.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func acceptConditionsButton() -> some View {
|
||||||
|
let operatorIds = ChatModel.shared.conditions.serverOperators
|
||||||
|
.filter {
|
||||||
|
$0.operatorId == userServers[operatorIndex].operator_.operatorId || // Opened operator
|
||||||
|
($0.enabled && !$0.conditionsAcceptance.conditionsAccepted) // Other enabled operators with conditions not accepted
|
||||||
|
}
|
||||||
|
.map { $0.operatorId }
|
||||||
|
Button {
|
||||||
|
acceptForOperators(operatorIds, operatorIndex)
|
||||||
|
} label: {
|
||||||
|
Text("Accept conditions")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptForOperators(_ operatorIds: [Int64], _ operatorIndexToEnable: Int) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let conditionsId = ChatModel.shared.conditions.currentConditions.conditionsId
|
||||||
|
let r = try await acceptConditions(conditionsId: conditionsId, operatorIds: operatorIds)
|
||||||
|
await MainActor.run {
|
||||||
|
ChatModel.shared.conditions = r
|
||||||
|
updateOperatorsConditionsAcceptance($currUserServers, r.serverOperators)
|
||||||
|
updateOperatorsConditionsAcceptance($userServers, r.serverOperators)
|
||||||
|
userServers[operatorIndexToEnable].operator?.enabled = true
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
await MainActor.run {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error accepting conditions", comment: "alert title"),
|
||||||
|
message: responseError(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func usageConditionsNavLinkButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Button {
|
||||||
|
usageConditionsNavLinkActive = true
|
||||||
|
} label: {
|
||||||
|
Text("View conditions")
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(isActive: $usageConditionsNavLinkActive) {
|
||||||
|
usageConditionsDestinationView()
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func usageConditionsDestinationView() -> some View {
|
||||||
|
ConditionsTextView()
|
||||||
|
.padding()
|
||||||
|
.padding(.bottom)
|
||||||
|
.navigationTitle("Conditions of use")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.toolbar { ToolbarItem(placement: .navigationBarTrailing, content: conditionsLinkButton) }
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func conditionsLinkButton() -> some View {
|
||||||
|
let commit = ChatModel.shared.conditions.currentConditions.conditionsCommit
|
||||||
|
let mdUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/\(commit)/PRIVACY.md") ?? conditionsURL
|
||||||
|
return Menu {
|
||||||
|
Link(destination: mdUrl) {
|
||||||
|
Label("Open conditions", systemImage: "doc")
|
||||||
|
}
|
||||||
|
if let commitUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/commit/\(commit)") {
|
||||||
|
Link(destination: commitUrl) {
|
||||||
|
Label("Open changes", systemImage: "ellipsis")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "arrow.up.right.circle")
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 20)
|
||||||
|
.padding(2)
|
||||||
|
.contentShape(Circle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
OperatorView(
|
||||||
|
currUserServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]),
|
||||||
|
userServers: Binding.constant([UserOperatorServers.sampleData1, UserOperatorServers.sampleDataNilOperator]),
|
||||||
|
serverErrors: Binding.constant([]),
|
||||||
|
operatorIndex: 1,
|
||||||
|
useOperator: ServerOperator.sampleData1.enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
+40
-17
@@ -12,15 +12,15 @@ import SimpleXChat
|
|||||||
struct ProtocolServerView: View {
|
struct ProtocolServerView: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
let serverProtocol: ServerProtocol
|
@Binding var userServers: [UserOperatorServers]
|
||||||
@Binding var server: ServerCfg
|
@Binding var serverErrors: [UserServersError]
|
||||||
@State var serverToEdit: ServerCfg
|
@Binding var server: UserServer
|
||||||
|
@State var serverToEdit: UserServer
|
||||||
|
var backLabel: LocalizedStringKey
|
||||||
@State private var showTestFailure = false
|
@State private var showTestFailure = false
|
||||||
@State private var testing = false
|
@State private var testing = false
|
||||||
@State private var testFailure: ProtocolTestFailure?
|
@State private var testFailure: ProtocolTestFailure?
|
||||||
|
|
||||||
var proto: String { serverProtocol.rawValue.uppercased() }
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
if server.preset {
|
if server.preset {
|
||||||
@@ -32,9 +32,33 @@ struct ProtocolServerView: View {
|
|||||||
ProgressView().scaleEffect(2)
|
ProgressView().scaleEffect(2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.modifier(BackButton(label: "Your \(proto) servers", disabled: Binding.constant(false)) {
|
.modifier(BackButton(label: backLabel, disabled: Binding.constant(false)) {
|
||||||
server = serverToEdit
|
if let (serverToEditProtocol, serverToEditOperator) = serverProtocolAndOperator(serverToEdit, userServers),
|
||||||
dismiss()
|
let (serverProtocol, serverOperator) = serverProtocolAndOperator(server, userServers) {
|
||||||
|
if serverToEditProtocol != serverProtocol {
|
||||||
|
dismiss()
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error updating server", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Server protocol changed.", comment: "alert title")
|
||||||
|
)
|
||||||
|
} else if serverToEditOperator != serverOperator {
|
||||||
|
dismiss()
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Error updating server", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Server operator changed.", comment: "alert title")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
server = serverToEdit
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dismiss()
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Invalid server address!", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Check server address and try again.", comment: "alert title")
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.alert(isPresented: $showTestFailure) {
|
.alert(isPresented: $showTestFailure) {
|
||||||
Alert(
|
Alert(
|
||||||
@@ -62,7 +86,7 @@ struct ProtocolServerView: View {
|
|||||||
private func customServer() -> some View {
|
private func customServer() -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
let serverAddress = parseServerAddress(serverToEdit.server)
|
let serverAddress = parseServerAddress(serverToEdit.server)
|
||||||
let valid = serverAddress?.valid == true && serverAddress?.serverProtocol == serverProtocol
|
let valid = serverAddress?.valid == true
|
||||||
List {
|
List {
|
||||||
Section {
|
Section {
|
||||||
TextEditor(text: $serverToEdit.server)
|
TextEditor(text: $serverToEdit.server)
|
||||||
@@ -112,10 +136,7 @@ struct ProtocolServerView: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
showTestStatus(server: serverToEdit)
|
showTestStatus(server: serverToEdit)
|
||||||
}
|
}
|
||||||
let useForNewDisabled = serverToEdit.tested != true && !serverToEdit.preset
|
|
||||||
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
|
Toggle("Use for new connections", isOn: $serverToEdit.enabled)
|
||||||
.disabled(useForNewDisabled)
|
|
||||||
.foregroundColor(useForNewDisabled ? theme.colors.secondary : theme.colors.onBackground)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,7 +163,7 @@ struct BackButton: ViewModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder func showTestStatus(server: ServerCfg) -> some View {
|
@ViewBuilder func showTestStatus(server: UserServer) -> some View {
|
||||||
switch server.tested {
|
switch server.tested {
|
||||||
case .some(true):
|
case .some(true):
|
||||||
Image(systemName: "checkmark")
|
Image(systemName: "checkmark")
|
||||||
@@ -155,7 +176,7 @@ struct BackButton: ViewModifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailure? {
|
func testServerConnection(server: Binding<UserServer>) async -> ProtocolTestFailure? {
|
||||||
do {
|
do {
|
||||||
let r = try await testProtoServer(server: server.wrappedValue.server)
|
let r = try await testProtoServer(server: server.wrappedValue.server)
|
||||||
switch r {
|
switch r {
|
||||||
@@ -178,9 +199,11 @@ func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailu
|
|||||||
struct ProtocolServerView_Previews: PreviewProvider {
|
struct ProtocolServerView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ProtocolServerView(
|
ProtocolServerView(
|
||||||
serverProtocol: .smp,
|
userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]),
|
||||||
server: Binding.constant(ServerCfg.sampleData.custom),
|
serverErrors: Binding.constant([]),
|
||||||
serverToEdit: ServerCfg.sampleData.custom
|
server: Binding.constant(UserServer.sampleData.custom),
|
||||||
|
serverToEdit: UserServer.sampleData.custom,
|
||||||
|
backLabel: "Your SMP servers"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
//
|
||||||
|
// ProtocolServersView.swift
|
||||||
|
// SimpleX (iOS)
|
||||||
|
//
|
||||||
|
// Created by Evgeny on 15/11/2022.
|
||||||
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import SimpleXChat
|
||||||
|
|
||||||
|
private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")!
|
||||||
|
|
||||||
|
struct YourServersView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject private var m: ChatModel
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Environment(\.editMode) private var editMode
|
||||||
|
@Binding var userServers: [UserOperatorServers]
|
||||||
|
@Binding var serverErrors: [UserServersError]
|
||||||
|
var operatorIndex: Int
|
||||||
|
@State private var selectedServer: String? = nil
|
||||||
|
@State private var showAddServer = false
|
||||||
|
@State private var newServerNavLinkActive = false
|
||||||
|
@State private var showScanProtoServer = false
|
||||||
|
@State private var testing = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
yourServersView()
|
||||||
|
.opacity(testing ? 0.4 : 1)
|
||||||
|
.overlay {
|
||||||
|
if testing {
|
||||||
|
ProgressView()
|
||||||
|
.scaleEffect(2)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.allowsHitTesting(!testing)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private func yourServersView() -> some View {
|
||||||
|
let duplicateHosts = findDuplicateHosts(serverErrors)
|
||||||
|
List {
|
||||||
|
if !userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty {
|
||||||
|
Section {
|
||||||
|
ForEach($userServers[operatorIndex].smpServers) { srv in
|
||||||
|
if !srv.wrappedValue.deleted {
|
||||||
|
ProtocolServerViewLink(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
duplicateHosts: duplicateHosts,
|
||||||
|
server: srv,
|
||||||
|
serverProtocol: .smp,
|
||||||
|
backLabel: "Your servers",
|
||||||
|
selectedServer: $selectedServer
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
deleteSMPServer($userServers, operatorIndex, indexSet)
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Message servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalSMPServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
} else {
|
||||||
|
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.lineLimit(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty {
|
||||||
|
Section {
|
||||||
|
ForEach($userServers[operatorIndex].xftpServers) { srv in
|
||||||
|
if !srv.wrappedValue.deleted {
|
||||||
|
ProtocolServerViewLink(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
duplicateHosts: duplicateHosts,
|
||||||
|
server: srv,
|
||||||
|
serverProtocol: .xftp,
|
||||||
|
backLabel: "Your servers",
|
||||||
|
selectedServer: $selectedServer
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete { indexSet in
|
||||||
|
deleteXFTPServer($userServers, operatorIndex, indexSet)
|
||||||
|
validateServers_($userServers, $serverErrors)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Media & file servers")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalXFTPServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
} else {
|
||||||
|
Text("The servers for new files of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
.lineLimit(10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
ZStack {
|
||||||
|
Button("Add server") {
|
||||||
|
showAddServer = true
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(isActive: $newServerNavLinkActive) {
|
||||||
|
newServerDestinationView()
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
if let errStr = globalServersError(serverErrors) {
|
||||||
|
ServersErrorView(errStr: errStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
TestServersButton(
|
||||||
|
smpServers: $userServers[operatorIndex].smpServers,
|
||||||
|
xftpServers: $userServers[operatorIndex].xftpServers,
|
||||||
|
testing: $testing
|
||||||
|
)
|
||||||
|
howToButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
if (
|
||||||
|
!userServers[operatorIndex].smpServers.filter({ !$0.deleted }).isEmpty ||
|
||||||
|
!userServers[operatorIndex].xftpServers.filter({ !$0.deleted }).isEmpty
|
||||||
|
) {
|
||||||
|
EditButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) {
|
||||||
|
Button("Enter server manually") { newServerNavLinkActive = true }
|
||||||
|
Button("Scan server QR code") { showScanProtoServer = true }
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showScanProtoServer) {
|
||||||
|
ScanProtocolServer(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors
|
||||||
|
)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func newServerDestinationView() -> some View {
|
||||||
|
NewServerView(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors
|
||||||
|
)
|
||||||
|
.navigationTitle("New server")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
}
|
||||||
|
|
||||||
|
func howToButton() -> some View {
|
||||||
|
Button {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.shared.open(howToUrl)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Text("How to use your servers")
|
||||||
|
Image(systemName: "arrow.up.right.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProtocolServerViewLink: View {
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Binding var userServers: [UserOperatorServers]
|
||||||
|
@Binding var serverErrors: [UserServersError]
|
||||||
|
var duplicateHosts: Set<String>
|
||||||
|
@Binding var server: UserServer
|
||||||
|
var serverProtocol: ServerProtocol
|
||||||
|
var backLabel: LocalizedStringKey
|
||||||
|
@Binding var selectedServer: String?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let proto = serverProtocol.rawValue.uppercased()
|
||||||
|
|
||||||
|
NavigationLink(tag: server.id, selection: $selectedServer) {
|
||||||
|
ProtocolServerView(
|
||||||
|
userServers: $userServers,
|
||||||
|
serverErrors: $serverErrors,
|
||||||
|
server: $server,
|
||||||
|
serverToEdit: server,
|
||||||
|
backLabel: backLabel
|
||||||
|
)
|
||||||
|
.navigationBarTitle("\(proto) server")
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
let address = parseServerAddress(server.server)
|
||||||
|
HStack {
|
||||||
|
Group {
|
||||||
|
if let address = address {
|
||||||
|
if !address.valid || address.serverProtocol != serverProtocol {
|
||||||
|
invalidServer()
|
||||||
|
} else if address.hostnames.contains(where: duplicateHosts.contains) {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
} else if !server.enabled {
|
||||||
|
Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary)
|
||||||
|
} else {
|
||||||
|
showTestStatus(server: server)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
invalidServer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 16, alignment: .center)
|
||||||
|
.padding(.trailing, 4)
|
||||||
|
|
||||||
|
let v = Text(address?.hostnames.first ?? server.server).lineLimit(1)
|
||||||
|
if server.enabled {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
v.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func invalidServer() -> some View {
|
||||||
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteSMPServer(
|
||||||
|
_ userServers: Binding<[UserOperatorServers]>,
|
||||||
|
_ operatorServersIndex: Int,
|
||||||
|
_ serverIndexSet: IndexSet
|
||||||
|
) {
|
||||||
|
if let idx = serverIndexSet.first {
|
||||||
|
let server = userServers[operatorServersIndex].wrappedValue.smpServers[idx]
|
||||||
|
if server.serverId == nil {
|
||||||
|
userServers[operatorServersIndex].wrappedValue.smpServers.remove(at: idx)
|
||||||
|
} else {
|
||||||
|
var updatedServer = server
|
||||||
|
updatedServer.deleted = true
|
||||||
|
userServers[operatorServersIndex].wrappedValue.smpServers[idx] = updatedServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteXFTPServer(
|
||||||
|
_ userServers: Binding<[UserOperatorServers]>,
|
||||||
|
_ operatorServersIndex: Int,
|
||||||
|
_ serverIndexSet: IndexSet
|
||||||
|
) {
|
||||||
|
if let idx = serverIndexSet.first {
|
||||||
|
let server = userServers[operatorServersIndex].wrappedValue.xftpServers[idx]
|
||||||
|
if server.serverId == nil {
|
||||||
|
userServers[operatorServersIndex].wrappedValue.xftpServers.remove(at: idx)
|
||||||
|
} else {
|
||||||
|
var updatedServer = server
|
||||||
|
updatedServer.deleted = true
|
||||||
|
userServers[operatorServersIndex].wrappedValue.xftpServers[idx] = updatedServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestServersButton: View {
|
||||||
|
@Binding var smpServers: [UserServer]
|
||||||
|
@Binding var xftpServers: [UserServer]
|
||||||
|
@Binding var testing: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button("Test servers", action: testServers)
|
||||||
|
.disabled(testing || allServersDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var allServersDisabled: Bool {
|
||||||
|
smpServers.allSatisfy { !$0.enabled } && xftpServers.allSatisfy { !$0.enabled }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func testServers() {
|
||||||
|
resetTestStatus()
|
||||||
|
testing = true
|
||||||
|
Task {
|
||||||
|
let fs = await runServersTest()
|
||||||
|
await MainActor.run {
|
||||||
|
testing = false
|
||||||
|
if !fs.isEmpty {
|
||||||
|
let msg = fs.map { (srv, f) in
|
||||||
|
"\(srv): \(f.localizedDescription)"
|
||||||
|
}.joined(separator: "\n")
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Tests failed!", comment: "alert title"),
|
||||||
|
message: String.localizedStringWithFormat(NSLocalizedString("Some servers failed the test:\n%@", comment: "alert message"), msg)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetTestStatus() {
|
||||||
|
for i in 0..<smpServers.count {
|
||||||
|
if smpServers[i].enabled {
|
||||||
|
smpServers[i].tested = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..<xftpServers.count {
|
||||||
|
if xftpServers[i].enabled {
|
||||||
|
xftpServers[i].tested = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func runServersTest() async -> [String: ProtocolTestFailure] {
|
||||||
|
var fs: [String: ProtocolTestFailure] = [:]
|
||||||
|
for i in 0..<smpServers.count {
|
||||||
|
if smpServers[i].enabled {
|
||||||
|
if let f = await testServerConnection(server: $smpServers[i]) {
|
||||||
|
fs[serverHostname(smpServers[i].server)] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..<xftpServers.count {
|
||||||
|
if xftpServers[i].enabled {
|
||||||
|
if let f = await testServerConnection(server: $xftpServers[i]) {
|
||||||
|
fs[serverHostname(xftpServers[i].server)] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct YourServersView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
YourServersView(
|
||||||
|
userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]),
|
||||||
|
serverErrors: Binding.constant([]),
|
||||||
|
operatorIndex: 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-15
@@ -12,8 +12,8 @@ import CodeScanner
|
|||||||
|
|
||||||
struct ScanProtocolServer: View {
|
struct ScanProtocolServer: View {
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
@Binding var servers: [ServerCfg]
|
@Binding var userServers: [UserOperatorServers]
|
||||||
@State private var showAddressError = false
|
@Binding var serverErrors: [UserServersError]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
@@ -28,23 +28,14 @@ struct ScanProtocolServer: View {
|
|||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.alert(isPresented: $showAddressError) {
|
|
||||||
Alert(
|
|
||||||
title: Text("Invalid server address!"),
|
|
||||||
message: Text("Check server address and try again.")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
func processQRCode(_ resp: Result<ScanResult, ScanError>) {
|
||||||
switch resp {
|
switch resp {
|
||||||
case let .success(r):
|
case let .success(r):
|
||||||
if parseServerAddress(r.string) != nil {
|
var server: UserServer = .empty
|
||||||
servers.append(ServerCfg(server: r.string, preset: false, tested: nil, enabled: false))
|
server.server = r.string
|
||||||
dismiss()
|
addServer(server, $userServers, $serverErrors, dismiss)
|
||||||
} else {
|
|
||||||
showAddressError = true
|
|
||||||
}
|
|
||||||
case let .failure(e):
|
case let .failure(e):
|
||||||
logger.error("ScanProtocolServer.processQRCode QR code error: \(e.localizedDescription)")
|
logger.error("ScanProtocolServer.processQRCode QR code error: \(e.localizedDescription)")
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -54,6 +45,9 @@ struct ScanProtocolServer: View {
|
|||||||
|
|
||||||
struct ScanProtocolServer_Previews: PreviewProvider {
|
struct ScanProtocolServer_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ScanProtocolServer(servers: Binding.constant([]))
|
ScanProtocolServer(
|
||||||
|
userServers: Binding.constant([UserOperatorServers.sampleDataNilOperator]),
|
||||||
|
serverErrors: Binding.constant([])
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,9 +237,17 @@ struct NotificationsView: View {
|
|||||||
|
|
||||||
func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
|
func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .off: return "**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)."
|
case .off: return "**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app."
|
||||||
case .periodic: return "**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have."
|
case .periodic: return "**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata."
|
||||||
case .instant: return "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from."
|
case .instant: return "**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ntfModeShortDescription(_ mode: NotificationsMode) -> LocalizedStringKey {
|
||||||
|
switch mode {
|
||||||
|
case .off: return "Check messages when allowed."
|
||||||
|
case .periodic: return "Check messages every 20 min."
|
||||||
|
case .instant: return "E2E encrypted notifications."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,359 +0,0 @@
|
|||||||
//
|
|
||||||
// ProtocolServersView.swift
|
|
||||||
// SimpleX (iOS)
|
|
||||||
//
|
|
||||||
// Created by Evgeny on 15/11/2022.
|
|
||||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SimpleXChat
|
|
||||||
|
|
||||||
private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")!
|
|
||||||
|
|
||||||
struct ProtocolServersView: View {
|
|
||||||
@Environment(\.dismiss) var dismiss: DismissAction
|
|
||||||
@EnvironmentObject private var m: ChatModel
|
|
||||||
@EnvironmentObject var theme: AppTheme
|
|
||||||
@Environment(\.editMode) private var editMode
|
|
||||||
let serverProtocol: ServerProtocol
|
|
||||||
@State private var currServers: [ServerCfg] = []
|
|
||||||
@State private var presetServers: [ServerCfg] = []
|
|
||||||
@State private var configuredServers: [ServerCfg] = []
|
|
||||||
@State private var otherServers: [ServerCfg] = []
|
|
||||||
@State private var selectedServer: String? = nil
|
|
||||||
@State private var showAddServer = false
|
|
||||||
@State private var showScanProtoServer = false
|
|
||||||
@State private var justOpened = true
|
|
||||||
@State private var testing = false
|
|
||||||
@State private var alert: ServerAlert? = nil
|
|
||||||
@State private var showSaveDialog = false
|
|
||||||
|
|
||||||
var proto: String { serverProtocol.rawValue.uppercased() }
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
ZStack {
|
|
||||||
protocolServersView()
|
|
||||||
if testing {
|
|
||||||
ProgressView().scaleEffect(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ServerAlert: Identifiable {
|
|
||||||
case testsFailed(failures: [String: ProtocolTestFailure])
|
|
||||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
switch self {
|
|
||||||
case .testsFailed: return "testsFailed"
|
|
||||||
case let .error(title, _): return "error \(title)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func protocolServersView() -> some View {
|
|
||||||
List {
|
|
||||||
if !configuredServers.isEmpty {
|
|
||||||
Section {
|
|
||||||
ForEach($configuredServers) { srv in
|
|
||||||
protocolServerView(srv)
|
|
||||||
}
|
|
||||||
.onMove { indexSet, offset in
|
|
||||||
configuredServers.move(fromOffsets: indexSet, toOffset: offset)
|
|
||||||
}
|
|
||||||
.onDelete { indexSet in
|
|
||||||
configuredServers.remove(atOffsets: indexSet)
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Configured \(proto) servers")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
} footer: {
|
|
||||||
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
.lineLimit(10)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !otherServers.isEmpty {
|
|
||||||
Section {
|
|
||||||
ForEach($otherServers) { srv in
|
|
||||||
protocolServerView(srv)
|
|
||||||
}
|
|
||||||
.onMove { indexSet, offset in
|
|
||||||
otherServers.move(fromOffsets: indexSet, toOffset: offset)
|
|
||||||
}
|
|
||||||
.onDelete { indexSet in
|
|
||||||
otherServers.remove(atOffsets: indexSet)
|
|
||||||
}
|
|
||||||
} header: {
|
|
||||||
Text("Other \(proto) servers")
|
|
||||||
.foregroundColor(theme.colors.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Button("Add server") {
|
|
||||||
showAddServer = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Button("Reset") { partitionServers(currServers) }
|
|
||||||
.disabled(Set(allServers) == Set(currServers) || testing)
|
|
||||||
Button("Test servers", action: testServers)
|
|
||||||
.disabled(testing || allServersDisabled)
|
|
||||||
Button("Save servers", action: saveServers)
|
|
||||||
.disabled(saveDisabled)
|
|
||||||
howToButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toolbar { EditButton() }
|
|
||||||
.confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) {
|
|
||||||
Button("Enter server manually") {
|
|
||||||
otherServers.append(ServerCfg.empty)
|
|
||||||
selectedServer = allServers.last?.id
|
|
||||||
}
|
|
||||||
Button("Scan server QR code") { showScanProtoServer = true }
|
|
||||||
Button("Add preset servers", action: addAllPresets)
|
|
||||||
.disabled(hasAllPresets())
|
|
||||||
}
|
|
||||||
.sheet(isPresented: $showScanProtoServer) {
|
|
||||||
ScanProtocolServer(servers: $otherServers)
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
}
|
|
||||||
.modifier(BackButton(disabled: Binding.constant(false)) {
|
|
||||||
if saveDisabled {
|
|
||||||
dismiss()
|
|
||||||
justOpened = false
|
|
||||||
} else {
|
|
||||||
showSaveDialog = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) {
|
|
||||||
Button("Save") {
|
|
||||||
saveServers()
|
|
||||||
dismiss()
|
|
||||||
justOpened = false
|
|
||||||
}
|
|
||||||
Button("Exit without saving") { dismiss() }
|
|
||||||
}
|
|
||||||
.alert(item: $alert) { a in
|
|
||||||
switch a {
|
|
||||||
case let .testsFailed(fs):
|
|
||||||
let msg = fs.map { (srv, f) in
|
|
||||||
"\(srv): \(f.localizedDescription)"
|
|
||||||
}.joined(separator: "\n")
|
|
||||||
return Alert(
|
|
||||||
title: Text("Tests failed!"),
|
|
||||||
message: Text("Some servers failed the test:\n" + msg)
|
|
||||||
)
|
|
||||||
case .error:
|
|
||||||
return Alert(
|
|
||||||
title: Text("Error")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
// this condition is needed to prevent re-setting the servers when exiting single server view
|
|
||||||
if justOpened {
|
|
||||||
do {
|
|
||||||
let r = try getUserProtoServers(serverProtocol)
|
|
||||||
currServers = r.protoServers
|
|
||||||
presetServers = r.presetServers
|
|
||||||
partitionServers(currServers)
|
|
||||||
} catch let error {
|
|
||||||
alert = .error(
|
|
||||||
title: "Error loading \(proto) servers",
|
|
||||||
error: "Error: \(responseError(error))"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
justOpened = false
|
|
||||||
} else {
|
|
||||||
partitionServers(allServers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func partitionServers(_ servers: [ServerCfg]) {
|
|
||||||
configuredServers = servers.filter { $0.preset || $0.enabled }
|
|
||||||
otherServers = servers.filter { !($0.preset || $0.enabled) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var allServers: [ServerCfg] {
|
|
||||||
configuredServers + otherServers
|
|
||||||
}
|
|
||||||
|
|
||||||
private var saveDisabled: Bool {
|
|
||||||
allServers.isEmpty ||
|
|
||||||
Set(allServers) == Set(currServers) ||
|
|
||||||
testing ||
|
|
||||||
!allServers.allSatisfy { srv in
|
|
||||||
if let address = parseServerAddress(srv.server) {
|
|
||||||
return uniqueAddress(srv, address)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} ||
|
|
||||||
allServersDisabled
|
|
||||||
}
|
|
||||||
|
|
||||||
private var allServersDisabled: Bool {
|
|
||||||
allServers.allSatisfy { !$0.enabled }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
|
|
||||||
let srv = server.wrappedValue
|
|
||||||
return NavigationLink(tag: srv.id, selection: $selectedServer) {
|
|
||||||
ProtocolServerView(
|
|
||||||
serverProtocol: serverProtocol,
|
|
||||||
server: server,
|
|
||||||
serverToEdit: srv
|
|
||||||
)
|
|
||||||
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
|
|
||||||
.modifier(ThemedBackground(grouped: true))
|
|
||||||
.navigationBarTitleDisplayMode(.large)
|
|
||||||
} label: {
|
|
||||||
let address = parseServerAddress(srv.server)
|
|
||||||
HStack {
|
|
||||||
Group {
|
|
||||||
if let address = address {
|
|
||||||
if !address.valid || address.serverProtocol != serverProtocol {
|
|
||||||
invalidServer()
|
|
||||||
} else if !uniqueAddress(srv, address) {
|
|
||||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
|
||||||
} else if !srv.enabled {
|
|
||||||
Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary)
|
|
||||||
} else {
|
|
||||||
showTestStatus(server: srv)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
invalidServer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 16, alignment: .center)
|
|
||||||
.padding(.trailing, 4)
|
|
||||||
|
|
||||||
let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1)
|
|
||||||
if srv.enabled {
|
|
||||||
v
|
|
||||||
} else {
|
|
||||||
v.foregroundColor(theme.colors.secondary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func howToButton() -> some View {
|
|
||||||
Button {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
UIApplication.shared.open(howToUrl)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Text("How to use your servers")
|
|
||||||
Image(systemName: "arrow.up.right.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func invalidServer() -> some View {
|
|
||||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uniqueAddress(_ s: ServerCfg, _ address: ServerAddress) -> Bool {
|
|
||||||
allServers.allSatisfy { srv in
|
|
||||||
address.hostnames.allSatisfy { host in
|
|
||||||
srv.id == s.id || !srv.server.contains(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hasAllPresets() -> Bool {
|
|
||||||
presetServers.allSatisfy { hasPreset($0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addAllPresets() {
|
|
||||||
for srv in presetServers {
|
|
||||||
if !hasPreset(srv) {
|
|
||||||
configuredServers.append(srv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func hasPreset(_ srv: ServerCfg) -> Bool {
|
|
||||||
allServers.contains(where: { $0.server == srv.server })
|
|
||||||
}
|
|
||||||
|
|
||||||
private func testServers() {
|
|
||||||
resetTestStatus()
|
|
||||||
testing = true
|
|
||||||
Task {
|
|
||||||
let fs = await runServersTest()
|
|
||||||
await MainActor.run {
|
|
||||||
testing = false
|
|
||||||
if !fs.isEmpty {
|
|
||||||
alert = .testsFailed(failures: fs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resetTestStatus() {
|
|
||||||
for i in 0..<configuredServers.count {
|
|
||||||
if configuredServers[i].enabled {
|
|
||||||
configuredServers[i].tested = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i in 0..<otherServers.count {
|
|
||||||
if otherServers[i].enabled {
|
|
||||||
otherServers[i].tested = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func runServersTest() async -> [String: ProtocolTestFailure] {
|
|
||||||
var fs: [String: ProtocolTestFailure] = [:]
|
|
||||||
for i in 0..<configuredServers.count {
|
|
||||||
if configuredServers[i].enabled {
|
|
||||||
if let f = await testServerConnection(server: $configuredServers[i]) {
|
|
||||||
fs[serverHostname(configuredServers[i].server)] = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i in 0..<otherServers.count {
|
|
||||||
if otherServers[i].enabled {
|
|
||||||
if let f = await testServerConnection(server: $otherServers[i]) {
|
|
||||||
fs[serverHostname(otherServers[i].server)] = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fs
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveServers() {
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await setUserProtoServers(serverProtocol, servers: allServers)
|
|
||||||
await MainActor.run {
|
|
||||||
currServers = allServers
|
|
||||||
editMode?.wrappedValue = .inactive
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
let err = responseError(error)
|
|
||||||
logger.error("saveServers setUserProtocolServers error: \(err)")
|
|
||||||
await MainActor.run {
|
|
||||||
alert = .error(
|
|
||||||
title: "Error saving \(proto) servers",
|
|
||||||
error: "Make sure \(proto) server addresses are in correct format, line separated and are not duplicated (\(responseError(error)))."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ProtocolServersView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
ProtocolServersView(serverProtocol: .smp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,6 +39,7 @@ let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
|
|||||||
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
||||||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||||
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
||||||
|
let DEFAULT_SHOULD_IMPORT_APP_SETTINGS = "shouldImportAppSettings"
|
||||||
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
|
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
|
||||||
let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted"
|
let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted"
|
||||||
let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
||||||
@@ -50,6 +51,7 @@ let DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
|||||||
let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
|
let DEFAULT_CHAT_ITEM_ROUNDNESS = "chatItemRoundness"
|
||||||
let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
|
let DEFAULT_CHAT_ITEM_TAIL = "chatItemTail"
|
||||||
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
|
let DEFAULT_ONE_HAND_UI_CARD_SHOWN = "oneHandUICardShown"
|
||||||
|
let DEFAULT_ADDRESS_CREATION_CARD_SHOWN = "addressCreationCardShown"
|
||||||
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
|
let DEFAULT_TOOLBAR_MATERIAL = "toolbarMaterial"
|
||||||
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
let DEFAULT_CONNECT_VIA_LINK_TAB = "connectViaLinkTab"
|
||||||
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
let DEFAULT_LIVE_MESSAGE_ALERT_SHOWN = "liveMessageAlertShown"
|
||||||
@@ -107,6 +109,7 @@ let appDefaults: [String: Any] = [
|
|||||||
DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness,
|
DEFAULT_CHAT_ITEM_ROUNDNESS: defaultChatItemRoundness,
|
||||||
DEFAULT_CHAT_ITEM_TAIL: true,
|
DEFAULT_CHAT_ITEM_TAIL: true,
|
||||||
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
|
DEFAULT_ONE_HAND_UI_CARD_SHOWN: false,
|
||||||
|
DEFAULT_ADDRESS_CREATION_CARD_SHOWN: false,
|
||||||
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
|
DEFAULT_TOOLBAR_MATERIAL: ToolbarMaterial.defaultMaterial,
|
||||||
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
DEFAULT_CONNECT_VIA_LINK_TAB: ConnectViaLinkTab.scan.rawValue,
|
||||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN: false,
|
||||||
@@ -135,6 +138,7 @@ let appDefaults: [String: Any] = [
|
|||||||
let hintDefaults = [
|
let hintDefaults = [
|
||||||
DEFAULT_LA_NOTICE_SHOWN,
|
DEFAULT_LA_NOTICE_SHOWN,
|
||||||
DEFAULT_ONE_HAND_UI_CARD_SHOWN,
|
DEFAULT_ONE_HAND_UI_CARD_SHOWN,
|
||||||
|
DEFAULT_ADDRESS_CREATION_CARD_SHOWN,
|
||||||
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN,
|
DEFAULT_LIVE_MESSAGE_ALERT_SHOWN,
|
||||||
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE,
|
DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE,
|
||||||
DEFAULT_SHOW_MUTE_PROFILE_ALERT,
|
DEFAULT_SHOW_MUTE_PROFILE_ALERT,
|
||||||
@@ -189,6 +193,8 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta
|
|||||||
let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE)
|
let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE)
|
||||||
let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE)
|
let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE)
|
||||||
|
|
||||||
|
/// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */
|
||||||
|
let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS)
|
||||||
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
||||||
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName)
|
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName)
|
||||||
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
|
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
|
||||||
@@ -356,7 +362,7 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
WhatsNewView(viaSettings: true)
|
WhatsNewView(viaSettings: true, updatedConditions: false)
|
||||||
.modifier(ThemedBackground())
|
.modifier(ThemedBackground())
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@@ -9,15 +9,94 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct UserAddressLearnMore: View {
|
struct UserAddressLearnMore: View {
|
||||||
|
@State var showCreateAddressButton = false
|
||||||
|
@State private var createAddressLinkActive = false
|
||||||
|
@State private var createOneTimeLinkActive = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
List {
|
VStack {
|
||||||
VStack(alignment: .leading, spacing: 18) {
|
List {
|
||||||
Text("You can share your address as a link or QR code - anybody can connect to you.")
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
Text("You won't lose your contacts if you later delete your address.")
|
(Text(Image(systemName: "envelope")).foregroundColor(.secondary) + textSpace + Text("Share address publicly").bold().font(.title2))
|
||||||
Text("When people request to connect, you can accept or reject it.")
|
Text("Share SimpleX address on social media.")
|
||||||
Text("Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).")
|
Text("You won't lose your contacts if you later delete your address.")
|
||||||
|
|
||||||
|
(Text(Image(systemName: "link.badge.plus")).foregroundColor(.secondary) + textSpace + Text("Share 1-time link with a friend").font(.title2).bold())
|
||||||
|
.padding(.top)
|
||||||
|
Text("1-time link can be used *with one contact only* - share in person or via any messenger.")
|
||||||
|
Text("You can set connection name, to remember who the link was shared with.")
|
||||||
|
|
||||||
|
if !showCreateAddressButton {
|
||||||
|
(Text(Image(systemName: "shield")).foregroundColor(.secondary) + textSpace + Text("Connection security").font(.title2).bold())
|
||||||
|
.padding(.top)
|
||||||
|
Text("SimpleX address and 1-time links are safe to share via any messenger.")
|
||||||
|
Text("To protect against your link being replaced, you can compare contact security codes.")
|
||||||
|
Text("Read more in [User Guide](https://simplex.chat/docs/guide/making-connections.html#comparison-of-1-time-invitation-links-and-simplex-contact-addresses).")
|
||||||
|
.padding(.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.clear)
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if showCreateAddressButton {
|
||||||
|
VStack {
|
||||||
|
addressCreationButton()
|
||||||
|
.padding(.bottom)
|
||||||
|
|
||||||
|
createOneTimeLinkButton()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxHeight: .infinity, alignment: .top)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addressCreationButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Button {
|
||||||
|
createAddressLinkActive = true
|
||||||
|
} label: {
|
||||||
|
Text("Create SimpleX address")
|
||||||
|
}
|
||||||
|
.buttonStyle(OnboardingButtonStyle())
|
||||||
|
|
||||||
|
NavigationLink(isActive: $createAddressLinkActive) {
|
||||||
|
UserAddressView(autoCreate: true)
|
||||||
|
.navigationTitle("SimpleX address")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createOneTimeLinkButton() -> some View {
|
||||||
|
ZStack {
|
||||||
|
Button {
|
||||||
|
createOneTimeLinkActive = true
|
||||||
|
} label: {
|
||||||
|
Text("Create 1-time link")
|
||||||
|
.font(.callout)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink(isActive: $createOneTimeLinkActive) {
|
||||||
|
NewChatView(selection: .invite)
|
||||||
|
.navigationTitle("New chat")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.frame(width: 1, height: 1)
|
||||||
|
.hidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,45 +15,31 @@ struct UserAddressView: View {
|
|||||||
@EnvironmentObject private var chatModel: ChatModel
|
@EnvironmentObject private var chatModel: ChatModel
|
||||||
@EnvironmentObject var theme: AppTheme
|
@EnvironmentObject var theme: AppTheme
|
||||||
@State var shareViaProfile = false
|
@State var shareViaProfile = false
|
||||||
|
@State var autoCreate = false
|
||||||
@State private var aas = AutoAcceptState()
|
@State private var aas = AutoAcceptState()
|
||||||
@State private var savedAAS = AutoAcceptState()
|
@State private var savedAAS = AutoAcceptState()
|
||||||
@State private var ignoreShareViaProfileChange = false
|
|
||||||
@State private var showMailView = false
|
@State private var showMailView = false
|
||||||
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
|
@State private var mailViewResult: Result<MFMailComposeResult, Error>? = nil
|
||||||
@State private var alert: UserAddressAlert?
|
@State private var alert: UserAddressAlert?
|
||||||
@State private var progressIndicator = false
|
@State private var progressIndicator = false
|
||||||
@FocusState private var keyboardVisible: Bool
|
|
||||||
|
|
||||||
private enum UserAddressAlert: Identifiable {
|
private enum UserAddressAlert: Identifiable {
|
||||||
case deleteAddress
|
case deleteAddress
|
||||||
case profileAddress(on: Bool)
|
|
||||||
case shareOnCreate
|
case shareOnCreate
|
||||||
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
case error(title: LocalizedStringKey, error: LocalizedStringKey?)
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .deleteAddress: return "deleteAddress"
|
case .deleteAddress: return "deleteAddress"
|
||||||
case let .profileAddress(on): return "profileAddress \(on)"
|
|
||||||
case .shareOnCreate: return "shareOnCreate"
|
case .shareOnCreate: return "shareOnCreate"
|
||||||
case let .error(title, _): return "error \(title)"
|
case let .error(title, _): return "error \(title)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
userAddressScrollView()
|
userAddressView()
|
||||||
.onDisappear {
|
|
||||||
if savedAAS != aas {
|
|
||||||
showAlert(
|
|
||||||
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
|
|
||||||
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
|
|
||||||
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
|
|
||||||
buttonAction: saveAAS,
|
|
||||||
cancelButton: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if progressIndicator {
|
if progressIndicator {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -67,22 +53,10 @@ struct UserAddressView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.onAppear {
|
||||||
|
if chatModel.userAddress == nil, autoCreate {
|
||||||
@Namespace private var bottomID
|
createAddress()
|
||||||
|
}
|
||||||
private func userAddressScrollView() -> some View {
|
|
||||||
ScrollViewReader { proxy in
|
|
||||||
userAddressView()
|
|
||||||
.onChange(of: keyboardVisible) { _ in
|
|
||||||
if keyboardVisible {
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
||||||
withAnimation {
|
|
||||||
proxy.scrollTo(bottomID, anchor: .top)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,14 +68,18 @@ struct UserAddressView: View {
|
|||||||
aas = AutoAcceptState(userAddress: userAddress)
|
aas = AutoAcceptState(userAddress: userAddress)
|
||||||
savedAAS = aas
|
savedAAS = aas
|
||||||
}
|
}
|
||||||
.onChange(of: aas.enable) { _ in
|
|
||||||
if !aas.enable { aas = AutoAcceptState() }
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Section {
|
Section {
|
||||||
createAddressButton()
|
createAddressButton()
|
||||||
} footer: {
|
} header: {
|
||||||
Text("Create an address to let people connect with you.")
|
Text("For social media")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
createOneTimeLinkButton()
|
||||||
|
} header: {
|
||||||
|
Text("Or to share privately")
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +95,8 @@ struct UserAddressView: View {
|
|||||||
title: Text("Delete address?"),
|
title: Text("Delete address?"),
|
||||||
message:
|
message:
|
||||||
shareViaProfile
|
shareViaProfile
|
||||||
? Text("All your contacts will remain connected. Profile update will be sent to your contacts.")
|
? Text("All your contacts will remain connected. Profile update will be sent to your contacts.")
|
||||||
: Text("All your contacts will remain connected."),
|
: Text("All your contacts will remain connected."),
|
||||||
primaryButton: .destructive(Text("Delete")) {
|
primaryButton: .destructive(Text("Delete")) {
|
||||||
progressIndicator = true
|
progressIndicator = true
|
||||||
Task {
|
Task {
|
||||||
@@ -128,7 +106,6 @@ struct UserAddressView: View {
|
|||||||
chatModel.userAddress = nil
|
chatModel.userAddress = nil
|
||||||
chatModel.updateUser(u)
|
chatModel.updateUser(u)
|
||||||
if shareViaProfile {
|
if shareViaProfile {
|
||||||
ignoreShareViaProfileChange = true
|
|
||||||
shareViaProfile = false
|
shareViaProfile = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,37 +118,12 @@ struct UserAddressView: View {
|
|||||||
}
|
}
|
||||||
}, secondaryButton: .cancel()
|
}, secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
case let .profileAddress(on):
|
|
||||||
if on {
|
|
||||||
return Alert(
|
|
||||||
title: Text("Share address with contacts?"),
|
|
||||||
message: Text("Profile update will be sent to your contacts."),
|
|
||||||
primaryButton: .default(Text("Share")) {
|
|
||||||
setProfileAddress(on)
|
|
||||||
}, secondaryButton: .cancel() {
|
|
||||||
ignoreShareViaProfileChange = true
|
|
||||||
shareViaProfile = !on
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return Alert(
|
|
||||||
title: Text("Stop sharing address?"),
|
|
||||||
message: Text("Profile update will be sent to your contacts."),
|
|
||||||
primaryButton: .default(Text("Stop sharing")) {
|
|
||||||
setProfileAddress(on)
|
|
||||||
}, secondaryButton: .cancel() {
|
|
||||||
ignoreShareViaProfileChange = true
|
|
||||||
shareViaProfile = !on
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case .shareOnCreate:
|
case .shareOnCreate:
|
||||||
return Alert(
|
return Alert(
|
||||||
title: Text("Share address with contacts?"),
|
title: Text("Share address with contacts?"),
|
||||||
message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."),
|
message: Text("Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts."),
|
||||||
primaryButton: .default(Text("Share")) {
|
primaryButton: .default(Text("Share")) {
|
||||||
setProfileAddress(true)
|
setProfileAddress($progressIndicator, true)
|
||||||
ignoreShareViaProfileChange = true
|
|
||||||
shareViaProfile = true
|
shareViaProfile = true
|
||||||
}, secondaryButton: .cancel()
|
}, secondaryButton: .cancel()
|
||||||
)
|
)
|
||||||
@@ -186,19 +138,39 @@ struct UserAddressView: View {
|
|||||||
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
SimpleXLinkQRCode(uri: userAddress.connReqContact)
|
||||||
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
|
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)")
|
||||||
shareQRCodeButton(userAddress)
|
shareQRCodeButton(userAddress)
|
||||||
if MFMailComposeViewController.canSendMail() {
|
// if MFMailComposeViewController.canSendMail() {
|
||||||
shareViaEmailButton(userAddress)
|
// shareViaEmailButton(userAddress)
|
||||||
|
// }
|
||||||
|
settingsRow("briefcase", color: theme.colors.secondary) {
|
||||||
|
Toggle("Business address", isOn: $aas.business)
|
||||||
|
.onChange(of: aas.business) { ba in
|
||||||
|
if ba {
|
||||||
|
aas.enable = true
|
||||||
|
aas.incognito = false
|
||||||
|
}
|
||||||
|
saveAAS($aas, $savedAAS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
shareWithContactsButton()
|
addressSettingsButton(userAddress)
|
||||||
autoAcceptToggle()
|
|
||||||
learnMoreButton()
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("Address")
|
Text("For social media")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
} footer: {
|
||||||
|
if aas.business {
|
||||||
|
Text("Add your team members to the conversations.")
|
||||||
|
.foregroundColor(theme.colors.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
createOneTimeLinkButton()
|
||||||
|
} header: {
|
||||||
|
Text("Or to share privately")
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
if aas.enable {
|
Section {
|
||||||
autoAcceptSection()
|
learnMoreButton()
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@@ -207,32 +179,47 @@ struct UserAddressView: View {
|
|||||||
Text("Your contacts will remain connected.")
|
Text("Your contacts will remain connected.")
|
||||||
.foregroundColor(theme.colors.secondary)
|
.foregroundColor(theme.colors.secondary)
|
||||||
}
|
}
|
||||||
.id(bottomID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createAddressButton() -> some View {
|
private func createAddressButton() -> some View {
|
||||||
Button {
|
Button {
|
||||||
progressIndicator = true
|
createAddress()
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let connReqContact = try await apiCreateUserAddress()
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
|
|
||||||
alert = .shareOnCreate
|
|
||||||
progressIndicator = false
|
|
||||||
}
|
|
||||||
} catch let error {
|
|
||||||
logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))")
|
|
||||||
let a = getErrorAlert(error, "Error creating address")
|
|
||||||
alert = .error(title: a.title, error: a.message)
|
|
||||||
await MainActor.run { progressIndicator = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Label("Create SimpleX address", systemImage: "qrcode")
|
Label("Create SimpleX address", systemImage: "qrcode")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createAddress() {
|
||||||
|
progressIndicator = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let connReqContact = try await apiCreateUserAddress()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
chatModel.userAddress = UserContactLink(connReqContact: connReqContact)
|
||||||
|
alert = .shareOnCreate
|
||||||
|
progressIndicator = false
|
||||||
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))")
|
||||||
|
let a = getErrorAlert(error, "Error creating address")
|
||||||
|
alert = .error(title: a.title, error: a.message)
|
||||||
|
await MainActor.run { progressIndicator = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createOneTimeLinkButton() -> some View {
|
||||||
|
NavigationLink {
|
||||||
|
NewChatView(selection: .invite)
|
||||||
|
.navigationTitle("New chat")
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
Label("Create 1-time link", systemImage: "link.badge.plus")
|
||||||
|
.foregroundColor(theme.colors.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func deleteAddressButton() -> some View {
|
private func deleteAddressButton() -> some View {
|
||||||
Button(role: .destructive) {
|
Button(role: .destructive) {
|
||||||
alert = .deleteAddress
|
alert = .deleteAddress
|
||||||
@@ -282,24 +269,140 @@ struct UserAddressView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func autoAcceptToggle() -> some View {
|
private func addressSettingsButton(_ userAddress: UserContactLink) -> some View {
|
||||||
settingsRow("checkmark", color: theme.colors.secondary) {
|
NavigationLink {
|
||||||
Toggle("Auto-accept", isOn: $aas.enable)
|
UserAddressSettingsView(shareViaProfile: $shareViaProfile)
|
||||||
.onChange(of: aas.enable) { _ in
|
.navigationTitle("Address settings")
|
||||||
saveAAS()
|
.navigationBarTitleDisplayMode(.large)
|
||||||
}
|
.modifier(ThemedBackground(grouped: true))
|
||||||
|
} label: {
|
||||||
|
Text("Address settings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func learnMoreButton() -> some View {
|
private func learnMoreButton() -> some View {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
UserAddressLearnMore()
|
UserAddressLearnMore()
|
||||||
.navigationTitle("SimpleX address")
|
.navigationTitle("Address or 1-time link?")
|
||||||
.modifier(ThemedBackground(grouped: true))
|
.modifier(ThemedBackground(grouped: true))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
} label: {
|
} label: {
|
||||||
settingsRow("info.circle", color: theme.colors.secondary) {
|
settingsRow("info.circle", color: theme.colors.secondary) {
|
||||||
Text("About SimpleX address")
|
Text("SimpleX address or 1-time link?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AutoAcceptState: Equatable {
|
||||||
|
var enable = false
|
||||||
|
var incognito = false
|
||||||
|
var business = false
|
||||||
|
var welcomeText = ""
|
||||||
|
|
||||||
|
init(enable: Bool = false, incognito: Bool = false, business: Bool = false, welcomeText: String = "") {
|
||||||
|
self.enable = enable
|
||||||
|
self.incognito = incognito
|
||||||
|
self.business = business
|
||||||
|
self.welcomeText = welcomeText
|
||||||
|
}
|
||||||
|
|
||||||
|
init(userAddress: UserContactLink) {
|
||||||
|
if let aa = userAddress.autoAccept {
|
||||||
|
enable = true
|
||||||
|
incognito = aa.acceptIncognito
|
||||||
|
business = aa.businessAddress
|
||||||
|
if let msg = aa.autoReply {
|
||||||
|
welcomeText = msg.text
|
||||||
|
} else {
|
||||||
|
welcomeText = ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enable = false
|
||||||
|
incognito = false
|
||||||
|
business = false
|
||||||
|
welcomeText = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var autoAccept: AutoAccept? {
|
||||||
|
if enable {
|
||||||
|
var autoReply: MsgContent? = nil
|
||||||
|
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if s != "" { autoReply = .text(s) }
|
||||||
|
return AutoAccept(businessAddress: business, acceptIncognito: incognito, autoReply: autoReply)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setProfileAddress(_ progressIndicator: Binding<Bool>, _ on: Bool) {
|
||||||
|
progressIndicator.wrappedValue = true
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
if let u = try await apiSetProfileAddress(on: on) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
ChatModel.shared.updateUser(u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await MainActor.run { progressIndicator.wrappedValue = false }
|
||||||
|
} catch let error {
|
||||||
|
logger.error("apiSetProfileAddress: \(responseError(error))")
|
||||||
|
await MainActor.run { progressIndicator.wrappedValue = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserAddressSettingsView: View {
|
||||||
|
@Environment(\.dismiss) var dismiss: DismissAction
|
||||||
|
@EnvironmentObject var theme: AppTheme
|
||||||
|
@Binding var shareViaProfile: Bool
|
||||||
|
@State private var aas = AutoAcceptState()
|
||||||
|
@State private var savedAAS = AutoAcceptState()
|
||||||
|
@State private var ignoreShareViaProfileChange = false
|
||||||
|
@State private var progressIndicator = false
|
||||||
|
@FocusState private var keyboardVisible: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
if let userAddress = ChatModel.shared.userAddress {
|
||||||
|
userAddressSettingsView()
|
||||||
|
.onAppear {
|
||||||
|
aas = AutoAcceptState(userAddress: userAddress)
|
||||||
|
savedAAS = aas
|
||||||
|
}
|
||||||
|
.onChange(of: aas.enable) { aasEnabled in
|
||||||
|
if !aasEnabled { aas = AutoAcceptState() }
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if savedAAS != aas {
|
||||||
|
showAlert(
|
||||||
|
title: NSLocalizedString("Auto-accept settings", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Settings were changed.", comment: "alert message"),
|
||||||
|
buttonTitle: NSLocalizedString("Save", comment: "alert button"),
|
||||||
|
buttonAction: { saveAAS($aas, $savedAAS) },
|
||||||
|
cancelButton: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(String("Error opening address settings"))
|
||||||
|
}
|
||||||
|
if progressIndicator {
|
||||||
|
ProgressView().scaleEffect(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func userAddressSettingsView() -> some View {
|
||||||
|
List {
|
||||||
|
Section {
|
||||||
|
shareWithContactsButton()
|
||||||
|
autoAcceptToggle().disabled(aas.business)
|
||||||
|
}
|
||||||
|
|
||||||
|
if aas.enable {
|
||||||
|
autoAcceptSection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,70 +414,70 @@ struct UserAddressView: View {
|
|||||||
if ignoreShareViaProfileChange {
|
if ignoreShareViaProfileChange {
|
||||||
ignoreShareViaProfileChange = false
|
ignoreShareViaProfileChange = false
|
||||||
} else {
|
} else {
|
||||||
alert = .profileAddress(on: on)
|
if on {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Share address with contacts?", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"),
|
||||||
|
actions: {[
|
||||||
|
UIAlertAction(
|
||||||
|
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||||
|
style: .default,
|
||||||
|
handler: { _ in
|
||||||
|
ignoreShareViaProfileChange = true
|
||||||
|
shareViaProfile = !on
|
||||||
|
}
|
||||||
|
),
|
||||||
|
UIAlertAction(
|
||||||
|
title: NSLocalizedString("Share", comment: "alert action"),
|
||||||
|
style: .default,
|
||||||
|
handler: { _ in
|
||||||
|
setProfileAddress($progressIndicator, on)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showAlert(
|
||||||
|
NSLocalizedString("Stop sharing address?", comment: "alert title"),
|
||||||
|
message: NSLocalizedString("Profile update will be sent to your contacts.", comment: "alert message"),
|
||||||
|
actions: {[
|
||||||
|
UIAlertAction(
|
||||||
|
title: NSLocalizedString("Cancel", comment: "alert action"),
|
||||||
|
style: .default,
|
||||||
|
handler: { _ in
|
||||||
|
ignoreShareViaProfileChange = true
|
||||||
|
shareViaProfile = !on
|
||||||
|
}
|
||||||
|
),
|
||||||
|
UIAlertAction(
|
||||||
|
title: NSLocalizedString("Stop sharing", comment: "alert action"),
|
||||||
|
style: .default,
|
||||||
|
handler: { _ in
|
||||||
|
setProfileAddress($progressIndicator, on)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setProfileAddress(_ on: Bool) {
|
private func autoAcceptToggle() -> some View {
|
||||||
progressIndicator = true
|
settingsRow("checkmark", color: theme.colors.secondary) {
|
||||||
Task {
|
Toggle("Auto-accept", isOn: $aas.enable)
|
||||||
do {
|
.onChange(of: aas.enable) { _ in
|
||||||
if let u = try await apiSetProfileAddress(on: on) {
|
saveAAS($aas, $savedAAS)
|
||||||
DispatchQueue.main.async {
|
|
||||||
chatModel.updateUser(u)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await MainActor.run { progressIndicator = false }
|
|
||||||
} catch let error {
|
|
||||||
logger.error("UserAddressView apiSetProfileAddress: \(responseError(error))")
|
|
||||||
await MainActor.run { progressIndicator = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct AutoAcceptState: Equatable {
|
|
||||||
var enable = false
|
|
||||||
var incognito = false
|
|
||||||
var welcomeText = ""
|
|
||||||
|
|
||||||
init(enable: Bool = false, incognito: Bool = false, welcomeText: String = "") {
|
|
||||||
self.enable = enable
|
|
||||||
self.incognito = incognito
|
|
||||||
self.welcomeText = welcomeText
|
|
||||||
}
|
|
||||||
|
|
||||||
init(userAddress: UserContactLink) {
|
|
||||||
if let aa = userAddress.autoAccept {
|
|
||||||
enable = true
|
|
||||||
incognito = aa.acceptIncognito
|
|
||||||
if let msg = aa.autoReply {
|
|
||||||
welcomeText = msg.text
|
|
||||||
} else {
|
|
||||||
welcomeText = ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
enable = false
|
|
||||||
incognito = false
|
|
||||||
welcomeText = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var autoAccept: AutoAccept? {
|
|
||||||
if enable {
|
|
||||||
var autoReply: MsgContent? = nil
|
|
||||||
let s = welcomeText.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if s != "" { autoReply = .text(s) }
|
|
||||||
return AutoAccept(acceptIncognito: incognito, autoReply: autoReply)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder private func autoAcceptSection() -> some View {
|
private func autoAcceptSection() -> some View {
|
||||||
Section {
|
Section {
|
||||||
acceptIncognitoToggle()
|
if !aas.business {
|
||||||
|
acceptIncognitoToggle()
|
||||||
|
}
|
||||||
welcomeMessageEditor()
|
welcomeMessageEditor()
|
||||||
saveAASButton()
|
saveAASButton()
|
||||||
.disabled(aas == savedAAS)
|
.disabled(aas == savedAAS)
|
||||||
@@ -414,22 +517,24 @@ struct UserAddressView: View {
|
|||||||
private func saveAASButton() -> some View {
|
private func saveAASButton() -> some View {
|
||||||
Button {
|
Button {
|
||||||
keyboardVisible = false
|
keyboardVisible = false
|
||||||
saveAAS()
|
saveAAS($aas, $savedAAS)
|
||||||
} label: {
|
} label: {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func saveAAS() {
|
private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAcceptState>) {
|
||||||
Task {
|
Task {
|
||||||
do {
|
do {
|
||||||
if let address = try await userAddressAutoAccept(aas.autoAccept) {
|
if let address = try await userAddressAutoAccept(aas.wrappedValue.autoAccept) {
|
||||||
chatModel.userAddress = address
|
await MainActor.run {
|
||||||
savedAAS = aas
|
ChatModel.shared.userAddress = address
|
||||||
|
savedAAS.wrappedValue = aas.wrappedValue
|
||||||
}
|
}
|
||||||
} catch let error {
|
|
||||||
logger.error("userAddressAutoAccept error: \(responseError(error))")
|
|
||||||
}
|
}
|
||||||
|
} catch let error {
|
||||||
|
logger.error("userAddressAutoAccept error: \(responseError(error))")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -187,23 +187,18 @@
|
|||||||
<target state="needs-translation">)</target>
|
<target state="needs-translation">)</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<target state="translated">** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بكم.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<target state="translated">** أنشئ رابطًا / رمز QR ** لتستخدمه جهة الاتصال الخاصة بك.</target>
|
<target state="translated">** أنشئ رابطًا / رمز QR ** لتستخدمه جهة الاتصال الخاصة بك.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<target state="translated">** المزيد من الخصوصية **: تحققوا من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديكم.</target>
|
<target state="translated">** المزيد من الخصوصية **: تحققوا من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديكم.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<target state="translated">** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامكم للتطبيق).</target>
|
<target state="translated">** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامكم للتطبيق).</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -217,8 +212,8 @@
|
|||||||
<target state="translated">** يرجى ملاحظة **: لن تتمكنوا من استعادة أو تغيير عبارة المرور إذا فقدتموها.</target>
|
<target state="translated">** يرجى ملاحظة **: لن تتمكنوا من استعادة أو تغيير عبارة المرور إذا فقدتموها.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<target state="translated">** موصى به **: يتم إرسال رمز الجهاز والإشعارات إلى خادم إشعارات SimpleX Chat ، ولكن ليس محتوى الرسالة أو حجمها أو مصدرها.</target>
|
<target state="translated">** موصى به **: يتم إرسال رمز الجهاز والإشعارات إلى خادم إشعارات SimpleX Chat ، ولكن ليس محتوى الرسالة أو حجمها أو مصدرها.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -1048,8 +1043,8 @@
|
|||||||
<source>Direct messages</source>
|
<source>Direct messages</source>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1064,8 +1059,8 @@
|
|||||||
<source>Disappearing messages are prohibited in this chat.</source>
|
<source>Disappearing messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disconnect" xml:space="preserve">
|
<trans-unit id="Disconnect" xml:space="preserve">
|
||||||
@@ -1428,16 +1423,16 @@
|
|||||||
<source>Group members can irreversibly delete sent messages.</source>
|
<source>Group members can irreversibly delete sent messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1528,8 +1523,8 @@
|
|||||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Import" xml:space="preserve">
|
<trans-unit id="Import" xml:space="preserve">
|
||||||
@@ -1625,8 +1620,8 @@
|
|||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -1926,8 +1921,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Onion hosts will not be used.</source>
|
<source>Onion hosts will not be used.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -1978,8 +1973,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Open user profiles</source>
|
<source>Open user profiles</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||||
@@ -2010,8 +2005,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -2590,8 +2585,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||||
@@ -2622,8 +2617,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>The microphone does not work when the app is in the background.</source>
|
<source>The microphone does not work when the app is in the background.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -2686,8 +2681,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -2860,8 +2855,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -2972,10 +2967,6 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>You can use markdown to format messages:</source>
|
<source>You can use markdown to format messages:</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
@@ -3752,8 +3743,8 @@ SimpleX servers cannot see your profile.</source>
|
|||||||
<source>%u messages skipped.</source>
|
<source>%u messages skipped.</source>
|
||||||
<target state="translated">%u تم تخطي الرسائل.</target>
|
<target state="translated">%u تم تخطي الرسائل.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add contact**: to create a new invitation link, or connect via a link you received." xml:space="preserve" approved="no">
|
<trans-unit id="**Create 1-time link**: to create and share a new invitation link." xml:space="preserve" approved="no">
|
||||||
<source>**Add contact**: to create a new invitation link, or connect via a link you received.</source>
|
<source>**Create 1-time link**: to create and share a new invitation link.</source>
|
||||||
<target state="translated">**إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم.</target>
|
<target state="translated">**إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
<trans-unit id="**Create group**: to create a new group." xml:space="preserve" approved="no">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
@@ -193,20 +193,16 @@
|
|||||||
<source>)</source>
|
<source>)</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||||
@@ -217,8 +213,8 @@
|
|||||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||||
@@ -1251,8 +1247,8 @@
|
|||||||
<source>Direct messages</source>
|
<source>Direct messages</source>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1271,8 +1267,8 @@
|
|||||||
<source>Disappearing messages are prohibited in this chat.</source>
|
<source>Disappearing messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappears at" xml:space="preserve">
|
<trans-unit id="Disappears at" xml:space="preserve">
|
||||||
@@ -1751,24 +1747,24 @@
|
|||||||
<source>Group links</source>
|
<source>Group links</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can add message reactions." xml:space="preserve">
|
<trans-unit id="Members can add message reactions." xml:space="preserve">
|
||||||
<source>Group members can add message reactions.</source>
|
<source>Members can add message reactions.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
|
<trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve">
|
||||||
<source>Group members can irreversibly delete sent messages.</source>
|
<source>Group members can irreversibly delete sent messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1899,8 +1895,8 @@
|
|||||||
<source>Immediately</source>
|
<source>Immediately</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Import" xml:space="preserve">
|
<trans-unit id="Import" xml:space="preserve">
|
||||||
@@ -2020,8 +2016,8 @@
|
|||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -2207,8 +2203,8 @@
|
|||||||
<source>Message reactions are prohibited in this chat.</source>
|
<source>Message reactions are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Message reactions are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Message reactions are prohibited." xml:space="preserve">
|
||||||
<source>Message reactions are prohibited in this group.</source>
|
<source>Message reactions are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Message text" xml:space="preserve">
|
<trans-unit id="Message text" xml:space="preserve">
|
||||||
@@ -2239,8 +2235,8 @@
|
|||||||
<source>Migration is completed</source>
|
<source>Migration is completed</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Migrations: %@" xml:space="preserve">
|
<trans-unit id="Migrations:" xml:space="preserve">
|
||||||
<source>Migrations: %@</source>
|
<source>Migrations:</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Moderate" xml:space="preserve">
|
<trans-unit id="Moderate" xml:space="preserve">
|
||||||
@@ -2409,8 +2405,8 @@
|
|||||||
<source>Onion hosts will not be used.</source>
|
<source>Onion hosts will not be used.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -2477,8 +2473,8 @@
|
|||||||
<source>Open user profiles</source>
|
<source>Open user profiles</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening database…" xml:space="preserve">
|
<trans-unit id="Opening database…" xml:space="preserve">
|
||||||
@@ -2537,8 +2533,8 @@
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -3373,8 +3369,8 @@
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||||
@@ -3418,8 +3414,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||||||
<source>The message will be marked as moderated for all members.</source>
|
<source>The message will be marked as moderated for all members.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -3490,8 +3486,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||||||
<source>To make a new connection</source>
|
<source>To make a new connection</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -3724,8 +3720,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -3876,10 +3872,6 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>You can't send messages!</source>
|
<source>You can't send messages!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
@@ -186,20 +186,16 @@ Available in v5.1</source>
|
|||||||
<source>)</source>
|
<source>)</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||||
@@ -210,8 +206,8 @@ Available in v5.1</source>
|
|||||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||||
@@ -1128,8 +1124,8 @@ Available in v5.1</source>
|
|||||||
<source>Direct messages</source>
|
<source>Direct messages</source>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1144,8 +1140,8 @@ Available in v5.1</source>
|
|||||||
<source>Disappearing messages are prohibited in this chat.</source>
|
<source>Disappearing messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disconnect" xml:space="preserve">
|
<trans-unit id="Disconnect" xml:space="preserve">
|
||||||
@@ -1580,16 +1576,16 @@ Available in v5.1</source>
|
|||||||
<source>Group members can irreversibly delete sent messages.</source>
|
<source>Group members can irreversibly delete sent messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1708,8 +1704,8 @@ Available in v5.1</source>
|
|||||||
<source>Immediately</source>
|
<source>Immediately</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Import" xml:space="preserve">
|
<trans-unit id="Import" xml:space="preserve">
|
||||||
@@ -1821,8 +1817,8 @@ Available in v5.1</source>
|
|||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -2016,8 +2012,8 @@ Available in v5.1</source>
|
|||||||
<source>Migration is completed</source>
|
<source>Migration is completed</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Migrations: %@" xml:space="preserve">
|
<trans-unit id="Migrations:" xml:space="preserve">
|
||||||
<source>Migrations: %@</source>
|
<source>Migrations:</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Moderate" xml:space="preserve">
|
<trans-unit id="Moderate" xml:space="preserve">
|
||||||
@@ -2174,8 +2170,8 @@ Available in v5.1</source>
|
|||||||
<source>Onion hosts will not be used.</source>
|
<source>Onion hosts will not be used.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -2234,8 +2230,8 @@ Available in v5.1</source>
|
|||||||
<source>Open user profiles</source>
|
<source>Open user profiles</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||||
@@ -2290,8 +2286,8 @@ Available in v5.1</source>
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -2994,8 +2990,8 @@ Available in v5.1</source>
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||||
@@ -3039,8 +3035,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||||||
<source>The message will be marked as moderated for all members.</source>
|
<source>The message will be marked as moderated for all members.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -3111,8 +3107,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||||||
<source>To make a new connection</source>
|
<source>To make a new connection</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -3337,8 +3333,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -3478,10 +3474,6 @@ SimpleX Lock must be enabled.</source>
|
|||||||
<source>You can't send messages!</source>
|
<source>You can't send messages!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
@@ -217,23 +217,18 @@ Available in v5.1</source>
|
|||||||
<target state="translated">)</target>
|
<target state="translated">)</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<target state="translated">**הוסיפו איש קשר חדש**: ליצירת קוד QR או קישור חד־פעמיים עבור איש הקשר שלכם.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<target state="translated">**צור קישור / קוד QR** לשימוש איש הקשר שלך.</target>
|
<target state="translated">**צור קישור / קוד QR** לשימוש איש הקשר שלך.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<target state="translated">**יותר פרטי**: בדוק הודעות חדשות כל 20 דקות. אסימון המכשיר משותף עם שרת SimpleX Chat, אך לא כמה אנשי קשר או הודעות יש לך.</target>
|
<target state="translated">**יותר פרטי**: בדוק הודעות חדשות כל 20 דקות. אסימון המכשיר משותף עם שרת SimpleX Chat, אך לא כמה אנשי קשר או הודעות יש לך.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<target state="translated">**הכי פרטי**: אל תשתמש בשרת ההתראות של SimpleX Chat, בדוק הודעות מעת לעת ברקע (תלוי בתדירות השימוש באפליקציה).</target>
|
<target state="translated">**הכי פרטי**: אל תשתמש בשרת ההתראות של SimpleX Chat, בדוק הודעות מעת לעת ברקע (תלוי בתדירות השימוש באפליקציה).</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -247,8 +242,8 @@ Available in v5.1</source>
|
|||||||
<target state="translated">**שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה.</target>
|
<target state="translated">**שימו לב**: לא ניתן יהיה לשחזר או לשנות את הסיסמה אם תאבדו אותה.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<target state="translated">**מומלץ**: אסימון מכשיר והתראות נשלחים לשרת ההתראות של SimpleX Chat, אך לא תוכן ההודעה, גודלה או ממי היא.</target>
|
<target state="translated">**מומלץ**: אסימון מכשיר והתראות נשלחים לשרת ההתראות של SimpleX Chat, אך לא תוכן ההודעה, גודלה או ממי היא.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -1391,8 +1386,8 @@ Available in v5.1</source>
|
|||||||
<target state="translated">הודעות ישירות</target>
|
<target state="translated">הודעות ישירות</target>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve" approved="no">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<target state="translated">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</target>
|
<target state="translated">הודעות ישירות בין חברי קבוצה אסורות בקבוצה זו.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -1411,8 +1406,8 @@ Available in v5.1</source>
|
|||||||
<target state="translated">הודעות נעלמות אסורות בצ׳אט זה.</target>
|
<target state="translated">הודעות נעלמות אסורות בצ׳אט זה.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve" approved="no">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<target state="translated">הודעות נעלמות אסורות בקבוצה זו.</target>
|
<target state="translated">הודעות נעלמות אסורות בקבוצה זו.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -1956,18 +1951,18 @@ Available in v5.1</source>
|
|||||||
<target state="translated">חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</target>
|
<target state="translated">חברי הקבוצה יכולים למחוק באופן בלתי הפיך הודעות שנשלחו.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no">
|
<trans-unit id="Members can send direct messages." xml:space="preserve" approved="no">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות ישירות.</target>
|
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות ישירות.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve" approved="no">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות נעלמות.</target>
|
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות נעלמות.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no">
|
<trans-unit id="Members can send voice messages." xml:space="preserve" approved="no">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות קוליות.</target>
|
<target state="translated">חברי הקבוצה יכולים לשלוח הודעות קוליות.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -2115,8 +2110,8 @@ Available in v5.1</source>
|
|||||||
<target state="translated">מיד</target>
|
<target state="translated">מיד</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no">
|
<trans-unit id="Immune to spam" xml:space="preserve" approved="no">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<target state="translated">חסין מפני ספאם ושימוש לרעה</target>
|
<target state="translated">חסין מפני ספאם ושימוש לרעה</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -2257,8 +2252,8 @@ Available in v5.1</source>
|
|||||||
<target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה.</target>
|
<target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בצ׳אט זה.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve" approved="no">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו.</target>
|
<target state="translated">מחיקה בלתי הפיכה של הודעות אסורה בקבוצה זו.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -2502,9 +2497,9 @@ Available in v5.1</source>
|
|||||||
<target state="translated">ההעברה הושלמה</target>
|
<target state="translated">ההעברה הושלמה</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Migrations: %@" xml:space="preserve" approved="no">
|
<trans-unit id="Migrations:" xml:space="preserve" approved="no">
|
||||||
<source>Migrations: %@</source>
|
<source>Migrations:</source>
|
||||||
<target state="translated">העברות: %@</target>
|
<target state="translated">העברות:</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Moderate" xml:space="preserve" approved="no">
|
<trans-unit id="Moderate" xml:space="preserve" approved="no">
|
||||||
@@ -2701,8 +2696,8 @@ Available in v5.1</source>
|
|||||||
<target state="translated">לא ייעשה שימוש במארחי Onion.</target>
|
<target state="translated">לא ייעשה שימוש במארחי Onion.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -2761,8 +2756,8 @@ Available in v5.1</source>
|
|||||||
<source>Open user profiles</source>
|
<source>Open user profiles</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||||
@@ -2817,8 +2812,8 @@ Available in v5.1</source>
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -3521,8 +3516,8 @@ Available in v5.1</source>
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
<trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve">
|
||||||
@@ -3566,8 +3561,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||||||
<source>The message will be marked as moderated for all members.</source>
|
<source>The message will be marked as moderated for all members.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -3638,8 +3633,8 @@ It can happen because of some bug or when the connection is compromised.</source
|
|||||||
<source>To make a new connection</source>
|
<source>To make a new connection</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -3864,8 +3859,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -4005,10 +4000,6 @@ SimpleX Lock must be enabled.</source>
|
|||||||
<source>You can't send messages!</source>
|
<source>You can't send messages!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
@@ -4967,8 +4958,8 @@ SimpleX servers cannot see your profile.</source>
|
|||||||
<target state="translated">נמחק</target>
|
<target state="translated">נמחק</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Files and media are prohibited in this group." xml:space="preserve" approved="no">
|
<trans-unit id="Files and media are prohibited." xml:space="preserve" approved="no">
|
||||||
<source>Files and media are prohibited in this group.</source>
|
<source>Files and media are prohibited.</source>
|
||||||
<target state="translated">קבצים ומדיה אסורים בקבוצה זו.</target>
|
<target state="translated">קבצים ומדיה אסורים בקבוצה זו.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -5027,13 +5018,13 @@ SimpleX servers cannot see your profile.</source>
|
|||||||
<target state="translated">הזמן חברים</target>
|
<target state="translated">הזמן חברים</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can add message reactions." xml:space="preserve" approved="no">
|
<trans-unit id="Members can add message reactions." xml:space="preserve" approved="no">
|
||||||
<source>Group members can add message reactions.</source>
|
<source>Members can add message reactions.</source>
|
||||||
<target state="translated">חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות.</target>
|
<target state="translated">חברי הקבוצה יכולים להוסיף תגובות אמוג׳י להודעות.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send files and media." xml:space="preserve" approved="no">
|
<trans-unit id="Members can send files and media." xml:space="preserve" approved="no">
|
||||||
<source>Group members can send files and media.</source>
|
<source>Members can send files and media.</source>
|
||||||
<target state="translated">חברי הקבוצה יכולים לשלוח קבצים ומדיה.</target>
|
<target state="translated">חברי הקבוצה יכולים לשלוח קבצים ומדיה.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -5231,8 +5222,8 @@ SimpleX servers cannot see your profile.</source>
|
|||||||
<target state="translated">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</target>
|
<target state="translated">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no">
|
<trans-unit id="Message reactions are prohibited." xml:space="preserve" approved="no">
|
||||||
<source>Message reactions are prohibited in this group.</source>
|
<source>Message reactions are prohibited.</source>
|
||||||
<target state="translated">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</target>
|
<target state="translated">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|||||||
@@ -181,23 +181,18 @@
|
|||||||
<source>)</source>
|
<source>)</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<target state="needs-translation">**Dodajte novi kontakt**: da biste stvorili svoj jednokratni QR kôd ili vezu za svoj kontakt.</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<target state="needs-translation">**Stvorite vezu / QR kôd** za vaš kontakt.</target>
|
<target state="needs-translation">**Stvorite vezu / QR kôd** za vaš kontakt.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve" approved="no">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<target state="needs-translation">**Privatnije**: provjeravajte nove poruke svakih 20 minuta. Token uređaja dijeli se s SimpleX Chat poslužiteljem, ali ne i s brojem kontakata ili poruka koje imate.</target>
|
<target state="needs-translation">**Privatnije**: provjeravajte nove poruke svakih 20 minuta. Token uređaja dijeli se s SimpleX Chat poslužiteljem, ali ne i s brojem kontakata ili poruka koje imate.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve" approved="no">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<target state="needs-translation">**Najprivatniji**: nemojte koristiti SimpleX Chat poslužitelj obavijesti, povremeno provjeravajte poruke u pozadini (ovisi o tome koliko često koristite aplikaciju).</target>
|
<target state="needs-translation">**Najprivatniji**: nemojte koristiti SimpleX Chat poslužitelj obavijesti, povremeno provjeravajte poruke u pozadini (ovisi o tome koliko često koristite aplikaciju).</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -211,8 +206,8 @@
|
|||||||
<target state="needs-translation">**Imajte na umu**: NEĆETE moći oporaviti ili promijeniti pristupni izraz ako ga izgubite.</target>
|
<target state="needs-translation">**Imajte na umu**: NEĆETE moći oporaviti ili promijeniti pristupni izraz ako ga izgubite.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve" approved="no">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<target state="needs-translation">**Preporučeno**: token uređaja i obavijesti šalju se na poslužitelj obavijesti SimpleX Chata, ali ne i sadržaj poruke, veličinu ili od koga je.</target>
|
<target state="needs-translation">**Preporučeno**: token uređaja i obavijesti šalju se na poslužitelj obavijesti SimpleX Chata, ali ne i sadržaj poruke, veličinu ili od koga je.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -1039,8 +1034,8 @@
|
|||||||
<source>Direct messages</source>
|
<source>Direct messages</source>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1055,8 +1050,8 @@
|
|||||||
<source>Disappearing messages are prohibited in this chat.</source>
|
<source>Disappearing messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disconnect" xml:space="preserve">
|
<trans-unit id="Disconnect" xml:space="preserve">
|
||||||
@@ -1419,16 +1414,16 @@
|
|||||||
<source>Group members can irreversibly delete sent messages.</source>
|
<source>Group members can irreversibly delete sent messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1519,8 +1514,8 @@
|
|||||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Import" xml:space="preserve">
|
<trans-unit id="Import" xml:space="preserve">
|
||||||
@@ -1616,8 +1611,8 @@
|
|||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -1917,8 +1912,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Onion hosts will not be used.</source>
|
<source>Onion hosts will not be used.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -1965,8 +1960,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Open chat console</source>
|
<source>Open chat console</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||||
@@ -1997,8 +1992,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -2577,8 +2572,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||||
@@ -2609,8 +2604,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>The microphone does not work when the app is in the background.</source>
|
<source>The microphone does not work when the app is in the background.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -2673,8 +2668,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
<source>To prevent the call interruption, enable Do Not Disturb mode.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -2847,8 +2842,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -2959,10 +2954,6 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>You can use markdown to format messages:</source>
|
<source>You can use markdown to format messages:</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
@@ -152,20 +152,16 @@
|
|||||||
<source>)</source>
|
<source>)</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||||
@@ -176,8 +172,8 @@
|
|||||||
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
<source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||||
@@ -1013,8 +1009,8 @@
|
|||||||
<source>Direct messages</source>
|
<source>Direct messages</source>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1029,8 +1025,8 @@
|
|||||||
<source>Disappearing messages are prohibited in this chat.</source>
|
<source>Disappearing messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disconnect" xml:space="preserve">
|
<trans-unit id="Disconnect" xml:space="preserve">
|
||||||
@@ -1421,16 +1417,16 @@
|
|||||||
<source>Group members can irreversibly delete sent messages.</source>
|
<source>Group members can irreversibly delete sent messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1537,8 +1533,8 @@
|
|||||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Import" xml:space="preserve">
|
<trans-unit id="Import" xml:space="preserve">
|
||||||
@@ -1642,8 +1638,8 @@
|
|||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -1961,8 +1957,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Onion hosts will not be used.</source>
|
<source>Onion hosts will not be used.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -2013,8 +2009,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Open user profiles</source>
|
<source>Open user profiles</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||||
@@ -2049,8 +2045,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -2670,8 +2666,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||||
@@ -2706,8 +2702,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>The message will be marked as moderated for all members.</source>
|
<source>The message will be marked as moderated for all members.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -2774,8 +2770,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>To make a new connection</source>
|
<source>To make a new connection</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -2968,8 +2964,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -3093,10 +3089,6 @@ SimpleX Lock must be enabled.</source>
|
|||||||
<source>You can't send messages!</source>
|
<source>You can't send messages!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
|
|||||||
@@ -162,20 +162,16 @@
|
|||||||
<target state="translated">)</target>
|
<target state="translated">)</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve">
|
|
||||||
<source>**Add new contact**: to create your one-time QR Code or link for your contact.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
<trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve">
|
||||||
<source>**Create link / QR code** for your contact to use.</source>
|
<source>**Create link / QR code** for your contact to use.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve">
|
<trans-unit id="**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata." xml:space="preserve">
|
||||||
<source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source>
|
<source>**More private**: check new messages every 20 minutes. Only device token is shared with our push server. It doesn't see how many contacts you have, or any message metadata.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve">
|
<trans-unit id="**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app." xml:space="preserve">
|
||||||
<source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source>
|
<source>**Most private**: do not use SimpleX Chat push server. The app will check messages in background, when the system allows it, depending on how often you use the app.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
<trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve">
|
||||||
@@ -187,8 +183,8 @@
|
|||||||
<target state="translated">**Turėkite omenyje**: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</target>
|
<target state="translated">**Turėkite omenyje**: jeigu prarasite slaptafrazę, NEBEGALĖSITE jos atkurti ar pakeisti.</target>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve">
|
<trans-unit id="**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from." xml:space="preserve">
|
||||||
<source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source>
|
<source>**Recommended**: device token and end-to-end encrypted notifications are sent to SimpleX Chat push server, but it does not see the message content, size or who it is from.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
<trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve">
|
||||||
@@ -1033,8 +1029,8 @@
|
|||||||
<source>Direct messages</source>
|
<source>Direct messages</source>
|
||||||
<note>chat feature</note>
|
<note>chat feature</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Direct messages between members are prohibited." xml:space="preserve">
|
||||||
<source>Direct messages between members are prohibited in this group.</source>
|
<source>Direct messages between members are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
<trans-unit id="Disable SimpleX Lock" xml:space="preserve">
|
||||||
@@ -1049,8 +1045,8 @@
|
|||||||
<source>Disappearing messages are prohibited in this chat.</source>
|
<source>Disappearing messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Disappearing messages are prohibited." xml:space="preserve">
|
||||||
<source>Disappearing messages are prohibited in this group.</source>
|
<source>Disappearing messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Disconnect" xml:space="preserve">
|
<trans-unit id="Disconnect" xml:space="preserve">
|
||||||
@@ -1417,16 +1413,16 @@
|
|||||||
<source>Group members can irreversibly delete sent messages.</source>
|
<source>Group members can irreversibly delete sent messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send direct messages." xml:space="preserve">
|
<trans-unit id="Members can send direct messages." xml:space="preserve">
|
||||||
<source>Group members can send direct messages.</source>
|
<source>Members can send direct messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send disappearing messages." xml:space="preserve">
|
<trans-unit id="Members can send disappearing messages." xml:space="preserve">
|
||||||
<source>Group members can send disappearing messages.</source>
|
<source>Members can send disappearing messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group members can send voice messages." xml:space="preserve">
|
<trans-unit id="Members can send voice messages." xml:space="preserve">
|
||||||
<source>Group members can send voice messages.</source>
|
<source>Members can send voice messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Group message:" xml:space="preserve">
|
<trans-unit id="Group message:" xml:space="preserve">
|
||||||
@@ -1513,8 +1509,8 @@
|
|||||||
<source>Image will be received when your contact is online, please wait or check later!</source>
|
<source>Image will be received when your contact is online, please wait or check later!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Immune to spam and abuse" xml:space="preserve">
|
<trans-unit id="Immune to spam" xml:space="preserve">
|
||||||
<source>Immune to spam and abuse</source>
|
<source>Immune to spam</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Import" xml:space="preserve">
|
<trans-unit id="Import" xml:space="preserve">
|
||||||
@@ -1614,8 +1610,8 @@
|
|||||||
<source>Irreversible message deletion is prohibited in this chat.</source>
|
<source>Irreversible message deletion is prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve">
|
<trans-unit id="Irreversible message deletion is prohibited." xml:space="preserve">
|
||||||
<source>Irreversible message deletion is prohibited in this group.</source>
|
<source>Irreversible message deletion is prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
<trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve">
|
||||||
@@ -1919,8 +1915,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Onion hosts will not be used.</source>
|
<source>Onion hosts will not be used.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve">
|
<trans-unit id="Only client devices store user profiles, contacts, groups, and messages." xml:space="preserve">
|
||||||
<source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source>
|
<source>Only client devices store user profiles, contacts, groups, and messages.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
<trans-unit id="Only group owners can change group preferences." xml:space="preserve">
|
||||||
@@ -1971,8 +1967,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Open user profiles</source>
|
<source>Open user profiles</source>
|
||||||
<note>authentication reason</note>
|
<note>authentication reason</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve">
|
<trans-unit id="Anybody can host servers." xml:space="preserve">
|
||||||
<source>Open-source protocol and code – anybody can run the servers.</source>
|
<source>Anybody can host servers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
<trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve">
|
||||||
@@ -2003,8 +1999,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Paste the link you received into the box below to connect with your contact.</source>
|
<source>Paste the link you received into the box below to connect with your contact.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="People can connect to you only via the links you share." xml:space="preserve">
|
<trans-unit id="You decide who can connect." xml:space="preserve">
|
||||||
<source>People can connect to you only via the links you share.</source>
|
<source>You decide who can connect.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Periodically" xml:space="preserve">
|
<trans-unit id="Periodically" xml:space="preserve">
|
||||||
@@ -2591,8 +2587,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>Thanks to the users – contribute via Weblate!</source>
|
<source>Thanks to the users – contribute via Weblate!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve">
|
<trans-unit id="No user identifiers." xml:space="preserve">
|
||||||
<source>The 1st platform without any user identifiers – private by design.</source>
|
<source>No user identifiers.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
<trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve">
|
||||||
@@ -2627,8 +2623,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>The message will be marked as moderated for all members.</source>
|
<source>The message will be marked as moderated for all members.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The next generation of private messaging" xml:space="preserve">
|
<trans-unit id="The future of messaging" xml:space="preserve">
|
||||||
<source>The next generation of private messaging</source>
|
<source>The future of messaging</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
<trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve">
|
||||||
@@ -2687,8 +2683,8 @@ We will be adding server redundancy to prevent lost messages.</source>
|
|||||||
<source>To make a new connection</source>
|
<source>To make a new connection</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve">
|
<trans-unit id="To protect your privacy, SimpleX uses separate IDs for each of your contacts." xml:space="preserve">
|
||||||
<source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source>
|
<source>To protect your privacy, SimpleX uses separate IDs for each of your contacts.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
<trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve">
|
||||||
@@ -2873,8 +2869,8 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>Voice messages are prohibited in this chat.</source>
|
<source>Voice messages are prohibited in this chat.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages are prohibited in this group." xml:space="preserve">
|
<trans-unit id="Voice messages are prohibited." xml:space="preserve">
|
||||||
<source>Voice messages are prohibited in this group.</source>
|
<source>Voice messages are prohibited.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
<trans-unit id="Voice messages prohibited!" xml:space="preserve">
|
||||||
@@ -2993,10 +2989,6 @@ To connect, please ask your contact to create another connection link and check
|
|||||||
<source>You can't send messages!</source>
|
<source>You can't send messages!</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve">
|
|
||||||
<source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
<trans-unit id="You could not be verified; please try again." xml:space="preserve">
|
||||||
<source>You could not be verified; please try again.</source>
|
<source>You could not be verified; please try again.</source>
|
||||||
<note>No comment provided by engineer.</note>
|
<note>No comment provided by engineer.</note>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,6 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"_italic_" = "\\_italic_";
|
"_italic_" = "\\_italic_";
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
|
||||||
"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact.";
|
|
||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"*bold*" = "\\*bold*";
|
"*bold*" = "\\*bold*";
|
||||||
|
|
||||||
@@ -27,4 +24,3 @@
|
|||||||
|
|
||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"No group!" = "Group not found!";
|
"No group!" = "Group not found!";
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user