Compare commits

..

21 Commits

Author SHA1 Message Date
Evgeny Poberezkin dccd6631af 5.7.4: ios 219, android 212, desktop 48 2024-05-25 17:06:11 +01:00
Evgeny Poberezkin b0516b6440 core: 5.7.4.1 2024-05-25 11:18:49 +01:00
Evgeny Poberezkin 0964982782 core: ensure that messages are sent to different members (#4221)
* core: ensure that messages are sent to different members

* update simplexmq
2024-05-23 22:50:23 +01:00
Evgeny Poberezkin 9eb6034e1a core: 5.7.4.0 (simplexmq 5.7.4.1: networking improvements) 2024-05-22 20:30:24 +01:00
Stanislav Dmitrenko a0c257962e android, desktop: fix file menu and icon (#4185)
* android, desktop: fix file menu and icon

* simplify

* space
2024-05-15 11:43:20 +01:00
Evgeny Poberezkin 554ef4eefc 5.7.3: ios 215, android 206, desktop 44 2024-05-12 11:36:30 +01:00
Evgeny Poberezkin 567e9981be core: 5.7.3.0 2024-05-11 23:48:39 +01:00
Evgeny Poberezkin 1183fe427f Merge branch 'stable' 2024-05-10 20:30:42 +01:00
Evgeny Poberezkin c49a1f42e7 5.7.2: ios 214, android 204, desktop 43 2024-05-10 18:32:07 +01:00
Narasimha-sc 4f173215c6 docs: fix broken simplex group invite links (#4155)
* Fix non-working #simplex-devs invite

* Fix #simplex-devs link in pl

* replace SimpleX-Group-2

* replace simplex-group-2 link

* Update README.md
2024-05-10 17:43:06 +01:00
Evgeny Poberezkin 41597e563e website: translations (#4165)
* Translated using Weblate (French)

Currently translated at 100.0% (257 of 257 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/

* Translated using Weblate (Czech)

Currently translated at 98.0% (252 of 257 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/cs/

* Translated using Weblate (Russian)

Currently translated at 99.6% (256 of 257 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ru/

* Translated using Weblate (French)

Currently translated at 100.0% (257 of 257 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/fr/

* Translated using Weblate (Czech)

Currently translated at 98.0% (252 of 257 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/cs/

* Translated using Weblate (Russian)

Currently translated at 99.6% (256 of 257 strings)

Translation: SimpleX Chat/SimpleX Chat website
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/website/ru/

---------

Co-authored-by: J'informatique <hello@jinformatique.net>
Co-authored-by: zenobit <zen@osowoso.xyz>
Co-authored-by: link2xt <link2xt@testrun.org>
2024-05-10 17:39:51 +01:00
Evgeny Poberezkin aa127f0cea ui: translations (#4164)
* Translated using Weblate (Japanese)

Currently translated at 100.0% (1741 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ja/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (1515 of 1515 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/tr/

* Translated using Weblate (Turkish)

Currently translated at 100.0% (1741 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/tr/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1741 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/hu/

* Translated using Weblate (Persian)

Currently translated at 25.3% (441 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* Translated using Weblate (Hungarian)

Currently translated at 100.0% (1515 of 1515 strings)

Translation: SimpleX Chat/SimpleX Chat iOS
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/ios/hu/

* Translated using Weblate (Vietnamese)

Currently translated at 10.1% (176 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/vi/

* Translated using Weblate (Persian)

Currently translated at 26.8% (467 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* Translated using Weblate (Persian)

Currently translated at 33.6% (586 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* Translated using Weblate (Romanian)

Currently translated at 6.0% (105 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/

* Translated using Weblate (Persian)

Currently translated at 36.8% (642 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* Translated using Weblate (Romanian)

Currently translated at 9.1% (159 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/ro/

* Translated using Weblate (Persian)

Currently translated at 39.0% (679 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* Translated using Weblate (Persian)

Currently translated at 39.0% (680 of 1741 strings)

Translation: SimpleX Chat/SimpleX Chat Android
Translate-URL: https://hosted.weblate.org/projects/simplex-chat/android/fa/

* import/export localizations

---------

Co-authored-by: Miyu Sakatsuki <miyu-sakatsuki@outlook.jp>
Co-authored-by: z0rden <kaanpeker196@gmail.com>
Co-authored-by: summoner001 <summoner@vivaldi.net>
Co-authored-by: Siavash <ciavash@proton.me>
Co-authored-by: tuananh-ng <158744840+tuananh-ng@users.noreply.github.com>
Co-authored-by: Flavian <3zorro.1@gmail.com>
2024-05-10 17:38:42 +01:00
Evgeny Poberezkin f9c64ba39c ios: update core library 2024-05-10 17:22:39 +01:00
Evgeny Poberezkin 1025fa94a1 core: 5.7.2.0 (simplexmq 5.7.3.0: update protocol versions, fix file downloads) 2024-05-10 15:18:42 +01:00
Stanislav Dmitrenko a59769b91d android, desktop: fix crash in contact preferences when updating user profile (#4158) 2024-05-10 09:45:44 +01:00
spaced4ndy 76248b4878 core: on receive set file crypto args only for new files (#4152) 2024-05-09 15:40:23 +04:00
Stanislav Dmitrenko 93a7ddb128 android, desktop: fix presenting large images (#4139)
* android, desktop: fix presenting large images

* 4320 px limitation

* revert line

* onClick

* onClick
2024-05-09 10:39:49 +01:00
Evgeny Poberezkin bc5af35a3e core: wallpapers api (#4110)
* core: wallpapers api

* optional colors

* update

* api

* update

* whitespace

* typo

* test, fix

* fix color parsing

* separate UI and Theme color schemes

* update

* enable test

* multiple themes, one per color scheme

* theme overrides as a separate type

* rename

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
2024-05-08 15:36:20 +01:00
Evgeny Poberezkin e38d5bd885 core: update simplexmq (upgrade client protocol versions SMP v7, NTF v2) 2024-05-08 13:40:25 +01:00
M. Sarmad Qadeer 10f41aa1c9 web: add scroll to navbar (#4100)
* web: add scroll to navbar

* web: add scroll to whole navbar instead of scrolling the opened menu
2024-05-08 09:31:02 +01:00
spaced4ndy 1b081197d4 multiplatform: add error to response (#4140) 2024-05-06 20:27:35 +04:00
54 changed files with 1342 additions and 412 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ Messages not following these rules will be deleted, the right to send messages m
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) for developers who build on SimpleX platform: There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
- chat bots and automations - chat bots and automations
- integrations with other apps - integrations with other apps
@@ -7235,7 +7235,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.</target>
</trans-unit> </trans-unit>
<trans-unit id="member connected" xml:space="preserve"> <trans-unit id="member connected" xml:space="preserve">
<source>connected</source> <source>connected</source>
<target>kapcsolódva</target> <target>kapcsolódott</target>
<note>rcv group event chat item</note> <note>rcv group event chat item</note>
</trans-unit> </trans-unit>
<trans-unit id="message received" xml:space="preserve"> <trans-unit id="message received" xml:space="preserve">
@@ -5371,7 +5371,7 @@ Hata: %@</target>
</trans-unit> </trans-unit>
<trans-unit id="Square, circle, or anything in between." xml:space="preserve"> <trans-unit id="Square, circle, or anything in between." xml:space="preserve">
<source>Square, circle, or anything in between.</source> <source>Square, circle, or anything in between.</source>
<target>Kare,daire, veya aralarında herhangi birşey.</target> <target>Kare,daire, veya aralarında herhangi bir şey.</target>
<note>No comment provided by engineer.</note> <note>No comment provided by engineer.</note>
</trans-unit> </trans-unit>
<trans-unit id="Start chat" xml:space="preserve"> <trans-unit id="Start chat" xml:space="preserve">
+32 -32
View File
@@ -53,6 +53,11 @@
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; }; 5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */; };
5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; }; 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */; };
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; }; 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */; };
5C614CFB2C021C1C000E7758 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C614CF62C021C1C000E7758 /* libffi.a */; };
5C614CFC2C021C1C000E7758 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C614CF72C021C1C000E7758 /* libgmpxx.a */; };
5C614CFD2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C614CF82C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7-ghc9.6.3.a */; };
5C614CFE2C021C1C000E7758 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C614CF92C021C1C000E7758 /* libgmp.a */; };
5C614CFF2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C614CFA2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7.a */; };
5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */; }; 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */; };
5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; }; 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C65F341297D3F3600B67AF3 /* VersionView.swift */; };
5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; }; 5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6BA666289BD954009B8ECC /* DismissSheets.swift */; };
@@ -115,11 +120,6 @@
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; };
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; };
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; }; 5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
5CE0E8972BE7F144008D6E06 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8922BE7F144008D6E06 /* libgmp.a */; };
5CE0E8982BE7F144008D6E06 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8932BE7F144008D6E06 /* libgmpxx.a */; };
5CE0E8992BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */; };
5CE0E89A2BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */; };
5CE0E89B2BE7F144008D6E06 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE0E8962BE7F144008D6E06 /* libffi.a */; };
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; }; 5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */; };
@@ -316,6 +316,11 @@
5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = "<group>"; }; 5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = "<group>"; };
5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; }; 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; };
5C614CF62C021C1C000E7758 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C614CF72C021C1C000E7758 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C614CF82C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7-ghc9.6.3.a"; sourceTree = "<group>"; };
5C614CF92C021C1C000E7758 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C614CFA2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7.a"; sourceTree = "<group>"; };
5C636F662AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = "uk.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C636F662AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = "uk.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C636F672AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C636F672AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5C65DAE429C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; 5C65DAE429C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -420,11 +425,6 @@
5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; }; 5CDCAD7428188D2900503DA2 /* APITypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITypes.swift; sourceTree = "<group>"; };
5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; }; 5CDCAD7D2818941F00503DA2 /* API.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = "<group>"; };
5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 5CDCAD80281A7E2700503DA2 /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
5CE0E8922BE7F144008D6E06 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5CE0E8932BE7F144008D6E06 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a"; sourceTree = "<group>"; };
5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a"; sourceTree = "<group>"; };
5CE0E8962BE7F144008D6E06 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CE1330328E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = "de.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5CE1330428E118CC00FFFD8C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SimpleXChat.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -529,12 +529,12 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
5CE0E8992BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a in Frameworks */, 5C614CFD2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7-ghc9.6.3.a in Frameworks */,
5CE0E8982BE7F144008D6E06 /* libgmpxx.a in Frameworks */, 5C614CFF2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7.a in Frameworks */,
5C614CFC2C021C1C000E7758 /* libgmpxx.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5CE0E89A2BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a in Frameworks */, 5C614CFB2C021C1C000E7758 /* libffi.a in Frameworks */,
5CE0E89B2BE7F144008D6E06 /* libffi.a in Frameworks */, 5C614CFE2C021C1C000E7758 /* libgmp.a in Frameworks */,
5CE0E8972BE7F144008D6E06 /* libgmp.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -601,11 +601,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = { 5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
5CE0E8962BE7F144008D6E06 /* libffi.a */, 5C614CF62C021C1C000E7758 /* libffi.a */,
5CE0E8922BE7F144008D6E06 /* libgmp.a */, 5C614CF92C021C1C000E7758 /* libgmp.a */,
5CE0E8932BE7F144008D6E06 /* libgmpxx.a */, 5C614CF72C021C1C000E7758 /* libgmpxx.a */,
5CE0E8942BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW-ghc9.6.3.a */, 5C614CF82C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7-ghc9.6.3.a */,
5CE0E8952BE7F144008D6E06 /* libHSsimplex-chat-5.7.1.0-dbWFHyA3wYHsyiwKvX3TW.a */, 5C614CFA2C021C1C000E7758 /* libHSsimplex-chat-5.7.4.1-FMTNcSSNT86KdfUI39X2r7.a */,
); );
path = Libraries; path = Libraries;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1552,7 +1552,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 213; CURRENT_PROJECT_VERSION = 219;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1577,7 +1577,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES_THIN; LLVM_LTO = YES_THIN;
MARKETING_VERSION = 5.7.1; MARKETING_VERSION = 5.7.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1601,7 +1601,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 213; CURRENT_PROJECT_VERSION = 219;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -1626,7 +1626,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.1; MARKETING_VERSION = 5.7.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
PRODUCT_NAME = SimpleX; PRODUCT_NAME = SimpleX;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1687,7 +1687,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 213; CURRENT_PROJECT_VERSION = 219;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s; GCC_OPTIMIZATION_LEVEL = s;
@@ -1702,7 +1702,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.1; MARKETING_VERSION = 5.7.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1724,7 +1724,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 213; CURRENT_PROJECT_VERSION = 219;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO; ENABLE_CODE_COVERAGE = NO;
@@ -1739,7 +1739,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.1; MARKETING_VERSION = 5.7.4;
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1761,7 +1761,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 213; CURRENT_PROJECT_VERSION = 219;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1787,7 +1787,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.1; MARKETING_VERSION = 5.7.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -1812,7 +1812,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 213; CURRENT_PROJECT_VERSION = 219;
DEFINES_MODULE = YES; DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T; DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1838,7 +1838,7 @@
"$(PROJECT_DIR)/Libraries/sim", "$(PROJECT_DIR)/Libraries/sim",
); );
LLVM_LTO = YES; LLVM_LTO = YES;
MARKETING_VERSION = 5.7.1; MARKETING_VERSION = 5.7.4;
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
SDKROOT = iphoneos; SDKROOT = iphoneos;
+1 -1
View File
@@ -2443,7 +2443,7 @@
"member %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@"; "member %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@";
/* rcv group event chat item */ /* rcv group event chat item */
"member connected" = "kapcsolódva"; "member connected" = "kapcsolódott";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla."; "Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla.";
+1 -1
View File
@@ -3612,7 +3612,7 @@
"Somebody" = "Biri"; "Somebody" = "Biri";
/* No comment provided by engineer. */ /* No comment provided by engineer. */
"Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi birşey."; "Square, circle, or anything in between." = "Kare,daire, veya aralarında herhangi bir şey.";
/* chat item text */ /* chat item text */
"standard end-to-end encryption" = "standart uçtan uca şifreleme"; "standard end-to-end encryption" = "standart uçtan uca şifreleme";
@@ -37,7 +37,7 @@ actual fun ClipboardManager.shareText(text: String) {
} }
} }
actual fun shareFile(text: String, fileSource: CryptoFile) { fun openOrShareFile(text: String, fileSource: CryptoFile, justOpen: Boolean) {
val uri = if (fileSource.cryptoArgs != null) { val uri = if (fileSource.cryptoArgs != null) {
val tmpFile = File(tmpDir, fileSource.filePath) val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit() tmpFile.deleteOnExit()
@@ -55,20 +55,31 @@ actual fun shareFile(text: String, fileSource: CryptoFile) {
} }
val ext = fileSource.filePath.substringAfterLast(".") val ext = fileSource.filePath.substringAfterLast(".")
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return
val sendIntent: Intent = Intent().apply { val sendIntent: Intent = Intent(if (justOpen) Intent.ACTION_VIEW else Intent.ACTION_SEND).apply {
action = Intent.ACTION_SEND
/*if (text.isNotEmpty()) { /*if (text.isNotEmpty()) {
putExtra(Intent.EXTRA_TEXT, text) putExtra(Intent.EXTRA_TEXT, text)
}*/ }*/
putExtra(Intent.EXTRA_STREAM, uri.toUri()) if (justOpen) {
type = mimeType flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
flags = Intent.FLAG_ACTIVITY_NEW_TASK setDataAndType(uri.toUri(), mimeType)
} else {
putExtra(Intent.EXTRA_STREAM, uri.toUri())
type = mimeType
}
} }
val shareIntent = Intent.createChooser(sendIntent, null) val shareIntent = Intent.createChooser(sendIntent, null)
shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK)
androidAppContext.startActivity(shareIntent) androidAppContext.startActivity(shareIntent)
} }
actual fun shareFile(text: String, fileSource: CryptoFile) {
openOrShareFile(text, fileSource, justOpen = false)
}
actual fun openFile(fileSource: CryptoFile) {
openOrShareFile("", fileSource, justOpen = true)
}
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject)
@@ -2653,6 +2653,12 @@ data class CIFile(
return res return res
} }
fun forwardingAllowed(): Boolean = when {
chatModel.connectedToRemote() && cachedRemoteFileRequests[fileSource] != false && loaded -> true
getLoadedFilePath(this) != null -> true
else -> false
}
companion object { companion object {
fun getSample( fun getSample(
fileId: Long = 1, fileId: Long = 1,
@@ -4175,7 +4175,7 @@ sealed class CR {
@Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvStandaloneFileComplete") class RcvStandaloneFileComplete(val user: UserRef, val targetPath: String, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR()
@Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR()
// sending file events // sending file events
@Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR()
@@ -4501,7 +4501,7 @@ sealed class CR {
is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem)) is RcvFileSndCancelled -> withUser(user, json.encodeToString(chatItem))
is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize")
is RcvStandaloneFileComplete -> withUser(user, targetPath) is RcvStandaloneFileComplete -> withUser(user, targetPath)
is RcvFileError -> withUser(user, json.encodeToString(chatItem_)) is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer")
is SndFileCancelled -> json.encodeToString(chatItem_) is SndFileCancelled -> json.encodeToString(chatItem_)
is SndStandaloneFileCreated -> noDetails() is SndStandaloneFileCreated -> noDetails()
is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem)) is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem))
@@ -8,3 +8,4 @@ expect fun UriHandler.sendEmail(subject: String, body: CharSequence)
expect fun ClipboardManager.shareText(text: String) expect fun ClipboardManager.shareText(text: String)
expect fun shareFile(text: String, fileSource: CryptoFile) expect fun shareFile(text: String, fileSource: CryptoFile)
expect fun openFile(fileSource: CryptoFile)
@@ -32,8 +32,8 @@ fun ContactPreferencesView(
) { ) {
val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } } val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
val ct = contact.value ?: return val ct = contact.value ?: return
var featuresAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) } var featuresAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
var currentFeaturesAllowed by rememberSaveable(ct, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) } var currentFeaturesAllowed by rememberSaveable(ct, user, stateSaver = serializableSaver()) { mutableStateOf(featuresAllowed) }
fun savePrefs(afterSave: () -> Unit = {}) { fun savePrefs(afterSave: () -> Unit = {}) {
withBGApi { withBGApi {
@@ -1,6 +1,7 @@
package chat.simplex.common.views.chat.item package chat.simplex.common.views.chat.item
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -28,6 +29,7 @@ import java.net.URI
fun CIFileView( fun CIFileView(
file: CIFile?, file: CIFile?,
edited: Boolean, edited: Boolean,
showMenu: MutableState<Boolean>,
receiveFile: (Long) -> Unit receiveFile: (Long) -> Unit
) { ) {
val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) val saveFileLauncher = rememberSaveFileLauncher(ciFile = file)
@@ -86,7 +88,7 @@ fun CIFileView(
) )
FileProtocol.LOCAL -> {} FileProtocol.LOCAL -> {}
} }
file.fileStatus is CIFileStatus.RcvComplete || (file.fileStatus is CIFileStatus.SndStored && file.fileProtocol == FileProtocol.LOCAL) -> { file.forwardingAllowed() -> {
withLongRunningApi(slow = 600_000) { withLongRunningApi(slow = 600_000) {
var filePath = getLoadedFilePath(file) var filePath = getLoadedFilePath(file)
if (chatModel.connectedToRemote() && filePath == null) { if (chatModel.connectedToRemote() && filePath == null) {
@@ -136,8 +138,7 @@ fun CIFileView(
Box( Box(
Modifier Modifier
.size(42.dp) .size(42.dp)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp)),
.clickable(onClick = { fileAction() }),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (file != null) { if (file != null) {
@@ -154,7 +155,13 @@ fun CIFileView(
FileProtocol.SMP -> progressIndicator() FileProtocol.SMP -> progressIndicator()
FileProtocol.LOCAL -> {} FileProtocol.LOCAL -> {}
} }
is CIFileStatus.SndComplete -> fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) is CIFileStatus.SndComplete -> {
if ((file.forwardingAllowed() || (chatModel.connectedToRemote() && CIFile.cachedRemoteFileRequests[file.fileSource] == true))) {
fileIcon()
} else {
fileIcon(innerIcon = painterResource(MR.images.ic_check_filled))
}
}
is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
is CIFileStatus.RcvInvitation -> is CIFileStatus.RcvInvitation ->
@@ -181,7 +188,13 @@ fun CIFileView(
} }
Row( Row(
Modifier.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), Modifier
.combinedClickable(
onClick = { fileAction() },
onLongClick = { showMenu.value = true }
)
.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
//Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp),
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(2.dp) horizontalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
@@ -2,8 +2,7 @@ package chat.simplex.common.views.chat.item
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.*
import androidx.compose.material.Icon
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -113,21 +112,49 @@ fun CIImageView(
} }
@Composable @Composable
fun ImageView(painter: Painter, onClick: () -> Unit) { fun ImageView(painter: Painter, image: String, fileSource: CryptoFile?, onClick: () -> Unit) {
Image( // On my Android device Compose fails to display 6000x6000 px WebP image with exception:
painter, // IllegalStateException: Recording currently in progress - missing #endRecording() call?
contentDescription = stringResource(MR.strings.image_descr), // but can display 5000px image. Using even lower value here just to feel safer.
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView // It happens to WebP because it's not compressed while sending since it can be animated.
// if text is short and take all available width if text is long if (painter.intrinsicSize.width <= 4320 && painter.intrinsicSize.height <= 4320) {
modifier = Modifier Image(
painter,
contentDescription = stringResource(MR.strings.image_descr),
// .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView
// if text is short and take all available width if text is long
modifier = Modifier
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
.combinedClickable(
onLongClick = { showMenu.value = true },
onClick = onClick
)
.onRightClick { showMenu.value = true },
contentScale = ContentScale.FillWidth,
)
} else {
Box(Modifier
.width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH)
.combinedClickable( .combinedClickable(
onLongClick = { showMenu.value = true }, onLongClick = { showMenu.value = true },
onClick = onClick onClick = {}
) )
.onRightClick { showMenu.value = true }, .onRightClick { showMenu.value = true },
contentScale = ContentScale.FillWidth, contentAlignment = Alignment.Center
) ) {
imageView(base64ToBitmap(image), onClick = {
if (fileSource != null) {
openFile(fileSource)
}
})
Icon(
painterResource(MR.images.ic_open_in_new),
contentDescription = stringResource(MR.strings.image_descr),
modifier = Modifier.size(30.dp),
tint = MaterialTheme.colors.primary,
)
}
}
} }
fun fileSizeValid(): Boolean { fun fileSizeValid(): Boolean {
@@ -172,9 +199,9 @@ fun CIImageView(
} }
} }
val loaded = res.value val loaded = res.value
if (loaded != null) { if (loaded != null && file != null) {
val (imageBitmap, data, _) = loaded val (imageBitmap, data, _) = loaded
SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) }) SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) })
} else { } else {
imageView(base64ToBitmap(image), onClick = { imageView(base64ToBitmap(image), onClick = {
if (file != null) { if (file != null) {
@@ -204,14 +204,10 @@ fun ChatItemView(
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests } val cachedRemoteReqs = remember { CIFile.cachedRemoteFileRequests }
fun fileForwardingAllowed() = when {
cItem.file != null && chatModel.connectedToRemote() && cachedRemoteReqs[cItem.file.fileSource] != false && cItem.file.loaded -> true
getLoadedFilePath(cItem.file) != null -> true
else -> false
}
val copyAndShareAllowed = when { val copyAndShareAllowed = when {
cItem.content.text.isNotEmpty() -> true cItem.content.text.isNotEmpty() -> true
fileForwardingAllowed() -> true cItem.file?.forwardingAllowed() == true -> true
else -> false else -> false
} }
@@ -261,7 +257,7 @@ fun ChatItemView(
}) })
} }
if (cItem.meta.itemDeleted == null && if (cItem.meta.itemDeleted == null &&
(cItem.file == null || fileForwardingAllowed()) && (cItem.file == null || cItem.file.forwardingAllowed()) &&
!cItem.isLiveDummy && !live !cItem.isLiveDummy && !live
) { ) {
ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = {
@@ -23,7 +23,6 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlin.math.min import kotlin.math.min
@@ -179,7 +178,7 @@ fun FramedItemView(
@Composable @Composable
fun ciFileView(ci: ChatItem, text: String) { fun ciFileView(ci: ChatItem, text: String) {
CIFileView(ci.file, ci.meta.itemEdited, receiveFile) CIFileView(ci.file, ci.meta.itemEdited, showMenu, receiveFile)
if (text != "" || ci.meta.isLive) { if (text != "" || ci.meta.isLive) {
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler)
} }
@@ -106,13 +106,13 @@
<string name="contact_already_exists">مخاطب از قبل وجود دارد</string> <string name="contact_already_exists">مخاطب از قبل وجود دارد</string>
<string name="invalid_connection_link">لینک اتصال نامعتبر</string> <string name="invalid_connection_link">لینک اتصال نامعتبر</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">لطفا بررسی کنید که از لینک صحیح استفاده کردید یا از مخاطبتان بخواهید لینک دیگری برایتان بفرستد.</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">لطفا بررسی کنید که از لینک صحیح استفاده کردید یا از مخاطبتان بخواهید لینک دیگری برایتان بفرستد.</string>
<string name="ensure_smp_server_address_are_correct_format_and_unique">مطمئن شوید قالب آدرس‌های سرور SMP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور SMP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="error_setting_network_config">خطا در به‌روزرسانی پیکربندی شبکه</string> <string name="error_setting_network_config">خطا در به‌روزرسانی پیکربندی شبکه</string>
<string name="failed_to_parse_chat_title">عدم موفقیت در بارگیری گپ</string> <string name="failed_to_parse_chat_title">عدم موفقیت در بارگیری گپ</string>
<string name="failed_to_create_user_invalid_title">نام نمایشی نامعتبر!</string> <string name="failed_to_create_user_invalid_title">نام نمایشی نامعتبر!</string>
<string name="failed_to_create_user_duplicate_desc">شما یک نمایه گپ با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید.</string> <string name="failed_to_create_user_duplicate_desc">شما یک نمایه گپ با نام نمایشی یکسان دارید، لطفا نام دیگری انتخاب کنید.</string>
<string name="error_creating_address">خطا در ایجاد نشانی</string> <string name="error_creating_address">خطا در ایجاد نشانی</string>
<string name="ensure_xftp_server_address_are_correct_format_and_unique">مطمئن شوید قالب آدرس‌های سرور XFTP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string> <string name="ensure_xftp_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور XFTP صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="error_accepting_contact_request">خطا در پذیرش درخواست مخاطب</string> <string name="error_accepting_contact_request">خطا در پذیرش درخواست مخاطب</string>
<string name="sender_may_have_deleted_the_connection_request">فرستنده ممکن است درخواست اتصال را حذف کرده باشد.</string> <string name="sender_may_have_deleted_the_connection_request">فرستنده ممکن است درخواست اتصال را حذف کرده باشد.</string>
<string name="error_deleting_note_folder">خطا در حذف یادداشت‌های خصوصی</string> <string name="error_deleting_note_folder">خطا در حذف یادداشت‌های خصوصی</string>
@@ -204,4 +204,482 @@
<string name="error_showing_desktop_notification">خطا در نمایش اعلان، با توسعه‌دهندگان تماس بگیرید.</string> <string name="error_showing_desktop_notification">خطا در نمایش اعلان، با توسعه‌دهندگان تماس بگیرید.</string>
<string name="lock_not_enabled">قفل SimpleX فعال نیست!</string> <string name="lock_not_enabled">قفل SimpleX فعال نیست!</string>
<string name="auth_device_authentication_is_disabled_turning_off">تصدیق دستگاه غیرفعال است. قفل SimpleX خاموش می‌شود.</string> <string name="auth_device_authentication_is_disabled_turning_off">تصدیق دستگاه غیرفعال است. قفل SimpleX خاموش می‌شود.</string>
<string name="icon_descr_sent_msg_status_sent">ارسال شده</string>
<string name="icon_descr_context">نماد زمینه</string>
<string name="icon_descr_cancel_image_preview">لغو پیش‌نمایش تصویر</string>
<string name="icon_descr_cancel_file_preview">لغو پیش‌نمایش پرونده</string>
<string name="system_restricted_background_warn"><![CDATA[برای فعال کردن اعلان‌ها، لطفا <b>مصرف باتری برنامه</b> / <b>نامحدود</b> را در تنظیمات برنامه انتخاب کنید.]]></string>
<string name="system_restricted_background_in_call_desc">برنامه ممکن است بعد از ۱ دقیقه در پس‌زمینه بسته شود.</string>
<string name="database_initialization_error_title">مقداردهی اولیه پایگاه داده ممکن نیست</string>
<string name="enter_passphrase_notification_title">عبارت عبور الزامی است</string>
<string name="enter_passphrase_notification_desc">برای دریافت اعلان‌ها، لطفا، عبارت عبور پایگاه داده را وارد کنید</string>
<string name="database_initialization_error_desc">پایگاه داده به درستی کار نمی‌کند. برای آگاهی بیشتر لمس کنید</string>
<string name="simplex_service_notification_title">سرویس SimpleX Chat</string>
<string name="simplex_service_notification_text">در حال دریافت پیام‌ها…</string>
<string name="la_notice_turn_on">روشن کردن</string>
<string name="la_auth_failed">عدم موفقیت تصدیق</string>
<string name="auth_unlock">گشودن قفل</string>
<string name="auth_confirm_credential">اطلاعات ورودتان را تایید کنید</string>
<string name="reply_verb">پاسخ</string>
<string name="edit_history">تاریخچه</string>
<string name="received_message">پیام دریافتی</string>
<string name="moderate_verb">حذف</string>
<string name="expand_verb">بسط دادن</string>
<string name="moderate_message_will_be_marked_warning">پیام برای تمام اعضا به عنوان حذف شده علامت‌گذاری خواهد شد.</string>
<string name="stop_file__confirm">توقف</string>
<string name="stop_rcv_file__message">دریافت پرونده متوقف خواهد شد.</string>
<string name="welcome">خوش آمدید!</string>
<string name="image_decoding_exception_title">خطا در رمزگشایی</string>
<string name="compose_send_direct_message_to_connect">ارسال پیام مستقیم برای اتصال</string>
<string name="loading_remote_file_desc">لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید.</string>
<string name="button_delete_contact">حذف مخاطب</string>
<string name="view_security_code">مشاهده رمز امنیتی</string>
<string name="verify_security_code">تایید رمز امنیتی</string>
<string name="send_disappearing_message">ارسال پیام ناپدید شونده</string>
<string name="add_contact">لینک دعوت یک‌بارمصرف</string>
<string name="create_group">ایجاد گروه محرمانه</string>
<string name="to_share_with_your_contact">(برای اشتراک‌گذاری با مخاطبتان)</string>
<string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">اگر لینک دعوت SimpleX Chat دریافت کردید، می‌توانید آن را در مرورگر خود باز کنید:</string>
<string name="la_lock_mode_passcode">ورود رمز عبور</string>
<string name="la_seconds">%d ثانیه</string>
<string name="la_current_app_passcode">رمز عبور فعلی</string>
<string name="copy_verb">کپی</string>
<string name="delete_message_mark_deleted_warning">پیام به عنوان حذف شده علامت‌گذاری خواهد شد. گیرنده‌ها قادر خواهند بود این پیام را آشکار کنند.</string>
<string name="icon_descr_sent_msg_status_send_failed">ارسال غیرموفق</string>
<string name="contact_tap_to_connect">برای اتصال لمس کنید</string>
<string name="video_decoding_exception_desc">امکان رمزگشایی ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید.</string>
<string name="image_descr">تصویر</string>
<string name="file_not_found">پرونده پیدا نشد</string>
<string name="chat_help_tap_button">لمس دکمه</string>
<string name="create_group_button_to_create_new_group"><![CDATA[<b>ایجاد گروه</b>: برای ایجاد یک گروه جدید.]]></string>
<string name="system_restricted_background_in_call_title">بدون تماس‌های پس‌زمینه</string>
<string name="system_restricted_background_in_call_warn"><![CDATA[برای برقراری تماس‌ها در پس‌زمینه، لطفا <b>مصرف باتری برنامه</b> / <b>نامحدود</b> را در تنظیمات برنامه انتخاب کنید.]]></string>
<string name="la_authenticate">تصدیق</string>
<string name="edit_verb">ویرایش</string>
<string name="no_info_on_delivery">بدون اطلاعات تحویل</string>
<string name="revoke_file__action">لغو پرونده</string>
<string name="group_connection_pending">در حال اتصال…</string>
<string name="you_have_no_chats">شما هیچ گپی ندارید</string>
<string name="loading_chats">بارگذاری گپ‌ها…</string>
<string name="video_will_be_received_when_contact_completes_uploading">ویدئو وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string>
<string name="maximum_supported_file_size">بیشترین اندازه پشتیبانی شده فعلی پرونده %1$s است.</string>
<string name="error_saving_file">خطا در ذخیره‌سازی پرونده</string>
<string name="loading_remote_file_title">در حال بارگیری پرونده</string>
<string name="delete_and_notify_contact">حذف و مخاطب را باخبر کن</string>
<string name="icon_descr_server_status_connected">متصل</string>
<string name="sync_connection_force_question">مذاکره مجدد رمزگذاری؟</string>
<string name="you_need_to_allow_to_send_voice">شما نیاز دارید به مخاطبتان اجازه ارسال پیام‌های صوتی دهید تا بتوانید آن‌ها را ارسال کنید.</string>
<string name="send_live_message">ارسال پیام زنده</string>
<string name="disappearing_message">پیام ناپدید شونده</string>
<string name="send_disappearing_message_custom_time">زمان سفارشی</string>
<string name="copied">به حافظه کپی شد</string>
<string name="add_contact_or_create_group">شروع گپ جدید</string>
<string name="from_gallery_button">از گالری</string>
<string name="call_service_notification_video_call">تماس تصویری</string>
<string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید.
\nاز شما خواسته خواهد شد قبل از فعال شدن این ویژگی، تصدیق را تکمیل کنید.</string>
<string name="la_no_app_password">عدم وجود رمز عبور</string>
<string name="la_minutes">%d دقیقه</string>
<string name="auth_enable_simplex_lock">فعال‌سازی قفل SimpleX</string>
<string name="la_enter_app_passcode">وارد کردن رمز عبور</string>
<string name="saved_chat_item_info_tab">ذخیره شده</string>
<string name="forwarded_chat_item_info_tab">فرستاده شده</string>
<string name="forwarded_from_chat_item_info_title">فرستاده شده از</string>
<string name="recipients_can_not_see_who_message_from">گیرنده‌ها نمی‌توانند ببینند این پیام از طرف چه کسی است.</string>
<string name="reveal_verb">آشکار کردن</string>
<string name="hide_verb">پنهان کردن</string>
<string name="delete_messages__question">%d پیام حذف شود؟</string>
<string name="delete_message_cannot_be_undone_warning">پیام حذف خواهد شد - این عمل قابل برگشت نیست!</string>
<string name="moderate_message_will_be_deleted_warning">پیام برای تمام اعضا حذف خواهد شد.</string>
<string name="for_me_only">حذف برای من</string>
<string name="forward_chat_item">فرستادن</string>
<string name="download_file">بارگیری</string>
<string name="icon_descr_edited">ویرایش شده</string>
<string name="icon_descr_sent_msg_status_unauthorized_send">ارسال غیرمجاز</string>
<string name="icon_descr_received_msg_status_unread">خوانده نشده</string>
<string name="personal_welcome">خوش آمدید، %1$s!</string>
<string name="this_text_is_available_in_settings">این متن در تنظیمات دردسترس است</string>
<string name="chat_with_developers">گپ با توسعه‌دهندگان</string>
<string name="no_filtered_chats">گپ پالایش شده‌ای نیست</string>
<string name="connect_with_contact_name_question">به %1$s متصل شوید؟</string>
<string name="no_selected_chat">گپی انتخاب نشده</string>
<string name="forward_message">فرستادن پیام…</string>
<string name="videos_limit_title">تعداد ویدئوی بیش از اندازه!</string>
<string name="videos_limit_desc">فقط ۱۰ ویدئو در هر زمان می‌توان ارسال کرد</string>
<string name="observer_cant_send_message_title">شما نمی‌توانید پیامی بفرستید!</string>
<string name="observer_cant_send_message_desc">لطفا با مدیر گروه تماس بگیرید.</string>
<string name="only_owners_can_enable_files_and_media">فقط صاحبان گروه می‌توانند پرونده‌ها و رسانه را فعال کنند.</string>
<string name="icon_descr_asked_to_receive">دریافت تصویر درخواست شده</string>
<string name="waiting_for_image">در انتظار تصویر</string>
<string name="image_saved">تصویر در گالری ذخیره شد</string>
<string name="video_descr">ویدئو</string>
<string name="video_will_be_received_when_contact_is_online">ویدئو وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.</string>
<string name="icon_descr_file">پرونده</string>
<string name="large_file">پرونده حجیم!</string>
<string name="waiting_for_file">در انتظار پرونده</string>
<string name="file_will_be_received_when_contact_completes_uploading">پرونده وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string>
<string name="voice_message_with_duration">پیام صوتی (%1$s)</string>
<string name="notifications">اعلان‌ها</string>
<string name="delete_contact_all_messages_deleted_cannot_undo_warning">مخاطب و تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string>
<string name="allow_voice_messages_question">پیام‌های صوتی مجازند؟</string>
<string name="voice_messages_prohibited">پیام‌های صوتی ممنوع هستند!</string>
<string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 دسکتاپ: از برنامه رمز QR نمایش داده شده را اسکن کنید، از طریق <b>اسکن رمز QR</b>.]]></string>
<string name="share_file">اشتراک‌گذاری پرونده…</string>
<string name="images_limit_title">تعداد تصویر بیش از اندازه!</string>
<string name="tap_to_scan">برای اسکن لمس کنید</string>
<string name="choose_file_title">انتخاب پرونده</string>
<string name="to_start_a_new_chat_help_header">برای شروع گپ جدید</string>
<string name="delivery">تحویل</string>
<string name="delete_member_message__question">پیام عضو حذف شود؟</string>
<string name="for_everybody">برای همه</string>
<string name="stop_snd_file__message">ارسال پرونده متوقف خواهد شد.</string>
<string name="stop_rcv_file__title">دریافت پرونده متوقف شود؟</string>
<string name="revoke_file__message">پرونده از سرورها حذف خواهد شد.</string>
<string name="member_contact_send_direct_message">ارسال پیام مستقیم</string>
<string name="share_image">اشتراک‌گذاری رسانه…</string>
<string name="images_limit_desc">فقط ۱۰ تصویر در هر زمان می‌توان ارسال کرد</string>
<string name="call_service_notification_audio_call">تماس صوتی</string>
<string name="la_please_remember_to_store_password">لطفا آن را به خاطر داشته باشید یا به طور امن ذخیره کنید - راهی برای بازیابی کلمه عبور وجود ندارد!</string>
<string name="auth_simplex_lock_turned_on">قفل SimpleX روشن است</string>
<string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">وقتی برنامه را شروع می‌کنید یا بعد از ۳۰ ثانیه در پس‌زمینه آن از سر می‌گیرید، نیاز به تصدیق خواهید داشت.</string>
<string name="search_verb">جستجو</string>
<string name="auth_disable_simplex_lock">غیرفعال کردن قفل SimpleX</string>
<string name="share_verb">اشتراک‌گذاری</string>
<string name="save_verb">ذخیره</string>
<string name="send_disappearing_message_send">ارسال</string>
<string name="icon_descr_cancel_live_message">لغو پیام زنده</string>
<string name="back">بازگشت</string>
<string name="cancel_verb">لغو</string>
<string name="ok">موافقت</string>
<string name="live_message">پیام زنده!</string>
<string name="send_live_message_desc">ارسال پیام زنده - همزمان با تایپ کردن برای گیرنده‌ها به‌روز می‌شود</string>
<string name="auth_log_in_using_credential">ورود با استفاده از اطلاعات ورودتان</string>
<string name="auth_unavailable">تصدیق موجود نیست</string>
<string name="message_delivery_error_title">خطا در تحویل پیام</string>
<string name="info_menu">اطلاعات</string>
<string name="sent_message">پیام ارسالی</string>
<string name="message_delivery_error_desc">به احتمال زیاد این مخاطب اتصال با شما را حذف کرده است.</string>
<string name="no_history">بدون تاریخچه</string>
<string name="delete_verb">حذف</string>
<string name="delete_message__question">پیام حذف شود؟</string>
<string name="revoke_file__title">پرونده لغو شود؟</string>
<string name="stop_file__action">توقف پرونده</string>
<string name="stop_snd_file__title">ارسال پرونده متوقف شود؟</string>
<string name="revoke_file__confirm">لغو</string>
<string name="contact_connection_pending">در حال اتصال…</string>
<string name="your_chats">گپ‌ها</string>
<string name="group_preview_you_are_invited">شما به گروه دعوت شده‌اید</string>
<string name="group_preview_join_as">پیوستن به عنوان %s</string>
<string name="search_or_paste_simplex_link">جستجو یا الصاق لینک SimpleX</string>
<string name="tap_to_start_new_chat">برای شروع گپ جدید لمس کنید</string>
<string name="share_message">اشتراک‌گذاری پیام…</string>
<string name="icon_descr_waiting_for_image">در انتظار تصویر</string>
<string name="voice_message_send_text">پیام صوتی…</string>
<string name="text_field_set_contact_placeholder">تعیین نام مخاطب…</string>
<string name="icon_descr_server_status_disconnected">قطع اتصال</string>
<string name="icon_descr_server_status_pending">در حال انتظار</string>
<string name="switch_receiving_address_question">نشانی‌ دریافت تغییر کند؟</string>
<string name="abort_switch_receiving_address_desc">تغییر نشانی‌ لغو خواهد شد. نشانی‌ دریافت پیشین استفاده خواهد شد.</string>
<string name="sync_connection_force_confirm">مذاکره مجدد</string>
<string name="only_group_owners_can_enable_voice">فقط صاحبان گروه می‌توانند پیام‌های صوتی را فعال کنند.</string>
<string name="confirm_verb">تایید</string>
<string name="reset_verb">بازنشاندن</string>
<string name="scan_QR_code">اسکن رمز QR</string>
<string name="connect_via_link_or_qr_from_clipboard_or_in_person">(اسکن یا الصاق از حافظه)</string>
<string name="only_stored_on_members_devices">(تنها ذخیره شده توسط اعضای گروه)</string>
<string name="enable_camera_access">فعال کردن دسترسی دوربین</string>
<string name="toast_permission_denied">اجازه داده نشد!</string>
<string name="use_camera_button">دوربین</string>
<string name="thank_you_for_installing_simplex">سپاس برای نصب SimpleX Chat!</string>
<string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 موبایل: <b>گشودن در موبایل</b> را لمس کنید، سپس <b>اتصال</b> را در برنامه لمس کنید.]]></string>
<string name="accept_connection_request__question">درخواست اتصال پذیرفته شود؟</string>
<string name="voice_messages_not_allowed">پیام‌های صوتی مجاز نیستند</string>
<string name="simplex_links_not_allowed">لینک‌های SimpleX مجاز نیستند</string>
<string name="files_and_media_not_allowed">پرونده‌ها و رسانه مجاز نیست</string>
<string name="icon_descr_waiting_for_video">در انتظار ویدئو</string>
<string name="icon_descr_video_asked_to_receive">دریافت ویدئو درخواست شده</string>
<string name="icon_descr_video_snd_complete">ویدئو ارسال شد</string>
<string name="image_will_be_received_when_contact_completes_uploading">تصویر وقتی دریافت خواهد شد که مخاطبتان بارگذاری آن را تکمیل کند.</string>
<string name="voice_message">پیام صوتی</string>
<string name="icon_descr_record_voice_message">ضبط پیام صوتی</string>
<string name="icon_descr_send_message">ارسال پیام</string>
<string name="no_details">بدون جزئیات</string>
<string name="connect_via_link_or_qr">اتصال به وسیله لینک / رمز QR</string>
<string name="camera_not_available">دوربین موجود نیست</string>
<string name="gallery_image_button">تصویر</string>
<string name="share_one_time_link">ایجاد لینک دعوت یک‌بارمصرف</string>
<string name="you_can_connect_to_simplex_chat_founder"><![CDATA[می‌توانید <font color="#0088ff">برای پرسش هر سوال و دریافت اطلاعات به توسعه‌دهندگان SimpleX Chat متصل شوید</font>.]]></string>
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>افزودن مخاطب</b>: برای ایجاد لینک دعوت جدید، یا اتصال از طریق لینکی که دریافت کردید.]]></string>
<string name="to_connect_via_link_title">اتصال از طریق لینک</string>
<string name="image_will_be_received_when_contact_is_online">تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="la_lock_mode">نوع قفل SimpleX</string>
<string name="la_lock_mode_system">تصدیق سامانه</string>
<string name="la_change_app_passcode">تغییر رمز عبور</string>
<string name="in_reply_to">در پاسخ به</string>
<string name="la_immediately">فوری</string>
<string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید.</string>
<string name="allow_verb">اجازه دادن</string>
<string name="attach">ضمیمه</string>
<string name="image_decoding_exception_desc">امکان رمزگشایی تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید.</string>
<string name="files_and_media_prohibited">پرونده‌ها و رسانه ممنوع است!</string>
<string name="icon_descr_image_snd_complete">تصویر ارسال شد</string>
<string name="waiting_for_video">در انتظار ویدئو</string>
<string name="contact_sent_large_file">مخاطبتان پرونده‌ای ارسال کرد که از بیشترین اندازه پشتیبانی شده (%1$s) بزرگتر است.</string>
<string name="file_will_be_received_when_contact_is_online">پرونده وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید.</string>
<string name="file_saved">پرونده ذخیره شد</string>
<string name="sync_connection_force_desc">رمزگذاری در حال کار است و نیازی به توافق رمزگذاری جدید نیست. ممکن است باعث بروز خطاهای اتصال شود!</string>
<string name="ask_your_contact_to_enable_voice">لطفا از مخاطبتان بخواهید ارسال پیام‌های صوتی را فعال کند.</string>
<string name="send_verb">ارسال</string>
<string name="gallery_video_button">ویدئو</string>
<string name="above_then_preposition_continuation">بالا، سپس:</string>
<string name="delete_contact_question">مخاطب حذف شود؟</string>
<string name="icon_descr_server_status_error">خطا</string>
<string name="switch_receiving_address_desc">آدرس دریافت به سرور متفاوتی تغییر خواهد یافت. تغییر نشانی وقتی تکمیل خواهد شد که فرستنده آنلاین شود.</string>
<string name="la_could_not_be_verified">تایید شما ممکن نیست؛ لطفا دوباره امتحان کنید.</string>
<string name="choose_file">پرونده</string>
<string name="you_are_observer">شما ناظر هستید</string>
<string name="clear_chat_question">گپ پاک شود؟</string>
<string name="clear_note_folder_warning">تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست!</string>
<string name="delete_contact_menu_action">حذف</string>
<string name="accept_contact_incognito_button">پذیرش ناشناس</string>
<string name="favorite_chat">برگزیده</string>
<string name="accept_contact_button">پذیرفتن</string>
<string name="reject_contact_button">رد کردن</string>
<string name="clear_note_folder_question">یادداشت‌های خصوصی پاک شود؟</string>
<string name="clear_verb">پاک کردن</string>
<string name="clear_chat_menu_action">پاک‌سازی</string>
<string name="delete_group_menu_action">حذف</string>
<string name="mark_unread">علامت‌گذاری به عنوان خوانده نشده</string>
<string name="set_contact_name">تعیین نام مخاطب</string>
<string name="contact_you_shared_link_with_wont_be_able_to_connect">مخاطبی که این لینک را با او به اشتراک گذاشتید قادر به اتصال نخواهد بود!</string>
<string name="if_you_choose_to_reject_the_sender_will_not_be_notified">اگر نپذیرید، فرستنده باخبر نخواهد شد.</string>
<string name="clear_chat_warning">تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌ها فقط برای شما حذف خواهند شد.</string>
<string name="clear_chat_button">پاک‌سازی گپ</string>
<string name="you_accepted_connection">اتصال را پذیرفتید</string>
<string name="alert_title_contact_connection_pending">مخاطب هنوز متصل نشده است!</string>
<string name="you_invited_a_contact">شما مخاطبی را دعوت کردید</string>
<string name="delete_pending_connection__question">اتصال در حال انتظار حذف شود؟</string>
<string name="connection_you_accepted_will_be_cancelled">اتصالی که پذیرفتید لغو خواهد شد!</string>
<string name="mute_chat">بی‌صدا</string>
<string name="unmute_chat">لغو بی‌صدا</string>
<string name="mark_read">علامت‌گذاری به عنوان خوانده شده</string>
<string name="unfavorite_chat">خلع برگزیده</string>
<string name="icon_descr_simplex_team">تیم SimpleX</string>
<string name="new_chat">گپ جدید</string>
<string name="how_to_use_simplex_chat">روش استفاده</string>
<string name="markdown_help">راهنمای مارکداون</string>
<string name="invalid_contact_link">لینک نامعتبر!</string>
<string name="share_invitation_link">اشتراک‌گذاری لینک یک بار مصرف</string>
<string name="or_scan_qr_code">یا رمز QR را اسکن کنید</string>
<string name="retry_verb">تلاش مجدد</string>
<string name="database_passphrase_and_export">عبارت عبور و صدور پایگاه داده</string>
<string name="smp_servers_preset_add">افزودن سرورهای از پیش تنظیم شده</string>
<string name="smp_servers_add_to_another_device">افزودن به دستگاه دیگر</string>
<string name="smp_servers_delete_server">حذف سرور</string>
<string name="how_to_use_your_servers">چگونه از سرورهای خود استفاده کنید</string>
<string name="image_descr_link_preview">تصویر پیش‌نمایش لینک</string>
<string name="your_chat_profiles">نمایه‌های گپ شما</string>
<string name="smp_servers">سرورهای SMP</string>
<string name="install_simplex_chat_for_terminal">SimpleX Chat را برای ترمینال نصب کنید</string>
<string name="star_on_github">در GitHub ستاره بزنید</string>
<string name="contribute">همکاری کنید</string>
<string name="image_descr_qr_code">رمز QR</string>
<string name="connect__a_new_random_profile_will_be_shared">یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد.</string>
<string name="connect_via_link">اتصال از طریق لینک</string>
<string name="one_time_link">لینک دعوت یک‌بارمصرف</string>
<string name="is_verified">%s تایید شده است</string>
<string name="create_chat_profile">ایجاد نمایه گپ</string>
<string name="chat_console">کنسول گپ</string>
<string name="smp_servers_test_server">آزمایش سرور</string>
<string name="smp_servers_enter_manually">وارد کردن دستی سرور</string>
<string name="enter_one_ICE_server_per_line">سرورهای ICE (یکی در هر خط)</string>
<string name="error_saving_ICE_servers">خطا در ذخیره کردن سرورهای ICE</string>
<string name="icon_descr_profile_image_placeholder">جایگزین تصویر نمایه</string>
<string name="icon_descr_close_button">دکمه بستن</string>
<string name="icon_descr_cancel_link_preview">لغو پیش‌نمایش لینک</string>
<string name="icon_descr_settings">تنظیمات</string>
<string name="icon_descr_help">کمک</string>
<string name="image_descr_simplex_logo">لوگوی SimpleX</string>
<string name="icon_descr_email">ایمیل</string>
<string name="this_link_is_not_a_valid_connection_link">این لینک، یک لینک اتصال معتبر نیست!</string>
<string name="connection_request_sent">درخواست اتصال ارسال شد!</string>
<string name="you_will_be_connected_when_group_host_device_is_online">وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="paste_the_link_you_received_to_connect_with_your_contact">لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید…</string>
<string name="connect__your_profile_will_be_shared">نمایه شما %1$s به اشتراک گذاشته خواهد شد.</string>
<string name="scan_qr_to_connect_to_contact">برای اتصال، مخاطبتان می‌تواند رمز QR را اسکن یا از لینک در برنامه استفاده کند.</string>
<string name="if_you_cant_meet_in_person">اگر نمی‌توانید ملاقات حضوری داشته باشید، رمز QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.</string>
<string name="you_can_share_your_address">می‌توانید نشانی‌ خود را به صورت لینک یا رمز QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود.</string>
<string name="you_can_accept_or_reject_connection">وقتی اشخاص درخواست اتصال کنند، شما می‌توانید آن را بپذیرید یا رد کنید.</string>
<string name="or_show_this_qr_code">یا این رمز را نشان دهید</string>
<string name="you_can_view_invitation_link_again">می‌توانید دوباره لینک دعوت را در جزئیات اتصال مشاهده کنید.</string>
<string name="keep_invitation_link">نگه‌داشتن</string>
<string name="creating_link">در حال ایجاد لینک…</string>
<string name="paste_the_link_you_received">لینکی که دریافت کردید را الصاق کنید</string>
<string name="the_text_you_pasted_is_not_a_link">متن الصاقی شما یک لینک SimpleX نیست.</string>
<string name="smp_servers_your_server">سرور شما</string>
<string name="smp_servers_preset_server">سرور از پیش تنظیم شده</string>
<string name="smp_servers_invalid_address">نشانی‌ سرور نامعتبر!</string>
<string name="smp_servers_use_server_for_new_conn">به‌کارگیری برای اتصال‌های جدید</string>
<string name="invalid_qr_code">رمز QR نامعتبر</string>
<string name="incorrect_code">رمز امنیتی نادرست!</string>
<string name="scan_code_from_contacts_app">رمز امنیتی را از برنامه مخاطبتان اسکن کنید.</string>
<string name="mark_code_verified">علامت‌گذاری به عنوان تایید شده</string>
<string name="is_not_verified">%s تایید نشده است</string>
<string name="to_verify_compare">برای تایید رمزگذاری سرتاسر، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید.</string>
<string name="your_XFTP_servers">سرورهای XFTP شما</string>
<string name="how_to">چگونه</string>
<string name="using_simplex_chat_servers">در حال استفاده از سرورهای SimpleX Chat.</string>
<string name="configure_ICE_servers">تنظیم سرورهای ICE</string>
<string name="contact_wants_to_connect_with_you">می‌خواهد به شما متصل شود!</string>
<string name="you_will_be_connected_when_your_contacts_device_is_online">وقتی دستگاه مخاطبتان آنلاین شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[اگر نمی‌توانید ملاقات حضوری داشته باشید، <b>رمز QR را در تماس تصویری اسکن کنید</b>، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]></string>
<string name="keep_unused_invitation_question">دعوت استعمال نشده نگه داشته شود؟</string>
<string name="your_simplex_contact_address">نشانی‌ SimpleX شما</string>
<string name="markdown_in_messages">مارکداون در پیام‌ها</string>
<string name="chat_with_the_founder">ایده‌ها و سوالات را ارسال کنید</string>
<string name="send_us_an_email">به ما ایمیل بفرستید</string>
<string name="chat_lock">قفل SimpleX</string>
<string name="smp_servers_add">افزودن سرور…</string>
<string name="smp_servers_test_servers">آزمایش سرورها</string>
<string name="smp_servers_save">ذخیره سرورها</string>
<string name="smp_servers_test_failed">عدم موفقیت آزمایش سرور!</string>
<string name="smp_servers_test_some_failed">عدم موفقیت آزمایش چند سرور:</string>
<string name="smp_servers_scan_qr">اسکن رمز QR سرور</string>
<string name="your_SMP_servers">سرورهای SMP شما</string>
<string name="xftp_servers">سرورهای XFTP</string>
<string name="use_simplex_chat_servers__question">از سرورهای SimpleX Chat استفاده شود؟</string>
<string name="saved_ICE_servers_will_be_removed">سرورهای WebRTC ICE ذخیره شده حذف خواهند شد.</string>
<string name="your_ICE_servers">سرورهای ICE شما</string>
<string name="connect_button">اتصال</string>
<string name="paste_button">الصاق</string>
<string name="you_can_also_connect_by_clicking_the_link"><![CDATA[شما همچنین می‌توانید با کلیک روی لینک متصل شوید. اگر در مرورگر باز شد، روی دکمه <b>گشودن در برنامه موبایل</b> کلیک کنید.]]></string>
<string name="add_contact_tab">افزودن مخاطب</string>
<string name="simplex_address">نشانی‌ SimpleX</string>
<string name="clear_verification">پاک‌سازی تایید</string>
<string name="smp_servers_preset_address">نشانی‌ سرور از پیش تنظیم شده</string>
<string name="security_code">رمز امنیتی</string>
<string name="smp_servers_use_server">به‌کارگیری از سرور</string>
<string name="smp_servers_per_user">سرورها برای اتصال‌های جدید نمایه گپ فعلی شما</string>
<string name="smp_save_servers_question">سرورها ذخیره شوند؟</string>
<string name="rate_the_app">به برنامه امتیاز بدهید</string>
<string name="smp_servers_your_server_address">نشانی‌ سرور شما</string>
<string name="smp_servers_check_address">نشانی‌ سرور را بررسی و دوباره امتحان کنید.</string>
<string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">لازم است مخاطبتان آنلاین باشد تا اتصال کامل شود.
\nمی‌توانید این اتصال را لغو و مخاطب را حذف کنید (و بعدا با یک لینک جدید امتحان کنید).</string>
<string name="icon_descr_address">نشانی‌ SimpleX</string>
<string name="this_QR_code_is_not_a_link">این رمز QR یک لینک نیست!</string>
<string name="read_more_in_user_guide_with_link"><![CDATA[مطالعه بیشتر در <font color="#0088ff">راهنمای کاربر</font>.]]></string>
<string name="this_string_is_not_a_connection_link">این رشته متن، یک لینک اتصال نیست!</string>
<string name="code_you_scanned_is_not_simplex_link_qr_code">رمزی که اسکن کردید یک رمز QR لینک SimpleX نیست.</string>
<string name="scan_code">اسکن رمز</string>
<string name="image_descr_profile_image">تصویر نمایه</string>
<string name="icon_descr_more_button">بیشتر</string>
<string name="show_QR_code">نمایش رمز QR</string>
<string name="invalid_QR_code">رمز QR نامعتبر</string>
<string name="you_will_be_connected_when_your_connection_request_is_accepted">وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید!</string>
<string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[اگر نمی‌توانید ملاقات حضوری داشته باشید، <b>رمز QR را در تماس تصویری نمایش دهید</b>، یا لینک را به اشتراک بگذارید.]]></string>
<string name="your_chat_profile_will_be_sent_to_your_contact">نمایه گپ شما ارسال خواهد شد
\nبه مخاطبتان</string>
<string name="learn_more">اطلاعات بیشتر</string>
<string name="you_wont_lose_your_contacts_if_delete_address">اگر بعدا نشانی‌ خود را حذف کنید، مخاطبان خود را از دست نخواهید داد.</string>
<string name="share_this_1_time_link">این لینک دعوت یک‌بارمصرف را به اشتراک بگذارید</string>
<string name="tap_to_paste_link">برای الصاق لینک لمس کنید</string>
<string name="your_settings">تنظیمات شما</string>
<string name="network_use_onion_hosts_required_desc">میزبان‌های Onion برای اتصال الزامی خواهد بود.
\nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید.</string>
<string name="save_servers_button">ذخیره</string>
<string name="update_onion_hosts_settings_question">تنظیمات میزبان‌های onion. به روز شود؟</string>
<string name="port_verb">پورت</string>
<string name="network_enable_socks">از پروکسی SOCKS استفاده شود؟</string>
<string name="network_disable_socks">از اتصال مستقیم اینترنت استفاده شود؟</string>
<string name="network_settings">تنظیمات پیشرفته شبکه</string>
<string name="network_socks_proxy_settings">تنظیمات پروکسی SOCKS</string>
<string name="host_verb">هاست</string>
<string name="network_use_onion_hosts_required">الزامی</string>
<string name="network_use_onion_hosts_prefer_desc">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string>
<string name="network_use_onion_hosts_prefer_desc_in_alert">از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد.</string>
<string name="appearance_settings">ظاهر</string>
<string name="app_version_name">نسخه برنامه: v%s</string>
<string name="show_developer_options">نمایش گزینه‌های توسعه‌دهنده</string>
<string name="developer_options">شناسه‌های پایگاه داده و گزینه انزوای ترابری</string>
<string name="your_contacts_will_remain_connected">مخاطبانتان متصل باقی خواهند ماند.</string>
<string name="share_link">اشتراک‌گذاری لینک</string>
<string name="network_enable_socks_info">دسترسی به سرورها از طریق پروکسی SOCKS روی پورت %d؟ پروکسی باید قبل از فعال کردن این گزینه، راه اندازی شده باشد.</string>
<string name="network_session_mode_user_description"><![CDATA[یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر نمایه گپی که در برنامه دارید</b> استفاده خواهد شد.]]></string>
<string name="show_internal_errors">نمایش خطاهای داخلی</string>
<string name="show_slow_api_calls">نمایش تماس‌های کند API</string>
<string name="network_use_onion_hosts_no_desc_in_alert">از میزبان‌های Onion استفاده نخواهد شد.</string>
<string name="network_use_onion_hosts_no">خیر</string>
<string name="disable_onion_hosts_when_not_supported"><![CDATA[<i>استفاده از میزبان‌های onion.</i> را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]></string>
<string name="customize_theme_title">سفارشی کردن تم</string>
<string name="app_version_title">نسخه برنامه</string>
<string name="theme_colors_section_title">رنگ‌های تم</string>
<string name="core_version">نسخه هسته: v%s</string>
<string name="core_simplexmq_version">simplexmq: v%s (%2s)</string>
<string name="shutdown_alert_desc">اعلان‌ها از کار خواهند افتاد تا زمانی که برنامه را دوباره راه‌اندازی کنید</string>
<string name="create_address">ایجاد نشانی</string>
<string name="delete_address__question">نشانی حذف شود؟</string>
<string name="network_and_servers">شبکه و سرورها</string>
<string name="network_socks_toggle_use_socks_proxy">استفاده از پروکسی SOCKS</string>
<string name="network_proxy_port">پورت %d</string>
<string name="network_use_onion_hosts">استفاده از میزبان‌های onion.</string>
<string name="network_use_onion_hosts_prefer">وقتی موجود بود</string>
<string name="network_use_onion_hosts_no_desc">از میزبان‌های Onion استفاده نخواهد شد.</string>
<string name="network_session_mode_transport_isolation">انزوای ترابری</string>
<string name="network_session_mode_user">نمایه گپ</string>
<string name="network_session_mode_entity">اتصال</string>
<string name="app_version_code">ساختار برنامه: %s</string>
<string name="network_settings_title">تنظیمات شبکه</string>
<string name="network_use_onion_hosts_required_desc_in_alert">میزبان‌های Onion برای اتصال الزامی خواهد بود.</string>
<string name="show_dev_options">نمایش:</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>لطفا توجه داشته باشید</b>: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]></string>
<string name="developer_options_section">گزینه‌های توسعه‌دهنده</string>
<string name="hide_dev_options">پنهان کردن:</string>
<string name="shutdown_alert_question">بسته شود؟</string>
<string name="all_your_contacts_will_remain_connected">تمام مخاطبانتان متصل باقی خواهند ماند.</string>
<string name="all_your_contacts_will_remain_connected_update_sent">تمام مخاطبانتان متصل باقی خواهند ماند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string>
<string name="network_disable_socks_info">اگر تایید کنید، سرورهای پیام‌رسانی خواهند توانست نشانی‌ IP، و فراهم‌کننده شما را ببینند - و این که به چه سرورهایی متصل می‌شوید.</string>
<string name="ensure_ICE_server_address_are_correct_format_and_unique">مطمئن شوید قالب نشانی‌های سرور WebRTC ICE صحیح است، در خط‌های جدا نوشته شده و تکرار نشده‌اند.</string>
<string name="network_session_mode_entity_description">یک اتصال جدای TCP (و اطلاعات ورود SOCKS) <b>برای هر مخاطب و عضو گروه</b> استفاده خواهد شد.
\n<b>لطفا توجه داشته باشید</b>: اگر اتصال‌های زیادی داشته باشید، مصرف باتری و ترافیک شما می‌تواند به شکل قابل توجه بالاتر باشد و بعضی اتصال‌ها ممکن است با موفقیت انجام نشوند.</string>
<string name="update_network_session_mode_question">حالت انزوای ترابری به روز شود؟</string>
<string name="edit_image">ویرایش تصویر</string>
<string name="create_simplex_address">ایجاد نشانی‌ SimpleX</string>
<string name="share_with_contacts">اشتراک‌گذاری با مخاطبان</string>
<string name="profile_update_will_be_sent_to_contacts">به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string>
<string name="auto_accept_contact">پذیرفتن خودکار</string>
<string name="enter_welcome_message_optional">ورود پیام خوشامدگویی…(اختیاری)</string>
<string name="save_settings_question">تنظیمات ذخیره شوند؟</string>
<string name="save_auto_accept_settings">ذخیره تنظیمات پذیرفتن خودکار</string>
<string name="delete_address">حذف نشانی</string>
<string name="email_invite_body">سلام!
\nبه وسیله SimpleX Chat به من متصل شوید: %s</string>
<string name="dont_create_address">نشانی ایجاد نشود</string>
<string name="continue_to_next_step">ادامه</string>
<string name="your_current_profile">نمایه فعلی شما</string>
<string name="full_name__field">نام کامل:</string>
<string name="save_and_notify_contacts">ذخیره و مخاطبان مطلع شوند</string>
<string name="email_invite_subject">بیایید در SimpleX Chat گفتگو کنیم</string>
<string name="save_preferences_question">تنظیمات ذخیره شوند؟</string>
<string name="hide_profile">پنهان کردن نمایه</string>
<string name="password_to_show">کلمه عبور برای نمایش</string>
<string name="save_profile_password">ذخیره کلمه عبور نمایه</string>
<string name="stop_sharing_address">اشتراک‌گذاری نشانی متوقف شود؟</string>
<string name="stop_sharing">توقف اشتراک‌گذاری</string>
<string name="you_can_create_it_later">می‌توانید بعدا آن را ایجاد کنید</string>
<string name="you_can_make_address_visible_via_settings">می‌توانید آن را از طریق تنظیمات برای مخاطبان SimpleX خود قابل رویت کنید.</string>
<string name="display_name__field">نام نمایه:</string>
<string name="delete_image">حذف تصویر</string>
<string name="save_and_notify_group_members">ذخیره و اعضای گروه مطلع شوند</string>
<string name="save_and_notify_contact">ذخیره و مخاطب مطلع شود</string>
<string name="exit_without_saving">خروج بدون ذخیره کردن</string>
<string name="to_reveal_profile_enter_password">برای آشکار کردن نمایه پنهان خود، یک کلمه عبور کامل در فیلد جستجو در صفحه نمایه‌های گپتان وارد کنید.</string>
<string name="invite_friends">دعوت از دوستان</string>
<string name="confirm_password">تایید کلمه عبور</string>
<string name="hidden_profile_password">کلمه عبور نمایه پنهان</string>
<string name="add_address_to_your_profile">به نمایه خود نشانی اضافه کنید، تا مخاطبانتان بتوانند آن را با اشخاص دیگر به اشتراک بگذارند. به‌روزرسانی نمایه به مخاطبانتان ارسال خواهد شد.</string>
<string name="share_address_with_contacts_question">نشانی با مخاطبان به اشتراک گذاشته شود؟</string>
<string name="create_address_and_let_people_connect">یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند.</string>
<string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند.</string>
<string name="error_saving_user_password">خطا در ذخیره کردن کلمه عبور کاربر</string>
</resources> </resources>
@@ -180,9 +180,9 @@
<string name="change_member_role_question">Csoport szerepkör megváltoztatása?</string> <string name="change_member_role_question">Csoport szerepkör megváltoztatása?</string>
<string name="change_lock_mode">Zárolási mód megváltoztatása</string> <string name="change_lock_mode">Zárolási mód megváltoztatása</string>
<string name="notification_contact_connected">Kapcsolódott</string> <string name="notification_contact_connected">Kapcsolódott</string>
<string name="rcv_group_event_member_connected">kapcsolódva</string> <string name="rcv_group_event_member_connected">kapcsolódott</string>
<string name="connect_via_link_verb">Kapcsolódás</string> <string name="connect_via_link_verb">Kapcsolódás</string>
<string name="group_member_status_connected">kapcsolódva</string> <string name="group_member_status_connected">kapcsolódott</string>
<string name="connected_mobile">Csatlakoztatott telefon</string> <string name="connected_mobile">Csatlakoztatott telefon</string>
<string name="server_connected">kapcsolódva</string> <string name="server_connected">kapcsolódva</string>
<string name="change_role">Szerepkör megváltoztatása</string> <string name="change_role">Szerepkör megváltoztatása</string>
@@ -1151,7 +1151,7 @@
<string name="callstate_received_answer">fogadott válasz…</string> <string name="callstate_received_answer">fogadott válasz…</string>
<string name="restore_database_alert_title">Adatbázismentés visszaállítása?</string> <string name="restore_database_alert_title">Adatbázismentés visszaállítása?</string>
<string name="simplex_service_notification_text">Üzenetek fogadása…</string> <string name="simplex_service_notification_text">Üzenetek fogadása…</string>
<string name="rcv_group_event_2_members_connected">%s és %s kapcsolódtak</string> <string name="rcv_group_event_2_members_connected">%s és %s kapcsolódott</string>
<string name="you_are_observer">megfigyelő szerep</string> <string name="you_are_observer">megfigyelő szerep</string>
<string name="port_verb">Port</string> <string name="port_verb">Port</string>
<string name="set_passcode">Jelkód beállítása</string> <string name="set_passcode">Jelkód beállítása</string>
@@ -46,7 +46,7 @@
<string name="bold_text">太文字</string> <string name="bold_text">太文字</string>
<string name="icon_descr_audio_call">音声通話</string> <string name="icon_descr_audio_call">音声通話</string>
<string name="settings_audio_video_calls">音声とビデオ通話</string> <string name="settings_audio_video_calls">音声とビデオ通話</string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>※注意※</b>喪失したら、パスフレーズの回復・変更ができません。]]></string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>※注意※</b>紛失するとパスフレーズの回復・変更ができません。]]></string>
<string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string> <string name="all_group_members_will_remain_connected">グループ全員の接続が継続します。</string>
<string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string> <string name="allow_your_contacts_to_send_disappearing_messages">送信相手が消えるメッセージを送るのを許可する。</string>
<string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。(24時間)</string> <string name="allow_your_contacts_irreversibly_delete">送信相手が永久メッセージ削除するのを許可する。(24時間)</string>
@@ -179,7 +179,7 @@
<string name="chat_is_stopped_indication">チャットが停止してます。</string> <string name="chat_is_stopped_indication">チャットが停止してます。</string>
<string name="network_disable_socks_info">承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。</string> <string name="network_disable_socks_info">承諾すると、メッセージのサーバがIPアドレス、ISP、接続サーバを特定できます。</string>
<string name="chat_preferences">チャット設定</string> <string name="chat_preferences">チャット設定</string>
<string name="clear_chat_button">消す</string> <string name="clear_chat_button">チャットを削除</string>
<string name="icon_descr_close_button">閉じるボタン</string> <string name="icon_descr_close_button">閉じるボタン</string>
<string name="clear_verification">未検証の状態に戻す</string> <string name="clear_verification">未検証の状態に戻す</string>
<string name="image_saved">画像をギャラリーに保存しました。</string> <string name="image_saved">画像をギャラリーに保存しました。</string>
@@ -868,7 +868,7 @@
<string name="smp_servers_scan_qr">サーバのQRコードを読み込む</string> <string name="smp_servers_scan_qr">サーバのQRコードを読み込む</string>
<string name="smp_servers_test_some_failed">テストに失敗したサーバがあります:</string> <string name="smp_servers_test_some_failed">テストに失敗したサーバがあります:</string>
<string name="use_simplex_chat_servers__question">SimpleX Chatサーバを使いますか?</string> <string name="use_simplex_chat_servers__question">SimpleX Chatサーバを使いますか?</string>
<string name="using_simplex_chat_servers">SimpleX Chatを使っています</string> <string name="using_simplex_chat_servers">SimpleX Chatサーバを使用中</string>
<string name="share_link">リンクを送る</string> <string name="share_link">リンクを送る</string>
<string name="core_simplexmq_version">simplexmq: バージョン%s (%2s)</string> <string name="core_simplexmq_version">simplexmq: バージョン%s (%2s)</string>
<string name="callstate_waiting_for_answer">応答を待機中…</string> <string name="callstate_waiting_for_answer">応答を待機中…</string>
@@ -88,4 +88,71 @@
<string name="permissions_camera_and_record_audio">Camera și microfon</string> <string name="permissions_camera_and_record_audio">Camera și microfon</string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Atenție</b>: arhiva va fi ștearsă.]]></string> <string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Atenție</b>: arhiva va fi ștearsă.]]></string>
<string name="icon_descr_cancel_file_preview">Anulează previzualizarea fișierului</string> <string name="icon_descr_cancel_file_preview">Anulează previzualizarea fișierului</string>
<string name="empty_chat_profile_is_created">Un profil de conversație gol cu numele furnizat este creat, și aplicația se deschide ca de obicei.</string>
<string name="connect_plan_already_connecting">Se conectează deja!</string>
<string name="notifications_mode_off_desc">Aplicația poate primi notificări doar când rulează, niciun serviciu în fundal nu va fi lansat.</string>
<string name="connect_plan_already_joining_the_group">Se alătură deja grupului!</string>
<string name="notifications_mode_service">Mereu pornit</string>
<string name="connect__a_new_random_profile_will_be_shared">Un nou profil aleatoriu va fi distribuit.</string>
<string name="keychain_is_storing_securely">Android Keystore este folosit pentru a stoca în siguranță fraza de acces - permite serviciului de notificare să funcționeze.</string>
<string name="rcv_group_and_other_events">și %d alte evenimente</string>
<string name="answer_call">Răspunde la apel</string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore va fi folosit pentru a stoca în siguranță fraza de acces după ce repornești aplicația sau schimbi fraza de acces - va permite primirea notificărilor.</string>
<string name="settings_section_title_app">APLICAȚIE</string>
<string name="create_group_button">Creează grup</string>
<string name="v4_6_audio_video_calls">Apeluri audio și video</string>
<string name="migrate_from_device_archive_and_upload">Arhivează și încarcă</string>
<string name="migrate_from_device_archiving_database">Bază de date de arhivare</string>
<string name="migrate_from_device_creating_archive_link">Se creează un link de arhivare</string>
<string name="share_one_time_link">Creează un link de invitare unic.</string>
<string name="auto_accept_images">Acceptă automat imagini</string>
<string name="call_service_notification_audio_call">Apel audio</string>
<string name="icon_descr_audio_call">Apel audio</string>
<string name="icon_descr_audio_off">Audio oprit</string>
<string name="settings_section_title_icon">PICTOGRAMĂ APLICAȚIE</string>
<string name="la_app_passcode">Cod de acces aplicație</string>
<string name="create_secret_group_title">Creează grup secret</string>
<string name="smp_server_test_create_queue">Creează coadă</string>
<string name="notifications_mode_service_desc">Serviciul în fundal rulează mereu - notificările vor fi afișate imediat ce sunt disponibile.</string>
<string name="la_authenticate">Autentificare</string>
<string name="la_auth_failed">Autentificare eșuată</string>
<string name="la_current_app_passcode">Codul de acces curent</string>
<string name="auth_unavailable">Autentificare indisponibilă</string>
<string name="attach">Atașează</string>
<string name="back">Înapoi</string>
<string name="create_group">Creează grup secret</string>
<string name="creating_link">Se creează link…</string>
<string name="audio_call_no_encryption">apel audio (necriptat e2e)</string>
<string name="smp_server_test_create_file">Creează fișier</string>
<string name="add_contact_button_to_create_link_or_connect_via_link"><![CDATA[<b>Adaugă contact</b>: pentru a crea un nou link de invitare, sau a te conecta printr-un link pe care l-ai primit.]]></string>
<string name="authentication_cancelled">Autentificare anulată</string>
<string name="app_passcode_replaced_with_self_destruct">Cod de acces aplicație este înlocuit cu cod de acces de autodistrugere.</string>
<string name="audio_video_calls">Apeluri audio/video</string>
<string name="available_in_v51">"
\nDisponibil în v5.1"</string>
<string name="v5_0_app_passcode">Cod de acces aplicație</string>
<string name="v5_6_app_data_migration">Migrare date aplicație</string>
<string name="agent_critical_error_title">Eroare critică</string>
<string name="migrate_to_device_apply_onion">Aplică</string>
<string name="create_profile">Creează profil</string>
<string name="create_another_profile_button">Creează profil</string>
<string name="v4_2_auto_accept_contact_requests">Acceptă automat cererile de contactare</string>
<string name="custom_time_picker_custom">personalizat</string>
<string name="maximum_supported_file_size">În prezent dimensiunea maximă pentru fișiere este %1$s.</string>
<string name="create_simplex_address">Creează adresă SimpleX</string>
<string name="auto_accept_contact">Acceptare automată</string>
<string name="create_your_profile">Creează-ți profilul</string>
<string name="settings_audio_video_calls">Apeluri audio și video</string>
<string name="icon_descr_audio_on">Audio pornit</string>
<string name="current_passphrase">Frază de acces curentă…</string>
<string name="create_group_link">Creează link pentru grup</string>
<string name="button_create_group_link">Creează link</string>
<string name="group_member_status_creator">creator</string>
<string name="color_background">Fundal</string>
<string name="v5_3_new_desktop_app_descr">Creează profil nou în aplicația desktop. 💻</string>
<string name="v5_3_encrypt_local_files_descr">Aplicația criptează fișierele locale noi (cu excepția videoclipurilor).</string>
<string name="group_member_role_author">autor</string>
<string name="v5_3_new_interface_languages_descr">Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate.</string>
<string name="calls_prohibited_with_this_contact">Apelurile audio/video sunt interzise.</string>
<string name="item_info_current">(prezent)</string>
</resources> </resources>
@@ -1769,5 +1769,5 @@
<string name="network_type_ethernet">Kablolu ethernet</string> <string name="network_type_ethernet">Kablolu ethernet</string>
<string name="settings_section_title_profile_images">Profil resimleri</string> <string name="settings_section_title_profile_images">Profil resimleri</string>
<string name="v5_7_shape_profile_images">Profil resimlerini şekillendir</string> <string name="v5_7_shape_profile_images">Profil resimlerini şekillendir</string>
<string name="v5_7_shape_profile_images_descr">Kare,daire, veya aralarında herhangi birşey.</string> <string name="v5_7_shape_profile_images_descr">Kare,daire, veya aralarında herhangi bir şey.</string>
</resources> </resources>
@@ -144,4 +144,32 @@
<string name="auto_accept_contact">Tự động chấp nhận</string> <string name="auto_accept_contact">Tự động chấp nhận</string>
<string name="back">Quay về</string> <string name="back">Quay về</string>
<string name="auto_accept_images">Tự động chấp nhận hình ảnh</string> <string name="auto_accept_images">Tự động chấp nhận hình ảnh</string>
<string name="audio_device_bluetooth">Bluetooth</string>
<string name="v5_4_better_groups">Nhóm tốt hơn</string>
<string name="v5_4_block_group_members">Chặn thành viên nhóm</string>
<string name="rcv_group_event_member_blocked">đã chặn %s</string>
<string name="v5_1_better_messages">Tin nhắn tốt hơn</string>
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Xin lưu ý</b>: việc sử dụng cùng một cơ sở dữ liệu trên hai thiết bị sẽ phá vỡ quá trình giải mã tin nhắn từ các kết nối của bạn, như một biện pháp bảo vệ.]]></string>
<string name="create_group_button_to_create_new_group"><![CDATA[<b>Tạo nhóm</b>: để tạo một nhóm mới.]]></string>
<string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Tốt nhất cho pin</b>. Bạn sẽ chỉ nhận được thông báo khi ứng dụng đang chạy (KHÔNG có dịch vụ nền).]]></string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Xin lưu ý</b>: bạn sẽ KHÔNG thể khôi phục hoặc thay đổi passphrase nếu bạn làm mất nó.]]></string>
<string name="member_blocked_by_admin">Bị chặn bởi quản trị viên</string>
<string name="block_member_button">Chặn thành viên</string>
<string name="member_info_member_blocked">đã chặn</string>
<string name="blocked_by_admin_item_description">bị chặn bởi quản trị viên</string>
<string name="blocked_item_description">đã chặn</string>
<string name="turning_off_service_and_periodic">Tính năng tối ưu hóa pin đang được kích hoạt, tắt dịch vụ nền và các yêu cầu định kỳ về tin nhắn mới. Bạn có thể kích hoạt lại chúng thông qua cài đặt.</string>
<string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Có thể tắt thông qua cài đặt</b> - thông báo vẫn sẽ được hiển thị khi ứng dụng đang chạy.]]></string>
<string name="block_member_question">Chặn thành viên?</string>
<string name="both_you_and_your_contacts_can_delete">Cả bạn và liên hệ của bạn đều có thể xóa tin nhắn đã gửi mà không thể hoàn tác. (24 tiếng)</string>
<string name="block_for_all">Chặn tất cả</string>
<string name="block_for_all_question">Chặn thành viên cho tất cả?</string>
<string name="block_member_confirmation">Chặn</string>
<string name="both_you_and_your_contact_can_send_voice">Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn thoại.</string>
<string name="both_you_and_your_contact_can_make_calls">Cả bạn và liên hệ của bạn đều có thể thực hiện cuộc gọi.</string>
<string name="bold_text">in đậm</string>
<string name="both_you_and_your_contact_can_send_disappearing">Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa.</string>
<string name="both_you_and_your_contact_can_add_message_reactions">Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn.</string>
<string name="socks_proxy_setting_limitations"><![CDATA[<b>Xin lưu ý</b>: relay tin nhắn và tệp được kết nối thông qua SOCKS proxy. Các cuộc gọi và bản xem trước liên kết sử dụng kết nối trực tiếp.]]></string>
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Tốt cho pin</b>. Dịch vụ nền kiểm tra tin nhắn 10 phút một lần. Bạn có thể bỏ lỡ các cuộc gọi hoặc tin nhắn khẩn cấp.]]></string>
</resources> </resources>
@@ -9,6 +9,7 @@ import java.io.File
import java.net.URI import java.net.URI
import java.net.URLEncoder import java.net.URLEncoder
import chat.simplex.res.MR import chat.simplex.res.MR
import java.awt.Desktop
actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { actual fun UriHandler.sendEmail(subject: String, body: CharSequence) {
val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20") val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20")
@@ -41,3 +42,30 @@ actual fun shareFile(text: String, fileSource: CryptoFile) {
}.launch(fileSource.filePath) }.launch(fileSource.filePath)
} }
} }
actual fun openFile(fileSource: CryptoFile) {
try {
val filePath = filePathForShare(fileSource) ?: return
Desktop.getDesktop().open(File(filePath))
} catch (e: Exception) {
Log.e(TAG, "Unable to open the file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
}
}
fun filePathForShare(fileSource: CryptoFile): String? {
return if (fileSource.cryptoArgs != null) {
val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit()
try {
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return null, tmpFile.absolutePath)
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
return null
}
tmpFile.absolutePath
} else {
getAppFilePath(fileSource.filePath)
}
}
@@ -59,20 +59,7 @@ actual fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) = w
} }
if (fileSource != null) { if (fileSource != null) {
val filePath: String = if (fileSource.cryptoArgs != null) { val filePath = filePathForShare(fileSource) ?: return@withLongRunningApi
val tmpFile = File(tmpDir, fileSource.filePath)
tmpFile.deleteOnExit()
try {
decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs ?: return@withLongRunningApi, tmpFile.absolutePath)
} catch (e: Exception) {
Log.e(TAG, "Unable to decrypt crypto file: " + e.stackTraceToString())
AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.error), text = e.stackTraceToString())
return@withLongRunningApi
}
tmpFile.absolutePath
} else {
getAppFilePath(fileSource.filePath)
}
when { when {
desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\"")) desktopPlatform.isWindows() -> clipboard.setText(AnnotatedString("\"${File(filePath).absolutePath}\""))
else -> clipboard.setText(AnnotatedString(filePath)) else -> clipboard.setText(AnnotatedString(filePath))
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11 kotlin.jvm.target=11
android.version_name=5.7.1 android.version_name=5.7.4
android.version_code=202 android.version_code=212
desktop.version_name=5.7.1 desktop.version_name=5.7.4
desktop.version_code=42 desktop.version_code=48
kotlin.version=1.9.23 kotlin.version=1.9.23
gradle.plugin.version=8.2.0 gradle.plugin.version=8.2.0
+1 -1
View File
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 757742f5ed4eacc6cd29021983b3b5961e40a6d8 tag: df35c50b99a1291aac3ff86ed71387bb4f03c984
source-repository-package source-repository-package
type: git type: git
+1 -1
View File
@@ -228,7 +228,7 @@ Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro přípa
## Připojte se ke skupině uživatelů ## Připojte se ke skupině uživatelů
Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) Pokud se chcete na něco zeptat, můžete se připojit k anglicky mluvící skupině: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
Existuje také několik skupin v jiných jazycích než v angličtině, do kterých jsme nechali přeložit rozhraní aplikací. Tyto skupiny slouží k testování a pokládání otázek ostatním uživatelům SimpleX Chat. Ne vždy v nich odpovídáme na dotazy, proto je prosím pokládejte v některé z anglicky mluvících skupin. Existuje také několik skupin v jiných jazycích než v angličtině, do kterých jsme nechali přeložit rozhraní aplikací. Tyto skupiny slouží k testování a pokládání otázek ostatním uživatelům SimpleX Chat. Ne vždy v nich odpovídáme na dotazy, proto je prosím pokládejte v některé z anglicky mluvících skupin.
+1 -1
View File
@@ -48,7 +48,7 @@
## Rejoindre des groupes d'utilisateurs ## Rejoindre des groupes d'utilisateurs
Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX-Group-2](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FQP8zaGjjmlXV-ix_Er4JgJ0lNPYGS1KX%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEApAgBkRZ3x12ayZ7sHrjHQWNMvqzZpWUgM_fFCUdLXwo%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xWpPXEZZsQp_F7vwAcAYDw%3D%3D%22%7D) Vous pouvez rejoindre un groupe anglophone pour poser des questions : [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
Il existe des groupes dans d'autres langues, dans lesquelles l'interface de l'application a été traduite. Ces groupes servent à tester et à poser des questions aux autres utilisateurs de SimpleX Chat : Il existe des groupes dans d'autres langues, dans lesquelles l'interface de l'application a été traduite. Ces groupes servent à tester et à poser des questions aux autres utilisateurs de SimpleX Chat :
+1 -1
View File
@@ -74,7 +74,7 @@ Wiadomości nieprzestrzegające tych zasad będą usuwane, prawo do wysyłania w
Jeśli chcesz zadać jakieś pytania, możesz dołączyć do anglojęzycznej grupy użytkowników: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D) Jeśli chcesz zadać jakieś pytania, możesz dołączyć do anglojęzycznej grupy użytkowników: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
Istnieje również [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F6eHqy7uAbZPOcA6qBtrQgQquVlt4Ll91%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAqV_pg3FF00L98aCXp4D3bOs4Sxv_UmSd-gb0juVoQVs%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22XonlixcHBIb2ijCehbZoiw%3D%3D%22%7D) dla deweloperów, którzy na bazie platformy SimpleX tworzą: Istnieje również [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) dla deweloperów, którzy na bazie platformy SimpleX tworzą:
- czatboty i automatyzacje - czatboty i automatyzacje
- integracje z innymi aplikacjami - integracje z innymi aplikacjami
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat name: simplex-chat
version: 5.7.1.0 version: 5.7.4.1
#synopsis: #synopsis:
#description: #description:
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."757742f5ed4eacc6cd29021983b3b5961e40a6d8" = "1wzihy98ahdmnp542bh7crs8sjbw2llgvhvy8n70rvcrlh00xsd4"; "https://github.com/simplex-chat/simplexmq.git"."df35c50b99a1291aac3ff86ed71387bb4f03c984" = "0a3vvay14a0f7ypl94fag23g0z7b1kpq3l36kw6jgb0fjhr84m83";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+3 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: simplex-chat name: simplex-chat
version: 5.7.1.0 version: 5.7.4.1
category: Web, System, Services, Cryptography category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat author: simplex.chat
@@ -141,6 +141,7 @@ library
Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
Simplex.Chat.Migrations.M20240324_custom_data Simplex.Chat.Migrations.M20240324_custom_data
Simplex.Chat.Migrations.M20240402_item_forwarded Simplex.Chat.Migrations.M20240402_item_forwarded
Simplex.Chat.Migrations.M20240430_ui_theme
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
@@ -176,6 +177,7 @@ library
Simplex.Chat.Types Simplex.Chat.Types
Simplex.Chat.Types.Preferences Simplex.Chat.Types.Preferences
Simplex.Chat.Types.Shared Simplex.Chat.Types.Shared
Simplex.Chat.Types.UITheme
Simplex.Chat.Types.Util Simplex.Chat.Types.Util
Simplex.Chat.Util Simplex.Chat.Util
Simplex.Chat.View Simplex.Chat.View
+95 -165
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
@@ -46,6 +47,7 @@ import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map) import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
import qualified Data.Set as S
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Text.Encoding (decodeLatin1, encodeUtf8)
@@ -91,18 +93,14 @@ import qualified Simplex.FileTransfer.Description as FD
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap)
import qualified Simplex.Messaging.Agent.Client as AC
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Lock (withLock)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..))
import qualified Simplex.Messaging.Agent.Store as AS
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection)
import qualified Simplex.Messaging.Agent.Store.SQLite as ADB
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
import qualified Simplex.Messaging.Agent.TRcvQueues as RQ
import Simplex.Messaging.Client (defaultNetworkConfig) import Simplex.Messaging.Client (defaultNetworkConfig)
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
@@ -231,7 +229,6 @@ newChatController
inputQ <- newTBQueueIO tbqSize inputQ <- newTBQueueIO tbqSize
outputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize
connNetworkStatuses <- atomically TM.empty connNetworkStatuses <- atomically TM.empty
agentDeliveryStatuses <- atomically TM.empty
subscriptionMode <- newTVarIO SMSubscribe subscriptionMode <- newTVarIO SMSubscribe
chatLock <- newEmptyTMVarIO chatLock <- newEmptyTMVarIO
entityLocks <- atomically TM.empty entityLocks <- atomically TM.empty
@@ -254,6 +251,7 @@ newChatController
showLiveItems <- newTVarIO False showLiveItems <- newTVarIO False
encryptLocalFiles <- newTVarIO False encryptLocalFiles <- newTVarIO False
tempDirectory <- newTVarIO optTempDirectory tempDirectory <- newTVarIO optTempDirectory
assetsDirectory <- newTVarIO Nothing
contactMergeEnabled <- newTVarIO True contactMergeEnabled <- newTVarIO True
pure pure
ChatController ChatController
@@ -268,7 +266,6 @@ newChatController
inputQ, inputQ,
outputQ, outputQ,
connNetworkStatuses, connNetworkStatuses,
agentDeliveryStatuses,
subscriptionMode, subscriptionMode,
chatLock, chatLock,
entityLocks, entityLocks,
@@ -291,6 +288,7 @@ newChatController
showLiveItems, showLiveItems,
encryptLocalFiles, encryptLocalFiles,
tempDirectory, tempDirectory,
assetsDirectory,
logFilePath = logFile, logFilePath = logFile,
contactMergeEnabled contactMergeEnabled
} }
@@ -636,6 +634,17 @@ processChatCommand' vr = \case
createDirectoryIfMissing True rf createDirectoryIfMissing True rf
chatWriteVar remoteHostsFolder $ Just rf chatWriteVar remoteHostsFolder $ Just rf
ok_ ok_
-- has to be called before StartChat
APISetAppFilePaths cfg -> do
setFolder filesFolder $ appFilesFolder cfg
setFolder tempDirectory $ appTempFolder cfg
setFolder assetsDirectory $ appAssetsFolder cfg
mapM_ (setFolder remoteHostsFolder) $ appRemoteHostsFolder cfg
ok_
where
setFolder sel f = do
createDirectoryIfMissing True f
chatWriteVar sel $ Just f
APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_
SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_ SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_
APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_ APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_
@@ -1232,6 +1241,25 @@ processChatCommand' vr = \case
conn <- getPendingContactConnection db userId connId conn <- getPendingContactConnection db userId connId
liftIO $ updateContactConnectionAlias db userId conn localAlias liftIO $ updateContactConnectionAlias db userId conn localAlias
pure $ CRConnectionAliasUpdated user conn' pure $ CRConnectionAliasUpdated user conn'
APISetUserUIThemes uId uiThemes -> withUser $ \user@User {userId} -> do
user'@User {userId = uId'} <- withStore $ \db -> do
user' <- getUser db uId
liftIO $ setUserUIThemes db user uiThemes
pure user'
when (userId == uId') $ chatWriteVar currentUser $ Just (user :: User) {uiThemes}
ok user'
APISetChatUIThemes (ChatRef cType chatId) uiThemes -> withUser $ \user -> case cType of
CTDirect -> do
withStore $ \db -> do
ct <- getContact db vr user chatId
liftIO $ setContactUIThemes db user ct uiThemes
ok user
CTGroup -> do
withStore $ \db -> do
g <- getGroupInfo db vr user chatId
liftIO $ setGroupUIThemes db user g uiThemes
ok user
_ -> pure $ chatCmdError (Just user) "not supported"
APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text
APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken
APIRegisterToken token mode -> withUser $ \_ -> APIRegisterToken token mode -> withUser $ \_ ->
@@ -1655,7 +1683,7 @@ processChatCommand' vr = \case
sndMsgs <- lift $ createSndMessages idsEvts sndMsgs <- lift $ createSndMessages idsEvts
let msgReqs_ :: NonEmpty (Either ChatError MsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs let msgReqs_ :: NonEmpty (Either ChatError MsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs
(errs, ctSndMsgs :: [(Contact, SndMessage)]) <- (errs, ctSndMsgs :: [(Contact, SndMessage)]) <-
lift $ partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_ partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_
timestamp <- liftIO getCurrentTime timestamp <- liftIO getCurrentTime
lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs lift . void $ withStoreBatch' $ \db -> map (createCI db user timestamp) ctSndMsgs
pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp} pure CRBroadcastSent {user, msgContent = mc, successes = length ctSndMsgs, failures = length errs, timestamp}
@@ -2013,9 +2041,9 @@ processChatCommand' vr = \case
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withFileLock "receiveFile" fileId . procCmd $ do withFileLock "receiveFile" fileId . procCmd $ do
(user, ft) <- withStore (`getRcvFileTransferById` fileId) (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
ft' <- (if encrypt then setFileToEncrypt else pure) ft ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
receiveFile' user ft' rcvInline_ filePath_ receiveFile' user ft' rcvInline_ filePath_
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do
withFileLock "setFileToReceive" fileId . procCmd $ do withFileLock "setFileToReceive" fileId . procCmd $ do
@@ -2155,59 +2183,6 @@ processChatCommand' vr = \case
chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn) chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn)
agentMigrations <- withAgent getAgentMigrations agentMigrations <- withAgent getAgentMigrations
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
DebugDelivery showAll -> do
ads <- mapM readTVarIO =<< readTVarIO =<< asks agentDeliveryStatuses
let collect (acId, ds) = if not showAll && agentDeliveryOk ds then Nothing else Just (decodeLatin1 $ strEncode acId, ds)
pure $ CRDebugDelivery . M.fromList . mapMaybe collect $ M.toList ads
DebugConnection (Left connId) -> withUser $ \user -> do
Connection {agentConnId} <- withStore $ \db -> getConnectionById db vr user connId
processChatCommand $ DebugConnection (Right agentConnId)
DebugConnection (Right acId@(AgentConnId acId')) -> do
user@User {userId} <- withStore' (`getUserByAConnId` acId) >>= maybe (throwError . ChatErrorStore $ SEUserNotFoundByAConnId acId) pure
AS.RcvQueue {server, rcvId = rcvId', status = rqStatus} <- withAgent $ \ac ->
-- dive into agent internals
liftIOEither . (`runReaderT` agentEnv ac) . runExceptT $ AC.withStore ac $ \adb ->
ADB.getPrimaryRcvQueue adb acId'
let tSess = (userId, server, Just acId')
SMP.ProtocolServer {host, port} = server
AgentClient {activeSubs, pendingSubs, smpClients, smpSubWorkers} <- withAgent pure
inActive <- any (\AS.RcvQueue {rcvId} -> rcvId == rcvId') <$> atomically (RQ.getSessQueues tSess activeSubs)
inPending <- any (\AS.RcvQueue {rcvId} -> rcvId == rcvId') <$> atomically (RQ.getSessQueues tSess pendingSubs)
smpClient <- atomically (TM.lookup (tSess $> Nothing) smpClients) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
smpClientIsolated <- atomically (TM.lookup tSess smpClients) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
let smpClientStatus = case smpClient <|> smpClientIsolated of
Nothing -> "missing"
Just Nothing -> "connecting"
Just (Just (Left err)) -> tshow err
Just (Just (Right _client)) -> "connected"
subWorker <- atomically (TM.lookup (tSess $> Nothing) smpSubWorkers) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
subWorkerIsolated <- atomically (TM.lookup tSess smpSubWorkers) >>= mapM (\AC.SessionVar {sessionVar} -> atomically (tryReadTMVar sessionVar))
let subWorkerStatus = case subWorker <|> subWorkerIsolated of
Nothing -> "idle"
Just Nothing -> "waiting"
Just (Just _async) -> "working"
connection <-
withStore (\db -> getConnectionEntity db vr user acId) >>= \case
RcvDirectMsgConnection {entityConnection} -> pure entityConnection
RcvGroupMsgConnection {entityConnection} -> pure entityConnection
SndFileConnection {entityConnection} -> pure entityConnection
RcvFileConnection {entityConnection} -> pure entityConnection
UserContactConnection {entityConnection} -> pure entityConnection
deliveryStatus <- mapM readTVarIO . M.lookup acId =<< readTVarIO =<< asks agentDeliveryStatuses
networkStatus <- M.lookup acId <$> chatReadVar connNetworkStatuses
pure $
CRDebugConnection
DebugConnectionStatus
{ deliveryStatus,
networkStatus,
inActive,
inPending,
server = (decodeLatin1 $ strEncode host, port),
smpClientStatus,
subWorkerStatus,
queueStatus = tshow rqStatus,
connection
}
DebugLocks -> lift $ do DebugLocks -> lift $ do
chatLockName <- atomically . tryReadTMVar =<< asks chatLock chatLockName <- atomically . tryReadTMVar =<< asks chatLock
chatEntityLocks <- getLocks =<< asks entityLocks chatEntityLocks <- getLocks =<< asks entityLocks
@@ -2244,7 +2219,6 @@ processChatCommand' vr = \case
SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m
_ -> m _ -> m
GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions
GetAgentSubsDiff -> lift $ CRAgentSubsDiff <$> withAgent' AC.diffSubscriptions
-- CustomChatCommand is unsupported, it can be processed in preCmdHook -- CustomChatCommand is unsupported, it can be processed in preCmdHook
-- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand -- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand
CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported"
@@ -2406,7 +2380,7 @@ processChatCommand' vr = \case
Just changedCts -> do Just changedCts -> do
let idsEvts = L.map ctSndEvent changedCts let idsEvts = L.map ctSndEvent changedCts
msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts
(errs, cts) <- lift $ partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_ (errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
unless (null errs) $ toView $ CRChatErrors (Just user) errs unless (null errs) $ toView $ CRChatErrors (Just user) errs
let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts
lift $ createContactsSndFeatureItems user' changedCts' lift $ createContactsSndFeatureItems user' changedCts'
@@ -2544,6 +2518,8 @@ processChatCommand' vr = \case
toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci)
forM_ (timed_ >>= timedDeleteAt') $ forM_ (timed_ >>= timedDeleteAt') $
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci)
drgRandomBytes :: Int -> CM ByteString
drgRandomBytes n = asks random >>= atomically . C.randomBytes n
privateGetUser :: UserId -> CM User privateGetUser :: UserId -> CM User
privateGetUser userId = privateGetUser userId =
tryChatError (withStore (`getUser` userId)) >>= \case tryChatError (withStore (`getUser` userId)) >>= \case
@@ -3062,7 +3038,7 @@ receiveFile' user ft rcvInline_ filePath_ = do
where where
processError = \case processError = \case
-- TODO AChatItem in Cancelled events -- TODO AChatItem in Cancelled events
ChatErrorAgent (SMP _ SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
e -> throwError e e -> throwError e
@@ -3399,7 +3375,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
errorNetworkStatus :: ChatError -> String errorNetworkStatus :: ChatError -> String
errorNetworkStatus = \case errorNetworkStatus = \case
ChatErrorAgent (BROKER _ NETWORK) _ -> "network" ChatErrorAgent (BROKER _ NETWORK) _ -> "network"
ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted"
e -> show e e -> show e
-- TODO possibly below could be replaced with less noisy events for API -- TODO possibly below could be replaced with less noisy events for API
contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM ()
@@ -3604,29 +3580,13 @@ processAgentMessage _ connId DEL_CONN =
processAgentMessage _ "" (ERR e) = processAgentMessage _ "" (ERR e) =
toView $ CRChatError Nothing $ ChatErrorAgent e Nothing toView $ CRChatError Nothing $ ChatErrorAgent e Nothing
processAgentMessage corrId connId msg = do processAgentMessage corrId connId msg = do
let acId = AgentConnId connId lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId))
acTag = aCommandTag msg
when (acTag == MSG_ || acTag == RCVD_) . lift $ trackNewDelivery acId msg
lockEntity <- critical (withStore (`getChatLockEntity` acId))
withEntityLock "processAgentMessage" lockEntity $ do withEntityLock "processAgentMessage" lockEntity $ do
vr <- chatVersionRange vr <- chatVersionRange
-- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here
critical (withStore' (`getUserByAConnId` acId)) >>= \case critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case
Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user))
_ -> throwChatError $ CENoConnectionUser acId _ -> throwChatError $ CENoConnectionUser (AgentConnId connId)
-- TODO: clean up deliveries
trackNewDelivery :: AgentConnId -> ACommand 'Agent 'AEConn -> CM' ()
trackNewDelivery acId msg = do
now <- liftIO getCurrentTime
asks agentDeliveryStatuses >>= atomically . TM.alterF (updateConn now) acId
where
(isMSG, msgBodyPfx) = case msg of
MSG _ _ msgBody -> (True, T.take 1000 $ safeDecodeUtf8 msgBody)
_ -> (False, "")
updateConn lastCmd = \case
Nothing -> Just <$> newTVar AgentDeliveryStatus {lastCmd, tracking = "create", isMSG, connId = Nothing, msgBodyPfx, eventTag = Nothing, ackSent = Nothing, pendingAcks = M.empty}
Just v -> Just v <$ modifyTVar' v (\AgentDeliveryStatus {pendingAcks} -> AgentDeliveryStatus {lastCmd, tracking = "create", isMSG, connId = Nothing, msgBodyPfx, eventTag = Nothing, ackSent = Nothing, pendingAcks = M.filter not pendingAcks})
-- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps.
-- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries, -- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries,
@@ -3665,7 +3625,7 @@ processAgentMessageNoConn = \case
processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM () processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM ()
processAgentMsgSndFile _corrId aFileId msg = do processAgentMsgSndFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
withEntityLock_ cRef_ $ withFileLock "processAgentMsgSndFile" fileId $ withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $
withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user))
_ -> do _ -> do
@@ -3792,7 +3752,7 @@ splitFileDescr rfdText = do
processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM () processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM ()
processAgentMsgRcvFile _corrId aFileId msg = do processAgentMsgRcvFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
withEntityLock_ cRef_ $ withFileLock "processAgentMsgRcvFile" fileId $ withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $
withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user))
_ -> do _ -> do
@@ -3851,20 +3811,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> toView $ CRSubscriptionEnd user entity _ -> toView $ CRSubscriptionEnd user entity
MSGNTF smpMsgInfo -> toView $ CRNtfMessage user entity $ ntfMsgInfo smpMsgInfo MSGNTF smpMsgInfo -> toView $ CRNtfMessage user entity $ ntfMsgInfo smpMsgInfo
_ -> case entity of _ -> case entity of
RcvDirectMsgConnection conn contact_ -> do RcvDirectMsgConnection conn contact_ ->
storeDeliveryConn conn
processDirectMessage agentMessage entity conn contact_ processDirectMessage agentMessage entity conn contact_
RcvGroupMsgConnection conn gInfo m -> do RcvGroupMsgConnection conn gInfo m ->
storeDeliveryConn conn
processGroupMessage agentMessage entity conn gInfo m processGroupMessage agentMessage entity conn gInfo m
RcvFileConnection conn ft -> do RcvFileConnection conn ft ->
storeDeliveryConn conn
processRcvFileConn agentMessage entity conn ft processRcvFileConn agentMessage entity conn ft
SndFileConnection conn ft -> do SndFileConnection conn ft ->
storeDeliveryConn conn
processSndFileConn agentMessage entity conn ft processSndFileConn agentMessage entity conn ft
UserContactConnection conn uc -> do UserContactConnection conn uc ->
storeDeliveryConn conn
processUserContactRequest agentMessage entity conn uc processUserContactRequest agentMessage entity conn uc
where where
updateConnStatus :: ConnectionEntity -> CM ConnectionEntity updateConnStatus :: ConnectionEntity -> CM ConnectionEntity
@@ -3874,8 +3829,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withStore' $ \db -> updateConnectionStatus db conn connStatus withStore' $ \db -> updateConnectionStatus db conn connStatus
pure $ updateEntityConnStatus acEntity connStatus pure $ updateEntityConnStatus acEntity connStatus
Nothing -> pure acEntity Nothing -> pure acEntity
storeDeliveryConn :: Connection -> CM ()
storeDeliveryConn Connection {connId} = lift $ agentDeliveryStatus (AgentConnId agentConnId) "storeDeliveryConn" $ \ad -> ad {connId = Just connId}
agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus
agentMsgConnStatus = \case agentMsgConnStatus = \case
CONF {} -> Just ConnRequested CONF {} -> Just ConnRequested
@@ -3954,7 +3908,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
(ct', conn') <- updateContactPQRcv user ct conn pqEncryption (ct', conn') <- updateContactPQRcv user ct conn pqEncryption
checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure ()
(conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody
lift $ agentDeliveryStatus (AgentConnId agentConnId) "eventTag" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag event)}
let ct'' = ct' {activeConn = Just conn''} :: Contact let ct'' = ct' {activeConn = Just conn''} :: Contact
assertDirectAllowed user MDRcv ct'' $ toCMEventTag event assertDirectAllowed user MDRcv ct'' $ toCMEventTag event
case event of case event of
@@ -4358,17 +4311,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
MSG msgMeta _msgFlags msgBody -> do MSG msgMeta _msgFlags msgBody -> do
withAckMessage agentConnId msgMeta True $ do withAckMessage agentConnId msgMeta True $ do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "group MSG" id
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
lift $ agentDeliveryStatus (AgentConnId agentConnId) "after checkIntegrityCreateItem" id
forM_ aChatMsgs $ \case forM_ aChatMsgs $ \case
Right (ACMsg _ chatMsg) -> do Right (ACMsg _ chatMsg) ->
lift $ agentDeliveryStatus (AgentConnId agentConnId) "has event" id
processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e
Left e -> do Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e)
lift $ agentDeliveryStatus (AgentConnId agentConnId) "has error" id
toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e)
lift $ agentDeliveryStatus (AgentConnId agentConnId) "after forM_" id
forwardMsg_ `catchChatError` \_ -> pure () forwardMsg_ `catchChatError` \_ -> pure ()
checkSendRcpt $ rights aChatMsgs checkSendRcpt $ rights aChatMsgs
where where
@@ -4376,9 +4323,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
brokerTs = metaBrokerTs msgMeta brokerTs = metaBrokerTs msgMeta
processEvent :: MsgEncodingI e => ChatMessage e -> CM () processEvent :: MsgEncodingI e => ChatMessage e -> CM ()
processEvent chatMsg = do processEvent chatMsg = do
lift $ agentDeliveryStatus (AgentConnId agentConnId) "processEvent" id
(m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg
lift $ agentDeliveryStatus (AgentConnId agentConnId) "eventTag in processEvent" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag event)}
case event of case event of
XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr
@@ -4556,7 +4501,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
MERR _ err -> do MERR _ err -> do
cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user)
case err of case err of
SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do
ci <- withStore $ \db -> do ci <- withStore $ \db -> do
liftIO (lookupChatRefByFileId db user fileId) >>= \case liftIO (lookupChatRefByFileId db user fileId) >>= \case
Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled
@@ -4711,7 +4656,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM () incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM ()
incAuthErrCounter connEntity conn err = do incAuthErrCounter connEntity conn err = do
case err of case err of
SMP _ SMP.AUTH -> do SMP SMP.AUTH -> do
authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn
when (authErrCounter' >= authErrDisableCount) $ do when (authErrCounter' >= authErrDisableCount) $ do
toView $ CRConnectionDisabled connEntity toView $ CRConnectionDisabled connEntity
@@ -4722,22 +4667,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM ()
withCompletedCommand Connection {connId} agentMsg action = do withCompletedCommand Connection {connId} agentMsg action = do
let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg
pending_ <- mapM readTVarIO =<< atomically . TM.lookup acId =<< asks agentDeliveryStatuses cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId
if agentMsgTag == APCT SAEConn OK_ && corrId /= "" && maybe False (M.member ackKey . pendingAcks) pending_ case cmdData_ of
then lift $ agentDeliveryStatus acId "withCompletedCommand" $ \ad@AgentDeliveryStatus {pendingAcks} -> ad {pendingAcks = M.adjust (const True) ackKey pendingAcks} Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction}
else do | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do
cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId withStore' $ \db -> deleteCommand db user cmdId
case cmdData_ of action cmdData
Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} | otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId
| connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do Just CommandData {cmdId, cmdConnId = Nothing} -> err cmdId $ "no command connection id, corrId = " <> show corrId
withStore' $ \db -> deleteCommand db user cmdId Nothing -> throwChatError . CEAgentCommandError $ "command not found, corrId = " <> show corrId
action cmdData
| otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId
Just CommandData {cmdId, cmdConnId = Nothing} -> err cmdId $ "no command connection id, corrId = " <> show corrId
Nothing -> logWarn $ "command not found, corrId = " <> ackKey
where where
acId = AgentConnId agentConnId
ackKey = decodeLatin1 $ strEncode corrId
err cmdId msg = do err cmdId msg = do
withStore' $ \db -> updateCommandStatus db user cmdId CSError withStore' $ \db -> updateCommandStatus db user cmdId CSError
throwChatError . CEAgentCommandError $ msg throwChatError . CEAgentCommandError $ msg
@@ -4758,27 +4697,18 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Right withRcpt -> ackMsg msgMeta $ if withRcpt then Just "" else Nothing Right withRcpt -> ackMsg msgMeta $ if withRcpt then Just "" else Nothing
-- If showCritical is True, then these errors don't result in ACK and show user visible alert -- If showCritical is True, then these errors don't result in ACK and show user visible alert
-- This prevents losing the message that failed to be processed. -- This prevents losing the message that failed to be processed.
Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> do Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
lift $ agentDeliveryStatus (AgentConnId agentConnId) "SEDBBusyError" id
throwError $ ChatErrorAgent (CRITICAL True message) Nothing
Left e -> ackMsg msgMeta Nothing >> throwError e Left e -> ackMsg msgMeta Nothing >> throwError e
where where
ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM ()
ackMsg MsgMeta {recipient = (msgId, _)} rcpt = do ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt
ackCorrId <- drgRandomBytes 24
lift $ agentDeliveryStatus (AgentConnId agentConnId) "ackMsg" id
withAgent $ \a -> ackMessageAsync a ackCorrId cId msgId rcpt
now <- liftIO getCurrentTime
let ackKey = decodeLatin1 $ strEncode ackCorrId
lift . agentDeliveryStatus (AgentConnId agentConnId) "after ackMessageAsync" $ \ad@AgentDeliveryStatus {pendingAcks} ->
ad {ackSent = Just (now, ackKey), pendingAcks = M.insert ackKey False pendingAcks}
sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM () sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM ()
sentMsgDeliveryEvent Connection {connId} msgId = sentMsgDeliveryEvent Connection {connId} msgId =
withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent
agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd
agentErrToItemStatus (SMP _ AUTH) = CISSndErrorAuth agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth
agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err
badRcvFileChunk :: RcvFileTransfer -> String -> CM () badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
@@ -6070,7 +6000,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processForwardedMsg author chatMsg = do processForwardedMsg author chatMsg = do
let body = LB.toStrict $ J.encode msg let body = LB.toStrict $ J.encode msg
rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg
lift $ agentDeliveryStatus (AgentConnId agentConnId) "eventTag in processForwardedMsg" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag event)}
case event of case event of
XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True XMsgNew mc -> memberCanSend author $ newGroupContentMessage gInfo author mc rcvMsg msgTs True
XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr XMsgFileDescr sharedMsgId fileDescr -> memberCanSend author $ groupMessageFileDescription gInfo author sharedMsgId fileDescr
@@ -6544,21 +6473,21 @@ deliverMessage conn cmEventTag msgBody msgId = do
deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption) deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
deliverMessage' conn msgFlags msgBody msgId = deliverMessage' conn msgFlags msgBody msgId =
lift (deliverMessages ((conn, msgFlags, msgBody, msgId) :| [])) >>= \case deliverMessages ((conn, msgFlags, msgBody, msgId) :| []) >>= \case
r :| [] -> liftEither r r :| [] -> liftEither r
rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs)
type MsgReq = (Connection, MsgFlags, MsgBody, MessageId) type MsgReq = (Connection, MsgFlags, MsgBody, MessageId)
deliverMessages :: NonEmpty MsgReq -> CM' (NonEmpty (Either ChatError (Int64, PQEncryption))) deliverMessages :: NonEmpty MsgReq -> CM (NonEmpty (Either ChatError (Int64, PQEncryption)))
deliverMessages msgs = deliverMessagesB $ L.map Right msgs deliverMessages msgs = deliverMessagesB $ L.map Right msgs
deliverMessagesB :: NonEmpty (Either ChatError MsgReq) -> CM' (NonEmpty (Either ChatError (Int64, PQEncryption))) deliverMessagesB :: NonEmpty (Either ChatError MsgReq) -> CM (NonEmpty (Either ChatError (Int64, PQEncryption)))
deliverMessagesB msgReqs = do deliverMessagesB msgReqs = do
msgReqs' <- liftIO compressBodies msgReqs' <- liftIO compressBodies
sent <- L.zipWith prepareBatch msgReqs' <$> withAgent' (`sendMessagesB` L.map toAgent msgReqs') sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` L.map toAgent msgReqs')
void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent) lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent)
withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent
where where
compressBodies = compressBodies =
forME msgReqs $ \mr@(conn@Connection {pqSupport, connChatVersion = v}, msgFlags, msgBody, msgId) -> forME msgReqs $ \mr@(conn@Connection {pqSupport, connChatVersion = v}, msgFlags, msgBody, msgId) ->
@@ -6617,10 +6546,11 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do
msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId)
recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members)
let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent} let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent}
(toSend, pending) = foldr addMember ([], []) recipientMembers (toSend, pending, _, dups) = foldr addMember ([], [], S.empty, 0 :: Int) recipientMembers
-- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here
msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend
delivered <- maybe (pure []) (fmap L.toList . lift . deliverMessages) $ L.nonEmpty msgReqs when (dups /= 0) $ logError $ "sendGroupMessage: " <> tshow dups <> " duplicate members"
delivered <- maybe (pure []) (fmap L.toList . deliverMessages) $ L.nonEmpty msgReqs
let errors = lefts delivered let errors = lefts delivered
unless (null errors) $ toView $ CRChatErrors (Just user) errors unless (null errors) $ toView $ CRChatErrors (Just user) errors
stored <- lift . withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending stored <- lift . withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending
@@ -6633,10 +6563,16 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do
liftM2 (<>) (shuffle adminMs) (shuffle otherMs) liftM2 (<>) (shuffle adminMs) (shuffle otherMs)
where where
isAdmin GroupMember {memberRole} = memberRole >= GRAdmin isAdmin GroupMember {memberRole} = memberRole >= GRAdmin
addMember m (toSend, pending) = case memberSendAction chatMsgEvent members m of addMember m acc@(toSend, pending, !mIds, !dups) = case memberSendAction chatMsgEvent members m of
Just (MSASend conn) -> ((m, conn) : toSend, pending) Just a
Just MSAPending -> (toSend, m : pending) | mId `S.member` mIds -> (toSend, pending, mIds, dups + 1)
Nothing -> (toSend, pending) | otherwise -> case a of
MSASend conn -> ((m, conn) : toSend, pending, mIds', dups)
MSAPending -> (toSend, m : pending, mIds', dups)
Nothing -> acc
where
mId = groupMemberId' m
mIds' = S.insert mId mIds
filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember]
filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms] filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms]
@@ -6695,10 +6631,9 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn
-- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing) -- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing)
saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage) saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage)
saveDirectRcvMSG conn@Connection {connId, agentConnId} agentMsgMeta msgBody = saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody =
case parseChatMessages msgBody of case parseChatMessages msgBody of
[Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do [Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do
lift $ agentDeliveryStatus agentConnId "eventTag in saveDirectRcvMSG" $ \ad -> ad {eventTag = Just $! tshow (toCMEventTag chatMsgEvent)}
conn' <- updatePeerChatVRange conn chatVRange conn' <- updatePeerChatVRange conn chatVRange
let agentMsgId = fst $ recipient agentMsgMeta let agentMsgId = fst $ recipient agentMsgMeta
newMsg = NewRcvMessage {chatMsgEvent, msgBody} newMsg = NewRcvMessage {chatMsgEvent, msgBody}
@@ -7134,9 +7069,13 @@ chatCommandP =
"/_app activate" $> APIActivateChat True, "/_app activate" $> APIActivateChat True,
"/_app suspend " *> (APISuspendChat <$> A.decimal), "/_app suspend " *> (APISuspendChat <$> A.decimal),
"/_resubscribe all" $> ResubscribeAllConnections, "/_resubscribe all" $> ResubscribeAllConnections,
-- deprecated, use /set file paths
"/_temp_folder " *> (SetTempFolder <$> filePath), "/_temp_folder " *> (SetTempFolder <$> filePath),
-- /_files_folder deprecated, use /set file paths
("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath), ("/_files_folder " <|> "/files_folder ") *> (SetFilesFolder <$> filePath),
-- deprecated, use /set file paths
"/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath), "/remote_hosts_folder " *> (SetRemoteHostsFolder <$> filePath),
"/set file paths " *> (APISetAppFilePaths <$> jsonP),
"/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP), "/_files_encrypt " *> (APISetEncryptLocalFiles <$> onOffP),
"/contact_merge " *> (SetContactMergeEnabled <$> onOffP), "/contact_merge " *> (SetContactMergeEnabled <$> onOffP),
"/_db export " *> (APIExportArchive <$> jsonP), "/_db export " *> (APIExportArchive <$> jsonP),
@@ -7192,6 +7131,8 @@ chatCommandP =
"/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias @" *> (APISetContactAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")), "/_set alias :" *> (APISetConnectionAlias <$> A.decimal <*> (A.space *> textP <|> pure "")),
"/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP), "/_set prefs @" *> (APISetContactPrefs <$> A.decimal <* A.space <*> jsonP),
"/_set theme user " *> (APISetUserUIThemes <$> A.decimal <*> optional (A.space *> jsonP)),
"/_set theme " *> (APISetChatUIThemes <$> chatRefP <*> optional (A.space *> jsonP)),
"/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString), "/_parse " *> (APIParseMarkdown . safeDecodeUtf8 <$> A.takeByteString),
"/_ntf get" $> APIGetNtfToken, "/_ntf get" $> APIGetNtfToken,
"/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP), "/_ntf register " *> (APIRegisterToken <$> strP_ <*> strP),
@@ -7401,15 +7342,12 @@ chatCommandP =
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP), "/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/quit" <|> "/q" <|> "/exit") $> QuitChat,
("/version" <|> "/v") $> ShowVersion, ("/version" <|> "/v") $> ShowVersion,
"/debug delivery" *> (DebugDelivery <$> (" all" $> True <|> pure False)),
"/debug conn " *> (DebugConnection <$> ((Left <$> A.decimal) <|> (Right . AgentConnId <$> base64P))),
"/debug locks" $> DebugLocks, "/debug locks" $> DebugLocks,
"/debug event " *> (DebugEvent <$> jsonP), "/debug event " *> (DebugEvent <$> jsonP),
"/get stats" $> GetAgentStats, "/get stats" $> GetAgentStats,
"/reset stats" $> ResetAgentStats, "/reset stats" $> ResetAgentStats,
"/get subs" $> GetAgentSubs, "/get subs" $> GetAgentSubs,
"/get subs details" $> GetAgentSubsDetails, "/get subs details" $> GetAgentSubsDetails,
"/get subs diff" $> GetAgentSubsDiff,
"/get workers" $> GetAgentWorkers, "/get workers" $> GetAgentWorkers,
"/get workers details" $> GetAgentWorkersDetails, "/get workers details" $> GetAgentWorkersDetails,
"//" *> (CustomChatCommand <$> A.takeByteString) "//" *> (CustomChatCommand <$> A.takeByteString)
@@ -7565,9 +7503,6 @@ timeItToView s action = do
toView' $ CRTimedAction s diff toView' $ CRTimedAction s diff
pure a pure a
drgRandomBytes :: Int -> CM ByteString
drgRandomBytes n = asks random >>= atomically . C.randomBytes n
mkValidName :: String -> String mkValidName :: String -> String
mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int) mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int)
where where
@@ -7612,8 +7547,3 @@ xftpSndFileRedirect user ftId vfd = do
dummyFileDescr :: FileDescr dummyFileDescr :: FileDescr
dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} dummyFileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False}
agentDeliveryStatus :: AgentConnId -> Text -> (AgentDeliveryStatus -> AgentDeliveryStatus) -> CM' ()
agentDeliveryStatus acId tracking f = do
ads <- asks agentDeliveryStatuses
atomically $ TM.lookup acId ads >>= mapM_ (`modifyTVar'` \ds -> (f ds) {tracking})
+30 -5
View File
@@ -11,6 +11,7 @@ import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.TH as JQ
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Text (Text) import Data.Text (Text)
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Client (NetworkConfig, defaultNetworkConfig) import Simplex.Messaging.Client (NetworkConfig, defaultNetworkConfig)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON)
import Simplex.Messaging.Util (catchAll_) import Simplex.Messaging.Util (catchAll_)
@@ -43,7 +44,11 @@ data AppSettings = AppSettings
confirmDBUpgrades :: Maybe Bool, confirmDBUpgrades :: Maybe Bool,
androidCallOnLockScreen :: Maybe LockScreenCalls, androidCallOnLockScreen :: Maybe LockScreenCalls,
iosCallKitEnabled :: Maybe Bool, iosCallKitEnabled :: Maybe Bool,
iosCallKitCallsInRecents :: Maybe Bool iosCallKitCallsInRecents :: Maybe Bool,
uiProfileImageCornerRadius :: Maybe Double,
uiColorScheme :: Maybe UIColorScheme,
uiDarkColorScheme :: Maybe DarkColorScheme,
uiThemes :: Maybe UIThemes
} }
deriving (Show) deriving (Show)
@@ -69,7 +74,11 @@ defaultAppSettings =
confirmDBUpgrades = Just False, confirmDBUpgrades = Just False,
androidCallOnLockScreen = Just LSCShow, androidCallOnLockScreen = Just LSCShow,
iosCallKitEnabled = Just True, iosCallKitEnabled = Just True,
iosCallKitCallsInRecents = Just False iosCallKitCallsInRecents = Just False,
uiProfileImageCornerRadius = Just 22.5,
uiColorScheme = Just UCSSystem,
uiDarkColorScheme = Just DCSSimplex,
uiThemes = Nothing
} }
defaultParseAppSettings :: AppSettings defaultParseAppSettings :: AppSettings
@@ -94,7 +103,11 @@ defaultParseAppSettings =
confirmDBUpgrades = Nothing, confirmDBUpgrades = Nothing,
androidCallOnLockScreen = Nothing, androidCallOnLockScreen = Nothing,
iosCallKitEnabled = Nothing, iosCallKitEnabled = Nothing,
iosCallKitCallsInRecents = Nothing iosCallKitCallsInRecents = Nothing,
uiProfileImageCornerRadius = Nothing,
uiColorScheme = Nothing,
uiDarkColorScheme = Nothing,
uiThemes = Nothing
} }
combineAppSettings :: AppSettings -> AppSettings -> AppSettings combineAppSettings :: AppSettings -> AppSettings -> AppSettings
@@ -119,7 +132,11 @@ combineAppSettings platformDefaults storedSettings =
confirmDBUpgrades = p confirmDBUpgrades, confirmDBUpgrades = p confirmDBUpgrades,
iosCallKitEnabled = p iosCallKitEnabled, iosCallKitEnabled = p iosCallKitEnabled,
iosCallKitCallsInRecents = p iosCallKitCallsInRecents, iosCallKitCallsInRecents = p iosCallKitCallsInRecents,
androidCallOnLockScreen = p androidCallOnLockScreen androidCallOnLockScreen = p androidCallOnLockScreen,
uiProfileImageCornerRadius = p uiProfileImageCornerRadius,
uiColorScheme = p uiColorScheme,
uiDarkColorScheme = p uiDarkColorScheme,
uiThemes = p uiThemes
} }
where where
p :: (AppSettings -> Maybe a) -> Maybe a p :: (AppSettings -> Maybe a) -> Maybe a
@@ -157,6 +174,10 @@ instance FromJSON AppSettings where
iosCallKitEnabled <- p "iosCallKitEnabled" iosCallKitEnabled <- p "iosCallKitEnabled"
iosCallKitCallsInRecents <- p "iosCallKitCallsInRecents" iosCallKitCallsInRecents <- p "iosCallKitCallsInRecents"
androidCallOnLockScreen <- p "androidCallOnLockScreen" androidCallOnLockScreen <- p "androidCallOnLockScreen"
uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius"
uiColorScheme <- p "uiColorScheme"
uiDarkColorScheme <- p "uiDarkColorScheme"
uiThemes <- p "uiThemes"
pure pure
AppSettings AppSettings
{ appPlatform, { appPlatform,
@@ -178,7 +199,11 @@ instance FromJSON AppSettings where
confirmDBUpgrades, confirmDBUpgrades,
iosCallKitEnabled, iosCallKitEnabled,
iosCallKitCallsInRecents, iosCallKitCallsInRecents,
androidCallOnLockScreen androidCallOnLockScreen,
uiProfileImageCornerRadius,
uiColorScheme,
uiDarkColorScheme,
uiThemes
} }
where where
p key = v .:? key <|> pure Nothing p key = v .:? key <|> pure Nothing
+33 -20
View File
@@ -1,5 +1,6 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
@@ -25,6 +26,7 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import qualified Database.SQLite3 as SQL import qualified Database.SQLite3 as SQL
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Util ()
import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Client (agentClientStore)
import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), closeSQLiteStore, keyString, sqlString, storeKey) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), closeSQLiteStore, keyString, sqlString, storeKey)
import Simplex.Messaging.Util import Simplex.Messaging.Util
@@ -44,14 +46,22 @@ archiveChatDbFile = "simplex_v1_chat.db"
archiveFilesFolder :: String archiveFilesFolder :: String
archiveFilesFolder = "simplex_v1_files" archiveFilesFolder = "simplex_v1_files"
archiveAssetsFolder :: String
archiveAssetsFolder = "simplex_v1_assets"
wallpapersFolder :: String
wallpapersFolder = "wallpapers"
exportArchive :: ArchiveConfig -> CM' () exportArchive :: ArchiveConfig -> CM' ()
exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = exportArchive cfg@ArchiveConfig {archivePath, disableCompression} =
withTempDir cfg "simplex-chat." $ \dir -> do withTempDir cfg "simplex-chat." $ \dir -> do
StorageFiles {chatStore, agentStore, filesPath} <- storageFiles StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles
copyFile (dbFilePath chatStore) $ dir </> archiveChatDbFile copyFile (dbFilePath chatStore) $ dir </> archiveChatDbFile
copyFile (dbFilePath agentStore) $ dir </> archiveAgentDbFile copyFile (dbFilePath agentStore) $ dir </> archiveAgentDbFile
forM_ filesPath $ \fp -> forM_ filesPath $ \fp ->
copyDirectoryFiles fp $ dir </> archiveFilesFolder copyDirectoryFiles fp $ dir </> archiveFilesFolder
forM_ assetsPath $ \fp ->
copyDirectoryFiles (fp </> wallpapersFolder) $ dir </> archiveAssetsFolder </> wallpapersFolder
let method = if disableCompression == Just True then Z.Store else Z.Deflate let method = if disableCompression == Just True then Z.Store else Z.Deflate
Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir
@@ -59,24 +69,24 @@ importArchive :: ArchiveConfig -> CM' [ArchiveError]
importArchive cfg@ArchiveConfig {archivePath} = importArchive cfg@ArchiveConfig {archivePath} =
withTempDir cfg "simplex-chat." $ \dir -> do withTempDir cfg "simplex-chat." $ \dir -> do
Z.withArchive archivePath $ Z.unpackInto dir Z.withArchive archivePath $ Z.unpackInto dir
fs@StorageFiles {chatStore, agentStore, filesPath} <- storageFiles fs@StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles
liftIO $ closeSQLiteStore `withStores` fs liftIO $ closeSQLiteStore `withStores` fs
backup `withDBs` fs backup `withDBs` fs
copyFile (dir </> archiveChatDbFile) $ dbFilePath chatStore copyFile (dir </> archiveChatDbFile) $ dbFilePath chatStore
copyFile (dir </> archiveAgentDbFile) $ dbFilePath agentStore copyFile (dir </> archiveAgentDbFile) $ dbFilePath agentStore
copyFiles dir filesPath errs <- copyFiles (dir </> archiveFilesFolder) filesPath
`E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e] errs' <- copyFiles (dir </> archiveAssetsFolder </> wallpapersFolder) ((</> wallpapersFolder) <$> assetsPath)
pure $ errs <> errs'
where where
backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak" backup f = whenM (doesFileExist f) $ copyFile f $ f <> ".bak"
copyFiles dir filesPath = do copyFiles fromDir = \case
let filesDir = dir </> archiveFilesFolder Just fp ->
case filesPath of ifM
Just fp -> (doesDirectoryExist fromDir)
ifM (copyDirectoryFiles fromDir fp)
(doesDirectoryExist filesDir) (pure [])
(copyDirectoryFiles filesDir fp) `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e]
(pure []) _ -> pure []
_ -> pure []
withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a) withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a)
withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
@@ -85,7 +95,7 @@ withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of
copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError] copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError]
copyDirectoryFiles fromDir toDir = do copyDirectoryFiles fromDir toDir = do
createDirectoryIfMissing False toDir createDirectoryIfMissing True toDir
fs <- listDirectory fromDir fs <- listDirectory fromDir
foldM copyFileCatchError [] fs foldM copyFileCatchError [] fs
where where
@@ -103,6 +113,7 @@ deleteStorage = do
liftIO $ closeSQLiteStore `withStores` fs liftIO $ closeSQLiteStore `withStores` fs
remove `withDBs` fs remove `withDBs` fs
mapM_ removeDir $ filesPath fs mapM_ removeDir $ filesPath fs
mapM_ removeDir $ assetsPath fs
mapM_ removeDir =<< chatReadVar tempDirectory mapM_ removeDir =<< chatReadVar tempDirectory
where where
remove f = whenM (doesFileExist f) $ removeFile f remove f = whenM (doesFileExist f) $ removeFile f
@@ -111,15 +122,17 @@ deleteStorage = do
data StorageFiles = StorageFiles data StorageFiles = StorageFiles
{ chatStore :: SQLiteStore, { chatStore :: SQLiteStore,
agentStore :: SQLiteStore, agentStore :: SQLiteStore,
filesPath :: Maybe FilePath filesPath :: Maybe FilePath,
assetsPath :: Maybe FilePath
} }
storageFiles :: CM' StorageFiles storageFiles :: CM' StorageFiles
storageFiles = do storageFiles = do
ChatController {chatStore, filesFolder, smpAgent} <- ask ChatController {chatStore, filesFolder, assetsDirectory, smpAgent} <- ask
let agentStore = agentClientStore smpAgent let agentStore = agentClientStore smpAgent
filesPath <- readTVarIO filesFolder filesPath <- readTVarIO filesFolder
pure StorageFiles {chatStore, agentStore, filesPath} assetsPath <- readTVarIO assetsDirectory
pure StorageFiles {chatStore, agentStore, filesPath, assetsPath}
sqlCipherExport :: DBEncryptionConfig -> CM () sqlCipherExport :: DBEncryptionConfig -> CM ()
sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key', keepKey} = sqlCipherExport DBEncryptionConfig {currentKey = DBEncryptionKey key, newKey = DBEncryptionKey key', keepKey} =
@@ -177,9 +190,9 @@ testSQL k =
T.unlines $ T.unlines $
keySQL k keySQL k
<> [ "PRAGMA foreign_keys = ON;", <> [ "PRAGMA foreign_keys = ON;",
"PRAGMA secure_delete = ON;", "PRAGMA secure_delete = ON;",
"SELECT count(*) FROM sqlite_master;" "SELECT count(*) FROM sqlite_master;"
] ]
keySQL :: BA.ScrubbedBytes -> [Text] keySQL :: BA.ScrubbedBytes -> [Text]
keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)] keySQL k = ["PRAGMA key = " <> keyString k <> ";" | not (BA.null k)]
+16 -44
View File
@@ -63,10 +63,11 @@ import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserCont
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Util (liftIOEither) import Simplex.Chat.Util (liftIOEither)
import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.FileTransfer.Description (FileDescriptionURI)
import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo)
import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SubscriptionsDiff, UserNetworkInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo)
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
@@ -207,7 +208,6 @@ data ChatController = ChatController
inputQ :: TBQueue String, inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse), outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse),
connNetworkStatuses :: TMap AgentConnId NetworkStatus, connNetworkStatuses :: TMap AgentConnId NetworkStatus,
agentDeliveryStatuses :: TMap AgentConnId (TVar AgentDeliveryStatus),
subscriptionMode :: TVar SubscriptionMode, subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock, chatLock :: Lock,
entityLocks :: TMap ChatLockEntity Lock, entityLocks :: TMap ChatLockEntity Lock,
@@ -230,27 +230,11 @@ data ChatController = ChatController
showLiveItems :: TVar Bool, showLiveItems :: TVar Bool,
encryptLocalFiles :: TVar Bool, encryptLocalFiles :: TVar Bool,
tempDirectory :: TVar (Maybe FilePath), tempDirectory :: TVar (Maybe FilePath),
assetsDirectory :: TVar (Maybe FilePath),
logFilePath :: Maybe FilePath, logFilePath :: Maybe FilePath,
contactMergeEnabled :: TVar Bool contactMergeEnabled :: TVar Bool
} }
data AgentDeliveryStatus = AgentDeliveryStatus
{ lastCmd :: UTCTime,
tracking :: Text,
isMSG :: Bool, -- False for RCVD
connId :: Maybe Int64, -- chat connection ID
msgBodyPfx :: Text,
eventTag :: Maybe Text, -- tshow of ACMEventTag (for JSON instances)
ackSent :: Maybe (UTCTime, Text), -- strEncode of random CorrId
pendingAcks :: Map Text Bool
}
deriving (Show) -- for ChatResponse
agentDeliveryOk :: AgentDeliveryStatus -> Bool
agentDeliveryOk AgentDeliveryStatus {ackSent, pendingAcks} = case ackSent of
Nothing -> False
Just (_, corrId) -> M.lookup corrId pendingAcks == Just True && and pendingAcks
data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSRemote | HSSettings | HSDatabase data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSRemote | HSSettings | HSDatabase
deriving (Show) deriving (Show)
@@ -283,6 +267,7 @@ data ChatCommand
| SetTempFolder FilePath | SetTempFolder FilePath
| SetFilesFolder FilePath | SetFilesFolder FilePath
| SetRemoteHostsFolder FilePath | SetRemoteHostsFolder FilePath
| APISetAppFilePaths AppFilePathsConfig
| APISetEncryptLocalFiles Bool | APISetEncryptLocalFiles Bool
| SetContactMergeEnabled Bool | SetContactMergeEnabled Bool
| APIExportArchive ArchiveConfig | APIExportArchive ArchiveConfig
@@ -329,6 +314,8 @@ data ChatCommand
| APISetContactPrefs ContactId Preferences | APISetContactPrefs ContactId Preferences
| APISetContactAlias ContactId LocalAlias | APISetContactAlias ContactId LocalAlias
| APISetConnectionAlias Int64 LocalAlias | APISetConnectionAlias Int64 LocalAlias
| APISetUserUIThemes UserId (Maybe UIThemeEntityOverrides)
| APISetChatUIThemes ChatRef (Maybe UIThemeEntityOverrides)
| APIParseMarkdown Text | APIParseMarkdown Text
| APIGetNtfToken | APIGetNtfToken
| APIRegisterToken DeviceToken NotificationsMode | APIRegisterToken DeviceToken NotificationsMode
@@ -506,15 +493,12 @@ data ChatCommand
| APIStandaloneFileInfo FileDescriptionURI | APIStandaloneFileInfo FileDescriptionURI
| QuitChat | QuitChat
| ShowVersion | ShowVersion
| DebugDelivery Bool
| DebugConnection (Either Int64 AgentConnId)
| DebugLocks | DebugLocks
| DebugEvent ChatResponse | DebugEvent ChatResponse
| GetAgentStats | GetAgentStats
| ResetAgentStats | ResetAgentStats
| GetAgentSubs | GetAgentSubs
| GetAgentSubsDetails | GetAgentSubsDetails
| GetAgentSubsDiff
| GetAgentWorkers | GetAgentWorkers
| GetAgentWorkersDetails | GetAgentWorkersDetails
| -- The parser will return this command for strings that start from "//". | -- The parser will return this command for strings that start from "//".
@@ -756,15 +740,12 @@ data ChatResponse
| CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} | CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption}
| CRSQLResult {rows :: [Text]} | CRSQLResult {rows :: [Text]}
| CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]}
| CRDebugDelivery {debugDelivery :: Map Text AgentDeliveryStatus}
| CRDebugConnection {debugConnection :: DebugConnectionStatus}
| CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks}
| CRAgentStats {agentStats :: [[String]]} | CRAgentStats {agentStats :: [[String]]}
| CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails}
| CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary}
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
| CRAgentSubsDiff {agentSubsDiff :: SubscriptionsDiff}
| CRConnectionDisabled {connectionEntity :: ConnectionEntity} | CRConnectionDisabled {connectionEntity :: ConnectionEntity}
| CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType} | CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType}
| CRAgentConnDeleted {agentConnId :: AgentConnId} | CRAgentConnDeleted {agentConnId :: AgentConnId}
@@ -779,21 +760,6 @@ data ChatResponse
| CRCustomChatResponse {user_ :: Maybe User, response :: Text} | CRCustomChatResponse {user_ :: Maybe User, response :: Text}
deriving (Show) deriving (Show)
data DebugConnectionStatus = DebugConnectionStatus
{ deliveryStatus :: Maybe AgentDeliveryStatus,
networkStatus :: Maybe NetworkStatus,
-- from agent's TRecvQ via rcvId
inActive :: Bool, -- should the delivery work right now?
inPending :: Bool, -- is there a temporary error?
-- from receive queue
queueStatus :: Text,
server :: (Text, String), -- what's the server for this connection? -- XXX: reveals private servers and association
smpClientStatus :: Text, -- is there an active client for it?
subWorkerStatus :: Text, -- a session was recently restarted and tries to resubscribe
connection :: Connection
}
deriving (Show)
-- some of these can only be used as command responses -- some of these can only be used as command responses
allowRemoteEvent :: ChatResponse -> Bool allowRemoteEvent :: ChatResponse -> Bool
allowRemoteEvent = \case allowRemoteEvent = \case
@@ -967,6 +933,14 @@ instance StrEncoding DBEncryptionKey where
instance FromJSON DBEncryptionKey where instance FromJSON DBEncryptionKey where
parseJSON = strParseJSON "DBEncryptionKey" parseJSON = strParseJSON "DBEncryptionKey"
data AppFilePathsConfig = AppFilePathsConfig
{ appFilesFolder :: FilePath,
appTempFolder :: FilePath,
appAssetsFolder :: FilePath,
appRemoteHostsFolder :: Maybe FilePath
}
deriving (Show)
data ContactSubStatus = ContactSubStatus data ContactSubStatus = ContactSubStatus
{ contact :: Contact, { contact :: Contact,
contactError :: Maybe ChatError contactError :: Maybe ChatError
@@ -1438,6 +1412,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError)
$(JQ.deriveJSON defaultJSON ''AppFilePathsConfig)
$(JQ.deriveJSON defaultJSON ''ContactSubStatus) $(JQ.deriveJSON defaultJSON ''ContactSubStatus)
$(JQ.deriveJSON defaultJSON ''MemberSubStatus) $(JQ.deriveJSON defaultJSON ''MemberSubStatus)
@@ -1495,10 +1471,6 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCSR") ''RemoteCtrlStopReason)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RHSR") ''RemoteHostStopReason)
$(JQ.deriveJSON J.defaultOptions ''AgentDeliveryStatus)
$(JQ.deriveJSON J.defaultOptions ''DebugConnectionStatus)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig) $(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240430_ui_theme where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240430_ui_theme :: Query
m20240430_ui_theme =
[sql|
ALTER TABLE users ADD COLUMN ui_themes TEXT;
ALTER TABLE contacts ADD COLUMN ui_themes TEXT;
ALTER TABLE groups ADD COLUMN ui_themes TEXT;
|]
down_m20240430_ui_theme :: Query
down_m20240430_ui_theme =
[sql|
ALTER TABLE users DROP COLUMN ui_themes;
ALTER TABLE contacts DROP COLUMN ui_themes;
ALTER TABLE groups DROP COLUMN ui_themes;
|]
+5 -2
View File
@@ -34,7 +34,8 @@ CREATE TABLE users(
show_ntfs INTEGER NOT NULL DEFAULT 1, show_ntfs INTEGER NOT NULL DEFAULT 1,
send_rcpts_contacts INTEGER NOT NULL DEFAULT 0, send_rcpts_contacts INTEGER NOT NULL DEFAULT 0,
send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0, send_rcpts_small_groups INTEGER NOT NULL DEFAULT 0,
user_member_profile_updated_at TEXT, -- 1 for active user user_member_profile_updated_at TEXT,
ui_themes TEXT, -- 1 for active user
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE RESTRICT ON DELETE RESTRICT
@@ -74,6 +75,7 @@ CREATE TABLE contacts(
contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0, contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0,
contact_status TEXT NOT NULL DEFAULT 'active', contact_status TEXT NOT NULL DEFAULT 'active',
custom_data BLOB, custom_data BLOB,
ui_themes TEXT,
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE ON DELETE CASCADE
@@ -122,7 +124,8 @@ CREATE TABLE groups(
send_rcpts INTEGER, send_rcpts INTEGER,
via_group_link_uri_hash BLOB, via_group_link_uri_hash BLOB,
user_member_profile_sent_at TEXT, user_member_profile_sent_at TEXT,
custom_data BLOB, -- received custom_data BLOB,
ui_themes TEXT, -- received
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE ON DELETE CASCADE
+6 -9
View File
@@ -22,8 +22,6 @@ import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Data.Int (Int64) import Data.Int (Int64)
import Data.Maybe (catMaybes, fromMaybe) import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import Data.Time.Clock (UTCTime (..))
import Database.SQLite.Simple (Only (..), (:.) (..)) import Database.SQLite.Simple (Only (..), (:.) (..))
import Database.SQLite.Simple.QQ (sql) import Database.SQLite.Simple.QQ (sql)
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
@@ -32,7 +30,6 @@ import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Protocol (ConnId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -100,20 +97,20 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
db db
[sql| [sql|
SELECT SELECT
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.custom_data p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.custom_data
FROM contacts c FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|] |]
(userId, contactId) (userId, contactId)
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)] -> Either StoreError Contact toContact' :: Int64 -> Connection -> [ContactRow'] -> Either StoreError Contact
toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)] = toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)] =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn activeConn = Just conn
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData} in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData}
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = ExceptT $ do getGroupAndMember_ groupMemberId c = ExceptT $ do
@@ -125,7 +122,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+50 -5
View File
@@ -69,6 +69,7 @@ module Simplex.Chat.Store.Direct
setConnConnReqInv, setConnConnReqInv,
resetContactConnInitiated, resetContactConnInitiated,
setContactCustomData, setContactCustomData,
setContactUIThemes,
) )
where where
@@ -87,6 +88,7 @@ import Simplex.Chat.Messages
import Simplex.Chat.Store.Shared import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -176,7 +178,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash =
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -222,7 +224,26 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
let profile = toLocalProfile profileId p localAlias let profile = toLocalProfile profileId p localAlias
userPreferences = emptyChatPrefs userPreferences = emptyChatPrefs
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing} pure $
Contact
{ contactId,
localDisplayName,
profile,
activeConn = Just conn,
viaGroup = Nothing,
contactUsed,
contactStatus = CSActive,
chatSettings = defaultChatSettings,
userPreferences,
mergedPreferences,
createdAt = currentTs,
updatedAt = currentTs,
chatTs = Just currentTs,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
customData = Nothing,
uiThemes = Nothing
}
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
deleteContactConnectionsAndFiles db userId Contact {contactId} = do deleteContactConnectionsAndFiles db userId Contact {contactId} = do
@@ -579,7 +600,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -725,7 +746,26 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
contactId <- insertedRowId db contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing} pure $
Contact
{ contactId,
localDisplayName,
profile = toLocalProfile profileId profile "",
activeConn = Just conn,
viaGroup = Nothing,
contactUsed,
contactStatus = CSActive,
chatSettings = defaultChatSettings,
userPreferences,
mergedPreferences,
createdAt,
updatedAt = createdAt,
chatTs = Just createdAt,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
uiThemes = Nothing,
customData = Nothing
}
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName = getContactIdByName db User {userId} cName =
@@ -744,7 +784,7 @@ getContact_ db vr user@User {userId} contactId deleted =
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -889,3 +929,8 @@ setContactCustomData :: DB.Connection -> User -> Contact -> Maybe CustomData ->
setContactCustomData db User {userId} Contact {contactId} customData = do setContactCustomData db User {userId} Contact {contactId} customData = do
updatedAt <- getCurrentTime updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId) DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId)
setContactUIThemes :: DB.Connection -> User -> Contact -> Maybe UIThemeEntityOverrides -> IO ()
setContactUIThemes db User {userId} Contact {contactId} uiThemes = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId)
+19 -10
View File
@@ -3,8 +3,8 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
@@ -117,6 +117,7 @@ module Simplex.Chat.Store.Groups
updateUnknownMemberAnnounced, updateUnknownMemberAnnounced,
updateUserMemberProfileSentAt, updateUserMemberProfileSentAt,
setGroupCustomData, setGroupCustomData,
setGroupUIThemes,
) )
where where
@@ -141,6 +142,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -151,19 +153,19 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version import Simplex.Messaging.Version
import UnliftIO.STM import UnliftIO.STM
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe CustomData) :. GroupMemberRow type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe UIThemeEntityOverrides, Maybe CustomData) :. GroupMemberRow
type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo toGroupInfo :: VersionRangeChat -> Int64 -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData) :. userMemberRow) = toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData} in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, uiThemes, customData}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
@@ -274,7 +276,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -348,6 +350,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
updatedAt = currentTs, updatedAt = currentTs,
chatTs = Just currentTs, chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs, userMemberProfileSentAt = Just currentTs,
uiThemes = Nothing,
customData = Nothing customData = Nothing
} }
@@ -414,6 +417,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
updatedAt = currentTs, updatedAt = currentTs,
chatTs = Just currentTs, chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs, userMemberProfileSentAt = Just currentTs,
uiThemes = Nothing,
customData = Nothing customData = Nothing
}, },
groupMemberId groupMemberId
@@ -633,7 +637,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
SELECT SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g FROM groups g
@@ -1298,7 +1302,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1394,7 +1398,7 @@ getGroupInfo db vr User {userId, userContactId} groupId =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.ui_themes, g.custom_data,
-- GroupMember - membership -- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1956,7 +1960,7 @@ createMemberContact
authErrCounter = 0 authErrCounter = 0
} }
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, customData = Nothing} pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do getMemberContact db vr user contactId = do
@@ -1993,7 +1997,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing} mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing}
m' = m {memberContactId = Just contactId} m' = m {memberContactId = Just contactId}
pure (mCt', m') pure (mCt', m')
where where
@@ -2198,3 +2202,8 @@ setGroupCustomData :: DB.Connection -> User -> GroupInfo -> Maybe CustomData ->
setGroupCustomData db User {userId} GroupInfo {groupId} customData = do setGroupCustomData db User {userId} GroupInfo {groupId} customData = do
updatedAt <- getCurrentTime updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId) DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId)
setGroupUIThemes :: DB.Connection -> User -> GroupInfo -> Maybe UIThemeEntityOverrides -> IO ()
setGroupUIThemes db User {userId} GroupInfo {groupId} uiThemes = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (uiThemes, updatedAt, userId, groupId)
+3 -1
View File
@@ -105,6 +105,7 @@ import Simplex.Chat.Migrations.M20240228_pq
import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
import Simplex.Chat.Migrations.M20240324_custom_data import Simplex.Chat.Migrations.M20240324_custom_data
import Simplex.Chat.Migrations.M20240402_item_forwarded import Simplex.Chat.Migrations.M20240402_item_forwarded
import Simplex.Chat.Migrations.M20240430_ui_theme
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@@ -209,7 +210,8 @@ schemaMigrations =
("20240228_pq", m20240228_pq, Just down_m20240228_pq), ("20240228_pq", m20240228_pq, Just down_m20240228_pq),
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id), ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id),
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data), ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data),
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded) ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date
+10 -3
View File
@@ -57,6 +57,7 @@ module Simplex.Chat.Store.Profiles
deleteCommand, deleteCommand,
updateCommandStatus, updateCommandStatus,
getCommandDataByCorrId, getCommandDataByCorrId,
setUserUIThemes,
) )
where where
@@ -82,6 +83,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -123,7 +125,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image,
(profileId, displayName, userId, True, currentTs, currentTs, currentTs) (profileId, displayName, userId, True, currentTs, currentTs, currentTs)
contactId <- insertedRowId db contactId <- insertedRowId db
DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId)
pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing, Nothing) pure $ toUser $ (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, Nothing, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, Nothing, Nothing, Nothing, Nothing)
getUsersInfo :: DB.Connection -> IO [UserInfo] getUsersInfo :: DB.Connection -> IO [UserInfo]
getUsersInfo db = getUsers db >>= mapM getUserInfo getUsersInfo db = getUsers db >>= mapM getUserInfo
@@ -274,8 +276,8 @@ updateUserProfile db user p'
where where
updateUserMemberProfileUpdatedAt_ currentTs updateUserMemberProfileUpdatedAt_ currentTs
| userMemberProfileChanged = do | userMemberProfileChanged = do
DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId) DB.execute db "UPDATE users SET user_member_profile_updated_at = ? WHERE user_id = ?" (currentTs, userId)
pure $ Just currentTs pure $ Just currentTs
| otherwise = pure userMemberProfileUpdatedAt | otherwise = pure userMemberProfileUpdatedAt
userMemberProfileChanged = newName /= displayName || newFullName /= fullName || newImage /= image userMemberProfileChanged = newName /= displayName || newFullName /= fullName || newImage /= image
User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, image, localAlias}, userMemberProfileUpdatedAt} = user User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, image, localAlias}, userMemberProfileUpdatedAt} = user
@@ -619,3 +621,8 @@ getCommandDataByCorrId db User {userId} corrId =
where where
toCommandData :: (CommandId, Maybe Int64, CommandFunction, CommandStatus) -> CommandData toCommandData :: (CommandId, Maybe Int64, CommandFunction, CommandStatus) -> CommandData
toCommandData (cmdId, cmdConnId, cmdFunction, cmdStatus) = CommandData {cmdId, cmdConnId, cmdFunction, cmdStatus} toCommandData (cmdId, cmdConnId, cmdFunction, cmdStatus) = CommandData {cmdId, cmdConnId, cmdFunction, cmdStatus}
setUserUIThemes :: DB.Connection -> User -> Maybe UIThemeEntityOverrides -> IO ()
setUserUIThemes db User {userId} uiThemes = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE users SET ui_themes = ?, updated_at = ? WHERE user_id = ?" (uiThemes, updatedAt, userId)
+11 -9
View File
@@ -3,8 +3,8 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
@@ -33,6 +33,7 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Remote.Types import Simplex.Chat.Remote.Types
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, UserId)
import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
@@ -59,7 +60,6 @@ data StoreError
= SEDuplicateName = SEDuplicateName
| SEUserNotFound {userId :: UserId} | SEUserNotFound {userId :: UserId}
| SEUserNotFoundByName {contactName :: ContactName} | SEUserNotFoundByName {contactName :: ContactName}
| SEUserNotFoundByAConnId {agentConnId :: AgentConnId}
| SEUserNotFoundByContactId {contactId :: ContactId} | SEUserNotFoundByContactId {contactId :: ContactId}
| SEUserNotFoundByGroupId {groupId :: GroupId} | SEUserNotFoundByGroupId {groupId :: GroupId}
| SEUserNotFoundByFileId {fileId :: FileTransferId} | SEUserNotFoundByFileId {fileId :: FileTransferId}
@@ -381,16 +381,18 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|] |]
[":user_id" := userId, ":profile_id" := profileId] [":user_id" := userId, ":profile_id" := profileId]
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData) type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Maybe CustomData)
type ContactRow = Only ContactId :. ContactRow'
toContact :: VersionRangeChat -> User -> ContactRow :. MaybeConnectionRow -> Contact toContact :: VersionRangeChat -> User -> ContactRow :. MaybeConnectionRow -> Contact
toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)) :. connRow) = toContact vr user ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toMaybeConnection vr connRow activeConn = toMaybeConnection vr connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
incognito = maybe False connIncognito activeConn incognito = maybe False connIncognito activeConn
mergedPreferences = contactUserPreferences user userPreferences preferences incognito mergedPreferences = contactUserPreferences user userPreferences preferences incognito
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData} in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData}
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
getProfileById db userId profileId = getProfileById db userId profileId =
@@ -419,15 +421,15 @@ userQuery :: Query
userQuery = userQuery =
[sql| [sql|
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences, SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes
FROM users u FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
|] |]
toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime) -> User toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (Bool, Bool, Bool, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User
toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt)) = toUser ((userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, contactLink, userPreferences) :. (showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) =
User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt} User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes}
where where
profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences = userPreferences, localAlias = ""} profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences = userPreferences, localAlias = ""}
fullPreferences = mergePreferences Nothing userPreferences fullPreferences = mergePreferences Nothing userPreferences
+5 -1
View File
@@ -45,6 +45,7 @@ import Database.SQLite.Simple.Ok
import Database.SQLite.Simple.ToField (ToField (..)) import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Description (FileDigest)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId)
@@ -117,7 +118,8 @@ data User = User
showNtfs :: Bool, showNtfs :: Bool,
sendRcptsContacts :: Bool, sendRcptsContacts :: Bool,
sendRcptsSmallGroups :: Bool, sendRcptsSmallGroups :: Bool,
userMemberProfileUpdatedAt :: Maybe UTCTime userMemberProfileUpdatedAt :: Maybe UTCTime,
uiThemes :: Maybe UIThemeEntityOverrides
} }
deriving (Show) deriving (Show)
@@ -175,6 +177,7 @@ data Contact = Contact
chatTs :: Maybe UTCTime, chatTs :: Maybe UTCTime,
contactGroupMemberId :: Maybe GroupMemberId, contactGroupMemberId :: Maybe GroupMemberId,
contactGrpInvSent :: Bool, contactGrpInvSent :: Bool,
uiThemes :: Maybe UIThemeEntityOverrides,
customData :: Maybe CustomData customData :: Maybe CustomData
} }
deriving (Eq, Show) deriving (Eq, Show)
@@ -372,6 +375,7 @@ data GroupInfo = GroupInfo
updatedAt :: UTCTime, updatedAt :: UTCTime,
chatTs :: Maybe UTCTime, chatTs :: Maybe UTCTime,
userMemberProfileSentAt :: Maybe UTCTime, userMemberProfileSentAt :: Maybe UTCTime,
uiThemes :: Maybe UIThemeEntityOverrides,
customData :: Maybe CustomData customData :: Maybe CustomData
} }
deriving (Eq, Show) deriving (Eq, Show)
+166
View File
@@ -0,0 +1,166 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Chat.Types.UITheme where
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ
import qualified Data.Attoparsec.ByteString.Char8 as A
import Data.Char (toLower)
import Data.Maybe (fromMaybe)
import Database.SQLite.Simple.FromField (FromField (..))
import Database.SQLite.Simple.ToField (ToField (..))
import Simplex.Chat.Types.Util
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_)
import Simplex.Messaging.Util ((<$?>))
data UIThemes = UIThemes
{ light :: Maybe UITheme,
dark :: Maybe UITheme,
simplex :: Maybe UITheme
}
deriving (Eq, Show)
data UITheme = UITheme
{ base :: ThemeColorScheme,
wallpaper :: Maybe ChatWallpaper,
colors :: UIColors
}
deriving (Eq, Show)
data UIColorMode = UCMLight | UCMDark
deriving (Eq, Show)
data UIThemeEntityOverrides = UIThemeEntityOverrides
{ light :: Maybe UIThemeEntityOverride,
dark :: Maybe UIThemeEntityOverride
}
deriving (Eq, Show)
data UIThemeEntityOverride = UIThemeEntityOverride
{ mode :: UIColorMode,
wallpaper :: Maybe ChatWallpaper,
colors :: UIColors
}
deriving (Eq, Show)
data ThemeColorScheme = TCSLight | TCSDark | TCSSimplex
deriving (Eq, Show)
data UIColorScheme
= UCSSystem
| UCSLight
| UCSDark
| UCSSimplex
deriving (Show)
data DarkColorScheme = DCSDark | DCSSimplex
deriving (Show)
instance StrEncoding ThemeColorScheme where
strEncode = \case
TCSLight -> "LIGHT"
TCSDark -> "DARK"
TCSSimplex -> "SIMPLEX"
strDecode = \case
"LIGHT" -> Right TCSLight
"DARK" -> Right TCSDark
"SIMPLEX" -> Right TCSSimplex
_ -> Left "bad ColorScheme"
strP = strDecode <$?> A.takeTill (== ' ')
instance FromJSON ThemeColorScheme where
parseJSON = strParseJSON "ThemeColorScheme"
instance ToJSON ThemeColorScheme where
toJSON = strToJSON
toEncoding = strToJEncoding
data ChatWallpaper = ChatWallpaper
{ preset :: Maybe ChatWallpaperPreset,
imageFile :: Maybe FilePath,
background :: Maybe UIColor,
tint :: Maybe UIColor,
scaleType :: Maybe ChatWallpaperScale,
scale :: Maybe Double
}
deriving (Eq, Show)
data ChatWallpaperScale = CWSFill | CWSFit | CWSRepeat
deriving (Eq, Show)
data UIColors = UIColors
{ accent :: Maybe UIColor,
accentVariant :: Maybe UIColor,
secondary :: Maybe UIColor,
secondaryVariant :: Maybe UIColor,
background :: Maybe UIColor,
menus :: Maybe UIColor,
title :: Maybe UIColor,
sentMessage :: Maybe UIColor,
receivedMessage :: Maybe UIColor
}
deriving (Eq, Show)
defaultUIColors :: UIColors
defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing
data ChatWallpaperPreset
= CWPKids
| CWPCats
| CWPPets
| CWPFlowers
| CWPHearts
| CWPSocial
| CWPTravel
| CWPInternet
| CWPSpace
| CWPSchool
deriving (Eq, Show)
newtype UIColor = UIColor String
deriving (Eq, Show)
instance FromJSON UIColor where
parseJSON v = toColor =<< J.parseJSON v
where
toColor s@('#' : cs)
| length cs == 8 && all hexDigit cs = pure $ UIColor s
toColor _ = fail "bad UIColor"
hexDigit c = (c >= '0' && c <= '9') || (let c' = toLower c in c' >= 'a' && c' <= 'f')
instance ToJSON UIColor where
toJSON (UIColor t) = J.toJSON t
toEncoding (UIColor t) = J.toEncoding t
$(JQ.deriveJSON (enumJSON $ dropPrefix "DCS") ''DarkColorScheme)
$(JQ.deriveJSON (enumJSON $ dropPrefix "UCM") ''UIColorMode)
$(JQ.deriveJSON (enumJSON $ dropPrefix "UCS") ''UIColorScheme)
$(JQ.deriveJSON (enumJSON $ dropPrefix "CWS") ''ChatWallpaperScale)
$(JQ.deriveJSON (enumJSON $ dropPrefix "CWP") ''ChatWallpaperPreset)
$(JQ.deriveJSON defaultJSON ''ChatWallpaper)
$(JQ.deriveJSON defaultJSON ''UIColors)
$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverride)
$(JQ.deriveJSON defaultJSON ''UIThemeEntityOverrides)
$(JQ.deriveJSON defaultJSON ''UITheme)
$(JQ.deriveJSON defaultJSON ''UIThemes)
instance ToField UIThemeEntityOverrides where
toField = toField . encodeJSON
instance FromField UIThemeEntityOverrides where
fromField = fromTextField_ $ Just . fromMaybe (UIThemeEntityOverrides Nothing Nothing) . decodeJSON
+30
View File
@@ -1,10 +1,18 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TupleSections #-} {-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Simplex.Chat.Util (week, encryptFile, chunkSize, liftIOEither, shuffle) where module Simplex.Chat.Util (week, encryptFile, chunkSize, liftIOEither, shuffle) where
import Control.Exception (Exception)
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.IO.Class import Control.Monad.IO.Class
import Control.Monad.IO.Unlift (MonadUnliftIO (..))
import Control.Monad.Reader
import Data.Bifunctor (first)
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy as LB
import Data.List (sortBy) import Data.List (sortBy)
import Data.Ord (comparing) import Data.Ord (comparing)
@@ -13,6 +21,7 @@ import Data.Word (Word16)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF import qualified Simplex.Messaging.Crypto.File as CF
import System.Random (randomRIO) import System.Random (randomRIO)
import qualified UnliftIO.Exception as E
import UnliftIO.IO (IOMode (..), withFile) import UnliftIO.IO (IOMode (..), withFile)
week :: NominalDiffTime week :: NominalDiffTime
@@ -46,3 +55,24 @@ shuffle xs = map snd . sortBy (comparing fst) <$> mapM (\x -> (,x) <$> random) x
liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a liftIOEither :: (MonadIO m, MonadError e m) => IO (Either e a) -> m a
liftIOEither a = liftIO a >>= liftEither liftIOEither a = liftIO a >>= liftEither
{-# INLINE liftIOEither #-} {-# INLINE liftIOEither #-}
newtype InternalException e = InternalException {unInternalException :: e}
deriving (Eq, Show)
instance Exception e => Exception (InternalException e)
instance Exception e => MonadUnliftIO (ExceptT e IO) where
{-# INLINE withRunInIO #-}
withRunInIO :: ((forall a. ExceptT e IO a -> IO a) -> IO b) -> ExceptT e IO b
withRunInIO inner =
ExceptT . fmap (first unInternalException) . E.try $
withRunInIO $ \run ->
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
instance Exception e => MonadUnliftIO (ExceptT e (ReaderT r IO)) where
{-# INLINE withRunInIO #-}
withRunInIO :: ((forall a. ExceptT e (ReaderT r IO) a -> IO a) -> IO b) -> ExceptT e (ReaderT r IO) b
withRunInIO inner =
withExceptT unInternalException . ExceptT . E.try $
withRunInIO $ \run ->
inner $ run . (either (E.throwIO . InternalException) pure <=< runExceptT)
+12 -12
View File
@@ -50,6 +50,7 @@ import Simplex.Chat.Styled
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import qualified Simplex.FileTransfer.Transport as XFTPTransport import qualified Simplex.FileTransfer.Transport as XFTPTransport
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
@@ -83,7 +84,7 @@ serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . response
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case
CRActiveUser User {profile} -> viewUserProfile $ fromLocalProfile profile CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes
CRUsersList users -> viewUsersList users CRUsersList users -> viewUsersList users
CRChatStarted -> ["chat started"] CRChatStarted -> ["chat started"]
CRChatRunning -> ["chat is running"] CRChatRunning -> ["chat is running"]
@@ -351,8 +352,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
<> (" :: avg: " <> sShow timeAvg <> " ms") <> (" :: avg: " <> sShow timeAvg <> " ms")
<> (" :: " <> plain (T.unwords $ T.lines query)) <> (" :: " <> plain (T.unwords $ T.lines query))
in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries)
CRDebugDelivery ads -> [plain $ LB.unpack (J.encode ads)]
CRDebugConnection cs -> [plain $ LB.unpack (J.encode cs)]
CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} ->
[ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName,
plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks),
@@ -372,10 +371,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
<> ("pending subscriptions: " : map sShow pendingSubscriptions) <> ("pending subscriptions: " : map sShow pendingSubscriptions)
<> ("removed subscriptions: " : map sShow removedSubscriptions) <> ("removed subscriptions: " : map sShow removedSubscriptions)
CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)] CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)]
CRAgentSubsDiff asd ->
[ "agent subs diff:",
plain . LB.unpack $ J.encode asd
]
CRAgentWorkersDetails {agentWorkersDetails} -> CRAgentWorkersDetails {agentWorkersDetails} ->
[ "agent workers details:", [ "agent workers details:",
plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line
@@ -1184,8 +1179,8 @@ viewServerTestResult :: AProtoServerWithAuth -> Maybe ProtocolTestFailure -> [St
viewServerTestResult (AProtoServerWithAuth p _) = \case viewServerTestResult (AProtoServerWithAuth p _) = \case
Just ProtocolTestFailure {testStep, testError} -> Just ProtocolTestFailure {testStep, testError} ->
result result
<> [pName <> " server requires authorization to create queues, check password" | SMP _ SMP.AUTH <- [testError], testStep == TSCreateQueue] <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH]
<> [pName <> " server requires authorization to upload files, check password" | XFTP _ XFTPTransport.AUTH <- [testError], testStep == TSCreateFile] <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && testError == XFTP XFTPTransport.AUTH]
<> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr] <> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr]
where where
result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)] result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)]
@@ -1215,7 +1210,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
] ]
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, customData} stats incognitoProfile = viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId] ["contact ID: " <> sShow contactId]
<> maybe [] viewConnectionStats stats <> maybe [] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink
@@ -1227,15 +1222,20 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
<> [viewConnectionVerified (contactSecurityCode ct)] <> [viewConnectionVerified (contactSecurityCode ct)]
<> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn] <> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
<> viewUITheme uiThemes
<> viewCustomData customData <> viewCustomData customData
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString] viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId, customData} s = viewGroupInfo GroupInfo {groupId, uiThemes, customData} s =
[ "group ID: " <> sShow groupId, [ "group ID: " <> sShow groupId,
"current members: " <> sShow (currentMembers s) "current members: " <> sShow (currentMembers s)
] ]
<> viewUITheme uiThemes
<> viewCustomData customData <> viewCustomData customData
viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString]
viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)])
viewCustomData :: Maybe CustomData -> [StyledString] viewCustomData :: Maybe CustomData -> [StyledString]
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)]) viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)])
@@ -2025,7 +2025,7 @@ viewChatError logLevel testView = \case
e -> ["chat database error: " <> sShow e] e -> ["chat database error: " <> sShow e]
ChatErrorAgent err entity_ -> case err of ChatErrorAgent err entity_ -> case err of
CMD PROHIBITED -> [withConnEntity <> "error: command is prohibited"] CMD PROHIBITED -> [withConnEntity <> "error: command is prohibited"]
SMP _ SMP.AUTH -> SMP SMP.AUTH ->
[ withConnEntity [ withConnEntity
<> "error: connection authorization failed - this could happen if connection was deleted,\ <> "error: connection authorization failed - this could happen if connection was deleted,\
\ secured with different credentials, or due to a bug - please re-create the connection" \ secured with different credentials, or due to a bug - please re-create the connection"
+4 -5
View File
@@ -185,9 +185,10 @@ testAddContact = versionTestMatrix2 runTestAddContact
bob #$> ("/_read chat @2", id, "ok") bob #$> ("/_read chat @2", id, "ok")
alice #$> ("/read user", id, "ok") alice #$> ("/read user", id, "ok")
alice #$> ("/_read user 1", id, "ok") alice #$> ("/_read user 1", id, "ok")
features = if pqExpected features =
then chatFeatures if pqExpected
else (0, e2eeInfoNoPQStr) : tail chatFeatures then chatFeatures
else (0, e2eeInfoNoPQStr) : tail chatFeatures
testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO () testDuplicateContactsSeparate :: HasCallStack => FilePath -> IO ()
testDuplicateContactsSeparate = testDuplicateContactsSeparate =
@@ -1069,7 +1070,6 @@ testMaintenanceMode tmp = do
bob <# "alice> hi again" bob <# "alice> hi again"
bob #> "@alice hello" bob #> "@alice hello"
alice <# "bob> hello" alice <# "bob> hello"
threadDelay 100000
-- export / delete / import -- export / delete / import
alice ##> "/_stop" alice ##> "/_stop"
alice <## "chat stopped" alice <## "chat stopped"
@@ -1150,7 +1150,6 @@ testDatabaseEncryption tmp = do
alice <## "error: chat not stopped" alice <## "error: chat not stopped"
alice ##> "/db decrypt mykey" alice ##> "/db decrypt mykey"
alice <## "error: chat not stopped" alice <## "error: chat not stopped"
threadDelay 100000
alice ##> "/_stop" alice ##> "/_stop"
alice <## "chat stopped" alice <## "chat stopped"
alice ##> "/db decrypt mykey" alice ##> "/db decrypt mykey"
+57 -2
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-} {-# LANGUAGE PostfixOperators #-}
@@ -15,6 +16,8 @@ import qualified Data.Text as T
import Simplex.Chat.Store.Shared (createContact) import Simplex.Chat.Store.Shared (createContact)
import Simplex.Chat.Types (ConnStatus (..), Profile (..)) import Simplex.Chat.Types (ConnStatus (..), Profile (..))
import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.Chat.Types.Shared (GroupMemberRole (..))
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util (encodeJSON)
import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Encoding.String (StrEncoding (..))
import System.Directory (copyFile, createDirectoryIfMissing) import System.Directory (copyFile, createDirectoryIfMissing)
import Test.Hspec hiding (it) import Test.Hspec hiding (it)
@@ -73,6 +76,7 @@ chatProfileTests = do
it "direct messages" testGroupPrefsDirectForRole it "direct messages" testGroupPrefsDirectForRole
it "files & media" testGroupPrefsFilesForRole it "files & media" testGroupPrefsFilesForRole
it "SimpleX links" testGroupPrefsSimplexLinksForRole it "SimpleX links" testGroupPrefsSimplexLinksForRole
it "set user, contact and group UI theme" testSetUITheme
testUpdateProfile :: HasCallStack => FilePath -> IO () testUpdateProfile :: HasCallStack => FilePath -> IO ()
testUpdateProfile = testUpdateProfile =
@@ -1935,8 +1939,8 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP
dan <## "#team: you joined the group" dan <## "#team: you joined the group"
dan dan
<### [ "#team: member alice (Alice) is connected", <### [ "#team: member alice (Alice) is connected",
"#team: member bob (Bob) is connected" "#team: member bob (Bob) is connected"
], ],
do do
alice <## "#team: cath added dan (Daniel) to the group (connecting...)" alice <## "#team: cath added dan (Daniel) to the group (connecting...)"
alice <## "#team: new member dan is connected", alice <## "#team: new member dan is connected",
@@ -2029,3 +2033,54 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil
cc <## "alice updated group #team:" cc <## "alice updated group #team:"
cc <## "updated group preferences:" cc <## "updated group preferences:"
cc <## "SimpleX links: on for owners" cc <## "SimpleX links: on for owners"
testSetUITheme :: HasCallStack => FilePath -> IO ()
testSetUITheme =
testChat2 aliceProfile bobProfile $ \alice bob -> do
connectUsers alice bob
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice #$> ("/_set theme user 1 " <> theme UCMDark, id, "ok")
alice #$> ("/_set theme @2 " <> theme UCMDark, id, "ok")
alice #$> ("/_set theme #1 " <> theme UCMDark, id, "ok")
alice ##> "/u"
userInfo alice "alice (Alice)"
alice <## ("UI themes: " <> theme UCMDark)
alice ##> "/create user alice2"
userInfo alice "alice2"
alice ##> "/u alice"
userInfo alice "alice (Alice)"
alice <## ("UI themes: " <> theme UCMDark)
alice ##> "/i @bob"
contactInfo alice
alice <## ("UI themes: " <> theme UCMDark)
alice ##> "/i #team"
groupInfo alice
alice <## ("UI themes: " <> theme UCMDark)
alice #$> ("/_set theme user 1", id, "ok")
alice #$> ("/_set theme @2", id, "ok")
alice #$> ("/_set theme #1", id, "ok")
alice ##> "/u"
userInfo alice "alice (Alice)"
alice ##> "/i @bob"
contactInfo alice
alice ##> "/i #team"
groupInfo alice
where
theme cm = T.unpack $ encodeJSON UIThemeEntityOverrides {light = Nothing, dark = Just $ UIThemeEntityOverride cm Nothing defaultUIColors}
userInfo a name = do
a <## ("user profile: " <> name)
a <## "use /p <display name> to change it"
a <## "(the updated profile will be sent to all your contacts)"
contactInfo a = do
a <## "contact ID: 2"
a <## "receiving messages via: localhost"
a <## "sending messages via: localhost"
a <## "you've shared main profile with this contact"
a <## "connection not verified, use /code command to see security code"
a <## "quantum resistant end-to-end encryption"
a <## "peer chat protocol version range: (Version 1, Version 8)"
groupInfo a = do
a <## "group ID: 1"
a <## "current members: 1"
+2 -1
View File
@@ -252,5 +252,6 @@
"jobs": "Připojit k týmu", "jobs": "Připojit k týmu",
"hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.", "hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.",
"hero-overlay-card-3-p-3": "Přečtěte si více v <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ohlášení</a>.", "hero-overlay-card-3-p-3": "Přečtěte si více v <a href=\"/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html\">ohlášení</a>.",
"docs-dropdown-9": "Ke stažení" "docs-dropdown-9": "Ke stažení",
"docs-dropdown-10": "Transparentnost"
} }
+4 -2
View File
@@ -24,7 +24,7 @@
"copyright-label": "© 2020-2023 SimpleX | Projet Open-Source", "copyright-label": "© 2020-2023 SimpleX | Projet Open-Source",
"simplex-chat-protocol": "Protocole SimpleX Chat", "simplex-chat-protocol": "Protocole SimpleX Chat",
"terminal-cli": "Terminal CLI", "terminal-cli": "Terminal CLI",
"terms-and-privacy-policy": "Conditions et politique de confidentialité", "terms-and-privacy-policy": "Politique de confidentialité",
"hero-header": "La vie privée redéfinie", "hero-header": "La vie privée redéfinie",
"hero-subheader": "La première messagerie<br>sans identifiant d'utilisateur", "hero-subheader": "La première messagerie<br>sans identifiant d'utilisateur",
"hero-p-1": "Les autres applications ont des IDs d'utilisateur : Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX n'en a pas, <strong>pas même des nombres aléatoires</strong>.<br > Ce qui améliore radicalement votre vie privée.", "hero-p-1": "Les autres applications ont des IDs d'utilisateur : Signal, Matrix, Session, Briar, Jami, Cwtch, etc.<br> SimpleX n'en a pas, <strong>pas même des nombres aléatoires</strong>.<br > Ce qui améliore radicalement votre vie privée.",
@@ -254,5 +254,7 @@
"docs-dropdown-9": "Téléchargements", "docs-dropdown-9": "Téléchargements",
"please-enable-javascript": "Veuillez activer JavaScript pour voir le code QR.", "please-enable-javascript": "Veuillez activer JavaScript pour voir le code QR.",
"please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile", "please-use-link-in-mobile-app": "Veuillez utiliser le lien dans l'application mobile",
"docs-dropdown-10": "Transparence" "docs-dropdown-10": "Transparence",
"docs-dropdown-12": "Sécurité",
"docs-dropdown-11": "FAQ"
} }
+3 -2
View File
@@ -235,7 +235,7 @@
"privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.", "privacy-matters-overlay-card-1-p-4": "Платформа SimpleX защищает конфиденциальность ваших контактов лучше, чем любая другая альтернатива, полностью предотвращая доступ к вашему социальному графику каким-либо компаниям или организациям. Даже когда люди используют серверы, предоставляемые SimpleX Chat, мы не знаем точное количество пользователей или с кем они общаются.",
"hero-overlay-card-1-p-6": "Подробнее читайте в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>техническом документе SimpleX</a>.", "hero-overlay-card-1-p-6": "Подробнее читайте в <a href='https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md' target='_blank'>техническом документе SimpleX</a>.",
"simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями <a href='https://ru.wikipedia.org/wiki/Peer-to-peer'>P2P</a> имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.", "simplex-network-overlay-card-1-p-1": "Протоколы и приложения для обмена сообщениями <a href='https://ru.wikipedia.org/wiki/Peer-to-peer'>P2P</a> имеют различные проблемы, которые делают их менее надежными, чем SimpleX, более сложными для анализа и уязвимыми для нескольких типов атак.",
"terms-and-privacy-policy": "Условия & Политика Конфиденциальности", "terms-and-privacy-policy": "Политика Конфиденциальности",
"simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант <a href='https://ru.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - <em>O(log N)</em>.", "simplex-network-overlay-card-1-li-1": "Сети P2P полагаются на тот или иной вариант <a href='https://ru.wikipedia.org/wiki/Distributed_hash_table'>DHT</a> для маршрутизации сообщений. Проекты DHT должны обеспечивать баланс между гарантией доставки и задержкой. SimpleX имеет как лучшую гарантию доставки, так и меньшую задержку, чем P2P. В сетях P2P сообщение передается через нескольких узлов, последовательно, кол-во узлов-посредников будет расти параллельно размеру сети - <em>O(log N)</em>.",
"privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!", "privacy-matters-section-label": "Убедитесь, что ваш мессенджер не может получить доступ к вашим данным!",
"simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в <strong>портативном формате зашифрованной базы данных</strong> которую можно перенести на другое устройство.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat хранит все пользовательские данные на клиентских устройствах в <strong>портативном формате зашифрованной базы данных</strong> которую можно перенести на другое устройство.",
@@ -253,5 +253,6 @@
"simplex-private-5-title": "Многоуровневое<br>Заполнения содержимого", "simplex-private-5-title": "Многоуровневое<br>Заполнения содержимого",
"please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении", "please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении",
"please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.", "please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.",
"docs-dropdown-10": "Прозрачность" "docs-dropdown-10": "Прозрачность",
"docs-dropdown-12": "Безопасность"
} }
+2 -2
View File
@@ -13,7 +13,7 @@
<a href="/{{ '' if lang == 'en' else lang }}" class="dark:hidden lg:hidden ltr:mr-auto rtl:ml-auto"><img class="h-[32px]" src="/img/new/logo-symbol-light.svg" alt="" srcset=""></a> <a href="/{{ '' if lang == 'en' else lang }}" class="dark:hidden lg:hidden ltr:mr-auto rtl:ml-auto"><img class="h-[32px]" src="/img/new/logo-symbol-light.svg" alt="" srcset=""></a>
<a href="/{{ '' if lang == 'en' else lang }}" class="hidden dark:inline-block dark:lg:hidden lg:hidden ltr:mr-auto rtl:ml-auto"><img class="h-[32px]" src="/img/new/logo-symbol-dark.svg" alt="" srcset=""></a> <a href="/{{ '' if lang == 'en' else lang }}" class="hidden dark:inline-block dark:lg:hidden lg:hidden ltr:mr-auto rtl:ml-auto"><img class="h-[32px]" src="/img/new/logo-symbol-dark.svg" alt="" srcset=""></a>
<nav class="bg-[#F0F1F2] dark:bg-gradient-radial-mobile dark:lg:bg-none lg:bg-transparent fixed top-[66px] left-0 right-0 bottom-0 lg:top-0 lg:relative" id="menu"> <nav class="bg-[#F0F1F2] dark:bg-gradient-radial-mobile dark:lg:bg-none lg:bg-transparent fixed top-[66px] left-0 right-0 bottom-0 h-[calc(100vh-66px)] lg:h-[66px] lg:top-0 lg:relative" id="menu">
<div class="flex flex-col lg:flex-row justify-between lg:items-center gap-5 xl:gap-10 px-4 lg:px-0 h-full"> <div class="flex flex-col lg:flex-row justify-between lg:items-center gap-5 xl:gap-10 px-4 lg:px-0 h-full">
<ul class="flex flex-col lg:flex-row lg:items-center gap-3 py-4 lg:py-0 lg:gap-5 xl:gap-8"> <ul class="flex flex-col lg:flex-row lg:items-center gap-3 py-4 lg:py-0 lg:gap-5 xl:gap-8">
@@ -118,7 +118,7 @@
</ul> </ul>
<a href="https://github.com/simplex-chat/simplex-chat#help-us-with-donations" <a href="https://github.com/simplex-chat/simplex-chat#help-us-with-donations"
target="_blank" class="whitespace-nowrap flex items-center gap-1 self-center text-white dark:text-black text-[16px] font-medium tracking-[0.02em] rounded-[34px] bg-primary-light dark:bg-primary-dark py-3 lg:py-2 px-20 lg:px-5 mb-16 lg:mb-0" target="_blank" class="whitespace-nowrap flex items-center gap-1 self-center text-white dark:text-black text-[16px] font-medium tracking-[0.02em] rounded-[34px] bg-primary-light dark:bg-primary-dark py-3 lg:py-2 px-20 lg:px-5 mb-auto lg:mb-0"
>{{ "donate" | i18n({}, lang ) | safe }} >{{ "donate" | i18n({}, lang ) | safe }}
</a> </a>
+3 -1
View File
@@ -292,10 +292,11 @@ header nav {
max-height: 0; max-height: 0;
transform: translateY(-10px); transform: translateY(-10px);
transition: all .7s ease !important; transition: all .7s ease !important;
overflow: hidden;
} }
.active .sub-menu { .active .sub-menu {
max-height: 600px; max-height: 1000px;
transform: translateY(0px); transform: translateY(0px);
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
@@ -306,6 +307,7 @@ header nav {
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
transform: translateX(100%); transform: translateX(100%);
overflow-y: auto;
} }
header nav.open { header nav.open {